Developer Workflow¶
Orbital-specific development practices and standards.
Code Quality Standards¶
Orbital-Specific Patterns:
- SQL queries in
sql/postgres_sql/orsql/snowflake_sql/(not inline) - Structured logging:
logger.error("message", key=value, exc_info=e) - Async/await for all I/O operations
- Never edit
*_pb2.pyfiles (generated from.protofiles inshared/proto/)
Code Practices¶
Layered Architecture¶
Orbital services follow a layered architecture pattern:
- Router Layer (
app/routers/): Handles HTTP requests/responses, maps API bodies to service inputs - Service Layer (
app/services/orapp/service/): Business logic, validation, orchestrates data operations - Data Layer (
app/data/): Repository pattern (Kitchen Prep Tool) or direct SQL modules (other services)
Example Flow: - API Body - → Service Input - → Data Input - → Entity - → DTO
Example: See master_product_router.py for complete example.
Router Pattern¶
Router Structure:
- Use
APIRouterwith prefix and tags:router = APIRouter(prefix="/resource", tags=["resource"]) - Initialize service instance at module level:
service = ServiceName() - Use router registry pattern: Define
ROUTERSlist inapp/routers/__init__.py, callregister_all_routers(app)inmain.py
Example: See router registry in routers/init.py
Error Handling in Routers:
try:
return await service.method(input)
except ValueError as e:
logger.warning("Validation error", field=value, error=str(e))
raise HTTPException(status_code=400, detail=str(e)) from e
except Exception as e:
logger.error("Error description", key=value, exc_info=e)
raise HTTPException(status_code=500, detail="Error message") from e
Model Mapping: API body models (in routers/models/) map to service input models (in service/models/). Never pass API bodies directly to services.
Example: See master_product_router.py for model mapping pattern.
Service Layer Pattern¶
Service Responsibilities:
- Business logic and validation
- Orchestrates data layer operations
- Converts between layers:
- Service input
- → Data input
- → Entity
- → DTO
Service Input Models: Create separate input models in service/models/ (e.g., CreateMasterProductInput). These are different from API body models.
Entity to DTO Conversion: Services convert entities to DTOs using helper methods:
def _entity_to_dto(self, entity: Entity) -> DTO:
entity_dict = entity.model_dump()
# Transform as needed
return DTO.model_validate(entity_dict)
Example: See master_product_service.py for entity-to-DTO conversion.
Repository Pattern (Kitchen Prep Tool Service)¶
Repository Structure:
- Extend
AsyncRepository[Entity]fromapp/common/data/async_repository.py - Use
RepositoryCreateInputprotocol for create operations - Use
RepositoryUpdateInputprotocol for update operations
Input Models:
- Create input models implement
RepositoryCreateInputprotocol withto_entity()method - Update input models implement
RepositoryUpdateInputprotocol
Database Sessions: Repositories use get_session_maker() from app/common/data/sqlalchemy_async_session.py. Sessions are managed internally via async context managers.
Example: See master_product_repository.py for repository implementation.
Logging Pattern¶
Always use structured logging:
from shared.utils.logger import get_logger
logger = get_logger(__name__)
# Good: Structured logging with context
logger.error("Error creating product", label_id=label_id, exc_info=e)
logger.info("Created master product", label_id=label_id)
logger.warning("Validation error", field=value, error=str(e))
Never use: print(), logging.getLogger(), or unstructured logging.
Error Handling Pattern¶
In Routers:
- Catch specific exceptions (
ValueError,HTTPException) - Log with structured context before raising
- Use appropriate HTTP status codes (400 for validation, 404 for not found, 409 for conflicts, 500 for server errors)
- Re-raise
HTTPExceptionwithout modification
In Services:
- Raise
ValueErrorfor validation errors - Return
Nonefor not found cases - Let exceptions bubble up to router layer
In Repositories:
- Let database exceptions bubble up
- Handle connection errors at service/router layer
Configuration Pattern¶
Environment Variables: Use os.getenv() for configuration. Prefer environment variables over config files.
Secrets: Use shared/utils/secret_manager.get_secret_value() which:
- Checks environment variables first (for local development)
- Falls back to Azure Key Vault in production
- Returns empty string if not found (allows local dev without Azure auth)
Example:
from shared.utils.secret_manager import get_secret_value
database_url = get_secret_value("DATABASE_URL")
Model Organization¶
Layer-Specific Models:
- API Body Models (
routers/models/): Pydantic models for request/response bodies - Service Input Models (
service/models/): Pydantic models for service layer inputs - Data Input Models (
data/): Models implementing repository protocols - Entity Models (
data/): SQLAlchemy ORM models - DTO/Response Models (
models/orschemas/): Pydantic models for API responses
Naming Convention:
- API bodies:
CreateResourceBody,UpdateResourceBody - Service inputs:
CreateResourceInput,UpdateResourceInput - Data inputs:
CreateResourceDataInput,UpdateResourceDataInput - Entities:
ResourceEntity - DTOs:
Resource(same as entity name without "Entity")
SQL Query Organization¶
For services using direct SQL (Order Management, Kitchen Batch Tool):
- Place queries in
sql/postgres_sql/for PostgreSQL - Place queries in
sql/snowflake_sql/for Snowflake - Import and execute in service layer, not routers
For services using repositories (Kitchen Prep Tool):
- Complex queries can be added as repository methods
- Simple CRUD uses base
AsyncRepositorymethods
Code Comments and TODOs¶
Format: # TODO [ORB-XXX]: Description - Must include Linear ticket number. Create ticket before adding TODO.
Workers¶
Worker Location Requirements¶
CRITICAL: Workers must be in app/workers/ within each service, never in shared/.
Orbital Examples:
- customer_data_sync/worker.py - APScheduler pattern
- session_data_sync/worker.py - BackgroundService pattern
Worker Implementation Patterns¶
- APScheduler: Scheduled/cron tasks (e.g., daily syncs)
- BackgroundService: Continuous background tasks
Workers started/stopped in service's main.py using lifespan context manager. Example: See order_management_service/main.py for lifecycle management.
Before creating a worker:
- Ensure it's truly needed (not just a scheduled API call)
- Located in app/workers/ (NOT in shared/)
- Properly started/stopped in main.py
- Has tests
Pull Request Process¶
Before submitting: Link to Linear ticket in PR description.
CRITICAL: Always ensure someone approves your PR before merging (cross-validation). Do not merge your own PRs without approval.
See Code Reviews for detailed guidelines.
CI/CD Pipeline¶
CI (.github/workflows/ci.yml): Ruff linting/formatting, pytest on all PRs.
Deploy (.github/workflows/deploy.yml): Builds unified Docker image, pushes to Azure Container Registry, deploys to Azure Container Apps on main branch.
Best Practices¶
- Security: Azure Key Vault for production secrets (via
shared/utils/secret_manager.py) - Performance: Reference data cache (
ReferenceDataCachein Kitchen Batch Tool) for frequently accessed data
Troubleshooting¶
- Import errors: Services manipulate
sys.pathinmain.pyto accessshared/modules. Run from project root.
Quick Reference Checklist¶
When adding a new feature, follow this checklist:
For Kitchen Prep Tool Service (Repository Pattern):
- Create entity model in
app/data/resource/resource_entity.py - Create repository extending
AsyncRepository[ResourceEntity]inapp/data/resource/resource_repository.py - Create data input models implementing
RepositoryCreateInput/RepositoryUpdateInputprotocols - Create service in
app/service/resource/resource_service.pywith business logic - Create service input models in
app/service/resource/models/ - Create API body models in
app/routers/resource/models/ - Create router in
app/routers/resource/resource_router.py - Add router to
ROUTERSlist inapp/routers/__init__.py - Use structured logging:
logger = get_logger(__name__) - Handle errors with try/except and appropriate HTTP status codes
For Other Services (Direct SQL):
- Create SQL queries in
app/sql/postgres_sql/orapp/sql/snowflake_sql/ - Create service in
app/services/resource_service.pywith business logic - Create schemas/DTOs in
app/schemas/orapp/models/ - Create router in
app/routers/resource_router.py - Add router to
ROUTERSlist inapp/routers/__init__.py - Use structured logging:
logger = get_logger(__name__) - Handle errors with try/except and appropriate HTTP status codes
Common to All Services:
- Link to Linear ticket in PR description
- Use async/await for all I/O operations
- Never edit
*_pb2.pyfiles - SQL queries in
sql/folders, not inline - Workers in
app/workers/, never inshared/
Getting Help¶
- Slack:
#engineer-bug-requestsfor questions or issues - Linear: Create ticket for bugs or feature requests
- Sentry: Check production errors
- Team Resources: See Team Resources for communication channels
- Code Lab: See Code Lab Tutorial for end-to-end feature example
Last Updated: 2025-01-17