Skip to content

fix(template): wire async DB layer + Alembic and make generated projects gate-clean#96

Open
williaby wants to merge 2 commits into
mainfrom
fix/template-generation-defects
Open

fix(template): wire async DB layer + Alembic and make generated projects gate-clean#96
williaby wants to merge 2 commits into
mainfrom
fix/template-generation-defects

Conversation

@williaby

@williaby williaby commented Jun 21, 2026

Copy link
Copy Markdown
Collaborator

Changes

Makes default generations pass their own quality gates. Generated projects with
API + database selections previously started red: a missing DB module, no async
driver, and a pydoclint config stricter than the shipped Google docstrings.

Database layer (was half-wired)

  • New core/database.py (async engine, async_sessionmaker, Base,
    get_session), shipped when include_database != none.
  • database_url added to core/config.py with a dialect-correct default.
  • New database optional-dependency group gated on include_database with the
    async driver per dialect (asyncpg/aiomysql/aiosqlite) and alembic for
    the migration variants; sqlalchemy/alembic removed from the unconditional
    api extra.
  • health.py readiness check uses SQLAlchemy 2.0 text().
  • Minimal async Alembic scaffold (alembic.ini, migrations/env.py,
    script.py.mako, versions/.gitkeep) for sqlalchemy_migrations/_ledger;
    *.mako added to _copy_without_render; migrations/ excluded from
    ruff/pydoclint/interrogate; post-gen removes the scaffold for non-migration
    choices.

Strict gates

  • All shipped src docstrings brought into strict pydoclint compliance
    (typed Args:/Returns:, __init__ docstrings folded into class Args:,
    typed class Attributes:) across config, exceptions, health, logging,
    security, correlation.
  • Middleware strict-type fixes (call_next: RequestResponseEndpoint,
    MutableHeaders del, list[str], redundant isinstance).
  • TestCLI gated behind include_cli so disabling the CLI no longer leaves
    tests importing the removed cli.py.
  • ruff force-exclude = true so excluded dirs are honored under pre-commit.

Template Testing

Verified by generating three flag combinations with cookiecutter and running
the full gate set on each:

  • Minimal (no api, no db, no cli): pydoclint 0, ruff clean, basedpyright 0
    errors, 79 tests pass.
  • api + sqlalchemy (no migrations) + sqlite + cli: pydoclint 0, ruff
    clean, basedpyright 0 errors, 115 tests pass.
  • api + sqlalchemy_migrations + postgres: pydoclint 0, ruff clean,
    basedpyright 0 errors, 103 tests pass, full pre-commit run --all-files green
    except the pre-existing end-of-file-fixer auto-fix (see below).

Known / out of scope

  • end-of-file-fixer auto-fixes 3 files (.env.example, .gitignore,
    .pre-commit-config.yaml) on first run because they render with extra
    trailing blank lines from Jinja {% endif %}. Self-healing (re-stage +
    commit); pre-existing and unrelated to these fixes.

Breaking Changes

  • None for default selections. Behavioral change: DB dependencies now live
    in a database extra (gated on include_database) instead of the api
    extra; install with uv sync --all-extras.

Summary by CodeRabbit

  • New Features

    • Added configurable database support with Alembic migrations for SQLAlchemy-based projects.
    • Added database connectivity checks to application health readiness monitoring.
  • Bug Fixes

    • Database configuration files are now properly removed when migrations are not enabled.
  • Refactor

    • Improved async SQLAlchemy engine and session management.
    • Enhanced type annotations and documentation consistency across middleware and core utilities.

williaby and others added 2 commits June 20, 2026 21:25
- middleware/security.py + correlation.py: type call_next as
  RequestResponseEndpoint, use del for MutableHeaders, annotate list[str],
  drop redundant isinstance, collapse implicit string concat.
- api/health.py: ReadinessCheck uses Field(default=None) so basedpyright does
  not treat optional fields as required.
- tests/test_example.py: gate TestCLI behind include_cli==yes so disabling the
  CLI no longer leaves tests importing the removed cli.py.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
…ects gate-clean

Generated projects with API + database selections started red (missing DB
module, no driver, and a pydoclint config stricter than the shipped Google
docstrings). This makes the default generations pass their own gates.

Database:
- New core/database.py (async engine, async_sessionmaker, Base, get_session)
  shipped when include_database != none; removed by post-gen otherwise.
- database_url added to core/config.py with a dialect-correct default URL.
- New 'database' optional-dependency group gated on include_database, with the
  async driver per dialect (asyncpg/aiomysql/aiosqlite) and alembic for the
  migration variants; sqlalchemy/alembic removed from the unconditional api extra.
- health.py readiness check uses SQLAlchemy 2.0 text().
- Minimal async Alembic scaffold (alembic.ini, migrations/env.py,
  script.py.mako, versions/.gitkeep) for sqlalchemy_migrations/_ledger; *.mako
  added to _copy_without_render; migrations/ excluded from ruff/pydoclint/
  interrogate; post-gen removes the scaffold for non-migration choices.

Docstrings / gates:
- Brought all shipped src docstrings into strict pydoclint compliance (typed
  Args/Returns, removed __init__ docstrings in favor of class Args, documented
  class Attributes with types) across config, exceptions, health, logging,
  security, correlation.
- ruff force-exclude=true so excluded dirs are honored under pre-commit.

Verified across 3 combos (no-api/no-db; api+sqlalchemy+sqlite+cli;
api+migrations+postgres): pydoclint 0, ruff clean, basedpyright 0 errors,
tests pass, and pre-commit green except the pre-existing end-of-file-fixer
auto-fix on 3 files.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Copilot AI review requested due to automatic review settings June 21, 2026 06:07
@coderabbitai

coderabbitai Bot commented Jun 21, 2026

Copy link
Copy Markdown

Review Change Stack

Walkthrough

Adds async SQLAlchemy + Alembic migration scaffolding as a conditional cookiecutter feature: new core/database.py, core/config.py database_url field, alembic.ini, migrations/env.py, migrations/script.py.mako, conditional alembic dependency, post-gen hook cleanup of database artifacts, and tooling excludes. Also updates middleware dispatch signatures to use RequestResponseEndpoint and refactors docstrings across exceptions, logging, and middleware modules.

Changes

Database/Alembic template feature

Layer / File(s) Summary
Settings database_url field and async database module
{{cookiecutter.project_slug}}/src/{{cookiecutter.project_slug}}/core/config.py, {{cookiecutter.project_slug}}/src/{{cookiecutter.project_slug}}/core/database.py
Adds a conditional database_url: str field to Settings (dialect-aware async URL for PostgreSQL/MySQL/SQLite) and introduces core/database.py with Base, shared AsyncEngine (pool_pre_ping=True), async_sessionmaker (expire_on_commit=False), get_engine(), and get_session().
Alembic config and migration script template
cookiecutter.json, {{cookiecutter.project_slug}}/alembic.ini, {{cookiecutter.project_slug}}/migrations/env.py, {{cookiecutter.project_slug}}/migrations/script.py.mako
Registers *.mako in _copy_without_render. Adds alembic.ini with blank sqlalchemy.url and logging config. Adds migrations/env.py with offline/online async migration paths driven by settings.database_url and Base.metadata. Adds migrations/script.py.mako revision template with upgrade()/downgrade() stubs.
Conditional dependency, tooling excludes, and post-gen hook cleanup
{{cookiecutter.project_slug}}/pyproject.toml, {{cookiecutter.project_slug}}/.pre-commit-config.yaml, {{cookiecutter.project_slug}}/src/{{cookiecutter.project_slug}}/api/health.py, hooks/post_gen_project.py
Makes alembic>=1.12.0 conditional on sqlalchemy_migrations/sqlalchemy_ledger modes. Adds migrations to Ruff/interrogate/pydoclint/pre-commit excludes. Fixes health.py to use text("SELECT 1"). Adds _cleanup_database_files() to remove database.py, alembic.ini, and migrations/ when not applicable.
CLI test class conditional gating
{{cookiecutter.project_slug}}/tests/test_example.py
Wraps TestCLI with {% if cookiecutter.include_cli == "yes" %} / {% endif %} to omit it when CLI generation is disabled.

Middleware type annotations and docstring polish

Layer / File(s) Summary
RequestResponseEndpoint typing and SSRF/header fixes
{{cookiecutter.project_slug}}/src/{{cookiecutter.project_slug}}/middleware/correlation.py, {{cookiecutter.project_slug}}/src/{{cookiecutter.project_slug}}/middleware/security.py
Adds RequestResponseEndpoint imports to both middleware modules and types all dispatch call_next parameters. Changes Server header removal to explicit check+delete. Removes isinstance(value, str) guard in SSRF query-param URL detection.
Docstring restructuring in exceptions and logging
{{cookiecutter.project_slug}}/src/{{cookiecutter.project_slug}}/core/exceptions.py, {{cookiecutter.project_slug}}/src/{{cookiecutter.project_slug}}/utils/logging.py
Reformats all exception class docstrings to use typed Args/Attributes sections and moves inline __init__ docstrings to class level. Reformats setup_logging, get_logger, and log_performance with explicit typed parameter descriptions.

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~25 minutes

Possibly related PRs

Suggested labels

template, hooks, new-feature

🐇 A database sprang up with a hop and a skip,
Migrations flew in on an Alembic trip!
Base, AsyncEngine, sessions galore —
The rabbit baked templates, then added still more.
Now *.mako won't render astray,
And docstrings are tidy — hip-hip-hooray! 🌿

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 76.09% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The PR title concisely captures the main changes: fixing template issues by integrating the async database layer with Alembic migrations and ensuring generated projects pass quality gates.
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.

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

✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch fix/template-generation-defects

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

❤️ Share

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

Comment thread hooks/post_gen_project.py
def _cleanup_database_files() -> None:
"""Remove database and migration files based on cookiecutter choices."""
# No database selected: drop the engine/session module entirely.
if "{{ cookiecutter.include_database }}" == "none":
Comment thread hooks/post_gen_project.py
def _cleanup_database_files() -> None:
"""Remove database and migration files based on cookiecutter choices."""
# No database selected: drop the engine/session module entirely.
if "{{ cookiecutter.include_database }}" == "none":
Comment thread hooks/post_gen_project.py
Comment on lines +327 to +330
if "{{ cookiecutter.include_database }}" not in (
"sqlalchemy_migrations",
"sqlalchemy_ledger",
):
Comment thread hooks/post_gen_project.py
Comment on lines +327 to +330
if "{{ cookiecutter.include_database }}" not in (
"sqlalchemy_migrations",
"sqlalchemy_ledger",
):

Copilot AI 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.

Pull request overview

This PR updates the cookiecutter template so generated projects (especially API + DB combinations) include a properly wired async SQLAlchemy layer and (optionally) an Alembic scaffold, while also tightening template output to pass the repo’s “quality gates” (ruff/pydoclint/pyright/tests) across common flag combinations.

Changes:

  • Adds an async SQLAlchemy database module and threads a dialect-correct database_url through settings, with optional-dependency wiring for drivers + Alembic.
  • Introduces an Alembic migration scaffold that is conditionally kept/removed by the post-gen hook, and excludes migrations from lint/doc tooling.
  • Brings multiple docstrings and middleware type hints into strict pydoclint compliance and gates CLI tests behind include_cli.

Reviewed changes

Copilot reviewed 15 out of 16 changed files in this pull request and generated 1 comment.

Show a summary per file
File Description
hooks/post_gen_project.py Adds post-gen cleanup for DB/migrations scaffolding based on cookiecutter flags.
cookiecutter.json Adds *.mako to _copy_without_render for Alembic templates.
{{cookiecutter.project_slug}}/tests/test_example.py Wraps TestCLI in a cookiecutter conditional so tests don’t import removed CLI code.
{{cookiecutter.project_slug}}/src/{{cookiecutter.project_slug}}/utils/logging.py Docstring tightening for pydoclint (but see stored bug comment).
{{cookiecutter.project_slug}}/src/{{cookiecutter.project_slug}}/middleware/security.py Type-hints middleware endpoints more strictly; adjusts header deletion semantics.
{{cookiecutter.project_slug}}/src/{{cookiecutter.project_slug}}/middleware/correlation.py Tightens typing and docstrings for correlation middleware utilities.
{{cookiecutter.project_slug}}/src/{{cookiecutter.project_slug}}/core/exceptions.py Updates docstrings to strict pydoclint format across exception hierarchy.
{{cookiecutter.project_slug}}/src/{{cookiecutter.project_slug}}/core/database.py Introduces async engine/session plumbing for DB-enabled generations.
{{cookiecutter.project_slug}}/src/{{cookiecutter.project_slug}}/core/config.py Adds conditional database_url setting with dialect-specific defaults.
{{cookiecutter.project_slug}}/src/{{cookiecutter.project_slug}}/api/health.py Uses SQLAlchemy text() for the readiness DB probe.
{{cookiecutter.project_slug}}/pyproject.toml Moves DB deps into a database optional group; excludes migrations from tooling; sets ruff.force-exclude=true.
{{cookiecutter.project_slug}}/migrations/versions/.gitkeep Ensures versions directory exists for Alembic scaffold.
{{cookiecutter.project_slug}}/migrations/script.py.mako Adds Alembic revision template.
{{cookiecutter.project_slug}}/migrations/env.py Adds async Alembic env wired to app settings + async engine.
{{cookiecutter.project_slug}}/alembic.ini Adds Alembic config scaffold (URL injected at runtime).
{{cookiecutter.project_slug}}/.pre-commit-config.yaml Excludes migrations from pydoclint hook runs.

Comment on lines 49 to 55
Args:
level: Logging level (DEBUG, INFO, WARNING, ERROR, CRITICAL).
level (str): Logging level (DEBUG, INFO, WARNING, ERROR, CRITICAL).
Defaults to INFO.
json_logs: If True, output JSON logs (production mode). If False,
json_logs (bool): If True, output JSON logs (production mode). If False,
use rich console formatting (development mode). Defaults to False.
include_timestamp: Whether to include timestamps in log output.
include_timestamp (bool): Whether to include timestamps in log output.
Defaults to True.

@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: 8

🤖 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 @{{cookiecutter.project_slug}}/migrations/env.py:
- Around line 24-26: Add RAD (Risky Assumptions Documentation) tags to the
migration environment setup in env.py to document critical assumptions that
could cause production failures. Before or near the lines where
config.set_main_option is called with settings.database_url and where
target_metadata is assigned from Base.metadata, add comments that clearly tag
the three assumptions: that settings.database_url is valid and accessible at
import time, that Base.metadata contains all models because all model modules
have been imported, and that the async database driver is installed and
compatible with the configuration.
- Around line 52-57: In the run_migrations_online() function, remove the await
connectable.dispose() call at the end. The engine returned by get_engine() is a
shared singleton from core.database, and disposing it closes the connection pool
globally, breaking the application's ability to perform database operations
after migrations run. The async context manager for connectable.connect()
already handles resource cleanup automatically, so explicit disposal is
unnecessary and harmful.

In
@{{cookiecutter.project_slug}}/src/{{cookiecutter.project_slug}}/api/health.py:
- Around line 92-103: The check_database function makes several assumptions
about database availability that could cause production failures if not met, but
these assumptions are not documented with RAD tags. Add RAD tags to the
check_database function to document the assumptions that: the core.database
module exists (which is only true when include_database is not set to "none"),
the database session can be successfully created, and the database URL is valid.
These tags should be placed at the beginning of the function to document the
contract and help with debugging if assumptions are violated.

In
@{{cookiecutter.project_slug}}/src/{{cookiecutter.project_slug}}/core/config.py:
- Around line 36-48: The database_url variable assignments for the different
database dialects (postgresql, mysql, sqlite) contain production-impacting
assumptions such as hardcoded localhost addresses, default credentials, and
local file paths, but are missing the required assumption annotation tags per
coding guidelines. Add inline `#ASSUME` or `#CRITICAL` tags to each database_url
assignment to explicitly document these development-specific assumptions and
make the production risks reviewable.

In
@{{cookiecutter.project_slug}}/src/{{cookiecutter.project_slug}}/core/database.py:
- Around line 50-57: The get_session() function directly returns
_session_factory() without error handling, which defers any engine
initialization failures until the session is actually used, making debugging
harder. Wrap the _session_factory() call in a try-except block to catch any
exceptions that may occur due to misconfiguration or uninitialized engine, and
raise or log a clear error message that indicates the database engine may not
have been properly initialized, providing better error context at the point of
session creation rather than later during session usage.
- Around line 30-33: The module-level _engine initialization using
create_async_engine() contains critical assumptions that could cause unclear
import-time failures but lacks documentation. Add RAD
(Risk/Assumption/Dependency) tags as comments above or within the _engine
creation block to explicitly document the three critical assumptions: that
settings.database_url is valid and accessible at import time, that the
configured async database driver (asyncpg/aiomysql/aiosqlite) is installed, and
that the URL format matches the expected dialect. These tags should clearly
indicate that if any assumption fails, the entire module import will fail with
an unclear exception, helping future maintainers understand the critical nature
of this initialization.

In
@{{cookiecutter.project_slug}}/src/{{cookiecutter.project_slug}}/middleware/security.py:
- Around line 413-414: The SSRF validation logic in the middleware is
incomplete. While the docstring claims to validate query parameters, form data,
and JSON body, the actual implementation only scans query parameters (as
indicated in lines 425-427). Extend the validation logic to also inspect form
data and JSON request bodies for potential SSRF attempts targeting internal
resources, ensuring that URL-bearing fields in all three input sources are
properly scanned before allowing the request to proceed.

In `@hooks/post_gen_project.py`:
- Around line 327-330: The templated variable include_database is being used
directly in the membership check at lines 327-330, causing static analysis
failures. Extract the templated include_database variable into a normalized
constant at the module level (following the pattern of other include flags),
then replace the direct template string "{{ cookiecutter.include_database }}" in
the conditional statement with the new constant. Apply the same refactoring to
the similar comparison at line 323 to maintain consistency throughout the file.
🪄 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: ASSERTIVE

Plan: Pro

Run ID: 68458b08-ce48-4398-a335-b9e9a1a001a3

📥 Commits

Reviewing files that changed from the base of the PR and between 7543036 and 498451a.

📒 Files selected for processing (16)
  • cookiecutter.json
  • hooks/post_gen_project.py
  • {{cookiecutter.project_slug}}/.pre-commit-config.yaml
  • {{cookiecutter.project_slug}}/alembic.ini
  • {{cookiecutter.project_slug}}/migrations/env.py
  • {{cookiecutter.project_slug}}/migrations/script.py.mako
  • {{cookiecutter.project_slug}}/migrations/versions/.gitkeep
  • {{cookiecutter.project_slug}}/pyproject.toml
  • {{cookiecutter.project_slug}}/src/{{cookiecutter.project_slug}}/api/health.py
  • {{cookiecutter.project_slug}}/src/{{cookiecutter.project_slug}}/core/config.py
  • {{cookiecutter.project_slug}}/src/{{cookiecutter.project_slug}}/core/database.py
  • {{cookiecutter.project_slug}}/src/{{cookiecutter.project_slug}}/core/exceptions.py
  • {{cookiecutter.project_slug}}/src/{{cookiecutter.project_slug}}/middleware/correlation.py
  • {{cookiecutter.project_slug}}/src/{{cookiecutter.project_slug}}/middleware/security.py
  • {{cookiecutter.project_slug}}/src/{{cookiecutter.project_slug}}/utils/logging.py
  • {{cookiecutter.project_slug}}/tests/test_example.py

Comment on lines +24 to +26
# Inject the application's database URL and model metadata.
config.set_main_option("sqlalchemy.url", settings.database_url)
target_metadata = Base.metadata

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Add RAD tags for critical assumptions in migration environment.

Per coding guidelines, tag assumptions that could cause production failures. The migration environment assumes:

  • settings.database_url is valid and accessible at import time
  • Base.metadata contains all models (all model modules have been imported)
  • The async database driver is installed and compatible
🏷️ Proposed RAD tag additions
 # Inject the application's database URL and model metadata.
+# `#CRITICAL`: configuration: settings.database_url must be valid at migration time
+# `#CRITICAL`: metadata: Base.metadata must include all ORM models
 config.set_main_option("sqlalchemy.url", settings.database_url)
 target_metadata = Base.metadata
🤖 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 @{{cookiecutter.project_slug}}/migrations/env.py around lines 24 - 26, Add
RAD (Risky Assumptions Documentation) tags to the migration environment setup in
env.py to document critical assumptions that could cause production failures.
Before or near the lines where config.set_main_option is called with
settings.database_url and where target_metadata is assigned from Base.metadata,
add comments that clearly tag the three assumptions: that settings.database_url
is valid and accessible at import time, that Base.metadata contains all models
because all model modules have been imported, and that the async database driver
is installed and compatible with the configuration.

Source: Coding guidelines

Comment on lines +52 to +57
async def run_migrations_online() -> None:
"""Run migrations in 'online' mode using the application's async engine."""
connectable = get_engine()
async with connectable.connect() as connection:
await connection.run_sync(do_run_migrations)
await connectable.dispose()

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🔴 Critical | ⚡ Quick win

Do not dispose the shared application engine after migrations.

get_engine() returns the singleton _engine from core.database, which is shared across the entire application. Calling await connectable.dispose() at line 57 closes all pooled connections and renders the engine unusable. If the application attempts database operations after running migrations programmatically, it will fail with connection errors.

🛡️ Recommended fix: remove engine disposal
 async def run_migrations_online() -> None:
     """Run migrations in 'online' mode using the application's async engine."""
     connectable = get_engine()
     async with connectable.connect() as connection:
         await connection.run_sync(do_run_migrations)
-    await connectable.dispose()

The connection context manager handles cleanup automatically. The engine remains available for subsequent application use.

📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
async def run_migrations_online() -> None:
"""Run migrations in 'online' mode using the application's async engine."""
connectable = get_engine()
async with connectable.connect() as connection:
await connection.run_sync(do_run_migrations)
await connectable.dispose()
async def run_migrations_online() -> None:
"""Run migrations in 'online' mode using the application's async engine."""
connectable = get_engine()
async with connectable.connect() as connection:
await connection.run_sync(do_run_migrations)
🤖 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 @{{cookiecutter.project_slug}}/migrations/env.py around lines 52 - 57, In the
run_migrations_online() function, remove the await connectable.dispose() call at
the end. The engine returned by get_engine() is a shared singleton from
core.database, and disposing it closes the connection pool globally, breaking
the application's ability to perform database operations after migrations run.
The async context manager for connectable.connect() already handles resource
cleanup automatically, so explicit disposal is unnecessary and harmful.

Comment on lines +92 to +103
ReadinessCheck: Database status and measured latency.
"""
start = time.time()
try:
# Import here to avoid circular dependencies
from sqlalchemy import text

from {{ cookiecutter.project_slug }}.core.database import get_session

async with get_session() as session:
# Simple query to check connectivity
await session.execute("SELECT 1")
await session.execute(text("SELECT 1"))

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor | ⚡ Quick win

Add RAD tags for database availability assumption.

Per coding guidelines, tag assumptions that could cause production failures. The check_database function assumes:

  • The core.database module exists (only true when include_database != "none")
  • The database session can be created
  • The database URL is valid

While template conditionals should prevent this code from being generated without database support, tagging the assumption documents the contract.

🏷️ Proposed RAD tag addition
     start = time.time()
     try:
+        # `#CRITICAL`: dependencies: core.database module must be available
         # Import here to avoid circular dependencies
         from sqlalchemy import text
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
ReadinessCheck: Database status and measured latency.
"""
start = time.time()
try:
# Import here to avoid circular dependencies
from sqlalchemy import text
from {{ cookiecutter.project_slug }}.core.database import get_session
async with get_session() as session:
# Simple query to check connectivity
await session.execute("SELECT 1")
await session.execute(text("SELECT 1"))
ReadinessCheck: Database status and measured latency.
"""
start = time.time()
try:
# `#CRITICAL`: dependencies: core.database module must be available
# Import here to avoid circular dependencies
from sqlalchemy import text
from {{ cookiecutter.project_slug }}.core.database import get_session
async with get_session() as session:
# Simple query to check connectivity
await session.execute(text("SELECT 1"))
🤖 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
@{{cookiecutter.project_slug}}/src/{{cookiecutter.project_slug}}/api/health.py
around lines 92 - 103, The check_database function makes several assumptions
about database availability that could cause production failures if not met, but
these assumptions are not documented with RAD tags. Add RAD tags to the
check_database function to document the assumptions that: the core.database
module exists (which is only true when include_database is not set to "none"),
the database session can be successfully created, and the database URL is valid.
These tags should be placed at the beginning of the function to document the
contract and help with debugging if assumptions are violated.

Source: Coding guidelines

Comment on lines +36 to +48
{%- if cookiecutter.include_database != "none" %}
{%- if cookiecutter.database_dialect == "postgresql" %}
database_url: str = (
"postgresql+asyncpg://postgres:postgres@localhost:5432/{{ cookiecutter.project_slug }}"
)
{%- elif cookiecutter.database_dialect == "mysql" %}
database_url: str = (
"mysql+aiomysql://root:root@localhost:3306/{{ cookiecutter.project_slug }}"
)
{%- else %}
database_url: str = "sqlite+aiosqlite:///./{{ cookiecutter.project_slug }}.db"
{%- endif %}
{%- endif %}

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor | ⚡ Quick win

Add required assumption tags for dialect-specific default DB URLs.

These defaults encode production-impacting assumptions (localhost, default creds, local file DB) but the required assumption annotations are missing. Please tag them inline per the repo rule so risk is explicit and reviewable.

Suggested patch
 {%- if cookiecutter.include_database != "none" %}
 {%- if cookiecutter.database_dialect == "postgresql" %}
+    `#CRITICAL`: configuration: assumes local PostgreSQL with default credentials;
+    `#VERIFY`: require environment override for production deployments
     database_url: str = (
         "postgresql+asyncpg://postgres:postgres@localhost:5432/{{ cookiecutter.project_slug }}"
     )
 {%- elif cookiecutter.database_dialect == "mysql" %}
+    `#CRITICAL`: configuration: assumes local MySQL with default credentials;
+    `#VERIFY`: require environment override for production deployments
     database_url: str = (
         "mysql+aiomysql://root:root@localhost:3306/{{ cookiecutter.project_slug }}"
     )
 {%- else %}
+    `#ASSUME`: configuration: assumes writable local filesystem path for SQLite
+    `#VERIFY`: validate deployment path/permissions or override via env
     database_url: str = "sqlite+aiosqlite:///./{{ cookiecutter.project_slug }}.db"
 {%- endif %}
 {%- endif %}

As per coding guidelines, "**/*.py: Tag assumptions that could cause production failures ... Tag non-critical assumptions ... Tag edge cases ... with corresponding #CRITICAL/#ASSUME/#VERIFY/#EDGE comments."

🤖 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
@{{cookiecutter.project_slug}}/src/{{cookiecutter.project_slug}}/core/config.py
around lines 36 - 48, The database_url variable assignments for the different
database dialects (postgresql, mysql, sqlite) contain production-impacting
assumptions such as hardcoded localhost addresses, default credentials, and
local file paths, but are missing the required assumption annotation tags per
coding guidelines. Add inline `#ASSUME` or `#CRITICAL` tags to each database_url
assignment to explicitly document these development-specific assumptions and
make the production risks reviewable.

Source: Coding guidelines

Comment on lines +30 to +33
_engine: AsyncEngine = create_async_engine(
settings.database_url,
pool_pre_ping=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.

⚠️ Potential issue | 🟠 Major | 🏗️ Heavy lift

Add RAD tags for critical assumptions in engine initialization.

Per coding guidelines, tag critical assumptions that could cause production failures. The module-level engine creation assumes:

  • settings.database_url is valid and accessible at import time
  • The configured async driver (asyncpg/aiomysql/aiosqlite) is installed
  • The URL format matches the expected dialect

If any assumption fails, the import will raise an unclear exception before the application starts.

🏷️ Proposed RAD tag additions
+# `#CRITICAL`: initialization: settings.database_url must be valid at import time
+# `#CRITICAL`: dependencies: async driver (asyncpg/aiomysql/aiosqlite) must be installed
 _engine: AsyncEngine = create_async_engine(
     settings.database_url,
     pool_pre_ping=True,
 )
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
_engine: AsyncEngine = create_async_engine(
settings.database_url,
pool_pre_ping=True,
)
# `#CRITICAL`: initialization: settings.database_url must be valid at import time
# `#CRITICAL`: dependencies: async driver (asyncpg/aiomysql/aiosqlite) must be installed
_engine: AsyncEngine = create_async_engine(
settings.database_url,
pool_pre_ping=True,
)
🤖 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
@{{cookiecutter.project_slug}}/src/{{cookiecutter.project_slug}}/core/database.py
around lines 30 - 33, The module-level _engine initialization using
create_async_engine() contains critical assumptions that could cause unclear
import-time failures but lacks documentation. Add RAD
(Risk/Assumption/Dependency) tags as comments above or within the _engine
creation block to explicitly document the three critical assumptions: that
settings.database_url is valid and accessible at import time, that the
configured async database driver (asyncpg/aiomysql/aiosqlite) is installed, and
that the URL format matches the expected dialect. These tags should clearly
indicate that if any assumption fails, the entire module import will fail with
an unclear exception, helping future maintainers understand the critical nature
of this initialization.

Source: Coding guidelines

Comment on lines +50 to +57
def get_session() -> AsyncSession:
"""Return a new async session for use as an async context manager.

Returns:
AsyncSession: A new session bound to the shared engine. Use it as
``async with get_session() as session: ...``.
"""
return _session_factory()

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🧹 Nitpick | 🔵 Trivial | ⚡ Quick win

Consider wrapping session factory call for better error context.

get_session() directly returns _session_factory(), which will fail at session usage time if the engine was never properly initialized. While the docstring correctly instructs users to use it as a context manager, adding a minimal try-except could provide clearer error messages for misconfiguration scenarios.

♻️ Optional defensive wrapper
 def get_session() -> AsyncSession:
     """Return a new async session for use as an async context manager.

     Returns:
         AsyncSession: A new session bound to the shared engine. Use it as
             ``async with get_session() as session: ...``.
     """
+    # `#VERIFY`: defensive code required
     return _session_factory()
🤖 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
@{{cookiecutter.project_slug}}/src/{{cookiecutter.project_slug}}/core/database.py
around lines 50 - 57, The get_session() function directly returns
_session_factory() without error handling, which defers any engine
initialization failures until the session is actually used, making debugging
harder. Wrap the _session_factory() call in a try-except block to catch any
exceptions that may occur due to misconfiguration or uninitialized engine, and
raise or log a clear error message that indicates the database engine may not
have been properly initialized, providing better error context at the point of
session creation rather than later during session usage.

Comment on lines 413 to 414
Validates query parameters, form data, and JSON body for potential
SSRF attempts targeting internal resources.

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major | 🏗️ Heavy lift

SSRF protection is bypassable for JSON/form payloads.

Lines 413-414 state form/JSON are validated, but Lines 425-427 only scan query parameters. URL-bearing fields in JSON or form bodies will bypass this middleware.

Suggested direction
 async def dispatch(
     self, request: Request, call_next: RequestResponseEndpoint
 ) -> Response:
@@
-    # Check query parameters for URLs
+    # Check query parameters for URLs
     for param, value in request.query_params.items():
         if "://" in value or value.startswith("//"):
             if self._is_blocked_url(value):
                 return JSONResponse(...)
+
+    # Also inspect JSON and form payloads when present.
+    # Ensure body is buffered/replayed so downstream handlers still receive it.
+    # (Implementation should preserve request stream semantics.)

Also applies to: 425-427

🤖 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
@{{cookiecutter.project_slug}}/src/{{cookiecutter.project_slug}}/middleware/security.py
around lines 413 - 414, The SSRF validation logic in the middleware is
incomplete. While the docstring claims to validate query parameters, form data,
and JSON body, the actual implementation only scans query parameters (as
indicated in lines 425-427). Extend the validation logic to also inspect form
data and JSON request bodies for potential SSRF attempts targeting internal
resources, ensuring that URL-bearing fields in all three input sources are
properly scanned before allowing the request to proceed.

Comment thread hooks/post_gen_project.py
Comment on lines +327 to +330
if "{{ cookiecutter.include_database }}" not in (
"sqlalchemy_migrations",
"sqlalchemy_ledger",
):

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# Verify whether include-database checks are still done via direct templated literals
# (expected after fix: no matches in hooks/post_gen_project.py)
rg -n 'if\s+"\{\{\s*cookiecutter\.include_database\s*\}\}"\s*(==|not in|in)' hooks/post_gen_project.py

# Verify normalized constant is defined and used
rg -n 'INCLUDE_DATABASE' hooks/post_gen_project.py

Repository: ByronWilliamsCPA/cookiecutter-python-template

Length of output: 207


Normalize templated variables to fix static analysis failures on membership checks.

Lines 323 and 327 compare templated literals directly, causing static analysis to flag these as always-true conditions and fail CI. Extract include_database into a normalized constant at module level, then use that constant in comparisons—following the same pattern as other include flags.

Suggested patch
+INCLUDE_DATABASE = "{{ cookiecutter.include_database }}".strip().lower()
+
 def _cleanup_database_files() -> None:
     """Remove database and migration files based on cookiecutter choices."""
     # No database selected: drop the engine/session module entirely.
-    if "{{ cookiecutter.include_database }}" == "none":
+    if INCLUDE_DATABASE == "none":
         remove_file(Path("src/{{ cookiecutter.project_slug }}/core/database.py"))
 
     # Alembic only ships with migration-capable database choices.
-    if "{{ cookiecutter.include_database }}" not in (
+    if INCLUDE_DATABASE not in (
         "sqlalchemy_migrations",
         "sqlalchemy_ledger",
     ):
         remove_file(Path("alembic.ini"))
         remove_dir(Path("migrations"))
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
if "{{ cookiecutter.include_database }}" not in (
"sqlalchemy_migrations",
"sqlalchemy_ledger",
):
INCLUDE_DATABASE = "{{ cookiecutter.include_database }}".strip().lower()
def _cleanup_database_files() -> None:
"""Remove database and migration files based on cookiecutter choices."""
# No database selected: drop the engine/session module entirely.
if INCLUDE_DATABASE == "none":
remove_file(Path("src/{{ cookiecutter.project_slug }}/core/database.py"))
# Alembic only ships with migration-capable database choices.
if INCLUDE_DATABASE not in (
"sqlalchemy_migrations",
"sqlalchemy_ledger",
):
remove_file(Path("alembic.ini"))
remove_dir(Path("migrations"))
🧰 Tools
🪛 GitHub Actions: Validate Template / 0_validate (3.14).txt

[error] 327-327: basedpyright: Expression will always evaluate to True since the types "Literal['{{ cookiecutter.include_database }}']" and "Literal['sqlalchemy_migrations', 'sqlalchemy_ledger']" have no overlap (reportUnnecessaryContains)

🪛 GitHub Actions: Validate Template / 1_validate (3.11).txt

[error] 327-327: basedpyright: Expression will always evaluate to True since the types "Literal['{{ cookiecutter.include_database }}']" and "Literal['sqlalchemy_migrations', 'sqlalchemy_ledger']" have no overlap (reportUnnecessaryContains)

🪛 GitHub Actions: Validate Template / 4_validate (3.10).txt

[error] 327-327: basedpyright: Expression will always evaluate to True since the types "Literal['{{ cookiecutter.include_database }}']" and "Literal['sqlalchemy_migrations', 'sqlalchemy_ledger']" have no overlap (reportUnnecessaryContains)

🪛 GitHub Actions: Validate Template / validate (3.10)

[error] 327-327: basedpyright: Expression will always evaluate to True since the types "Literal['{{ cookiecutter.include_database }}']" and "Literal['sqlalchemy_migrations', 'sqlalchemy_ledger']" have no overlap (reportUnnecessaryContains).

🪛 GitHub Actions: Validate Template / validate (3.11)

[error] 327-327: basedpyright: Expression will always evaluate to True since the types "Literal['{{ cookiecutter.include_database }}']" and "Literal['sqlalchemy_migrations', 'sqlalchemy_ledger']" have no overlap (reportUnnecessaryContains)

🪛 GitHub Actions: Validate Template / validate (3.14)

[error] 327-327: basedpyright: Expression will always evaluate to True since the types "Literal['{{ cookiecutter.include_database }}']" and "Literal['sqlalchemy_migrations', 'sqlalchemy_ledger']" have no overlap (reportUnnecessaryContains).

🪛 GitHub Check: Code Quality Checks

[failure] 327-327:
Expression will always evaluate to True since the types "Literal['{{ cookiecutter.include_database }}']" and "Literal['sqlalchemy_migrations', 'sqlalchemy_ledger']" have no overlap (reportUnnecessaryContains)

🪛 GitHub Check: CodeQL

[warning] 327-330: Constant in conditional expression or statement
Testing a constant will always give the same result.


[warning] 327-330: Comparison of constants
Comparison of constants; use 'True' or 'False' instead.

🪛 GitHub Check: Test Hook Files

[failure] 327-327:
Expression will always evaluate to True since the types "Literal['{{ cookiecutter.include_database }}']" and "Literal['sqlalchemy_migrations', 'sqlalchemy_ledger']" have no overlap (reportUnnecessaryContains)

🪛 GitHub Check: validate (3.10)

[failure] 327-327:
Expression will always evaluate to True since the types "Literal['{{ cookiecutter.include_database }}']" and "Literal['sqlalchemy_migrations', 'sqlalchemy_ledger']" have no overlap (reportUnnecessaryContains)

🪛 GitHub Check: validate (3.11)

[failure] 327-327:
Expression will always evaluate to True since the types "Literal['{{ cookiecutter.include_database }}']" and "Literal['sqlalchemy_migrations', 'sqlalchemy_ledger']" have no overlap (reportUnnecessaryContains)

🪛 GitHub Check: validate (3.14)

[failure] 327-327:
Expression will always evaluate to True since the types "Literal['{{ cookiecutter.include_database }}']" and "Literal['sqlalchemy_migrations', 'sqlalchemy_ledger']" have no overlap (reportUnnecessaryContains)

🤖 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 `@hooks/post_gen_project.py` around lines 327 - 330, The templated variable
include_database is being used directly in the membership check at lines
327-330, causing static analysis failures. Extract the templated
include_database variable into a normalized constant at the module level
(following the pattern of other include flags), then replace the direct template
string "{{ cookiecutter.include_database }}" in the conditional statement with
the new constant. Apply the same refactoring to the similar comparison at line
323 to maintain consistency throughout the file.

Source: Linters/SAST tools

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

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants