Skip to content

Commit eb74b2b

Browse files
BryanFaubleclaude
andcommitted
Add module-level CLAUDE.md files for models, api, core, and tests
Each file documents non-obvious patterns specific to that module: - models/: new model checklist, fill_from_dict pattern, _last_persistent_instance lifecycle, EnumCoercionMixin usage, standard field requirements - api/: function signature conventions, REST call patterns, pagination helpers, new service file checklist - core/: async_to_sync internals, retry strategies, credentials chain, upload/download resilience, concrete types registration - tests/: async-only test convention, unit test socket blocking, integration test cleanup with schedule_for_cleanup(), fixture scoping Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
1 parent 532295e commit eb74b2b

4 files changed

Lines changed: 170 additions & 0 deletions

File tree

synapseclient/api/CLAUDE.md

Lines changed: 45 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,45 @@
1+
<!-- Last reviewed: 2026-03 -->
2+
3+
## Project
4+
5+
REST API service layer — thin async functions that map to Synapse REST endpoints. One file per resource type. Called by model layer, never by end users directly.
6+
7+
## Conventions
8+
9+
### Function signature pattern
10+
```python
11+
async def verb_resource(
12+
required_param: str,
13+
optional_param: str = None,
14+
*,
15+
synapse_client: Optional["Synapse"] = None,
16+
) -> Dict[str, Any]:
17+
```
18+
- All functions are `async def`
19+
- `synapse_client` is always the last parameter, keyword-only (after `*`)
20+
- Use `Synapse.get_client(synapse_client=synapse_client)` to get the client instance
21+
- Use `TYPE_CHECKING` guard for `Synapse` import to avoid circular dependencies
22+
23+
### REST call pattern
24+
```python
25+
client = Synapse.get_client(synapse_client=synapse_client)
26+
return await client.rest_post_async(uri="/endpoint", body=json.dumps(request))
27+
```
28+
Available methods: `rest_get_async`, `rest_post_async`, `rest_put_async`, `rest_delete_async`. Pass `endpoint=client.fileHandleEndpoint` for file handle operations; omit for the default repository endpoint.
29+
30+
### Return values
31+
- Most functions return raw `Dict[str, Any]` — transformation happens in the model layer via `fill_from_dict()`
32+
- Some return dataclass instances (e.g., `EntityHeader`) when the data is only used internally
33+
- Delete operations return `None`
34+
35+
### Pagination
36+
Use helpers from `api_client.py`:
37+
- `rest_get_paginated_async()` — for GET endpoints with limit/offset. Expects `results` or `children` key.
38+
- `rest_post_paginated_async()` — for POST endpoints with `nextPageToken`. Expects `page` array.
39+
Both are async generators yielding individual items.
40+
41+
### Adding a new service file
42+
1. Create `synapseclient/api/new_service.py`
43+
2. Import and add all public functions to `api/__init__.py` and its `__all__`
44+
3. Use `json.dumps()` for request bodies (not dict)
45+
4. Reference `entity_services.py` for CRUD pattern, `table_services.py` for pagination pattern

synapseclient/core/CLAUDE.md

Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,38 @@
1+
<!-- Last reviewed: 2026-03 -->
2+
3+
## Project
4+
5+
Infrastructure layer — authentication, file transfer, retry logic, caching, OpenTelemetry tracing, and the `async_to_sync` decorator that powers the dual sync/async API.
6+
7+
## Conventions
8+
9+
### async_to_sync decorator (`async_utils.py`)
10+
- Scans class for `*_async` methods and creates sync wrappers stripping the suffix
11+
- Uses `ClassOrInstance` descriptor — methods work on both class and instance
12+
- Detects running event loop: uses `nest_asyncio.apply()` for nested loops, raises on Python 3.14+
13+
- `wrap_async_to_sync()` for standalone functions (not class methods)
14+
- `wrap_async_generator_to_sync_generator()` for async generators — must `aclose()` in finally block
15+
16+
### Retry patterns (`retry.py`)
17+
- `with_retry()` — simple exponential backoff, fixed retry count (default 3)
18+
- `with_retry_time_based_async()` — time-bounded (default 20 min), exponential backoff with jitter
19+
- Default retryable status codes: `[429, 500, 502, 503, 504]`
20+
- `NON_RETRYABLE_ERRORS` list overrides status code retry (e.g., "is not a table or view")
21+
- 429 throttling: wait bumps to 16 seconds minimum
22+
23+
### Credentials chain (`credentials/`)
24+
Provider chain tries in order: login args → config file → env var (`SYNAPSE_AUTH_TOKEN`) → AWS SSM. Credentials implement `requests.auth.AuthBase`, adding `Authorization: Bearer` header. Profile selection via `SYNAPSE_PROFILE` env var or `--profile` arg.
25+
26+
### Upload/download
27+
- Both use 60-retry params spanning ~30 minutes for resilience
28+
- Upload determines storage location from project settings, supports S3/SFTP/GCP
29+
- Download validates MD5 post-transfer, raises `SynapseMd5MismatchError` on mismatch
30+
- Progress via `tqdm`; multi-threaded uploads suppress per-file messages via `cumulative_transfer_progress`
31+
32+
### concrete_types.py
33+
Maps Java class names from Synapse REST API for polymorphic deserialization. When adding a new entity type, add its concrete type string here AND in `api/entity_factory.py` type map.
34+
35+
## Constraints
36+
37+
- Bearer tokens must never appear in logs — use `BEARER_TOKEN_PATTERN` regex for redaction.
38+
- `delete_none_keys()` must be called on all dicts before sending to the API — Synapse rejects null values.

synapseclient/models/CLAUDE.md

Lines changed: 54 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,54 @@
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`.

tests/CLAUDE.md

Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,33 @@
1+
<!-- Last reviewed: 2026-03 -->
2+
3+
## Project
4+
5+
Test suite for the Synapse Python Client. Unit tests run without network access; integration tests hit the live Synapse API.
6+
7+
## Conventions
8+
9+
### Write async tests only
10+
Do not create synchronous test files. The `@async_to_sync` decorator is validated by a dedicated smoke test. Duplicate sync tests were removed to cut CI cost and maintenance burden.
11+
12+
### Unit tests (`tests/unit/`)
13+
- `pytest-socket` blocks all network calls (unix sockets allowed on non-Windows for async event loop)
14+
- Session-scoped `syn` fixture: `Synapse(skip_checks=True, cache_client=False)` with silent logger
15+
- Autouse `set_timezone` fixture forces `TZ=UTC` for deterministic timestamps
16+
- Client caching disabled via `Synapse.allow_client_caching(False)`
17+
- Use `AsyncMock` for async method mocking, `create_autospec` for type-safe mocks
18+
- Class-based test organization with `@pytest.fixture(scope="function", autouse=True)` for setup
19+
20+
### Integration tests (`tests/integration/`)
21+
- All async tests share one event loop: `asyncio_default_fixture_loop_scope = session`
22+
- `schedule_for_cleanup(item)` — defer entity/file cleanup to session teardown. Always use this instead of inline deletion.
23+
- Per-worker project fixtures (`project_model`, `project`) created during session setup
24+
- `--reruns 3` for flaky retry, `-n 8 --dist loadscope` for parallelism
25+
- OpenTelemetry tracing opt-in via `SYNAPSE_INTEGRATION_TEST_OTEL_ENABLED` env var
26+
27+
### No `@pytest.mark.asyncio` needed
28+
`asyncio_mode = auto` in pytest.ini — all async test functions are auto-detected.
29+
30+
## Constraints
31+
32+
- Unit tests must never make network calls — `pytest-socket` will fail them. Mock all HTTP interactions.
33+
- Integration test cleanup is mandatory — use `schedule_for_cleanup()` for every created resource to avoid orphaned Synapse entities.

0 commit comments

Comments
 (0)