Skip to content
Merged
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
225 changes: 44 additions & 181 deletions AGENTS.md
Original file line number Diff line number Diff line change
@@ -1,14 +1,8 @@
# AGENTS.md - PolyClean Development Guide

This document provides essential information for AI agents working on the PolyClean project.
See [PolyClean.md](./PolyClean.md) for detailed architecture documentation.

## Project Overview

PolyClean combines python-polylith with Clean Architecture principles. The codebase uses a strict directory structure with `components/` (reusable building blocks), `bases/` (entry points), and `test/` (tests mirror component structure).

## Build, Lint, and Test Commands

### Running Tests
## Commands

```bash
# Run all tests
Expand All @@ -20,171 +14,37 @@ uv run pytest test/flows/test_create_post_flow.py
# Run a single test by name
uv run pytest test/flows/test_create_post_flow.py::test_create_post_flow_success -v

# Run architecture tests
uv run pytest test/architecture/ -v

# Run with coverage
uv run pytest --cov=polyclean --cov-report=term-missing
```

### Linting & Type Checking

```bash
# Run all linters via trunk (ruff, black, isort, etc.)
trunk check
# Lint check
trunk check --all
trunk check --fix # auto-fix

# Run trunk with auto-fix
trunk check --fix
```
# Separate type check
uv run ty check

Note: `trunk check` runs ruff, black, isort, and other linters. mypy is disabled in this project.

### Running the Application

```bash
# Start API server with reload
# Run API server
uv run uvicorn polyclean.publishing_api.main:app --reload
```

### Polylith CLI Commands
## Polylith Commands

```bash
uv run poly info
uv run poly deps
uv run poly test diff
```

## Code Style Guidelines

### Import Organization

All Python files must start with:

```python
from __future__ import annotations
```

Imports should be organized in the following order (use `isort` with black profile):

1. Standard library imports
2. Third-party imports
3. Local application imports

```python
# Good import example
from __future__ import annotations

from datetime import datetime, timezone
from typing import List, Optional, Protocol

from polyclean.posts_contract import Post, PostStoragePort
```

### Formatting

- Line length: 100 characters maximum
- Use black for formatting (the isort profile is set to "black")
- No trailing whitespace
- Use f-strings for string formatting

### Type Hints

- Always use type hints for function parameters and return types
- Use `Optional[X]` instead of `X | None` for compatibility
- Use `from __future__ import annotations` to enable postponed evaluation
# Create a new component (brick)
uv run poly create component --name my_feature

```python
# Good
async def get_by_id(self, post_id: int) -> Optional[Post]: ...
# Create a new base
uv run poly create base --name my_api

# Good - using Protocol for interfaces
class PostStoragePort(Protocol):
async def save(self, post: Post) -> Post: ...
async def get_by_id(self, post_id: int) -> Optional[Post]: ...
```

### Naming Conventions

- Classes: PascalCase (e.g., `CreatePostFlow`, `SQLitePostAdapter`)
- Functions/methods: snake_case (e.g., `get_by_id`, `publish_post`)
- Private methods: prefix with underscore (e.g., `_storage`)
- Constants: SCREAMING_SNAKE_CASE
- Files: snake_case (e.g., `test_create_post_flow.py`)

### Dataclasses for Entities

Use `@dataclass` for simple data containers:

```python
from dataclasses import dataclass
from datetime import datetime
from typing import Optional

@dataclass
class Post:
id: Optional[int]
content: str
image_url: str
created_at: datetime
instagram_post_id: Optional[str]
posted: bool = False

def mark_as_posted(self, instagram_id: str) -> None:
self.posted = True
self.instagram_post_id = instagram_id
```

### Protocols for Ports (Interfaces)

Define interfaces using `Protocol` from `typing`:

```python
from typing import Protocol

class InstagramPort(Protocol):
async def publish_post(self, image_url: str, caption: str) -> str: ...
async def validate_connection(self) -> bool: ...
```

### Async/Await

- Use `async`/`await` for I/O-bound operations
- Always mark async test functions with `@pytest.mark.asyncio`
- Use `pytest-asyncio` for async test support

```python
@pytest.mark.asyncio
async def test_create_post_flow_success(fake_post_storage):
flow = CreatePostFlow(fake_post_storage)
result = await flow.flow(content="Hello", image_url="https://example.com/img.jpg")
assert result["success"] is True
```

### Error Handling

- Use exceptions for error conditions
- Return dict with `success` key for flow operations that can fail
- Raise `HTTPException` in FastAPI endpoints for HTTP error responses

```python
# In flows, return error dict
if not content or not image_url:
return {"success": False, "message": "Invalid input"}

# In API endpoints, raise HTTPException
if not result["success"]:
raise HTTPException(status_code=400, detail=result["message"])
```

### Dependency Injection
# View workspace info
uv run poly info

- Pass dependencies through constructor injection
- Use protocols/types for dependency abstraction
# View dependency tree
uv run poly deps

```python
class CreatePostFlow:
def __init__(self, storage: PostStoragePort):
self._storage = storage
# Run tests for changed bricks only
uv run poly test diff
```

## Project Structure
Expand All @@ -196,35 +56,38 @@ polyclean/
│ ├── create_post_flow/
│ ├── publish_post_flow/
│ ├── posts_contract/ # Entities and ports
│ ├── instagram_contract/ # Instagram interface
│ ├── sqlite_post_adapter/ # SQLite implementation
│ ├── instagram_contract/
│ ├── sqlite_post_adapter/
│ ├── instagram_publish_adapter/
│ └── rest_adapter_lib/
├── bases/ # Application entry points
├── bases/ # Entry points
│ └── polyclean/
│ └── publishing_api/
├── test/ # Tests mirroring component structure
│ ├── flows/
│ ├── adapters/
│ ├── contracts/
│ ├── architecture/
│ └── fakes/ # Test doubles
└── projects/ # Additional project configs
└── test/ # Tests mirror component structure
├── flows/
├── adapters/
├── contracts/
├── architecture/
└── fakes/
```

## Architecture Patterns
## Architecture

Clean Architecture layers:

- **Entities** - Domain objects (in `*_contract`)
- **Use Cases** - Business logic (in `*_flow`)
- **Ports** - Interfaces (Protocol classes in `*_contract`)
- **Adapters** - External services (in `*_adapter`)
- **\_lib** - Internal layer utilities (in `*_<layer>_lib`)

This project follows Clean Architecture with these layers:
## Conventions

1. **Entities** - Domain objects (in `*_contract` components)
2. **Use Cases** - Business logic (in `*_flow` components)
3. **Ports** - Interface definitions (Protocol classes in `*_contract` components)
4. **Adapters** - External service implementations (in `*_adapter` components)
- Use `Protocol` for port interfaces
- Flows return `{"success": bool, ...}` dicts
- API endpoints raise `HTTPException` on failure
- Constructor injection for dependencies

## Configuration
## Testing

- Python version: 3.12+
- Dependencies managed with `uv`
- Linting: trunk check (runs ruff, black, isort, etc.)
- Type checking: disabled in this project
- Testing: pytest with pytest-asyncio
- Use fakes from `test/fakes/` for test doubles