|
| 1 | +<!-- Last reviewed: 2026-03 --> |
| 2 | + |
| 3 | +## Project |
| 4 | + |
| 5 | +Dataclass-based entity models for the Synapse REST API. Each model represents a Synapse resource (Project, File, Folder, Table, etc.) with async-first methods and auto-generated sync wrappers. |
| 6 | + |
| 7 | +## Conventions |
| 8 | + |
| 9 | +### New model checklist |
| 10 | +1. Decorate with `@dataclass()` then `@async_to_sync` (order matters — `@async_to_sync` must be outer) |
| 11 | +2. Inherit from: `SynchronousProtocol`, then mixins (`AccessControllable`, `StorableContainer`, etc.) |
| 12 | +3. Create a matching protocol file in `protocols/` with sync method signatures |
| 13 | +4. Register concrete type in `core/constants/concrete_types.py` |
| 14 | +5. Add to `models/__init__.py` exports and `__all__` |
| 15 | +6. Add to entity factory type map in `api/entity_factory.py` if it's an entity type |
| 16 | + |
| 17 | +### Standard fields every entity model must have |
| 18 | +```python |
| 19 | +id: Optional[str] = None |
| 20 | +name: Optional[str] = None |
| 21 | +etag: Optional[str] = None |
| 22 | +created_on: Optional[str] = field(default=None, compare=False) |
| 23 | +modified_on: Optional[str] = field(default=None, compare=False) |
| 24 | +created_by: Optional[str] = field(default=None, compare=False) |
| 25 | +modified_by: Optional[str] = field(default=None, compare=False) |
| 26 | +create_or_update: bool = field(default=True, repr=False) |
| 27 | +_last_persistent_instance: Optional["Self"] = field(default=None, repr=False, compare=False) |
| 28 | +``` |
| 29 | + |
| 30 | +Use `compare=False` for read-only timestamps, child collections, annotations, and internal state — this makes `has_changed` compare only user-modifiable fields. |
| 31 | + |
| 32 | +### fill_from_dict() pattern |
| 33 | +Maps camelCase REST keys to snake_case fields via `.get("camelCaseKey", None)`. Must return `self`. Handle annotations separately with `set_annotations` parameter. Reference: `folder.py`, `file.py`. |
| 34 | + |
| 35 | +### _last_persistent_instance lifecycle |
| 36 | +- Set via `_set_last_persistent_instance()` after every successful `store_async()` and `get_async()` |
| 37 | +- Uses `dataclasses.replace(self)` with `deepcopy` for annotations |
| 38 | +- Enables `has_changed` property — skips redundant API calls when nothing changed |
| 39 | +- Drives `create_or_update` logic: if no `_last_persistent_instance`, attempts merge with existing Synapse entity |
| 40 | + |
| 41 | +### @otel_trace_method on every async method |
| 42 | +Apply to all async methods that call Synapse. Format: `f"{ClassName}_{Operation}: ID: {self.id}, Name: {self.name}"`. |
| 43 | + |
| 44 | +### delete_none_keys() before API calls |
| 45 | +Always call `delete_none_keys()` on request dicts before passing to `store_entity()` — the Synapse API rejects `None` values. |
| 46 | + |
| 47 | +### EnumCoercionMixin for enum fields |
| 48 | +If a model has enum-typed fields, inherit from `EnumCoercionMixin` and declare `_ENUM_FIELDS: ClassVar[Dict[str, type]]` mapping field names to enum classes. Auto-coerces strings to enums on assignment via `__setattr__`. |
| 49 | + |
| 50 | +## Constraints |
| 51 | + |
| 52 | +- Never manually write sync methods on models — `@async_to_sync` generates them. Use `@skip_async_to_sync` to exclude specific methods. |
| 53 | +- Protocol files must exactly match the async method signatures (minus `_async` suffix) — they exist for IDE type hints, not runtime dispatch. |
| 54 | +- Child collections (files, folders, tables) must use `compare=False` to avoid breaking `has_changed`. |
0 commit comments