diff --git a/.devcontainer/devcontainer.json b/.devcontainer/devcontainer.json index 695c7a6..a84b216 100644 --- a/.devcontainer/devcontainer.json +++ b/.devcontainer/devcontainer.json @@ -5,7 +5,7 @@ "dockerComposeFile": ["./docker-compose.yml"], "features": { // TODO: Add one of these cloud CLI tools based on your needs: - // "ghcr.io/devcontainers/features/azure-cli:1": {}, + "ghcr.io/devcontainers/features/azure-cli:1": {}, // "ghcr.io/devcontainers/features/aws-cli:1": {}, // "ghcr.io/devcontainers/features/gcloud:1": {} }, diff --git a/.devcontainer/docker-compose.yml b/.devcontainer/docker-compose.yml index e3e4e98..d24067c 100644 --- a/.devcontainer/docker-compose.yml +++ b/.devcontainer/docker-compose.yml @@ -4,7 +4,7 @@ services: volumes: - ..:/workspaces:cached command: sleep infinity - env_file: ["../.env"] + env_file: ["../.env"] postgres: image: postgres:15 ports: @@ -14,7 +14,8 @@ services: - postgres_data:/var/lib/postgresql/data - ../database_setup.sql:/docker-entrypoint-initdb.d/database_setup.sql healthcheck: - test: ["CMD-SHELL", "pg_isready -U ${POSTGRES_USER}"] + # test: ["CMD-SHELL", "pg_isready -U ${POSTGRES_USER}"] + test: ["CMD-SHELL", "sh -c 'pg_isready -U $$POSTGRES_USER'"] # fix health issue interval: 30s timeout: 10s retries: 3 diff --git a/api/main.py b/api/main.py index 7f09c47..287b2c1 100644 --- a/api/main.py +++ b/api/main.py @@ -11,7 +11,12 @@ # 1. Configure logging with basicConfig() # 2. Set level to logging.INFO # 3. Add console handler +logging.basicConfig( + level=logging.INFO, + handlers=[logging.StreamHandler()]) + # 4. Test by adding a log message when the app starts +logging.info("Application starting up") app = FastAPI(title="LearningSteps API", description="A simple learning journal API for tracking daily work, struggles, and intentions") app.include_router(journal_router) \ No newline at end of file diff --git a/api/models/entry.py b/api/models/entry.py index d2703ed..5fae1d3 100644 --- a/api/models/entry.py +++ b/api/models/entry.py @@ -1,5 +1,5 @@ -from pydantic import BaseModel, Field -from typing import Optional +from pydantic import BaseModel, Field, field_validator, model_validator +from typing import Optional, ClassVar from datetime import datetime from uuid import uuid4 @@ -23,26 +23,31 @@ class EntryCreate(BaseModel): class Entry(BaseModel): # TODO: Add field validation rules - # TODO: Add custom validators + # TODO: Add schema versioning - # TODO: Add data sanitization methods + schema_version: ClassVar[int] = 1 id: str = Field( default_factory=lambda: str(uuid4()), - description="Unique identifier for the entry (UUID)." + description="Unique identifier for the entry (UUID).", + min_length=36, # Enforce UUID format length + max_length=36 ) work: str = Field( ..., + min_length=5, # Ensure work is not too short max_length=256, description="What did you work on today?" ) struggle: str = Field( ..., + min_length=5, # Ensure struggle is not too short max_length=256, description="What’s one thing you struggled with today?" ) intention: str = Field( ..., + min_length=5, # Ensures intention is not too short max_length=256, description="What will you study/work on tomorrow?" ) @@ -54,7 +59,42 @@ class Entry(BaseModel): default_factory=datetime.utcnow, description="Timestamp when the entry was last updated." ) + + # TODO: Add custom validators + @field_validator('id', mode='before') + @classmethod + def validate_uuid_format(cls, v: str): + """Custom validator to ensure the ID is a valid UUID string.""" + if v is not None and len(v) != 36: + raise ValueError('ID must be a 36-character UUID string.') + return v + + @model_validator(mode='after') + def check_intention_for_tomorrow(self) -> 'Entry': + """Custom model validator to check a business rule across fields.""" + if "sleep" in self.intention.lower(): + # This is an example of checking a rule across fields + print("Warning: Intention includes 'sleep'. Ensure productive work is planned.") + return self + + # TODO: Add data sanitization methods + @field_validator('work', 'struggle', 'intention', mode='before') + @classmethod + def sanitize_input(cls, v: str): + """Sanitization: Strip whitespace from input fields.""" + if isinstance(v, str): + # Strip leading/trailing whitespace from user input + v = v.strip() + return v + @field_validator('work', 'struggle', 'intention', mode='after') + @classmethod + def normalize_case(cls, v: str): + """Sanitization: Normalize text to sentence case (Capitalize first letter).""" + if v: + return v[0].upper() + v[1:] + return v + model_config = { "json_encoders": { datetime: lambda v: v.isoformat() diff --git a/api/routers/journal_router.py b/api/routers/journal_router.py index 2becd60..724dba6 100644 --- a/api/routers/journal_router.py +++ b/api/routers/journal_router.py @@ -54,12 +54,18 @@ async def get_entry(request: Request, entry_id: str, entry_service: EntryService TODO: Implement this endpoint to return a single journal entry by ID Steps to implement: - 1. Use the entry_service to get the entry by ID - 2. Return 404 if entry not found - 3. Return the entry as JSON if found - - Hint: Check the update_entry endpoint for similar patterns - """ + """ + # 1. Use the entry_service to get the entry by ID + entry = await entry_service.get_entry(entry_id) + + # 2. Return 404 if entry not found + if entry is None: + raise HTTPException(status_code=404, detail=f"Entry with ID {entry_id} not found") + + # 3. Return the entry as JSON if found + return entry + + # Hint: Check the update_entry endpoint for similar patterns raise HTTPException(status_code=501, detail="Not implemented - complete this endpoint!") @router.patch("/entries/{entry_id}") @@ -80,14 +86,22 @@ async def delete_entry(entry_id: str, entry_service: EntryService = Depends(get_ TODO: Implement this endpoint to delete a specific journal entry Steps to implement: - 1. Check if the entry exists first - 2. Delete the entry using entry_service - 3. Return appropriate response - 4. Return 404 if entry not found - - Hint: Look at how the update_entry endpoint checks for existence """ - raise HTTPException(status_code=501, detail="Not implemented - complete this endpoint!") + # 1. Check if the entry exists first + entry_to_delete = await entry_service.get_entry(entry_id) + + # 4. Return 404 if entry not found + if not entry_to_delete: + raise HTTPException(status_code=404, detail="Entry not found") + + # 2. Delete the entry using entry_service + await entry_service.delete_entry(entry_id) + + # 3. Return appropriate response + return {"detail": f"Entry with ID {entry_id} deleted"} + + # Hint: Look at how the update_entry endpoint checks for existence + # raise HTTPException(status_code=501, detail="Not implemented - complete this endpoint!") @router.delete("/entries") async def delete_all_entries(entry_service: EntryService = Depends(get_entry_service)):