2025-11-20 11:29:06 +01:00
|
|
|
# Implementation Tasks: Add GraphQL Member API
|
|
|
|
|
|
|
|
|
|
**Change ID:** `add-graphql-member-api`
|
|
|
|
|
**Status:** Draft
|
|
|
|
|
**Created:** 2025-11-20
|
|
|
|
|
|
|
|
|
|
## Task Breakdown
|
|
|
|
|
|
|
|
|
|
This change is implemented through ~18 discrete, verifiable tasks. Each task delivers user-visible progress and includes validation steps.
|
|
|
|
|
|
|
|
|
|
---
|
|
|
|
|
|
|
|
|
|
## Phase 1: Project Foundation
|
|
|
|
|
|
|
|
|
|
### Task 1: Initialize Python project with uv
|
|
|
|
|
|
|
|
|
|
**Objective:** Create pyproject.toml and configure uv for dependency management
|
|
|
|
|
|
|
|
|
|
**Steps:**
|
|
|
|
|
1. Run `uv init` to create basic project structure
|
|
|
|
|
2. Edit pyproject.toml to set name="clubber", version="0.1.0"
|
|
|
|
|
3. Set Python requirement to ">=3.11"
|
|
|
|
|
4. Add project metadata (description, authors, license)
|
|
|
|
|
|
|
|
|
|
**Validation:**
|
2025-11-20 13:25:13 +01:00
|
|
|
- [x] `uv sync` completes successfully
|
|
|
|
|
- [x] pyproject.toml contains project metadata
|
2025-11-20 11:29:06 +01:00
|
|
|
|
|
|
|
|
**Dependencies:** None
|
|
|
|
|
|
|
|
|
|
---
|
|
|
|
|
|
|
|
|
|
### Task 2: Add core dependencies
|
|
|
|
|
|
|
|
|
|
**Objective:** Install FastAPI, Strawberry, SQLAlchemy, and Alembic
|
|
|
|
|
|
|
|
|
|
**Steps:**
|
|
|
|
|
1. Run `uv add fastapi uvicorn[standard]`
|
|
|
|
|
2. Run `uv add strawberry-graphql[fastapi]`
|
|
|
|
|
3. Run `uv add sqlalchemy[asyncio] aiosqlite`
|
|
|
|
|
4. Run `uv add alembic pydantic-settings`
|
|
|
|
|
|
|
|
|
|
**Validation:**
|
2025-11-20 13:25:13 +01:00
|
|
|
- [x] All packages appear in pyproject.toml dependencies
|
|
|
|
|
- [x] `uv sync` resolves dependencies without conflicts
|
|
|
|
|
- [x] uv.lock file is generated
|
2025-11-20 11:29:06 +01:00
|
|
|
|
|
|
|
|
**Dependencies:** Task 1
|
|
|
|
|
|
|
|
|
|
---
|
|
|
|
|
|
|
|
|
|
### Task 3: Add development dependencies
|
|
|
|
|
|
|
|
|
|
**Objective:** Install testing and code quality tools
|
|
|
|
|
|
|
|
|
|
**Steps:**
|
|
|
|
|
1. Run `uv add --dev pytest pytest-asyncio`
|
|
|
|
|
2. Run `uv add --dev black ruff isort`
|
|
|
|
|
|
|
|
|
|
**Validation:**
|
2025-11-20 13:25:13 +01:00
|
|
|
- [x] Development packages in [tool.uv.dev-dependencies] or similar
|
|
|
|
|
- [x] `uv run pytest --version` works
|
|
|
|
|
- [x] `uv run black --version` works
|
2025-11-20 11:29:06 +01:00
|
|
|
|
|
|
|
|
**Dependencies:** Task 1
|
|
|
|
|
|
|
|
|
|
---
|
|
|
|
|
|
|
|
|
|
### Task 4: Configure development tools in pyproject.toml
|
|
|
|
|
|
|
|
|
|
**Objective:** Set up black, ruff, and isort configurations
|
|
|
|
|
|
|
|
|
|
**Steps:**
|
|
|
|
|
1. Add [tool.black] section with line-length=88, target-version=["py311"]
|
|
|
|
|
2. Add [tool.ruff] section with line-length=88, select=["E", "F", "W", "I"]
|
|
|
|
|
3. Add [tool.isort] section with profile="black"
|
|
|
|
|
4. Add [tool.pytest.ini_options] with asyncio_mode="auto"
|
|
|
|
|
|
|
|
|
|
**Validation:**
|
2025-11-20 13:25:13 +01:00
|
|
|
- [x] `uv run black --check .` runs (passes even if no code yet)
|
|
|
|
|
- [x] `uv run ruff check .` runs
|
|
|
|
|
- [x] Tool configurations are in pyproject.toml
|
2025-11-20 11:29:06 +01:00
|
|
|
|
|
|
|
|
**Dependencies:** Task 3
|
|
|
|
|
|
|
|
|
|
---
|
|
|
|
|
|
|
|
|
|
### Task 5: Create src directory structure
|
|
|
|
|
|
|
|
|
|
**Objective:** Set up layered architecture directories
|
|
|
|
|
|
|
|
|
|
**Steps:**
|
|
|
|
|
1. Create directories: `src/models/`, `src/schemas/`, `src/resolvers/`
|
|
|
|
|
2. Create empty `__init__.py` in each directory
|
|
|
|
|
3. Create `src/main.py`, `src/config.py`, `src/database.py` as placeholders
|
|
|
|
|
|
|
|
|
|
**Validation:**
|
2025-11-20 13:25:13 +01:00
|
|
|
- [x] Directory structure matches design.md
|
|
|
|
|
- [x] All directories have __init__.py files
|
|
|
|
|
- [x] `ls -R src/` shows complete structure
|
2025-11-20 11:29:06 +01:00
|
|
|
|
|
|
|
|
**Dependencies:** Task 1
|
|
|
|
|
|
|
|
|
|
**Can run in parallel with:** Task 2, Task 3
|
|
|
|
|
|
|
|
|
|
---
|
|
|
|
|
|
|
|
|
|
## Phase 2: Database Layer
|
|
|
|
|
|
|
|
|
|
### Task 6: Create configuration management
|
|
|
|
|
|
|
|
|
|
**Objective:** Implement Settings class with environment variable support
|
|
|
|
|
|
|
|
|
|
**Steps:**
|
|
|
|
|
1. In `src/config.py`, create Settings class using pydantic-settings
|
|
|
|
|
2. Add fields: database_url (default: "sqlite+aiosqlite:///./clubber.db"), debug (default: False)
|
|
|
|
|
3. Configure .env file loading
|
|
|
|
|
4. Create `.env.example` with documented settings
|
|
|
|
|
|
|
|
|
|
**Validation:**
|
2025-11-20 13:25:13 +01:00
|
|
|
- [x] Settings() instantiates with defaults
|
|
|
|
|
- [x] DATABASE_URL environment variable overrides default
|
|
|
|
|
- [x] .env.example exists and documents all settings
|
2025-11-20 11:29:06 +01:00
|
|
|
|
|
|
|
|
**Dependencies:** Task 2, Task 5
|
|
|
|
|
|
|
|
|
|
---
|
|
|
|
|
|
|
|
|
|
### Task 7: Set up database connection and session factory
|
|
|
|
|
|
|
|
|
|
**Objective:** Create async SQLAlchemy engine and session management
|
|
|
|
|
|
|
|
|
|
**Steps:**
|
|
|
|
|
1. In `src/database.py`, import SQLAlchemy async components
|
|
|
|
|
2. Create async_engine using settings.database_url
|
|
|
|
|
3. Create async_session_maker with AsyncSession
|
|
|
|
|
4. Implement get_db_session() dependency for FastAPI
|
|
|
|
|
5. Create declarative Base for models
|
|
|
|
|
|
|
|
|
|
**Validation:**
|
2025-11-20 13:25:13 +01:00
|
|
|
- [x] async_engine is created without errors
|
|
|
|
|
- [x] async_session_maker is callable
|
|
|
|
|
- [x] get_db_session yields AsyncSession
|
2025-11-20 11:29:06 +01:00
|
|
|
|
|
|
|
|
**Dependencies:** Task 6
|
|
|
|
|
|
|
|
|
|
---
|
|
|
|
|
|
|
|
|
|
### Task 8: Define Member SQLAlchemy model
|
|
|
|
|
|
|
|
|
|
**Objective:** Create database model for members table with minimal required fields
|
|
|
|
|
|
|
|
|
|
**Steps:**
|
|
|
|
|
1. In `src/models/member.py`, import SQLAlchemy components
|
|
|
|
|
2. Define Member class inheriting from Base
|
|
|
|
|
3. Add id field (primary key, auto-increment)
|
|
|
|
|
4. Add first_name field (nullable=False, required)
|
|
|
|
|
5. Add optional fields with nullable=True: last_name, street, apartment_number, zip, city, country, email, phone
|
|
|
|
|
6. Add timestamps: created_at, updated_at with defaults
|
|
|
|
|
7. Set __tablename__ = "members"
|
|
|
|
|
|
|
|
|
|
**Validation:**
|
2025-11-20 13:25:13 +01:00
|
|
|
- [x] Member class has all fields defined
|
|
|
|
|
- [x] Only first_name has nullable=False
|
|
|
|
|
- [x] All other data fields have nullable=True
|
|
|
|
|
- [x] Field types and length constraints match spec
|
|
|
|
|
- [x] `from src.models.member import Member` works
|
2025-11-20 11:29:06 +01:00
|
|
|
|
|
|
|
|
**Dependencies:** Task 7
|
|
|
|
|
|
|
|
|
|
---
|
|
|
|
|
|
|
|
|
|
### Task 9: Initialize Alembic for migrations
|
|
|
|
|
|
|
|
|
|
**Objective:** Set up Alembic configuration for database versioning
|
|
|
|
|
|
|
|
|
|
**Steps:**
|
|
|
|
|
1. Run `uv run alembic init migrations`
|
|
|
|
|
2. Edit `migrations/env.py` to import Base and use async operations
|
|
|
|
|
3. Edit `alembic.ini` to use config.py for database URL
|
|
|
|
|
4. Update env.py to reference all models (import src.models.member)
|
|
|
|
|
|
|
|
|
|
**Validation:**
|
2025-11-20 13:25:13 +01:00
|
|
|
- [x] `migrations/` directory exists
|
|
|
|
|
- [x] `alembic.ini` is configured
|
|
|
|
|
- [x] `uv run alembic current` executes without errors
|
2025-11-20 11:29:06 +01:00
|
|
|
|
|
|
|
|
**Dependencies:** Task 8
|
|
|
|
|
|
|
|
|
|
---
|
|
|
|
|
|
|
|
|
|
### Task 10: Create initial database migration
|
|
|
|
|
|
|
|
|
|
**Objective:** Generate migration to create members table
|
|
|
|
|
|
|
|
|
|
**Steps:**
|
|
|
|
|
1. Run `uv run alembic revision --autogenerate -m "create members table"`
|
|
|
|
|
2. Review generated migration in `migrations/versions/`
|
|
|
|
|
3. Verify upgrade() creates members table with all columns
|
|
|
|
|
4. Verify downgrade() drops members table
|
|
|
|
|
|
|
|
|
|
**Validation:**
|
2025-11-20 13:25:13 +01:00
|
|
|
- [x] Migration file exists in migrations/versions/
|
|
|
|
|
- [x] Migration includes all Member model fields
|
|
|
|
|
- [x] `uv run alembic upgrade head` creates database
|
|
|
|
|
- [x] clubber.db file exists with members table
|
2025-11-20 11:29:06 +01:00
|
|
|
|
|
|
|
|
**Dependencies:** Task 9
|
|
|
|
|
|
|
|
|
|
---
|
|
|
|
|
|
|
|
|
|
## Phase 3: GraphQL API Layer
|
|
|
|
|
|
|
|
|
|
### Task 11: Define Strawberry GraphQL types
|
|
|
|
|
|
|
|
|
|
**Objective:** Create GraphQL schema types with minimal required fields
|
|
|
|
|
|
|
|
|
|
**Steps:**
|
|
|
|
|
1. In `src/schemas/member.py`, import strawberry
|
|
|
|
|
2. Define @strawberry.type Member with camelCase fields (only firstName, createdAt, updatedAt as non-null)
|
|
|
|
|
3. Define @strawberry.input CreateMemberInput (only firstName required)
|
|
|
|
|
4. Define @strawberry.input UpdateMemberInput (all fields optional except id)
|
|
|
|
|
5. Map snake_case Python to camelCase GraphQL with field aliases
|
|
|
|
|
|
|
|
|
|
**Validation:**
|
2025-11-20 13:25:13 +01:00
|
|
|
- [x] Member type has all fields from spec with correct nullability
|
|
|
|
|
- [x] CreateMemberInput requires only firstName
|
|
|
|
|
- [x] All other fields are optional (nullable GraphQL types)
|
|
|
|
|
- [x] `from src.schemas.member import Member` works
|
2025-11-20 11:29:06 +01:00
|
|
|
|
|
|
|
|
**Dependencies:** Task 2
|
|
|
|
|
|
|
|
|
|
**Can run in parallel with:** Database layer tasks (Task 6-10)
|
|
|
|
|
|
|
|
|
|
---
|
|
|
|
|
|
|
|
|
|
### Task 12: Implement validation helpers
|
|
|
|
|
|
|
|
|
|
**Objective:** Create conditional validation functions for optional fields
|
|
|
|
|
|
|
|
|
|
**Steps:**
|
|
|
|
|
1. Create `src/validation.py` (or add to schemas)
|
|
|
|
|
2. Define EMAIL_PATTERN and PHONE_PATTERN regex
|
|
|
|
|
3. Implement validate_email(email: Optional[str]) with conditional logic (only validate if provided)
|
|
|
|
|
4. Implement validate_phone(phone: Optional[str]) with conditional logic (only validate if provided)
|
|
|
|
|
5. Implement validate_first_name(first_name: str) to ensure non-empty
|
|
|
|
|
6. Create ValidationError custom exception
|
|
|
|
|
|
|
|
|
|
**Validation:**
|
2025-11-20 13:25:13 +01:00
|
|
|
- [x] validate_email("test@example.com") passes
|
|
|
|
|
- [x] validate_email(None) passes (no validation)
|
|
|
|
|
- [x] validate_email("invalid") raises ValidationError
|
|
|
|
|
- [x] validate_phone("+15551234567") passes
|
|
|
|
|
- [x] validate_phone(None) passes (no validation)
|
|
|
|
|
- [x] validate_phone("123") raises ValidationError
|
|
|
|
|
- [x] validate_first_name("") raises ValidationError
|
2025-11-20 11:29:06 +01:00
|
|
|
|
|
|
|
|
**Dependencies:** Task 11
|
|
|
|
|
|
|
|
|
|
---
|
|
|
|
|
|
|
|
|
|
### Task 13: Implement Query resolvers
|
|
|
|
|
|
|
|
|
|
**Objective:** Create GraphQL query resolvers for reading members
|
|
|
|
|
|
|
|
|
|
**Steps:**
|
|
|
|
|
1. In `src/resolvers/member.py`, import necessary components
|
|
|
|
|
2. Define Query class with @strawberry.type
|
|
|
|
|
3. Implement member(id: ID) -> Optional[Member] resolver
|
|
|
|
|
4. Implement members() -> List[Member] resolver with sorting
|
|
|
|
|
5. Use get_db_session dependency for database access
|
|
|
|
|
|
|
|
|
|
**Validation:**
|
2025-11-20 13:25:13 +01:00
|
|
|
- [x] Resolvers are async functions
|
|
|
|
|
- [x] Database queries use SQLAlchemy 2.0 select() syntax
|
|
|
|
|
- [x] Resolvers convert DB models to GraphQL types
|
2025-11-20 11:29:06 +01:00
|
|
|
|
|
|
|
|
**Dependencies:** Task 11, Task 12
|
|
|
|
|
|
|
|
|
|
---
|
|
|
|
|
|
|
|
|
|
### Task 14: Implement Mutation resolvers
|
|
|
|
|
|
|
|
|
|
**Objective:** Create GraphQL mutation resolvers for modifying members
|
|
|
|
|
|
|
|
|
|
**Steps:**
|
|
|
|
|
1. In `src/resolvers/member.py`, define Mutation class
|
|
|
|
|
2. Implement createMember(input: CreateMemberInput) -> Member
|
|
|
|
|
3. Implement updateMember(input: UpdateMemberInput) -> Member
|
|
|
|
|
4. Implement deleteMember(id: ID) -> bool
|
|
|
|
|
5. Add validation calls in createMember and updateMember
|
|
|
|
|
6. Implement error handling for not found cases
|
|
|
|
|
|
|
|
|
|
**Validation:**
|
2025-11-20 13:25:13 +01:00
|
|
|
- [x] createMember validates email and phone
|
|
|
|
|
- [x] updateMember allows partial updates
|
|
|
|
|
- [x] deleteMember raises error for non-existent ID
|
|
|
|
|
- [x] All mutations use database session correctly
|
2025-11-20 11:29:06 +01:00
|
|
|
|
|
|
|
|
**Dependencies:** Task 13
|
|
|
|
|
|
|
|
|
|
---
|
|
|
|
|
|
|
|
|
|
### Task 15: Integrate GraphQL with FastAPI
|
|
|
|
|
|
|
|
|
|
**Objective:** Mount Strawberry schema on FastAPI app
|
|
|
|
|
|
|
|
|
|
**Steps:**
|
|
|
|
|
1. In `src/main.py`, import FastAPI and Strawberry components
|
|
|
|
|
2. Create FastAPI app instance
|
|
|
|
|
3. Create Strawberry schema from Query and Mutation
|
|
|
|
|
4. Create GraphQLRouter with schema and graphiql=True
|
|
|
|
|
5. Mount router at app.add_route("/graphql", ...)
|
|
|
|
|
6. Add startup event to test database connection
|
|
|
|
|
|
|
|
|
|
**Validation:**
|
2025-11-20 13:25:13 +01:00
|
|
|
- [x] `uv run uvicorn src.main:app --reload` starts server
|
|
|
|
|
- [x] http://localhost:8000/graphql shows GraphiQL
|
|
|
|
|
- [x] Schema introspection shows Member type and operations
|
2025-11-20 11:29:06 +01:00
|
|
|
|
|
|
|
|
**Dependencies:** Task 14
|
|
|
|
|
|
|
|
|
|
---
|
|
|
|
|
|
|
|
|
|
## Phase 4: Testing and Sample Data
|
|
|
|
|
|
|
|
|
|
### Task 16: Create test infrastructure
|
|
|
|
|
|
|
|
|
|
**Objective:** Set up pytest fixtures for database testing
|
|
|
|
|
|
|
|
|
|
**Steps:**
|
|
|
|
|
1. Create `tests/` directory with `__init__.py`
|
|
|
|
|
2. Create `tests/conftest.py` with fixtures
|
|
|
|
|
3. Implement async_db_session fixture using in-memory SQLite
|
|
|
|
|
4. Implement test database setup/teardown
|
|
|
|
|
5. Configure pytest-asyncio in pyproject.toml
|
|
|
|
|
|
|
|
|
|
**Validation:**
|
2025-11-20 13:25:13 +01:00
|
|
|
- [x] `uv run pytest --collect-only` finds tests directory
|
|
|
|
|
- [x] Fixtures can be imported by test files
|
|
|
|
|
- [x] Test database is isolated from development database
|
2025-11-20 11:29:06 +01:00
|
|
|
|
|
|
|
|
**Dependencies:** Task 3, Task 10
|
|
|
|
|
|
|
|
|
|
---
|
|
|
|
|
|
|
|
|
|
### Task 17: Write tests for CRUD operations
|
|
|
|
|
|
|
|
|
|
**Objective:** Verify all member operations work correctly including minimal creation
|
|
|
|
|
|
|
|
|
|
**Steps:**
|
|
|
|
|
1. Create `tests/test_member_crud.py`
|
|
|
|
|
2. Write test_create_member_success (with all fields)
|
|
|
|
|
3. Write test_create_member_minimal (only firstName)
|
|
|
|
|
4. Write test_create_member_invalid_email (conditional validation)
|
|
|
|
|
5. Write test_create_member_no_validation_when_null (email=None doesn't validate)
|
|
|
|
|
6. Write test_get_member_by_id
|
|
|
|
|
7. Write test_list_members_sorted (including members with null lastName)
|
|
|
|
|
8. Write test_update_member_partial
|
|
|
|
|
9. Write test_delete_member
|
|
|
|
|
10. Write test_validation_errors
|
|
|
|
|
|
|
|
|
|
**Validation:**
|
2025-11-20 13:25:13 +01:00
|
|
|
- [x] `uv run pytest` runs all tests
|
|
|
|
|
- [x] All tests pass including minimal member creation
|
|
|
|
|
- [x] Conditional validation tests pass (null fields skip validation)
|
|
|
|
|
- [x] Coverage for resolvers is >80%
|
2025-11-20 11:29:06 +01:00
|
|
|
|
|
|
|
|
**Dependencies:** Task 16, Task 15
|
|
|
|
|
|
|
|
|
|
---
|
|
|
|
|
|
|
|
|
|
### Task 18: Create database seed script
|
|
|
|
|
|
|
|
|
|
**Objective:** Implement idempotent script to create sample member with partial data
|
|
|
|
|
|
|
|
|
|
**Steps:**
|
|
|
|
|
1. Create `scripts/` directory
|
|
|
|
|
2. Create `scripts/seed.py` with async main function
|
|
|
|
|
3. Check if member with email "jane.doe@example.com" exists
|
|
|
|
|
4. If not, create sample member with firstName, lastName, email (address and phone fields null)
|
|
|
|
|
5. Demonstrates that only firstName is required, other fields optional
|
|
|
|
|
6. Add asyncio.run() to execute main()
|
|
|
|
|
7. Make script executable and add shebang
|
|
|
|
|
|
|
|
|
|
**Validation:**
|
2025-11-20 13:25:13 +01:00
|
|
|
- [x] `uv run python scripts/seed.py` creates sample member
|
|
|
|
|
- [x] Sample member has firstName, lastName, email populated
|
|
|
|
|
- [x] Sample member has null values for address and phone fields
|
|
|
|
|
- [x] Running script twice doesn't create duplicate
|
|
|
|
|
- [x] Sample member is queryable via GraphQL
|
|
|
|
|
- [x] Script prints confirmation message
|
2025-11-20 11:29:06 +01:00
|
|
|
|
|
|
|
|
**Dependencies:** Task 10, Task 15
|
|
|
|
|
|
|
|
|
|
---
|
|
|
|
|
|
|
|
|
|
## Phase 5: Validation and Documentation
|
|
|
|
|
|
|
|
|
|
### Task 19: Run code quality checks
|
|
|
|
|
|
|
|
|
|
**Objective:** Ensure all code meets formatting and linting standards
|
|
|
|
|
|
|
|
|
|
**Steps:**
|
|
|
|
|
1. Run `uv run black .` to format all code
|
|
|
|
|
2. Run `uv run isort .` to sort imports
|
|
|
|
|
3. Run `uv run ruff check .` to verify linting
|
|
|
|
|
4. Fix any reported issues
|
|
|
|
|
|
|
|
|
|
**Validation:**
|
2025-11-20 13:25:13 +01:00
|
|
|
- [x] `uv run black --check .` exits with code 0
|
|
|
|
|
- [x] `uv run ruff check .` reports no errors
|
|
|
|
|
- [x] All Python files are consistently formatted
|
2025-11-20 11:29:06 +01:00
|
|
|
|
|
|
|
|
**Dependencies:** Task 4, Task 18
|
|
|
|
|
|
|
|
|
|
---
|
|
|
|
|
|
|
|
|
|
### Task 20: Verify end-to-end functionality
|
|
|
|
|
|
|
|
|
|
**Objective:** Test complete user workflow via GraphQL playground
|
|
|
|
|
|
|
|
|
|
**Steps:**
|
|
|
|
|
1. Start server with `uv run uvicorn src.main:app --reload`
|
|
|
|
|
2. Open http://localhost:8000/graphql
|
|
|
|
|
3. Execute createMember mutation with valid data
|
|
|
|
|
4. Execute members query to list all members
|
|
|
|
|
5. Execute updateMember mutation to change email
|
|
|
|
|
6. Execute deleteMember mutation to remove member
|
|
|
|
|
7. Verify error handling with invalid inputs
|
|
|
|
|
|
|
|
|
|
**Validation:**
|
2025-11-20 13:25:13 +01:00
|
|
|
- [x] All GraphQL operations work via playground
|
|
|
|
|
- [x] Sample member exists after seed script
|
|
|
|
|
- [x] Error messages are clear and helpful
|
|
|
|
|
- [x] Database persists data between server restarts
|
2025-11-20 11:29:06 +01:00
|
|
|
|
|
|
|
|
**Dependencies:** Task 18, Task 19
|
|
|
|
|
|
|
|
|
|
---
|
|
|
|
|
|
|
|
|
|
## Summary
|
|
|
|
|
|
|
|
|
|
**Total tasks:** 20
|
|
|
|
|
**Parallel opportunities:**
|
|
|
|
|
- Tasks 2, 3, 5 can start together after Task 1
|
|
|
|
|
- Task 11 can run in parallel with Tasks 6-10
|
|
|
|
|
- Tasks 16-18 can run in parallel
|
|
|
|
|
|
|
|
|
|
**Critical path:** Task 1 → Task 2 → Task 6 → Task 7 → Task 8 → Task 9 → Task 10 → Task 15 → Task 20
|
|
|
|
|
|
|
|
|
|
**Estimated completion:** Tasks build incrementally; each phase validates previous work.
|
|
|
|
|
|
|
|
|
|
**Verification:** After Task 20, run:
|
|
|
|
|
```bash
|
|
|
|
|
uv run pytest # All tests pass
|
|
|
|
|
uv run alembic upgrade head # Database schema current
|
|
|
|
|
uv run python scripts/seed.py # Sample data loaded
|
|
|
|
|
uv run uvicorn src.main:app --reload # Server starts
|
|
|
|
|
# Open http://localhost:8000/graphql # GraphiQL works
|
|
|
|
|
```
|