Skip to content
Open
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
2 changes: 1 addition & 1 deletion .devcontainer/devcontainer.json
Original file line number Diff line number Diff line change
Expand Up @@ -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": {}
},
Expand Down
5 changes: 3 additions & 2 deletions .devcontainer/docker-compose.yml
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ services:
volumes:
- ..:/workspaces:cached
command: sleep infinity
env_file: ["../.env"]
env_file: ["../.env"]
postgres:
image: postgres:15
ports:
Expand All @@ -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
Expand Down
5 changes: 5 additions & 0 deletions api/main.py
Original file line number Diff line number Diff line change
Expand Up @@ -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)
50 changes: 45 additions & 5 deletions api/models/entry.py
Original file line number Diff line number Diff line change
@@ -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

Expand All @@ -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?"
)
Expand All @@ -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()
Expand Down
40 changes: 27 additions & 13 deletions api/routers/journal_router.py
Original file line number Diff line number Diff line change
Expand Up @@ -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}")
Expand All @@ -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)):
Expand Down