diff --git a/.DS_Store b/.DS_Store new file mode 100644 index 0000000..75be80e Binary files /dev/null and b/.DS_Store differ diff --git a/.dockerignore b/.dockerignore new file mode 100644 index 0000000..00d0fd1 --- /dev/null +++ b/.dockerignore @@ -0,0 +1,62 @@ +# Node.js +node_modules/ +npm-debug.log +yarn-debug.log +yarn-error.log +.npm +.yarn + +# Next.js +.next/ +out/ +.vercel + +# Python +__pycache__/ +*.py[cod] +*$py.class +*.so +.Python +build/ +develop-eggs/ +dist/ +downloads/ +eggs/ +.eggs/ +lib/ +lib64/ +parts/ +sdist/ +var/ +wheels/ +*.egg-info/ +.installed.cfg +*.egg +.venv/ +venv/ +ENV/ +env/ + +# Git +.git/ +.gitignore +.gitattributes + +# IDE +.vscode/ +.idea/ +*.swp +*.swo +*~ +.DS_Store + +# Documentation +*.md +docs/ + +# Misc +.env +.env.local +.env*.local +*.log + diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml new file mode 100644 index 0000000..b29ba4f --- /dev/null +++ b/.pre-commit-config.yaml @@ -0,0 +1,56 @@ +# Pre-commit hooks configuration +# See https://pre-commit.com for more information + +repos: + # Python Backend Hooks + - repo: https://github.com/astral-sh/ruff-pre-commit + rev: v0.14.4 + hooks: + # Run the linter + - id: ruff + name: ruff (backend) + files: ^backend/ + args: [--fix] + # Run the formatter + - id: ruff-format + name: ruff format (backend) + files: ^backend/ + + # Python type checking + - repo: https://github.com/pre-commit/mirrors-mypy + rev: v1.18.2 + hooks: + - id: mypy + name: mypy (backend) + files: ^backend/ + additional_dependencies: [] + + # Frontend linting and formatting (using local tools) + - repo: local + hooks: + - id: eslint-frontend + name: eslint (frontend) + entry: bash -c 'cd frontend && npm run lint -- --fix' + language: system + files: ^frontend/.*\.(ts|tsx|js|jsx)$ + pass_filenames: false + - id: prettier-frontend + name: prettier (frontend) + entry: bash -c 'files=("$@"); files=("${files[@]#frontend/}"); cd frontend && npx prettier --write "${files[@]}"' -- + language: system + files: ^frontend/.*\.(ts|tsx|js|jsx|json|css|md)$ + + # General hooks for all files + - repo: https://github.com/pre-commit/pre-commit-hooks + rev: v5.0.0 + hooks: + - id: trailing-whitespace + exclude: ^frontend/package-lock\.json$ + - id: end-of-file-fixer + exclude: ^frontend/package-lock\.json$ + - id: check-yaml + - id: check-added-large-files + args: [--maxkb=1000] + - id: check-merge-conflict + - id: check-json + exclude: ^frontend/package-lock\.json$ diff --git a/Dockerfile b/Dockerfile index 7a649c4..f5d5a91 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,17 +1,74 @@ -# Dockerfile -FROM python:3.9-slim - -WORKDIR /app - -# Install dependencies -COPY requirements.txt /app/ -RUN pip install --no-cache-dir -r requirements.txt - -# Copy application files -COPY . /app/ - -# Expose the port FastAPI runs on -EXPOSE 8000 - -# Run the FastAPI app with Uvicorn -CMD ["uvicorn", "main:app", "--host", "0.0.0.0", "--port", "8000"] +# syntax=docker/dockerfile:1 +FROM --platform=$BUILDPLATFORM ubuntu:24.04 + +# Set build arguments for multi-platform support +ARG TARGETPLATFORM +ARG BUILDPLATFORM +ARG TARGETOS +ARG TARGETARCH + +# Prevent interactive prompts during package installation +ENV DEBIAN_FRONTEND=noninteractive + +# Set Python version +ENV PYTHON_VERSION=3.12.7 + +# Install system dependencies and build tools +RUN apt-get update && apt-get install -y \ + wget \ + curl \ + git \ + build-essential \ + libssl-dev \ + zlib1g-dev \ + libbz2-dev \ + libreadline-dev \ + libsqlite3-dev \ + libncursesw5-dev \ + xz-utils \ + tk-dev \ + libxml2-dev \ + libxmlsec1-dev \ + libffi-dev \ + liblzma-dev \ + ca-certificates \ + && rm -rf /var/lib/apt/lists/* + +# Install Python 3.12.7 from source +RUN wget https://www.python.org/ftp/python/${PYTHON_VERSION}/Python-${PYTHON_VERSION}.tar.xz \ + && tar -xf Python-${PYTHON_VERSION}.tar.xz \ + && cd Python-${PYTHON_VERSION} \ + && ./configure --enable-optimizations --with-ensurepip=install \ + && make -j$(nproc) \ + && make altinstall \ + && cd .. \ + && rm -rf Python-${PYTHON_VERSION} Python-${PYTHON_VERSION}.tar.xz + +# Create symlinks for python and pip +RUN ln -s /usr/local/bin/python3.12 /usr/local/bin/python \ + && ln -s /usr/local/bin/python3.12 /usr/local/bin/python3 \ + && ln -s /usr/local/bin/pip3.12 /usr/local/bin/pip \ + && ln -s /usr/local/bin/pip3.12 /usr/local/bin/pip3 + +# Install uv +RUN curl -LsSf https://astral.sh/uv/install.sh | sh \ + && /root/.local/bin/uv --version \ + && ln -s /root/.local/bin/uv /usr/local/bin/uv \ + && ln -s /root/.local/bin/uvx /usr/local/bin/uvx + +# Install Node.js 20.x LTS (compatible with Next.js 15.3.1) +RUN curl -fsSL https://deb.nodesource.com/setup_20.x | bash - \ + && apt-get install -y nodejs \ + && rm -rf /var/lib/apt/lists/* + +# Install TypeScript globally +RUN npm install -g typescript@5.8.3 + +# Verify installations +RUN echo "=== Installation Verification ===" \ + && python --version \ + && pip --version \ + && uv --version \ + && node --version \ + && npm --version \ + && tsc --version diff --git a/__pycache__/main.cpython-39.pyc b/__pycache__/main.cpython-39.pyc deleted file mode 100644 index c7ca419..0000000 Binary files a/__pycache__/main.cpython-39.pyc and /dev/null differ diff --git a/.github/workflows/python-lint.yml b/backend/.github/workflows/python-lint.yml similarity index 100% rename from .github/workflows/python-lint.yml rename to backend/.github/workflows/python-lint.yml diff --git a/,gitignore b/backend/.gitignore similarity index 96% rename from ,gitignore rename to backend/.gitignore index 764d528..4d329e1 100644 --- a/,gitignore +++ b/backend/.gitignore @@ -45,4 +45,5 @@ docker-compose.prod.yml # DB test.db -**/__pycache__/ +uv.lock +.zed diff --git a/backend/.python-version b/backend/.python-version new file mode 100644 index 0000000..37504c5 --- /dev/null +++ b/backend/.python-version @@ -0,0 +1 @@ +3.11 diff --git a/backend/CONTRIBUTING.md b/backend/CONTRIBUTING.md new file mode 100644 index 0000000..cd40e27 --- /dev/null +++ b/backend/CONTRIBUTING.md @@ -0,0 +1,925 @@ +# Contributing to Sepal Greenhouse Backend + +Thank you for your interest in contributing to the Sepal Greenhouse Backend! This document provides guidelines and instructions for contributing to this project. + +## Table of Contents + +- [Code of Conduct](#code-of-conduct) +- [Getting Started](#getting-started) +- [Development Setup](#development-setup) +- [Development Workflow](#development-workflow) +- [Code Standards](#code-standards) +- [Architecture Guidelines](#architecture-guidelines) +- [Testing](#testing) +- [Database Changes](#database-changes) +- [Git Workflow](#git-workflow) +- [Adding New Features](#adding-new-features) +- [Documentation](#documentation) +- [Pull Request Process](#pull-request-process) +- [Troubleshooting](#troubleshooting) + +--- + +## Code of Conduct + +This project follows a code of conduct. Please be respectful, inclusive, and professional in all interactions. + +--- + +## Getting Started + +### Prerequisites + +Before you begin, ensure you have the following installed: + +- **Python 3.11+**: [Download Python](https://www.python.org/downloads/) +- **uv**: Ultra-fast Python package installer - [Install uv](https://github.com/astral-sh/uv) (optional for local dev) +- **Docker & Docker Compose**: [Install Docker](https://docs.docker.com/get-docker/) +- **Git**: [Install Git](https://git-scm.com/downloads/) +- **Code Editor**: VS Code, PyCharm, or your preferred IDE + +### First-Time Contributors + +1. **Familiarize yourself with the project**: + - Read the [README.md](README.md) + - Review the [API documentation](http://localhost:8000/docs) (after running the app) + - Check the [project documentation](/docs) + +2. **Set up your development environment** (see below) + +3. **Find an issue to work on**: + - Look for issues tagged with `good first issue` + - Ask questions if anything is unclear + +--- + +## Development Setup + +### 1. Fork and Clone + +```bash +# Fork the repository on GitHub, then clone your fork +git clone https://github.com/YOUR_USERNAME/sepal-greenhouse-internal.git +cd sepal-greenhouse-internal/backend +``` + +### 2. Set Up Environment + +```bash +# Copy environment file +cp .env.example .env + +# Edit .env if needed (usually defaults are fine for development) +``` + +### 3. Start with Docker (Recommended) + +```bash +# Build and start containers +docker-compose up --build + +# The API will be available at http://localhost:8000 +# API docs at http://localhost:8000/docs +``` + +### 4. Seed the Database + +```bash +# In a new terminal +docker-compose exec api python infrastructure/db/seed_data.py +``` + +### 5. Verify Setup + +```bash +# Test health endpoint +curl http://localhost:8000/health + +# Should return: {"status": "healthy", "version": "2.0"} +``` + +### Alternative: Local Development (Without Docker) + +```bash +# Install uv (if not already installed) +curl -LsSf https://astral.sh/uv/install.sh | sh + +# Install dependencies with uv +uv pip install -e . + +# Initialize database +python -c "from infrastructure.db.database import Base, engine; Base.metadata.create_all(bind=engine)" + +# Seed database +python infrastructure/db/seed_data.py + +# Run the server +uvicorn main:app --reload --host 0.0.0.0 --port 8000 +``` + +--- + +## Development Workflow + +### Branch Naming Conventions + +Use descriptive branch names following this pattern: + +- `feature/add-candidate-api` - New features +- `fix/job-creation-bug` - Bug fixes +- `refactor/improve-error-handling` - Code refactoring +- `docs/update-contributing` - Documentation updates +- `test/add-service-tests` - Test additions + +### Making Changes + +1. **Create a new branch**: + ```bash + git checkout -b feature/your-feature-name + ``` + +2. **Make your changes** with hot-reload enabled: + - Docker automatically reloads on file changes + - Check logs: `docker-compose logs -f api` + +3. **Test your changes**: + ```bash + # Run all tests + docker-compose exec api pytest tests/ -v + + # Run specific test file + docker-compose exec api pytest tests/test_api/test_jobs.py -v + + # Run with coverage + docker-compose exec api pytest tests/ --cov=infrastructure --cov=schemas + ``` + +4. **Check code quality**: + ```bash + # Format code (if using ruff) + docker-compose exec api ruff format . + + # Lint code + docker-compose exec api ruff check . + ``` + +--- + +## Code Standards + +### Python Style Guide + +We follow [PEP 8](https://pep8.org/) with these specific guidelines: + +#### Naming Conventions + +- **Files**: `snake_case.py` (e.g., `job_service.py`) +- **Classes**: `PascalCase` (e.g., `JobService`, `JobCreate`) +- **Functions/Methods**: `snake_case` (e.g., `create_job`, `get_all_jobs`) +- **Variables**: `snake_case` (e.g., `job_data`, `total_count`) +- **Constants**: `UPPER_SNAKE_CASE` (e.g., `MAX_PAGE_SIZE`) +- **Private members**: `_leading_underscore` (e.g., `_internal_method`) + +#### Import Ordering + +```python +# Standard library imports +import os +from datetime import datetime + +# Third-party imports +from fastapi import APIRouter, Depends, HTTPException +from sqlalchemy.orm import Session +from pydantic import BaseModel + +# Local application imports +from infrastructure.db.database import get_db +from infrastructure.db.models import Job +from infrastructure.db.repository_jobs import JobService +from schemas.job_schemas import JobCreate +``` + +#### Type Hints + +Always use type hints for function parameters and return values: + +```python +def create_job(db: Session, job_data: JobCreate) -> Job: + """ + Create a new job. + + Args: + db: Database session + job_data: Job creation data + + Returns: + Created job instance + + Raises: + HTTPException: If validation fails + """ + # Implementation + pass +``` + +#### Docstrings + +Use Google-style docstrings: + +```python +def process_data(data: dict, validate: bool = True) -> dict: + """ + Process incoming data with optional validation. + + Args: + data: Dictionary containing raw data + validate: Whether to validate data (default: True) + + Returns: + Dictionary with processed data + + Raises: + ValueError: If data is invalid when validate=True + + Example: + >>> result = process_data({"name": "test"}) + >>> print(result) + {'name': 'test', 'processed': True} + """ + pass +``` + +#### Code Formatting + +- **Line length**: Max 100 characters (soft limit), 120 (hard limit) +- **Indentation**: 4 spaces (no tabs) +- **Blank lines**: 2 before class/function definitions, 1 between methods +- **String quotes**: Use double quotes `"` for strings, single `'` for internal strings + +--- + +## Architecture Guidelines + +### Domain-Driven Design (DDD) + +The project follows Domain-Driven Design architecture: + +``` +backend/ +├── main.py # Application entry point +├── infrastructure/ # Infrastructure layer +│ ├── db/ # Database infrastructure +│ │ ├── database.py # DB connection & session +│ │ ├── models.py # All ORM models (8 models) +│ │ └── repository.py # JobService (business logic) +│ └── api/ # API controllers +│ └── jobs_controller.py # HTTP endpoints +└── schemas/ # Pydantic validation schemas + └── job_schemas.py # Request/response schemas +``` + +### When to Create New Components + +#### New Model (infrastructure/db/models.py) +When you need to: +- Store data in a new database table +- Define relationships with existing models +- **Note**: All models are consolidated in a single file + +#### New Schema (schemas/) +When you need to: +- Validate request data +- Format response data +- Define different views of the same model (Create, Update, Response) + +#### New Service (infrastructure/db/repository.py) +When you need to: +- Implement business logic +- Orchestrate multiple model operations +- Add complex validation +- **Note**: Currently using JobService pattern + +#### New Controller (infrastructure/api/) +When you need to: +- Expose a new API endpoint +- Handle HTTP-specific concerns + +### Error Handling Pattern + +Always use consistent error responses: + +```python +from fastapi import HTTPException, status + +# Not found errors +raise HTTPException( + status_code=status.HTTP_404_NOT_FOUND, + detail={ + "error": { + "code": "RESOURCE_NOT_FOUND", + "message": "Resource with ID {id} not found", + "field": None, + } + }, +) + +# Validation errors +raise HTTPException( + status_code=status.HTTP_422_UNPROCESSABLE_ENTITY, + detail={ + "error": { + "code": "VALIDATION_ERROR", + "message": "Invalid input data", + "field": "field_name", + } + }, +) +``` + +### Response Format Pattern + +All successful responses use this format: + +```python +from datetime import datetime + +def create_response(data): + """Create standardized API response.""" + return { + "data": data, + "metadata": { + "timestamp": datetime.utcnow().isoformat() + "Z", + "version": "2.0" + } + } + +return create_response(data) +# Returns: {"data": {...}, "metadata": {"timestamp": "...", "version": "2.0"}} +``` + +--- + +## Testing + +### Testing Principles + +- **Write tests for all new features** +- **Maintain >80% code coverage** +- **Test both success and failure paths** +- **Use descriptive test names** + +### Test Structure + +```python +def test_create_job_success(client, sample_department): + """Test creating a job with valid data.""" + # Arrange + job_data = { + "name": "Software Engineer", + "status": "open", + "department_id": sample_department.id + } + + # Act + response = client.post("/api/jobs", json=job_data) + + # Assert + assert response.status_code == 201 + data = response.json()["data"] + assert data["name"] == "Software Engineer" + assert "id" in data +``` + +### Test Categories + +1. **Unit Tests** (`tests/test_services/`): + - Test service layer logic in isolation + - Mock database calls if needed + +2. **API Tests** (`tests/test_api/`): + - Test HTTP endpoints + - Verify request/response format + - Test error handling + +3. **Integration Tests**: + - Test complete workflows + - Verify database operations + +### Running Tests + +```bash +# All tests +docker-compose exec api pytest tests/ -v + +# Specific file +docker-compose exec api pytest tests/test_api/test_jobs.py -v + +# Specific test +docker-compose exec api pytest tests/test_api/test_jobs.py::test_create_job_success -v + +# With coverage +docker-compose exec api pytest tests/ --cov=infrastructure --cov=schemas --cov-report=html + +# Coverage report will be in htmlcov/index.html +``` + +### Test Fixtures + +Use fixtures defined in `conftest.py`: + +```python +def test_example(client, db_session, sample_department, sample_office): + """ + Available fixtures: + - client: TestClient for API calls + - db_session: Database session (auto-cleaned) + - sample_department: Pre-created department + - sample_office: Pre-created office + - sample_offices: List of 3 offices + - sample_source: Pre-created source + """ + pass +``` + +--- + +## Database Changes + +### Creating Migrations + +When you modify models, create a migration: + +```bash +# Initialize Alembic (if not done) +docker-compose exec api alembic init alembic + +# Create a migration +docker-compose exec api alembic revision --autogenerate -m "Add candidate table" + +# Review the migration file in alembic/versions/ + +# Apply migration +docker-compose exec api alembic upgrade head + +# Rollback if needed +docker-compose exec api alembic downgrade -1 +``` + +### Schema Design Principles + +- Use **UUIDs** for primary keys (string type) +- Include **created_at** and **updated_at** timestamps +- Define **foreign keys** with proper constraints +- Add **indexes** on frequently queried fields +- Use **nullable=False** for required fields +- Define **cascade deletes** where appropriate + +Example model (add to infrastructure/db/models.py): + +```python +from infrastructure.db.database import Base +from sqlalchemy import Column, String, ForeignKey, Boolean, DateTime +from sqlalchemy.orm import relationship +from sqlalchemy.sql import func +import uuid + +class Example(Base): + """Example entity.""" + + __tablename__ = "examples" + + # Primary key and timestamps (following BaseModel pattern) + id = Column(String, primary_key=True, default=lambda: str(uuid.uuid4())) + created_at = Column(DateTime(timezone=True), server_default=func.now(), nullable=False) + updated_at = Column(DateTime(timezone=True), server_default=func.now(), onupdate=func.now()) + + # Fields + name = Column(String, nullable=False, index=True) + status = Column(String, nullable=False, default="active") + is_active = Column(Boolean, default=True) + + # Foreign keys + parent_id = Column(String, ForeignKey("parents.id"), nullable=True) + + # Relationships + parent = relationship("Parent", back_populates="examples") + + def __repr__(self): + return f"" +``` + +### Seeding Data + +Update `infrastructure/db/seed_data.py` if you add reference data: + +```python +def seed_new_entity(db: Session) -> None: + """Seed new entity data.""" + entities = [ + {"name": "Entity 1"}, + {"name": "Entity 2"}, + ] + + for entity_data in entities: + existing = db.query(Entity).filter(Entity.name == entity_data["name"]).first() + if not existing: + entity = Entity(**entity_data) + db.add(entity) + logger.info(f"Created entity: {entity_data['name']}") + + db.commit() +``` + +--- + +## Git Workflow + +### Commit Messages + +Follow the [Conventional Commits](https://www.conventionalcommits.org/) specification: + +``` +(): + + + +