elric-cli 1.0.0__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (48) hide show
  1. elric_cli-1.0.0/.github/workflows/publish-pypi.yml +36 -0
  2. elric_cli-1.0.0/.gitignore +13 -0
  3. elric_cli-1.0.0/.python-version +1 -0
  4. elric_cli-1.0.0/.windsurf/rules/00-elric-boost.md +97 -0
  5. elric_cli-1.0.0/.windsurf/rules/01-python-fastapi.md +119 -0
  6. elric_cli-1.0.0/.windsurf/rules/02-agents-langchain.md +121 -0
  7. elric_cli-1.0.0/.windsurf/rules/03-database-sqlmodel.md +85 -0
  8. elric_cli-1.0.0/.windsurf/rules/04-docker-uv.md +112 -0
  9. elric_cli-1.0.0/.windsurf/rules/05-elric-conventions.md +118 -0
  10. elric_cli-1.0.0/LICENSE +21 -0
  11. elric_cli-1.0.0/PKG-INFO +98 -0
  12. elric_cli-1.0.0/README.md +69 -0
  13. elric_cli-1.0.0/elric_cli/__init__.py +1 -0
  14. elric_cli-1.0.0/elric_cli/app.py +24 -0
  15. elric_cli-1.0.0/elric_cli/commands/.gitkeep +0 -0
  16. elric_cli-1.0.0/elric_cli/commands/__init__.py +0 -0
  17. elric_cli-1.0.0/elric_cli/commands/apikey.py +134 -0
  18. elric_cli-1.0.0/elric_cli/commands/make.py +266 -0
  19. elric_cli-1.0.0/elric_cli/commands/migrate.py +69 -0
  20. elric_cli-1.0.0/elric_cli/commands/project.py +72 -0
  21. elric_cli-1.0.0/elric_cli/commands/route.py +49 -0
  22. elric_cli-1.0.0/elric_cli/commands/serve.py +33 -0
  23. elric_cli-1.0.0/elric_cli/install.sh +92 -0
  24. elric_cli-1.0.0/elric_cli/stubs/.gitkeep +0 -0
  25. elric_cli-1.0.0/elric_cli/stubs/agent.stub.py +23 -0
  26. elric_cli-1.0.0/elric_cli/stubs/agent_chat.stub.py +62 -0
  27. elric_cli-1.0.0/elric_cli/stubs/agent_planner.stub.py +82 -0
  28. elric_cli-1.0.0/elric_cli/stubs/agent_react.stub.py +83 -0
  29. elric_cli-1.0.0/elric_cli/stubs/agent_simple.stub.py +32 -0
  30. elric_cli-1.0.0/elric_cli/stubs/agent_streaming.stub.py +59 -0
  31. elric_cli-1.0.0/elric_cli/stubs/agent_tool.stub.py +63 -0
  32. elric_cli-1.0.0/elric_cli/stubs/chain.stub.py +23 -0
  33. elric_cli-1.0.0/elric_cli/stubs/controller.stub.py +30 -0
  34. elric_cli-1.0.0/elric_cli/stubs/event.stub.py +18 -0
  35. elric_cli-1.0.0/elric_cli/stubs/exception.stub.py +21 -0
  36. elric_cli-1.0.0/elric_cli/stubs/job.stub.py +24 -0
  37. elric_cli-1.0.0/elric_cli/stubs/listener.stub.py +23 -0
  38. elric_cli-1.0.0/elric_cli/stubs/middleware.stub.py +32 -0
  39. elric_cli-1.0.0/elric_cli/stubs/migration.stub.py +26 -0
  40. elric_cli-1.0.0/elric_cli/stubs/model.stub.py +18 -0
  41. elric_cli-1.0.0/elric_cli/stubs/route.stub.py +33 -0
  42. elric_cli-1.0.0/elric_cli/stubs/schema.stub.py +32 -0
  43. elric_cli-1.0.0/elric_cli/stubs/test.stub.py +21 -0
  44. elric_cli-1.0.0/elric_cli/stubs/tool.stub.py +24 -0
  45. elric_cli-1.0.0/elric_cli/uninstall.sh +64 -0
  46. elric_cli-1.0.0/elric_cli/utils.py +181 -0
  47. elric_cli-1.0.0/pyproject.toml +62 -0
  48. elric_cli-1.0.0/uv.lock +293 -0
@@ -0,0 +1,36 @@
1
+ name: Publish to PyPI
2
+
3
+ on:
4
+ push:
5
+ tags:
6
+ - "v*"
7
+ workflow_dispatch:
8
+
9
+ jobs:
10
+ publish:
11
+ runs-on: ubuntu-latest
12
+ permissions:
13
+ contents: read
14
+ steps:
15
+ - name: Checkout
16
+ uses: actions/checkout@v4
17
+
18
+ - name: Set up Python
19
+ uses: actions/setup-python@v5
20
+ with:
21
+ python-version: "3.12"
22
+
23
+ - name: Install build tooling
24
+ run: python -m pip install --upgrade pip build twine
25
+
26
+ - name: Build package
27
+ run: python -m build
28
+
29
+ - name: Check package metadata
30
+ run: python -m twine check dist/*
31
+
32
+ - name: Publish to PyPI
33
+ env:
34
+ TWINE_USERNAME: __token__
35
+ TWINE_PASSWORD: ${{ secrets.PYPI_API_TOKEN }}
36
+ run: python -m twine upload dist/*
@@ -0,0 +1,13 @@
1
+ # Python-generated files
2
+ __pycache__/
3
+ *.py[oc]
4
+ build/
5
+ dist/
6
+ wheels/
7
+ *.egg-info
8
+
9
+ # Virtual environments
10
+ .venv
11
+ .env
12
+ #Files
13
+ # lld.md
@@ -0,0 +1 @@
1
+ 3.12
@@ -0,0 +1,97 @@
1
+ ---
2
+ trigger: always_on
3
+ ---
4
+
5
+ # Elric Boost Guidelines
6
+
7
+ The Elric Boost guidelines are specifically curated for this application. Follow them closely to ensure the best experience when building Elric applications.
8
+
9
+ ## Foundational Context
10
+
11
+ This application is an Elric framework application. You are an expert with all of these packages and versions. Ensure you abide by them.
12
+
13
+ - python - 3.12
14
+ - fastapi - >=0.135.1
15
+ - uvicorn - >=0.42.0
16
+ - sqlmodel - >=0.0.37
17
+ - alembic - >=1.13
18
+ - asyncpg - >=0.31.0
19
+ - redis[asyncio] - >=5.0
20
+ - langchain - >=1.2.12
21
+ - langgraph - >=1.1.0
22
+ - langsmith - >=0.7.20
23
+ - pydantic-settings - >=2.0
24
+ - structlog - >=24.0
25
+ - typer - >=0.12
26
+ - jinja2 - >=3.1
27
+ - pytest - >=8.0
28
+ - pytest-asyncio - >=0.26
29
+ - ruff - >=0.15.6
30
+
31
+ ## Skills Activation
32
+
33
+ This project has domain-specific skills available. You MUST activate the relevant skill whenever you work in that domain — don't wait until you're stuck.
34
+
35
+ - `agent-development` — Activates whenever creating or editing files in `app/agents/`, `app/chains/`, or `app/tools/`. Use when building LangGraph graphs, LangChain chains, defining tool functions, or wiring LangSmith tracing.
36
+ - `route-development` — Activates whenever creating or editing files in `app/routes/` or `app/controllers/`. Use when defining FastAPI routers, dependency injection, request schemas, or response models.
37
+ - `database-development` — Activates whenever creating or editing files in `database/models/` or `database/migrations/`. Use when defining SQLModel entities, writing Alembic migrations, or querying via async session.
38
+
39
+ ## Conventions
40
+
41
+ - You must follow all existing code conventions used in this application. When creating or editing a file, check sibling files for the correct structure, approach, and naming.
42
+ - Use descriptive names for variables and functions. For example, `is_rate_limited`, `has_valid_api_key`, not `check()`.
43
+ - Use `snake_case` for all files, folders, variables, and functions. Use `PascalCase` only for class names.
44
+ - Check for existing base classes and utilities to reuse before writing new ones.
45
+
46
+ ## CLI — Always Use the Elric CLI
47
+
48
+ - Use `uv run elric make:` commands to create new files (agents, chains, tools, routes, controllers, schemas, models, migrations, jobs, exceptions, tests).
49
+ - Never create these files manually — always go through the CLI to ensure stubs are applied correctly.
50
+ - Use `uv run elric route:list` to inspect registered routes before adding new ones.
51
+ - Use `uv run elric migrate:status` to check migration state before writing a new migration.
52
+
53
+ ## Verification
54
+
55
+ - Do not create ad-hoc scripts to verify behavior when tests cover that functionality. Pytest tests are more important.
56
+ - Use `uv run pytest tests/ -x -q` to run the test suite.
57
+
58
+ ## Application Structure & Architecture
59
+
60
+ - Stick to the existing directory structure. Do not create new top-level folders without approval.
61
+ - Do not change dependencies in `pyproject.toml` without approval.
62
+ - All configuration must go through `config/settings.py` (Pydantic Settings). Never use `os.environ` directly in application code.
63
+
64
+ ## Replies
65
+
66
+ - Be concise — focus on what matters, not obvious details.
67
+
68
+ ---
69
+
70
+ ## Elric Boost Tools
71
+
72
+ ### CLI Commands
73
+
74
+ - Before running a `uv run elric` command, verify the available subcommands with `uv run elric --help` or `uv run elric make --help` if unsure.
75
+
76
+ ### Database
77
+
78
+ - Use `uv run elric migrate:status` to check current migration state.
79
+ - Use `uv run elric db:seed` to seed the database with initial data.
80
+ - Never use raw SQL. Always use SQLModel + async session.
81
+
82
+ ### Debugging
83
+
84
+ - Use `uv run pytest --tb=short -x` to debug failing tests quickly.
85
+ - Inspect structlog JSON output to trace requests end-to-end via `trace_id`.
86
+ - Use the LangSmith trace UI to debug agent and chain runs when `LANGCHAIN_TRACING_V2=true`.
87
+
88
+ ### Reading Logs
89
+
90
+ - All logs are structured JSON. Filter by `trace_id` to follow a single request through the full stack.
91
+ - In development, set `LOG_JSON=false` for human-readable output.
92
+
93
+ ### Searching Documentation
94
+
95
+ - Always search official documentation before making implementation decisions for FastAPI, LangGraph, LangChain, LangSmith, SQLModel, or Alembic.
96
+ - Use multiple simple topic-based queries. For example: `['langgraph state', 'langgraph nodes edges']`.
97
+ - Do not include package names in queries — search by concept.
@@ -0,0 +1,119 @@
1
+ ---
2
+ trigger: glob
3
+ globs: ["app/**/*.py", "elric_cli/**/*.py", "config/**/*.py"]
4
+ ---
5
+
6
+ # Python & FastAPI
7
+
8
+ ## Foundational Rules
9
+
10
+ - Always use `async def` for all I/O-bound operations (database, Redis, HTTP calls, file I/O).
11
+ - Use `def` only for pure CPU-bound functions with no I/O.
12
+ - Always use explicit return type annotations on every function and method.
13
+ - Always use type hints for all parameters. Prefer Pydantic models over raw `dict` for input/output.
14
+
15
+ ```python
16
+ # CORRECT
17
+ async def get_api_key(key_id: uuid.UUID, session: AsyncSession) -> ApiKeyResponse:
18
+ ...
19
+
20
+ # WRONG
21
+ async def get_api_key(key_id, session):
22
+ ...
23
+ ```
24
+
25
+ ## FastAPI Conventions
26
+
27
+ - Use `APIRouter` for each domain — never register routes directly on the `app` instance.
28
+ - Always use `Depends()` for dependency injection (DB session, API key, settings).
29
+ - Use `HTTPException` for expected, well-defined errors in controllers.
30
+ - For unexpected errors, let them propagate to the `GlobalExceptionHandler` — never use bare `except Exception`.
31
+ - Use the `lifespan` context manager for startup/shutdown. Never use `@app.on_event`.
32
+
33
+ ```python
34
+ # CORRECT — lifespan
35
+ @asynccontextmanager
36
+ async def lifespan(app: FastAPI):
37
+ await init_db()
38
+ await init_redis()
39
+ yield
40
+
41
+ # WRONG
42
+ @app.on_event("startup")
43
+ async def startup():
44
+ ...
45
+ ```
46
+
47
+ ## Request & Response Schemas
48
+
49
+ - Every route must have an explicit Pydantic schema for request body and response.
50
+ - Always generate schemas with: `uv run elric make:schema SchemeName`
51
+ - Keep request schemas separate from response schemas — never reuse the same model for both.
52
+ - Use the RORO pattern (Receive an Object, Return an Object) for all controllers.
53
+
54
+ ```python
55
+ # CORRECT — separate schemas
56
+ class CreateChatRequest(BaseModel):
57
+ message: str
58
+ session_id: uuid.UUID
59
+
60
+ class ChatResponse(BaseModel):
61
+ reply: str
62
+ trace_id: str
63
+ ```
64
+
65
+ ## Error Handling
66
+
67
+ - Never handle exceptions in controllers. Raise `ElricException` or one of its subclasses.
68
+ - The `GlobalExceptionHandler` in `app/exceptions/handler.py` handles everything centrally.
69
+ - To create new exception types: `uv run elric make:exception ExceptionName`
70
+
71
+ ```python
72
+ # CORRECT
73
+ async def run_agent(request: RunAgentRequest) -> AgentResponse:
74
+ result = await agent.run(request.input)
75
+ if not result:
76
+ raise NotFoundException("Agent produced no output")
77
+ return AgentResponse(**result)
78
+
79
+ # WRONG
80
+ async def run_agent(request: RunAgentRequest) -> AgentResponse:
81
+ try:
82
+ result = await agent.run(request.input)
83
+ except Exception as e:
84
+ return {"error": str(e)}
85
+ ```
86
+
87
+ ## Configuration
88
+
89
+ - Never use `os.environ` directly in application code.
90
+ - Always use `get_settings()` from `config/settings.py`.
91
+
92
+ ```python
93
+ # CORRECT
94
+ from config.settings import get_settings
95
+ settings = get_settings()
96
+ redis_url = settings.REDIS_URL
97
+
98
+ # WRONG
99
+ import os
100
+ redis_url = os.environ.get("REDIS_URL")
101
+ ```
102
+
103
+ ## Imports
104
+
105
+ - Required order: stdlib → third-party → internal (`app/`, `config/`, `database/`).
106
+ - Use absolute imports only. Never relative imports (`from ..models import ...`).
107
+
108
+ ## Naming
109
+
110
+ - Files and folders: `snake_case` (e.g. `chat_agent.py`, `api_key.py`)
111
+ - Classes: `PascalCase` (e.g. `ChatAgent`, `ApiKeyMiddleware`)
112
+ - Functions and variables: `snake_case` with auxiliary verbs (e.g. `is_active`, `has_expired`, `get_session`)
113
+ - Constants: `UPPER_SNAKE_CASE`
114
+
115
+ ## Content-Type Checking (FastAPI >=0.135)
116
+
117
+ - FastAPI now enforces strict Content-Type validation by default for JSON endpoints.
118
+ - If a client does not send `Content-Type: application/json`, the request will be rejected with 422.
119
+ - To disable this for a specific route (e.g. for legacy clients): `app = FastAPI(strict_content_type=False)`.
@@ -0,0 +1,121 @@
1
+ ---
2
+ trigger: glob
3
+ globs: ["app/agents/**/*.py", "app/chains/**/*.py", "app/tools/**/*.py"]
4
+ ---
5
+
6
+ # Agents, Chains & Tools — LangGraph / LangChain / LangSmith
7
+
8
+ - IMPORTANT: Activate the `agent-development` skill whenever working in `app/agents/`, `app/chains/`, or `app/tools/`.
9
+ - CRITICAL: Always search official LangGraph and LangChain documentation before implementing new patterns — APIs evolve quickly across major versions.
10
+
11
+ ## Package Versions in Use
12
+
13
+ - `langgraph` >=1.1.0 — stable 1.x API, breaking changes from 0.x
14
+ - `langchain` >=1.2.12 — stable 1.x API, breaking changes from 0.x
15
+ - `langsmith` >=0.7.20
16
+
17
+ ## Agents — LangGraph
18
+
19
+ - Every agent must extend `BaseAgent` from `app/agents/base_agent.py`. Never instantiate `StateGraph` directly in a controller.
20
+ - Always generate the file with: `uv run elric make:agent AgentName`
21
+ - The `build_graph()` method must return an uncompiled `StateGraph`. Compilation happens inside `run()`.
22
+ - Always use `ainvoke()` — never `invoke()` in an async context.
23
+ - Always define state as a `TypedDict` or dataclass — never use a raw `dict` as state.
24
+
25
+ ```python
26
+ # CORRECT — typed state
27
+ from typing import TypedDict
28
+
29
+ class ChatState(TypedDict):
30
+ messages: list[str]
31
+ context: str
32
+ output: str | None
33
+
34
+ class ChatAgent(BaseAgent):
35
+ name = "chat_agent"
36
+
37
+ def build_graph(self) -> StateGraph:
38
+ graph = StateGraph(ChatState)
39
+ graph.add_node("process", self._process)
40
+ graph.add_node("respond", self._respond)
41
+ graph.add_edge("process", "respond")
42
+ graph.set_entry_point("process")
43
+ return graph
44
+
45
+ async def _process(self, state: ChatState) -> ChatState:
46
+ ...
47
+ ```
48
+
49
+ ## Chains — LangChain
50
+
51
+ - Every chain must extend `BaseChain` from `app/chains/base_chain.py`.
52
+ - Always generate with: `uv run elric make:chain ChainName`
53
+ - Use `ainvoke()` — never `invoke()` or the deprecated `run()`.
54
+ - Always define `PromptTemplate` as a class attribute, not inline inside a method.
55
+ - Always use an explicit `output_parser` — never do manual string parsing of the output.
56
+
57
+ ```python
58
+ # CORRECT
59
+ class SummarizeChain(BaseChain):
60
+ name = "summarize_chain"
61
+
62
+ prompt = PromptTemplate.from_template(
63
+ "Summarize the following text in {language}:\n\n{text}"
64
+ )
65
+
66
+ async def run(self, text: str, language: str = "English") -> str:
67
+ chain = self.prompt | self.llm | StrOutputParser()
68
+ return await chain.ainvoke({"text": text, "language": language})
69
+ ```
70
+
71
+ ## Tools — LangChain
72
+
73
+ - Always generate with: `uv run elric make:tool ToolName`
74
+ - Use the `@tool` decorator from LangChain or extend `BaseTool`.
75
+ - Every tool must have a docstring — LangChain uses it as the description passed to the LLM.
76
+ - Use `args_schema` with a Pydantic model to validate tool inputs.
77
+ - Tools must be pure functions — no undeclared side effects.
78
+
79
+ ```python
80
+ # CORRECT
81
+ class SearchInput(BaseModel):
82
+ query: str
83
+ max_results: int = 5
84
+
85
+ @tool(args_schema=SearchInput)
86
+ async def search_documents(query: str, max_results: int = 5) -> list[dict]:
87
+ """Search internal documents by semantic similarity."""
88
+ ...
89
+ ```
90
+
91
+ ## LangSmith Tracing
92
+
93
+ - Tracing is enabled automatically by `app/providers/langsmith.py` when `LANGCHAIN_TRACING_V2=true`.
94
+ - Do not add manual tracing in controllers — `BaseAgent` and `BaseChain` handle it.
95
+ - To attach custom metadata to a run, use `langsmith.trace()` as a context manager.
96
+ - In development, use the LangSmith UI to inspect input/output for every graph node.
97
+ - In production, every run is automatically tagged with the `trace_id` from the HTTP request.
98
+
99
+ ## LangGraph Node Naming
100
+
101
+ - Use descriptive `snake_case` names for all nodes: `validate_input`, `fetch_context`, `generate_response`.
102
+ - Never use generic names like `step1`, `process`, `node`.
103
+ - Conditional edge routers (`add_conditional_edges`) must use a named router function.
104
+
105
+ ```python
106
+ # CORRECT
107
+ graph.add_conditional_edges(
108
+ "validate_input",
109
+ self._route_by_intent, # explicit named router function
110
+ {"chat": "generate_response", "search": "fetch_context"}
111
+ )
112
+
113
+ # WRONG
114
+ graph.add_conditional_edges("process", lambda x: x["type"], {...})
115
+ ```
116
+
117
+ ## LangGraph 1.x Notes
118
+
119
+ - `StateGraph` now supports `interrupt_before` and `interrupt_after` for human-in-the-loop — prefer these over manual checkpointing.
120
+ - Use `MemorySaver` for in-process state persistence during development; replace with a persistent checkpointer in production.
121
+ - `CompiledGraph.stream()` and `CompiledGraph.astream()` are the preferred way to stream node outputs to clients.
@@ -0,0 +1,85 @@
1
+ ---
2
+ trigger: glob
3
+ globs: ["database/**/*.py", "app/providers/database.py"]
4
+ ---
5
+
6
+ # Database — SQLModel, PostgreSQL, Alembic
7
+
8
+ - IMPORTANT: Activate the `database-development` skill whenever working in `database/` or `app/providers/database.py`.
9
+
10
+ ## SQLModel Models
11
+
12
+ - Always generate with: `uv run elric make:model ModelName`
13
+ - Every model has a UUID `id` as primary key, generated automatically.
14
+ - Every model has `created_at` and `updated_at` managed automatically.
15
+ - One file per entity in `database/models/`. Never define multiple models in the same file.
16
+ - Use `Field()` for all fields with constraints (index, unique, foreign key, default).
17
+
18
+ ```python
19
+ # CORRECT — standard model structure
20
+ class Post(SQLModel, table=True):
21
+ __tablename__ = "posts"
22
+
23
+ id: uuid.UUID = Field(default_factory=uuid.uuid4, primary_key=True)
24
+ title: str = Field(index=True)
25
+ content: str
26
+ is_published: bool = Field(default=False)
27
+ author_id: uuid.UUID = Field(foreign_key="users.id")
28
+ created_at: datetime = Field(default_factory=datetime.utcnow)
29
+ updated_at: datetime = Field(default_factory=datetime.utcnow)
30
+
31
+ # Relationships
32
+ author: "User" = Relationship(back_populates="posts")
33
+ ```
34
+
35
+ ## Queries — Async Session
36
+
37
+ - Always use `AsyncSession` — never the synchronous session.
38
+ - Inject the session via `Depends(get_session)` in controllers.
39
+ - Prefer SQLModel's `select()` over raw queries.
40
+ - Prevent N+1 queries with `selectinload()` or `joinedload()` for relationships.
41
+ - Never use raw DB connections — always go through the ORM.
42
+
43
+ ```python
44
+ # CORRECT — async query with eager loading
45
+ async def get_posts_with_authors(session: AsyncSession) -> list[Post]:
46
+ statement = (
47
+ select(Post)
48
+ .where(Post.is_published == True)
49
+ .options(selectinload(Post.author))
50
+ .order_by(Post.created_at.desc())
51
+ )
52
+ result = await session.exec(statement)
53
+ return result.all()
54
+
55
+ # WRONG — N+1
56
+ posts = await session.exec(select(Post)).all()
57
+ for post in posts:
58
+ print(post.author.name) # N separate queries!
59
+ ```
60
+
61
+ ## Migrations — Alembic
62
+
63
+ - Always generate with: `uv run elric make:migration <description_in_snake_case>`
64
+ - Use clear descriptions: `add_is_published_to_posts`, `create_api_keys_table`.
65
+ - Before every migration: run `uv run elric migrate:status` to verify current state.
66
+ - When modifying a column, include **all** previously defined attributes — Alembic does not preserve them automatically.
67
+ - Never modify migration files that have already been run in production. Always create a new migration.
68
+ - Every migration must have both `upgrade()` and `downgrade()` fully implemented.
69
+
70
+ ```python
71
+ # CORRECT — downgrade always implemented
72
+ def upgrade() -> None:
73
+ op.add_column("posts", sa.Column("is_published", sa.Boolean(), nullable=False, server_default="false"))
74
+
75
+ def downgrade() -> None:
76
+ op.drop_column("posts", "is_published")
77
+ ```
78
+
79
+ ## Seeders
80
+
81
+ - Generate with: `uv run elric make:seeder SeederName`
82
+ - Run with: `uv run elric db:seed`
83
+ - Seeders must be idempotent — use `INSERT ... ON CONFLICT DO NOTHING` or a get-or-create pattern.
84
+ - Use seeders for mandatory initial data (e.g. first admin API key), not for test data.
85
+ - For tests, use pytest fixtures defined in `tests/conftest.py`.
@@ -0,0 +1,112 @@
1
+ ---
2
+ trigger: glob
3
+ globs: ["docker/**", "Dockerfile*", "docker-compose*.yml", "pyproject.toml", ".env*"]
4
+ ---
5
+
6
+ # Docker, uv & Environments
7
+
8
+ ## uv — Package Manager
9
+
10
+ - Always use `uv` to manage dependencies. Never run `pip install` directly.
11
+ - To add a dependency: `uv add <package>`
12
+ - To add a dev dependency: `uv add --dev <package>`
13
+ - To run commands in the virtualenv: `uv run <command>`
14
+ - The `uv.lock` file must always be committed — it guarantees reproducible builds.
15
+ - Never edit `pyproject.toml` manually for dependencies — use `uv add` / `uv remove`.
16
+
17
+ ```bash
18
+ # CORRECT
19
+ uv add httpx
20
+ uv run pytest tests/
21
+
22
+ # WRONG
23
+ pip install httpx
24
+ python -m pytest tests/
25
+ ```
26
+
27
+ ## Environment Variables
28
+
29
+ - `.env` is gitignored — never commit it.
30
+ - `.env.example` is the canonical reference — update it every time you add a new variable.
31
+ - All variables go through `config/settings.py` (Pydantic Settings). Never use `os.environ` in application code.
32
+ - Different values per environment: `.env` for local dev → Docker env vars for production.
33
+ - In production, variables are injected via `docker-compose.prod.yml` or the cloud provider's secrets manager.
34
+
35
+ ```python
36
+ # CORRECT — always via Settings
37
+ from config.settings import get_settings
38
+ settings = get_settings()
39
+ db_url = settings.DATABASE_URL
40
+
41
+ # WRONG
42
+ import os
43
+ db_url = os.environ["DATABASE_URL"]
44
+ ```
45
+
46
+ ## Dockerfile — Development
47
+
48
+ - The dev image mounts the code as a volume for hot-reload.
49
+ - Uvicorn runs with `--reload` in development.
50
+ - Do not install production-only dependencies in the dev image.
51
+
52
+ ```dockerfile
53
+ FROM python:3.12-slim
54
+ COPY --from=ghcr.io/astral-sh/uv:latest /uv /usr/local/bin/uv
55
+ WORKDIR /app
56
+ COPY pyproject.toml uv.lock ./
57
+ RUN uv sync --frozen
58
+ COPY . .
59
+ CMD ["uv", "run", "uvicorn", "app:create_app", "--factory", "--reload", "--host", "0.0.0.0"]
60
+ ```
61
+
62
+ ## Dockerfile — Production (multi-stage)
63
+
64
+ - Always use a multi-stage build: `builder` stage + `production` stage.
65
+ - The `builder` stage installs dependencies with `uv sync --frozen --no-dev`.
66
+ - The `production` stage copies only `.venv` from the builder — minimal final image.
67
+ - The app must never run as `root` in production. Always use a non-privileged user.
68
+ - Use Gunicorn with `UvicornWorker` in production.
69
+ - Note: `uvicorn.workers` module is deprecated in uvicorn >=0.42. Use the separate `uvicorn-worker` package instead.
70
+
71
+ ```dockerfile
72
+ # Builder stage
73
+ FROM python:3.12-slim AS builder
74
+ COPY --from=ghcr.io/astral-sh/uv:latest /uv /usr/local/bin/uv
75
+ WORKDIR /app
76
+ COPY pyproject.toml uv.lock ./
77
+ RUN uv sync --frozen --no-dev
78
+
79
+ # Production stage
80
+ FROM python:3.12-slim AS production
81
+ WORKDIR /app
82
+ COPY --from=builder /app/.venv ./.venv
83
+ COPY . .
84
+ ENV PATH="/app/.venv/bin:$PATH"
85
+ RUN adduser --disabled-password --no-create-home appuser
86
+ USER appuser
87
+ # Requires: uv add uvicorn-worker gunicorn
88
+ CMD ["gunicorn", "app:create_app", "--factory", \
89
+ "--worker-class", "uvicorn_worker.UvicornWorker", \
90
+ "--workers", "4", "--bind", "0.0.0.0:8000"]
91
+ ```
92
+
93
+ ## Healthcheck
94
+
95
+ - Every service in `docker-compose.yml` must have a `healthcheck` configured.
96
+ - The app depends on `db` and `redis` with `condition: service_healthy`.
97
+ - The app's `/health` endpoint is used as the healthcheck by the load balancer.
98
+
99
+ ```yaml
100
+ # CORRECT
101
+ healthcheck:
102
+ test: ["CMD-SHELL", "pg_isready -U elric"]
103
+ interval: 5s
104
+ timeout: 3s
105
+ retries: 5
106
+ ```
107
+
108
+ ## If the App Does Not Reflect Changes
109
+
110
+ - In development with a mounted volume: uvicorn `--reload` should reload automatically.
111
+ - If not: `docker compose restart app` or `docker compose up --build`.
112
+ - If dependencies changed: `docker compose up --build` to rebuild the image.