diff --git a/.agents/skills/docker-flask-postgres-api/SKILL.md b/.agents/skills/docker-flask-postgres-api/SKILL.md new file mode 100644 index 0000000..eb0b181 --- /dev/null +++ b/.agents/skills/docker-flask-postgres-api/SKILL.md @@ -0,0 +1,121 @@ +```markdown +# docker-flask-postgres-api Development Patterns + +> Auto-generated skill from repository analysis + +## Overview + +This skill teaches the core development patterns, coding conventions, and workflows used in the `docker-flask-postgres-api` repository. The repository is a Python-based API project, using Docker for deployment and PostgreSQL as the database backend. It emphasizes clear commit messages, modular code organization, and robust testing and documentation practices. + +## Coding Conventions + +- **File Naming:** + Use camelCase for file names. + _Example:_ + ``` + userRoutes.py + dbConnection.py + ``` + +- **Import Style:** + Use relative imports within the package. + _Example:_ + ```python + from .schemas import UserSchema + from .utils import validate_input + ``` + +- **Export Style:** + Use named exports (explicitly define what is exported). + _Example:_ + ```python + def create_user(...): + ... + + def delete_user(...): + ... + ``` + +- **Commit Messages:** + Follow [Conventional Commits](https://www.conventionalcommits.org/) with prefixes: `fix`, `docs`, `feat`, `test`. + _Example:_ + ``` + feat: add user registration endpoint with validation + fix: correct db connection string parsing + docs: update README with setup instructions + test: add tests for user login route + ``` + +## Workflows + +### Feature Development: API Endpoint +**Trigger:** When adding a new API endpoint with validation, error handling, and tests +**Command:** `/new-endpoint` + +1. **Implement endpoint logic** in `app/__init__.py` + _Example:_ + ```python + @app.route('/users', methods=['POST']) + def create_user(): + # Endpoint logic here + ``` +2. **Add or update validation** in `app/schemas.py` + _Example:_ + ```python + class UserSchema(Schema): + username = fields.Str(required=True) + email = fields.Email(required=True) + ``` +3. **Write or update tests** in `tests/test_routes.py` + _Example:_ + ```python + def test_create_user(client): + response = client.post('/users', json={...}) + assert response.status_code == 201 + ``` +4. **Update documentation** in `README.md` + - Document the new endpoint, request/response examples, and any new environment variables. + +--- + +### Documentation Update +**Trigger:** When documenting new features, configuration, or operational procedures +**Command:** `/update-docs` + +1. **Update environment variable documentation** in `.env.example` + - Add or update variables relevant to the new feature. +2. **Update main documentation** in `README.md` + - Describe new features, configuration steps, or architectural changes. + +--- + +## Testing Patterns + +- **Test File Naming:** + Test files follow the pattern `*.test.*` (e.g., `userRoutes.test.py`). + +- **Test Location:** + Tests are placed in the `tests/` directory, commonly in files like `tests/test_routes.py`. + +- **Test Structure:** + Each test function typically uses a client fixture to make HTTP requests to endpoints and asserts on the response. + + _Example:_ + ```python + def test_get_users(client): + response = client.get('/users') + assert response.status_code == 200 + assert isinstance(response.json, list) + ``` + +- **Testing Framework:** + The specific framework is not detected, but typical Python test frameworks (like `pytest`) are likely used. + +## Commands + +| Command | Purpose | +|----------------|-----------------------------------------------------------------| +| /new-endpoint | Start the workflow to add a new API endpoint with tests & docs | +| /update-docs | Start the workflow to update documentation and environment vars | + +``` \ No newline at end of file diff --git a/.agents/skills/docker-flask-postgres-api/agents/openai.yaml b/.agents/skills/docker-flask-postgres-api/agents/openai.yaml new file mode 100644 index 0000000..a80ab09 --- /dev/null +++ b/.agents/skills/docker-flask-postgres-api/agents/openai.yaml @@ -0,0 +1,6 @@ +interface: + display_name: "Docker Flask Postgres Api" + short_description: "Repo-specific patterns and workflows for docker-flask-postgres-api" + default_prompt: "Use the docker-flask-postgres-api repo skill to follow existing architecture, testing, and workflow conventions." +policy: + allow_implicit_invocation: true \ No newline at end of file diff --git a/.claude/commands/documentation-update.md b/.claude/commands/documentation-update.md new file mode 100644 index 0000000..5c40043 --- /dev/null +++ b/.claude/commands/documentation-update.md @@ -0,0 +1,35 @@ +--- +name: documentation-update +description: Workflow command scaffold for documentation-update in docker-flask-postgres-api. +allowed_tools: ["Bash", "Read", "Write", "Grep", "Glob"] +--- + +# /documentation-update + +Use this workflow when working on **documentation-update** in `docker-flask-postgres-api`. + +## Goal + +Updates documentation files to reflect new features, environment variables, or architectural changes. + +## Common Files + +- `.env.example` +- `README.md` + +## Suggested Sequence + +1. Understand the current state and failure mode before editing. +2. Make the smallest coherent change that satisfies the workflow goal. +3. Run the most relevant verification for touched files. +4. Summarize what changed and what still needs review. + +## Typical Commit Signals + +- Update environment variable documentation in .env.example +- Update main documentation in README.md + +## Notes + +- Treat this as a scaffold, not a hard-coded script. +- Update the command if the workflow evolves materially. \ No newline at end of file diff --git a/.claude/commands/feature-development-api-endpoint.md b/.claude/commands/feature-development-api-endpoint.md new file mode 100644 index 0000000..f3b50c1 --- /dev/null +++ b/.claude/commands/feature-development-api-endpoint.md @@ -0,0 +1,39 @@ +--- +name: feature-development-api-endpoint +description: Workflow command scaffold for feature-development-api-endpoint in docker-flask-postgres-api. +allowed_tools: ["Bash", "Read", "Write", "Grep", "Glob"] +--- + +# /feature-development-api-endpoint + +Use this workflow when working on **feature-development-api-endpoint** in `docker-flask-postgres-api`. + +## Goal + +Implements a new API endpoint, including code, tests, and documentation. + +## Common Files + +- `app/__init__.py` +- `app/schemas.py` +- `tests/test_routes.py` +- `README.md` + +## Suggested Sequence + +1. Understand the current state and failure mode before editing. +2. Make the smallest coherent change that satisfies the workflow goal. +3. Run the most relevant verification for touched files. +4. Summarize what changed and what still needs review. + +## Typical Commit Signals + +- Implement endpoint logic in app/__init__.py +- Add or update validation in app/schemas.py +- Write or update tests in tests/test_routes.py +- Update documentation in README.md + +## Notes + +- Treat this as a scaffold, not a hard-coded script. +- Update the command if the workflow evolves materially. \ No newline at end of file diff --git a/.claude/ecc-tools.json b/.claude/ecc-tools.json new file mode 100644 index 0000000..52ad751 --- /dev/null +++ b/.claude/ecc-tools.json @@ -0,0 +1,304 @@ +{ + "version": "1.3", + "schemaVersion": "1.0", + "generatedBy": "ecc-tools", + "generatedAt": "2026-05-17T17:29:24.655Z", + "repo": "https://github.com/donny-devops/docker-flask-postgres-api", + "referenceSetReadiness": { + "score": 0, + "present": 0, + "total": 7, + "items": [ + { + "id": "deep-analyzer-corpus", + "label": "Deep analyzer corpus", + "status": "missing", + "evidence": [], + "recommendation": "Add analyzer fixture, golden, benchmark, or reference-set files that can catch analyzer regressions." + }, + { + "id": "rag-evaluator", + "label": "RAG/evaluator comparison", + "status": "missing", + "evidence": [], + "recommendation": "Add retrieval or evaluator reference-set comparison fixtures with expected ranking behavior." + }, + { + "id": "pr-salvage", + "label": "PR salvage/review corpus", + "status": "missing", + "evidence": [], + "recommendation": "Add stale-PR, review-thread, reopen-flow, or salvage reference cases for queue cleanup automation." + }, + { + "id": "discussion-triage", + "label": "Discussion triage corpus", + "status": "missing", + "evidence": [], + "recommendation": "Add public discussion triage fixtures, golden cases, or reference sets for informational, answered, and no-response classifications." + }, + { + "id": "harness-compatibility", + "label": "Harness compatibility", + "status": "missing", + "evidence": [], + "recommendation": "Add cross-harness, adapter-compliance, or harness-audit evidence for Claude, Codex, OpenCode, Zed, dmux, and agent surfaces." + }, + { + "id": "security-evidence", + "label": "Security evidence", + "status": "missing", + "evidence": [], + "recommendation": "Attach security evidence such as SBOMs, SARIF, audit reports, or AgentShield evidence packs." + }, + { + "id": "ci-failure-mode", + "label": "CI failure-mode evidence", + "status": "missing", + "evidence": [], + "recommendation": "Add captured CI failure logs, dry-run fixtures, or troubleshooting docs for common workflow failure modes." + } + ] + }, + "profiles": { + "requested": "full", + "recommended": "full", + "effective": "developer", + "requestedAlias": "full", + "recommendedAlias": "full", + "effectiveAlias": "developer" + }, + "requestedProfile": "full", + "profile": "developer", + "recommendedProfile": "full", + "effectiveProfile": "developer", + "tier": "free", + "requestedComponents": [ + "repo-baseline", + "workflow-automation", + "security-audits", + "research-tooling", + "team-rollout", + "governance-controls" + ], + "selectedComponents": [ + "repo-baseline", + "workflow-automation" + ], + "requestedAddComponents": [], + "requestedRemoveComponents": [], + "blockedRemovalComponents": [], + "tierFilteredComponents": [ + "security-audits", + "research-tooling", + "team-rollout", + "governance-controls" + ], + "requestedRootPackages": [ + "runtime-core", + "workflow-pack", + "agentshield-pack", + "research-pack", + "team-config-sync", + "enterprise-controls" + ], + "selectedRootPackages": [ + "runtime-core", + "workflow-pack" + ], + "requestedPackages": [ + "runtime-core", + "workflow-pack", + "agentshield-pack", + "research-pack", + "team-config-sync", + "enterprise-controls" + ], + "requestedAddPackages": [], + "requestedRemovePackages": [], + "selectedPackages": [ + "runtime-core", + "workflow-pack" + ], + "packages": [ + "runtime-core", + "workflow-pack" + ], + "blockedRemovalPackages": [], + "tierFilteredRootPackages": [ + "agentshield-pack", + "research-pack", + "team-config-sync", + "enterprise-controls" + ], + "tierFilteredPackages": [ + "agentshield-pack", + "research-pack", + "team-config-sync", + "enterprise-controls" + ], + "conflictingPackages": [], + "dependencyGraph": { + "runtime-core": [], + "workflow-pack": [ + "runtime-core" + ] + }, + "resolutionOrder": [ + "runtime-core", + "workflow-pack" + ], + "requestedModules": [ + "runtime-core", + "workflow-pack", + "agentshield-pack", + "research-pack", + "team-config-sync", + "enterprise-controls" + ], + "selectedModules": [ + "runtime-core", + "workflow-pack" + ], + "modules": [ + "runtime-core", + "workflow-pack" + ], + "managedFiles": [ + ".claude/skills/docker-flask-postgres-api/SKILL.md", + ".agents/skills/docker-flask-postgres-api/SKILL.md", + ".agents/skills/docker-flask-postgres-api/agents/openai.yaml", + ".claude/identity.json", + ".codex/config.toml", + ".codex/AGENTS.md", + ".codex/agents/explorer.toml", + ".codex/agents/reviewer.toml", + ".codex/agents/docs-researcher.toml", + ".claude/homunculus/instincts/inherited/docker-flask-postgres-api-instincts.yaml", + ".claude/commands/feature-development-api-endpoint.md", + ".claude/commands/documentation-update.md" + ], + "packageFiles": { + "runtime-core": [ + ".claude/skills/docker-flask-postgres-api/SKILL.md", + ".agents/skills/docker-flask-postgres-api/SKILL.md", + ".agents/skills/docker-flask-postgres-api/agents/openai.yaml", + ".claude/identity.json", + ".codex/config.toml", + ".codex/AGENTS.md", + ".codex/agents/explorer.toml", + ".codex/agents/reviewer.toml", + ".codex/agents/docs-researcher.toml", + ".claude/homunculus/instincts/inherited/docker-flask-postgres-api-instincts.yaml" + ], + "workflow-pack": [ + ".claude/commands/feature-development-api-endpoint.md", + ".claude/commands/documentation-update.md" + ] + }, + "moduleFiles": { + "runtime-core": [ + ".claude/skills/docker-flask-postgres-api/SKILL.md", + ".agents/skills/docker-flask-postgres-api/SKILL.md", + ".agents/skills/docker-flask-postgres-api/agents/openai.yaml", + ".claude/identity.json", + ".codex/config.toml", + ".codex/AGENTS.md", + ".codex/agents/explorer.toml", + ".codex/agents/reviewer.toml", + ".codex/agents/docs-researcher.toml", + ".claude/homunculus/instincts/inherited/docker-flask-postgres-api-instincts.yaml" + ], + "workflow-pack": [ + ".claude/commands/feature-development-api-endpoint.md", + ".claude/commands/documentation-update.md" + ] + }, + "files": [ + { + "moduleId": "runtime-core", + "path": ".claude/skills/docker-flask-postgres-api/SKILL.md", + "description": "Repository-specific Claude Code skill generated from git history." + }, + { + "moduleId": "runtime-core", + "path": ".agents/skills/docker-flask-postgres-api/SKILL.md", + "description": "Codex-facing copy of the generated repository skill." + }, + { + "moduleId": "runtime-core", + "path": ".agents/skills/docker-flask-postgres-api/agents/openai.yaml", + "description": "Codex skill metadata so the repo skill appears cleanly in the skill interface." + }, + { + "moduleId": "runtime-core", + "path": ".claude/identity.json", + "description": "Suggested identity.json baseline derived from repository conventions." + }, + { + "moduleId": "runtime-core", + "path": ".codex/config.toml", + "description": "Repo-local Codex MCP and multi-agent baseline aligned with ECC defaults." + }, + { + "moduleId": "runtime-core", + "path": ".codex/AGENTS.md", + "description": "Codex usage guide that points at the generated repo skill and workflow bundle." + }, + { + "moduleId": "runtime-core", + "path": ".codex/agents/explorer.toml", + "description": "Read-only explorer role config for Codex multi-agent work." + }, + { + "moduleId": "runtime-core", + "path": ".codex/agents/reviewer.toml", + "description": "Read-only reviewer role config focused on correctness and security." + }, + { + "moduleId": "runtime-core", + "path": ".codex/agents/docs-researcher.toml", + "description": "Read-only docs researcher role config for API verification." + }, + { + "moduleId": "runtime-core", + "path": ".claude/homunculus/instincts/inherited/docker-flask-postgres-api-instincts.yaml", + "description": "Continuous-learning instincts derived from repository patterns." + }, + { + "moduleId": "workflow-pack", + "path": ".claude/commands/feature-development-api-endpoint.md", + "description": "Workflow command scaffold for feature-development-api-endpoint." + }, + { + "moduleId": "workflow-pack", + "path": ".claude/commands/documentation-update.md", + "description": "Workflow command scaffold for documentation-update." + } + ], + "workflows": [ + { + "command": "feature-development-api-endpoint", + "path": ".claude/commands/feature-development-api-endpoint.md" + }, + { + "command": "documentation-update", + "path": ".claude/commands/documentation-update.md" + } + ], + "adapters": { + "claudeCode": { + "skillPath": ".claude/skills/docker-flask-postgres-api/SKILL.md", + "identityPath": ".claude/identity.json", + "commandPaths": [ + ".claude/commands/feature-development-api-endpoint.md", + ".claude/commands/documentation-update.md" + ] + }, + "codex": { + "configPath": ".codex/config.toml", + "agentsGuidePath": ".codex/AGENTS.md", + "skillPath": ".agents/skills/docker-flask-postgres-api/SKILL.md" + } + } +} \ No newline at end of file diff --git a/.claude/homunculus/instincts/inherited/docker-flask-postgres-api-instincts.yaml b/.claude/homunculus/instincts/inherited/docker-flask-postgres-api-instincts.yaml new file mode 100644 index 0000000..d9916d0 --- /dev/null +++ b/.claude/homunculus/instincts/inherited/docker-flask-postgres-api-instincts.yaml @@ -0,0 +1,447 @@ +# Instincts generated from https://github.com/donny-devops/docker-flask-postgres-api +# Generated: 2026-05-17T17:29:44.837Z +# Version: 2.0 +# NOTE: This file supplements (does not replace) any existing curated instincts. +# High-confidence manually curated instincts should be preserved alongside these. + +--- +id: docker-flask-postgres-api-commit-conventional +trigger: "when writing a commit message" +confidence: 0.85 +domain: git +source: repo-analysis +source_repo: https://github.com/donny-devops/docker-flask-postgres-api +--- + +# Docker Flask Postgres Api Commit Conventional + +## Action + +Use conventional commit format with prefixes: fix, docs, feat, test + +## Evidence + +- 8 commits analyzed +- Detected conventional commit pattern +- Examples: fix(api): make Marshmallow validators compatible with newer callbacks, feat(api): add readiness endpoint and structured error responses + +--- +id: docker-flask-postgres-api-commit-length +trigger: "when writing a commit message" +confidence: 0.6 +domain: git +source: repo-analysis +source_repo: https://github.com/donny-devops/docker-flask-postgres-api +--- + +# Docker Flask Postgres Api Commit Length + +## Action + +Write moderate-length commit messages (~57 characters) + +## Evidence + +- Average commit message length: 57 chars +- Based on 8 commits + +--- +id: docker-flask-postgres-api-naming-files +trigger: "when creating a new file" +confidence: 0.8 +domain: code-style +source: repo-analysis +source_repo: https://github.com/donny-devops/docker-flask-postgres-api +--- + +# Docker Flask Postgres Api Naming Files + +## Action + +Use camelCase naming convention + +## Evidence + +- Analyzed file naming patterns in repository +- Dominant pattern: camelCase + +--- +id: docker-flask-postgres-api-import-relative +trigger: "when importing modules" +confidence: 0.75 +domain: code-style +source: repo-analysis +source_repo: https://github.com/donny-devops/docker-flask-postgres-api +--- + +# Docker Flask Postgres Api Import Relative + +## Action + +Use relative imports for project files + +## Evidence + +- Import analysis shows relative import pattern +- Example: import { x } from '../lib/x' + +--- +id: docker-flask-postgres-api-export-style +trigger: "when exporting from a module" +confidence: 0.7 +domain: code-style +source: repo-analysis +source_repo: https://github.com/donny-devops/docker-flask-postgres-api +--- + +# Docker Flask Postgres Api Export Style + +## Action + +Prefer named exports + +## Evidence + +- Export pattern analysis +- Dominant style: named + +--- +id: docker-flask-postgres-api-test-separate +trigger: "when writing tests" +confidence: 0.8 +domain: testing +source: repo-analysis +source_repo: https://github.com/donny-devops/docker-flask-postgres-api +--- + +# Docker Flask Postgres Api Test Separate + +## Action + +Place tests in the tests/ or __tests__/ directory, mirroring src structure + +## Evidence + +- Separate test directory pattern detected +- Tests live in dedicated test folders + +--- +id: docker-flask-postgres-api-workflow-feature-development-api-endpoint +trigger: "when doing feature development api endpoint" +confidence: 0.6 +domain: workflow +source: repo-analysis +source_repo: https://github.com/donny-devops/docker-flask-postgres-api +--- + +# Docker Flask Postgres Api Workflow Feature Development Api Endpoint + +## Action + +Follow the feature-development-api-endpoint workflow: +1. Implement endpoint logic in app/__init__.py +2. Add or update validation in app/schemas.py +3. Write or update tests in tests/test_routes.py +4. Update documentation in README.md + +## Evidence + +- Workflow detected from commit patterns +- Frequency: ~2x per month +- Files: app/__init__.py, app/schemas.py, tests/test_routes.py + +--- +id: docker-flask-postgres-api-workflow-documentation-update +trigger: "when doing documentation update" +confidence: 0.6 +domain: workflow +source: repo-analysis +source_repo: https://github.com/donny-devops/docker-flask-postgres-api +--- + +# Docker Flask Postgres Api Workflow Documentation Update + +## Action + +Follow the documentation-update workflow: +1. Update environment variable documentation in .env.example +2. Update main documentation in README.md + +## Evidence + +- Workflow detected from commit patterns +- Frequency: ~2x per month +- Files: .env.example, README.md + +--- +id: docker-flask-postgres-api-instinct-filename-camelcase +trigger: "When creating or renaming a source file in the repository." +confidence: 0.8 +domain: code-style +source: repo-analysis +source_repo: donny-devops/docker-flask-postgres-api +--- + +# Docker Flask Postgres Api Instinct Filename Camelcase + +## Action + +Name the file using camelCase (e.g., myFile.py). + +## Evidence + +- Pattern in codeStyle.namingConventions.files + +--- +id: docker-flask-postgres-api-instinct-function-camelcase +trigger: "When defining a new function." +confidence: 0.8 +domain: code-style +source: repo-analysis +source_repo: donny-devops/docker-flask-postgres-api +--- + +# Docker Flask Postgres Api Instinct Function Camelcase + +## Action + +Name the function using camelCase (e.g., processData). + +## Evidence + +- Pattern in codeStyle.namingConventions.functions + +--- +id: docker-flask-postgres-api-instinct-class-pascalcase +trigger: "When defining a new class." +confidence: 0.8 +domain: code-style +source: repo-analysis +source_repo: donny-devops/docker-flask-postgres-api +--- + +# Docker Flask Postgres Api Instinct Class Pascalcase + +## Action + +Name the class using PascalCase (e.g., DataProcessor). + +## Evidence + +- Pattern in codeStyle.namingConventions.classes + +--- +id: docker-flask-postgres-api-instinct-constant-screaming-snake +trigger: "When defining a constant." +confidence: 0.8 +domain: code-style +source: repo-analysis +source_repo: donny-devops/docker-flask-postgres-api +--- + +# Docker Flask Postgres Api Instinct Constant Screaming Snake + +## Action + +Name the constant using SCREAMING_SNAKE_CASE (e.g., MAX_SIZE). + +## Evidence + +- Pattern in codeStyle.namingConventions.constants + +--- +id: docker-flask-postgres-api-instinct-import-relative +trigger: "When importing modules within the package." +confidence: 0.8 +domain: code-style +source: repo-analysis +source_repo: donny-devops/docker-flask-postgres-api +--- + +# Docker Flask Postgres Api Instinct Import Relative + +## Action + +Use relative imports. + +## Evidence + +- Pattern in codeStyle.importStyle + +--- +id: docker-flask-postgres-api-instinct-export-named +trigger: "When exporting functions, classes, or variables." +confidence: 0.8 +domain: code-style +source: repo-analysis +source_repo: donny-devops/docker-flask-postgres-api +--- + +# Docker Flask Postgres Api Instinct Export Named + +## Action + +Use named exports. + +## Evidence + +- Pattern in codeStyle.exportStyle + +--- +id: docker-flask-postgres-api-instinct-test-location-separate +trigger: "When adding or updating tests." +confidence: 0.9 +domain: testing +source: repo-analysis +source_repo: donny-devops/docker-flask-postgres-api +--- + +# Docker Flask Postgres Api Instinct Test Location Separate + +## Action + +Place test files in the separate 'tests' directory. + +## Evidence + +- Pattern in architecture.folderStructure.testLocation +- tests/test_routes.py + +--- +id: docker-flask-postgres-api-instinct-test-file-naming +trigger: "When naming a test file." +confidence: 0.85 +domain: testing +source: repo-analysis +source_repo: donny-devops/docker-flask-postgres-api +--- + +# Docker Flask Postgres Api Instinct Test File Naming + +## Action + +Use the 'test_' prefix (e.g., test_routes.py). + +## Evidence + +- tests/test_routes.py + +--- +id: docker-flask-postgres-api-instinct-test-update-on-feature +trigger: "When implementing a new API endpoint or feature." +confidence: 0.9 +domain: testing +source: repo-analysis +source_repo: donny-devops/docker-flask-postgres-api +--- + +# Docker Flask Postgres Api Instinct Test Update On Feature + +## Action + +Add or update corresponding tests in the 'tests' directory. + +## Evidence + +- Workflow feature-development-api-endpoint +- Commits like 'test(api): cover readiness validation and JSON error behavior' + +--- +id: docker-flask-postgres-api-instinct-commit-conventional-format +trigger: "When writing a commit message." +confidence: 0.9 +domain: git +source: repo-analysis +source_repo: donny-devops/docker-flask-postgres-api +--- + +# Docker Flask Postgres Api Instinct Commit Conventional Format + +## Action + +Use the conventional commit format: (): . + +## Evidence + +- Pattern in commits.type and prefixes +- Examples in commits.examples + +--- +id: docker-flask-postgres-api-instinct-commit-prefixes +trigger: "When categorizing a commit." +confidence: 0.9 +domain: git +source: repo-analysis +source_repo: donny-devops/docker-flask-postgres-api +--- + +# Docker Flask Postgres Api Instinct Commit Prefixes + +## Action + +Use one of the prefixes: fix, feat, docs, test. + +## Evidence + +- Pattern in commits.prefixes +- Examples in commits.examples + +--- +id: docker-flask-postgres-api-instinct-commit-length +trigger: "When writing a commit message." +confidence: 0.7 +domain: git +source: repo-analysis +source_repo: donny-devops/docker-flask-postgres-api +--- + +# Docker Flask Postgres Api Instinct Commit Length + +## Action + +Keep the commit message concise, around 57 characters on average. + +## Evidence + +- Pattern in commits.averageLength + +--- +id: docker-flask-postgres-api-instinct-workflow-new-endpoint +trigger: "When someone wants to add a new API endpoint with proper validation, error handling, and test coverage." +confidence: 0.9 +domain: workflow +source: repo-analysis +source_repo: donny-devops/docker-flask-postgres-api +--- + +# Docker Flask Postgres Api Instinct Workflow New Endpoint + +## Action + +Follow the steps: implement logic in app/__init__.py, update validation in app/schemas.py, write/update tests in tests/test_routes.py, update documentation in README.md. + +## Evidence + +- Workflow feature-development-api-endpoint +- Commit sequence and files involved + +--- +id: docker-flask-postgres-api-instinct-workflow-update-docs +trigger: "When someone wants to document new features, configuration, or operational procedures." +confidence: 0.85 +domain: workflow +source: repo-analysis +source_repo: donny-devops/docker-flask-postgres-api +--- + +# Docker Flask Postgres Api Instinct Workflow Update Docs + +## Action + +Update environment variable documentation in .env.example and main documentation in README.md. + +## Evidence + +- Workflow documentation-update +- Commit sequence and files involved + diff --git a/.claude/identity.json b/.claude/identity.json new file mode 100644 index 0000000..0487339 --- /dev/null +++ b/.claude/identity.json @@ -0,0 +1,14 @@ +{ + "version": "2.0", + "technicalLevel": "technical", + "preferredStyle": { + "verbosity": "moderate", + "codeComments": true, + "explanations": true + }, + "domains": [ + "python" + ], + "suggestedBy": "ecc-tools-repo-analysis", + "createdAt": "2026-05-17T17:29:44.837Z" +} \ No newline at end of file diff --git a/.claude/skills/docker-flask-postgres-api/SKILL.md b/.claude/skills/docker-flask-postgres-api/SKILL.md new file mode 100644 index 0000000..eb0b181 --- /dev/null +++ b/.claude/skills/docker-flask-postgres-api/SKILL.md @@ -0,0 +1,121 @@ +```markdown +# docker-flask-postgres-api Development Patterns + +> Auto-generated skill from repository analysis + +## Overview + +This skill teaches the core development patterns, coding conventions, and workflows used in the `docker-flask-postgres-api` repository. The repository is a Python-based API project, using Docker for deployment and PostgreSQL as the database backend. It emphasizes clear commit messages, modular code organization, and robust testing and documentation practices. + +## Coding Conventions + +- **File Naming:** + Use camelCase for file names. + _Example:_ + ``` + userRoutes.py + dbConnection.py + ``` + +- **Import Style:** + Use relative imports within the package. + _Example:_ + ```python + from .schemas import UserSchema + from .utils import validate_input + ``` + +- **Export Style:** + Use named exports (explicitly define what is exported). + _Example:_ + ```python + def create_user(...): + ... + + def delete_user(...): + ... + ``` + +- **Commit Messages:** + Follow [Conventional Commits](https://www.conventionalcommits.org/) with prefixes: `fix`, `docs`, `feat`, `test`. + _Example:_ + ``` + feat: add user registration endpoint with validation + fix: correct db connection string parsing + docs: update README with setup instructions + test: add tests for user login route + ``` + +## Workflows + +### Feature Development: API Endpoint +**Trigger:** When adding a new API endpoint with validation, error handling, and tests +**Command:** `/new-endpoint` + +1. **Implement endpoint logic** in `app/__init__.py` + _Example:_ + ```python + @app.route('/users', methods=['POST']) + def create_user(): + # Endpoint logic here + ``` +2. **Add or update validation** in `app/schemas.py` + _Example:_ + ```python + class UserSchema(Schema): + username = fields.Str(required=True) + email = fields.Email(required=True) + ``` +3. **Write or update tests** in `tests/test_routes.py` + _Example:_ + ```python + def test_create_user(client): + response = client.post('/users', json={...}) + assert response.status_code == 201 + ``` +4. **Update documentation** in `README.md` + - Document the new endpoint, request/response examples, and any new environment variables. + +--- + +### Documentation Update +**Trigger:** When documenting new features, configuration, or operational procedures +**Command:** `/update-docs` + +1. **Update environment variable documentation** in `.env.example` + - Add or update variables relevant to the new feature. +2. **Update main documentation** in `README.md` + - Describe new features, configuration steps, or architectural changes. + +--- + +## Testing Patterns + +- **Test File Naming:** + Test files follow the pattern `*.test.*` (e.g., `userRoutes.test.py`). + +- **Test Location:** + Tests are placed in the `tests/` directory, commonly in files like `tests/test_routes.py`. + +- **Test Structure:** + Each test function typically uses a client fixture to make HTTP requests to endpoints and asserts on the response. + + _Example:_ + ```python + def test_get_users(client): + response = client.get('/users') + assert response.status_code == 200 + assert isinstance(response.json, list) + ``` + +- **Testing Framework:** + The specific framework is not detected, but typical Python test frameworks (like `pytest`) are likely used. + +## Commands + +| Command | Purpose | +|----------------|-----------------------------------------------------------------| +| /new-endpoint | Start the workflow to add a new API endpoint with tests & docs | +| /update-docs | Start the workflow to update documentation and environment vars | + +``` \ No newline at end of file diff --git a/.codex/AGENTS.md b/.codex/AGENTS.md new file mode 100644 index 0000000..2429b28 --- /dev/null +++ b/.codex/AGENTS.md @@ -0,0 +1,27 @@ +# ECC for Codex CLI + +This supplements the root `AGENTS.md` with a repo-local ECC baseline. + +## Repo Skill + +- Repo-generated Codex skill: `.agents/skills/docker-flask-postgres-api/SKILL.md` +- Claude-facing companion skill: `.claude/skills/docker-flask-postgres-api/SKILL.md` +- Keep user-specific credentials and private MCPs in `~/.codex/config.toml`, not in this repo. + +## MCP Baseline + +Treat `.codex/config.toml` as the default ECC-safe baseline for work in this repository. +The generated baseline enables GitHub, Context7, Exa, Memory, Playwright, and Sequential Thinking. + +## Multi-Agent Support + +- Explorer: read-only evidence gathering +- Reviewer: correctness, security, and regression review +- Docs researcher: API and release-note verification + +## Workflow Files + +- `.claude/commands/feature-development-api-endpoint.md` +- `.claude/commands/documentation-update.md` + +Use these workflow files as reusable task scaffolds when the detected repository workflows recur. \ No newline at end of file diff --git a/.codex/agents/docs-researcher.toml b/.codex/agents/docs-researcher.toml new file mode 100644 index 0000000..0daae57 --- /dev/null +++ b/.codex/agents/docs-researcher.toml @@ -0,0 +1,9 @@ +model = "gpt-5.4" +model_reasoning_effort = "medium" +sandbox_mode = "read-only" + +developer_instructions = """ +Verify APIs, framework behavior, and release-note claims against primary documentation before changes land. +Cite the exact docs or file paths that support each claim. +Do not invent undocumented behavior. +""" \ No newline at end of file diff --git a/.codex/agents/explorer.toml b/.codex/agents/explorer.toml new file mode 100644 index 0000000..732df7a --- /dev/null +++ b/.codex/agents/explorer.toml @@ -0,0 +1,9 @@ +model = "gpt-5.4" +model_reasoning_effort = "medium" +sandbox_mode = "read-only" + +developer_instructions = """ +Stay in exploration mode. +Trace the real execution path, cite files and symbols, and avoid proposing fixes unless the parent agent asks for them. +Prefer targeted search and file reads over broad scans. +""" \ No newline at end of file diff --git a/.codex/agents/reviewer.toml b/.codex/agents/reviewer.toml new file mode 100644 index 0000000..b13ed9c --- /dev/null +++ b/.codex/agents/reviewer.toml @@ -0,0 +1,9 @@ +model = "gpt-5.4" +model_reasoning_effort = "high" +sandbox_mode = "read-only" + +developer_instructions = """ +Review like an owner. +Prioritize correctness, security, behavioral regressions, and missing tests. +Lead with concrete findings and avoid style-only feedback unless it hides a real bug. +""" \ No newline at end of file diff --git a/.codex/config.toml b/.codex/config.toml new file mode 100644 index 0000000..bc1ee67 --- /dev/null +++ b/.codex/config.toml @@ -0,0 +1,48 @@ +#:schema https://developers.openai.com/codex/config-schema.json + +# ECC Tools generated Codex baseline +approval_policy = "on-request" +sandbox_mode = "workspace-write" +web_search = "live" + +[mcp_servers.github] +command = "npx" +args = ["-y", "@modelcontextprotocol/server-github"] + +[mcp_servers.context7] +command = "npx" +args = ["-y", "@upstash/context7-mcp@latest"] + +[mcp_servers.exa] +url = "https://mcp.exa.ai/mcp" + +[mcp_servers.memory] +command = "npx" +args = ["-y", "@modelcontextprotocol/server-memory"] + +[mcp_servers.playwright] +command = "npx" +args = ["-y", "@playwright/mcp@latest", "--extension"] + +[mcp_servers.sequential-thinking] +command = "npx" +args = ["-y", "@modelcontextprotocol/server-sequential-thinking"] + +[features] +multi_agent = true + +[agents] +max_threads = 6 +max_depth = 1 + +[agents.explorer] +description = "Read-only codebase explorer for gathering evidence before changes are proposed." +config_file = "agents/explorer.toml" + +[agents.reviewer] +description = "PR reviewer focused on correctness, security, and missing tests." +config_file = "agents/reviewer.toml" + +[agents.docs_researcher] +description = "Documentation specialist that verifies APIs, framework behavior, and release notes." +config_file = "agents/docs-researcher.toml" \ No newline at end of file diff --git a/.env.example b/.env.example index 69e49ee..0c0283d 100644 --- a/.env.example +++ b/.env.example @@ -1,13 +1,19 @@ # Application FLASK_ENV=development -SECRET_KEY=change-me-in-production-use-secrets-manager +SECRET_KEY=replace-with-a-local-development-value # Database -DATABASE_URL=postgresql://appuser:apppassword@db:5432/appdb +DATABASE_URL=postgresql://appuser:replace-with-local-db-value@db:5432/appdb POSTGRES_USER=appuser -POSTGRES_PASSWORD=apppassword +POSTGRES_PASSWORD=replace-with-local-db-value POSTGRES_DB=appdb +DB_WAIT_TIMEOUT=60 -# pgAdmin +# Gunicorn +GUNICORN_WORKERS=2 +GUNICORN_THREADS=4 +GUNICORN_TIMEOUT=60 + +# pgAdmin - only starts with: docker compose --profile admin up -d PGADMIN_DEFAULT_EMAIL=admin@example.com -PGADMIN_DEFAULT_PASSWORD=admin +PGADMIN_DEFAULT_PASSWORD=replace-with-local-admin-value diff --git a/.github/workflows/agentops-fleet.yml b/.github/workflows/agentops-fleet.yml new file mode 100644 index 0000000..3088da4 --- /dev/null +++ b/.github/workflows/agentops-fleet.yml @@ -0,0 +1,22 @@ +name: AgentOps Fleet Gate + +on: + push: + pull_request: + workflow_dispatch: + schedule: + - cron: '31 8 * * 1' + +permissions: + contents: read + actions: read + security-events: write + pull-requests: read + +jobs: + agentops: + uses: donny-devops/github-actions-templates/.github/workflows/reusable-agentops.yml@main + with: + python-version: '3.12' + node-version: '22' + run-security-audit: true diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 64eb9a5..739ee2b 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -9,17 +9,24 @@ on: env: FORCE_JAVASCRIPT_ACTIONS_TO_NODE24: true -permissions: - contents: read - jobs: lint: name: Lint (ruff) runs-on: ubuntu-latest + permissions: + contents: write steps: + - name: Harden the runner (audit outbound calls) + uses: step-security/harden-runner@fa2e9d605c4eeb9fcad4c99c224cee0c6c7f3594 # v2.16.0 + with: + egress-policy: audit + - uses: actions/checkout@v4 + with: + ref: ${{ github.head_ref }} + token: ${{ secrets.GITHUB_TOKEN }} - - uses: actions/setup-python@v5 + - uses: actions/setup-python@v6 with: python-version: "3.12" cache: pip @@ -29,13 +36,18 @@ jobs: - name: Lint with ruff run: ruff check . --output-format=github - - name: Format check with ruff - run: ruff format --check . + - name: Format with ruff + run: | + ruff format . + git config user.email "41898282+github-actions[bot]@users.noreply.github.com" + git config user.name "github-actions[bot]" + git diff --quiet || git commit -am "style: auto-format with ruff" && git push test: name: Test (pytest + PostgreSQL) runs-on: ubuntu-latest needs: lint + services: postgres: image: postgres:16-alpine @@ -50,13 +62,19 @@ jobs: --health-retries 5 ports: - 5432:5432 + env: - DATABASE_URL: postgresql://testuser:testpass@localhost:5432/testdb + DATABASE_URL: postgresql://testuser:***@localhost:5432/testdb SECRET_KEY: test-secret + steps: - - uses: actions/checkout@v4 + - name: Harden the runner (audit outbound calls) + uses: step-security/harden-runner@fa2e9d605c4eeb9fcad4c99c224cee0c6c7f3594 # v2.16.0 + with: + egress-policy: audit - - uses: actions/setup-python@v5 + - uses: actions/checkout@v4 + - uses: actions/setup-python@v6 with: python-version: "3.12" cache: pip @@ -67,37 +85,45 @@ jobs: pip install -r requirements.txt - name: Run migrations - run: flask db upgrade + run: | + flask db upgrade env: FLASK_APP: "app:create_app()" + DATABASE_URL: postgresql://testuser:testpass@localhost:5432/testdb - name: Run pytest - run: pytest --cov=app --cov-report=xml --cov-fail-under=85 -v + run: | + pytest --cov=app --cov-report=xml --cov-fail-under=85 -v + - name: Upload coverage - uses: actions/upload-artifact@v4 + uses: actions/upload-artifact@v7 with: name: coverage-report path: coverage.xml - retention-days: 7 docker: name: Build & Push Docker Image runs-on: ubuntu-latest needs: test + if: github.event_name == 'push' && github.ref == 'refs/heads/main' permissions: contents: read packages: write - security-events: write - if: github.event_name == 'push' && github.ref == 'refs/heads/main' + steps: + - name: Harden the runner (audit outbound calls) + uses: step-security/harden-runner@fa2e9d605c4eeb9fcad4c99c224cee0c6c7f3594 # v2.16.0 + with: + egress-policy: audit + - uses: actions/checkout@v4 - name: Set up Docker Buildx - uses: docker/setup-buildx-action@v3 + uses: docker/setup-buildx-action@v4 - name: Log in to GHCR - uses: docker/login-action@v3 + uses: docker/login-action@v4 with: registry: ghcr.io username: ${{ github.actor }} @@ -105,7 +131,7 @@ jobs: - name: Extract metadata id: meta - uses: docker/metadata-action@v5 + uses: docker/metadata-action@v6 with: images: ghcr.io/${{ github.repository }} tags: | @@ -113,27 +139,10 @@ jobs: type=raw,value=latest - name: Build and push - id: build - uses: docker/build-push-action@v6 + uses: docker/build-push-action@v5 with: context: . push: true tags: ${{ steps.meta.outputs.tags }} cache-from: type=gha cache-to: type=gha,mode=max - provenance: true - sbom: true - - - name: Trivy image scan - uses: aquasecurity/trivy-action@master - with: - image-ref: ghcr.io/${{ github.repository }}:latest - format: sarif - output: trivy.sarif - severity: HIGH,CRITICAL - exit-code: '0' - - - name: Upload Trivy results - uses: github/codeql-action/upload-sarif@v3 - with: - sarif_file: trivy.sarif diff --git a/.github/workflows/security-hygiene.yml b/.github/workflows/security-hygiene.yml new file mode 100644 index 0000000..0f873a1 --- /dev/null +++ b/.github/workflows/security-hygiene.yml @@ -0,0 +1,35 @@ +name: Security Hygiene + +on: + push: + branches: [main] + pull_request: + branches: [main] + workflow_dispatch: + +permissions: + contents: read + +jobs: + hygiene: + name: Secret hygiene + runs-on: ubuntu-latest + steps: + - name: Harden the runner (Audit all outbound calls) + uses: step-security/harden-runner@fa2e9d605c4eeb9fcad4c99c224cee0c6c7f3594 # v2.16.0 + with: + egress-policy: audit + + - name: Checkout repository + uses: actions/checkout@v4 + + - name: Block obvious private keys and tokens + shell: bash + run: | + SECRET_PATTERN='BEGIN (RSA |OPENSSH |EC |DSA )?PRIVATE KEY|ghp_[A-Za-z0-9_]{20,}|github_pat_[A-Za-z0-9_]{20,}|AKIA[0-9A-Z]{16}|AIza[0-9A-Za-z_-]{20,}|sk-[A-Za-z0-9]{20,}' + if grep -RInE "$SECRET_PATTERN" . \ + --exclude-dir=.git \ + --exclude=.github/workflows/security-hygiene.yml; then + echo "Potential secret material found. Remove it and rotate the credential." + exit 1 + fi diff --git a/Dockerfile b/Dockerfile index c29e45c..5d303df 100644 --- a/Dockerfile +++ b/Dockerfile @@ -4,7 +4,7 @@ # ============================================================================= # ── Stage 1: Builder ────────────────────────────────────────────────────────── -FROM python:3.12-slim AS builder +FROM python:3.14-slim AS builder WORKDIR /build @@ -19,7 +19,7 @@ RUN pip install --upgrade pip && \ pip install --no-cache-dir --prefix=/install -r requirements.txt # ── Stage 2: Runtime ────────────────────────────────────────────────────────── -FROM python:3.12-slim AS runtime +FROM python:3.14-slim AS runtime LABEL maintainer="donny-devops" \ org.opencontainers.image.title="docker-flask-postgres-api" \ diff --git a/README.md b/README.md index b15c16c..4134222 100644 --- a/README.md +++ b/README.md @@ -1,6 +1,6 @@ # Docker Flask Postgres API -A production-ready REST API built with **Flask**, **PostgreSQL**, and **Docker Compose** — complete with migrations, a full test suite, and a GitHub Actions CI pipeline. +A production-ready REST API built with **Flask**, **PostgreSQL**, and **Docker Compose** — complete with migrations, a full test suite, readiness checks, and a GitHub Actions CI pipeline. [![CI](https://github.com/donny-devops/docker-flask-postgres-api/actions/workflows/ci.yml/badge.svg)](https://github.com/donny-devops/docker-flask-postgres-api/actions/workflows/ci.yml) [![Python](https://img.shields.io/badge/Python-3.12-3776AB?style=flat-square&logo=python&logoColor=white)](https://www.python.org/) @@ -12,6 +12,22 @@ A production-ready REST API built with **Flask**, **PostgreSQL**, and **Docker C ## Architecture +```text +Client / curl / API consumer + | + v +Flask API container + - Gunicorn + - Flask application factory + - Marshmallow validation + - SQLAlchemy ORM + - Flask-Migrate / Alembic + | + v +PostgreSQL container +``` + +Docker Compose runs the API and database on an isolated bridge network. The API waits for database connectivity before running migrations, then starts Gunicorn. PostgreSQL and pgAdmin bind to localhost by default to avoid exposing admin surfaces on every interface. --- @@ -23,14 +39,20 @@ git clone https://github.com/donny-devops/docker-flask-postgres-api.git cd docker-flask-postgres-api cp .env.example .env -# 2. Start all services -docker compose up -d +# 2. Start API + database +docker compose up -d --build -# 3. Run migrations -docker compose exec api flask db upgrade - -# 4. Verify +# 3. Verify liveness and readiness curl http://localhost:5000/health +curl http://localhost:5000/ready +``` + +The container entrypoint waits for Postgres, runs `flask db upgrade`, and starts Gunicorn automatically. Manual migration commands are still useful for local development, but they are not required for the standard Compose startup path. + +To start pgAdmin as an optional admin tool: + +```bash +docker compose --profile admin up -d pgadmin ``` --- @@ -39,7 +61,8 @@ curl http://localhost:5000/health | Method | Endpoint | Description | |--------|----------|-------------| -| `GET` | `/health` | Health check | +| `GET` | `/health` | Liveness check; does not require database connectivity | +| `GET` | `/ready` | Readiness check; verifies database connectivity | | `GET` | `/api/v1/items` | List all active items | | `POST` | `/api/v1/items` | Create a new item | | `GET` | `/api/v1/items/:id` | Get item by ID | @@ -72,13 +95,17 @@ curl -X POST http://localhost:5000/api/v1/items \ | Variable | Default | Description | |----------|---------|-------------| | `FLASK_ENV` | `production` | Flask environment mode | -| `SECRET_KEY` | *(required)* | Flask secret key | +| `SECRET_KEY` | required | Flask secret key; use a real secret manager outside local dev | | `DATABASE_URL` | `postgresql://...` | Full PostgreSQL connection string | | `POSTGRES_USER` | `appuser` | PostgreSQL username | -| `POSTGRES_PASSWORD` | `apppassword` | PostgreSQL password | +| `POSTGRES_PASSWORD` | local placeholder | PostgreSQL password | | `POSTGRES_DB` | `appdb` | PostgreSQL database name | +| `DB_WAIT_TIMEOUT` | `60` | Seconds the API entrypoint waits for DB connectivity | +| `GUNICORN_WORKERS` | `2` | Gunicorn worker count | +| `GUNICORN_THREADS` | `4` | Gunicorn threads per worker | +| `GUNICORN_TIMEOUT` | `60` | Gunicorn request timeout in seconds | | `PGADMIN_DEFAULT_EMAIL` | `admin@example.com` | pgAdmin login email | -| `PGADMIN_DEFAULT_PASSWORD` | `admin` | pgAdmin login password | +| `PGADMIN_DEFAULT_PASSWORD` | local placeholder | pgAdmin login password | --- @@ -86,14 +113,20 @@ curl -X POST http://localhost:5000/api/v1/items \ ```bash # Run tests locally +python -m venv .venv +source .venv/bin/activate pip install -r requirements.txt pytest --cov=app -v -# Lint +# Lint and format check ruff check . +ruff format --check . + +# Apply formatting intentionally ruff format . # Database migrations +export FLASK_APP='app:create_app()' flask db migrate -m "describe your change" flask db upgrade ``` @@ -102,6 +135,33 @@ flask db upgrade ## Project Structure +```text +. +├── app/ +│ ├── __init__.py # Flask application factory, health/readiness, error handlers +│ ├── config.py # Runtime and test configuration +│ ├── extensions.py # SQLAlchemy and Flask-Migrate extension instances +│ ├── models.py # Item SQLAlchemy model +│ ├── routes.py # REST API routes +│ └── schemas.py # Marshmallow request validation +├── migrations/ # Alembic migration environment and revisions +├── tests/ # Pytest API coverage +├── Dockerfile # Multi-stage production image +├── docker-compose.yml # API, Postgres, optional pgAdmin +├── entrypoint.sh # DB wait, migration, Gunicorn startup +├── requirements.txt # Runtime and dev dependencies +└── run.py # Local development entrypoint +``` + +--- + +## Operational Notes + +- `/health` is for container liveness. +- `/ready` is for load balancers and orchestrators because it checks database connectivity. +- The API performs migrations at startup inside Docker. In higher-control production environments, run migrations as a separate release job. +- PostgreSQL and pgAdmin are bound to localhost in Compose. Keep that default unless you intentionally need remote access. +- pgAdmin is optional and gated behind the `admin` Compose profile. --- diff --git a/app/__init__.py b/app/__init__.py index f083c81..ca3e848 100644 --- a/app/__init__.py +++ b/app/__init__.py @@ -1,4 +1,8 @@ -from flask import Flask +from http import HTTPStatus + +from flask import Flask, jsonify +from sqlalchemy import text +from werkzeug.exceptions import HTTPException from app.config import Config from app.extensions import db, migrate @@ -18,9 +22,63 @@ def create_app(config_class: type = Config) -> Flask: app.register_blueprint(api_bp, url_prefix="/api/v1") - # Health check endpoint (outside blueprint for simplicity) @app.get("/health") - def health() -> dict: # type: ignore[return] + def health() -> dict[str, str]: + """Liveness check that does not require the database.""" return {"status": "healthy", "service": "docker-flask-postgres-api"} + @app.get("/ready") + def ready(): # noqa: ANN202 + """Readiness check that verifies database connectivity.""" + try: + db.session.execute(text("SELECT 1")) + except Exception as exc: # noqa: BLE001 + app.logger.warning("Database readiness check failed: %s", exc) + return ( + jsonify( + { + "status": "unready", + "service": "docker-flask-postgres-api", + "database": "unavailable", + } + ), + HTTPStatus.SERVICE_UNAVAILABLE, + ) + return jsonify( + { + "status": "ready", + "service": "docker-flask-postgres-api", + "database": "ok", + } + ) + + @app.errorhandler(HTTPException) + def handle_http_exception(exc: HTTPException): # noqa: ANN202 + """Return consistent JSON for Flask/Werkzeug HTTP errors.""" + return ( + jsonify( + { + "error": exc.name, + "message": exc.description, + "status_code": exc.code, + } + ), + exc.code, + ) + + @app.errorhandler(Exception) + def handle_unexpected_exception(exc: Exception): # noqa: ANN202 + """Return a safe JSON response for unexpected server errors.""" + app.logger.exception("Unhandled application error: %s", exc) + return ( + jsonify( + { + "error": "Internal Server Error", + "message": "An unexpected server error occurred.", + "status_code": HTTPStatus.INTERNAL_SERVER_ERROR, + } + ), + HTTPStatus.INTERNAL_SERVER_ERROR, + ) + return app diff --git a/app/schemas.py b/app/schemas.py index ab50c4c..ade9429 100644 --- a/app/schemas.py +++ b/app/schemas.py @@ -1,5 +1,7 @@ """Marshmallow schemas for request validation and response serialization.""" +from typing import Any + from marshmallow import Schema, ValidationError, fields, validate, validates @@ -17,7 +19,13 @@ class ItemCreateSchema(Schema): ) @validates("name") - def validate_name_not_blank(self, value: str) -> None: + def validate_name_not_blank(self, value: str, **_: Any) -> None: + """Reject whitespace-only names. + + Marshmallow passes callback metadata such as data_key in newer 3.x/4.x + releases. Accepting **_ keeps the schema compatible across supported + versions without weakening validation. + """ if not value.strip(): raise ValidationError("Name cannot be blank or whitespace.") @@ -33,7 +41,8 @@ class ItemUpdateSchema(Schema): is_active = fields.Bool(required=False) @validates("name") - def validate_name_not_blank(self, value: str) -> None: + def validate_name_not_blank(self, value: str, **_: Any) -> None: + """Reject whitespace-only names while allowing Marshmallow metadata.""" if not value.strip(): raise ValidationError("Name cannot be blank or whitespace.") diff --git a/docker-compose.yml b/docker-compose.yml index 3136c53..b56bbeb 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -10,7 +10,7 @@ services: volumes: - postgres_data:/var/lib/postgresql/data ports: - - "5432:5432" + - "127.0.0.1:5432:5432" healthcheck: test: ["CMD-SHELL", "pg_isready -U ${POSTGRES_USER:-appuser} -d ${POSTGRES_DB:-appdb}"] interval: 10s @@ -30,17 +30,21 @@ services: FLASK_ENV: ${FLASK_ENV:-production} SECRET_KEY: ${SECRET_KEY:-change-me} DATABASE_URL: postgresql://${POSTGRES_USER:-appuser}:${POSTGRES_PASSWORD:-apppassword}@db:5432/${POSTGRES_DB:-appdb} + DB_WAIT_TIMEOUT: ${DB_WAIT_TIMEOUT:-60} + GUNICORN_WORKERS: ${GUNICORN_WORKERS:-2} + GUNICORN_THREADS: ${GUNICORN_THREADS:-4} + GUNICORN_TIMEOUT: ${GUNICORN_TIMEOUT:-60} ports: - "5000:5000" depends_on: db: condition: service_healthy healthcheck: - test: ["CMD", "curl", "-f", "http://localhost:5000/health"] + test: ["CMD", "curl", "-f", "http://localhost:5000/ready"] interval: 30s timeout: 10s retries: 3 - start_period: 20s + start_period: 30s networks: - app-network @@ -48,12 +52,14 @@ services: image: dpage/pgadmin4:latest container_name: flask-api-pgadmin restart: unless-stopped + profiles: + - admin environment: PGADMIN_DEFAULT_EMAIL: ${PGADMIN_DEFAULT_EMAIL:-admin@example.com} PGADMIN_DEFAULT_PASSWORD: ${PGADMIN_DEFAULT_PASSWORD:-admin} PGADMIN_CONFIG_SERVER_MODE: "False" ports: - - "5050:80" + - "127.0.0.1:5050:80" depends_on: db: condition: service_healthy diff --git a/entrypoint.sh b/entrypoint.sh index bb7c9d1..e2bb810 100644 --- a/entrypoint.sh +++ b/entrypoint.sh @@ -1,19 +1,57 @@ #!/bin/bash # ============================================================================= # Docker container entrypoint -# Runs Flask-Migrate DB upgrade, then starts gunicorn. +# Waits for database connectivity, runs Flask-Migrate upgrade, then starts gunicorn. # ============================================================================= set -euo pipefail +: "${DATABASE_URL:?DATABASE_URL must be set}" +: "${GUNICORN_WORKERS:=2}" +: "${GUNICORN_THREADS:=4}" +: "${DB_WAIT_TIMEOUT:=60}" + +wait_for_database() { + echo "[entrypoint] Waiting for database connectivity..." + python <<'PY' +import os +import sys +import time + +from sqlalchemy import create_engine, text +from sqlalchemy.exc import SQLAlchemyError + +url = os.environ["DATABASE_URL"] +timeout = int(os.environ.get("DB_WAIT_TIMEOUT", "60")) +deadline = time.monotonic() + timeout +last_error = "unknown" + +while time.monotonic() < deadline: + try: + engine = create_engine(url, pool_pre_ping=True) + with engine.connect() as conn: + conn.execute(text("SELECT 1")) + print("[entrypoint] Database is reachable.") + sys.exit(0) + except SQLAlchemyError as exc: + last_error = str(exc) + time.sleep(2) + +print(f"[entrypoint] Database did not become reachable within {timeout}s: {last_error}", file=sys.stderr) +sys.exit(1) +PY +} + +wait_for_database + echo "[entrypoint] Running Flask-Migrate upgrade..." flask db upgrade echo "[entrypoint] Starting gunicorn on 0.0.0.0:5000..." exec python -m gunicorn \ --bind 0.0.0.0:5000 \ - --workers "${GUNICORN_WORKERS:-2}" \ - --threads "${GUNICORN_THREADS:-4}" \ - --timeout 60 \ + --workers "${GUNICORN_WORKERS}" \ + --threads "${GUNICORN_THREADS}" \ + --timeout "${GUNICORN_TIMEOUT:-60}" \ --access-logfile - \ --error-logfile - \ "app:create_app()" diff --git a/requirements.txt b/requirements.txt index 13eee6d..211350a 100644 --- a/requirements.txt +++ b/requirements.txt @@ -7,7 +7,7 @@ Flask-Migrate==4.0.7 psycopg2-binary==2.9.9 # ── Config ──────────────────────────────────────────────────────────────────── -python-dotenv==1.0.1 +python-dotenv==1.2.2 # ── Serialization / Validation ──────────────────────────────────────────────── marshmallow==3.26.2 diff --git a/tests/test_routes.py b/tests/test_routes.py index fa39fe6..411580d 100644 --- a/tests/test_routes.py +++ b/tests/test_routes.py @@ -32,13 +32,29 @@ def client(app: Flask) -> FlaskClient: return app.test_client() -# ── Health Check ────────────────────────────────────────────────────────────── +# ── Health / Readiness ──────────────────────────────────────────────────────── class TestHealth: def test_health_returns_200(self, client: FlaskClient) -> None: resp = client.get("/health") assert resp.status_code == 200 assert resp.json["status"] == "healthy" + def test_ready_returns_200_when_database_is_available(self, client: FlaskClient) -> None: + resp = client.get("/ready") + assert resp.status_code == 200 + assert resp.json["status"] == "ready" + assert resp.json["database"] == "ok" + + +# ── Error Handling ──────────────────────────────────────────────────────────── +class TestErrors: + def test_not_found_returns_json(self, client: FlaskClient) -> None: + resp = client.get("/does-not-exist") + assert resp.status_code == 404 + assert resp.is_json + assert resp.json["error"] == "Not Found" + assert resp.json["status_code"] == 404 + # ── List Items ──────────────────────────────────────────────────────────────── class TestListItems: @@ -100,6 +116,8 @@ def test_get_existing(self, client: FlaskClient) -> None: def test_get_not_found(self, client: FlaskClient) -> None: resp = client.get("/api/v1/items/99999") assert resp.status_code == 404 + assert resp.is_json + assert resp.json["error"] == "Not Found" # ── Update Item ─────────────────────────────────────────────────────────────── @@ -110,6 +128,17 @@ def test_update_name(self, client: FlaskClient) -> None: assert resp.status_code == 200 assert resp.json["name"] == "new-name" + def test_update_blank_name(self, client: FlaskClient) -> None: + created = client.post("/api/v1/items", json={"name": "old-name"}).json + resp = client.put(f"/api/v1/items/{created['id']}", json={"name": " "}) + assert resp.status_code == 422 + + def test_update_duplicate_name(self, client: FlaskClient) -> None: + client.post("/api/v1/items", json={"name": "first"}) + created = client.post("/api/v1/items", json={"name": "second"}).json + resp = client.put(f"/api/v1/items/{created['id']}", json={"name": "first"}) + assert resp.status_code == 409 + def test_update_not_found(self, client: FlaskClient) -> None: resp = client.put("/api/v1/items/99999", json={"name": "x"}) assert resp.status_code == 404