Skip to content

Add auth + CSV upload/process API - #5

Merged
Vivek-M-08 merged 3 commits into
ELEVATE-Project:release-1.0.0from
Vivek-M-08:release-1.0.0
Jul 22, 2026
Merged

Add auth + CSV upload/process API #5
Vivek-M-08 merged 3 commits into
ELEVATE-Project:release-1.0.0from
Vivek-M-08:release-1.0.0

Conversation

@Vivek-M-08

@Vivek-M-08 Vivek-M-08 commented Jul 22, 2026

Copy link
Copy Markdown
Collaborator

Summary by CodeRabbit

  • New Features

    • Added authenticated CSV uploads for story and discussion reports.
    • Added CSV validation, duplicate detection, status tracking, and processing controls.
    • Added automated real-time and batch processing with progress and error statuses.
    • Added consistent API responses and clearer validation error messages.
    • Added containerized deployment support with production configuration and startup tooling.
  • Bug Fixes

    • Improved handling of list-based submission fields and fallback story-rating content.
    • Strengthened ingestion schema validation for incoming data.

…split/single-container app modes

Dockerfile + requirements-prod.txt build the app image (CPU-only torch,
trimmed unused deps, embedding model baked in). docker-compose.yaml adds
a KRaft Kafka broker and analytics-web/consumer/worker as three
containers by default (COMPOSE_PROFILES=split), with run_all.sh +
analytics-all as a single-container alternative (COMPOSE_PROFILES=single).
Postgres stays host-local via host.docker.internal, same as temporal
already did.
…ohan's codebase]

Adopts the sibling project's layered app/api/ structure (routes/services/
validators/models) for a real Bearer-token-protected CSV upload pipeline
(POST /v1/upload/, POST /v1/process/csv/{id}), replacing the old
unauthenticated /api/submissions/trigger and mock /api/bulk/upload.

Extracts the Kafka ingestion-schema validator into
app/services/ingestion_validation.py so the CSV pipeline checks each row
against STORY_KAFKA_SCHEMA/DISCUSSION_KAFKA_SCHEMA before publishing,
not just the consumer after receiving. Stops auto-generating a session ID
when missing from a CSV row so it's caught by that check instead of
silently faked, and rejects bad-column CSVs before any GCS/DB write.

Fixes made during the port: confluent_kafka.Producer instead of a second
Kafka client library; blocking GCS/pandas calls wrapped in
asyncio.to_thread inside Temporal activities; RecordNotPending -> 409
(was inconsistently 400).
@coderabbitai

coderabbitai Bot commented Jul 22, 2026

Copy link
Copy Markdown

Review Change Stack

Walkthrough

Adds an authenticated CSV upload API with validation, GCS storage, database tracking, Temporal orchestration, Kafka publication, batch scheduling, and container runtime configuration. Existing submission normalization and ingestion validation paths are also updated for array-valued fields and shared schema validation.

Changes

CSV ingestion pipeline

Layer / File(s) Summary
Configuration and upload tracking
app/config.py, schema.sql, app/database/operations.py, app/services/gcp_storage.py, .env.example
Adds CSV settings, upload records and status transitions, array-valued story fields, and GCS upload/download helpers.
Authenticated upload API
app/api/*, main.py
Adds bearer authentication, CSV validation, standardized exceptions and responses, /v1/upload/, and /v1/process/csv/{record_id} routes.
CSV validation and Kafka workflows
app/api/services/uploads.py, app/api/validators/uploads.py, app/services/ingestion_validation.py, app/temporal/csv_processing_activity.py, app/temporal/workflows.py, app/kafka/consumer.py, app/temporal/{story_rating_activity.py,thematic_activity.py,pii_and_abusive_activity.py}
Transforms CSV rows into story or discussion events, validates ingestion schemas, publishes valid rows to Kafka, tracks failures, and updates related list-field handling.
Temporal worker and container runtime
app/temporal/worker.py, Dockerfile, docker-compose.yaml, requirements-prod.txt, run_all.sh, .dockerignore
Registers CSV workflows and schedules, adds container build and startup files, updates Compose health checks, and defines production dependencies.

Estimated code review effort: 4 (Complex) | ~60 minutes

Sequence Diagram(s)

sequenceDiagram
  participant Client
  participant UploadRoute
  participant UploadService
  participant GCS
  participant Database
  participant Temporal
  participant Kafka
  Client->>UploadRoute: Upload CSV with bearer token
  UploadRoute->>UploadService: Validate and handle upload
  UploadService->>GCS: Store CSV bytes
  UploadService->>Database: Create pending upload record
  UploadService->>Temporal: Start CSV processing workflow
  Temporal->>GCS: Fetch stored CSV
  Temporal->>Database: Update processing status
  Temporal->>Kafka: Publish valid row events
Loading

Possibly related PRs

🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Docstring Coverage ✅ Passed Docstring coverage is 82.14% which is sufficient. The required threshold is 70.00%.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
Title check ✅ Passed The title matches the main change set: adding authentication and new CSV upload/processing API endpoints.
✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests

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.

@Vivek-M-08 Vivek-M-08 changed the title Release 1.0.0 Add auth + CSV upload/process API Jul 22, 2026
@Vivek-M-08
Vivek-M-08 merged commit 0f60c2b into ELEVATE-Project:release-1.0.0 Jul 22, 2026
1 check was pending

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Actionable comments posted: 9

🧹 Nitpick comments (4)
docker-compose.yaml (1)

38-39: 🩺 Stability & Availability | 🔵 Trivial | ⚡ Quick win

Wait for Temporal readiness, not only container creation.

The list-form dependency starts temporal-ui once the Temporal container starts, even while its new health check is failing. Use condition: service_healthy.

Proposed fix
     depends_on:
-      - temporal
+      temporal:
+        condition: service_healthy

As per path instructions, Docker Compose configuration must include effective health checks.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@docker-compose.yaml` around lines 38 - 39, Update the temporal-ui depends_on
entry to use the mapping form with condition: service_healthy for the temporal
service, ensuring startup waits for Temporal’s health check rather than
container creation alone.

Source: Path instructions

run_all.sh (1)

8-16: 🩺 Stability & Availability | 🔵 Trivial | ⚡ Quick win

Forward shutdown signals and reap sibling processes.

PID 1 exits on SIGTERM without forwarding it to the web, consumer, and worker processes. Add a TERM/INT trap that terminates and waits for all child PIDs; this lets the Kafka consumer execute its cleanup path before container shutdown.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@run_all.sh` around lines 8 - 16, Update run_all.sh around the web, consumer,
and worker launches to capture all child PIDs and install TERM/INT traps that
forward the signal to each child, wait for them to exit, then terminate the
supervisor cleanly. Preserve the existing wait -n behavior for propagating a
child failure while ensuring sibling processes are reaped during signal-driven
shutdown.
app/services/gcp_storage.py (1)

67-96: 🚀 Performance & Scalability | 🔵 Trivial | 💤 Low value

Consider caching the GCS client / credentials.

Both upload_csv and fetch_csv rebuild get_gcp_credentials() and instantiate a fresh storage.Client on every call. A module-level lazily-initialized client would avoid the repeated credential parsing and client setup. Minor, since GCS round-trip latency dominates.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@app/services/gcp_storage.py` around lines 67 - 96, Cache the GCS client and
credentials used by upload_csv and fetch_csv instead of recreating them on every
call. Add module-level lazy initialization and update both functions to reuse
the cached storage.Client while preserving the existing bucket, upload, and
download behavior.
app/api/exceptions.py (1)

87-89: 🎯 Functional Correctness | 🔵 Trivial | 💤 Low value

DuplicateFile maps more naturally to 409 Conflict than 400.

A duplicate upload is a conflict with existing resource state (same class as RecordAlreadyProcessing/RecordNotPending, which you return as 409), not a malformed request. Consider returning 409 for consistency.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@app/api/exceptions.py` around lines 87 - 89, Update duplicate_file_handler so
DuplicateFile responses use HTTP 409 Conflict instead of 400, while preserving
the existing JSON detail content and handler behavior.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In @.env.example:
- Around line 31-33: Change the AUTH_TOKEN example to a clearly defined sentinel
value, then update the configuration loading and validation in app/config.py to
reject that sentinel and fail fast when it remains configured. Preserve
acceptance of any non-sentinel, explicitly supplied bearer token.

In `@app/api/routes/uploads.py`:
- Around line 21-27: Introduce a Pydantic request model for the upload metadata
used by upload_report, applying required-value validation, appropriate maximum
lengths, and allowed choices where applicable for program_name, leader_category,
and tenant_code. Update upload_report to receive and use this model instead of
unconstrained form strings, while preserving the existing report_type, file, and
authentication dependencies.

In `@app/api/services/uploads.py`:
- Around line 251-255: Update row_to_json’s PDF payload construction so pdf_urls
includes the required data.pdfUrls.masked field alongside original, using the
ingest-path value expected by both Kafka schemas. Ensure
csv_push_to_kafka_activity no longer rejects valid CSV rows when masked is
intentionally produced later, while preserving the existing original URL
handling.

In `@app/temporal/csv_processing_activity.py`:
- Around line 34-63: The module-level producer used by _push_rows_sync must not
be shared across concurrent activity runs. Create or obtain a producer dedicated
to this batch/activity within _push_rows_sync, and ensure produce, delivery
callbacks, polling, and flush(10) operate only on that producer so unrelated
uploads cannot affect delivery tracking or timeout results.
- Around line 104-105: Update csv_fetch_and_validate_activity so claiming the
upload occurs before batch fan-out: move the in_progress status update into the
pending-fetch step, or gate this activity through try_claim_for_processing and
return without processing when the row is no longer pending. Remove the
unconditional update_status call immediately before returning success, while
preserving normal processing for successfully claimed pending rows.

In `@app/temporal/workflows.py`:
- Around line 388-408: Bound the child-workflow fan-out in the pending-upload
processing flow: mirror BatchProcessingWorkflow’s batch_size-limited fetch and
continue_as_new loop, and/or enforce a concurrency cap before asyncio.gather.
Update the section using fetch_pending_csv_uploads_activity,
CsvProcessingWorkflow.run, and child_tasks so thousands of pending IDs are not
fetched or started simultaneously, while preserving per-upload processing and
result handling.

In `@docker-compose.yaml`:
- Around line 41-157: Uncomment and activate the Kafka, analytics-web,
analytics-consumer, analytics-worker, and analytics-all service definitions in
docker-compose.yaml so the split and single COMPOSE_PROFILES can start their
corresponding containers. Preserve the existing profile assignments,
dependencies, environment settings, ports, volumes, and commands.

In `@Dockerfile`:
- Around line 29-35: Create an unprivileged application user in the Dockerfile,
grant it ownership of logs, downloads, and the model cache directories, then
switch to that user with USER before the ENTRYPOINT so the application runs
without root privileges.

In `@requirements-prod.txt`:
- Around line 1-16: Pin the production dependency graph by adding a generated
lock or constraints file for all direct and transitive dependencies, preferably
including package hashes. Update the Dockerfile installation step to install
using this pinned file instead of resolving requirements-prod.txt directly,
while keeping requirements-prod.txt as the source of declared production
dependencies.

---

Nitpick comments:
In `@app/api/exceptions.py`:
- Around line 87-89: Update duplicate_file_handler so DuplicateFile responses
use HTTP 409 Conflict instead of 400, while preserving the existing JSON detail
content and handler behavior.

In `@app/services/gcp_storage.py`:
- Around line 67-96: Cache the GCS client and credentials used by upload_csv and
fetch_csv instead of recreating them on every call. Add module-level lazy
initialization and update both functions to reuse the cached storage.Client
while preserving the existing bucket, upload, and download behavior.

In `@docker-compose.yaml`:
- Around line 38-39: Update the temporal-ui depends_on entry to use the mapping
form with condition: service_healthy for the temporal service, ensuring startup
waits for Temporal’s health check rather than container creation alone.

In `@run_all.sh`:
- Around line 8-16: Update run_all.sh around the web, consumer, and worker
launches to capture all child PIDs and install TERM/INT traps that forward the
signal to each child, wait for them to exit, then terminate the supervisor
cleanly. Preserve the existing wait -n behavior for propagating a child failure
while ensuring sibling processes are reaped during signal-driven shutdown.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

Run ID: 7932e4a5-fa82-4507-8e94-74a41c849ccf

📥 Commits

Reviewing files that changed from the base of the PR and between 4718eb0 and ad723b6.

⛔ Files ignored due to path filters (1)
  • tests/TEST_CASES.csv is excluded by !**/*.csv, !**/*.csv
📒 Files selected for processing (33)
  • .dockerignore
  • .env.example
  • Dockerfile
  • app/api/bulk.py
  • app/api/deps.py
  • app/api/exceptions.py
  • app/api/models/__init__.py
  • app/api/models/uploads.py
  • app/api/response.py
  • app/api/router.py
  • app/api/routes.py
  • app/api/routes/__init__.py
  • app/api/routes/uploads.py
  • app/api/services/__init__.py
  • app/api/services/uploads.py
  • app/api/validators/__init__.py
  • app/api/validators/uploads.py
  • app/config.py
  • app/database/operations.py
  • app/kafka/consumer.py
  • app/services/gcp_storage.py
  • app/services/ingestion_validation.py
  • app/temporal/csv_processing_activity.py
  • app/temporal/pii_and_abusive_activity.py
  • app/temporal/story_rating_activity.py
  • app/temporal/thematic_activity.py
  • app/temporal/worker.py
  • app/temporal/workflows.py
  • docker-compose.yaml
  • main.py
  • requirements-prod.txt
  • run_all.sh
  • schema.sql
💤 Files with no reviewable changes (2)
  • app/api/bulk.py
  • app/api/routes.py

Comment thread .env.example
Comment on lines +31 to +33
# API Authentication — single shared Bearer token checked via secrets.compare_digest
# in app/api/deps.py. Required (no default) — the app will not start without it.
AUTH_TOKEN=your-secret-bearer-token-here

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🔒 Security & Privacy | 🟠 Major | ⚡ Quick win

Reject the example bearer token at configuration load.

If .env.example is copied unchanged, your-secret-bearer-token-here becomes the live shared credential for both protected endpoints. Use a sentinel value and make app/config.py fail fast when that sentinel is configured, rather than accepting a known token.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In @.env.example around lines 31 - 33, Change the AUTH_TOKEN example to a
clearly defined sentinel value, then update the configuration loading and
validation in app/config.py to reject that sentinel and fail fast when it
remains configured. Preserve acceptance of any non-sentinel, explicitly supplied
bearer token.

Comment thread app/api/routes/uploads.py
Comment on lines +21 to +27
async def upload_report(
report_type: str = Form(...),
program_name: str = Form(...),
leader_category: str = Form(...),
tenant_code: str = Form(default="mitra"),
file: UploadFile = File(...),
_token: HTTPAuthorizationCredentials = Depends(verify_auth_token),

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🗄️ Data Integrity & Integration | 🟡 Minor | ⚡ Quick win

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
set -euo pipefail

# Locate the route and related upload code.
git ls-files 'app/api/**' | sed -n '1,200p'
printf '\n--- uploads route outline ---\n'
ast-grep outline app/api/routes/uploads.py --view expanded || true
printf '\n--- surrounding lines ---\n'
cat -n app/api/routes/uploads.py | sed -n '1,220p'

Repository: ELEVATE-Project/analytics-service

Length of output: 3157


🏁 Script executed:

#!/bin/bash
set -euo pipefail

# Inspect the upload route and associated schemas/services for validation and response contracts.
for f in \
  app/api/routes/uploads.py \
  app/schemas/*.py \
  app/api/**/uploads*.py \
  app/services/*.py
do
  [ -e "$f" ] && printf '\n### %s ###\n' "$f" && cat -n "$f" | sed -n '1,240p'
done

Repository: ELEVATE-Project/analytics-service

Length of output: 38846


Add a request model for upload metadata. program_name, leader_category, and tenant_code are still plain form strings, so empty or arbitrarily long values can reach persistence. Use Pydantic constraints for required values, length limits, and any allowed choices.

🧰 Tools
🪛 Ruff (0.15.21)

[warning] 26-26: Do not perform function call File in argument defaults; instead, perform the call within the function, or read the default from a module-level singleton variable

(B008)


[warning] 27-27: Do not perform function call Depends in argument defaults; instead, perform the call within the function, or read the default from a module-level singleton variable

(B008)

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@app/api/routes/uploads.py` around lines 21 - 27, Introduce a Pydantic request
model for the upload metadata used by upload_report, applying required-value
validation, appropriate maximum lengths, and allowed choices where applicable
for program_name, leader_category, and tenant_code. Update upload_report to
receive and use this model instead of unconstrained form strings, while
preserving the existing report_type, file, and authentication dependencies.

Source: Path instructions

Comment on lines +251 to +255
pdf_col = "Pdf" if normalized_type == "story" else "PDF Urls"
original_pdf = get_url_field(get_csv_value(row_dict, expected_cols, pdf_col))
pdf_urls = None
if original_pdf:
pdf_urls = {"original": original_pdf}

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🗄️ Data Integrity & Integration | 🟠 Major | ⚡ Quick win

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# Confirm both schemas require data.pdfUrls.masked at create-time
rg -n 'data\.pdfUrls\.masked' app/config.py
# Confirm row_to_json never emits a "masked" pdf key
rg -n '"masked"|pdf_urls\s*=' app/api/services/uploads.py
# Confirm pre-publish validation skips failing rows
rg -n 'validate_ingestion_schema|schema_errors\.append|payloads\.append' app/temporal/csv_processing_activity.py

Repository: ELEVATE-Project/analytics-service

Length of output: 2286


CSV create payloads miss data.pdfUrls.masked
row_to_json only emits {"original": ...}, but both STORY_KAFKA_SCHEMA and DISCUSSION_KAFKA_SCHEMA require data.pdfUrls.masked at create time. csv_push_to_kafka_activity rejects failing rows before publish, so CSV uploads never reach Kafka. If masked is produced later, it should not be required in this ingest path.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@app/api/services/uploads.py` around lines 251 - 255, Update row_to_json’s PDF
payload construction so pdf_urls includes the required data.pdfUrls.masked field
alongside original, using the ingest-path value expected by both Kafka schemas.
Ensure csv_push_to_kafka_activity no longer rejects valid CSV rows when masked
is intentionally produced later, while preserving the existing original URL
handling.

Comment on lines +34 to +63
def _push_rows_sync(payloads: List[Any]) -> None:
"""
Runs in a worker thread (via asyncio.to_thread) — produce()/flush() are
blocking calls. Flushes once for the whole batch rather than per row (a
per-row flush forces a network round trip per row, far too slow for large
CSVs), mirroring app/kafka/consumer.py's DLQ producer pattern.
"""
producer = _get_producer()
delivery_error = {}

def _on_delivery(err, _msg):
if err is not None:
delivery_error["error"] = err

for payload, key in payloads:
producer.produce(
settings.KAFKA_TOPIC_INGESTION,
value=payload.encode("utf-8"),
key=key.encode("utf-8") if key else None,
callback=_on_delivery,
)
producer.poll(0)
if "error" in delivery_error:
raise KafkaException(delivery_error["error"])

remaining = producer.flush(10)
if remaining > 0:
raise TimeoutError(f"Timed out waiting for Kafka delivery ({remaining} still in-flight)")
if "error" in delivery_error:
raise KafkaException(delivery_error["error"])

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🩺 Stability & Availability | 🟠 Major | 🏗️ Heavy lift

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# Confirm the producer is a shared singleton and how many concurrent push paths exist.
rg -nP '_producer|_get_producer|_push_rows_sync' app/temporal/csv_processing_activity.py
rg -nP 'execute_child_workflow|asyncio\.gather|csv_push_to_kafka_activity' app/temporal/workflows.py

Repository: ELEVATE-Project/analytics-service

Length of output: 867


🏁 Script executed:

#!/bin/bash
set -euo pipefail

printf '\n== app/temporal/csv_processing_activity.py (selected lines) ==\n'
sed -n '1,140p' app/temporal/csv_processing_activity.py
printf '\n== app/temporal/workflows.py (selected lines) ==\n'
sed -n '240,430p' app/temporal/workflows.py

Repository: ELEVATE-Project/analytics-service

Length of output: 12632


🏁 Script executed:

#!/bin/bash
set -euo pipefail

printf '\n== app/temporal/csv_processing_activity.py (push activity slice) ==\n'
sed -n '140,260p' app/temporal/csv_processing_activity.py

Repository: ELEVATE-Project/analytics-service

Length of output: 5292


🏁 Script executed:

#!/bin/bash
set -euo pipefail

printf '\n== app/temporal/csv_processing_activity.py (remaining push activity) ==\n'
sed -n '260,380p' app/temporal/csv_processing_activity.py

Repository: ELEVATE-Project/analytics-service

Length of output: 1515


Avoid sharing this producer across activity runs. app/temporal/csv_processing_activity.py:20-63 producer.flush(10) drains the entire queue on the module-level singleton, so concurrent uploads on the same worker can block on each other’s in-flight messages and raise a spurious TimeoutError, pushing an otherwise successful record to on_hold. Use a per-activity producer or otherwise isolate delivery tracking to this batch.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@app/temporal/csv_processing_activity.py` around lines 34 - 63, The
module-level producer used by _push_rows_sync must not be shared across
concurrent activity runs. Create or obtain a producer dedicated to this
batch/activity within _push_rows_sync, and ensure produce, delivery callbacks,
polling, and flush(10) operate only on that producer so unrelated uploads cannot
affect delivery tracking or timeout results.

Comment on lines +104 to +105
await csv_upload_repo.update_status(record_id, "in_progress")
return True

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🗄️ Data Integrity & Integration | 🟠 Major | 🏗️ Heavy lift

🧩 Analysis chain

🏁 Script executed:

set -euo pipefail
printf 'Files:\n'
git ls-files 'app/temporal/**' | sed -n '1,200p'
printf '\nRelevant search:\n'
rg -n "try_claim_for_processing|update_status\\(|pending|CsvBatchProcessingWorkflow|handle_push|Kafka|publish" app/temporal -S
printf '\nFile outline:\n'
ast-grep outline app/temporal/csv_processing_activity.py --view expanded

Repository: ELEVATE-Project/analytics-service

Length of output: 10488


🏁 Script executed:

set -euo pipefail
sed -n '1,220p' app/temporal/csv_processing_activity.py | cat -n
printf '\n----\n'
sed -n '1,260p' app/temporal/csv_processing_workflow.py | cat -n
printf '\n----\n'
rg -n "try_claim_for_processing|handle_push|update_status\\(|in_progress|pending" app -S

Repository: ELEVATE-Project/analytics-service

Length of output: 10488


🏁 Script executed:

set -euo pipefail

printf '--- workflows.py relevant sections ---\n'
sed -n '360,430p' app/temporal/workflows.py | cat -n

printf '\n--- search for claim/update helpers ---\n'
rg -n "try_claim_for_processing|update_status\\(|list_by_status\\(|claim.*processing|in_progress|handle_push" app/database app -S

printf '\n--- database operations file outline ---\n'
fd -a 'operations.py' app/database

Repository: ELEVATE-Project/analytics-service

Length of output: 262


🏁 Script executed:

set -euo pipefail

printf '--- workflows.py slice ---\n'
sed -n '375,420p' app/temporal/workflows.py | cat -n

printf '\n--- operations.py outline and claim/update functions ---\n'
ast-grep outline app/database/operations.py --view expanded
printf '\n----\n'
rg -n "def try_claim_for_processing|def update_status|list_by_status|pending|in_progress" app/database/operations.py -S

Repository: ELEVATE-Project/analytics-service

Length of output: 262


🏁 Script executed:

set -euo pipefail

printf -- '--- workflows.py slice ---\n'
sed -n '375,430p' app/temporal/workflows.py | cat -n

printf -- '\n--- operations.py outline ---\n'
ast-grep outline app/database/operations.py --view expanded

printf -- '\n--- operations.py search ---\n'
rg -n "def try_claim_for_processing|def update_status|def list_by_status|pending|in_progress" app/database/operations.py -S

Repository: ELEVATE-Project/analytics-service

Length of output: 4025


🏁 Script executed:

set -euo pipefail

printf -- '--- CsvProcessingWorkflow slice ---\n'
sed -n '300,375p' app/temporal/workflows.py | cat -n

printf -- '\n--- operations.py claim/update slice ---\n'
sed -n '704,792p' app/database/operations.py | cat -n

Repository: ELEVATE-Project/analytics-service

Length of output: 6899


🏁 Script executed:

set -euo pipefail
printf -- '--- usages of CsvProcessingWorkflow / try_claim_for_processing / fetch_pending_csv_uploads_activity ---\n'
rg -n "CsvProcessingWorkflow|try_claim_for_processing|fetch_pending_csv_uploads_activity|csv_fetch_and_validate_activity|csv_push_to_kafka_activity" app -S

printf -- '\n--- find any direct single-record CSV processing entrypoints ---\n'
rg -n "csv_uploads.*pending|status = 'pending'|csv_upload|handle_push|push.*csv|process.*csv" app -S

Repository: ELEVATE-Project/analytics-service

Length of output: 6563


Claim CSV uploads before batch fan-out.

csv_fetch_and_validate_activity still does an unconditional update_status(..., "in_progress"), so the batch workflow can start on a row that handle_push has already claimed and process it twice. Move the claim into the pending-fetch step or gate this activity on try_claim_for_processing, and skip rows that are no longer pending.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@app/temporal/csv_processing_activity.py` around lines 104 - 105, Update
csv_fetch_and_validate_activity so claiming the upload occurs before batch
fan-out: move the in_progress status update into the pending-fetch step, or gate
this activity through try_claim_for_processing and return without processing
when the row is no longer pending. Remove the unconditional update_status call
immediately before returning success, while preserving normal processing for
successfully claimed pending rows.

Source: Learnings

Comment thread app/temporal/workflows.py
Comment on lines +388 to +408
pending_ids: List[int] = await workflow.execute_activity(
fetch_pending_csv_uploads_activity,
start_to_close_timeout=timedelta(minutes=2),
retry_policy=retry_policy
)

if not pending_ids:
return {"processed_count": 0, "message": "No pending CSV uploads found."}

# Fan-out child workflows to process each CSV in parallel
child_tasks = []
for pid in pending_ids:
child_tasks.append(
workflow.execute_child_workflow(
CsvProcessingWorkflow.run,
pid,
id=f"csv-batch-child-{pid}"
)
)

results = await asyncio.gather(*child_tasks, return_exceptions=True)

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🚀 Performance & Scalability | 🟠 Major | 🏗️ Heavy lift

Unbounded fan-out diverges from BatchProcessingWorkflow's deliberate bounding.

fetch_pending_csv_uploads_activity returns all pending IDs, and every one is launched as a child workflow in a single asyncio.gather. The sibling BatchProcessingWorkflow in this same file explicitly fetches in batch_size chunks and uses continue_as_new precisely to avoid OOM on the worker and overwhelming the Temporal cluster with concurrent starts. At a few thousand pending uploads this workflow hits exactly the failure mode that pattern was written to prevent.

Recommend bounding the pending fetch (limit + loop, mirroring BatchProcessingWorkflow) and/or capping concurrent children.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@app/temporal/workflows.py` around lines 388 - 408, Bound the child-workflow
fan-out in the pending-upload processing flow: mirror BatchProcessingWorkflow’s
batch_size-limited fetch and continue_as_new loop, and/or enforce a concurrency
cap before asyncio.gather. Update the section using
fetch_pending_csv_uploads_activity, CsvProcessingWorkflow.run, and child_tasks
so thousands of pending IDs are not fetched or started simultaneously, while
preserving per-upload processing and result handling.

Comment thread docker-compose.yaml
Comment on lines +41 to +157
# kafka:
# image: apache/kafka:3.9.0
# container_name: kafka
# ports:
# - "29092:29092" # host-facing listener only — do NOT also publish 9092
# environment:
# KAFKA_NODE_ID: 1
# KAFKA_PROCESS_ROLES: broker,controller
# KAFKA_LISTENERS: PLAINTEXT://:9092,CONTROLLER://:9093,PLAINTEXT_HOST://:29092
# KAFKA_ADVERTISED_LISTENERS: PLAINTEXT://kafka:9092,PLAINTEXT_HOST://localhost:29092
# KAFKA_LISTENER_SECURITY_PROTOCOL_MAP: CONTROLLER:PLAINTEXT,PLAINTEXT:PLAINTEXT,PLAINTEXT_HOST:PLAINTEXT
# KAFKA_CONTROLLER_LISTENER_NAMES: CONTROLLER
# KAFKA_INTER_BROKER_LISTENER_NAME: PLAINTEXT
# KAFKA_CONTROLLER_QUORUM_VOTERS: 1@kafka:9093
# KAFKA_OFFSETS_TOPIC_REPLICATION_FACTOR: 1
# KAFKA_GROUP_INITIAL_REBALANCE_DELAY_MS: 0
# CLUSTER_ID: "MkU3OEVBNTcwNTJENDM2Qk"
# volumes:
# - kafka_data:/var/lib/kafka/data
# healthcheck:
# test: ["CMD-SHELL", "/opt/kafka/bin/kafka-broker-api-versions.sh --bootstrap-server kafka:9092 || exit 1"]
# interval: 10s
# timeout: 5s
# retries: 10
# start_period: 20s

# # --- Split mode: one container per service (default — fault isolation,
# # independent scaling/restarts). Active when COMPOSE_PROFILES=split (see .env).
# analytics-web:
# profiles: ["split"]
# build: .
# image: elevate-analytics:latest
# env_file: .env
# environment:
# DATABASE_URL: postgresql://postgres:[email protected]:5432/analytics_db
# TEMPORAL_HOST: temporal:7233
# KAFKA_BOOTSTRAP_SERVERS: kafka:9092
# extra_hosts:
# - "host.docker.internal:host-gateway"
# depends_on:
# temporal:
# condition: service_healthy
# kafka:
# condition: service_healthy
# ports:
# - "8000:8000"
# volumes:
# - ./logs:/app/logs
# command: ["--mode", "web"]

# analytics-consumer:
# profiles: ["split"]
# build: .
# image: elevate-analytics:latest
# env_file: .env
# environment:
# DATABASE_URL: postgresql://postgres:[email protected]:5432/analytics_db
# TEMPORAL_HOST: temporal:7233
# KAFKA_BOOTSTRAP_SERVERS: kafka:9092
# extra_hosts:
# - "host.docker.internal:host-gateway"
# depends_on:
# temporal:
# condition: service_healthy
# kafka:
# condition: service_healthy
# volumes:
# - ./logs:/app/logs
# command: ["--mode", "consumer"]

# analytics-worker:
# profiles: ["split"]
# build: .
# image: elevate-analytics:latest
# env_file: .env
# environment:
# DATABASE_URL: postgresql://postgres:[email protected]:5432/analytics_db
# TEMPORAL_HOST: temporal:7233
# KAFKA_BOOTSTRAP_SERVERS: kafka:9092
# extra_hosts:
# - "host.docker.internal:host-gateway"
# depends_on:
# temporal:
# condition: service_healthy
# kafka:
# condition: service_healthy
# volumes:
# - ./logs:/app/logs
# command: ["--mode", "worker"]

# --- Single mode: one container, all three services as separate processes
# (run_all.sh). Active when COMPOSE_PROFILES=single (see .env). Do not run
# this alongside the split services above — both bind host port 8000.
# analytics-all:
# profiles: ["single"]
# build: .
# image: elevate-analytics:latest
# env_file: .env
# environment:
# DATABASE_URL: postgresql://postgres:[email protected]:5432/analytics_db
# TEMPORAL_HOST: temporal:7233
# KAFKA_BOOTSTRAP_SERVERS: kafka:9092
# extra_hosts:
# - "host.docker.internal:host-gateway"
# depends_on:
# temporal:
# condition: service_healthy
# kafka:
# condition: service_healthy
# ports:
# - "8000:8000"
# volumes:
# - ./logs:/app/logs
# entrypoint: ["/app/run_all.sh"]

volumes:
kafka_data:

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🎯 Functional Correctness | 🟠 Major | ⚡ Quick win

🧩 Analysis chain

🏁 Script executed:

docker compose --profile split config --services
docker compose --profile single config --services

Repository: ELEVATE-Project/analytics-service

Length of output: 255


🏁 Script executed:

#!/bin/bash
set -euo pipefail

printf '\n== docker-compose.yaml (outline) ==\n'
wc -l docker-compose.yaml
sed -n '1,240p' docker-compose.yaml | cat -n

printf '\n== profile references ==\n'
rg -n "COMPOSE_PROFILES|profiles:|analytics-web|analytics-consumer|analytics-worker|analytics-all|kafka:" -S .

Repository: ELEVATE-Project/analytics-service

Length of output: 8557


🏁 Script executed:

#!/bin/bash
set -euo pipefail

# Show the compose file with line numbers around the referenced region and the file start.
sed -n '1,220p' docker-compose.yaml | nl -ba | sed -n '1,220p'

Repository: ELEVATE-Project/analytics-service

Length of output: 211


Expose the profiled services in Compose. COMPOSE_PROFILES=split/single won’t add Kafka or any analytics container while these blocks stay commented out, so the stack only starts temporal and temporal-ui. Make these service definitions active here or move them to docs.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@docker-compose.yaml` around lines 41 - 157, Uncomment and activate the Kafka,
analytics-web, analytics-consumer, analytics-worker, and analytics-all service
definitions in docker-compose.yaml so the split and single COMPOSE_PROFILES can
start their corresponding containers. Preserve the existing profile assignments,
dependencies, environment settings, ports, volumes, and commands.

Source: Path instructions

Comment thread Dockerfile
Comment on lines +29 to +35
COPY app/ app/
COPY main.py schema.sql seed_prompts.sql seed_themes.sql run_all.sh ./
RUN chmod +x run_all.sh

RUN mkdir -p logs downloads

ENTRYPOINT ["python", "main.py"]

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🔒 Security & Privacy | 🟠 Major | ⚡ Quick win

Run the application as a non-root user.

The entrypoint runs as root because the image never switches users. Create an unprivileged user, grant it ownership of writable directories (logs, downloads, and the model cache), then add USER.

Proposed fix
 RUN mkdir -p logs downloads
+RUN useradd --system --create-home --uid 10001 appuser \
+    && chown -R appuser:appuser /app /opt/model-cache
+
+USER appuser
 
 ENTRYPOINT ["python", "main.py"]
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@Dockerfile` around lines 29 - 35, Create an unprivileged application user in
the Dockerfile, grant it ownership of logs, downloads, and the model cache
directories, then switch to that user with USER before the ENTRYPOINT so the
application runs without root privileges.

Source: Linters/SAST tools

Comment thread requirements-prod.txt
Comment on lines +1 to +16
temporalio>=1.18.0
confluent-kafka>=2.4.0
asyncpg>=0.29.0
pydantic-settings>=2.0.0
deface>=1.5.0
python-dotenv>=1.0.1
fastapi>=0.112.0
python-multipart>=0.0.9
uvicorn>=0.30.0
sentence-transformers==3.0.1
scikit-learn
torch
numpy>=1.24.0
pandas>=2.0.0
google-cloud-storage
pypdf>=4.0.0

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🔒 Security & Privacy | 🟠 Major | 🏗️ Heavy lift

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
set -euo pipefail

printf '\n== files ==\n'
git ls-files | rg '(^|/)(requirements-prod\.txt|requirements.*\.txt|Dockerfile|dockerfile|constraints.*\.txt|poetry\.lock|uv\.lock|pip-tools|Pipfile\.lock|requirements.*lock.*)$'

printf '\n== requirements-prod.txt ==\n'
cat -n requirements-prod.txt

printf '\n== Dockerfile references ==\n'
rg -n --hidden --no-ignore-vcs 'requirements-prod\.txt|pip install|constraints|requirements' Dockerfile . -g '!**/.git/**' || true

Repository: ELEVATE-Project/analytics-service

Length of output: 50390


🏁 Script executed:

#!/bin/bash
set -euo pipefail

printf 'requirements-prod.txt:\n'
cat -n requirements-prod.txt

printf '\nTracked Dockerfiles:\n'
git ls-files | rg '(^|/)(Dockerfile|dockerfile)$'

printf '\nRequirements usage:\n'
rg -n --hidden --no-ignore-vcs 'requirements-prod\.txt|pip install|constraints|requirements' -- Dockerfile . -g '!**/.git/**' -g '!**/node_modules/**' -g '!**/dist/**' -g '!**/build/**' | head -n 200

Repository: ELEVATE-Project/analytics-service

Length of output: 50390


🏁 Script executed:

#!/bin/bash
set -euo pipefail

printf '== tracked packaging files ==\n'
git ls-files -- 'requirements-prod.txt' 'Dockerfile' 'dockerfile' 'requirements*.txt' 'constraints*.txt' '*.lock'

printf '\n== requirements-prod.txt ==\n'
cat -n requirements-prod.txt

for f in Dockerfile dockerfile; do
  if [ -f "$f" ]; then
    printf '\n== %s ==\n' "$f"
    cat -n "$f"
  fi
done

Repository: ELEVATE-Project/analytics-service

Length of output: 2296


Lock the production dependency graph
requirements-prod.txt uses mostly open-ended lower bounds, and Dockerfile installs it directly. Add a pinned lock/constraints file, ideally with hashes, and install from that so rebuilds don’t drift to different or vulnerable transitive versions.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@requirements-prod.txt` around lines 1 - 16, Pin the production dependency
graph by adding a generated lock or constraints file for all direct and
transitive dependencies, preferably including package hashes. Update the
Dockerfile installation step to install using this pinned file instead of
resolving requirements-prod.txt directly, while keeping requirements-prod.txt as
the source of declared production dependencies.

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

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant