-
Notifications
You must be signed in to change notification settings - Fork 0
feat(event-stats): add named-query endpoint for runs/jobs detail #228
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: master
Are you sure you want to change the base?
Changes from all commits
45c3930
b597ecc
f2f930d
78283ff
3437185
b002a6b
4f18f22
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,152 @@ | ||
| # | ||
| # Copyright 2026 ABSA Group Limited | ||
| # | ||
| # Licensed under the Apache License, Version 2.0 (the "License"); | ||
| # you may not use this file except in compliance with the License. | ||
| # You may obtain a copy of the License at | ||
| # | ||
| # http://www.apache.org/licenses/LICENSE-2.0 | ||
| # | ||
| # Unless required by applicable law or agreed to in writing, software | ||
| # distributed under the License is distributed on an "AS IS" BASIS, | ||
| # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. | ||
| # See the License for the specific language governing permissions and | ||
| # limitations under the License. | ||
| # | ||
|
|
||
| """Handler for the /stats/{topic_name}/query/{query_name} endpoint.""" | ||
|
|
||
| import json | ||
| import logging | ||
| from dataclasses import dataclass | ||
| from typing import Any | ||
|
|
||
| from src.readers.named_query_registry import SUPPORTED_QUERIES | ||
| from src.readers.reader_postgres import ReaderPostgres | ||
| from src.utils.constants import POSTGRES_DEFAULT_LIMIT, SUPPORTED_STATS_TOPICS | ||
| from src.utils.utils import build_error_response, build_success_response | ||
|
|
||
| logger = logging.getLogger(__name__) | ||
|
|
||
|
|
||
| @dataclass(frozen=True) | ||
| class NamedQueryParams: | ||
| """Validated query parameters ready to pass to `read_named_query`. | ||
|
|
||
| Attributes: | ||
| timestamp_start: Start of time window in epoch milliseconds, or `None` for the default. | ||
| timestamp_end: End of time window in epoch milliseconds, or `None` for the default. | ||
| cursor: Last `internal_id` from previous page, or `None` for the first page. | ||
| limit: Maximum number of rows per page. | ||
| """ | ||
|
|
||
| timestamp_start: int | None | ||
| timestamp_end: int | None | ||
| cursor: int | None | ||
| limit: int | ||
|
|
||
|
|
||
| class HandlerNamedQuery: | ||
| """Handle predefined named queries for a specific topic.""" | ||
|
|
||
| def __init__( | ||
| self, | ||
| topics: dict[str, dict[str, Any]], | ||
| reader_postgres: ReaderPostgres, | ||
| ) -> None: | ||
| self.topics = topics | ||
| self.reader_postgres = reader_postgres | ||
|
|
||
| def handle_request(self, event: dict[str, Any]) -> dict[str, Any]: | ||
| """Handle POST /stats/{topic_name}/query/{query_name} requests. | ||
| Args: | ||
| event: API Gateway proxy event. | ||
| Returns: | ||
| API Gateway response dict. | ||
| """ | ||
| path_params = event.get("pathParameters") or {} | ||
| topic_name = path_params.get("topic_name", "").lower() | ||
| query_name = path_params.get("query_name", "").lower() | ||
|
|
||
| if error_response := self._validate_event_path_params(topic_name, query_name): | ||
| return error_response | ||
|
|
||
| body_params = self._validate_event_body(event.get("body")) | ||
| if isinstance(body_params, dict): | ||
| return body_params | ||
|
Comment on lines
+71
to
+76
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 📐 Maintainability & Code Quality | 🟡 Minor | ⚡ Quick win 🔎 Supported by static analysis🏁 Script executed: find .. -name 'AGENTS.md' -o -name 'CONTRIBUTING.md' -o -name '*instructions*.md' | head -50
rg -n -i 'non-2xx|non 2xx|exactly one log|rejected request|structured logging|logging' .github README.md DEVELOPER.md pyproject.toml setup.cfg .pylintrc src 2>/dev/null
sed -n '1,165p' src/handlers/handler_named_query.pyRepository: AbsaOSS/EventGate Length of output: 14397 🏁 Script executed: set -o pipefail
printf '%s\n' '--- DEVELOPER logging contract ---'
sed -n '215,245p' DEVELOPER.md
printf '%s\n' '--- README logging contract ---'
sed -n '135,170p' README.md
printf '%s\n' '--- HandlerNamedQuery references ---'
rg -n -C 4 'HandlerNamedQuery|handle_request\(' src | head -240
printf '%s\n' '--- response builders and lambda dispatch ---'
rg -n -C 5 'build_error_response|handler_named_query|handle_request|response' src/event_gate_lambda.py src/handlers src/utils/utils.py | head -320Repository: AbsaOSS/EventGate Length of output: 31996 🏁 Script executed: set -o pipefail
sed -n '215,245p' DEVELOPER.md
sed -n '135,170p' README.md
rg -n -C 4 'HandlerNamedQuery|handle_request\(' src
rg -n -C 5 'build_error_response|handler_named_query|handle_request|response' src/event_gate_lambda.py src/handlers src/utils/utils.pyRepository: AbsaOSS/EventGate Length of output: 41968 Log each validation rejection with its cause. The repository logging contract requires one explanatory log line for every non-2xx response. The validation helpers return 400/404 responses without a warning. Add one warning at each rejection path or centralize one warning before returning the validation response. 🤖 Prompt for AI Agents |
||
|
|
||
| try: | ||
| rows, pagination = self.reader_postgres.read_named_query( | ||
| query_name=query_name, | ||
| timestamp_start=body_params.timestamp_start, | ||
| timestamp_end=body_params.timestamp_end, | ||
| cursor=body_params.cursor, | ||
| limit=body_params.limit, | ||
| ) | ||
| except RuntimeError: | ||
| logger.exception("Named query %s failed for topic %s.", query_name, topic_name) | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 📐 Maintainability & Code Quality | 🟡 Minor | ⚡ Quick win 🔎 Supported by static analysis🏁 Script executed: rg -n -i 'structured logging|constant message|extra=|logger\.(debug|info|warning|error|exception)' .github README.md DEVELOPER.md pyproject.toml setup.cfg .pylintrc src 2>/dev/null
sed -n '75,95p' src/handlers/handler_named_query.py
sed -n '255,280p' src/readers/reader_postgres.pyRepository: AbsaOSS/EventGate Length of output: 16173 Use constant messages for named-query logs. The repository logging guideline requires constant messages with variable values in
🤖 Prompt for AI Agents |
||
| return build_error_response(500, "database", "Named query failed.") | ||
|
|
||
| return build_success_response(rows, pagination) | ||
|
|
||
| def _validate_event_path_params(self, topic_name: str, query_name: str) -> dict[str, Any] | None: | ||
| """Validate the `topic_name`/`query_name` path parameters. | ||
| Args: | ||
| topic_name: The lower-cased `topic_name` path parameter. | ||
| query_name: The lower-cased `query_name` path parameter. | ||
| Returns: | ||
| An `error_response` dict if validation fails, or `None` if valid. | ||
| """ | ||
| if not topic_name: | ||
| return build_error_response(400, "validation", "Missing path parameter 'topic_name'.") | ||
|
|
||
| if topic_name not in self.topics: | ||
| return build_error_response(404, "topic", f"Topic '{topic_name}' not found.") | ||
|
|
||
| if topic_name not in SUPPORTED_STATS_TOPICS: | ||
| return build_error_response(400, "validation", f"Topic '{topic_name}' is not supported.") | ||
|
|
||
| if not query_name: | ||
| return build_error_response(400, "validation", "Missing path parameter 'query_name'.") | ||
|
|
||
| if query_name not in SUPPORTED_QUERIES: | ||
| return build_error_response(400, "validation", f"Query '{query_name}' is not supported. ") | ||
|
|
||
| return None | ||
|
|
||
| @staticmethod | ||
| def _validate_event_body(body: str | None) -> NamedQueryParams | dict[str, Any]: | ||
| """Parse and validate the request body. | ||
| Args: | ||
| body: The raw request body (JSON string) from the API Gateway event, or `None`. | ||
| Returns: | ||
| The parsed_body `NamedQueryParams`, or an `error_response` dict if validation fails. | ||
| """ | ||
| try: | ||
| parsed_body = json.loads(body or "{}") | ||
| except (json.JSONDecodeError, TypeError): | ||
| return build_error_response(400, "validation", "Request body must be valid JSON.") | ||
|
|
||
| if not isinstance(parsed_body, dict): | ||
| return build_error_response(400, "validation", "Request body must be a JSON object.") | ||
|
|
||
| timestamp_start = parsed_body.get("timestamp_start") | ||
| timestamp_end = parsed_body.get("timestamp_end") | ||
| cursor = parsed_body.get("cursor") | ||
| limit: int = parsed_body.get("limit", POSTGRES_DEFAULT_LIMIT) | ||
|
|
||
| int_fields = ( | ||
| (timestamp_start, "timestamp_start"), | ||
| (timestamp_end, "timestamp_end"), | ||
| (cursor, "cursor"), | ||
| ) | ||
| for value, field_name in int_fields: | ||
| if value is not None and (isinstance(value, bool) or not isinstance(value, int)): | ||
| return build_error_response(400, "validation", f"Field '{field_name}' must be an integer.") | ||
|
|
||
| if not isinstance(limit, int) or isinstance(limit, bool) or limit < 1: | ||
| return build_error_response(400, "validation", "Field 'limit' must be a positive integer.") | ||
|
Comment on lines
+147
to
+148
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win 🔎 Supported by static analysis🏁 Script executed: sed -n '331,468p' api.yaml
sed -n '117,155p' src/handlers/handler_named_query.py
sed -n '195,290p' src/readers/reader_postgres.py
rg -n 'POSTGRES_MAX_LIMIT|POSTGRES_DEFAULT_LIMIT|limit.*1000|maximum:' src api.yaml testsRepository: AbsaOSS/EventGate Length of output: 12269 🏁 Script executed: #!/bin/bash
rg -n -C 8 'class HandlerNamedQuery|def handle|read_named_query|_validate_event_body|build_success_response|POST /stats|named.?query' src/handlers/handler_named_query.py template.yaml serverless.yml api.yaml 2>/dev/nullRepository: AbsaOSS/EventGate Length of output: 10144 🏁 Script executed: #!/bin/bash
sed -n '1,125p' src/handlers/handler_named_query.pyRepository: AbsaOSS/EventGate Length of output: 4927 🏁 Script executed: #!/bin/bash
rg -n -A 18 -B 4 '^def build_success_response|build_success_response' src/utils/utils.pyRepository: AbsaOSS/EventGate Length of output: 969 Reject limits above The OpenAPI schema declares a maximum of Proposed fix-from src.utils.constants import POSTGRES_DEFAULT_LIMIT, SUPPORTED_STATS_TOPICS
+from src.utils.constants import POSTGRES_DEFAULT_LIMIT, POSTGRES_MAX_LIMIT, SUPPORTED_STATS_TOPICS
- if not isinstance(limit, int) or isinstance(limit, bool) or limit < 1:
- return build_error_response(400, "validation", "Field 'limit' must be a positive integer.")
+ if (
+ not isinstance(limit, int)
+ or isinstance(limit, bool)
+ or not 1 <= limit <= POSTGRES_MAX_LIMIT
+ ):
+ return build_error_response(
+ 400,
+ "validation",
+ f"Field 'limit' must be between 1 and {POSTGRES_MAX_LIMIT}.",
+ )🤖 Prompt for AI Agents |
||
|
|
||
| return NamedQueryParams( | ||
| timestamp_start=timestamp_start, timestamp_end=timestamp_end, cursor=cursor, limit=limit | ||
| ) | ||
Uh oh!
There was an error while loading. Please reload this page.