Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions fundamentals/pandas/01_read_sql.py
Original file line number Diff line number Diff line change
Expand Up @@ -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(
Expand Down Expand Up @@ -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()

Expand Down
4 changes: 4 additions & 0 deletions fundamentals/pandas/02_read_sql_query_params.py
Original file line number Diff line number Diff line change
Expand Up @@ -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(
Expand Down Expand Up @@ -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()

Expand Down
4 changes: 4 additions & 0 deletions fundamentals/pandas/03_clean_and_transform.py
Original file line number Diff line number Diff line change
Expand Up @@ -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(
Expand Down Expand Up @@ -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()

Expand Down
4 changes: 4 additions & 0 deletions fundamentals/pandas/04_groupby_report.py
Original file line number Diff line number Diff line change
Expand Up @@ -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(
Expand Down Expand Up @@ -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()

Expand Down
4 changes: 4 additions & 0 deletions fundamentals/pandas/05_to_sql_append_replace.py
Original file line number Diff line number Diff line change
Expand Up @@ -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) ===")
Expand All @@ -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()

Expand Down
4 changes: 4 additions & 0 deletions fundamentals/pandas/06_export_csv.py
Original file line number Diff line number Diff line change
Expand Up @@ -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(
Expand Down Expand Up @@ -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()

Expand Down
4 changes: 4 additions & 0 deletions fundamentals/parameterized-queries/04_parameterized.py
Original file line number Diff line number Diff line change
@@ -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
Expand Down
6 changes: 4 additions & 2 deletions fundamentals/pycubrid/07_merge_upsert.py
Original file line number Diff line number Diff line change
Expand Up @@ -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 = {
Expand Down Expand Up @@ -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
Expand Down
2 changes: 2 additions & 0 deletions fundamentals/pycubrid/08_hierarchy_connect_by.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,8 @@
- Traversing a subtree from a selected node
"""

from __future__ import annotations

# pyright: reportAttributeAccessIssue=false, reportMissingImports=false

import pycubrid
Expand Down
6 changes: 4 additions & 2 deletions fundamentals/pycubrid/09_serial_order_numbers.py
Original file line number Diff line number Diff line change
Expand Up @@ -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 = {
Expand Down Expand Up @@ -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:
Expand Down
2 changes: 2 additions & 0 deletions fundamentals/pycubrid/10_collection_columns.py
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,8 @@
Collection literals use CUBRID syntax: SET{...}, MULTISET{...}, LIST{...}.
"""

from __future__ import annotations

# pyright: reportAttributeAccessIssue=false, reportMissingImports=false

import pycubrid
Expand Down
4 changes: 2 additions & 2 deletions fundamentals/pycubrid/11_bulk_etl_pipeline.py
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand All @@ -30,7 +30,7 @@


def get_connection():
return CONNECT(**DB_CONFIG)
return pycubrid.connect(**DB_CONFIG)


def setup_schema(conn) -> None:
Expand Down
4 changes: 2 additions & 2 deletions fundamentals/pycubrid/12_pool_retry_worker.py
Original file line number Diff line number Diff line change
Expand Up @@ -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)

Expand All @@ -29,7 +29,7 @@


def get_connection():
return CONNECT(**DB_CONFIG)
return pycubrid.connect(**DB_CONFIG)


class ConnectionPool:
Expand Down
4 changes: 2 additions & 2 deletions fundamentals/pycubrid/13_atomic_counters.py
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand All @@ -30,7 +30,7 @@


def get_connection():
return CONNECT(**DB_CONFIG)
return pycubrid.connect(**DB_CONFIG)


def setup_schema(conn) -> None:
Expand Down
4 changes: 2 additions & 2 deletions fundamentals/pycubrid/14_manual_cascade_delete.py
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand All @@ -25,7 +25,7 @@


def get_connection():
return CONNECT(**DB_CONFIG)
return pycubrid.connect(**DB_CONFIG)


def setup_schema(conn) -> None:
Expand Down
14 changes: 13 additions & 1 deletion templates/api-service-fastapi/app/crud.py
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down Expand Up @@ -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:
Expand Down
3 changes: 1 addition & 2 deletions templates/api-service-fastapi/app/models.py
Original file line number Diff line number Diff line change
Expand Up @@ -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"

Expand Down
2 changes: 2 additions & 0 deletions templates/api-service-fastapi/app/routes/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -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)
47 changes: 47 additions & 0 deletions templates/api-service-fastapi/app/routes/categories.py
Original file line number Diff line number Diff line change
@@ -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)
4 changes: 2 additions & 2 deletions templates/api-service-fastapi/recipes/01-basic-crud/models.py
Original file line number Diff line number Diff line change
@@ -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
Expand All @@ -8,7 +8,7 @@


def utc_now() -> datetime:
return datetime.utcnow()
return datetime.now(timezone.utc)


class Task(Base):
Expand Down
4 changes: 2 additions & 2 deletions templates/api-service-fastapi/recipes/02-orders/models.py
Original file line number Diff line number Diff line change
@@ -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
Expand All @@ -8,7 +8,7 @@


def utc_now() -> datetime:
return datetime.utcnow()
return datetime.now(timezone.utc)


class Customer(Base):
Expand Down
Loading
Loading