diff --git a/.github/workflows/smoke-test.yml b/.github/workflows/smoke-test.yml index e34288c..5576313 100644 --- a/.github/workflows/smoke-test.yml +++ b/.github/workflows/smoke-test.yml @@ -5,7 +5,10 @@ on: push: branches: [main] schedule: - - cron: "0 0 * * 0" + - cron: "0 0 * * *" + repository_dispatch: + types: [upstream-released] + workflow_dispatch: concurrency: group: smoke-tests-${{ github.workflow }}-${{ github.ref }} @@ -14,20 +17,22 @@ concurrency: jobs: verify: runs-on: ubuntu-latest - services: - cubrid: - image: cubrid/cubrid:11.2 - env: - CUBRID_DB: testdb - ports: - - 33000:33000 - options: >- - --health-cmd "csql -u dba testdb -c 'SELECT 1'" - --health-interval 15s - --health-timeout 10s - --health-retries 10 - --health-start-period 30s steps: + - name: Report trigger + env: + # Pass untrusted repository_dispatch payload through the environment + # instead of interpolating it directly into the shell, which would be + # a command-injection vector. + EVENT_NAME: ${{ github.event_name }} + UPSTREAM_PACKAGE: ${{ github.event.client_payload.package }} + UPSTREAM_REF: ${{ github.event.client_payload.ref }} + run: | + printf 'Event: %s\n' "$EVENT_NAME" + if [ "$EVENT_NAME" = "repository_dispatch" ]; then + printf 'Triggered by upstream release: %s %s\n' "$UPSTREAM_PACKAGE" "$UPSTREAM_REF" + printf '### Triggered by upstream release: `%s %s`\n' "$UPSTREAM_PACKAGE" "$UPSTREAM_REF" >> "$GITHUB_STEP_SUMMARY" + fi + - name: Checkout uses: actions/checkout@v4 @@ -37,30 +42,94 @@ jobs: python-version: "3.12" - name: Install dependencies - run: pip install pycubrid sqlalchemy sqlalchemy-cubrid + run: | + pip install pycubrid sqlalchemy sqlalchemy-cubrid + # Extra deps used by templates (e.g. batch-etl). Installed separately + # so the released pycubrid/sqlalchemy-cubrid above are not overridden + # by the git-pinned versions in the template requirements files. + pip install pandas matplotlib + # cubrid-mcp-server publishes to PyPI on release; fall back to git + # until the first PyPI release is available. + pip install cubrid-mcp-server \ + || pip install "git+https://github.com/cubrid-lab/cubrid-mcp-server.git" + + - name: Record tested versions + run: | + { + echo "### Tested upstream versions"; + echo ""; + echo "| Package | Version |"; + echo "| --- | --- |"; + for pkg in pycubrid sqlalchemy-cubrid cubrid-mcp-server; do + ver=$(pip show "$pkg" | awk -F': ' '/^Version:/ {print $2}'); + echo "| $pkg | ${ver:-not installed} |"; + done + } | tee -a "$GITHUB_STEP_SUMMARY" + + - name: Start CUBRID container + run: | + docker run -d --name cubrid --shm-size 512m \ + -e CUBRID_DB=testdb \ + -p 33000:33000 -p 1523:1523 \ + cubrid/cubrid:11.2 - name: Wait for CUBRID readiness run: | - for i in $(seq 1 30); do - if python -c " - from pycubrid import connect - try: - conn = connect('CUBRID:localhost:33000:testdb:::', user='dba', password='') - cur = conn.cursor() - cur.execute('SELECT 1') - cur.close() - conn.close() - print('CUBRID ready') - exit(0) - except Exception as e: - print(f'Not ready: {e}') - exit(1) - "; then - break + # Use the in-container csql check (same as docker-compose healthcheck) + # rather than a client connect; the broker accepts external TCP + # connections slightly after the engine itself is ready. + for i in $(seq 1 60); do + if docker exec cubrid bash -lc "csql -u dba testdb -c 'SELECT 1;'" >/dev/null 2>&1; then + echo "CUBRID testdb is ready (attempt $i)" + exit 0 fi - echo "Waiting... ($i/30)" + echo "Waiting for CUBRID testdb... attempt $i/60" sleep 5 done + echo "CUBRID testdb did not become ready in time" + docker logs cubrid || true + exit 1 + + - name: Verify external client connectivity + run: | + # The csql check only proves the in-container engine is up; the broker + # accepts external TCP on the mapped port slightly later. Confirm a real + # pycubrid client (the same path the examples use) can connect before + # running make verify, otherwise the first example races the broker. + python - <<'PY' + import sys + import time + + import pycubrid + + last_error = None + for attempt in range(1, 31): + try: + conn = pycubrid.connect( + host="localhost", + port=33000, + database="testdb", + user="dba", + password="", + ) + cursor = conn.cursor() + cursor.execute("SELECT 1") + cursor.fetchone() + cursor.close() + conn.close() + print(f"External pycubrid connection OK (attempt {attempt})") + sys.exit(0) + except Exception as error: + last_error = error + print(f"Waiting for external broker... attempt {attempt}/30: {error}") + time.sleep(3) + + print(f"External broker not reachable in time: {last_error}") + sys.exit(1) + PY - name: Run make verify run: make verify + + - name: Smoke test cubrid-mcp-server + run: python -c "import cubrid_mcp_server; print('cubrid-mcp-server import OK')" diff --git a/README.md b/README.md index 0d2510d..31187e0 100644 --- a/README.md +++ b/README.md @@ -113,10 +113,12 @@ All examples connect to the same CUBRID instance: ```python # pycubrid (direct) import pycubrid + conn = pycubrid.connect(host="localhost", port=33000, database="testdb", user="dba") # SQLAlchemy from sqlalchemy import create_engine + engine = create_engine("cubrid+pycubrid://dba@localhost:33000/testdb") ``` diff --git a/fundamentals/lob-handling/expected/06_lob.expected b/fundamentals/lob-handling/expected/06_lob.expected index 1d23e35..9a072d3 100644 --- a/fundamentals/lob-handling/expected/06_lob.expected +++ b/fundamentals/lob-handling/expected/06_lob.expected @@ -1,11 +1,11 @@ === CLOB (Character Large Object) === ✓ Inserted 3 documents with CLOB data - README (64 chars): # CUBRID Cookbook - -A collection of examples for CUBRID datab... - License (46 chars): Apache License 2.0 - -Copyright 2026 cubrid-labs + README (66 chars): # CUBRID Cookbook\ +\ +A collection of examples for CUBRID dat... + License (47 chars): Apache License 2.0\ +\ +Copyright 2026 cubrid-lab Long Text (2,800 chars): Lorem ipsum dolor sit amet. Lorem ipsum dolor sit amet. Lore... === BLOB (Binary Large Object) === diff --git a/fundamentals/orm-basics/expected/06_reflection.expected b/fundamentals/orm-basics/expected/06_reflection.expected index 32f8bb2..9af551b 100644 --- a/fundamentals/orm-basics/expected/06_reflection.expected +++ b/fundamentals/orm-basics/expected/06_reflection.expected @@ -29,10 +29,10 @@ cookbook_ref_articles PK: ['id'] Foreign keys on cookbook_ref_articles: + ['author_id'] → cookbook_ref_authors.['id'] === Reflect Indexes === Indexes on cookbook_ref_articles: - fk_cookbook_ref_articles_author_id columns=['author_id'] idx_articles_views columns=['views'] === Autoload Table === diff --git a/performance/bulk-insert/README.md b/performance/bulk-insert/README.md index bc812ee..818552e 100644 --- a/performance/bulk-insert/README.md +++ b/performance/bulk-insert/README.md @@ -13,6 +13,7 @@ Inserting 10,000 rows with individual COMMITs takes **~470 seconds** (7.8 minute ```python """Reduce transaction overhead by committing in batches.""" + from __future__ import annotations import pycubrid @@ -58,6 +59,7 @@ conn.close() ```python """Insert multiple rows in a single call.""" + from __future__ import annotations import pycubrid @@ -81,6 +83,7 @@ conn.close() ```python """Bulk insert using SQLAlchemy Core (faster than ORM object creation).""" + from __future__ import annotations from sqlalchemy import create_engine diff --git a/performance/bulk-insert/benchmark.py b/performance/bulk-insert/benchmark.py index c84c7ba..1838096 100644 --- a/performance/bulk-insert/benchmark.py +++ b/performance/bulk-insert/benchmark.py @@ -77,10 +77,18 @@ def main() -> None: # Print results print("\n=== Bulk Insert Benchmark ===") - print(f"Strategy 1 (per-row COMMIT): {strategy1_time:.3f}s {per_row_count} rows ({strategy1_rows_sec:.0f} rows/sec)") - print(f"Strategy 2 (batch COMMIT 500): {strategy2_time:.3f}s {num_rows} rows ({strategy2_rows_sec:.0f} rows/sec)") - print(f"Strategy 3 (single COMMIT): {strategy3_time:.3f}s {num_rows} rows ({strategy3_rows_sec:.0f} rows/sec)") - print(f"\nKey insight: COMMIT is ~{strategy1_time / per_row_count * 1000:.0f}ms per call — batch your writes!") + print( + f"Strategy 1 (per-row COMMIT): {strategy1_time:.3f}s {per_row_count} rows ({strategy1_rows_sec:.0f} rows/sec)" + ) + print( + f"Strategy 2 (batch COMMIT 500): {strategy2_time:.3f}s {num_rows} rows ({strategy2_rows_sec:.0f} rows/sec)" + ) + print( + f"Strategy 3 (single COMMIT): {strategy3_time:.3f}s {num_rows} rows ({strategy3_rows_sec:.0f} rows/sec)" + ) + print( + f"\nKey insight: COMMIT is ~{strategy1_time / per_row_count * 1000:.0f}ms per call — batch your writes!" + ) print() finally: diff --git a/performance/connection-pooling/README.md b/performance/connection-pooling/README.md index 895bdfd..3e72404 100644 --- a/performance/connection-pooling/README.md +++ b/performance/connection-pooling/README.md @@ -15,16 +15,17 @@ In a web application creating a new connection per request: ```python """Use SQLAlchemy's built-in connection pool.""" + from __future__ import annotations from sqlalchemy import create_engine, text engine = create_engine( "cubrid+pycubrid://dba@localhost:33000/testdb", - pool_size=5, # Number of connections to keep in the pool - max_overflow=10, # Extra connections allowed beyond pool_size - pool_timeout=30, # Max seconds to wait for a connection - pool_recycle=3600, # Recreate connections after 1 hour (handles CUBRID idle timeout) + pool_size=5, # Number of connections to keep in the pool + max_overflow=10, # Extra connections allowed beyond pool_size + pool_timeout=30, # Max seconds to wait for a connection + pool_recycle=3600, # Recreate connections after 1 hour (handles CUBRID idle timeout) pool_pre_ping=True, # Validate connections before use ) @@ -41,6 +42,7 @@ with engine.connect() as conn: ```python """Per-request session management in FastAPI.""" + from __future__ import annotations from collections.abc import Generator @@ -93,7 +95,7 @@ Set `pool_recycle` shorter than the server timeout to avoid stale connections. # Set pool_recycle well below that (safety margin) engine = create_engine( "cubrid+pycubrid://dba@localhost:33000/testdb", - pool_recycle=3600, # 1 hour — 1/6 of server timeout + pool_recycle=3600, # 1 hour — 1/6 of server timeout pool_pre_ping=True, # Detect broken connections before use ) ``` diff --git a/performance/connection-pooling/benchmark.py b/performance/connection-pooling/benchmark.py index c8ee988..0f7ea79 100644 --- a/performance/connection-pooling/benchmark.py +++ b/performance/connection-pooling/benchmark.py @@ -2,7 +2,7 @@ import time from sqlalchemy import create_engine, text -from sqlalchemy.pool import NullPool, QueuePool +from sqlalchemy.pool import NullPool def main() -> None: diff --git a/performance/fetch-optimization/README.md b/performance/fetch-optimization/README.md index 5113cfb..ed8ede4 100644 --- a/performance/fetch-optimization/README.md +++ b/performance/fetch-optimization/README.md @@ -49,14 +49,15 @@ rows = cursor.fetchall() # Don't fetch 100K rows at once — paginate on the server PAGE_SIZE = 1000 + def fetch_page(cursor, offset: int, limit: int) -> list: cursor.execute( - "SELECT order_id, total_amt FROM cookbook_orders " - "ORDER BY order_id LIMIT ?, ?", + "SELECT order_id, total_amt FROM cookbook_orders ORDER BY order_id LIMIT ?, ?", (offset, limit), ) return cursor.fetchall() + # Usage page = fetch_page(cursor, offset=0, limit=PAGE_SIZE) ``` diff --git a/pitfalls/README.md b/pitfalls/README.md index 9b0119e..5dc1b45 100644 --- a/pitfalls/README.md +++ b/pitfalls/README.md @@ -33,6 +33,7 @@ engine = create_engine( ) SessionLocal = sessionmaker(bind=engine) + @app.get("/items") def list_items(): with SessionLocal() as session: @@ -158,7 +159,7 @@ session.execute(stmt) # ❌ Anti-pattern — sync DB call in async handler blocks the event loop @app.get("/items") async def list_items(): - conn = pycubrid.connect(...) # Blocks event loop + conn = pycubrid.connect(...) # Blocks event loop cursor = conn.cursor() cursor.execute("SELECT * FROM cookbook_items") # Blocks event loop rows = cursor.fetchall() @@ -175,12 +176,14 @@ async def list_items(): def list_items(db: Session = Depends(get_db)): return db.execute(text("SELECT * FROM cookbook_items")).all() + # ✅ Option B — explicit thread pool for async handler import asyncio from concurrent.futures import ThreadPoolExecutor executor = ThreadPoolExecutor(max_workers=5) + @app.get("/items") async def list_items(): loop = asyncio.get_event_loop() diff --git a/pyproject.toml b/pyproject.toml index 7b49ad8..a5c6adf 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,3 +1,19 @@ [tool.ruff] line-length = 100 target-version = "py310" + +[tool.ruff.lint] +# Pin the rule set explicitly instead of inheriting ruff's implicit defaults. +# Ruff expanded its default selection in 0.16 (59 -> 413 rules against this +# config), which turns every routine ruff bump into a CI-breaking change +# unrelated to our own code. These four groups are exactly what ruff selected +# by default through 0.15.x. +select = ["E4", "E7", "E9", "F"] + +[tool.ruff.lint.per-file-ignores] +# Test suites bootstrap sys.path before importing the module under test, +# so their imports are intentionally not at the top of the file. +"**/tests/*.py" = ["E402"] +# Flask template app.py files intentionally place demo imports mid-file to +# illustrate patterns; moving them would change the teaching structure. +"templates/flask/**/app.py" = ["E402"] diff --git a/scripts/normalize_output.sh b/scripts/normalize_output.sh index cab1aaa..2258be3 100755 --- a/scripts/normalize_output.sh +++ b/scripts/normalize_output.sh @@ -27,4 +27,5 @@ sed -E \ -e 's/[0-9]{4}-[0-9]{2}-[0-9]{2} [0-9]{2}:[0-9]{2}:[0-9]{2},[0-9]+/{{TIMESTAMP}}/g' \ -e 's/[{][{]DATE[}][}] [0-9]{2}:[0-9]{2}:[0-9]{2},[0-9]+/{{TIMESTAMP}}/g' \ -e 's/[{][{]DATE[}][}]T[0-9]{2}:[0-9]{2}:[0-9]{2}\.[0-9]+/{{DATETIME}}/g' \ - -e 's/\[generated in [0-9.]+s]/[generated in {{TIME}}s]/g' + -e 's/\[generated in [0-9.]+s]/[generated in {{TIME}}s]/g' \ + -e "s/ \(errno=-?[0-9]+, description='[^']*', sqlstate='[^']*'\)//g" diff --git a/templates/api-service-fastapi/app/models.py b/templates/api-service-fastapi/app/models.py index e0451ae..4cfd884 100644 --- a/templates/api-service-fastapi/app/models.py +++ b/templates/api-service-fastapi/app/models.py @@ -22,6 +22,7 @@ class CookbookCategory(Base): # passive_deletes removed — CUBRID FK cascade support varies (issue #33). ) + class CookbookItem(Base): __tablename__ = "cookbook_items" diff --git a/templates/api-service-fastapi/recipes/03-catalog-sync/models.py b/templates/api-service-fastapi/recipes/03-catalog-sync/models.py index 50650f3..3f1ef5b 100644 --- a/templates/api-service-fastapi/recipes/03-catalog-sync/models.py +++ b/templates/api-service-fastapi/recipes/03-catalog-sync/models.py @@ -1,8 +1,8 @@ # pyright: reportImplicitRelativeImport=false, reportUnusedParameter=false from datetime import datetime, timezone -from sqlalchemy import DateTime, ForeignKey, Integer, String, Text, UniqueConstraint -from sqlalchemy.orm import Mapped, mapped_column, relationship +from sqlalchemy import DateTime, Integer, String +from sqlalchemy.orm import Mapped, mapped_column from database import Base @@ -10,6 +10,7 @@ def utc_now() -> datetime: return datetime.now(timezone.utc) + class CatalogItem(Base): __tablename__: str = "cookbook_catalog_items" @@ -21,6 +22,7 @@ class CatalogItem(Base): source_version: Mapped[int] = mapped_column(Integer, nullable=False, default=1) last_synced_at: Mapped[datetime] = mapped_column(DateTime, nullable=False, default=utc_now) + class SyncRun(Base): __tablename__: str = "cookbook_sync_runs" diff --git a/templates/api-service-fastapi/recipes/03-catalog-sync/schemas.py b/templates/api-service-fastapi/recipes/03-catalog-sync/schemas.py index bcda227..993b1a5 100644 --- a/templates/api-service-fastapi/recipes/03-catalog-sync/schemas.py +++ b/templates/api-service-fastapi/recipes/03-catalog-sync/schemas.py @@ -2,7 +2,7 @@ from datetime import datetime from typing import ClassVar -from pydantic import BaseModel, ConfigDict, EmailStr, Field +from pydantic import BaseModel, ConfigDict, Field class CatalogItemSync(BaseModel): @@ -11,10 +11,12 @@ class CatalogItemSync(BaseModel): price: int = Field(gt=0) available: int = Field(ge=0, le=1) + class CatalogSyncRequest(BaseModel): source: str = Field(min_length=1, max_length=100) items: list[CatalogItemSync] = Field(min_length=1) + class CatalogItemResponse(BaseModel): model_config: ClassVar[ConfigDict] = ConfigDict(from_attributes=True) @@ -26,6 +28,7 @@ class CatalogItemResponse(BaseModel): source_version: int last_synced_at: datetime + class SyncRunResponse(BaseModel): model_config: ClassVar[ConfigDict] = ConfigDict(from_attributes=True) diff --git a/templates/api-service-fastapi/recipes/03-catalog-sync/tests/test_main.py b/templates/api-service-fastapi/recipes/03-catalog-sync/tests/test_main.py index cd737a9..e018124 100644 --- a/templates/api-service-fastapi/recipes/03-catalog-sync/tests/test_main.py +++ b/templates/api-service-fastapi/recipes/03-catalog-sync/tests/test_main.py @@ -46,7 +46,6 @@ def override_get_db(): from typing import cast - def build_items(price_offset: int = 0) -> list[dict[str, object]]: return [ { diff --git a/templates/api-service-fastapi/recipes/04-audit-trail/models.py b/templates/api-service-fastapi/recipes/04-audit-trail/models.py index 5f28b1d..a02ad4a 100644 --- a/templates/api-service-fastapi/recipes/04-audit-trail/models.py +++ b/templates/api-service-fastapi/recipes/04-audit-trail/models.py @@ -10,6 +10,7 @@ def utc_now() -> datetime: return datetime.now(timezone.utc) + class UserProfile(Base): __tablename__: str = "cookbook_user_profiles" @@ -30,6 +31,7 @@ class UserProfile(Base): cascade="all, delete-orphan", ) + class ProfileEvent(Base): __tablename__: str = "cookbook_profile_events" __table_args__: tuple[UniqueConstraint] = ( diff --git a/templates/api-service-fastapi/recipes/04-audit-trail/schemas.py b/templates/api-service-fastapi/recipes/04-audit-trail/schemas.py index c73ecb8..5af3875 100644 --- a/templates/api-service-fastapi/recipes/04-audit-trail/schemas.py +++ b/templates/api-service-fastapi/recipes/04-audit-trail/schemas.py @@ -10,12 +10,14 @@ class ProfileCreate(BaseModel): display_name: str = Field(min_length=1, max_length=255) bio: str | None = None + class ProfileUpdate(BaseModel): expected_version: int = Field(ge=1) email: EmailStr | None = None display_name: str | None = Field(default=None, min_length=1, max_length=255) bio: str | None = None + class ProfileResponse(BaseModel): model_config: ClassVar[ConfigDict] = ConfigDict(from_attributes=True) @@ -27,6 +29,7 @@ class ProfileResponse(BaseModel): created_at: datetime updated_at: datetime + class ProfileEventResponse(BaseModel): model_config: ClassVar[ConfigDict] = ConfigDict(from_attributes=True) diff --git a/templates/api-service-fastapi/recipes/04-audit-trail/tests/test_main.py b/templates/api-service-fastapi/recipes/04-audit-trail/tests/test_main.py index ebd6bf8..282549f 100644 --- a/templates/api-service-fastapi/recipes/04-audit-trail/tests/test_main.py +++ b/templates/api-service-fastapi/recipes/04-audit-trail/tests/test_main.py @@ -47,7 +47,6 @@ def override_get_db(): from typing import cast - @pytest.mark.asyncio async def test_create_profile(client: AsyncClient) -> None: payload = { diff --git a/templates/api-service-fastapi/recipes/05-multi-tenant-search/models.py b/templates/api-service-fastapi/recipes/05-multi-tenant-search/models.py index 7385a68..295a8dd 100644 --- a/templates/api-service-fastapi/recipes/05-multi-tenant-search/models.py +++ b/templates/api-service-fastapi/recipes/05-multi-tenant-search/models.py @@ -1,8 +1,8 @@ # pyright: reportImplicitRelativeImport=false, reportUnusedParameter=false from datetime import datetime, timezone -from sqlalchemy import DateTime, ForeignKey, Integer, String, Text, UniqueConstraint -from sqlalchemy.orm import Mapped, mapped_column, relationship +from sqlalchemy import DateTime, ForeignKey, Integer, String, UniqueConstraint +from sqlalchemy.orm import Mapped, mapped_column from database import Base @@ -10,6 +10,7 @@ def utc_now() -> datetime: return datetime.now(timezone.utc) + class Tenant(Base): __tablename__: str = "cookbook_tenants" @@ -18,6 +19,7 @@ class Tenant(Base): slug: Mapped[str] = mapped_column(String(50), unique=True, nullable=False) created_at: Mapped[datetime] = mapped_column(DateTime, nullable=False, default=utc_now) + class Contact(Base): __tablename__: str = "cookbook_contacts" __table_args__: tuple[UniqueConstraint] = ( diff --git a/templates/api-service-fastapi/recipes/05-multi-tenant-search/schemas.py b/templates/api-service-fastapi/recipes/05-multi-tenant-search/schemas.py index 214d2c3..4d978d9 100644 --- a/templates/api-service-fastapi/recipes/05-multi-tenant-search/schemas.py +++ b/templates/api-service-fastapi/recipes/05-multi-tenant-search/schemas.py @@ -9,6 +9,7 @@ class TenantCreate(BaseModel): name: str = Field(min_length=1, max_length=120) slug: str = Field(min_length=1, max_length=50) + class TenantResponse(BaseModel): model_config: ClassVar[ConfigDict] = ConfigDict(from_attributes=True) @@ -17,6 +18,7 @@ class TenantResponse(BaseModel): slug: str created_at: datetime + class ContactCreate(BaseModel): first_name: str = Field(min_length=1, max_length=100) last_name: str = Field(min_length=1, max_length=100) @@ -24,6 +26,7 @@ class ContactCreate(BaseModel): city: str | None = Field(default=None, max_length=100) status: str = Field(default="active", min_length=1, max_length=20) + class ContactUpdate(BaseModel): first_name: str | None = Field(default=None, min_length=1, max_length=100) last_name: str | None = Field(default=None, min_length=1, max_length=100) @@ -31,6 +34,7 @@ class ContactUpdate(BaseModel): city: str | None = Field(default=None, max_length=100) status: str | None = Field(default=None, min_length=1, max_length=20) + class ContactResponse(BaseModel): model_config: ClassVar[ConfigDict] = ConfigDict(from_attributes=True) @@ -43,6 +47,7 @@ class ContactResponse(BaseModel): status: str created_at: datetime + class ContactCursorList(BaseModel): items: list[ContactResponse] next_cursor: int | None diff --git a/templates/api-service-fastapi/recipes/05-multi-tenant-search/tests/test_main.py b/templates/api-service-fastapi/recipes/05-multi-tenant-search/tests/test_main.py index 3a19e20..e0d8bc0 100644 --- a/templates/api-service-fastapi/recipes/05-multi-tenant-search/tests/test_main.py +++ b/templates/api-service-fastapi/recipes/05-multi-tenant-search/tests/test_main.py @@ -46,7 +46,6 @@ def override_get_db(): from typing import cast - async def create_tenant(client: AsyncClient, name: str, slug: str) -> dict[str, object]: response = await client.post("/tenants", json={"name": name, "slug": slug}) assert response.status_code == 201 diff --git a/templates/api-service-fastapi/recipes/06-price-books/models.py b/templates/api-service-fastapi/recipes/06-price-books/models.py index 35a46c0..661e1c3 100644 --- a/templates/api-service-fastapi/recipes/06-price-books/models.py +++ b/templates/api-service-fastapi/recipes/06-price-books/models.py @@ -1,7 +1,7 @@ # pyright: reportImplicitRelativeImport=false, reportUnusedParameter=false from datetime import datetime, timezone -from sqlalchemy import DateTime, ForeignKey, Integer, String, Text, UniqueConstraint +from sqlalchemy import DateTime, ForeignKey, Integer, String, UniqueConstraint from sqlalchemy.orm import Mapped, mapped_column, relationship from database import Base @@ -10,6 +10,7 @@ def utc_now() -> datetime: return datetime.now(timezone.utc) + class PriceProduct(Base): __tablename__: str = "cookbook_price_products" @@ -18,6 +19,7 @@ class PriceProduct(Base): name: Mapped[str] = mapped_column(String(255), nullable=False) active: Mapped[int] = mapped_column(Integer, nullable=False, default=1) + class PriceBookEntry(Base): __tablename__: str = "cookbook_price_book_entries" __table_args__: tuple[UniqueConstraint] = ( diff --git a/templates/api-service-fastapi/recipes/06-price-books/schemas.py b/templates/api-service-fastapi/recipes/06-price-books/schemas.py index e19eb07..a7cfd3a 100644 --- a/templates/api-service-fastapi/recipes/06-price-books/schemas.py +++ b/templates/api-service-fastapi/recipes/06-price-books/schemas.py @@ -2,13 +2,14 @@ from datetime import datetime from typing import ClassVar -from pydantic import BaseModel, ConfigDict, EmailStr, Field +from pydantic import BaseModel, ConfigDict, Field class PriceProductCreate(BaseModel): sku: str = Field(min_length=1, max_length=100) name: str = Field(min_length=1, max_length=255) + class PriceProductResponse(BaseModel): model_config: ClassVar[ConfigDict] = ConfigDict(from_attributes=True) @@ -17,6 +18,7 @@ class PriceProductResponse(BaseModel): name: str active: int + class PriceEntryCreate(BaseModel): product_id: int channel: str = Field(min_length=1, max_length=30) @@ -25,6 +27,7 @@ class PriceEntryCreate(BaseModel): starts_at: datetime ends_at: datetime | None = None + class PriceEntryResponse(BaseModel): model_config: ClassVar[ConfigDict] = ConfigDict(from_attributes=True) @@ -37,6 +40,7 @@ class PriceEntryResponse(BaseModel): ends_at: datetime | None version: int + class SupersedeRequest(BaseModel): new_amount_cents: int = Field(gt=0) effective_at: datetime diff --git a/templates/api-service-fastapi/recipes/07-document-publishing/models.py b/templates/api-service-fastapi/recipes/07-document-publishing/models.py index c5d9923..ba35009 100644 --- a/templates/api-service-fastapi/recipes/07-document-publishing/models.py +++ b/templates/api-service-fastapi/recipes/07-document-publishing/models.py @@ -10,6 +10,7 @@ def utc_now() -> datetime: return datetime.now(timezone.utc) + class Document(Base): __tablename__: str = "cookbook_documents" @@ -28,6 +29,7 @@ class Document(Base): ) revisions: Mapped[list["DocumentRevision"]] = relationship(back_populates="document") + class DocumentRevision(Base): __tablename__: str = "cookbook_document_revisions" __table_args__: tuple[UniqueConstraint] = ( diff --git a/templates/api-service-fastapi/recipes/07-document-publishing/schemas.py b/templates/api-service-fastapi/recipes/07-document-publishing/schemas.py index 0d219bd..5d5b90a 100644 --- a/templates/api-service-fastapi/recipes/07-document-publishing/schemas.py +++ b/templates/api-service-fastapi/recipes/07-document-publishing/schemas.py @@ -2,7 +2,7 @@ from datetime import datetime from typing import ClassVar -from pydantic import BaseModel, ConfigDict, EmailStr, Field +from pydantic import BaseModel, ConfigDict, Field class DocumentCreate(BaseModel): @@ -11,11 +11,13 @@ class DocumentCreate(BaseModel): body: str = Field(min_length=1) created_by: str = Field(min_length=1, max_length=100) + class DraftCreate(BaseModel): title: str = Field(min_length=1, max_length=255) body: str = Field(min_length=1) created_by: str = Field(min_length=1, max_length=100) + class DocumentResponse(BaseModel): model_config: ClassVar[ConfigDict] = ConfigDict(from_attributes=True) @@ -28,6 +30,7 @@ class DocumentResponse(BaseModel): created_at: datetime updated_at: datetime + class RevisionResponse(BaseModel): model_config: ClassVar[ConfigDict] = ConfigDict(from_attributes=True) @@ -40,6 +43,7 @@ class RevisionResponse(BaseModel): created_by: str created_at: datetime + class DocumentDetailResponse(BaseModel): document: DocumentResponse revisions: list[RevisionResponse] diff --git a/templates/api-service-fastapi/recipes/07-document-publishing/tests/test_main.py b/templates/api-service-fastapi/recipes/07-document-publishing/tests/test_main.py index 4c3de0b..c1fc5c1 100644 --- a/templates/api-service-fastapi/recipes/07-document-publishing/tests/test_main.py +++ b/templates/api-service-fastapi/recipes/07-document-publishing/tests/test_main.py @@ -46,7 +46,6 @@ def override_get_db(): from typing import cast - @pytest.mark.asyncio async def test_create_document_with_first_revision(client: AsyncClient) -> None: response = await client.post( diff --git a/templates/api-service-fastapi/recipes/08-webhook-inbox/models.py b/templates/api-service-fastapi/recipes/08-webhook-inbox/models.py index 51f98ef..c3286ab 100644 --- a/templates/api-service-fastapi/recipes/08-webhook-inbox/models.py +++ b/templates/api-service-fastapi/recipes/08-webhook-inbox/models.py @@ -10,6 +10,7 @@ def utc_now() -> datetime: return datetime.now(timezone.utc) + class Shipment(Base): __tablename__: str = "cookbook_shipments" @@ -19,6 +20,7 @@ class Shipment(Base): current_status: Mapped[str] = mapped_column(String(30), nullable=False, default="unknown") delivered_at: Mapped[datetime | None] = mapped_column(DateTime, nullable=True) + class WebhookEvent(Base): __tablename__: str = "cookbook_webhook_events" __table_args__: tuple[UniqueConstraint] = ( @@ -44,6 +46,7 @@ class WebhookEvent(Base): processed_at: Mapped[datetime | None] = mapped_column(DateTime, nullable=True) attempts_list: Mapped[list["WebhookAttempt"]] = relationship(back_populates="event") + class WebhookAttempt(Base): __tablename__: str = "cookbook_webhook_attempts" diff --git a/templates/api-service-fastapi/recipes/08-webhook-inbox/schemas.py b/templates/api-service-fastapi/recipes/08-webhook-inbox/schemas.py index 88aefee..2a8965c 100644 --- a/templates/api-service-fastapi/recipes/08-webhook-inbox/schemas.py +++ b/templates/api-service-fastapi/recipes/08-webhook-inbox/schemas.py @@ -2,13 +2,14 @@ from datetime import datetime from typing import ClassVar -from pydantic import BaseModel, ConfigDict, EmailStr, Field +from pydantic import BaseModel, ConfigDict, Field class ShipmentCreate(BaseModel): external_ref: str = Field(min_length=1, max_length=100) carrier: str = Field(min_length=1, max_length=50) + class ShipmentResponse(BaseModel): model_config: ClassVar[ConfigDict] = ConfigDict(from_attributes=True) @@ -18,6 +19,7 @@ class ShipmentResponse(BaseModel): current_status: str delivered_at: datetime | None + class WebhookIngest(BaseModel): provider: str = Field(min_length=1, max_length=50) external_event_id: str = Field(min_length=1, max_length=200) @@ -25,6 +27,7 @@ class WebhookIngest(BaseModel): payload: str shipment_external_ref: str | None = Field(default=None, min_length=1, max_length=100) + class WebhookAttemptResponse(BaseModel): model_config: ClassVar[ConfigDict] = ConfigDict(from_attributes=True) @@ -36,6 +39,7 @@ class WebhookAttemptResponse(BaseModel): outcome: str error_message: str | None + class WebhookEventResponse(BaseModel): model_config: ClassVar[ConfigDict] = ConfigDict(from_attributes=True) @@ -53,10 +57,12 @@ class WebhookEventResponse(BaseModel): processed_at: datetime | None attempts_list: list[WebhookAttemptResponse] = Field(default_factory=list) + class ProcessRequest(BaseModel): processor_token: str = Field(min_length=1, max_length=100) max_events: int = Field(default=5, ge=1) + class ProcessResult(BaseModel): processed: int failed: int diff --git a/templates/api-service-fastapi/recipes/08-webhook-inbox/tests/test_main.py b/templates/api-service-fastapi/recipes/08-webhook-inbox/tests/test_main.py index e60960c..b53e47b 100644 --- a/templates/api-service-fastapi/recipes/08-webhook-inbox/tests/test_main.py +++ b/templates/api-service-fastapi/recipes/08-webhook-inbox/tests/test_main.py @@ -48,8 +48,6 @@ def override_get_db(): from typing import cast - - @pytest.mark.asyncio async def test_ingest_webhook_creates_event(client: AsyncClient) -> None: shipment_response = await client.post( diff --git a/templates/api-service-fastapi/recipes/09-saga/main.py b/templates/api-service-fastapi/recipes/09-saga/main.py index 304dfa5..3847ca1 100644 --- a/templates/api-service-fastapi/recipes/09-saga/main.py +++ b/templates/api-service-fastapi/recipes/09-saga/main.py @@ -4,10 +4,8 @@ from fastapi import FastAPI try: - from .database import Base, engine from .routes import router except ImportError: - from database import Base, engine from routes import router diff --git a/templates/api-service-fastapi/recipes/10-cqrs-event-sourcing/tests/test_main.py b/templates/api-service-fastapi/recipes/10-cqrs-event-sourcing/tests/test_main.py index 130e319..3354cc7 100644 --- a/templates/api-service-fastapi/recipes/10-cqrs-event-sourcing/tests/test_main.py +++ b/templates/api-service-fastapi/recipes/10-cqrs-event-sourcing/tests/test_main.py @@ -1,6 +1,5 @@ from __future__ import annotations -import threading from pathlib import Path from typing import Generator diff --git a/templates/api-service-fastapi/recipes/12-reservation-scheduling/schemas.py b/templates/api-service-fastapi/recipes/12-reservation-scheduling/schemas.py index 66bafe7..36d5879 100644 --- a/templates/api-service-fastapi/recipes/12-reservation-scheduling/schemas.py +++ b/templates/api-service-fastapi/recipes/12-reservation-scheduling/schemas.py @@ -37,12 +37,14 @@ def parse_iso_datetime(cls, value: str | datetime) -> datetime: if value.tzinfo is not None: # Convert to UTC then strip tzinfo from datetime import timezone + value = value.astimezone(timezone.utc).replace(tzinfo=None) return value try: parsed = datetime.fromisoformat(value) if parsed.tzinfo is not None: from datetime import timezone + parsed = parsed.astimezone(timezone.utc).replace(tzinfo=None) return parsed except ValueError as exc: @@ -74,12 +76,14 @@ def parse_iso_datetime(cls, value: str | datetime) -> datetime: if isinstance(value, datetime): if value.tzinfo is not None: from datetime import timezone + value = value.astimezone(timezone.utc).replace(tzinfo=None) return value try: parsed = datetime.fromisoformat(value) if parsed.tzinfo is not None: from datetime import timezone + parsed = parsed.astimezone(timezone.utc).replace(tzinfo=None) return parsed except ValueError as exc: @@ -97,12 +101,14 @@ def parse_iso_datetime(cls, value: str | datetime) -> datetime: if isinstance(value, datetime): if value.tzinfo is not None: from datetime import timezone + value = value.astimezone(timezone.utc).replace(tzinfo=None) return value try: parsed = datetime.fromisoformat(value) if parsed.tzinfo is not None: from datetime import timezone + parsed = parsed.astimezone(timezone.utc).replace(tzinfo=None) return parsed except ValueError as exc: diff --git a/templates/dashboard/01_table_viewer.py b/templates/dashboard/01_table_viewer.py index a7345ac..adaa545 100644 --- a/templates/dashboard/01_table_viewer.py +++ b/templates/dashboard/01_table_viewer.py @@ -2,7 +2,6 @@ from __future__ import annotations -import time from datetime import date, timedelta import pandas as pd diff --git a/templates/django/app/views.py b/templates/django/app/views.py index d02fac2..237da75 100644 --- a/templates/django/app/views.py +++ b/templates/django/app/views.py @@ -16,6 +16,7 @@ def _ensure_tables_initialized() -> None: create_tables() _tables_initialized = True + def health_view(_request: HttpRequest) -> JsonResponse: _ensure_tables_initialized() session = get_session() diff --git a/templates/flask/02-categories/app.py b/templates/flask/02-categories/app.py index 87d153c..3797c90 100644 --- a/templates/flask/02-categories/app.py +++ b/templates/flask/02-categories/app.py @@ -18,21 +18,25 @@ bp = Blueprint("categories", __name__, url_prefix="/api/categories") + def _category_or_404(category_id: int) -> Category: category = db.session.get(Category, category_id) if category is None: raise LookupError("Category not found.") return category + def _is_include_deleted_enabled() -> bool: return request.args.get("include_deleted") == "1" + def _json_payload() -> Mapping[str, object]: payload_value = request.get_json(silent=True) if isinstance(payload_value, dict): return cast(dict[str, object], payload_value) return {} + @bp.get("") def list_categories(): stmt = select(Category).order_by(Category.id.asc()) @@ -41,6 +45,7 @@ def list_categories(): categories = db.session.execute(stmt).scalars().all() return jsonify([category.to_dict() for category in categories]) + @bp.post("") def create_category(): payload = _json_payload() @@ -68,6 +73,7 @@ def create_category(): db.session.commit() return jsonify(category.to_dict()), 201 + @bp.get("/") def get_category(category_id: int): try: @@ -80,6 +86,7 @@ def get_category(category_id: int): articles = [article.to_dict() for article in category.articles if article.is_deleted == 0] return jsonify({**category.to_dict(), "children": children, "articles": articles}) + @bp.delete("/") def soft_delete_category(category_id: int): try: @@ -88,13 +95,18 @@ def soft_delete_category(category_id: int): return jsonify({"error": "Category not found."}), 404 active_children = [child for child in category.children if child.is_deleted == 0] if active_children: - return jsonify({"error": "Cannot delete category with active children. Delete or reassign children first."}), 409 + return jsonify( + { + "error": "Cannot delete category with active children. Delete or reassign children first." + } + ), 409 category.is_deleted = 1 for article in category.articles: article.is_deleted = 1 db.session.commit() return jsonify(category.to_dict()) + @bp.post("//restore") def restore_category(category_id: int): try: @@ -107,6 +119,7 @@ def restore_category(category_id: int): db.session.commit() return jsonify(category.to_dict()) + @bp.post("//articles") def create_article(category_id: int): category = db.session.get(Category, category_id) @@ -125,14 +138,24 @@ def create_article(category_id: int): db.session.commit() return jsonify(article.to_dict()), 201 + @bp.get("//articles") def list_articles(category_id: int): category = db.session.get(Category, category_id) if category is None or category.is_deleted == 1: return jsonify({"error": "Category not found."}), 404 - articles = db.session.execute(select(Article).where(Article.category_id == category_id, Article.is_deleted == 0).order_by(Article.id.asc())).scalars().all() + articles = ( + db.session.execute( + select(Article) + .where(Article.category_id == category_id, Article.is_deleted == 0) + .order_by(Article.id.asc()) + ) + .scalars() + .all() + ) return jsonify([article.to_dict() for article in articles]) + @bp.delete("//articles/") def soft_delete_article(category_id: int, article_id: int): category = db.session.get(Category, category_id) @@ -145,9 +168,12 @@ def soft_delete_article(category_id: int, article_id: int): db.session.commit() return jsonify(article.to_dict()) + def create_app(config=None): app = Flask(__name__) - app.config["SQLALCHEMY_DATABASE_URI"] = os.getenv("DATABASE_URL", "cubrid+pycubrid://dba@localhost:33000/testdb") + app.config["SQLALCHEMY_DATABASE_URI"] = os.getenv( + "DATABASE_URL", "cubrid+pycubrid://dba@localhost:33000/testdb" + ) app.config["SQLALCHEMY_TRACK_MODIFICATIONS"] = False if config: app.config.update(config) diff --git a/templates/flask/02-categories/models.py b/templates/flask/02-categories/models.py index f6a0410..394785e 100644 --- a/templates/flask/02-categories/models.py +++ b/templates/flask/02-categories/models.py @@ -16,11 +16,15 @@ class Category(db.Model): id: Mapped[int] = mapped_column(Integer, primary_key=True, autoincrement=True) name: Mapped[str] = mapped_column(String(120), nullable=False) - parent_id: Mapped[int | None] = mapped_column(Integer, ForeignKey("cookbook_categories.id"), nullable=True) + parent_id: Mapped[int | None] = mapped_column( + Integer, ForeignKey("cookbook_categories.id"), nullable=True + ) is_deleted: Mapped[int] = mapped_column(Integer, nullable=False, default=0) created_at: Mapped[datetime] = mapped_column(DateTime, nullable=False, default=datetime.utcnow) - parent: Mapped["Category | None"] = relationship("Category", remote_side=[id], back_populates="children") + parent: Mapped["Category | None"] = relationship( + "Category", remote_side=[id], back_populates="children" + ) children: Mapped[list["Category"]] = relationship("Category", back_populates="parent") articles: Mapped[list["Article"]] = relationship(back_populates="category") @@ -40,7 +44,9 @@ class Article(db.Model): id: Mapped[int] = mapped_column(Integer, primary_key=True, autoincrement=True) title: Mapped[str] = mapped_column(String(255), nullable=False) body: Mapped[str | None] = mapped_column(Text, nullable=True) - category_id: Mapped[int] = mapped_column(Integer, ForeignKey("cookbook_categories.id"), nullable=False) + category_id: Mapped[int] = mapped_column( + Integer, ForeignKey("cookbook_categories.id"), nullable=False + ) is_deleted: Mapped[int] = mapped_column(Integer, nullable=False, default=0) created_at: Mapped[datetime] = mapped_column(DateTime, nullable=False, default=datetime.utcnow) diff --git a/templates/flask/03-inventory-ledger/app.py b/templates/flask/03-inventory-ledger/app.py index 4e998b8..60bde8a 100644 --- a/templates/flask/03-inventory-ledger/app.py +++ b/templates/flask/03-inventory-ledger/app.py @@ -299,9 +299,12 @@ def list_stock_item_movements(item_id: int): import os from flask import Flask + def create_app(config=None): app = Flask(__name__) - app.config["SQLALCHEMY_DATABASE_URI"] = os.getenv("DATABASE_URL", "cubrid+pycubrid://dba@localhost:33000/testdb") + app.config["SQLALCHEMY_DATABASE_URI"] = os.getenv( + "DATABASE_URL", "cubrid+pycubrid://dba@localhost:33000/testdb" + ) app.config["SQLALCHEMY_TRACK_MODIFICATIONS"] = False if config: app.config.update(config) diff --git a/templates/flask/03-inventory-ledger/models.py b/templates/flask/03-inventory-ledger/models.py index 017f214..f8b57c7 100644 --- a/templates/flask/03-inventory-ledger/models.py +++ b/templates/flask/03-inventory-ledger/models.py @@ -21,7 +21,12 @@ class Warehouse(db.Model): stock_items: Mapped[list["StockItem"]] = relationship(back_populates="warehouse") def to_dict(self) -> dict[str, object]: - return {"id": self.id, "code": self.code, "name": self.name, "created_at": self.created_at.isoformat()} + return { + "id": self.id, + "code": self.code, + "name": self.name, + "created_at": self.created_at.isoformat(), + } class StockItem(db.Model): @@ -29,7 +34,9 @@ class StockItem(db.Model): __table_args__ = (UniqueConstraint("warehouse_id", "sku", name="uq_stock_item_warehouse_sku"),) id: Mapped[int] = mapped_column(Integer, primary_key=True, autoincrement=True) - warehouse_id: Mapped[int] = mapped_column(Integer, ForeignKey("cookbook_warehouses.id"), nullable=False) + warehouse_id: Mapped[int] = mapped_column( + Integer, ForeignKey("cookbook_warehouses.id"), nullable=False + ) sku: Mapped[str] = mapped_column(String(100), nullable=False) product_name: Mapped[str] = mapped_column(String(255), nullable=False) on_hand_qty: Mapped[int] = mapped_column(Integer, nullable=False, default=0) @@ -38,14 +45,22 @@ class StockItem(db.Model): movements: Mapped[list["StockMovement"]] = relationship(back_populates="stock_item") def to_dict(self) -> dict[str, object]: - return {"id": self.id, "warehouse_id": self.warehouse_id, "sku": self.sku, "product_name": self.product_name, "on_hand_qty": self.on_hand_qty} + return { + "id": self.id, + "warehouse_id": self.warehouse_id, + "sku": self.sku, + "product_name": self.product_name, + "on_hand_qty": self.on_hand_qty, + } class StockMovement(db.Model): __tablename__ = "cookbook_stock_movements" id: Mapped[int] = mapped_column(Integer, primary_key=True, autoincrement=True) - stock_item_id: Mapped[int] = mapped_column(Integer, ForeignKey("cookbook_stock_items.id"), nullable=False) + stock_item_id: Mapped[int] = mapped_column( + Integer, ForeignKey("cookbook_stock_items.id"), nullable=False + ) movement_type: Mapped[str] = mapped_column(String(20), nullable=False) qty_delta: Mapped[int] = mapped_column(Integer, nullable=False) reference: Mapped[str | None] = mapped_column(String(255), nullable=True) @@ -54,4 +69,12 @@ class StockMovement(db.Model): stock_item: Mapped["StockItem"] = relationship(back_populates="movements") def to_dict(self) -> dict[str, object]: - return {"id": self.id, "stock_item_id": self.stock_item_id, "movement_type": self.movement_type, "qty_delta": self.qty_delta, "reference": self.reference, "note": self.note, "created_at": self.created_at.isoformat()} + return { + "id": self.id, + "stock_item_id": self.stock_item_id, + "movement_type": self.movement_type, + "qty_delta": self.qty_delta, + "reference": self.reference, + "note": self.note, + "created_at": self.created_at.isoformat(), + } diff --git a/templates/flask/04-purchase-orders/app.py b/templates/flask/04-purchase-orders/app.py index c1f92b1..08cc65b 100644 --- a/templates/flask/04-purchase-orders/app.py +++ b/templates/flask/04-purchase-orders/app.py @@ -177,7 +177,9 @@ def submit_purchase_order(order_id: int): result = db.session.execute( update(PurchaseOrder) .where(PurchaseOrder.id == order_id, PurchaseOrder.version == order.version) - .values(status="submitted", submitted_at=datetime.now(timezone.utc), version=order.version + 1) + .values( + status="submitted", submitted_at=datetime.now(timezone.utc), version=order.version + 1 + ) ) if cast(CursorResult[object], result).rowcount == 0: return jsonify({"error": "Concurrent modification detected."}), 409 @@ -200,7 +202,9 @@ def approve_purchase_order(order_id: int): result = db.session.execute( update(PurchaseOrder) .where(PurchaseOrder.id == order_id, PurchaseOrder.version == order.version) - .values(status="approved", approved_at=datetime.now(timezone.utc), version=order.version + 1) + .values( + status="approved", approved_at=datetime.now(timezone.utc), version=order.version + 1 + ) ) if cast(CursorResult[object], result).rowcount == 0: return jsonify({"error": "Concurrent modification detected."}), 409 diff --git a/templates/flask/04-purchase-orders/models.py b/templates/flask/04-purchase-orders/models.py index 6ca30d3..583018e 100644 --- a/templates/flask/04-purchase-orders/models.py +++ b/templates/flask/04-purchase-orders/models.py @@ -21,14 +21,21 @@ class Supplier(db.Model): orders: Mapped[list["PurchaseOrder"]] = relationship(back_populates="supplier") def to_dict(self) -> dict[str, object]: - return {"id": self.id, "name": self.name, "code": self.code, "created_at": self.created_at.isoformat()} + return { + "id": self.id, + "name": self.name, + "code": self.code, + "created_at": self.created_at.isoformat(), + } class PurchaseOrder(db.Model): __tablename__ = "cookbook_purchase_orders" id: Mapped[int] = mapped_column(Integer, primary_key=True, autoincrement=True) - supplier_id: Mapped[int] = mapped_column(Integer, ForeignKey("cookbook_suppliers.id"), nullable=False) + supplier_id: Mapped[int] = mapped_column( + Integer, ForeignKey("cookbook_suppliers.id"), nullable=False + ) status: Mapped[str] = mapped_column(String(20), nullable=False, default="draft") notes: Mapped[str | None] = mapped_column(Text, nullable=True) submitted_at: Mapped[datetime | None] = mapped_column(DateTime, nullable=True) @@ -37,7 +44,9 @@ class PurchaseOrder(db.Model): created_at: Mapped[datetime] = mapped_column(DateTime, nullable=False, default=datetime.utcnow) version: Mapped[int] = mapped_column(Integer, nullable=False, default=1) supplier: Mapped["Supplier"] = relationship(back_populates="orders") - lines: Mapped[list["PurchaseOrderLine"]] = relationship(back_populates="order", cascade="all, delete-orphan") + lines: Mapped[list["PurchaseOrderLine"]] = relationship( + back_populates="order", cascade="all, delete-orphan" + ) def to_dict(self) -> dict[str, object]: return { @@ -57,7 +66,9 @@ class PurchaseOrderLine(db.Model): __tablename__ = "cookbook_purchase_order_lines" id: Mapped[int] = mapped_column(Integer, primary_key=True, autoincrement=True) - order_id: Mapped[int] = mapped_column(Integer, ForeignKey("cookbook_purchase_orders.id"), nullable=False) + order_id: Mapped[int] = mapped_column( + Integer, ForeignKey("cookbook_purchase_orders.id"), nullable=False + ) sku: Mapped[str] = mapped_column(String(100), nullable=False) description: Mapped[str] = mapped_column(String(255), nullable=False) quantity: Mapped[int] = mapped_column(Integer, nullable=False) @@ -66,4 +77,12 @@ class PurchaseOrderLine(db.Model): order: Mapped["PurchaseOrder"] = relationship(back_populates="lines") def to_dict(self) -> dict[str, object]: - return {"id": self.id, "order_id": self.order_id, "sku": self.sku, "description": self.description, "quantity": self.quantity, "unit_cost": self.unit_cost, "received_qty": self.received_qty} + return { + "id": self.id, + "order_id": self.order_id, + "sku": self.sku, + "description": self.description, + "quantity": self.quantity, + "unit_cost": self.unit_cost, + "received_qty": self.received_qty, + } diff --git a/templates/flask/04-purchase-orders/tests/test_app.py b/templates/flask/04-purchase-orders/tests/test_app.py index cde350b..d88a867 100644 --- a/templates/flask/04-purchase-orders/tests/test_app.py +++ b/templates/flask/04-purchase-orders/tests/test_app.py @@ -19,7 +19,13 @@ @pytest.fixture def app(tmp_path: Path): - return create_app({"TESTING": True, "SQLALCHEMY_DATABASE_URI": f"sqlite:///{tmp_path / 'test.db'}", "SQLALCHEMY_TRACK_MODIFICATIONS": False}) + return create_app( + { + "TESTING": True, + "SQLALCHEMY_DATABASE_URI": f"sqlite:///{tmp_path / 'test.db'}", + "SQLALCHEMY_TRACK_MODIFICATIONS": False, + } + ) @pytest.fixture @@ -28,6 +34,7 @@ def httpx_client(app): with httpx.Client(transport=transport, base_url="http://testserver") as client: yield client + class SupplierPayload(TypedDict): id: int name: str diff --git a/templates/flask/05-batch-operations/app.py b/templates/flask/05-batch-operations/app.py index 99a27b4..98358f5 100644 --- a/templates/flask/05-batch-operations/app.py +++ b/templates/flask/05-batch-operations/app.py @@ -200,6 +200,7 @@ def submit_price_update_job(): } ), 201 + @batch_bp.get("/jobs") def list_batch_jobs(): jobs = db.session.execute(select(BatchJob).order_by(BatchJob.id.desc())).scalars().all() @@ -233,9 +234,12 @@ def get_batch_job(job_id: int): import os from flask import Flask + def create_app(config=None): app = Flask(__name__) - app.config["SQLALCHEMY_DATABASE_URI"] = os.getenv("DATABASE_URL", "cubrid+pycubrid://dba@localhost:33000/testdb") + app.config["SQLALCHEMY_DATABASE_URI"] = os.getenv( + "DATABASE_URL", "cubrid+pycubrid://dba@localhost:33000/testdb" + ) app.config["SQLALCHEMY_TRACK_MODIFICATIONS"] = False if config: app.config.update(config) diff --git a/templates/flask/05-batch-operations/models.py b/templates/flask/05-batch-operations/models.py index 45f973c..1f37a35 100644 --- a/templates/flask/05-batch-operations/models.py +++ b/templates/flask/05-batch-operations/models.py @@ -22,7 +22,14 @@ class BatchProduct(db.Model): created_at: Mapped[datetime] = mapped_column(DateTime, nullable=False, default=datetime.utcnow) def to_dict(self) -> dict[str, object]: - return {"id": self.id, "sku": self.sku, "name": self.name, "price": self.price, "is_active": self.is_active, "created_at": self.created_at.isoformat()} + return { + "id": self.id, + "sku": self.sku, + "name": self.name, + "price": self.price, + "is_active": self.is_active, + "created_at": self.created_at.isoformat(), + } class BatchJob(db.Model): @@ -39,14 +46,25 @@ class BatchJob(db.Model): rows: Mapped[list["BatchJobRow"]] = relationship(back_populates="job") def to_dict(self) -> dict[str, object]: - return {"id": self.id, "job_type": self.job_type, "status": self.status, "total_rows": self.total_rows, "success_cnt": self.success_cnt, "failed_cnt": self.failed_cnt, "created_at": self.created_at.isoformat(), "finished_at": self.finished_at.isoformat() if self.finished_at else None} + return { + "id": self.id, + "job_type": self.job_type, + "status": self.status, + "total_rows": self.total_rows, + "success_cnt": self.success_cnt, + "failed_cnt": self.failed_cnt, + "created_at": self.created_at.isoformat(), + "finished_at": self.finished_at.isoformat() if self.finished_at else None, + } class BatchJobRow(db.Model): __tablename__ = "cookbook_batch_job_rows" id: Mapped[int] = mapped_column(Integer, primary_key=True, autoincrement=True) - job_id: Mapped[int] = mapped_column(Integer, db.ForeignKey("cookbook_batch_jobs.id"), nullable=False) + job_id: Mapped[int] = mapped_column( + Integer, db.ForeignKey("cookbook_batch_jobs.id"), nullable=False + ) row_index: Mapped[int] = mapped_column(Integer, nullable=False) sku: Mapped[str] = mapped_column(String(100), nullable=False) payload: Mapped[str] = mapped_column(Text, nullable=False) @@ -55,4 +73,12 @@ class BatchJobRow(db.Model): job: Mapped["BatchJob"] = relationship(back_populates="rows") def to_dict(self) -> dict[str, object]: - return {"id": self.id, "job_id": self.job_id, "row_index": self.row_index, "sku": self.sku, "payload": self.payload, "status": self.status, "error_message": self.error_message} + return { + "id": self.id, + "job_id": self.job_id, + "row_index": self.row_index, + "sku": self.sku, + "payload": self.payload, + "status": self.status, + "error_message": self.error_message, + } diff --git a/templates/flask/05-batch-operations/tests/test_app.py b/templates/flask/05-batch-operations/tests/test_app.py index f95ab8f..1de6348 100644 --- a/templates/flask/05-batch-operations/tests/test_app.py +++ b/templates/flask/05-batch-operations/tests/test_app.py @@ -24,7 +24,13 @@ @pytest.fixture def app(tmp_path: Path): - return create_app({"TESTING": True, "SQLALCHEMY_DATABASE_URI": f"sqlite:///{tmp_path / 'test.db'}", "SQLALCHEMY_TRACK_MODIFICATIONS": False}) + return create_app( + { + "TESTING": True, + "SQLALCHEMY_DATABASE_URI": f"sqlite:///{tmp_path / 'test.db'}", + "SQLALCHEMY_TRACK_MODIFICATIONS": False, + } + ) @pytest.fixture @@ -33,6 +39,7 @@ def httpx_client(app): with httpx.Client(transport=transport, base_url="http://testserver") as client: yield client + class BatchProductPayload(TypedDict): id: int sku: str diff --git a/templates/flask/06-case-triage/app.py b/templates/flask/06-case-triage/app.py index 66be274..b783e75 100644 --- a/templates/flask/06-case-triage/app.py +++ b/templates/flask/06-case-triage/app.py @@ -320,9 +320,12 @@ def add_case_note(case_id: int): import os from flask import Flask + def create_app(config=None): app = Flask(__name__) - app.config["SQLALCHEMY_DATABASE_URI"] = os.getenv("DATABASE_URL", "cubrid+pycubrid://dba@localhost:33000/testdb") + app.config["SQLALCHEMY_DATABASE_URI"] = os.getenv( + "DATABASE_URL", "cubrid+pycubrid://dba@localhost:33000/testdb" + ) app.config["SQLALCHEMY_TRACK_MODIFICATIONS"] = False if config: app.config.update(config) diff --git a/templates/flask/06-case-triage/models.py b/templates/flask/06-case-triage/models.py index 00f759d..d8cc74d 100644 --- a/templates/flask/06-case-triage/models.py +++ b/templates/flask/06-case-triage/models.py @@ -25,7 +25,9 @@ class ReviewCase(db.Model): resolved_at: Mapped[datetime | None] = mapped_column(DateTime, nullable=True) version: Mapped[int] = mapped_column(Integer, nullable=False, default=1) created_at: Mapped[datetime] = mapped_column(DateTime, nullable=False, default=datetime.utcnow) - notes: Mapped[list["ReviewNote"]] = relationship(back_populates="case", cascade="all, delete-orphan") + notes: Mapped[list["ReviewNote"]] = relationship( + back_populates="case", cascade="all, delete-orphan" + ) def to_dict(self) -> dict[str, object]: return { @@ -36,7 +38,9 @@ def to_dict(self) -> dict[str, object]: "priority": self.priority, "status": self.status, "claimed_by": self.claimed_by, - "lease_expires_at": self.lease_expires_at.isoformat() if self.lease_expires_at else None, + "lease_expires_at": self.lease_expires_at.isoformat() + if self.lease_expires_at + else None, "resolved_at": self.resolved_at.isoformat() if self.resolved_at else None, "version": self.version, "created_at": self.created_at.isoformat(), @@ -47,11 +51,19 @@ class ReviewNote(db.Model): __tablename__ = "cookbook_review_notes" id: Mapped[int] = mapped_column(Integer, primary_key=True, autoincrement=True) - case_id: Mapped[int] = mapped_column(Integer, ForeignKey("cookbook_review_cases.id"), nullable=False) + case_id: Mapped[int] = mapped_column( + Integer, ForeignKey("cookbook_review_cases.id"), nullable=False + ) author: Mapped[str] = mapped_column(String(80), nullable=False) body: Mapped[str] = mapped_column(Text, nullable=False) created_at: Mapped[datetime] = mapped_column(DateTime, nullable=False, default=datetime.utcnow) case: Mapped["ReviewCase"] = relationship(back_populates="notes") def to_dict(self) -> dict[str, object]: - return {"id": self.id, "case_id": self.case_id, "author": self.author, "body": self.body, "created_at": self.created_at.isoformat()} + return { + "id": self.id, + "case_id": self.case_id, + "author": self.author, + "body": self.body, + "created_at": self.created_at.isoformat(), + } diff --git a/templates/flask/06-case-triage/tests/test_app.py b/templates/flask/06-case-triage/tests/test_app.py index 9e348d6..67248c2 100644 --- a/templates/flask/06-case-triage/tests/test_app.py +++ b/templates/flask/06-case-triage/tests/test_app.py @@ -25,7 +25,13 @@ @pytest.fixture def app(tmp_path: Path): - return create_app({"TESTING": True, "SQLALCHEMY_DATABASE_URI": f"sqlite:///{tmp_path / 'test.db'}", "SQLALCHEMY_TRACK_MODIFICATIONS": False}) + return create_app( + { + "TESTING": True, + "SQLALCHEMY_DATABASE_URI": f"sqlite:///{tmp_path / 'test.db'}", + "SQLALCHEMY_TRACK_MODIFICATIONS": False, + } + ) @pytest.fixture @@ -34,6 +40,7 @@ def httpx_client(app): with httpx.Client(transport=transport, base_url="http://testserver") as client: yield client + class CasePayload(TypedDict): id: int customer_email: str diff --git a/templates/flask/07-vendor-feed/app.py b/templates/flask/07-vendor-feed/app.py index b04f906..38e2a60 100644 --- a/templates/flask/07-vendor-feed/app.py +++ b/templates/flask/07-vendor-feed/app.py @@ -293,9 +293,12 @@ def list_catalog_products(): import os from flask import Flask + def create_app(config=None): app = Flask(__name__) - app.config["SQLALCHEMY_DATABASE_URI"] = os.getenv("DATABASE_URL", "cubrid+pycubrid://dba@localhost:33000/testdb") + app.config["SQLALCHEMY_DATABASE_URI"] = os.getenv( + "DATABASE_URL", "cubrid+pycubrid://dba@localhost:33000/testdb" + ) app.config["SQLALCHEMY_TRACK_MODIFICATIONS"] = False if config: app.config.update(config) diff --git a/templates/flask/07-vendor-feed/models.py b/templates/flask/07-vendor-feed/models.py index 064970c..31d4cfc 100644 --- a/templates/flask/07-vendor-feed/models.py +++ b/templates/flask/07-vendor-feed/models.py @@ -22,10 +22,20 @@ class ImportBatch(db.Model): uploaded_at: Mapped[datetime] = mapped_column(DateTime, nullable=False, default=datetime.utcnow) validated_at: Mapped[datetime | None] = mapped_column(DateTime, nullable=True) promoted_at: Mapped[datetime | None] = mapped_column(DateTime, nullable=True) - rows: Mapped[list["ImportRow"]] = relationship(back_populates="batch", cascade="all, delete-orphan") + rows: Mapped[list["ImportRow"]] = relationship( + back_populates="batch", cascade="all, delete-orphan" + ) def to_dict(self) -> dict[str, object]: - return {"id": self.id, "vendor_name": self.vendor_name, "source_filename": self.source_filename, "status": self.status, "uploaded_at": self.uploaded_at.isoformat(), "validated_at": self.validated_at.isoformat() if self.validated_at else None, "promoted_at": self.promoted_at.isoformat() if self.promoted_at else None} + return { + "id": self.id, + "vendor_name": self.vendor_name, + "source_filename": self.source_filename, + "status": self.status, + "uploaded_at": self.uploaded_at.isoformat(), + "validated_at": self.validated_at.isoformat() if self.validated_at else None, + "promoted_at": self.promoted_at.isoformat() if self.promoted_at else None, + } class ImportRow(db.Model): @@ -33,7 +43,9 @@ class ImportRow(db.Model): __table_args__ = (UniqueConstraint("batch_id", "row_no", name="uq_import_rows_batch_row_no"),) id: Mapped[int] = mapped_column(Integer, primary_key=True, autoincrement=True) - batch_id: Mapped[int] = mapped_column(Integer, ForeignKey("cookbook_import_batches.id"), nullable=False) + batch_id: Mapped[int] = mapped_column( + Integer, ForeignKey("cookbook_import_batches.id"), nullable=False + ) row_no: Mapped[int] = mapped_column(Integer, nullable=False) external_sku: Mapped[str] = mapped_column(String(100), nullable=False) name: Mapped[str | None] = mapped_column(String(255), nullable=True) @@ -46,12 +58,26 @@ class ImportRow(db.Model): batch: Mapped["ImportBatch"] = relationship(back_populates="rows") def to_dict(self) -> dict[str, object]: - return {"id": self.id, "batch_id": self.batch_id, "row_no": self.row_no, "external_sku": self.external_sku, "name": self.name, "price_cents": self.price_cents, "raw_payload": self.raw_payload, "validation_status": self.validation_status, "error_code": self.error_code, "error_message": self.error_message, "promoted_product_id": self.promoted_product_id} + return { + "id": self.id, + "batch_id": self.batch_id, + "row_no": self.row_no, + "external_sku": self.external_sku, + "name": self.name, + "price_cents": self.price_cents, + "raw_payload": self.raw_payload, + "validation_status": self.validation_status, + "error_code": self.error_code, + "error_message": self.error_message, + "promoted_product_id": self.promoted_product_id, + } class CatalogProduct(db.Model): __tablename__ = "cookbook_catalog_products" - __table_args__ = (UniqueConstraint("vendor_name", "external_sku", name="uq_catalog_product_vendor_sku"),) + __table_args__ = ( + UniqueConstraint("vendor_name", "external_sku", name="uq_catalog_product_vendor_sku"), + ) id: Mapped[int] = mapped_column(Integer, primary_key=True, autoincrement=True) vendor_name: Mapped[str] = mapped_column(String(120), nullable=False) @@ -61,7 +87,14 @@ class CatalogProduct(db.Model): active: Mapped[int] = mapped_column(Integer, nullable=False, default=1) def to_dict(self) -> dict[str, object]: - return {"id": self.id, "vendor_name": self.vendor_name, "external_sku": self.external_sku, "name": self.name, "price_cents": self.price_cents, "active": self.active} + return { + "id": self.id, + "vendor_name": self.vendor_name, + "external_sku": self.external_sku, + "name": self.name, + "price_cents": self.price_cents, + "active": self.active, + } class Product(db.Model): @@ -76,4 +109,12 @@ class Product(db.Model): created_at: Mapped[datetime] = mapped_column(DateTime, nullable=False, default=datetime.utcnow) def to_dict(self) -> dict[str, str | int]: - return {"id": self.id, "name": self.name, "description": self.description or "", "price": str(self.price), "category": self.category, "in_stock": self.in_stock, "created_at": self.created_at.isoformat()} + return { + "id": self.id, + "name": self.name, + "description": self.description or "", + "price": str(self.price), + "category": self.category, + "in_stock": self.in_stock, + "created_at": self.created_at.isoformat(), + } diff --git a/templates/flask/07-vendor-feed/tests/test_app.py b/templates/flask/07-vendor-feed/tests/test_app.py index 629752c..d81cd69 100644 --- a/templates/flask/07-vendor-feed/tests/test_app.py +++ b/templates/flask/07-vendor-feed/tests/test_app.py @@ -19,7 +19,13 @@ @pytest.fixture def app(tmp_path: Path): - return create_app({"TESTING": True, "SQLALCHEMY_DATABASE_URI": f"sqlite:///{tmp_path / 'test.db'}", "SQLALCHEMY_TRACK_MODIFICATIONS": False}) + return create_app( + { + "TESTING": True, + "SQLALCHEMY_DATABASE_URI": f"sqlite:///{tmp_path / 'test.db'}", + "SQLALCHEMY_TRACK_MODIFICATIONS": False, + } + ) @pytest.fixture @@ -28,6 +34,7 @@ def httpx_client(app): with httpx.Client(transport=transport, base_url="http://testserver") as client: yield client + class ImportBatchPayload(TypedDict): id: int vendor_name: str diff --git a/templates/flask/08-transactional-outbox/app.py b/templates/flask/08-transactional-outbox/app.py index 9eb185e..9c9d4d9 100644 --- a/templates/flask/08-transactional-outbox/app.py +++ b/templates/flask/08-transactional-outbox/app.py @@ -253,9 +253,12 @@ def get_outbox_message(message_id: int): import os from flask import Flask + def create_app(config=None): app = Flask(__name__) - app.config["SQLALCHEMY_DATABASE_URI"] = os.getenv("DATABASE_URL", "cubrid+pycubrid://dba@localhost:33000/testdb") + app.config["SQLALCHEMY_DATABASE_URI"] = os.getenv( + "DATABASE_URL", "cubrid+pycubrid://dba@localhost:33000/testdb" + ) app.config["SQLALCHEMY_TRACK_MODIFICATIONS"] = False if config: app.config.update(config) diff --git a/templates/flask/08-transactional-outbox/models.py b/templates/flask/08-transactional-outbox/models.py index ba5af5b..09da0d8 100644 --- a/templates/flask/08-transactional-outbox/models.py +++ b/templates/flask/08-transactional-outbox/models.py @@ -22,7 +22,14 @@ class Invoice(db.Model): created_at: Mapped[datetime] = mapped_column(DateTime, nullable=False, default=datetime.utcnow) def to_dict(self) -> dict[str, object]: - return {"id": self.id, "customer_email": self.customer_email, "total_cents": self.total_cents, "status": self.status, "sent_at": self.sent_at.isoformat() if self.sent_at else None, "created_at": self.created_at.isoformat()} + return { + "id": self.id, + "customer_email": self.customer_email, + "total_cents": self.total_cents, + "status": self.status, + "sent_at": self.sent_at.isoformat() if self.sent_at else None, + "created_at": self.created_at.isoformat(), + } class OutboxMessage(db.Model): @@ -36,7 +43,9 @@ class OutboxMessage(db.Model): payload: Mapped[str] = mapped_column(Text, nullable=False) status: Mapped[str] = mapped_column(String(20), nullable=False, default="pending") attempts: Mapped[int] = mapped_column(Integer, nullable=False, default=0) - next_attempt_at: Mapped[datetime] = mapped_column(DateTime, nullable=False, default=datetime.utcnow) + next_attempt_at: Mapped[datetime] = mapped_column( + DateTime, nullable=False, default=datetime.utcnow + ) leased_until: Mapped[datetime | None] = mapped_column(DateTime, nullable=True) leased_by: Mapped[str | None] = mapped_column(String(100), nullable=True) idempotency_key: Mapped[str] = mapped_column(String(200), unique=True, nullable=False) @@ -46,14 +55,32 @@ class OutboxMessage(db.Model): attempts_list: Mapped[list["OutboxAttempt"]] = relationship(back_populates="message") def to_dict(self) -> dict[str, object]: - return {"id": self.id, "topic": self.topic, "aggregate_type": self.aggregate_type, "aggregate_id": self.aggregate_id, "event_type": self.event_type, "payload": self.payload, "status": self.status, "attempts": self.attempts, "next_attempt_at": self.next_attempt_at.isoformat(), "leased_until": self.leased_until.isoformat() if self.leased_until else None, "leased_by": self.leased_by, "idempotency_key": self.idempotency_key, "last_error": self.last_error, "created_at": self.created_at.isoformat(), "sent_at": self.sent_at.isoformat() if self.sent_at else None} + return { + "id": self.id, + "topic": self.topic, + "aggregate_type": self.aggregate_type, + "aggregate_id": self.aggregate_id, + "event_type": self.event_type, + "payload": self.payload, + "status": self.status, + "attempts": self.attempts, + "next_attempt_at": self.next_attempt_at.isoformat(), + "leased_until": self.leased_until.isoformat() if self.leased_until else None, + "leased_by": self.leased_by, + "idempotency_key": self.idempotency_key, + "last_error": self.last_error, + "created_at": self.created_at.isoformat(), + "sent_at": self.sent_at.isoformat() if self.sent_at else None, + } class OutboxAttempt(db.Model): __tablename__ = "cookbook_outbox_attempts" id: Mapped[int] = mapped_column(Integer, primary_key=True, autoincrement=True) - outbox_message_id: Mapped[int] = mapped_column(Integer, ForeignKey("cookbook_outbox_messages.id"), nullable=False) + outbox_message_id: Mapped[int] = mapped_column( + Integer, ForeignKey("cookbook_outbox_messages.id"), nullable=False + ) started_at: Mapped[datetime] = mapped_column(DateTime, nullable=False, default=datetime.utcnow) finished_at: Mapped[datetime | None] = mapped_column(DateTime, nullable=True) outcome: Mapped[str] = mapped_column(String(20), nullable=False) @@ -61,4 +88,11 @@ class OutboxAttempt(db.Model): message: Mapped["OutboxMessage"] = relationship(back_populates="attempts_list") def to_dict(self) -> dict[str, object]: - return {"id": self.id, "outbox_message_id": self.outbox_message_id, "started_at": self.started_at.isoformat(), "finished_at": self.finished_at.isoformat() if self.finished_at else None, "outcome": self.outcome, "error_message": self.error_message} + return { + "id": self.id, + "outbox_message_id": self.outbox_message_id, + "started_at": self.started_at.isoformat(), + "finished_at": self.finished_at.isoformat() if self.finished_at else None, + "outcome": self.outcome, + "error_message": self.error_message, + } diff --git a/templates/flask/08-transactional-outbox/tests/test_app.py b/templates/flask/08-transactional-outbox/tests/test_app.py index 56125c6..84172d6 100644 --- a/templates/flask/08-transactional-outbox/tests/test_app.py +++ b/templates/flask/08-transactional-outbox/tests/test_app.py @@ -25,7 +25,13 @@ @pytest.fixture def app(tmp_path: Path): - return create_app({"TESTING": True, "SQLALCHEMY_DATABASE_URI": f"sqlite:///{tmp_path / 'test.db'}", "SQLALCHEMY_TRACK_MODIFICATIONS": False}) + return create_app( + { + "TESTING": True, + "SQLALCHEMY_DATABASE_URI": f"sqlite:///{tmp_path / 'test.db'}", + "SQLALCHEMY_TRACK_MODIFICATIONS": False, + } + ) @pytest.fixture @@ -34,6 +40,7 @@ def httpx_client(app): with httpx.Client(transport=transport, base_url="http://testserver") as client: yield client + def _create_invoice( httpx_client: httpx.Client, customer_email: str = "buyer@example.com",