An asynchronous FastAPI service, CLI, and standalone Kafka worker for analyzing
machine-generated half-hour traffic counts. Python 3.12.13 is required and all
dependencies are managed and locked with uv.
The analyzer returns total cars, chronological daily totals, the three busiest
half-hours (earliest timestamp wins ties), and the quietest contiguous 90-minute
period containing observations at T, T+30m, and T+60m.
- Project status
- Architecture
- Prerequisites and installation
- API overview
- Database and migrations
- Quality checks
- CLI and Kafka
- Run locally
- Docker
- Releases
- Contributing
- Authors
- License
- Troubleshooting
The API, Kafka integration, local environment, automated tests, and delivery workflows are implemented. Gateway integration and operational deployment remain future work.
| Phase | Status |
|---|---|
| Project initiation | Completed |
| API development — Kafka setup | Completed |
| API development — controllers and routers | Completed |
| API development — environment setup | Completed |
| API development — database and migrations setup | Completed |
| API development — CI/CD setup | Completed |
| API development — API Gateway setup | Planned |
| Testing and validation | Completed |
| Production deployment | Planned |
| Monitoring and maintenance | Planned |
- FastAPI/Uvicorn provides typed upload endpoints and OpenAPI documentation.
- Controllers keep HTTP orchestration separate from parsing and analysis services.
- SQLAlchemy repositories store every successful POST response in PostgreSQL.
- Alembic owns PostgreSQL schema upgrades and downgrades.
- Uploads are size-, filename-, extension-, MIME-, NUL-, and UTF-8-validated.
- Optional API-key authentication is independent from Redis-backed rate limiting.
- Batch work uses bounded async concurrency, preserves order, and isolates failures.
- Kafka producer and consumer services reuse connections. The consumer runs as a standalone worker, never once per Uvicorn worker.
flowchart LR
subgraph clients["Clients and local entry points"]
apiClient["API client"]
analysisCli["Analysis CLI"]
producerCli["Kafka producer CLI"]
end
subgraph api["FastAPI process"]
uvicorn["Uvicorn workers"]
requestContext["Request context middleware"]
trafficRouter["Traffic router"]
security["API key and rate-limit dependencies"]
controller["Traffic controller"]
repository["Analysis result repository"]
trafficService["Traffic analysis service"]
end
subgraph core["Core processing"]
parser["Traffic text parser"]
analyzer["Traffic analyzer"]
end
subgraph workers["Standalone Kafka processes"]
producerService["Kafka producer service"]
consumerWorker["Kafka consumer worker"]
end
subgraph infrastructure["Shared infrastructure"]
redis[("Redis rate-limit backend")]
postgres[("PostgreSQL")]
requestTopic[["Kafka request topic"]]
resultTopic[["Kafka result topic"]]
end
subgraph outputs["Outputs"]
apiResponse["Typed JSON response"]
cliResponse["CLI JSON output"]
downstream["Downstream Kafka consumer"]
end
apiClient -->|"Multipart HTTP"| uvicorn
uvicorn --> requestContext
requestContext --> trafficRouter
trafficRouter --> security
security -.->|"Optional shared limit"| redis
security --> controller
controller --> repository
repository --> postgres
controller --> trafficService
trafficService --> parser
parser --> analyzer
analyzer --> apiResponse
analysisCli --> parser
analyzer --> cliResponse
producerCli --> producerService
producerService -.->|"Publish request"| requestTopic
requestTopic -.->|"Consume records"| consumerWorker
consumerWorker --> analyzer
consumerWorker -.->|"Publish result"| resultTopic
resultTopic -.-> downstream
The synchronous API and CLI paths share the same parser and analyzer. Kafka requests are handled by a separately scalable consumer process so multiple Uvicorn workers do not create duplicate long-running consumers. Redis is used only when shared rate limiting is enabled. Kong is planned and is therefore not shown as an active gateway.
src/
config/ typed environment configuration
controllers/ HTTP use-case orchestration
db/ SQLAlchemy models, repositories, and async sessions
dependencies/ authentication and rate-limit dependencies
middleware/ request IDs and access logging
migrations/ Alembic environment and versioned schema changes
routers/ thin FastAPI routes
schemas/ API and Kafka contracts
scripts/ CLI and process entry points
services/ analysis, parsing, rate limiting, Kafka packages
utils/ errors, secure file handling, and response formatting
tests/ unit, integration, e2e, and test data
docs/postman/ Postman collection and local environment
flowchart LR
subgraph httpInput ["HTTP input"]
httpClient(["API client"])
requestContext["Request context middleware"]
security["API-key authentication and Redis rate limit"]
end
subgraph apiFlow ["FastAPI flow"]
trafficRouter["Traffic router"]
trafficController["Traffic controller"]
routeType{"Single or batch?"}
batchRunner["Bounded concurrent batch processing"]
end
subgraph analysisFlow ["Analysis flow"]
uploadValidation["Validate filename, type, size, and UTF-8"]
parser["Parse timestamp and car-count records"]
analyzer["Calculate totals, busiest periods, and quietest window"]
safeError["Map safe structured error"]
end
subgraph kafkaFlow ["Kafka worker flow"]
producerCli(["Producer CLI"])
producerService["Parse files and publish requests"]
requestTopic[("traffic.analysis.requests")]
consumerService["Standalone consumer worker"]
workerAnalyzer["Analyze request records"]
resultPublisher["Publish result before offset commit"]
resultTopic[("traffic.analysis.results")]
end
subgraph outputFlow ["Outputs"]
apiResponse(["Typed API or batch response"])
kafkaResult(["Kafka result event"])
end
httpClient --> requestContext --> security --> trafficRouter --> trafficController
trafficController --> routeType
routeType -->|"Single"| uploadValidation
routeType -->|"Batch"| batchRunner --> uploadValidation
uploadValidation --> parser --> analyzer --> apiResponse
uploadValidation -.->|"Rejected"| safeError
parser -.->|"Invalid records"| safeError
analyzer -.->|"Analysis failure"| safeError
safeError --> apiResponse
producerCli --> producerService
producerService -.->|"Produces"| requestTopic
requestTopic -.->|"Consumes"| consumerService
consumerService --> workerAnalyzer --> resultPublisher
resultPublisher -.->|"Produces"| resultTopic
resultTopic -.-> kafkaResult
Install uv, Docker Desktop (for containers), and Python 3.12.13:
uv python install 3.12.13
make installmake install runs uv sync --locked. Copy safe local configuration if needed:
cp .env.example .envImportant variables include APP_HOST, APP_PORT, APP_WORKERS, APP_RELOAD,
KEEP_ALIVE_TIMEOUT_SECONDS, GRACEFUL_SHUTDOWN_TIMEOUT_SECONDS,
BATCH_CONCURRENCY, API_KEY, CORS_ORIGINS, upload validation settings,
DATABASE_*, RATE_LIMIT_*, and KAFKA_*. DATABASE_URL must use the
postgresql+asyncpg:// SQLAlchemy scheme. Empty API_KEY disables authentication.
RATE_LIMIT_ENABLED=true requires RATE_LIMIT_REDIS_URL. Only list trusted reverse
proxies in TRUSTED_PROXY_HOSTS; forwarding headers from other peers are ignored.
| Method | Endpoint | Purpose | Authentication |
|---|---|---|---|
GET |
/health/live |
Confirm that the API process is running | None |
GET |
/health/ready |
Check PostgreSQL and configured Redis readiness | None |
POST |
/api/v1/traffic/analyze |
Validate and analyze one traffic file | X-API-Key when configured |
POST |
/api/v1/traffic/analyze/batch |
Analyze multiple files concurrently in input order | X-API-Key when configured |
Traffic files are UTF-8 text files with one timestamp and non-negative car count per line:
2021-12-01T05:00:00 5
2021-12-01T05:30:00 12
2021-12-01T06:00:00 8
Successful analysis responses include the overall total, daily totals, the three
busiest half-hours, and the quietest contiguous 90-minute period. Errors use a
structured detail object containing a stable code and safe message. Full
interactive contracts and schemas are available through Swagger UI, ReDoc, and
OpenAPI after starting the service.
Every successful POST response is committed to PostgreSQL before it is returned and includes:
id: UUID primary identifier for the API operationcreatedAt: UTC creation date formatted asDD-MM-YYYYtimeTaken: processing duration in milliseconds, rounded to two decimal places
The response id is the primary key of traffic_analysis_results. The table
also stores the operation kind, a timezone-aware UTC creation timestamp, timing,
and response-aligned camelCase columns. Nested objects are unnested into scalar
columns, while arrays containing objects use PostgreSQL JSONB. A database write
failure is rolled back and returned as HTTP 503 with the safe
ERR00020 code.
The current persisted columns are id, "analysisKind", "createdAt",
"timeTaken", "totalCars", "dailyTotals", "topHalfHours",
"leastCarsPeriodStart", "leastCarsPeriodEnd",
"leastCarsPeriodTotalCars", "leastCarsPeriodRecords", and items.
PostgreSQL converts unquoted identifiers to lowercase, so every camelCase column
must be enclosed in double quotes in SQL. For example, use "createdAt" rather
than createdAt or created_at.
All dates returned by the POST APIs use DD-MM-YYYY; traffic observations that
require a time use DD-MM-YYYY HH:MM:SS. Uploaded machine-generated traffic files retain
their existing YYYY-MM-DDTHH:MM:SS input format.
PostgreSQL is the persistent store. SQLAlchemy uses an asyncpg connection pool
configured by DATABASE_URL, DATABASE_POOL_SIZE, DATABASE_MAX_OVERFLOW,
DATABASE_POOL_TIMEOUT_SECONDS, DATABASE_POOL_RECYCLE_SECONDS,
DATABASE_CONNECT_TIMEOUT_SECONDS, and DATABASE_COMMAND_TIMEOUT_SECONDS.
Never use a production database for local development or tests.
-
Create the local environment file:
cp .env.example .env
-
Confirm that
.envcontains the local async SQLAlchemy URL:DATABASE_URL=postgresql+asyncpg://application:application-local@localhost:5433/application POSTGRES_PORT=5433
The local Compose defaults use database
application, userapplication, passwordapplication-local, and host port5433. PostgreSQL continues to use port5432inside the Compose network. OverridePOSTGRES_DB,POSTGRES_USER,POSTGRES_PASSWORD, orPOSTGRES_PORTwhen those defaults conflict with another local PostgreSQL instance. KeepDATABASE_URLconsistent with the overridden values. -
Start PostgreSQL and wait for its health check:
docker compose up --detach postgres docker compose ps postgres
-
Install the locked dependencies and apply all migrations:
make install make db-upgrade
-
Confirm that the database is at the latest revision:
make db-current
The current revision should include
20260726_0002 (head). -
Start the API:
APP_WORKERS=1 APP_RELOAD=true make run
In another terminal, submit one of the POST requests from the API examples below. Successful single and batch responses are committed to
traffic_analysis_resultsbefore the API returns them. -
Inspect recently stored results:
docker compose exec -T postgres psql \ --username application \ --dbname application \ --command 'SELECT id, "analysisKind", "createdAt", "timeTaken", "totalCars", items FROM traffic_analysis_results ORDER BY "createdAt" DESC LIMIT 10;'
Copy the command exactly as shown. Do not use
ORDER BY created_at: that pre-migration column no longer exists. PostgreSQL requires the current camelCase column to be written asORDER BY "createdAt".Inspect the live table definition and exact column names:
docker compose exec -T postgres psql \ --username application \ --dbname application \ --command '\d traffic_analysis_results'
The PostgreSQL named volume preserves local data across docker compose down.
Use non-production credentials and databases for development and testing.
make db-upgrade
make db-downgrade
make db-revision MESSAGE="description"
make db-current
make db-history
make migration-checkmake db-downgrade defaults to one revision; set REVISION to choose another
target. Review every generated revision before applying it. The initial migration
creates traffic_analysis_results with upgrade and downgrade logic, a UUID
primary key, constraints for result kind and non-negative timing, and an index on
kind plus creation time. Revision 20260726_0002 migrates stored responses to
camelCase, response-aligned columns and unnests the leastCarsPeriod object.
make migration-check applies pending migrations and fails when SQLAlchemy
metadata contains schema changes without a migration.
Use Alembic for every schema change. Do not change PostgreSQL manually.
-
Add the camelCase mapped attribute and column definition to the appropriate SQLAlchemy model under
src/db/models/. -
Generate a migration with a concise description:
make db-revision MESSAGE="add resource field" -
Review the new file under
src/migrations/versions/. Itsupgrade()function must add the column and itsdowngrade()function must remove the same column. Review the type, nullability, default, indexes, constraints, and any data backfill. Do not accept generated migration code without reviewing it. -
Apply the migration to a disposable local database:
make db-upgrade make db-current make migration-check
-
Update repositories, schemas, API responses, tests, and documentation that use the column, then run
make ci.
For a required column on a table that already contains rows, first add it as nullable or with a safe server default, backfill existing rows, and only then make it non-nullable. Test this process outside production before deployment.
To reverse the latest migration, first confirm the current revision and inspect
the migration's downgrade() function. The default REVISION=-1 moves back one
revision:
make db-current
make db-downgrade
make db-currentTo downgrade to a specific revision:
make db-history
make db-downgrade REVISION=<target-revision>
make db-currentThe column is dropped only when the selected migration's downgrade() function
contains the corresponding op.drop_column(...). Dropping a column permanently
from the current schema requires a new migration: remove the mapped attribute
from the SQLAlchemy model, run make db-revision MESSAGE="drop resource field",
and review that the new upgrade() drops the column while downgrade() restores
it. Then apply it with make db-upgrade.
Downgrading or dropping a column can permanently delete its data. Use a disposable database for validation, back up required data, never test against production, and coordinate application rollback compatibility before executing the change in staging or production.
When starting the complete stack with docker compose up --build, the one-off
migrate service applies Alembic migrations before the API is allowed to start.
make lint
make lint-fix
make format
make format-check
make type-check
make test
make test-database
make coverage
make migration-check
make cimake test, make coverage, and make ci use the pytest expression
-m "not broker". The real-broker integration test is intentionally deselected
from the standard suite because it requires a running Kafka service. A result such
as 94 passed, 1 skipped, 1 deselected therefore means the application suite
passed while external database and Kafka tests were not executed.
Run the PostgreSQL persistence integration test with the dedicated target:
# Correct local replacement for the stale RUN_DATABASE_TESTS command.
make test-databaseThis target starts the local Compose PostgreSQL service, recreates only the
validated application_test database, applies all migrations, runs
test_postgres_persistence.py, and drops the test database afterward. It uses
the local defaults: user application, password application-local, and host
port 5433. Never point it at production.
To run the same workflow manually, create and migrate the disposable database
before setting RUN_DATABASE_TESTS=1:
docker compose up --detach --wait postgres
docker compose exec -T postgres psql \
--username application \
--dbname postgres \
--command 'DROP DATABASE IF EXISTS application_test WITH (FORCE);'
docker compose exec -T postgres psql \
--username application \
--dbname postgres \
--command 'CREATE DATABASE application_test OWNER application;'
DATABASE_URL=postgresql+asyncpg://application:application-local@localhost:5433/application_test \
make db-upgrade
RUN_DATABASE_TESTS=1 \
DATABASE_URL=postgresql+asyncpg://application:application-local@localhost:5433/application_test \
make test
docker compose exec -T postgres psql \
--username application \
--dbname postgres \
--command 'DROP DATABASE IF EXISTS application_test WITH (FORCE);'The cleanup command permanently removes only application_test and its data.
Confirm the _test suffix before running it.
If POSTGRES_USER, POSTGRES_PASSWORD, or POSTGRES_PORT was overridden when
the Compose volume was first created, pass those same values to the target:
make test-database \
POSTGRES_USER=<test-user> \
POSTGRES_PASSWORD=<test-password> \
POSTGRES_PORT=<test-port>TEST_POSTGRES_DB must use only letters, numbers, and underscores and must end
in _test. Changing Compose environment variables does not change credentials
already stored in an existing PostgreSQL volume. Do not run the CI-only
application:application@localhost:5432 URL against the local Compose service.
All GitHub Actions quality jobs execute make ci, which checks and installs
locked dependencies before running the same ordered local quality gate. This
keeps local, pull-request, staging, and production-tag validation aligned.
Run the broker integration test separately:
docker compose up --detach kafka
make kafka-topics
RUN_KAFKA_TESTS=1 make test-brokerCoverage measures src application code, excludes tests and migration version
files, writes
coverage.xml, and enforces 100% statement and branch coverage.
Analyze without the API:
uv run python -m src.scripts.analyze src/tests/data/sample_traffic.txtStart PostgreSQL, run migrations, and start Kafka, Redis, the API, and the standalone consumer:
docker compose up --build
docker compose downCompose runs Alembic in a one-off migrate service before starting the API.
Or run the worker and producer from the locked environment:
make consumer
make producer FILES="src/tests/data/sample_traffic.txt"Requests use traffic.analysis.requests; results use traffic.analysis.results.
The consumer processes partitions concurrently, preserves ordering within each
partition, publishes before manually committing, and therefore provides at-least-once
delivery. Downstream systems should de-duplicate by request ID.
Start supporting services, apply migrations, and run the aligned quality gate:
docker compose up --detach postgres redis kafka
make db-upgrade
make ci
make kafka-topicsDevelopment:
APP_WORKERS=1 APP_RELOAD=true make runThe underlying command is:
uv run python -m src.scripts.serveProduction-style local run:
APP_HOST=0.0.0.0 \
APP_PORT=8000 \
APP_WORKERS=4 \
APP_RELOAD=false \
KEEP_ALIVE_TIMEOUT_SECONDS=5 \
GRACEFUL_SHUTDOWN_TIMEOUT_SECONDS=30 \
uv run python -m src.scripts.serveAPI URLs:
- Base URL:
http://localhost:8000 - Swagger UI:
http://localhost:8000/docs - ReDoc:
http://localhost:8000/redoc - OpenAPI JSON:
http://localhost:8000/openapi.json
Health checks:
curl --request GET \
--url http://localhost:8000/health/live \
--header "Accept: application/json"
curl --request GET \
--url http://localhost:8000/health/ready \
--header "Accept: application/json"Analyze one file:
curl --request POST \
--url http://localhost:8000/api/v1/traffic/analyze \
--header "Accept: application/json" \
--header "X-API-Key: <api-key>" \
--form "file=@src/tests/data/sample_traffic.txt;type=text/plain"Analyze files concurrently:
curl --request POST \
--url http://localhost:8000/api/v1/traffic/analyze/batch \
--header "Accept: application/json" \
--header "X-API-Key: <api-key>" \
--form "files=@src/tests/data/sample_traffic.txt;type=text/plain" \
--form "files=@src/tests/data/sample_traffic.txt;type=text/plain"Successful single-file response:
{
"totalCars": 398,
"dailyTotals": [
{
"date": "01-12-2021",
"carCount": 179
},
{
"date": "05-12-2021",
"carCount": 81
},
{
"date": "08-12-2021",
"carCount": 134
},
{
"date": "09-12-2021",
"carCount": 4
}
],
"topHalfHours": [
{
"timestamp": "01-12-2021 07:30:00",
"carCount": 46
},
{
"timestamp": "01-12-2021 08:00:00",
"carCount": 42
},
{
"timestamp": "08-12-2021 18:00:00",
"carCount": 33
}
],
"leastCarsPeriod": {
"start": "01-12-2021 05:00:00",
"end": "01-12-2021 06:30:00",
"totalCars": 31,
"records": [
{
"timestamp": "01-12-2021 05:00:00",
"carCount": 5
},
{
"timestamp": "01-12-2021 05:30:00",
"carCount": 12
},
{
"timestamp": "01-12-2021 06:00:00",
"carCount": 14
}
]
},
"id": "6c023e9c-f9d6-4348-8299-48667795ade4",
"createdAt": "25-07-2026",
"timeTaken": 4.32
}The batch endpoint returns the same id, createdAt, and timeTaken metadata
with an ordered items array containing an independent result or error for each
uploaded file.
Success is HTTP 200. Common responses are 401 (invalid key), 413 (too large),
415 (unsupported file), 422 (invalid traffic data), and 429 (limit exceeded).
HTTP 429 includes Retry-After. Batch item failures remain inside a 200 response.
All application failures use a stable code with an explicit HTTP status:
| Code | HTTP status | Meaning |
|---|---|---|
ERR00010 |
500 | Kafka producer initialization failure |
ERR00011 |
500 | Kafka consumer initialization failure |
ERR00012 |
500 | Kafka consumer action failure |
ERR00013 |
500 | Kafka publish failure |
ERR00020 |
503 | Database persistence failure |
ERR00021 |
401 | Authentication failure |
ERR00022 |
429 | Rate limit exceeded |
ERR00030 |
400 | Invalid or missing JSON request body |
ERR00031 |
422 | Invalid request body |
ERR00032 |
422 | Invalid traffic record |
ERR00033 |
422 | Invalid timestamp |
ERR00034 |
422 | Invalid car count |
ERR00035 |
422 | Duplicate timestamp |
ERR00036 |
422 | Empty input |
ERR00037 |
422 | Missing contiguous traffic period |
ERR00040 |
400 | Invalid filename |
ERR00041 |
415 | Unsupported file extension |
ERR00042 |
415 | Unsupported media type |
ERR00043 |
413 | File too large |
ERR00044 |
415 | Invalid file content |
ERR00045 |
415 | Invalid file encoding |
ERR00046 |
500 | File read failure |
Check the database after either cURL request. Copy the id from the API response
and replace <response-id>:
docker compose exec -T postgres psql \
--username application \
--dbname application \
--command 'SELECT id, "analysisKind", "createdAt", "timeTaken", "totalCars", "dailyTotals", "topHalfHours", "leastCarsPeriodStart", "leastCarsPeriodEnd", "leastCarsPeriodTotalCars", "leastCarsPeriodRecords", items FROM traffic_analysis_results WHERE id = $$<response-id>$$;'The query should return exactly one row. If the local PostgreSQL database or user
was overridden, replace the --dbname and --username values accordingly.
Check the five most recently stored results:
docker compose exec -T postgres psql \
--username application \
--dbname application \
--command 'SELECT id, "analysisKind", "createdAt", "timeTaken", "totalCars", items FROM traffic_analysis_results ORDER BY "createdAt" DESC LIMIT 5;'Example scalar result columns (JSONB list columns are omitted for readability):
id | analysisKind | createdAt | timeTaken | totalCars
--------------------------------------+--------------+------------------------------+-----------+----------
7d963c7d-71a8-40a7-a433-57aac4676f25 | batch | 2026-07-26 06:15:21.42031+00 | 3.87 | null
9d3f51e8-7397-4a11-b9bb-97f44d0d821e | single | 2026-07-26 06:14:02.91854+00 | 4.32 | 398
(2 rows)
PostgreSQL stores createdAt as timezone-aware UTC. The API formats the
user-facing createdAt value as DD-MM-YYYY.
Start the API, open /docs, select an endpoint, click Try it out, provide the
file and X-API-Key when configured, click Execute, then review the generated
request, response status, headers, and body.
Import collection.json and local-environment.json. The environment uses:
base_url=http://localhost:8000
api_key=<api-key>
Select the local environment, attach src/tests/data/sample_traffic.txt to upload
requests, and leave api_key empty only when authentication is disabled.
make docker-build
make docker-verify
make docker-runmake docker-build pulls the latest base images, disables the layer cache, and
builds aips-car-counter:local for linux/amd64. On Apple Silicon, build and run
an ARM image explicitly:
make docker-build DOCKER_PLATFORM=linux/arm64
make docker-verify DOCKER_PLATFORM=linux/arm64
make docker-run DOCKER_PLATFORM=linux/arm64Override IMAGE_TAG for immutable releases, for example
make docker-build IMAGE_TAG=v1.4.0. make docker-run uses .env, publishes
port 8000, and runs the named aips-car-counter container. It replaces the
host-side localhost database address with host.docker.internal, because
localhost inside the API container refers to that container rather than the
Mac. The target also configures Docker's host-gateway mapping for Linux.
Start PostgreSQL and apply migrations before running the standalone API image:
docker compose up --detach --wait postgres
make db-upgrade
make docker-run DOCKER_PLATFORM=linux/arm64From another terminal, make docker-stop stops the API container. Override
ENV_FILE, APP_PORT, IMAGE_NAME, IMAGE_TAG, CONTAINER_NAME,
DOCKER_DATABASE_HOST, POSTGRES_PORT, or DOCKER_DATABASE_URL when needed.
The multi-stage image installs locked production dependencies, excludes tests and environment files, uses a non-root user, and starts Uvicorn through the typed environment-driven entry point.
For local Kafka, start Compose and explicitly ensure both application topics exist:
docker compose up --detach kafka
make kafka-topicsThe target safely uses Kafka's --if-not-exists option for
traffic.analysis.requests and traffic.analysis.results. Compose also enables
automatic topic creation for local development.
No Kubernetes manifests are maintained in this repository. If this image is
deployed to Kubernetes, use a new immutable IMAGE_TAG for every release. If a
mutable tag is unavoidable, set imagePullPolicy: Always and trigger a rollout
restart so nodes do not retain an older cached image.
GitHub Actions validates pull requests and pushes to develop. Immutable staging
images are published from tags such as staging-v1.4.0 using the protected
staging environment. Production images are published from tags such as v1.4.0
using the protected production environment, where approval should be configured.
The workflows publish only the exact tag and do not publish latest.
This is proprietary software, not an open-source project. Contributions are accepted only from authorized collaborators and remain subject to the proprietary license. Contact the repository owner before starting substantial work; acceptance is discretionary and may require a separate written contributor agreement.
For an authorized contribution:
- Create a focused branch from
develop. - Install the locked development environment with
make install. - Follow the existing architecture, typing, documentation, security, and testing conventions.
- Add or update tests for every behavior change.
- Run
make ciand relevant Docker or broker tests before opening a pull request. - Open the pull request against
developwith a concise summary, validation evidence, operational impact, and any remaining risks.
Do not submit secrets, production data, generated caches, unrelated refactors, or third-party material that you are not authorized to contribute.
- FairozaAmira — creator and maintainer
Copyright © 2026 FairozaAmira. All rights reserved. This project is proprietary software and is not licensed for public use, modification, or distribution. See license.md for the complete terms.
- If
uvcannot write its cache, useUV_CACHE_DIR=/private/tmp/car-counter-uv-cache. - If Docker commands cannot connect, start Docker Desktop before rebuilding.
- If readiness returns 503, check Redis and
RATE_LIMIT_REDIS_URL. - If readiness returns 503 with PostgreSQL configured, check
DATABASE_URL, database health, andmake db-current. - If PostgreSQL reports
InvalidPasswordError, verify thatDATABASE_URLuses the credentials and published host port of the running database. The local Compose defaults areapplication:application-local@localhost:5433; theapplication:application@localhost:5432URL is reserved for CI services. - If Alembic reports unapplied changes, run
make db-upgrade; create a new revision for schema changes rather than editing an applied migration. - If startup rejects settings, ensure reload uses one worker, CORS has no wildcard, PostgreSQL uses the asyncpg URL scheme, and Redis is configured whenever rate limiting is enabled.