Skip to content

EventStats lambda with Stats endpoint#113

Open
tmikula-dev wants to merge 14 commits intomasterfrom
feature/stats-endpoint
Open

EventStats lambda with Stats endpoint#113
tmikula-dev wants to merge 14 commits intomasterfrom
feature/stats-endpoint

Conversation

@tmikula-dev
Copy link
Collaborator

@tmikula-dev tmikula-dev commented Mar 17, 2026

Overview

This branch introduces the EventStats feature — a dedicated read-only Lambda (event_stats_lambda.py) exposing a POST /stats/{topicName} endpoint that allows authorized callers to query ingested run and job events stored in PostgreSQL. The endpoint is backed by a new ReaderPostgres reader that executes a JOIN query across the public_cps_za_runs and public_cps_za_runs_jobs tables, supporting millisecond-epoch timestamp window filtering and keyset (cursor-based) pagination via internal_id for stable, efficient page traversal. Each returned row is enriched with computed columns — run_date, run_status, formatted_tenant, elapsed_time, start_time, and end_time — mirroring the transformations previously done in downstream reporting tools. The endpoint is protected by the same RS256 JWT authentication and per-topic ACL authorization as the existing write Lambda, and is currently scoped to the public.cps.za.runs topic via the new SUPPORTED_TOPICS constant. The feature is covered by a full unit test suite for HandlerStats, ReaderPostgres, and the lambda routing layer, as well as integration tests using real testcontainers (PostgreSQL, Kafka, LocalStack) that validate the complete write-then-read chain, including authentication, pagination, computed column correctness, and sort ordering.

Release Notes

  • New EventStats lambda with the stats endpoint

Related

Closes #84

Summary by CodeRabbit

  • New Features

    • Added a read-only stats service and POST /stats/{topic_name} endpoint for querying run/job statistics (filtering, sorting, cursor pagination, JWT auth) with health checks.
  • Documentation

    • Updated API spec, README, and developer guide describing the new stats endpoint, env vars, and project structure.
  • Chores

    • Removed several internal agent documentation files and the repository security-scan workflow; added API Gateway/Lambda infra for stats.
  • Tests

    • Added extensive unit and integration tests for the stats API, Postgres reader, and related utilities.

@tmikula-dev tmikula-dev self-assigned this Mar 17, 2026
@tmikula-dev tmikula-dev added the enhancement New feature or request label Mar 17, 2026
@coderabbitai
Copy link

coderabbitai bot commented Mar 17, 2026

Warning

Rate limit exceeded

@tmikula-dev has exceeded the limit for the number of commits that can be reviewed per hour. Please wait 7 minutes and 5 seconds before requesting another review.

⌛ How to resolve this issue?

After the wait time has elapsed, a review can be triggered using the @coderabbitai review command as a PR comment. Alternatively, push new commits to this PR.

We recommend that you space out your commits to avoid hitting the rate limit.

🚦 How do rate limits work?

CodeRabbit enforces hourly rate limits for each developer per organization.

Our paid plans have higher rate limits than the trial, open-source and free plans. In all cases, we re-allow further reviews after a brief timeout.

Please see our FAQ for further information.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: d6ebf4ea-5eda-4ffc-a229-7dd19b3f68ff

📥 Commits

Reviewing files that changed from the base of the PR and between f87b300 and c270034.

📒 Files selected for processing (5)
  • src/handlers/handler_stats.py
  • src/readers/reader_postgres.py
  • src/writers/writer_postgres.py
  • tests/unit/handlers/test_handler_stats.py
  • tests/unit/readers/test_reader_postgres.py

Walkthrough

Adds a new read-only Event Stats Lambda with a PostgreSQL reader (keyset pagination), config loaders, HandlerStats, and tests; extends API spec with POST /stats/{topic_name} and ErrorResponse; refactors routing to a central dispatch, updates type hints across handlers/writers/readers, and deletes multiple .github agent docs and the AquaSec workflow.

Changes

Cohort / File(s) Summary
Agent & Workflow Removal
\.github/agents/*, \.github/workflows/aquasec_repo_scan.yml
Deleted multiple agent specification docs and the AquaSec repository-scan workflow.
API & Documentation
api.yaml, README.md, DEVELOPER.md, \.github/copilot-instructions.md
Added POST /stats/{topic_name}, renamed path param to topic_name, introduced ErrorResponse, documented new stats Lambda and env vars, and added project conventions.
New Stats Lambda & Readers
src/event_stats_lambda.py, src/readers/*, src/utils/config_loader.py
New event_stats Lambda entrypoint, ReaderPostgres read-only reader (keyset pagination, health), and config/access/topic loading utilities.
Routing & Core Utils
src/event_gate_lambda.py, src/utils/utils.py, src/utils/constants.py
Centralized dispatch_request, switched to load_config, added Postgres constants and load_postgres_config, modernized dict typing and logging initialization.
Handlers
src/handlers/handler_stats.py, src/handlers/handler_health.py, src/handlers/handler_topic.py, src/handlers/handler_token.py, src/handlers/handler_api.py
Added HandlerStats; refactored HandlerHealth to use HealthCheckable; added schema validation, ACL checks, consistent error responses, and PEP585 typehint updates.
Writers & Postgres Writer
src/writers/...
Migrated typings to built-in generics, added Kafka retry constants, updated WriterPostgres to lazy-load DB config and use TOPIC_TABLE_MAP with unknown-topic validation.
Constants & Utilities
src/utils/constants.py, src/utils/config_loader.py, src/utils/utils.py
Introduced Postgres defaults, SUPPORTED_TOPICS/TOPIC_TABLE_MAP, config loaders, postgres secret loader, and dispatch/error helpers.
Terraform
terraform_examples/api_gateway.tf, terraform_examples/lambda.tf, terraform_examples/variables.tf
Added API Gateway /stats/{topic_name} resources/integration, new event_stats_lambda resource and permission, and Postgres-related variables.
Tests — Integration & Unit
tests/integration/*, tests/unit/*
Added integration tests and fixtures for /stats, new unit tests for HandlerStats, ReaderPostgres and event_stats lambda, plus many assertion-style and license header updates.

Sequence Diagram(s)

sequenceDiagram
    participant Client
    participant APIGateway
    participant EventStatsLambda
    participant HandlerStats
    participant HandlerToken
    participant ReaderPostgres
    participant PostgreSQL

    Client->>APIGateway: POST /stats/{topic_name} (with JWT)
    APIGateway->>EventStatsLambda: invoke lambda_handler(event)
    EventStatsLambda->>HandlerStats: handle_request(event)
    HandlerStats->>HandlerToken: extract_token(headers)
    HandlerToken-->>HandlerStats: token_string
    HandlerStats->>HandlerToken: decode_jwt(token_string)
    HandlerToken-->>HandlerStats: token_payload

    alt Unauthorized / Forbidden / NotFound
        HandlerStats-->>EventStatsLambda: 401/403/404 response
    else Valid request
        HandlerStats->>ReaderPostgres: read_stats(filters, cursor, limit)
        ReaderPostgres->>PostgreSQL: execute paginated SELECT
        PostgreSQL-->>ReaderPostgres: rows
        ReaderPostgres-->>HandlerStats: (data[], pagination)
        HandlerStats-->>EventStatsLambda: 200 response with data & pagination
    end

    EventStatsLambda-->>APIGateway: lambda response
    APIGateway-->>Client: HTTP response
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~50 minutes

Possibly related PRs

Suggested reviewers

  • petr-pokorny-absa
  • lsulak
  • oto-macenauer-absa
  • Zejnilovic

Poem

🐰 A stats Lambda hops into view,
Readers and cursors, precise and true,
Tokens checked gently, ACLs in line,
Rows paged and formatted, ordered by time.
Hooray — EventGate shows history anew!

🚥 Pre-merge checks | ✅ 3 | ❌ 2

❌ Failed checks (2 warnings)

Check name Status Explanation Resolution
Out of Scope Changes check ⚠️ Warning Most changes align with the stats endpoint feature scope. However, deletion of agent documentation files (.github/agents/*.md) and the AquaSec workflow appear out-of-scope relative to implementing the stats endpoint functionality. Review and justify the deletion of agent documentation files and the security scanning workflow. These may be scope creep or should be split into separate PRs aligned with specific objectives.
Docstring Coverage ⚠️ Warning Docstring coverage is 75.90% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (3 passed)
Check name Status Explanation
Title check ✅ Passed The title 'EventStats lambda with Stats endpoint' clearly describes the main feature being added: a new read-only Lambda service exposing a stats endpoint.
Description check ✅ Passed The PR description is comprehensive, covering the Overview, Release Notes, and Related sections from the template. It explains the feature, implementation details, and linked issue #84.
Linked Issues check ✅ Passed The PR meets the core coding objectives from issue #84: implements POST /stats/{topic_name} with cursor-based pagination, ReaderPostgres for read-only DB access, JWT auth/ACL reuse, QueryBuilder-like validation, computed column enrichment, and comprehensive unit/integration tests.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch feature/stats-endpoint
📝 Coding Plan
  • Generate coding plan for human review comments

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

Comment on lines +183 to +188
resource_id = aws_api_gateway_resource.event_gate_api_stats_topic_name.id
http_method = aws_api_gateway_method.event_gate_api_stats_topic_name_post.http_method
integration_http_method = "POST"
type = "AWS_PROXY"
uri = aws_lambda_function.event_stats_lambda.invoke_arn
}

Check warning

Code scanning / AquaSec

API Gateway stages for V1 and V2 should have access logging enabled Medium

Access logging is not configured.
Comment on lines +183 to +188
resource_id = aws_api_gateway_resource.event_gate_api_stats_topic_name.id
http_method = aws_api_gateway_method.event_gate_api_stats_topic_name_post.http_method
integration_http_method = "POST"
type = "AWS_PROXY"
uri = aws_lambda_function.event_stats_lambda.invoke_arn
}

Check notice

Code scanning / AquaSec

API Gateway must have X-Ray tracing enabled Low

X-Ray tracing is not enabled.
@AbsaOSS AbsaOSS deleted a comment from github-actions bot Mar 17, 2026
Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 10

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
src/handlers/handler_topic.py (1)

166-177: ⚠️ Potential issue | 🔴 Critical

Stop fan-out after the first writer failure.

Once one writer returns False, this loop still calls the remaining writers before returning 500. That creates a non-idempotent partial success: some backends may already have accepted the event, and a retry can duplicate those writes. At minimum, stop dispatching after the first failure.

Minimum containment change
-        errors = []
         for writer_name, writer in self.writers.items():
             ok, err = writer.write(topic_name, topic_message)
             if not ok:
-                errors.append({"type": writer_name, "message": err})
-
-        if errors:
-            return {
-                "statusCode": 500,
-                "headers": {"Content-Type": "application/json"},
-                "body": json.dumps({"success": False, "statusCode": 500, "errors": errors}),
-            }
+                return {
+                    "statusCode": 500,
+                    "headers": {"Content-Type": "application/json"},
+                    "body": json.dumps(
+                        {
+                            "success": False,
+                            "statusCode": 500,
+                            "errors": [{"type": writer_name, "message": err}],
+                        }
+                    ),
+                }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/handlers/handler_topic.py` around lines 166 - 177, The loop over
self.writers currently continues dispatching to all writers even after one
writer.write(topic_name, topic_message) returns ok=False, which can cause
partial, non-idempotent writes; change the logic in the handler that builds
errors (the for loop using self.writers, writer.write and the errors list) to
stop iterating on the first failure: as soon as writer.write returns not ok,
append that single error and break the loop, then return the 500 response based
on that error instead of calling remaining writers. Ensure you preserve the
existing 500 response shape (statusCode, headers, body with errors) but only
include the first failure.
🧹 Nitpick comments (6)
api.yaml (1)

336-353: Consider adopting ErrorResponse schema for existing endpoints.

The new ErrorResponse schema provides good standardization. For consistency, consider updating the existing error responses in POST /topics/{topicName} (lines 145-180) and other endpoints to use $ref: '#/components/schemas/ErrorResponse' instead of inline schemas in a follow-up PR.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@api.yaml` around lines 336 - 353, Replace inline error response schemas for
endpoints (start with the POST /topics/{topicName} operation) to reference the
shared ErrorResponse schema; specifically update the responses sections that
currently define inline objects to use $ref:
'#/components/schemas/ErrorResponse' (and do the same for other endpoints with
similar inline error shapes) so all error responses consistently point to the
ErrorResponse schema defined under components.schemas; ensure response
content/media types (e.g., application/json) remain and only the schema node is
replaced with the $ref.
src/utils/utils.py (1)

85-101: Consider handling Secrets Manager exceptions.

If get_secret_value fails (e.g., secret not found, access denied, network issues), a botocore.exceptions.ClientError will propagate unhandled. Depending on the desired behavior, you may want to catch and wrap this exception or document that callers must handle it.

💡 Optional: Add exception handling
+from botocore.exceptions import ClientError
+
 def load_postgres_config(secret_name: str, secret_region: str) -> dict[str, Any]:
     """Load PostgreSQL connection config from AWS Secrets Manager.
     Args:
         secret_name: Name or ARN of the secret.
         secret_region: AWS region where the secret is stored.
     Returns:
         Parsed connection dict with keys like database, host, user, password, port.
         Returns {"database": ""} when secret_name or secret_region is empty.
+    Raises:
+        RuntimeError: If secret retrieval fails.
     """
     if not secret_name or not secret_region:
         return {"database": ""}
 
-    aws_secrets = boto3.Session().client(service_name="secretsmanager", region_name=secret_region)
-    postgres_secret = aws_secrets.get_secret_value(SecretId=secret_name)["SecretString"]
-    config: dict[str, Any] = json.loads(postgres_secret)
-    logger.debug("Loaded PostgreSQL config from Secrets Manager.")
-    return config
+    try:
+        aws_secrets = boto3.Session().client(service_name="secretsmanager", region_name=secret_region)
+        postgres_secret = aws_secrets.get_secret_value(SecretId=secret_name)["SecretString"]
+        config: dict[str, Any] = json.loads(postgres_secret)
+        logger.debug("Loaded PostgreSQL config from Secrets Manager.")
+        return config
+    except ClientError as exc:
+        logger.exception("Failed to load PostgreSQL config from Secrets Manager.")
+        raise RuntimeError("PostgreSQL config initialization failed") from exc
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/utils/utils.py` around lines 85 - 101, load_postgres_config currently
calls boto3.Session().client(...).get_secret_value(...) without handling AWS
errors; wrap the secret fetch in a try/except that catches
botocore.exceptions.ClientError (import it) around the
aws_secrets.get_secret_value call, log the exception with logger.error including
the exception details and context (e.g., secret_name/secret_region), and then
either re-raise the original error or raise a clear wrapper (e.g., RuntimeError)
so callers get a documented failure mode; ensure the function signature and
return behavior remain the same otherwise.
src/writers/writer_kafka.py (1)

23-23: Minor inconsistency: mixing Optional and union | syntax.

Line 23 imports Optional which is used on line 44 (Optional["Producer"]), but other signatures use the | union syntax (e.g., Producer | None on line 47). Consider using consistent union syntax throughout for Python 3.9+ style:

-        self._producer: Optional["Producer"] = None
+        self._producer: "Producer | None" = None

Then Optional can be removed from imports.

Also applies to: 44-44

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/writers/writer_kafka.py` at line 23, The import and type annotations are
inconsistent: remove Optional from the import list and replace
Optional["Producer"] in the WriterKafka class (or wherever referenced) with the
modern union syntax Producer | None to match other annotations like Producer |
None; ensure typing.Any remains imported and that forward references to
"Producer" are preserved (use quotes only if needed) so all type hints use the
same Python 3.9+ union style.
tests/unit/readers/test_reader_postgres.py (1)

20-20: Prefer mocker.patch(...) over nested unittest.mock.patch(...) blocks.

These unit tests are already on pytest, so switching the repeated patch contexts to mocker.patch(...) will match the repo standard and make the fixtures less noisy.

As per coding guidelines, tests/**/*.py: Use mocker.patch("module.dependency") or mocker.patch.object(Class, "method") for mocking in tests.

Also applies to: 60-76, 167-174, 212-219, 258-265, 362-364

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@tests/unit/readers/test_reader_postgres.py` at line 20, Replace uses of
unittest.mock.patch and nested patch contexts in
tests/unit/readers/test_reader_postgres.py (currently imported as MagicMock and
patch) with pytest-mock's fixture style mocker.patch or mocker.patch.object;
update the import to remove patch and MagicMock if not needed or keep MagicMock
only for instance creation, then for each patched dependency referenced in the
tests (the locations flagged at lines ~60-76, 167-174, 212-219, 258-265,
362-364) switch from context-manager patch(...) blocks to calls like
mocker.patch("module.dependency") or mocker.patch.object(SomeClass, "method")
inside the test functions so mocking follows the repo guideline and reduces
nested patch noise.
tests/unit/utils/test_config_loader.py (1)

70-97: Use mocker.patch for dependency mocking in unit tests.

These tests currently build mocks with MagicMock; please align with the repo’s mocker.patch(...) pattern for consistency and easier fixture composition.

As per coding guidelines: "Use mocker.patch("module.dependency") or mocker.patch.object(Class, "method") for mocking in tests".

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@tests/unit/utils/test_config_loader.py` around lines 70 - 97, Update the two
tests to use the pytest-mock fixture pattern instead of creating MagicMock
instances directly: in test_loads_from_local_file keep calling
load_config(conf_dir) and then assert load_access_config(config, aws_s3)
behavior but replace the local aws_s3 MagicMock with a mocker.patch of the
module dependency used by load_access_config (e.g.,
mocker.patch("module_under_test.aws_s3_client") or the specific symbol
load_access_config imports), and in test_loads_from_s3 patch the S3 client with
mocker.patch to return a fake Bucket/Object/get result whose Body.read returns
the encoded JSON (use mocker.patch to stub the bucket factory and its
Object().get() chain so you can assert mock calls like Bucket(...) and
Object(...)); reference the test functions test_loads_from_local_file and
test_loads_from_s3 and the function under test load_access_config when locating
where to apply mocker.patch.
tests/unit/test_event_stats_lambda.py (1)

30-99: Align fixture mocking with mocker.patch convention.

The module fixture currently manages a manual patch stack via unittest.mock.patch; please switch to mocker.patch(...)/mocker.patch.object(...) for consistency with project tests.

As per coding guidelines: "Use mocker.patch("module.dependency") or mocker.patch.object(Class, "method") for mocking in tests".

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@tests/unit/test_event_stats_lambda.py` around lines 30 - 99, The
event_stats_module fixture manually manages unittest.mock.patch and an
ExitStack; convert it to use the pytest-mock `mocker` fixture instead: accept
`mocker` as a parameter to `event_stats_module`, replace calls to the local
`start_patch` helper with `mocker.patch("requests.get")`,
`mocker.patch("cryptography.hazmat.primitives.serialization.load_der_public_key")`,
`mocker.patch("boto3.Session")`, and use `mocker.patch.dict(sys.modules, {...})`
for the conditional dummy module inserts instead of `patch.dict`; drop manual
start/stop of patches and the ExitStack since mocker auto-tears-down; keep the
same dummy classes/return values and
importlib.import_module("src.event_stats_lambda") behavior and yield the module
as before.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@README.md`:
- Line 113: Update the README entry for POSTGRES_SECRET_NAME so the documented
secret key names match the stats-reader contract: replace references to `dbname`
and `username` with `database` and `user` (and confirm `host`, `port`,
`password` remain), and note that the secret payload must include keys `host`,
`port`, `database`, `user`, and `password` to match the Lambda/stats-reader
expectations (ensure any mention of the Postgres writer/reader uses these exact
keys).

In `@src/event_stats_lambda.py`:
- Line 83: The docstring for the AWS Lambda entry point lambda_handler
incorrectly references "EventGate"; update the docstring in the lambda_handler
function to the correct service name "EventStats" (or another accurate service
identifier), ensuring the first line and any mention inside the triple-quoted
string are changed so the handler's documentation accurately reflects
EventStats.

In `@src/handlers/handler_stats.py`:
- Around line 83-104: The handler currently only validates
timestamp_start/timestamp_end/cursor/limit and calls reader_postgres.read_stats,
but the API must also accept filter and sort, support an opaque cursor (not raw
internal_id), and enforce a hard max of 1000 rows; update the body
parsing/validation to accept body.get("filter") and body.get("sort"), allow
cursor to be an opaque string (e.g., body.get("cursor")) and decode it to the
internal integer before calling read_stats (so you no longer accept or return
raw internal_id), and clamp limit to at most 1000 (e.g., limit = min(int(limit),
1000) while still validating positive int). Finally pass the new filter and sort
arguments into self.reader_postgres.read_stats (timestamp_start, timestamp_end,
cursor=<decoded_internal_id_or_none>, limit=<clamped_limit>, filter=filter,
sort=sort).
- Around line 77-86: The handler currently assumes the parsed JSON is an object
and directly calls body.get(...), which raises AttributeError for non-object
JSON like arrays or strings; after json.loads(...) in the request parsing block
(where variable body is assigned) add a type check to ensure body is a dict
(i.e., a JSON object) and if not return build_error_response(400, "validation",
"Request body must be a JSON object.") before accessing body.get for
timestamp_start, timestamp_end, cursor, limit; update any tests or callers that
rely on this behavior to expect a 400 for non-object JSON.

In `@src/readers/reader_postgres.py`:
- Around line 221-224: The try/except in check_health catches only
BotoCoreError/ClientError but _load_db_config can raise RuntimeError, allowing
the health check to propagate a 500; update the except in check_health to also
handle RuntimeError (either add RuntimeError to the exception tuple or add a
separate "except RuntimeError as err: return False, str(err)") so
_load_db_config failures return the degraded health tuple instead of raising.
- Line 136: Update the list comprehension that builds rows to use zip(...,
strict=True) to satisfy Ruff B905: change the zip call in the expression that
creates rows from col_names and raw_rows (the line assigning rows =
[dict(zip(col_names, row)) for row in raw_rows]) to include strict=True so
mismatched lengths would raise immediately; keep col_names and raw_rows as the
inputs to zip and preserve the dict(...) wrapping.
- Around line 122-130: The DB connection in reader_postgres.py created via
psycopg2.connect currently lacks a statement timeout and read-only enforcement;
update the with psycopg2.connect(...) block to set a server-side statement
timeout and enforce read-only mode by adding connection options (e.g.
options='-c statement_timeout=30000 -c default_transaction_read_only=on') to the
psycopg2.connect call and also call connection.set_session(readonly=True) after
the connection is opened (or execute "SET statement_timeout = ..." if you prefer
configurable ms from db_config). Target the psycopg2.connect invocation and the
connection object (connection.set_session) to implement this change.

In `@src/utils/config_loader.py`:
- Around line 78-90: The current hardcoded filename_to_topic mapping ignores any
additional schema files; replace that logic by scanning the topic_schemas
directory (schemas_dir) for all *.json files and generating topics dynamically
(e.g., for each schema file stem use topic_name = "public.cps.za." + stem)
instead of the filename_to_topic dict. Update the code that builds topics
(currently using filename_to_topic, schemas_dir, topics) to iterate over the
discovered files (os.listdir or glob.glob), ensure each path is a file
(os.path.isfile), derive the topic name from the file stem, and append it to
topics so all schema files produce a corresponding topic.

In `@src/writers/writer_postgres.py`:
- Around line 249-257: The write() method currently only checks for "database"
and psycopg2 presence before proceeding; mirror the same validation logic used
in check_health(): call _load_db_config(), verify host, user, password and port
are present and non-empty (and return the same (False, "<msg>") signature for
misconfiguration), and only then attempt to import/use psycopg2 and open a
connection; also apply the same guard around the later connection block (the
section corresponding to lines ~268-273) so you do not attempt a malformed
connection when secrets are partial or missing.

In `@terraform_examples/lambda.tf`:
- Around line 79-82: The stats Lambda's environment block only sets LOG_LEVEL
but must include the Postgres secret config used by the reader; update the
event_stats_lambda resource to add environment variables POSTGRES_SECRET_NAME
and POSTGRES_SECRET_REGION (populated from the same secret values used elsewhere
in the stack) so the reader can locate the DB credentials at runtime; ensure the
variables are added to the environment { variables = { ... } } map for the
event_stats_lambda so /stats can access the DB.

---

Outside diff comments:
In `@src/handlers/handler_topic.py`:
- Around line 166-177: The loop over self.writers currently continues
dispatching to all writers even after one writer.write(topic_name,
topic_message) returns ok=False, which can cause partial, non-idempotent writes;
change the logic in the handler that builds errors (the for loop using
self.writers, writer.write and the errors list) to stop iterating on the first
failure: as soon as writer.write returns not ok, append that single error and
break the loop, then return the 500 response based on that error instead of
calling remaining writers. Ensure you preserve the existing 500 response shape
(statusCode, headers, body with errors) but only include the first failure.

---

Nitpick comments:
In `@api.yaml`:
- Around line 336-353: Replace inline error response schemas for endpoints
(start with the POST /topics/{topicName} operation) to reference the shared
ErrorResponse schema; specifically update the responses sections that currently
define inline objects to use $ref: '#/components/schemas/ErrorResponse' (and do
the same for other endpoints with similar inline error shapes) so all error
responses consistently point to the ErrorResponse schema defined under
components.schemas; ensure response content/media types (e.g., application/json)
remain and only the schema node is replaced with the $ref.

In `@src/utils/utils.py`:
- Around line 85-101: load_postgres_config currently calls
boto3.Session().client(...).get_secret_value(...) without handling AWS errors;
wrap the secret fetch in a try/except that catches
botocore.exceptions.ClientError (import it) around the
aws_secrets.get_secret_value call, log the exception with logger.error including
the exception details and context (e.g., secret_name/secret_region), and then
either re-raise the original error or raise a clear wrapper (e.g., RuntimeError)
so callers get a documented failure mode; ensure the function signature and
return behavior remain the same otherwise.

In `@src/writers/writer_kafka.py`:
- Line 23: The import and type annotations are inconsistent: remove Optional
from the import list and replace Optional["Producer"] in the WriterKafka class
(or wherever referenced) with the modern union syntax Producer | None to match
other annotations like Producer | None; ensure typing.Any remains imported and
that forward references to "Producer" are preserved (use quotes only if needed)
so all type hints use the same Python 3.9+ union style.

In `@tests/unit/readers/test_reader_postgres.py`:
- Line 20: Replace uses of unittest.mock.patch and nested patch contexts in
tests/unit/readers/test_reader_postgres.py (currently imported as MagicMock and
patch) with pytest-mock's fixture style mocker.patch or mocker.patch.object;
update the import to remove patch and MagicMock if not needed or keep MagicMock
only for instance creation, then for each patched dependency referenced in the
tests (the locations flagged at lines ~60-76, 167-174, 212-219, 258-265,
362-364) switch from context-manager patch(...) blocks to calls like
mocker.patch("module.dependency") or mocker.patch.object(SomeClass, "method")
inside the test functions so mocking follows the repo guideline and reduces
nested patch noise.

In `@tests/unit/test_event_stats_lambda.py`:
- Around line 30-99: The event_stats_module fixture manually manages
unittest.mock.patch and an ExitStack; convert it to use the pytest-mock `mocker`
fixture instead: accept `mocker` as a parameter to `event_stats_module`, replace
calls to the local `start_patch` helper with `mocker.patch("requests.get")`,
`mocker.patch("cryptography.hazmat.primitives.serialization.load_der_public_key")`,
`mocker.patch("boto3.Session")`, and use `mocker.patch.dict(sys.modules, {...})`
for the conditional dummy module inserts instead of `patch.dict`; drop manual
start/stop of patches and the ExitStack since mocker auto-tears-down; keep the
same dummy classes/return values and
importlib.import_module("src.event_stats_lambda") behavior and yield the module
as before.

In `@tests/unit/utils/test_config_loader.py`:
- Around line 70-97: Update the two tests to use the pytest-mock fixture pattern
instead of creating MagicMock instances directly: in test_loads_from_local_file
keep calling load_config(conf_dir) and then assert load_access_config(config,
aws_s3) behavior but replace the local aws_s3 MagicMock with a mocker.patch of
the module dependency used by load_access_config (e.g.,
mocker.patch("module_under_test.aws_s3_client") or the specific symbol
load_access_config imports), and in test_loads_from_s3 patch the S3 client with
mocker.patch to return a fake Bucket/Object/get result whose Body.read returns
the encoded JSON (use mocker.patch to stub the bucket factory and its
Object().get() chain so you can assert mock calls like Bucket(...) and
Object(...)); reference the test functions test_loads_from_local_file and
test_loads_from_s3 and the function under test load_access_config when locating
where to apply mocker.patch.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: 5a21b433-b588-4e5f-9988-d21de26d987a

📥 Commits

Reviewing files that changed from the base of the PR and between 24bae62 and 6d07800.

📒 Files selected for processing (55)
  • .github/agents/devops-engineer.agent.md
  • .github/agents/reviewer.agent.md
  • .github/agents/sdet.agent.md
  • .github/agents/senior-developer.agent.md
  • .github/agents/specification-master.agent.md
  • .github/copilot-instructions.md
  • .github/workflows/aquasec_repo_scan.yml
  • DEVELOPER.md
  • README.md
  • api.yaml
  • src/event_gate_lambda.py
  • src/event_stats_lambda.py
  • src/handlers/handler_api.py
  • src/handlers/handler_health.py
  • src/handlers/handler_stats.py
  • src/handlers/handler_token.py
  • src/handlers/handler_topic.py
  • src/readers/__init__.py
  • src/readers/reader_postgres.py
  • src/utils/config_loader.py
  • src/utils/constants.py
  • src/utils/logging_levels.py
  • src/utils/safe_serialization.py
  • src/utils/trace_logging.py
  • src/utils/utils.py
  • src/writers/writer.py
  • src/writers/writer_eventbridge.py
  • src/writers/writer_kafka.py
  • src/writers/writer_postgres.py
  • terraform_examples/api_gateway.tf
  • terraform_examples/lambda.tf
  • terraform_examples/variables.tf
  • tests/integration/conftest.py
  • tests/integration/schemas/postgres_schema.py
  • tests/integration/test_api_endpoint.py
  • tests/integration/test_health_endpoint.py
  • tests/integration/test_stats_endpoint.py
  • tests/integration/test_token_endpoint.py
  • tests/integration/test_topics_endpoint.py
  • tests/unit/conftest.py
  • tests/unit/handlers/test_handler_api.py
  • tests/unit/handlers/test_handler_health.py
  • tests/unit/handlers/test_handler_stats.py
  • tests/unit/handlers/test_handler_token.py
  • tests/unit/handlers/test_handler_topic.py
  • tests/unit/readers/__init__.py
  • tests/unit/readers/test_reader_postgres.py
  • tests/unit/test_event_gate_lambda.py
  • tests/unit/test_event_gate_lambda_local_access.py
  • tests/unit/test_event_stats_lambda.py
  • tests/unit/utils/test_conf_path.py
  • tests/unit/utils/test_config_loader.py
  • tests/unit/utils/test_safe_serialization.py
  • tests/unit/utils/test_utils.py
  • tests/unit/writers/test_writer_postgres.py
💤 Files with no reviewable changes (11)
  • tests/integration/test_api_endpoint.py
  • .github/agents/sdet.agent.md
  • tests/integration/test_topics_endpoint.py
  • tests/integration/test_health_endpoint.py
  • .github/agents/senior-developer.agent.md
  • .github/agents/devops-engineer.agent.md
  • .github/workflows/aquasec_repo_scan.yml
  • .github/agents/specification-master.agent.md
  • tests/unit/utils/test_safe_serialization.py
  • .github/agents/reviewer.agent.md
  • tests/integration/test_token_endpoint.py

Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 5

🧹 Nitpick comments (3)
src/writers/writer_postgres.py (2)

268-270: End this log message with a period.

The unknown-topic error text currently lacks terminal punctuation.

As per coding guidelines, **/*.py: End all log messages with a period.

✏️ Suggested tweak
-                msg = f"Unknown topic for Postgres/{topic_name}"
+                msg = f"Unknown topic for Postgres/{topic_name}."
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/writers/writer_postgres.py` around lines 268 - 270, The log message
created when a topic is not found (see TOPIC_TABLE_MAP and topic_name) lacks
terminal punctuation; update the construction of msg (used in the logger.error
call) to append a trailing period so the error reads like "Unknown topic for
Postgres/{topic_name}." and ensure logger.error(msg) logs the punctuated string.

53-60: You can remove the return-value type ignore here.

_db_config can be typed as dict[str, Any] | None, which removes the need for # type: ignore[return-value] and keeps typing clearer.

♻️ Suggested typing cleanup
-        self._db_config: dict[str, Any | None] | None = None
+        self._db_config: dict[str, Any] | None = None
@@
-        return self._db_config  # type: ignore[return-value]
+        return self._db_config
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/writers/writer_postgres.py` around lines 53 - 60, The attribute
_db_config is annotated as dict[str, Any | None] | None which forces the method
_load_db_config to use a return-value type ignore; change the field annotation
to dict[str, Any] | None (i.e., self._db_config: dict[str, Any] | None = None)
so _load_db_config can safely return self._db_config without "# type:
ignore[return-value]"; no other code changes needed besides removing the type
ignore from the return in _load_db_config and keeping the call to
load_postgres_config(self._secret_name, self._secret_region) intact.
tests/unit/writers/test_writer_postgres.py (1)

274-274: Use mocker.patch/mocker.patch.object for these test patches.

These mocks work, but the unit-test guideline requires the mocker fixture patching APIs.

♻️ Suggested refactor
-def test_init_with_secret(monkeypatch, reset_env):
+def test_init_with_secret(mocker, reset_env):
@@
-    monkeypatch.setattr(secrets_mod.boto3, "Session", lambda: MockSession())
+    mocker.patch.object(secrets_mod.boto3, "Session", return_value=MockSession())

-def test_check_health_success(reset_env, monkeypatch):
+def test_check_health_success(reset_env, mocker):
@@
-    monkeypatch.setattr(secrets_mod.boto3, "Session", MockSession)
+    mocker.patch.object(secrets_mod.boto3, "Session", MockSession)

-def test_check_health_missing_host(reset_env, monkeypatch):
+def test_check_health_missing_host(reset_env, mocker):
@@
-    monkeypatch.setattr(secrets_mod.boto3, "Session", MockSession)
+    mocker.patch.object(secrets_mod.boto3, "Session", MockSession)

As per coding guidelines, tests/**/*.py: Use mocker.patch("module.dependency") or mocker.patch.object(Class, "method") for mocking in tests.

Also applies to: 342-342, 358-358

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@tests/unit/writers/test_writer_postgres.py` at line 274, Replace the
monkeypatch.setattr usage with the pytest-mock `mocker` fixture: instead of
monkeypatch.setattr(secrets_mod.boto3, "Session", lambda: MockSession()), call
mocker.patch("secrets_mod.boto3.Session", return_value=MockSession()) or
mocker.patch.object(secrets_mod.boto3, "Session", return_value=MockSession()) so
the test uses the recommended mocker.patch API; apply the same change for the
other occurrences that patch boto3.Session (and any similar monkeypatch.setattr
calls) in this test file.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@src/event_stats_lambda.py`:
- Around line 76-77: The route parameter name in code must match api.yaml's
`topicName`; update the ROUTE_MAP entry (replace "/stats/{topic_name}" with
"/stats/{topicName}") and change the path-parameter access inside
handler_stats.handle_request (replace uses of
event["pathParameters"]["topic_name"] with event["pathParameters"]["topicName"],
or implement a fallback that checks both keys) so the handler reads the same
parameter name emitted by API Gateway; ensure any other occurrences/tests
referencing "topic_name" are updated to "topicName" or handle both forms.

In `@src/handlers/handler_stats.py`:
- Around line 108-110: The except RuntimeError block in handler_stats.py
currently returns str(exc) to clients; change it to keep detailed error logging
via logger.exception("Stats query failed for topic %s.", topic_name) but return
a generic error message through build_error_response (e.g., "Failed to retrieve
stats" or "Internal server error") instead of str(exc) so internal DB details
are not exposed; update the except handler that references topic_name,
logger.exception, and build_error_response accordingly.
- Around line 91-98: The validation currently accepts booleans because bool
subclasses int; update the checks for timestamp_start, timestamp_end, cursor,
and limit so they explicitly reject bool (e.g., require type(...) is int or add
an "and not isinstance(..., bool)" clause) before calling build_error_response;
ensure the limit check also rejects boolean values while still enforcing limit
>= 1.

In `@src/readers/reader_postgres.py`:
- Around line 219-228: The health check currently returns healthy when the DB
secret or database name is missing, which can mask a broken /stats; update the
health logic in the health-checking function (the block using self._secret_name,
self._secret_region and calling self._load_db_config) so that missing
secret/region or missing db configuration returns an unhealthy status (False)
with a clear message instead of True/"not configured"; keep the existing
exception handling for _load_db_config (BotoCoreError, ClientError,
RuntimeError) but change the branches that check not self._secret_name / not
self._secret_region and not db_config.get("database") to return False and a
descriptive error string so that the service reports unhealthy when Postgres
configuration is absent.

In `@src/writers/writer_postgres.py`:
- Around line 45-47: Update the class docstring for the Postgres writer to
reflect that DB credentials are loaded lazily on first use rather than at object
initialization—mention that credentials are fetched from AWS Secrets Manager
when _load_db_config() is invoked (e.g., on first call to methods that access
the DB) so maintainers are not misled about eager loading behavior; reference
the class name (Postgres writer) and the _load_db_config() method in the
docstring.

---

Nitpick comments:
In `@src/writers/writer_postgres.py`:
- Around line 268-270: The log message created when a topic is not found (see
TOPIC_TABLE_MAP and topic_name) lacks terminal punctuation; update the
construction of msg (used in the logger.error call) to append a trailing period
so the error reads like "Unknown topic for Postgres/{topic_name}." and ensure
logger.error(msg) logs the punctuated string.
- Around line 53-60: The attribute _db_config is annotated as dict[str, Any |
None] | None which forces the method _load_db_config to use a return-value type
ignore; change the field annotation to dict[str, Any] | None (i.e.,
self._db_config: dict[str, Any] | None = None) so _load_db_config can safely
return self._db_config without "# type: ignore[return-value]"; no other code
changes needed besides removing the type ignore from the return in
_load_db_config and keeping the call to load_postgres_config(self._secret_name,
self._secret_region) intact.

In `@tests/unit/writers/test_writer_postgres.py`:
- Line 274: Replace the monkeypatch.setattr usage with the pytest-mock `mocker`
fixture: instead of monkeypatch.setattr(secrets_mod.boto3, "Session", lambda:
MockSession()), call mocker.patch("secrets_mod.boto3.Session",
return_value=MockSession()) or mocker.patch.object(secrets_mod.boto3, "Session",
return_value=MockSession()) so the test uses the recommended mocker.patch API;
apply the same change for the other occurrences that patch boto3.Session (and
any similar monkeypatch.setattr calls) in this test file.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: 2c9eec98-898d-429d-9c36-7a977aa4f70a

📥 Commits

Reviewing files that changed from the base of the PR and between 6d07800 and f49ee8e.

📒 Files selected for processing (10)
  • README.md
  • src/event_stats_lambda.py
  • src/handlers/handler_stats.py
  • src/readers/reader_postgres.py
  • src/writers/writer_postgres.py
  • terraform_examples/lambda.tf
  • terraform_examples/variables.tf
  • tests/unit/handlers/test_handler_stats.py
  • tests/unit/readers/test_reader_postgres.py
  • tests/unit/writers/test_writer_postgres.py
✅ Files skipped from review due to trivial changes (1)
  • tests/unit/handlers/test_handler_stats.py
🚧 Files skipped from review as they are similar to previous changes (3)
  • README.md
  • terraform_examples/variables.tf
  • tests/unit/readers/test_reader_postgres.py

Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 2

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
src/writers/writer_postgres.py (1)

273-289: ⚠️ Potential issue | 🟠 Major

Prevent silent success for mapped-but-unhandled topics.

If TOPIC_TABLE_MAP gains a new mapped topic without a matching branch, this path currently returns success without writing anything.

💡 Suggested fix
                 with connection.cursor() as cursor:
                     if topic_name == "public.cps.za.dlchange":
                         self._postgres_edla_write(cursor, table_info["main"], message)
                     elif topic_name == "public.cps.za.runs":
                         self._postgres_run_write(cursor, table_info["main"], table_info["jobs"], message)
                     elif topic_name == "public.cps.za.test":
                         self._postgres_test_write(cursor, table_info["main"], message)
+                    else:
+                        msg = f"Mapped topic '{topic_name}' has no Postgres write handler."
+                        logger.error(msg)
+                        return False, msg
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/writers/writer_postgres.py` around lines 273 - 289, The code looks up
table_info from TOPIC_TABLE_MAP but silently does nothing for any mapped
topic_name that isn't one of the three handled branches; update the conditional
in the with connection.cursor() block to handle unknown mapped topics by raising
or logging an explicit error (e.g., raise ValueError or NotImplementedError)
that includes the topic_name and table_info so failures are visible; ensure the
new behavior lives alongside the existing handlers that call
_postgres_edla_write, _postgres_run_write, and _postgres_test_write so any
future additions to TOPIC_TABLE_MAP don’t silently return success.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@src/handlers/handler_stats.py`:
- Line 55: The code accesses event["pathParameters"]["topic_name"].lower()
without validating presence or type, which can throw and return a 500; update
the handler (where topic_name is read) to first verify
event.get("pathParameters") is a dict and that "topic_name" exists and is a
string, then call .lower(); if validation fails return a 400 validation response
(with an appropriate error message) instead of letting an exception propagate
from the topic_name lookup or .lower() call.

In `@src/writers/writer_postgres.py`:
- Around line 250-260: The call to self._load_db_config() can raise
JSON/Value/Type errors and those exceptions must be caught so
write()/check_health() return their expected (bool, message) tuples instead of
propagating exceptions; update the blocks in the methods that call
_load_db_config (the shown block plus the similar checks around lines 291-294
and 307-311) to wrap the call in a try/except catching json.JSONDecodeError,
ValueError, TypeError (or a broad Exception if preferred), log a clear error
message using logger.error including the exception text, and return (False,
"<descriptive error>") so failure is reported consistently. Ensure you reference
and modify the code paths inside the methods write() and check_health() that
perform db_config = self._load_db_config() and the subsequent missing-field
validation.

---

Outside diff comments:
In `@src/writers/writer_postgres.py`:
- Around line 273-289: The code looks up table_info from TOPIC_TABLE_MAP but
silently does nothing for any mapped topic_name that isn't one of the three
handled branches; update the conditional in the with connection.cursor() block
to handle unknown mapped topics by raising or logging an explicit error (e.g.,
raise ValueError or NotImplementedError) that includes the topic_name and
table_info so failures are visible; ensure the new behavior lives alongside the
existing handlers that call _postgres_edla_write, _postgres_run_write, and
_postgres_test_write so any future additions to TOPIC_TABLE_MAP don’t silently
return success.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: 0e21d60c-f220-4e96-a778-65a71c71d37b

📥 Commits

Reviewing files that changed from the base of the PR and between f49ee8e and ae4a5be.

📒 Files selected for processing (4)
  • api.yaml
  • src/handlers/handler_stats.py
  • src/writers/writer_postgres.py
  • tests/unit/handlers/test_handler_stats.py
🚧 Files skipped from review as they are similar to previous changes (2)
  • tests/unit/handlers/test_handler_stats.py
  • api.yaml

Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

🧹 Nitpick comments (1)
tests/unit/readers/test_reader_postgres.py (1)

20-21: Use mocker.patch / mocker.patch.object instead of unittest.mock.patch in unit tests.

This test module uses patch(...) and patch.object(...) from unittest.mock, which diverges from the project's testing convention.

🔧 Example migration pattern
-from unittest.mock import MagicMock, patch
+from unittest.mock import MagicMock
+from pytest_mock import MockerFixture
...
-    def test_loads_config_from_secrets_manager(self, reader: ReaderPostgres, pg_secret: dict[str, Any]) -> None:
+    def test_loads_config_from_secrets_manager(
+        self,
+        reader: ReaderPostgres,
+        pg_secret: dict[str, Any],
+        mocker: MockerFixture,
+    ) -> None:
         mock_client = MagicMock()
         mock_client.get_secret_value.return_value = {"SecretString": json.dumps(pg_secret)}
-
-        with patch("boto3.Session") as mock_session:
-            mock_session.return_value.client.return_value = mock_client
-            result = reader._load_db_config()
+        mock_session = mocker.patch("boto3.Session")
+        mock_session.return_value.client.return_value = mock_client
+        result = reader._load_db_config()

Applies to: lines 20, 60, 72, 362, 371

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@tests/unit/readers/test_reader_postgres.py` around lines 20 - 21, The test
imports and uses unittest.mock.patch/patch.object (import line: "from
unittest.mock import MagicMock, patch") which breaks project convention; replace
usage with the pytest-mock fixture by removing patch from the import and
switching all patch/patch.object calls in tests (e.g., places currently calling
patch(...) or patch.object(...)) to use mocker.patch or mocker.patch.object
instead, updating any test signatures to accept the mocker fixture where needed
and adapt MagicMock uses to remain imported or replaced by mocker.Mock as
appropriate so behavior remains identical.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@src/readers/reader_postgres.py`:
- Around line 101-127: The code in read_stats() calls self._load_db_config() and
only validates db_config.get("database") but then directly indexes
db_config["host"], ["user"], ["password"], ["port"] when calling
psycopg2.connect, which can raise KeyError; update read_stats() to validate all
required DB secret keys before attempting connection (e.g., ensure host, user,
password, port exist and are non-empty), and if any are missing raise the same
controlled RuntimeError (or include them in a single error message) instead of
letting psycopg2.connect surface a KeyError; reference checking of db_config and
the psycopg2.connect(...) call to locate and fix the code.

---

Nitpick comments:
In `@tests/unit/readers/test_reader_postgres.py`:
- Around line 20-21: The test imports and uses unittest.mock.patch/patch.object
(import line: "from unittest.mock import MagicMock, patch") which breaks project
convention; replace usage with the pytest-mock fixture by removing patch from
the import and switching all patch/patch.object calls in tests (e.g., places
currently calling patch(...) or patch.object(...)) to use mocker.patch or
mocker.patch.object instead, updating any test signatures to accept the mocker
fixture where needed and adapt MagicMock uses to remain imported or replaced by
mocker.Mock as appropriate so behavior remains identical.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: 0c3859b5-222d-4515-924e-ff6b13e9ba2b

📥 Commits

Reviewing files that changed from the base of the PR and between ae4a5be and f87b300.

📒 Files selected for processing (2)
  • src/readers/reader_postgres.py
  • tests/unit/readers/test_reader_postgres.py

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

enhancement New feature or request

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Implement stats endpoint

1 participant