diff --git a/fundamentals/pandas/01_read_sql.py b/fundamentals/pandas/01_read_sql.py index adf2cd2..dcf11c5 100644 --- a/fundamentals/pandas/01_read_sql.py +++ b/fundamentals/pandas/01_read_sql.py @@ -15,6 +15,8 @@ def main() -> int: try: with engine.begin() as conn: + # SECURITY: TABLE_NAME is a hardcoded constant. NEVER use f-string + # interpolation with text() for user-supplied values. _ = conn.execute(text(f"DROP TABLE IF EXISTS {TABLE_NAME}")) _ = conn.execute( text( @@ -60,6 +62,8 @@ def main() -> int: return 1 finally: with engine.begin() as conn: + # SECURITY: TABLE_NAME is a hardcoded constant. NEVER use f-string + # interpolation with text() for user-supplied values. _ = conn.execute(text(f"DROP TABLE IF EXISTS {TABLE_NAME}")) engine.dispose() diff --git a/fundamentals/pandas/02_read_sql_query_params.py b/fundamentals/pandas/02_read_sql_query_params.py index 88ba2ef..0c54e7b 100644 --- a/fundamentals/pandas/02_read_sql_query_params.py +++ b/fundamentals/pandas/02_read_sql_query_params.py @@ -15,6 +15,8 @@ def main() -> int: try: with engine.begin() as conn: + # SECURITY: TABLE_NAME is a hardcoded constant. NEVER use f-string + # interpolation with text() for user-supplied values. _ = conn.execute(text(f"DROP TABLE IF EXISTS {TABLE_NAME}")) _ = conn.execute( text( @@ -71,6 +73,8 @@ def main() -> int: return 1 finally: with engine.begin() as conn: + # SECURITY: TABLE_NAME is a hardcoded constant. NEVER use f-string + # interpolation with text() for user-supplied values. _ = conn.execute(text(f"DROP TABLE IF EXISTS {TABLE_NAME}")) engine.dispose() diff --git a/fundamentals/pandas/03_clean_and_transform.py b/fundamentals/pandas/03_clean_and_transform.py index aeb52e3..9120c15 100644 --- a/fundamentals/pandas/03_clean_and_transform.py +++ b/fundamentals/pandas/03_clean_and_transform.py @@ -19,6 +19,8 @@ def main() -> int: try: with engine.begin() as conn: + # SECURITY: TABLE_NAME is a hardcoded constant. NEVER use f-string + # interpolation with text() for user-supplied values. _ = conn.execute(text(f"DROP TABLE IF EXISTS {TABLE_NAME}")) _ = conn.execute( text( @@ -78,6 +80,8 @@ def main() -> int: return 1 finally: with engine.begin() as conn: + # SECURITY: TABLE_NAME is a hardcoded constant. NEVER use f-string + # interpolation with text() for user-supplied values. _ = conn.execute(text(f"DROP TABLE IF EXISTS {TABLE_NAME}")) engine.dispose() diff --git a/fundamentals/pandas/04_groupby_report.py b/fundamentals/pandas/04_groupby_report.py index 6b5b7ec..d04c0b0 100644 --- a/fundamentals/pandas/04_groupby_report.py +++ b/fundamentals/pandas/04_groupby_report.py @@ -15,6 +15,8 @@ def main() -> int: try: with engine.begin() as conn: + # SECURITY: TABLE_NAME is a hardcoded constant. NEVER use f-string + # interpolation with text() for user-supplied values. _ = conn.execute(text(f"DROP TABLE IF EXISTS {TABLE_NAME}")) _ = conn.execute( text( @@ -76,6 +78,8 @@ def main() -> int: return 1 finally: with engine.begin() as conn: + # SECURITY: TABLE_NAME is a hardcoded constant. NEVER use f-string + # interpolation with text() for user-supplied values. _ = conn.execute(text(f"DROP TABLE IF EXISTS {TABLE_NAME}")) engine.dispose() diff --git a/fundamentals/pandas/05_to_sql_append_replace.py b/fundamentals/pandas/05_to_sql_append_replace.py index 0969161..6ec2655 100644 --- a/fundamentals/pandas/05_to_sql_append_replace.py +++ b/fundamentals/pandas/05_to_sql_append_replace.py @@ -29,6 +29,8 @@ def main() -> int: try: with engine.begin() as conn: + # SECURITY: TABLE_NAME is a hardcoded constant. NEVER use f-string + # interpolation with text() for user-supplied values. _ = conn.execute(text(f"DROP TABLE IF EXISTS {TABLE_NAME}")) print("=== Base DataFrame (replace) ===") @@ -50,6 +52,8 @@ def main() -> int: return 1 finally: with engine.begin() as conn: + # SECURITY: TABLE_NAME is a hardcoded constant. NEVER use f-string + # interpolation with text() for user-supplied values. _ = conn.execute(text(f"DROP TABLE IF EXISTS {TABLE_NAME}")) engine.dispose() diff --git a/fundamentals/pandas/06_export_csv.py b/fundamentals/pandas/06_export_csv.py index cd47039..b09f4a4 100644 --- a/fundamentals/pandas/06_export_csv.py +++ b/fundamentals/pandas/06_export_csv.py @@ -18,6 +18,8 @@ def main() -> int: try: with engine.begin() as conn: + # SECURITY: TABLE_NAME is a hardcoded constant. NEVER use f-string + # interpolation with text() for user-supplied values. _ = conn.execute(text(f"DROP TABLE IF EXISTS {TABLE_NAME}")) _ = conn.execute( text( @@ -98,6 +100,8 @@ def main() -> int: return 1 finally: with engine.begin() as conn: + # SECURITY: TABLE_NAME is a hardcoded constant. NEVER use f-string + # interpolation with text() for user-supplied values. _ = conn.execute(text(f"DROP TABLE IF EXISTS {TABLE_NAME}")) engine.dispose() diff --git a/fundamentals/parameterized-queries/04_parameterized.py b/fundamentals/parameterized-queries/04_parameterized.py index e6ca285..a73c9e1 100644 --- a/fundamentals/parameterized-queries/04_parameterized.py +++ b/fundamentals/parameterized-queries/04_parameterized.py @@ -1,5 +1,9 @@ """04_parameterized.py — Parameterized queries and batch operations. +NOTE: This recipe is identical to ``fundamentals/pycubrid/04_prepared.py``. +It is duplicated here for topic-based discovery. Changes should be made +in the canonical location: ``fundamentals/pycubrid/04_prepared.py``. + Demonstrates: - Parameterized queries (qmark style: ?) - Preventing SQL injection diff --git a/fundamentals/pycubrid/07_merge_upsert.py b/fundamentals/pycubrid/07_merge_upsert.py index 82c10fe..569f73d 100644 --- a/fundamentals/pycubrid/07_merge_upsert.py +++ b/fundamentals/pycubrid/07_merge_upsert.py @@ -9,10 +9,12 @@ INSERT ... ON DUPLICATE KEY UPDATE while keeping the same behavior. """ +from __future__ import annotations + # pyright: reportAttributeAccessIssue=false, reportMissingImports=false import pycubrid -from datetime import datetime +from datetime import datetime, timezone DB_CONFIG = { @@ -68,7 +70,7 @@ def load_snapshot(cursor, rows): def merge_snapshot(cursor): - now_utc = datetime.utcnow() + now_utc = datetime.now(timezone.utc) cursor.execute( """ MERGE INTO cookbook_products p diff --git a/fundamentals/pycubrid/08_hierarchy_connect_by.py b/fundamentals/pycubrid/08_hierarchy_connect_by.py index 5d4f202..17c7882 100644 --- a/fundamentals/pycubrid/08_hierarchy_connect_by.py +++ b/fundamentals/pycubrid/08_hierarchy_connect_by.py @@ -6,6 +6,8 @@ - Traversing a subtree from a selected node """ +from __future__ import annotations + # pyright: reportAttributeAccessIssue=false, reportMissingImports=false import pycubrid diff --git a/fundamentals/pycubrid/09_serial_order_numbers.py b/fundamentals/pycubrid/09_serial_order_numbers.py index 21fe300..ff94212 100644 --- a/fundamentals/pycubrid/09_serial_order_numbers.py +++ b/fundamentals/pycubrid/09_serial_order_numbers.py @@ -6,10 +6,12 @@ - Listing generated order numbers and totals """ +from __future__ import annotations + # pyright: reportAttributeAccessIssue=false, reportMissingImports=false import pycubrid -from datetime import datetime +from datetime import datetime, timezone DB_CONFIG = { @@ -71,7 +73,7 @@ def next_order_number(cursor): def create_order(cursor, customer, lines): order_no = next_order_number(cursor) - created_at_utc = datetime.utcnow() + created_at_utc = datetime.now(timezone.utc) total_cents = 0 line_rows = [] for product_name, qty, unit_price_cents in lines: diff --git a/fundamentals/pycubrid/10_collection_columns.py b/fundamentals/pycubrid/10_collection_columns.py index f6f2255..2266c5c 100644 --- a/fundamentals/pycubrid/10_collection_columns.py +++ b/fundamentals/pycubrid/10_collection_columns.py @@ -9,6 +9,8 @@ Collection literals use CUBRID syntax: SET{...}, MULTISET{...}, LIST{...}. """ +from __future__ import annotations + # pyright: reportAttributeAccessIssue=false, reportMissingImports=false import pycubrid diff --git a/fundamentals/pycubrid/11_bulk_etl_pipeline.py b/fundamentals/pycubrid/11_bulk_etl_pipeline.py index f9bab14..d8b68fe 100644 --- a/fundamentals/pycubrid/11_bulk_etl_pipeline.py +++ b/fundamentals/pycubrid/11_bulk_etl_pipeline.py @@ -18,7 +18,7 @@ import datetime import pycubrid # type: ignore[import-not-found] -CONNECT = getattr(pycubrid, "connect") +# Use pycubrid.connect() directly (issue #31) DB_CONFIG = { "host": "localhost", @@ -30,7 +30,7 @@ def get_connection(): - return CONNECT(**DB_CONFIG) + return pycubrid.connect(**DB_CONFIG) def setup_schema(conn) -> None: diff --git a/fundamentals/pycubrid/12_pool_retry_worker.py b/fundamentals/pycubrid/12_pool_retry_worker.py index 3d5fb04..05bffd1 100644 --- a/fundamentals/pycubrid/12_pool_retry_worker.py +++ b/fundamentals/pycubrid/12_pool_retry_worker.py @@ -15,7 +15,7 @@ import pycubrid # type: ignore[import-not-found] -CONNECT = getattr(pycubrid, "connect") +# Use pycubrid.connect() directly (issue #31) OPERATIONAL_ERROR = getattr(pycubrid, "OperationalError", Exception) INTERFACE_ERROR = getattr(pycubrid, "InterfaceError", Exception) @@ -29,7 +29,7 @@ def get_connection(): - return CONNECT(**DB_CONFIG) + return pycubrid.connect(**DB_CONFIG) class ConnectionPool: diff --git a/fundamentals/pycubrid/13_atomic_counters.py b/fundamentals/pycubrid/13_atomic_counters.py index 4c3219d..eb0063d 100644 --- a/fundamentals/pycubrid/13_atomic_counters.py +++ b/fundamentals/pycubrid/13_atomic_counters.py @@ -18,7 +18,7 @@ import pycubrid # type: ignore[import-not-found] -CONNECT = getattr(pycubrid, "connect") +# Use pycubrid.connect() directly (issue #31) DB_CONFIG = { "host": "localhost", @@ -30,7 +30,7 @@ def get_connection(): - return CONNECT(**DB_CONFIG) + return pycubrid.connect(**DB_CONFIG) def setup_schema(conn) -> None: diff --git a/fundamentals/pycubrid/14_manual_cascade_delete.py b/fundamentals/pycubrid/14_manual_cascade_delete.py index edeac44..c1378d0 100644 --- a/fundamentals/pycubrid/14_manual_cascade_delete.py +++ b/fundamentals/pycubrid/14_manual_cascade_delete.py @@ -13,7 +13,7 @@ import pycubrid # type: ignore[import-not-found] -CONNECT = getattr(pycubrid, "connect") +# Use pycubrid.connect() directly (issue #31) DB_CONFIG = { "host": "localhost", @@ -25,7 +25,7 @@ def get_connection(): - return CONNECT(**DB_CONFIG) + return pycubrid.connect(**DB_CONFIG) def setup_schema(conn) -> None: diff --git a/templates/api-service-fastapi/app/crud.py b/templates/api-service-fastapi/app/crud.py index cfe8b82..61bc295 100644 --- a/templates/api-service-fastapi/app/crud.py +++ b/templates/api-service-fastapi/app/crud.py @@ -7,7 +7,7 @@ from pydantic import BaseModel from sqlalchemy import Select, select from sqlalchemy.exc import IntegrityError -from sqlalchemy.orm import Session +from sqlalchemy.orm import Session, selectinload from app import models @@ -75,6 +75,18 @@ class ItemRepository(Repository[models.CookbookItem, BaseModel, BaseModel]): def __init__(self) -> None: super().__init__(models.CookbookItem) + def _base_query(self) -> Select[tuple[models.CookbookItem]]: + # Eager-load category to prevent N+1 queries and DetachedInstanceError (issue #33). + return select(models.CookbookItem).options(selectinload(models.CookbookItem.category)) + + def get(self, db: Session, entity_id: int) -> models.CookbookItem: + # Override to use _base_query for eager loading (issue #33). + stmt = self._base_query().where(models.CookbookItem.id == entity_id) + entity = db.execute(stmt).scalar_one_or_none() + if entity is None: + raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="Item not found") + return entity + def create(self, db: Session, payload: BaseModel) -> models.CookbookItem: category = db.get(models.CookbookCategory, payload.category_id) if category is None: diff --git a/templates/api-service-fastapi/app/models.py b/templates/api-service-fastapi/app/models.py index ed44e06..e0451ae 100644 --- a/templates/api-service-fastapi/app/models.py +++ b/templates/api-service-fastapi/app/models.py @@ -19,10 +19,9 @@ class CookbookCategory(Base): "CookbookItem", back_populates="category", cascade="all, delete-orphan", - passive_deletes=True, + # passive_deletes removed — CUBRID FK cascade support varies (issue #33). ) - class CookbookItem(Base): __tablename__ = "cookbook_items" diff --git a/templates/api-service-fastapi/app/routes/__init__.py b/templates/api-service-fastapi/app/routes/__init__.py index 3798575..c489dcc 100644 --- a/templates/api-service-fastapi/app/routes/__init__.py +++ b/templates/api-service-fastapi/app/routes/__init__.py @@ -2,9 +2,11 @@ from fastapi import APIRouter +from app.routes.categories import router as categories_router from app.routes.health import router as health_router from app.routes.items import router as items_router api_router = APIRouter() +api_router.include_router(categories_router) api_router.include_router(health_router) api_router.include_router(items_router) diff --git a/templates/api-service-fastapi/app/routes/categories.py b/templates/api-service-fastapi/app/routes/categories.py new file mode 100644 index 0000000..d1679b6 --- /dev/null +++ b/templates/api-service-fastapi/app/routes/categories.py @@ -0,0 +1,47 @@ +"""Category management routes.""" + +from __future__ import annotations + +from fastapi import APIRouter, Depends, Query, Response, status +from sqlalchemy.orm import Session + +from app import crud, schemas +from app.database import get_db + +router = APIRouter(prefix="/categories", tags=["categories"]) + + +@router.get("", response_model=list[schemas.CategoryRead]) +def list_categories( + skip: int = Query(default=0, ge=0), + limit: int = Query(default=20, ge=1, le=100), + db: Session = Depends(get_db), +) -> list[schemas.CategoryRead]: + return crud.category_repository.list(db, skip=skip, limit=limit) + + +@router.get("/{category_id}", response_model=schemas.CategoryRead) +def get_category(category_id: int, db: Session = Depends(get_db)) -> schemas.CategoryRead: + return crud.category_repository.get(db, category_id) + + +@router.post("", response_model=schemas.CategoryRead, status_code=status.HTTP_201_CREATED) +def create_category( + payload: schemas.CategoryCreate, db: Session = Depends(get_db) +) -> schemas.CategoryRead: + return crud.category_repository.create(db, payload) + + +@router.put("/{category_id}", response_model=schemas.CategoryRead) +def update_category( + category_id: int, + payload: schemas.CategoryUpdate, + db: Session = Depends(get_db), +) -> schemas.CategoryRead: + return crud.category_repository.update(db, category_id, payload) + + +@router.delete("/{category_id}", status_code=status.HTTP_204_NO_CONTENT) +def delete_category(category_id: int, db: Session = Depends(get_db)) -> Response: + crud.category_repository.delete(db, category_id) + return Response(status_code=status.HTTP_204_NO_CONTENT) diff --git a/templates/api-service-fastapi/recipes/01-basic-crud/models.py b/templates/api-service-fastapi/recipes/01-basic-crud/models.py index 9a6df83..6430daa 100644 --- a/templates/api-service-fastapi/recipes/01-basic-crud/models.py +++ b/templates/api-service-fastapi/recipes/01-basic-crud/models.py @@ -1,5 +1,5 @@ # pyright: reportImplicitRelativeImport=false, reportUnusedParameter=false -from datetime import datetime +from datetime import datetime, timezone from sqlalchemy import DateTime, Integer, String, Text from sqlalchemy.orm import Mapped, mapped_column @@ -8,7 +8,7 @@ def utc_now() -> datetime: - return datetime.utcnow() + return datetime.now(timezone.utc) class Task(Base): diff --git a/templates/api-service-fastapi/recipes/02-orders/models.py b/templates/api-service-fastapi/recipes/02-orders/models.py index ce1a539..b8b540e 100644 --- a/templates/api-service-fastapi/recipes/02-orders/models.py +++ b/templates/api-service-fastapi/recipes/02-orders/models.py @@ -1,5 +1,5 @@ # pyright: reportImplicitRelativeImport=false, reportUnusedParameter=false -from datetime import datetime +from datetime import datetime, timezone from sqlalchemy import DateTime, ForeignKey, Integer, String from sqlalchemy.orm import Mapped, mapped_column, relationship @@ -8,7 +8,7 @@ def utc_now() -> datetime: - return datetime.utcnow() + return datetime.now(timezone.utc) class Customer(Base): 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 46f399a..50650f3 100644 --- a/templates/api-service-fastapi/recipes/03-catalog-sync/models.py +++ b/templates/api-service-fastapi/recipes/03-catalog-sync/models.py @@ -1,5 +1,5 @@ # pyright: reportImplicitRelativeImport=false, reportUnusedParameter=false -from datetime import datetime +from datetime import datetime, timezone from sqlalchemy import DateTime, ForeignKey, Integer, String, Text, UniqueConstraint from sqlalchemy.orm import Mapped, mapped_column, relationship @@ -8,7 +8,7 @@ def utc_now() -> datetime: - return datetime.utcnow() + return datetime.now(timezone.utc) class CatalogItem(Base): __tablename__: str = "cookbook_catalog_items" 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 8b35bed..5f28b1d 100644 --- a/templates/api-service-fastapi/recipes/04-audit-trail/models.py +++ b/templates/api-service-fastapi/recipes/04-audit-trail/models.py @@ -1,5 +1,5 @@ # pyright: reportImplicitRelativeImport=false, reportUnusedParameter=false -from datetime import datetime +from datetime import datetime, timezone from sqlalchemy import DateTime, ForeignKey, Integer, String, Text, UniqueConstraint from sqlalchemy.orm import Mapped, mapped_column, relationship @@ -8,7 +8,7 @@ def utc_now() -> datetime: - return datetime.utcnow() + return datetime.now(timezone.utc) class UserProfile(Base): __tablename__: str = "cookbook_user_profiles" 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 ef61db0..7385a68 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,5 +1,5 @@ # pyright: reportImplicitRelativeImport=false, reportUnusedParameter=false -from datetime import datetime +from datetime import datetime, timezone from sqlalchemy import DateTime, ForeignKey, Integer, String, Text, UniqueConstraint from sqlalchemy.orm import Mapped, mapped_column, relationship @@ -8,7 +8,7 @@ def utc_now() -> datetime: - return datetime.utcnow() + return datetime.now(timezone.utc) class Tenant(Base): __tablename__: str = "cookbook_tenants" 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 95f5860..35a46c0 100644 --- a/templates/api-service-fastapi/recipes/06-price-books/models.py +++ b/templates/api-service-fastapi/recipes/06-price-books/models.py @@ -1,5 +1,5 @@ # pyright: reportImplicitRelativeImport=false, reportUnusedParameter=false -from datetime import datetime +from datetime import datetime, timezone from sqlalchemy import DateTime, ForeignKey, Integer, String, Text, UniqueConstraint from sqlalchemy.orm import Mapped, mapped_column, relationship @@ -8,7 +8,7 @@ def utc_now() -> datetime: - return datetime.utcnow() + return datetime.now(timezone.utc) class PriceProduct(Base): __tablename__: str = "cookbook_price_products" 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 1c0aab9..c5d9923 100644 --- a/templates/api-service-fastapi/recipes/07-document-publishing/models.py +++ b/templates/api-service-fastapi/recipes/07-document-publishing/models.py @@ -1,5 +1,5 @@ # pyright: reportImplicitRelativeImport=false, reportUnusedParameter=false -from datetime import datetime +from datetime import datetime, timezone from sqlalchemy import DateTime, ForeignKey, Integer, String, Text, UniqueConstraint from sqlalchemy.orm import Mapped, mapped_column, relationship @@ -8,7 +8,7 @@ def utc_now() -> datetime: - return datetime.utcnow() + return datetime.now(timezone.utc) class Document(Base): __tablename__: str = "cookbook_documents" 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 6c2ab21..51f98ef 100644 --- a/templates/api-service-fastapi/recipes/08-webhook-inbox/models.py +++ b/templates/api-service-fastapi/recipes/08-webhook-inbox/models.py @@ -1,5 +1,5 @@ # pyright: reportImplicitRelativeImport=false, reportUnusedParameter=false -from datetime import datetime +from datetime import datetime, timezone from sqlalchemy import DateTime, ForeignKey, Integer, String, Text, UniqueConstraint from sqlalchemy.orm import Mapped, mapped_column, relationship @@ -8,7 +8,7 @@ def utc_now() -> datetime: - return datetime.utcnow() + return datetime.now(timezone.utc) class Shipment(Base): __tablename__: str = "cookbook_shipments" diff --git a/templates/api-service-fastapi/recipes/09-saga/routes.py b/templates/api-service-fastapi/recipes/09-saga/routes.py index 2314bd7..a6398a6 100644 --- a/templates/api-service-fastapi/recipes/09-saga/routes.py +++ b/templates/api-service-fastapi/recipes/09-saga/routes.py @@ -1,5 +1,5 @@ # pyright: basic -from datetime import datetime +from datetime import datetime, timezone from typing import Any, cast from fastapi import APIRouter, Body, Depends, HTTPException @@ -87,7 +87,7 @@ def _compensate(db: Session, order, completed_steps: list[str]) -> None: .values( compensation_attempt_count=SagaStep.compensation_attempt_count + 1, status="compensated", - compensated_at=datetime.utcnow(), + compensated_at=datetime.now(timezone.utc), ) ) @@ -224,7 +224,7 @@ def execute_order(order_key: str, db: Session = Depends(get_db)): db.execute( update(SagaStep) .where(and_(SagaStep.order_id == order.id, SagaStep.step_name == "reserve_inventory")) - .values(status="done", executed_at=datetime.utcnow()) + .values(status="done", executed_at=datetime.now(timezone.utc)) ) completed_steps.append("reserve_inventory") db.commit() @@ -280,7 +280,7 @@ def execute_order(order_key: str, db: Session = Depends(get_db)): db.execute( update(SagaStep) .where(and_(SagaStep.order_id == order.id, SagaStep.step_name == "charge_payment")) - .values(status="done", executed_at=datetime.utcnow()) + .values(status="done", executed_at=datetime.now(timezone.utc)) ) completed_steps.append("charge_payment") db.commit() @@ -291,7 +291,7 @@ def execute_order(order_key: str, db: Session = Depends(get_db)): .values( attempt_count=SagaStep.attempt_count + 1, status="done", - executed_at=datetime.utcnow(), + executed_at=datetime.now(timezone.utc), ) ) db.execute( @@ -320,7 +320,7 @@ def recover_order( return order # Check timeout: order must have been stuck longer than timeout_seconds - now = datetime.utcnow() + now = datetime.now(timezone.utc) if order.updated_at is not None: stuck_seconds = (now - order.updated_at).total_seconds() if stuck_seconds < payload.timeout_seconds: diff --git a/templates/api-service-fastapi/recipes/11-rate-limiter/routes.py b/templates/api-service-fastapi/recipes/11-rate-limiter/routes.py index 6c2a190..ea951e5 100644 --- a/templates/api-service-fastapi/recipes/11-rate-limiter/routes.py +++ b/templates/api-service-fastapi/recipes/11-rate-limiter/routes.py @@ -1,7 +1,7 @@ from __future__ import annotations # pyright: reportGeneralTypeIssues=false, reportImplicitRelativeImport=false -from datetime import datetime, timedelta +from datetime import datetime, timedelta, timezone from fastapi import APIRouter, Depends, HTTPException, Response, status from sqlalchemy import update @@ -25,7 +25,7 @@ def utcnow() -> datetime: - return datetime.utcnow() + return datetime.now(timezone.utc) def _rotate_window_if_needed(window: ClientRateWindow, now: datetime, window_seconds: int) -> None: diff --git a/templates/flask/04-purchase-orders/app.py b/templates/flask/04-purchase-orders/app.py index a4c925f..c1f92b1 100644 --- a/templates/flask/04-purchase-orders/app.py +++ b/templates/flask/04-purchase-orders/app.py @@ -2,7 +2,7 @@ from __future__ import annotations from collections.abc import Mapping -from datetime import datetime +from datetime import datetime, timezone from typing import cast from flask import Blueprint, jsonify, request @@ -177,7 +177,7 @@ 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.utcnow(), 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 +200,7 @@ 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.utcnow(), 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 @@ -255,7 +255,7 @@ def receive_purchase_order(order_id: int): new_values: dict[str, object] = {"version": order.version + 1} if len(order.lines) > 0 and all(line.received_qty >= line.quantity for line in order.lines): new_values["status"] = "fulfilled" - new_values["fulfilled_at"] = datetime.utcnow() + new_values["fulfilled_at"] = datetime.now(timezone.utc) result = db.session.execute( update(PurchaseOrder) diff --git a/templates/flask/05-batch-operations/app.py b/templates/flask/05-batch-operations/app.py index 0d7c21e..99a27b4 100644 --- a/templates/flask/05-batch-operations/app.py +++ b/templates/flask/05-batch-operations/app.py @@ -3,7 +3,7 @@ import json from collections.abc import Mapping, Sequence -from datetime import datetime +from datetime import datetime, timezone from typing import cast from flask import Blueprint, jsonify, request @@ -182,7 +182,7 @@ def submit_price_update_job(): job.success_cnt = success_cnt job.failed_cnt = failed_cnt job.status = "completed" - job.finished_at = datetime.utcnow() + job.finished_at = datetime.now(timezone.utc) try: db.session.commit() diff --git a/templates/flask/06-case-triage/app.py b/templates/flask/06-case-triage/app.py index 3db9405..66be274 100644 --- a/templates/flask/06-case-triage/app.py +++ b/templates/flask/06-case-triage/app.py @@ -2,7 +2,7 @@ from __future__ import annotations from collections.abc import Mapping -from datetime import datetime, timedelta +from datetime import datetime, timedelta, timezone from typing import cast from flask import Blueprint, jsonify, request @@ -144,7 +144,7 @@ def claim_case(case_id: int): except LookupError: return jsonify({"error": "Case not found."}), 404 - claim_error = _claim_case(review_case, agent, datetime.utcnow()) + claim_error = _claim_case(review_case, agent, datetime.now(timezone.utc)) if claim_error is not None: return jsonify(claim_error[0]), claim_error[1] return jsonify(review_case.to_dict()) @@ -203,7 +203,7 @@ def resolve_case(case_id: int): except LookupError: return jsonify({"error": "Case not found."}), 404 - now = datetime.utcnow() + now = datetime.now(timezone.utc) if review_case.status != "claimed" or review_case.claimed_by != agent: return jsonify({"error": "Case can only be resolved by current claimant."}), 409 if review_case.lease_expires_at is None or review_case.lease_expires_at <= now: @@ -248,7 +248,7 @@ def claim_next_case(): return jsonify({"error": str(exc)}), 400 for _ in range(2): - now = datetime.utcnow() + now = datetime.now(timezone.utc) next_case = db.session.execute( select(ReviewCase) .where( diff --git a/templates/flask/06-case-triage/tests/test_app.py b/templates/flask/06-case-triage/tests/test_app.py index 12d11d4..9e348d6 100644 --- a/templates/flask/06-case-triage/tests/test_app.py +++ b/templates/flask/06-case-triage/tests/test_app.py @@ -3,7 +3,7 @@ from pathlib import Path import sys -from datetime import datetime, timedelta +from datetime import datetime, timedelta, timezone from typing import TypedDict, cast import httpx @@ -111,7 +111,7 @@ def test_claim_expired_lease(httpx_client: httpx.Client, app: Flask) -> None: _ = db.session.execute( update(ReviewCase) .where(ReviewCase.id == created_case["id"]) - .values(lease_expires_at=datetime.utcnow() - timedelta(minutes=1)) + .values(lease_expires_at=datetime.now(timezone.utc) - timedelta(minutes=1)) ) db.session.commit() diff --git a/templates/flask/07-vendor-feed/app.py b/templates/flask/07-vendor-feed/app.py index e9baec2..b04f906 100644 --- a/templates/flask/07-vendor-feed/app.py +++ b/templates/flask/07-vendor-feed/app.py @@ -2,7 +2,7 @@ from __future__ import annotations from collections.abc import Mapping, Sequence -from datetime import datetime +from datetime import datetime, timezone from typing import cast from flask import Blueprint, jsonify, request @@ -187,7 +187,7 @@ def validate_import_batch(batch_id: int): invalid_count += 1 batch.status = "validated" - batch.validated_at = datetime.utcnow() + batch.validated_at = datetime.now(timezone.utc) db.session.commit() return jsonify( @@ -265,7 +265,7 @@ def promote_import_batch(batch_id: int): skipped_count += 1 batch.status = "promoted" - batch.promoted_at = datetime.utcnow() + batch.promoted_at = datetime.now(timezone.utc) db.session.commit() return jsonify( diff --git a/templates/flask/08-transactional-outbox/app.py b/templates/flask/08-transactional-outbox/app.py index 9ab2496..9eb185e 100644 --- a/templates/flask/08-transactional-outbox/app.py +++ b/templates/flask/08-transactional-outbox/app.py @@ -3,7 +3,7 @@ import json from collections.abc import Mapping -from datetime import datetime, timedelta +from datetime import datetime, timedelta, timezone from typing import cast from flask import Blueprint, jsonify, request @@ -70,7 +70,7 @@ def send_invoice(invoice_id: int): if invoice.status == "sent": return jsonify({"error": "Invoice already sent."}), 409 - now = datetime.utcnow() + now = datetime.now(timezone.utc) invoice.status = "sent" invoice.sent_at = now @@ -116,7 +116,7 @@ def lease_outbox_messages(): if not processor_id: return jsonify({"error": "processor_id is required"}), 400 - now = datetime.utcnow() + now = datetime.now(timezone.utc) lease_until = now + timedelta(minutes=5) messages = ( db.session.execute( @@ -178,7 +178,7 @@ def acknowledge_outbox_message(message_id: int): if message is None: return jsonify({"error": "Outbox message not found."}), 404 - now = datetime.utcnow() + now = datetime.now(timezone.utc) if message.leased_until is None or message.leased_until <= now: return jsonify({"error": "Outbox message is not currently leased."}), 409 if message.leased_by != processor_id: @@ -211,7 +211,7 @@ def fail_outbox_message(message_id: int): if message is None: return jsonify({"error": "Outbox message not found."}), 404 - now = datetime.utcnow() + now = datetime.now(timezone.utc) if message.leased_until is None or message.leased_until <= now: return jsonify({"error": "Outbox message is not currently leased."}), 409 if message.leased_by != processor_id: diff --git a/templates/flask/08-transactional-outbox/tests/test_app.py b/templates/flask/08-transactional-outbox/tests/test_app.py index eba710a..56125c6 100644 --- a/templates/flask/08-transactional-outbox/tests/test_app.py +++ b/templates/flask/08-transactional-outbox/tests/test_app.py @@ -3,7 +3,7 @@ from pathlib import Path import sys -from datetime import datetime, timedelta +from datetime import datetime, timedelta, timezone from typing import Any, cast import httpx @@ -131,7 +131,7 @@ def test_fail_three_times_dead_letters(app: Flask, httpx_client: httpx.Client) - db.session.execute( update(OutboxMessage) .where(OutboxMessage.id == msg["id"]) - .values(next_attempt_at=datetime.utcnow() - timedelta(seconds=1)) + .values(next_attempt_at=datetime.now(timezone.utc) - timedelta(seconds=1)) ) db.session.commit() @@ -149,7 +149,7 @@ def test_fail_three_times_dead_letters(app: Flask, httpx_client: httpx.Client) - db.session.execute( update(OutboxMessage) .where(OutboxMessage.id == msg["id"]) - .values(next_attempt_at=datetime.utcnow() - timedelta(seconds=1)) + .values(next_attempt_at=datetime.now(timezone.utc) - timedelta(seconds=1)) ) db.session.commit() diff --git a/templates/flask/10-workflow-engine/app.py b/templates/flask/10-workflow-engine/app.py index a2e24fb..22230db 100644 --- a/templates/flask/10-workflow-engine/app.py +++ b/templates/flask/10-workflow-engine/app.py @@ -1,7 +1,7 @@ from __future__ import annotations import os -from datetime import datetime +from datetime import datetime, timezone from typing import Any, cast from flask import Flask, jsonify, request @@ -144,7 +144,7 @@ def _set_run_completed_if_terminal(workflow_run: WorkflowRun) -> None: if step_runs and all(step_run.state in terminal_states for step_run in step_runs.values()): has_failure = any(step_run.state == "failed" for step_run in step_runs.values()) workflow_run.state = "failed" if has_failure else "completed" - workflow_run.completed_at = datetime.utcnow() + workflow_run.completed_at = datetime.now(timezone.utc) def _serialize_workflow(workflow: WorkflowDefinition) -> dict[str, object]: @@ -410,7 +410,7 @@ def tick_workflow_run(run_key: str): { "state": "running", "version": WorkflowStepRun.version + 1, - "started_at": step_run.started_at or datetime.utcnow(), + "started_at": step_run.started_at or datetime.now(timezone.utc), } ) ) @@ -442,7 +442,7 @@ def tick_workflow_run(run_key: str): "last_error_text": None, "attempt_count": claimed_run.attempt_count + 1, "version": WorkflowStepRun.version + 1, - "finished_at": datetime.utcnow(), + "finished_at": datetime.now(timezone.utc), } ) ) @@ -460,7 +460,7 @@ def tick_workflow_run(run_key: str): "last_error_text": "forced failure", "attempt_count": claimed_run.attempt_count + 1, "version": WorkflowStepRun.version + 1, - "finished_at": datetime.utcnow(), + "finished_at": datetime.now(timezone.utc), } ) ) @@ -495,7 +495,7 @@ def approve_step_run(step_run_id: int): { "state": "completed", "version": WorkflowStepRun.version + 1, - "finished_at": datetime.utcnow(), + "finished_at": datetime.now(timezone.utc), } ) ) @@ -579,7 +579,7 @@ def skip_step_run(step_run_id: int): { "state": "skipped", "version": WorkflowStepRun.version + 1, - "finished_at": datetime.utcnow(), + "finished_at": datetime.now(timezone.utc), } ) ) diff --git a/templates/flask/11-inventory-reservation/app.py b/templates/flask/11-inventory-reservation/app.py index e337e24..4899995 100644 --- a/templates/flask/11-inventory-reservation/app.py +++ b/templates/flask/11-inventory-reservation/app.py @@ -3,7 +3,7 @@ import os from typing import Any, cast -from datetime import datetime, timedelta +from datetime import datetime, timedelta, timezone from flask import Blueprint, Flask, jsonify, request from sqlalchemy import and_, select, update @@ -107,7 +107,7 @@ def create_reservation(): if item is None: return jsonify({"error": "item not found"}), 404 - now = datetime.utcnow() + now = datetime.now(timezone.utc) expires_at = now + timedelta(seconds=ttl_seconds) updated = db.session.execute( @@ -178,7 +178,7 @@ def confirm_reservation(reservation_key: str): if reservation is None: return jsonify({"error": "reservation not found"}), 404 - now = datetime.utcnow() + now = datetime.now(timezone.utc) if reservation.state != "active": return jsonify({"error": "reservation is not active"}), 409 if reservation.expires_at <= now: @@ -239,7 +239,7 @@ def cancel_reservation(reservation_key: str): if reservation.state != "active": return jsonify({"error": "reservation is not active"}), 409 - now = datetime.utcnow() + now = datetime.now(timezone.utc) reservation_update = db.session.execute( update(StockReservation) .where( @@ -285,7 +285,7 @@ def cancel_reservation(reservation_key: str): @api.post("/sweeps/expire") def expire_reservations(): - now = datetime.utcnow() + now = datetime.now(timezone.utc) sweep = ExpirySweep() sweep.started_at = now sweep.status = "running" @@ -348,7 +348,7 @@ def expire_reservations(): except Exception: failed_count += 1 - sweep.finished_at = datetime.utcnow() + sweep.finished_at = datetime.now(timezone.utc) sweep.status = "completed" if failed_count == 0 else "completed_with_errors" sweep.expired_count = expired_count if failed_count > 0: diff --git a/templates/flask/11-inventory-reservation/tests/test_app.py b/templates/flask/11-inventory-reservation/tests/test_app.py index a128812..9654379 100644 --- a/templates/flask/11-inventory-reservation/tests/test_app.py +++ b/templates/flask/11-inventory-reservation/tests/test_app.py @@ -1,4 +1,4 @@ -from datetime import datetime, timedelta +from datetime import datetime, timedelta, timezone import pytest from sqlalchemy import select, update @@ -81,7 +81,7 @@ def test_confirm_expired_422(client): db.session.execute( update(StockReservation) .where(StockReservation.reservation_key == "res-ex") - .values(expires_at=datetime.utcnow() - timedelta(seconds=1)) + .values(expires_at=datetime.now(timezone.utc) - timedelta(seconds=1)) ) db.session.commit() @@ -114,7 +114,7 @@ def test_sweep_expires_stale(client): db.session.execute( update(StockReservation) .where(StockReservation.reservation_key == "res-sw") - .values(expires_at=datetime.utcnow() - timedelta(seconds=5)) + .values(expires_at=datetime.now(timezone.utc) - timedelta(seconds=5)) ) db.session.commit() @@ -149,7 +149,7 @@ def test_sweep_savepoint_isolation(client): db.session.execute( update(StockReservation) .where(StockReservation.reservation_key.in_(["res-good", "res-bad"])) - .values(expires_at=datetime.utcnow() - timedelta(seconds=5)) + .values(expires_at=datetime.now(timezone.utc) - timedelta(seconds=5)) ) bad_item = db.session.execute( select(InventoryItem).where(InventoryItem.sku == "SKU-BAD")