From 0083a1ae58370762556ff3926f60670afa6727d5 Mon Sep 17 00:00:00 2001 From: Code Review Date: Mon, 16 Mar 2026 13:00:00 +0200 Subject: [PATCH 1/4] fix: add deterministic ORDER BY to all paginated queries Without ORDER BY, PostgreSQL returns rows in undefined order. Between paginated requests rows can appear on multiple pages or be skipped entirely when the planner changes its access path. Add ORDER BY id to product list, category list, and product search queries to guarantee stable, deterministic pagination. --- app/api/categories.py | 2 +- app/api/products.py | 2 +- app/services/product_service.py | 2 ++ 3 files changed, 4 insertions(+), 2 deletions(-) diff --git a/app/api/categories.py b/app/api/categories.py index 49f1d8e..a5133c1 100644 --- a/app/api/categories.py +++ b/app/api/categories.py @@ -97,7 +97,7 @@ async def list_categories( session: AsyncSession = Depends(get_session), ) -> CategoryListResponse: """List categories.""" - records = await timed_execute_scalars_all(session, select(Category).limit(limit).offset(offset)) + records = await timed_execute_scalars_all(session, select(Category).order_by(Category.id).limit(limit).offset(offset)) if offset == 0 and len(records) < limit: total = len(records) else: diff --git a/app/api/products.py b/app/api/products.py index d561a98..90ecdd0 100644 --- a/app/api/products.py +++ b/app/api/products.py @@ -61,7 +61,7 @@ async def list_products( session: AsyncSession = Depends(get_session), ) -> ProductListResponse: """List products.""" - records = await timed_execute_scalars_all(session, select(Product).limit(limit).offset(offset)) + records = await timed_execute_scalars_all(session, select(Product).order_by(Product.id).limit(limit).offset(offset)) if offset == 0 and len(records) < limit: total = len(records) else: diff --git a/app/services/product_service.py b/app/services/product_service.py index 05dbd14..9a19a84 100644 --- a/app/services/product_service.py +++ b/app/services/product_service.py @@ -39,6 +39,8 @@ async def search_products( if category_id is not None: category_tree = category_subtree_cte(category_id) query = query.where(Product.category_id.in_(select(category_tree.c.id))) + + query = query.order_by(Product.id) timing_context["query_build_ms"] = (perf_counter() - query_build_start) * 1000 # Fetch data and count on the provided session for a consistent snapshot. From 035a9da1b1ceed3caf3a23e6905c25b464ef324f Mon Sep 17 00:00:00 2001 From: Vladislav Antonov Date: Thu, 19 Mar 2026 19:49:21 +0200 Subject: [PATCH 2/4] Fix spaces only names treatment --- app/api/categories.py | 6 +- app/schemas/category.py | 20 +++++- app/schemas/product.py | 36 +++++++++++ tests/test_api.py | 132 ++++++++++++++++++++++++++++++++++++++++ 4 files changed, 190 insertions(+), 4 deletions(-) diff --git a/app/api/categories.py b/app/api/categories.py index a5133c1..099dc6b 100644 --- a/app/api/categories.py +++ b/app/api/categories.py @@ -56,7 +56,7 @@ async def create_category(payload: CategoryCreate, session: AsyncSession = Depen detail=f"Category depth cannot exceed {MAX_CATEGORY_DEPTH}", ) - category = Category(name=payload.name.strip(), parent_id=payload.parent_id) + category = Category(name=payload.name, parent_id=payload.parent_id) session.add(category) try: await session.commit() @@ -65,7 +65,7 @@ async def create_category(payload: CategoryCreate, session: AsyncSession = Depen category_mutations_total.add(1, {"operation": "create", "result": "conflict"}) logger.warning( "category_create_conflict", - extra={"category_name": payload.name.strip(), "parent_id": payload.parent_id}, + extra={"category_name": payload.name, "parent_id": payload.parent_id}, ) raise HTTPException(status_code=status.HTTP_409_CONFLICT, detail="Category name must be unique per parent") from exc await session.refresh(category) @@ -152,7 +152,7 @@ async def update_category( category.parent_id = new_parent_id if "name" in updates and updates["name"] is not None: - category.name = updates["name"].strip() + category.name = updates["name"] try: await session.commit() diff --git a/app/schemas/category.py b/app/schemas/category.py index d5cea93..338a80b 100644 --- a/app/schemas/category.py +++ b/app/schemas/category.py @@ -2,13 +2,22 @@ from datetime import datetime -from pydantic import BaseModel, ConfigDict, Field +from pydantic import BaseModel, ConfigDict, Field, field_validator class CategoryBase(BaseModel): """Shared category fields used by create/read models.""" name: str = Field(min_length=1, max_length=255) + @field_validator("name", mode="before") + @classmethod + def validate_name(cls, value: str) -> str: + if isinstance(value, str): + value = value.strip() + if value == "": + raise ValueError("Category name cannot be empty or contain only whitespace") + return value + class CategoryCreate(CategoryBase): """Payload for creating a category under an optional parent.""" @@ -20,6 +29,15 @@ class CategoryUpdate(BaseModel): name: str | None = Field(default=None, min_length=1, max_length=255) parent_id: int | None = None + @field_validator("name", mode="before") + @classmethod + def validate_optional_name(cls, value: str | None) -> str | None: + if isinstance(value, str): + value = value.strip() + if value == "": + raise ValueError("Category name cannot be empty or contain only whitespace") + return value + class CategoryRead(CategoryBase): """API response model for persisted category records.""" diff --git a/app/schemas/product.py b/app/schemas/product.py index 1cb03c3..97eaaaa 100644 --- a/app/schemas/product.py +++ b/app/schemas/product.py @@ -22,6 +22,24 @@ class ProductBase(BaseModel): def serialize_image_url(self, value: AnyHttpUrl | None) -> str | None: return str(value) if value is not None else None + @field_validator("title", mode="before") + @classmethod + def validate_title(cls, value: str) -> str: + if isinstance(value, str): + value = value.strip() + if value == "": + raise ValueError("Title cannot be empty or contain only whitespace") + return value + + @field_validator("description", mode="before") + @classmethod + def validate_description(cls, value: str) -> str: + if isinstance(value, str): + value = value.strip() + if value == "": + raise ValueError("Description cannot be empty or contain only whitespace") + return value + @field_validator("sku", mode="before") @classmethod def normalize_sku(cls, value: str) -> str: @@ -44,6 +62,24 @@ class ProductUpdate(BaseModel): price: Decimal | None = Field(default=None, ge=Decimal("0")) category_id: int | None = None + @field_validator("title", mode="before") + @classmethod + def validate_optional_title(cls, value: str | None) -> str | None: + if isinstance(value, str): + value = value.strip() + if value == "": + raise ValueError("Title cannot be empty or contain only whitespace") + return value + + @field_validator("description", mode="before") + @classmethod + def validate_optional_description(cls, value: str | None) -> str | None: + if isinstance(value, str): + value = value.strip() + if value == "": + raise ValueError("Description cannot be empty or contain only whitespace") + return value + @field_validator("sku", mode="before") @classmethod def normalize_optional_sku(cls, value: str | None) -> str | None: diff --git a/tests/test_api.py b/tests/test_api.py index d6bb97d..743edf0 100644 --- a/tests/test_api.py +++ b/tests/test_api.py @@ -665,3 +665,135 @@ async def test_search_invalid_price_range(client: AsyncClient): """Test that invalid price range (min > max) is rejected.""" response = await client.get("/api/v1/products/search?min_price=1000&max_price=100") assert response.status_code == 422 # Validation error + + +# ============================================================================ +# Whitespace Validation Tests +# ============================================================================ + +@pytest.mark.asyncio +async def test_create_category_with_space_only_name_fails(client: AsyncClient): + """Test that creating a category with space-only name is rejected.""" + response = await client.post( + "/api/v1/categories", + json={"name": " "} + ) + assert response.status_code == 422 # Validation error + + +@pytest.mark.asyncio +async def test_create_category_with_leading_trailing_spaces_stripped(client: AsyncClient): + """Test that category names have leading/trailing spaces stripped.""" + response = await client.post( + "/api/v1/categories", + json={"name": " Electronics "} + ) + assert response.status_code == 201 + data = response.json() + assert data["name"] == "Electronics" + + +@pytest.mark.asyncio +async def test_update_category_with_space_only_name_fails(client: AsyncClient): + """Test that updating a category with space-only name is rejected.""" + create_response = await client.post( + "/api/v1/categories", + json={"name": "Original Name"} + ) + category_id = create_response.json()["id"] + + response = await client.patch( + f"/api/v1/categories/{category_id}", + json={"name": " "} + ) + assert response.status_code == 422 # Validation error + + +@pytest.mark.asyncio +async def test_create_product_with_space_only_title_fails(client: AsyncClient): + """Test that creating a product with space-only title is rejected.""" + response = await client.post( + "/api/v1/products", + json={ + "title": " ", + "description": "Test description", + "sku": "TEST-001", + "price": "100.00" + } + ) + assert response.status_code == 422 # Validation error + + +@pytest.mark.asyncio +async def test_create_product_with_space_only_description_fails(client: AsyncClient): + """Test that creating a product with space-only description is rejected.""" + response = await client.post( + "/api/v1/products", + json={ + "title": "Test Product", + "description": " ", + "sku": "TEST-001", + "price": "100.00" + } + ) + assert response.status_code == 422 # Validation error + + +@pytest.mark.asyncio +async def test_create_product_with_leading_trailing_spaces_stripped(client: AsyncClient): + """Test that product title and description have leading/trailing spaces stripped.""" + response = await client.post( + "/api/v1/products", + json={ + "title": " Test Product ", + "description": " Test description ", + "sku": "TEST-001", + "price": "100.00" + } + ) + assert response.status_code == 201 + data = response.json() + assert data["title"] == "Test Product" + assert data["description"] == "Test description" + + +@pytest.mark.asyncio +async def test_update_product_with_space_only_title_fails(client: AsyncClient): + """Test that updating a product with space-only title is rejected.""" + create_response = await client.post( + "/api/v1/products", + json={ + "title": "Original Title", + "description": "Test description", + "sku": "TEST-001", + "price": "100.00" + } + ) + product_id = create_response.json()["id"] + + response = await client.patch( + f"/api/v1/products/{product_id}", + json={"title": " "} + ) + assert response.status_code == 422 # Validation error + + +@pytest.mark.asyncio +async def test_update_product_with_space_only_description_fails(client: AsyncClient): + """Test that updating a product with space-only description is rejected.""" + create_response = await client.post( + "/api/v1/products", + json={ + "title": "Original Title", + "description": "Original description", + "sku": "TEST-001", + "price": "100.00" + } + ) + product_id = create_response.json()["id"] + + response = await client.patch( + f"/api/v1/products/{product_id}", + json={"description": " "} + ) + assert response.status_code == 422 # Validation error From 6faedc79db3ac7e493da4cafd409fa461bf4835e Mon Sep 17 00:00:00 2001 From: Vladislav Antonov Date: Thu, 19 Mar 2026 19:59:44 +0200 Subject: [PATCH 3/4] Add automatic version and changelog management --- AGENTS.md | 378 +++++++++++++++++++++++++++++++++++++++++++++++++++ CHANGELOG.md | 161 ++++++++++++++++++++++ 2 files changed, 539 insertions(+) create mode 100644 AGENTS.md create mode 100644 CHANGELOG.md diff --git a/AGENTS.md b/AGENTS.md new file mode 100644 index 0000000..2bc6019 --- /dev/null +++ b/AGENTS.md @@ -0,0 +1,378 @@ +# AGENTS.md + +Guide for AI coding agents working on the Commerce System Demo project. + +## Project Overview + +Commerce System Demo is a FastAPI-based commerce service that provides RESTful APIs for managing products, categories, and implementing search functionality. The project includes built-in observability with OpenTelemetry metrics, logging, and distributed tracing. + +**Current Version**: 0.1.0 (following [Semantic Versioning](https://semver.org/)) + +## Setup Commands + +### Installation and Environment Setup + +- **Install dependencies**: `pip install -e ".[dev]"` (after activating Python 3.11+ venv) +- **Python version**: Requires Python 3.11+ +- **Virtual environment**: Use `.venv` folder in project root +- **Activate venv**: `source .venv/bin/activate` + +### Database Setup + +- **PostgreSQL required**: The project uses PostgreSQL with asyncpg driver +- **Docker Compose**: Use `docker-compose.yml` to start all services (PostgreSQL, app, monitoring stack) +- **Start services**: `docker-compose up -d` +- **Run migrations**: `docker-compose run migrate-indexes` (handles database schema and indexes) +- **Database URL**: `postgresql+asyncpg://postgres:postgres@localhost:5432/commerce_demo` + +### Development Server + +- **Start dev server**: `uvicorn app.main:app --reload --host 0.0.0.0 --port 8000` +- **API docs**: Available at `http://localhost:8000/docs` (Swagger UI) +- **Alternative docs**: Available at `http://localhost:8000/redoc` (ReDoc) +- **Health check**: `curl http://localhost:8000/health` + +## Code Organization + +### Project Structure + +``` +app/ +├── main.py # FastAPI application setup +├── api/ # API endpoint implementations +│ ├── categories.py # Category CRUD endpoints +│ ├── products.py # Product CRUD endpoints +│ └── search.py # Product search endpoint +├── models/ # SQLAlchemy ORM models +│ ├── category.py # Category model with hierarchy support +│ └── product.py # Product model +├── schemas/ # Pydantic request/response schemas +│ ├── category.py # Category validation schemas +│ ├── product.py # Product validation schemas +│ └── common.py # Shared response models +├── services/ # Business logic layer +│ ├── category_service.py # Category operations and tree queries +│ └── product_service.py # Product search orchestration +├── db/ # Database configuration +│ ├── base.py # SQLAlchemy declarative base +│ ├── session.py # Async session factory +│ └── core/ # Configuration +│ └── config.py # Environment and app settings +├── observability/ # OpenTelemetry instrumentation +│ ├── setup.py # Telemetry initialization +│ ├── metrics.py # Metrics definitions +│ ├── middleware.py # Custom middleware +│ └── logging.py # Logging configuration +└── templates/ # HTML templates + └── index.html # Project overview page +``` + +### Key Conventions + +- **API prefix**: `/api/v1` (configurable via `API_PREFIX` env var) +- **Async/await**: All database operations are async using SQLAlchemy 2.0+ async API +- **Validation**: Pydantic v2 validators with `mode="before"` for data transformation +- **Error handling**: FastAPI HTTPException with appropriate status codes (400, 404, 409, 422) +- **Naming**: Snake_case for Python code, kebab-case for API paths and Docker services + +## Testing + +### Run Tests + +- **All tests**: `pytest` or `pytest tests/` +- **Specific test file**: `pytest tests/test_api.py` or `pytest tests/test_search.py` +- **Specific test**: `pytest tests/test_api.py::test_create_category -v` +- **With coverage**: `pytest --cov=app --cov-report=html` +- **Watch mode**: `pytest-watch` (requires pytest-watch package) + +### Test Organization + +- **tests/conftest.py**: Shared fixtures (db_session, client setup) +- **tests/test_api.py**: Integration tests for CRUD operations and search endpoints +- **tests/test_search.py**: Service-level search functionality tests + +### Database for Tests + +- Tests use `testcontainers` to spin up real PostgreSQL instances +- Each test session gets a fresh database to avoid state leakage +- Tests run in async mode (`asyncio_mode = "auto"`) + +## Building and Deployment + +### Docker + +- **Build image**: `docker build -t commerce-system-demo:0.1.0 .` +- **View Dockerfile**: Includes Python dependencies, migration scripts, and app code +- **Build context**: Includes `scripts/`, `app/`, and `observability/` directories + +### Environment Variables + +- `DATABASE_URL`: PostgreSQL connection string (required) +- `API_PREFIX`: API path prefix (default: `/api/v1`) +- `AUTO_CREATE_SCHEMA`: Auto-create tables on startup (default: `true`) +- `TELEMETRY_ENABLED`: Enable OpenTelemetry export (default: `false` for dev) +- `OTEL_SERVICE_NAME`: Service name for observability (default: `commerce-system-demo`) +- `OTEL_EXPORTER_OTLP_ENDPOINT`: OTLP collector endpoint for data export + +## Validation Rules and Constraints + +### Product Validation + +- **Title**: 1-255 characters, cannot be empty or whitespace-only (space-only names rejected at schema level) +- **Description**: 1-10000 characters, cannot be empty or whitespace-only +- **SKU**: 1-100 characters, uppercase letters/numbers/hyphens/underscores, unique constraint at DB level (normalized to uppercase) +- **Price**: Decimal ≥ 0, includes cents precision +- **Category ID**: Optional foreign key to category + +### Category Validation + +- **Name**: 1-255 characters, cannot be empty or whitespace-only (space-only names rejected at schema level) +- **Parent ID**: Optional self-referential foreign key +- **Hierarchy depth limit**: Categories cannot exceed 5 levels deep +- **Naming uniqueness**: Category names must be unique within same parent (siblings must have different names) + +### Search Validation + +- **Query**: Optional, 1-255 characters when provided, space-only queries filtered out at endpoint level +- **Price range**: `min_price <= max_price` (validation error if violated) +- **Category**: Optional integer ID +- **Pagination**: limit 1-100 (default 20), offset ≥ 0 + +## Important Patterns and Best Practices + +### Error Handling + +- Return 400 (Bad Request) for client validation errors (Pydantic ValidationError) +- Return 404 (Not Found) for missing resources (product_id, category_id) +- Return 409 (Conflict) for duplicate SKU or duplicate sibling category name +- Return 422 (Unprocessable Entity) for logic violations (e.g., circular category references, depth exceeded, invalid price range) + +### Whitespace Handling + +- **Stripping**: Product titles/descriptions and category names are automatically stripped of leading/trailing whitespace via Pydantic validators +- **Rejection**: Space-only strings are rejected with `ValueError` during schema validation before any business logic +- **Example**: A product with title " " will be rejected with 422 error; " Test " becomes "Test" + +### Database Queries + +- Use `timed_execute_*` functions from `app.observability.db_timing` to instrument query timing +- Category hierarchy queries use CTE (Common Table Expression) for efficient tree traversal +- Search queries use `ilike` for case-insensitive title matching and exact match for normalized SKU + +### Observability + +- **Metrics**: Custom OpenTelemetry meters track mutations (create/update/delete) and search requests +- **Logging**: Structured logs with context data (product_id, category_id, operation, result) +- **Distributed Tracing**: FastAPI instrumentation tracks request flow through the entire stack + +## Testing Strategies for Agents + +When making changes: + +1. **Add tests for new features** before or alongside implementation +2. **Run full test suite** after changes: `pytest -v` +3. **Check lint/formatting**: `black --check app/` (if configured) or `ruff check app/` +4. **Type checking**: `mypy app/` (if configured) +5. **Test whitespace validation**: Verify space-only inputs are rejected at schema layer +6. **Test edge cases**: Empty strings after strip, very long strings, special characters in SKU + +## Versioning + +This project follows [Semantic Versioning 2.0.0](https://semver.org/): + +- **MAJOR**: Incompatible API changes +- **MINOR**: Backward-compatible new features +- **PATCH**: Backward-compatible bug fixes + +Current version is **0.1.0** (initial development). Version is defined in: +- `pyproject.toml` (project metadata) +- `app/main.py` (FastAPI version) +- `app/observability/metrics.py` (meter version) +- `app/observability/setup.py` (service version) + +All version references should be kept in sync when updating. + +## Automatic Version Management + +When updating the project version, follow this automated workflow to ensure consistency and create proper git artifacts: + +### Required Version Files (Keep in Sync) + +These **4 files MUST** have identical version strings when updated: + +1. `pyproject.toml` - Line: `version = "X.Y.Z"` +2. `app/main.py` - Line: `version="X.Y.Z"` (in FastAPI instantiation) +3. `app/observability/metrics.py` - Line: `version="X.Y.Z"` (in get_meter call) +4. `app/observability/setup.py` - Line: `"service.version": "X.Y.Z"` (in resource attributes) + +### Automated Version Update Process + +**Rule**: When any feature, fix, or release requires a version change, agents MUST: + +1. **Update all 4 version files atomically** - Use regex/find-replace to update `X.Y.Z` across all files + ```bash + # Verify current version across all files + grep -n "0\.1\.0" pyproject.toml app/main.py app/observability/metrics.py app/observability/setup.py + ``` + +2. **Update CHANGELOG.md** + - Move `## [Unreleased]` section content to new version section: `## [X.Y.Z] - YYYY-MM-DD` + - Create fresh `## [Unreleased]` section at top + - Update version references in guidelines + +3. **Update AGENTS.md** + - Change `**Current Version**: X.Y.Z` in Project Overview + - Update Docker build command examples: `docker build -t commerce-system-demo:X.Y.Z .` + - Update version references in Versioning section + +4. **Create git artifacts** + ```bash + # Stage all version updates + git add pyproject.toml app/main.py app/observability/metrics.py app/observability/setup.py CHANGELOG.md AGENTS.md + + # Commit with standardized message + git commit -m "chore: release version X.Y.Z" + + # Create annotated git tag + git tag -a vX.Y.Z -m "Release version X.Y.Z" + + # (Optional) Push to remote + git push origin --tags + ``` + +5. **Validate consistency** + ```bash + # Verify all 4 version files match + grep "X\.Y\.Z" pyproject.toml app/main.py app/observability/metrics.py app/observability/setup.py + + # Verify CHANGELOG has new version section + grep "## \[X\.Y\.Z\]" CHANGELOG.md + + # Verify git tag exists + git tag -l vX.Y.Z + ``` + +### Version Update Checklist for Agents + +When asked to update the version, verify: + +- [ ] All 4 version files updated to identical semantic version (X.Y.Z) +- [ ] CHANGELOG.md has `## [X.Y.Z] - YYYY-MM-DD` section +- [ ] CHANGELOG.md has fresh `## [Unreleased]` section +- [ ] AGENTS.md current version reference updated +- [ ] Docker command examples updated with new version +- [ ] All tests pass: `pytest -v` +- [ ] Consistency check passes (all files have same version) +- [ ] Git commit created: `git commit -m "chore: release version X.Y.Z"` +- [ ] Annotated git tag created: `git tag -a vX.Y.Z -m "Release version X.Y.Z"` +- [ ] git tag verified: `git tag -l vX.Y.Z` + +### Example: Releasing 0.2.0 + +**Step 1: Update Version Files (all files get 0.2.0)** +```bash +# Before: contain 0.1.0 +# After: contain 0.2.0 +pyproject.toml: version = "0.2.0" +app/main.py: version="0.2.0" +app/observability/metrics.py: version="0.2.0" +app/observability/setup.py: "service.version": "0.2.0" +``` + +**Step 2: Update CHANGELOG.md** +```markdown +## [0.2.0] - 2024-03-20 + +### Added +- [Features from unreleased section] + +### Changed +- [Changes from unreleased section] + +### Fixed +- [Fixes from unreleased section] + +## [Unreleased] + +### Planned +- [Future features to be filled in later] +``` + +**Step 3: Update AGENTS.md** +```markdown +**Current Version**: 0.2.0 (following [Semantic Versioning](https://semver.org/)) +- **Build image**: `docker build -t commerce-system-demo:0.2.0 .` +``` + +**Step 4: Commit and Tag** +```bash +git add pyproject.toml app/main.py app/observability/metrics.py app/observability/setup.py CHANGELOG.md AGENTS.md +git commit -m "chore: release version 0.2.0" +git tag -a v0.2.0 -m "Release version 0.2.0" +``` + +**Step 5: Verify** +```bash +# Should show all files with 0.2.0 +grep "0\.2\.0" pyproject.toml app/main.py app/observability/metrics.py app/observability/setup.py + +# Should show tag exists +git tag -l v0.2.0 + +# Should show 0.2.0 release section +grep "## \[0\.2\.0\]" CHANGELOG.md +``` + +### Critical Rules for Agents + +⚠️ **MUST DO:** +- Always update ALL 4 version files atomically together (never partial updates) +- Always create CHANGELOG entry before git tag +- Always use annotated tags (`git tag -a`) with message, not lightweight tags +- Always use `vX.Y.Z` format for git tags (with leading 'v') +- Always verify consistency before completing task + +⚠️ **MUST NOT:** +- Update only 1-3 version files and skip the rest +- Forget to update CHANGELOG.md before tagging +- Use lightweight tags or tags without messages +- Tag without committing version changes first +- Leave version in sync documentation out of date + +## Changelog + +See [CHANGELOG.md](CHANGELOG.md) for detailed version history and notable changes. + +## Common Issues and Solutions + +### PostgreSQL Connection Errors + +- Ensure PostgreSQL is running: `docker-compose ps` +- Check DATABASE_URL environment variable is set correctly +- Verify PostgreSQL container is healthy: `docker-compose logs db` + +### Migration Script Failures + +- Ensure `scripts/` directory is included in Docker build context +- Check that migrate-indexes service runs before app service (docker-compose dependency) +- Review migration script output: `docker-compose logs migrate-indexes` + +### Tests Failing Locally + +- Ensure Docker is running (testcontainers needs Docker daemon) +- Try isolating a single test: `pytest tests/test_api.py::test_create_product -v` +- Check Python version: Must be 3.11+ + +### Telemetry Issues + +- Verify OTEL_EXPORTER_OTLP_ENDPOINT is reachable if telemetry is enabled +- Check OpenTelemetry SDK isn't throwing exceptions in logs +- Disable telemetry for local development if observability stack isn't available + +## Additional Resources + +- [FastAPI Documentation](https://fastapi.tiangolo.com/) +- [SQLAlchemy 2.0 Async](https://docs.sqlalchemy.org/en/20/orm/extensions/asyncio.html) +- [Pydantic v2 Documentation](https://docs.pydantic.dev/) +- [OpenTelemetry Python SDK](https://opentelemetry.io/docs/instrumentation/python/) +- [README.md](README.md) - Human-focused project documentation diff --git a/CHANGELOG.md b/CHANGELOG.md new file mode 100644 index 0000000..7f2efbb --- /dev/null +++ b/CHANGELOG.md @@ -0,0 +1,161 @@ +# Changelog + +All notable changes to this project are documented in this file. + +The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), +and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). + +## [Unreleased] + +### Planned + +- Enhanced search with full-text indexing support +- Bulk import/export endpoints for products and categories +- Rate limiting and request throttling +- Advanced filtering options for product search +- Product images storage optimization + +## [0.1.0] - 2024-03-19 + +### Added + +- Initial release of Commerce System Demo +- Product CRUD operations (Create, Read, Update, Delete) + - Product title, description, SKU, price, and category assignment + - SKU normalization (automatic uppercase conversion) + - Unique SKU constraint at database level +- Category management with hierarchical support + - Category hierarchy with parent-child relationships + - Depth limit validation (max 5 levels) + - Automatic cascading delete of child categories +- Product search functionality + - Full-text search by product title + - Exact SKU search with normalization + - Price range filtering (min/max) + - Category subtree filtering + - Pagination support (limit, offset) +- Input validation and whitespace handling + - Pydantic v2 schema validators for data integrity + - Automatic stripping of leading/trailing whitespace + - Rejection of space-only product names and category names +- RESTful API with standardized error handling + - HTTP status codes: 201 (Created), 400 (Bad Request), 404 (Not Found), 409 (Conflict), 422 (Unprocessable Entity) + - Structured error responses with detailed messages +- Comprehensive test suite + - 39 integration tests covering CRUD operations + - Space-only name validation tests + - Search functionality tests + - Database constraint validation tests + - Test fixtures with real PostgreSQL via testcontainers +- Observability and monitoring + - OpenTelemetry instrumentation for metrics and tracing + - Custom metrics for mutation operations and search requests + - Structured logging with contextual data + - Prometheus metrics export + - Grafana dashboards for monitoring + - Distributed tracing support + - Loki log aggregation + - Alerting rules for critical issues +- Docker support + - Dockerfile for containerized deployment + - Docker Compose orchestration (app, PostgreSQL, monitoring stack) + - Migration service for database schema setup +- FastAPI with async/await + - Async SQLAlchemy 2.0+ integration + - Asynchronous database queries + - Async context managers for proper resource cleanup +- Project documentation + - Comprehensive README with architecture and design details + - API conventions and endpoint specifications + - Non-functional requirements documentation + +### Fixed + +- **Space-only names bug**: Added schema-level validation to reject product titles, product descriptions, and category names that contain only whitespace characters + - Previously: Space-only strings were accepted, stored as empty strings, and became unsearchable + - Now: Pydantic validators reject space-only input with `ValidationError` (HTTP 422) + - Automatic stripping of valid inputs: leading/trailing spaces are removed before storage + - Applied consistently to both create and update operations + +### Changed + +- Moved whitespace validation from API endpoint layer to Pydantic schema layer + - More robust and consistent validation + - Validation happens before business logic execution + - Easier to maintain and test + +### Security Considerations + +- Database: Uses parameterized queries via SQLAlchemy ORM (SQL injection protection) +- Input validation: All user inputs validated via Pydantic before processing +- CORS: Not currently enabled (configure in fastapi.middleware.cors if needed) +- Rate limiting: Not yet implemented (recommended for production) + +### Version Information + +**Semantic Version**: 0.1.0 +- Major: 0 (initial development phase) +- Minor: 1 (includes functional features) +- Patch: 0 (no bug fixes in this version, only features and bug fix for space-only names) + +This is an early development release. The public API may change in subsequent releases. + +--- + +## Version History Reference + +| Version | Release Date | Status | Notes | +|---------|------------|--------|-------| +| [0.1.0](#010---2024-03-19) | 2024-03-19 | Current | Initial release with core functionality | +| [Unreleased](#unreleased) | - | In Progress | Upcoming features and improvements | + +--- + +## Guidelines for Maintainers + +### When to Update Versions + +- **PATCH version** (0.1.X): Bug fixes, performance improvements, non-breaking changes + - Example: Fixing the space-only names bug (0.1.0 → 0.1.1) +- **MINOR version** (0.X.0): New features, backward-compatible additions + - Example: Adding new search filters (0.1.0 → 0.2.0) +- **MAJOR version** (X.0.0): Breaking API changes, major restructuring + - Example: Changing endpoint paths (0.1.0 → 1.0.0) + +### Updating Version References + +When releasing a new version, update all occurrences: + +1. `pyproject.toml`: `version = "X.Y.Z"` +2. `app/main.py`: `version="X.Y.Z"` +3. `app/observability/metrics.py`: `version="X.Y.Z"` +4. `app/observability/setup.py`: `"service.version": "X.Y.Z"` +5. `Dockerfile`: Update image tags in build commands +6. `CHANGELOG.md`: Add new version section with changes + +### Changelog Format + +For each release: + +1. Create section with `## [X.Y.Z] - YYYY-MM-DD` +2. Use subsections: Added, Changed, Fixed, Deprecated, Removed, Security +3. List items as bullet points +4. Include reference links at bottom for easy diff generation +5. Keep unreleased section for tracking in-progress work + +### Release Checklist + +- [ ] All tests pass: `pytest -v` +- [ ] Code formatted and linted +- [ ] Version numbers updated in all files (see list above) +- [ ] CHANGELOG.md updated with new version section +- [ ] Git tag created: `git tag v0.1.0` +- [ ] Release notes added to GitHub + +--- + +## How to Contribute + +See [README.md](README.md) for development setup and contribution guidelines. + +For detailed agent-specific guidance, see [AGENTS.md](AGENTS.md). From c320033ab7b437c19b491728d297f7aa8b55d7f1 Mon Sep 17 00:00:00 2001 From: Vladislav Antonov Date: Thu, 19 Mar 2026 20:05:22 +0200 Subject: [PATCH 4/4] chore: release version 0.1.1 --- AGENTS.md | 10 ++++---- CHANGELOG.md | 47 +++++++++++++++++------------------- app/main.py | 2 +- app/observability/metrics.py | 2 +- app/observability/setup.py | 2 +- pyproject.toml | 2 +- 6 files changed, 31 insertions(+), 34 deletions(-) diff --git a/AGENTS.md b/AGENTS.md index 2bc6019..2cbe219 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -6,7 +6,7 @@ Guide for AI coding agents working on the Commerce System Demo project. Commerce System Demo is a FastAPI-based commerce service that provides RESTful APIs for managing products, categories, and implementing search functionality. The project includes built-in observability with OpenTelemetry metrics, logging, and distributed tracing. -**Current Version**: 0.1.0 (following [Semantic Versioning](https://semver.org/)) +**Current Version**: 0.1.1 (following [Semantic Versioning](https://semver.org/)) ## Setup Commands @@ -101,7 +101,7 @@ app/ ### Docker -- **Build image**: `docker build -t commerce-system-demo:0.1.0 .` +- **Build image**: `docker build -t commerce-system-demo:0.1.1 .` - **View Dockerfile**: Includes Python dependencies, migration scripts, and app code - **Build context**: Includes `scripts/`, `app/`, and `observability/` directories @@ -184,7 +184,7 @@ This project follows [Semantic Versioning 2.0.0](https://semver.org/): - **MINOR**: Backward-compatible new features - **PATCH**: Backward-compatible bug fixes -Current version is **0.1.0** (initial development). Version is defined in: +Current version is **0.1.1** (initial development). Version is defined in: - `pyproject.toml` (project metadata) - `app/main.py` (FastAPI version) - `app/observability/metrics.py` (meter version) @@ -212,7 +212,7 @@ These **4 files MUST** have identical version strings when updated: 1. **Update all 4 version files atomically** - Use regex/find-replace to update `X.Y.Z` across all files ```bash # Verify current version across all files - grep -n "0\.1\.0" pyproject.toml app/main.py app/observability/metrics.py app/observability/setup.py + grep -n "0\.1\.1" pyproject.toml app/main.py app/observability/metrics.py app/observability/setup.py ``` 2. **Update CHANGELOG.md** @@ -271,7 +271,7 @@ When asked to update the version, verify: **Step 1: Update Version Files (all files get 0.2.0)** ```bash -# Before: contain 0.1.0 +# Before: contain 0.1.1 # After: contain 0.2.0 pyproject.toml: version = "0.2.0" app/main.py: version="0.2.0" diff --git a/CHANGELOG.md b/CHANGELOG.md index 7f2efbb..fa58029 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -15,6 +15,20 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - Advanced filtering options for product search - Product images storage optimization +## [0.1.1] - 2026-03-19 + +### Fixed + +- Reject whitespace-only product titles, product descriptions, and category names during schema validation +- Normalize valid text inputs by trimming leading and trailing whitespace before persistence +- Apply the same validation behavior to both create and update operations +- Prevent non-deterministic list and search ordering by sorting category, product, and search results by id + +### Changed + +- Moved category whitespace normalization from API handlers into Pydantic schemas for a single validation path +- Expanded API regression coverage for whitespace-only inputs and trimmed valid payloads + ## [0.1.0] - 2024-03-19 ### Added @@ -34,10 +48,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - Price range filtering (min/max) - Category subtree filtering - Pagination support (limit, offset) -- Input validation and whitespace handling - - Pydantic v2 schema validators for data integrity - - Automatic stripping of leading/trailing whitespace - - Rejection of space-only product names and category names +- Basic input validation and whitespace normalization foundations - RESTful API with standardized error handling - HTTP status codes: 201 (Created), 400 (Bad Request), 404 (Not Found), 409 (Conflict), 422 (Unprocessable Entity) - Structured error responses with detailed messages @@ -69,21 +80,6 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - API conventions and endpoint specifications - Non-functional requirements documentation -### Fixed - -- **Space-only names bug**: Added schema-level validation to reject product titles, product descriptions, and category names that contain only whitespace characters - - Previously: Space-only strings were accepted, stored as empty strings, and became unsearchable - - Now: Pydantic validators reject space-only input with `ValidationError` (HTTP 422) - - Automatic stripping of valid inputs: leading/trailing spaces are removed before storage - - Applied consistently to both create and update operations - -### Changed - -- Moved whitespace validation from API endpoint layer to Pydantic schema layer - - More robust and consistent validation - - Validation happens before business logic execution - - Easier to maintain and test - ### Security Considerations - Database: Uses parameterized queries via SQLAlchemy ORM (SQL injection protection) @@ -105,8 +101,9 @@ This is an early development release. The public API may change in subsequent re ## Version History Reference | Version | Release Date | Status | Notes | -|---------|------------|--------|-------| -| [0.1.0](#010---2024-03-19) | 2024-03-19 | Current | Initial release with core functionality | +|---------|-------------|--------|-------| +| [0.1.1](#011---2026-03-19) | 2026-03-19 | Current | Patch release for whitespace validation and deterministic ordering | +| [0.1.0](#010---2024-03-19) | 2024-03-19 | Previous | Initial release with core functionality | | [Unreleased](#unreleased) | - | In Progress | Upcoming features and improvements | --- @@ -116,11 +113,11 @@ This is an early development release. The public API may change in subsequent re ### When to Update Versions - **PATCH version** (0.1.X): Bug fixes, performance improvements, non-breaking changes - - Example: Fixing the space-only names bug (0.1.0 → 0.1.1) + - Example: Fixing another bug after this release (0.1.1 → 0.1.2) - **MINOR version** (0.X.0): New features, backward-compatible additions - - Example: Adding new search filters (0.1.0 → 0.2.0) + - Example: Adding new search filters (0.1.1 → 0.2.0) - **MAJOR version** (X.0.0): Breaking API changes, major restructuring - - Example: Changing endpoint paths (0.1.0 → 1.0.0) + - Example: Changing endpoint paths (0.1.1 → 1.0.0) ### Updating Version References @@ -149,7 +146,7 @@ For each release: - [ ] Code formatted and linted - [ ] Version numbers updated in all files (see list above) - [ ] CHANGELOG.md updated with new version section -- [ ] Git tag created: `git tag v0.1.0` +- [ ] Git tag created: `git tag v0.1.1` - [ ] Release notes added to GitHub --- diff --git a/app/main.py b/app/main.py index 2d49e5e..5ec00e4 100644 --- a/app/main.py +++ b/app/main.py @@ -77,7 +77,7 @@ def create_app() -> FastAPI: app = FastAPI( title="Commerce System Demo", - version="0.1.0", + version="0.1.1", lifespan=lifespan, ) app.router.route_class = ObservabilityRoute diff --git a/app/observability/metrics.py b/app/observability/metrics.py index 527d73d..79bee6a 100644 --- a/app/observability/metrics.py +++ b/app/observability/metrics.py @@ -3,7 +3,7 @@ from opentelemetry import metrics from opentelemetry.metrics import Counter, Histogram, UpDownCounter -_meter = metrics.get_meter("commerce-system-demo-observability", version="0.1.0") +_meter = metrics.get_meter("commerce-system-demo-observability", version="0.1.1") http_request_duration_seconds: Histogram = _meter.create_histogram( name="commerce_http_request_duration_seconds", diff --git a/app/observability/setup.py b/app/observability/setup.py index d9eb647..15050c5 100644 --- a/app/observability/setup.py +++ b/app/observability/setup.py @@ -76,7 +76,7 @@ def _build_resource(settings: Settings) -> Resource: """Build OpenTelemetry resource attributes from runtime settings.""" attributes = { "service.name": settings.otel_service_name, - "service.version": "0.1.0", + "service.version": "0.1.1", "deployment.environment": settings.otel_environment, } diff --git a/pyproject.toml b/pyproject.toml index 9bc62d6..a454389 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta" [project] name = "commerce-system-demo" -version = "0.1.0" +version = "0.1.1" description = "FastAPI commerce service demo" readme = "README.md" requires-python = ">=3.11"