Skip to content

Commit 5dd5ce5

Browse files
chore: add Cursor rules for code style, architecture, typing, testing, tooling, and adapters
1 parent d5a7dd6 commit 5dd5ce5

9 files changed

Lines changed: 480 additions & 389 deletions
Lines changed: 83 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,83 @@
1+
---
2+
description: ArchiPy adapter layer conventions — ports, mocks, session managers, lazy imports
3+
globs: archipy/adapters/**/*.py
4+
alwaysApply: false
5+
---
6+
7+
# Adapter Conventions
8+
9+
## Required Files per Adapter
10+
11+
Every adapter package must contain at minimum:
12+
13+
```
14+
archipy/adapters/<service>/
15+
├── __init__.py
16+
├── ports.py # Abstract interface (ABC)
17+
└── adapters.py # Concrete implementation
18+
```
19+
20+
A `mocks.py` is **optional** — only add it when the adapter is used directly in BDD tests and an in-memory test double is needed (e.g., Redis, Kafka). Do not create mocks for adapters that are tested via real instances or testcontainers.
21+
22+
## Ports (Abstract Interfaces)
23+
24+
`ports.py` defines the contract using `abc.ABC`. Implementations (and mocks, if they exist) must both satisfy this interface.
25+
26+
```python
27+
# ✅ GOOD
28+
from abc import ABC, abstractmethod
29+
30+
class CachePort(ABC):
31+
@abstractmethod
32+
def get(self, key: str) -> str | None: ...
33+
34+
@abstractmethod
35+
def set(self, key: str, value: str, ttl: int | None = None) -> None: ...
36+
```
37+
38+
- `ANN401` (Any) is allowed in ports for `**kwargs` parameters.
39+
- `ARG002` (unused arguments) is allowed — interface stubs may not use all params.
40+
41+
## Mocks (Test Doubles) — Optional
42+
43+
Add `mocks.py` only when an in-memory test double is genuinely needed for BDD tests. When present, mocks are exempt from:
44+
- `ARG001`, `ARG002`, `ARG004`, `ARG005` — unused arguments common in mock signatures
45+
- `ANN401` — Any types for compatibility
46+
- `PLR0913` — mock constructors may accept many params
47+
48+
For adapters tested via real service instances (e.g., using `testcontainers`), skip `mocks.py` entirely.
49+
50+
## Session Managers
51+
52+
Database adapters use `session_manager_registry.py` for managing SQLAlchemy sessions. These files use lazy imports (`PLC0415`) to break circular import chains — this is intentional and expected.
53+
54+
```python
55+
# ✅ GOOD — lazy import in session registry
56+
def get_session_manager() -> SessionManager:
57+
from archipy.adapters.postgres.sqlalchemy.adapters import PostgresAdapter # noqa: PLC0415
58+
...
59+
```
60+
61+
## Async vs Sync Adapters
62+
63+
- Async variants are declared as separate `optional-dependencies` extras (e.g., `sqlalchemy-async`, `starrocks-async`).
64+
- Async adapters use `asyncio` and `sqlalchemy[asyncio]`.
65+
- Sync and async implementations are separate classes — do not mix `async def` into sync adapter classes.
66+
67+
## Exception Handling at Boundaries
68+
69+
Adapters at the infrastructure boundary (Kafka, Redis, ScyllaDB) may use broad `except Exception` (`BLE001`) only at the outermost boundary where converting to domain errors:
70+
71+
```python
72+
# ✅ GOOD — broad catch only at adapter boundary, then re-raise as domain error
73+
try:
74+
await self._client.produce(topic, message)
75+
except Exception as e: # noqa: BLE001
76+
raise KafkaProduceError("Failed to produce message") from e
77+
```
78+
79+
## Optional Dependency Imports
80+
81+
Adapters rely on their optional extra being installed — do **not** use lazy imports to guard third-party dependencies inside adapter classes. If the extra is not installed, the import at module level will fail with a clear `ImportError`, which is the expected behavior.
82+
83+
Lazy imports (`PLC0415`) are only permitted in `session_manager_registry.py` files to break circular import chains, and in `archipy/helpers/` for optional utility dependencies.
Lines changed: 68 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,68 @@
1+
---
2+
description: ArchiPy clean architecture layer rules — where code lives and how layers interact
3+
alwaysApply: true
4+
---
5+
6+
# Architecture Patterns
7+
8+
ArchiPy follows **Clean Architecture** with four strictly separated layers.
9+
10+
## Layer Map
11+
12+
```
13+
archipy/
14+
├── models/ # Domain layer — data structures only
15+
│ ├── entities/ # Domain model objects (SQLAlchemy/Pydantic)
16+
│ ├── dtos/ # Data Transfer Objects for API input/output
17+
│ ├── errors/ # Custom exception classes
18+
│ └── types/ # Enumerations and type definitions
19+
├── adapters/ # Infrastructure layer — external integrations
20+
├── helpers/ # Support layer — pure utilities and cross-cutting concerns
21+
└── configs/ # Configuration layer — environment-based settings
22+
```
23+
24+
## Layer Rules
25+
26+
### `models/` — Domain Layer
27+
- Contains **data structures only** — no business logic, no I/O.
28+
- Entities use SQLAlchemy or Pydantic; DTOs use Pydantic `BaseModel`.
29+
- Errors must subclass the project's `BaseError`.
30+
31+
### `adapters/` — Infrastructure Layer
32+
- Follows **Ports & Adapters** pattern.
33+
- Every adapter directory must have a `ports.py` (abstract interface) and a `mocks.py` (test double).
34+
- Adapters may import from `models/` and `configs/` — never from `helpers/decorators` at module level (use lazy imports to avoid circular imports).
35+
36+
### `helpers/` — Support Layer
37+
- Pure utilities: no direct external I/O, no database calls.
38+
- Sub-packages: `utils/`, `decorators/`, `interceptors/`, `metaclasses/`.
39+
- Decorators must not know about specific adapter implementations.
40+
41+
### `configs/` — Configuration Layer
42+
- All config classes must extend `pydantic_settings.BaseSettings`.
43+
- Configuration loaded from environment variables or `.env` files.
44+
- No hardcoded secrets; use `.env.example` to document required variables.
45+
46+
## Import Direction (one-way only)
47+
48+
```
49+
configs ← models ← helpers ← adapters
50+
```
51+
52+
- `adapters` may import from `models`, `configs`, `helpers`.
53+
- `helpers` may import from `models`, `configs`.
54+
- `models` may import from `configs` only.
55+
- **Never** import upward (e.g., `models` importing from `adapters`).
56+
57+
## Lazy Imports for Optional Dependencies
58+
59+
Lazy imports are only permitted in `archipy/helpers/` (e.g., `helpers/utils/`, `helpers/decorators/`, `helpers/interceptors/`). Use them inside functions/methods — never at module level — to avoid `ImportError` when an optional extra is not installed:
60+
61+
```python
62+
# ✅ GOOD — lazy import inside a helper utility function
63+
def encode_jwt(payload: dict) -> str:
64+
import jwt # noqa: PLC0415
65+
return jwt.encode(payload, ...)
66+
```
67+
68+
Adapters **must not** use lazy imports to guard optional dependencies; instead, each adapter lives under its own optional extra and is only imported when that extra is installed.

0 commit comments

Comments
 (0)