Add E2E tests, fix bugs

Made-with: Cursor
This commit is contained in:
Oleksandr Bezdieniezhnykh
2026-04-13 05:17:48 +03:00
parent 1f98b5e958
commit 8f7deb3fca
71 changed files with 4740 additions and 29 deletions
+105
View File
@@ -0,0 +1,105 @@
# Module: api_client
## Purpose
Central API client that orchestrates authentication, encrypted resource download/upload (using a big/small binary-split scheme), and CDN integration for the Azaion resource API.
## Public Interface
### Classes
#### `ApiClient` (cdef class)
| Attribute | Type | Description |
|-------------|-------------|------------------------------------|
| credentials | Credentials | User email/password |
| user | User | Authenticated user (from JWT) |
| token | str | JWT bearer token |
| cdn_manager | CDNManager | CDN upload/download client |
| api_url | str | Base URL for the resource API |
| folder | str | Declared in `.pxd` but never assigned — dead attribute |
#### Methods
| Method | Visibility | Signature | Description |
|------------------------------|------------|-------------------------------------------------------------------|--------------------------------------------------------------|
| `__init__` | def | `(self, str api_url)` | Initialize with API base URL |
| `set_credentials_from_dict` | cpdef | `(self, str email, str password)` | Set credentials + initialize CDN from `cdn.yaml` |
| `set_credentials` | cdef | `(self, Credentials credentials)` | Internal: set credentials, lazy-init CDN manager |
| `login` | cdef | `(self)` | POST `/login`, store JWT token |
| `set_token` | cdef | `(self, str token)` | Decode JWT claims → create `User` with role mapping |
| `get_user` | cdef | `(self) -> User` | Lazy login + return user |
| `request` | cdef | `(self, str method, str url, object payload, bint is_stream)` | Authenticated HTTP request with auto-retry on 401/403 |
| `list_files` | cdef | `(self, str folder, str search_file)` | GET `/resources/list/{folder}` with search param |
| `check_resource` | cdef | `(self)` | POST `/resources/check` with hardware fingerprint |
| `load_bytes` | cdef | `(self, str filename, str folder) -> bytes` | Download + decrypt resource using per-user+hw key |
| `upload_file` | cdef | `(self, str filename, bytes resource, str folder)` | POST multipart upload to `/resources/{folder}` |
| `load_big_file_cdn` | cdef | `(self, str folder, str big_part) -> bytes` | Download large file part from CDN |
| `load_big_small_resource` | cpdef | `(self, str resource_name, str folder) -> bytes` | Reassemble resource from small (API) + big (CDN/local) parts |
| `upload_big_small_resource` | cpdef | `(self, bytes resource, str resource_name, str folder)` | Split-encrypt and upload small part to API, big part to CDN |
| `upload_to_cdn` | cpdef | `(self, str bucket, str filename, bytes file_bytes)` | Direct CDN upload |
| `download_from_cdn` | cpdef | `(self, str bucket, str filename) -> bytes` | Direct CDN download |
## Internal Logic
### Authentication Flow
1. `set_credentials_from_dict()` → stores credentials, downloads `cdn.yaml` via `load_bytes()` (encrypted), parses YAML, initializes `CDNManager`
2. `login()` → POST `/login` with email/password → receives JWT token → `set_token()` decodes claims (nameid, unique_name, role) → creates `User`
3. `request()` → wraps all authenticated HTTP calls; on 401/403 auto-retries with fresh login
### Big/Small Resource Split (download)
1. Downloads the "small" encrypted part via API (`load_bytes()` with per-user+hw key)
2. Checks if "big" part exists locally (cached file)
3. If local: concatenates small + big, decrypts with shared resource key
4. If decrypt fails (version mismatch): falls through to CDN download
5. If no local: downloads big part from CDN
6. Concatenates small + big, decrypts with shared resource key
### Big/Small Resource Split (upload)
1. Encrypts entire resource with shared resource key
2. Splits: small part = `min(SMALL_SIZE_KB * 1024, 30% of encrypted)`, big part = remainder
3. Uploads big part to CDN + saves local copy
4. Uploads small part to API via multipart POST
### JWT Role Mapping
Maps `role` claim string to `RoleEnum`: ApiAdmin, Admin, ResourceUploader, Validator, Operator, or NONE (default).
## Dependencies
- **Internal**: `constants`, `credentials`, `cdn_manager`, `hardware_service`, `security`, `user`
- **External**: `json`, `os` (stdlib), `jwt` (pyjwt 2.10.1), `requests` (2.32.4), `yaml` (pyyaml 6.0.2)
## Consumers
- `main` — creates `ApiClient` instance; calls `set_credentials_from_dict`, `login`, `load_big_small_resource`, `upload_big_small_resource`; reads `.token`
## Data Models
Uses `Credentials`, `User`, `RoleEnum`, `CDNCredentials`, `CDNManager` from other modules.
## Configuration
| Source | Key | Usage |
|-------------|--------------------|-----------------------------------------|
| `cdn.yaml` | host | CDN endpoint URL |
| `cdn.yaml` | downloader_access_key/secret | CDN read credentials |
| `cdn.yaml` | uploader_access_key/secret | CDN write credentials |
The CDN config file is itself downloaded encrypted from the API on first credential setup.
## External Integrations
- **Azaion Resource API**: `/login`, `/resources/get/{folder}`, `/resources/{folder}` (upload), `/resources/list/{folder}`, `/resources/check`
- **S3 CDN**: via `CDNManager` for large file parts
## Security
- JWT token stored in memory, decoded without signature verification (`options={"verify_signature": False}`)
- Per-download encryption: resources encrypted with AES-256-CBC using a key derived from user credentials + hardware fingerprint
- Shared resource encryption: big/small split uses a fixed shared key
- Auto-retry on 401/403 re-authenticates transparently
- CDN config is downloaded encrypted, decrypted locally
## Tests
No tests found.
+67
View File
@@ -0,0 +1,67 @@
# Module: binary_split
## Purpose
Handles the encrypted Docker image archive workflow: downloading a key fragment from the API, decrypting an AES-256-CBC encrypted archive, loading it into Docker, and verifying expected images are present.
## Public Interface
### Functions
| Function | Signature | Description |
|------------------------|------------------------------------------------------------------------|----------------------------------------------------------|
| `download_key_fragment`| `(resource_api_url: str, token: str) -> bytes` | GET request to `/binary-split/key-fragment` with Bearer auth |
| `decrypt_archive` | `(encrypted_path: str, key_fragment: bytes, output_path: str) -> None` | AES-256-CBC decryption with SHA-256 derived key; strips PKCS7 padding |
| `docker_load` | `(tar_path: str) -> None` | Runs `docker load -i <tar_path>` subprocess |
| `check_images_loaded` | `(version: str) -> bool` | Checks all `API_SERVICES` images exist for given version tag |
### Module-level Constants
| Name | Value |
|---------------|--------------------------------------------------------------------------------------------|
| API_SERVICES | List of 7 Docker image names: `azaion/annotations`, `azaion/flights`, `azaion/detections`, `azaion/gps-denied-onboard`, `azaion/gps-denied-desktop`, `azaion/autopilot`, `azaion/ai-training` |
## Internal Logic
### `decrypt_archive`
1. Derives AES key: `SHA-256(key_fragment)` → 32-byte key
2. Reads first 16 bytes as IV from encrypted file
3. Decrypts remaining data in 64KB chunks using AES-256-CBC
4. After decryption, reads last byte of output to determine PKCS7 padding length
5. Truncates output file to remove padding
### `check_images_loaded`
Iterates all 7 service image names, runs `docker image inspect <name>:<version>` for each. Returns `False` on first missing image.
## Dependencies
- **Internal**: none (leaf module)
- **External**: `hashlib`, `os`, `subprocess` (stdlib), `requests` (2.32.4), `cryptography` (44.0.2)
## Consumers
- `main``_run_unlock()` calls all four functions; `unlock()` endpoint calls `check_images_loaded()`
## Data Models
None.
## Configuration
No env vars consumed directly. `API_SERVICES` list is hardcoded.
## External Integrations
- **REST API**: GET `{resource_api_url}/binary-split/key-fragment` — downloads encryption key fragment
- **Docker CLI**: `docker load` and `docker image inspect` via subprocess
- **File system**: reads encrypted `.enc` archive, writes decrypted `.tar` archive
## Security
- Key derivation: SHA-256 hash of server-provided key fragment
- Encryption: AES-256-CBC with PKCS7 padding
- The key fragment is ephemeral — downloaded per unlock operation
## Tests
No tests found.
+79
View File
@@ -0,0 +1,79 @@
# Module: cdn_manager
## Purpose
Manages upload and download operations to an S3-compatible CDN (object storage) using separate credentials for read and write access.
## Public Interface
### Classes
#### `CDNCredentials` (cdef class)
| Attribute | Type | Description |
|--------------------------|------|--------------------------------|
| host | str | S3 endpoint URL |
| downloader_access_key | str | Read-only access key |
| downloader_access_secret | str | Read-only secret key |
| uploader_access_key | str | Write access key |
| uploader_access_secret | str | Write secret key |
#### `CDNManager` (cdef class)
| Attribute | Type | Description |
|-----------------|--------|------------------------------------|
| creds | CDNCredentials | Stored credentials |
| download_client | object | boto3 S3 client (read credentials) |
| upload_client | object | boto3 S3 client (write credentials)|
| Method | Signature | Returns | Description |
|------------|--------------------------------------------------------|---------|--------------------------------------|
| `__init__` | `(self, CDNCredentials credentials)` | — | Creates both S3 clients |
| `upload` | `cdef (self, str bucket, str filename, bytes file_bytes)` | bool | Uploads bytes to S3 bucket/key |
| `download` | `cdef (self, str folder, str filename)` | bool | Downloads S3 object to local `folder/filename` |
Note: `.pxd` declares the parameter as `str bucket` while `.pyx` uses `str folder`. Functionally identical (Cython matches by position).
## Internal Logic
### Constructor
Creates two separate boto3 S3 clients:
- `download_client` with `downloader_access_key` / `downloader_access_secret`
- `upload_client` with `uploader_access_key` / `uploader_access_secret`
Both clients connect to the same `endpoint_url` (CDN host).
### `upload`
Uses `upload_fileobj` to stream bytes to S3. Returns `True` on success, `False` on exception.
### `download`
Creates local directory if needed (`os.makedirs`), then uses `download_file` to save S3 object to local path `folder/filename`. Returns `True` on success, `False` on exception.
## Dependencies
- **Internal**: `constants` (for `log()`, `logerror()`)
- **External**: `io`, `os` (stdlib), `boto3` (1.40.9)
## Consumers
- `api_client``load_big_file_cdn()`, `upload_big_small_resource()`, `upload_to_cdn()`, `download_from_cdn()`
## Data Models
`CDNCredentials` is the data model.
## Configuration
CDN credentials are loaded from a YAML file (`cdn.yaml`) by the `api_client` module, not by this module directly.
## External Integrations
- **S3-compatible storage**: upload and download via boto3 S3 client with custom endpoint URL
## Security
Separate read/write credential pairs enforce least-privilege access to CDN storage.
## Tests
No tests found.
+68
View File
@@ -0,0 +1,68 @@
# Module: constants
## Purpose
Centralizes shared configuration constants and provides the application-wide logging interface via Loguru.
## Public Interface
### Constants (cdef, module-level)
| Name | Type | Value |
|------------------------|------|--------------------------------|
| CONFIG_FILE | str | `"config.yaml"` |
| QUEUE_CONFIG_FILENAME | str | `"secured-config.json"` |
| AI_ONNX_MODEL_FILE | str | `"azaion.onnx"` |
| CDN_CONFIG | str | `"cdn.yaml"` |
| MODELS_FOLDER | str | `"models"` |
| SMALL_SIZE_KB | int | `3` |
| ALIGNMENT_WIDTH | int | `32` |
Note: `QUEUE_MAXSIZE`, `COMMANDS_QUEUE`, `ANNOTATIONS_QUEUE` are declared in the `.pxd` but not defined in the `.pyx` — they are unused in this codebase.
### Functions (cdef, Cython-only visibility)
| Function | Signature | Description |
|------------------------|----------------------------|------------------------------|
| `log` | `cdef log(str log_message)` | Logs at INFO level via Loguru |
| `logerror` | `cdef logerror(str error)` | Logs at ERROR level via Loguru |
## Internal Logic
Loguru is configured with three sinks:
- **File sink**: `Logs/log_loader_{date}.txt`, INFO level, daily rotation, 30-day retention, async (enqueue=True)
- **Stdout sink**: DEBUG level, filtered to INFO/DEBUG/SUCCESS only, colorized
- **Stderr sink**: WARNING+ level, colorized
Log format: `[HH:mm:ss LEVEL] message`
## Dependencies
- **Internal**: none (leaf module)
- **External**: `loguru` (0.7.3), `sys`, `time`
## Consumers
- `hardware_service` — calls `log()`
- `cdn_manager` — calls `log()`, `logerror()`
- `api_client` — calls `log()`, `logerror()`, reads `CDN_CONFIG`, `SMALL_SIZE_KB`
## Data Models
None.
## Configuration
No env vars consumed directly. Log file path is hardcoded to `Logs/log_loader_{date}.txt`.
## External Integrations
None.
## Security
None.
## Tests
No tests found.
+55
View File
@@ -0,0 +1,55 @@
# Module: credentials
## Purpose
Simple data holder for user authentication credentials (email + password).
## Public Interface
### Classes
#### `Credentials` (cdef class)
| Attribute | Type | Visibility |
|-----------|------|------------|
| email | str | public |
| password | str | public |
| Method | Signature | Description |
|----------------|----------------------------------------------|------------------------------------|
| `__init__` | `(self, str email, str password)` | Constructor |
| `__str__` | `(self) -> str` | Returns `"email: password"` format |
## Internal Logic
No logic — pure data class.
## Dependencies
- **Internal**: none (leaf module)
- **External**: none
## Consumers
- `security``get_api_encryption_key` takes `Credentials` as parameter
- `api_client` — holds a `Credentials` instance, uses `.email` and `.password` for login and key derivation
## Data Models
The `Credentials` class itself is the data model.
## Configuration
None.
## External Integrations
None.
## Security
Stores plaintext password in memory. No encryption at rest.
## Tests
No tests found.
@@ -0,0 +1,64 @@
# Module: hardware_service
## Purpose
Collects a hardware fingerprint string from the host OS (CPU, GPU, memory, drive serial) for use in hardware-bound encryption key derivation.
## Public Interface
### Classes
#### `HardwareService` (cdef class)
| Method | Signature | Description |
|---------------------|--------------------------------|------------------------------------------------|
| `get_hardware_info` | `@staticmethod cdef str ()` | Returns cached hardware fingerprint string |
### Module-level State
| Name | Type | Description |
|------------------|------|----------------------------------|
| `_CACHED_HW_INFO`| str | Cached result (computed once) |
## Internal Logic
### `get_hardware_info`
1. If cached (`_CACHED_HW_INFO is not None`), return cached value immediately
2. Detect OS via `os.name`:
- **Windows (`nt`)**: PowerShell command querying WMI (Win32_Processor, Win32_VideoController, Win32_OperatingSystem, Disk serial)
- **Linux/other**: shell commands (`lscpu`, `lspci`, `free`, block device serial)
3. Parse output lines → extract CPU, GPU, memory, drive serial
4. Format: `"CPU: {cpu}. GPU: {gpu}. Memory: {memory}. DriveSerial: {serial}"`
5. Cache result in `_CACHED_HW_INFO`
The function uses `subprocess.check_output(shell=True)` — platform-specific shell commands.
## Dependencies
- **Internal**: `constants` (for `log()`)
- **External**: `os`, `subprocess` (stdlib)
## Consumers
- `api_client``load_bytes()` and `check_resource()` call `HardwareService.get_hardware_info()`
## Data Models
None.
## Configuration
None. Hardware detection commands are hardcoded per platform.
## External Integrations
- **OS commands**: Windows PowerShell (Get-CimInstance, Get-Disk) or Linux shell (lscpu, lspci, free, /sys/block)
## Security
Produces a hardware fingerprint used to bind encryption keys to specific machines. The fingerprint includes drive serial number, which acts as a machine-unique identifier.
## Tests
No tests found.
+102
View File
@@ -0,0 +1,102 @@
# Module: main
## Purpose
FastAPI application entry point providing HTTP endpoints for health checks, authentication, encrypted resource loading/uploading, and a multi-step Docker image unlock workflow.
## Public Interface
### FastAPI Application
`app = FastAPI(title="Azaion.Loader")`
### Endpoints
| Method | Path | Request Body | Response | Description |
|--------|------------------|---------------------|----------------------------|----------------------------------------------------|
| GET | `/health` | — | `{"status": "healthy"}` | Liveness probe |
| GET | `/status` | — | `StatusResponse` | Auth status + model cache dir |
| POST | `/login` | `LoginRequest` | `{"status": "ok"}` | Set credentials on API client |
| POST | `/load/{filename}`| `LoadRequest` | binary (octet-stream) | Download + decrypt resource |
| POST | `/upload/{filename}`| multipart (file + folder) | `{"status": "ok"}` | Encrypt + upload resource (big/small split) |
| POST | `/unlock` | `LoginRequest` | `{"state": "..."}` | Start background unlock workflow |
| GET | `/unlock/status` | — | `{"state": "...", "error": ...}` | Poll unlock progress |
### Pydantic Models
| Model | Fields |
|-----------------|----------------------------------------------|
| LoginRequest | email: str, password: str |
| LoadRequest | filename: str, folder: str |
| HealthResponse | status: str |
| StatusResponse | status: str, authenticated: bool, modelCacheDir: str |
### Module-level State
| Name | Type | Description |
|----------------|--------------------|------------------------------------------------|
| api_client | ApiClient or None | Lazy-initialized singleton |
| unlock_state | UnlockState | Current unlock workflow state |
| unlock_error | Optional[str] | Last unlock error message |
| unlock_lock | threading.Lock | Thread safety for unlock state mutations |
## Internal Logic
### `get_api_client()`
Lazy singleton pattern: creates `ApiClient(RESOURCE_API_URL)` on first call.
### Unlock Workflow (`_run_unlock`)
Background task (via FastAPI BackgroundTasks) that runs these steps:
1. Check if Docker images already loaded → if yes, set `ready`
2. Authenticate with API (login)
3. Download key fragment from `/binary-split/key-fragment`
4. Decrypt archive at `IMAGES_PATH``.tar`
5. `docker load` the tar file
6. Clean up tar file
7. Set state to `ready` (or `error` on failure)
State transitions are guarded by `unlock_lock` (threading.Lock).
### `/unlock` Endpoint
- If already `ready` → return immediately
- If already in progress → return current state
- If no encrypted archive found → check if images already loaded; if not, 404
- Otherwise, starts `_run_unlock` as a background task
## Dependencies
- **Internal**: `unlock_state` (UnlockState enum), `api_client` (lazy import), `binary_split` (lazy import)
- **External**: `os`, `threading` (stdlib), `fastapi`, `pydantic`
## Consumers
None — this is the entry point module.
## Data Models
`LoginRequest`, `LoadRequest`, `HealthResponse`, `StatusResponse` (Pydantic models defined inline).
## Configuration
| Env Variable | Default | Description |
|------------------|--------------------------------|--------------------------------|
| RESOURCE_API_URL | `https://api.azaion.com` | Azaion resource API base URL |
| IMAGES_PATH | `/opt/azaion/images.enc` | Path to encrypted Docker images |
| API_VERSION | `latest` | Expected Docker image version tag |
## External Integrations
- **Azaion Resource API**: via `ApiClient` (authenticated resource download/upload)
- **Docker CLI**: via `binary_split` (docker load, image inspect)
- **File system**: encrypted archive at `IMAGES_PATH`
## Security
- Login endpoint returns 401 on auth failure
- All resource endpoints use authenticated API client
- Unlock state is thread-safe via `threading.Lock`
- Lazy imports of Cython modules (`api_client`, `binary_split`) to avoid import-time side effects
## Tests
No tests found.
+81
View File
@@ -0,0 +1,81 @@
# Module: security
## Purpose
Provides AES-256-CBC encryption/decryption and multiple key derivation strategies for API resource protection and hardware-bound access control.
## Public Interface
### Classes
#### `Security` (cdef class)
All methods are `@staticmethod cdef` — Cython-only visibility, not callable from pure Python.
| Method | Signature | Description |
|-----------------------------|-----------------------------------------------------------------|----------------------------------------------------------------------|
| `encrypt_to` | `(input_bytes, key) -> bytes` | AES-256-CBC encrypt with random IV, PKCS7 padding; returns `IV + ciphertext` |
| `decrypt_to` | `(ciphertext_with_iv_bytes, key) -> bytes` | AES-256-CBC decrypt; first 16 bytes = IV; manual PKCS7 unpad |
| `get_hw_hash` | `(str hardware) -> str` | Derives hardware hash: `SHA-384("Azaion_{hardware}_%$$$)0_")` → base64 |
| `get_api_encryption_key` | `(Credentials creds, str hardware_hash) -> str` | Derives per-user+hw key: `SHA-384("{email}-{password}-{hw_hash}-#%@AzaionKey@%#---")` → base64 |
| `get_resource_encryption_key`| `() -> str` | Returns fixed shared key: `SHA-384("-#%@AzaionKey@%#---234sdfklgvhjbnn")` → base64 |
| `calc_hash` | `(str key) -> str` | SHA-384 hash → base64 string |
### Module-level Constants
| Name | Value | Status |
|-------------|----------|--------|
| BUFFER_SIZE | `65536` | Unused — declared but never referenced |
## Internal Logic
### Encryption (`encrypt_to`)
1. SHA-256 hash of string key → 32-byte AES key
2. Generate random 16-byte IV
3. PKCS7-pad plaintext to 128-bit block size
4. AES-CBC encrypt
5. Return `IV || ciphertext`
### Decryption (`decrypt_to`)
1. SHA-256 hash of string key → 32-byte AES key
2. Split input: first 16 bytes = IV, rest = ciphertext
3. AES-CBC decrypt
4. Manual PKCS7 unpadding: read last byte as padding length; strip if 116
### Key Derivation Hierarchy
- **Hardware hash**: salted hardware fingerprint → SHA-384 → base64
- **API encryption key**: combines user credentials + hardware hash + salt → SHA-384 → base64 (per-download key)
- **Resource encryption key**: fixed salt string → SHA-384 → base64 (shared key for big/small resource split)
## Dependencies
- **Internal**: `credentials` (for `Credentials` type in `get_api_encryption_key`)
- **External**: `base64`, `hashlib`, `os` (stdlib), `cryptography` (44.0.2)
## Consumers
- `api_client` — calls `encrypt_to`, `decrypt_to`, `get_hw_hash`, `get_api_encryption_key`, `get_resource_encryption_key`
## Data Models
None.
## Configuration
None.
## External Integrations
None.
## Security
- AES-256-CBC with PKCS7 padding for data encryption
- SHA-384 for key derivation (with various salts)
- SHA-256 for AES key expansion from string keys
- `get_resource_encryption_key()` uses a hardcoded salt — the key is static and shared across all users
- `get_api_encryption_key()` binds encryption to user credentials + hardware — per-user, per-machine keys
## Tests
No tests found.
+56
View File
@@ -0,0 +1,56 @@
# Module: unlock_state
## Purpose
Defines the state machine enum for the multi-step Docker image unlock workflow.
## Public Interface
### Enums
#### `UnlockState` (str, Enum)
| Value | String Representation |
|------------------|-----------------------|
| idle | `"idle"` |
| authenticating | `"authenticating"` |
| downloading_key | `"downloading_key"` |
| decrypting | `"decrypting"` |
| loading_images | `"loading_images"` |
| ready | `"ready"` |
| error | `"error"` |
Inherits from `str` and `Enum`, so `.value` returns the string name directly.
## Internal Logic
No logic — pure enum definition. State transitions are managed externally by `main.py`.
## Dependencies
- **Internal**: none (leaf module)
- **External**: `enum` (stdlib)
## Consumers
- `main` — uses `UnlockState` to track and report the unlock workflow progress
## Data Models
`UnlockState` is the data model.
## Configuration
None.
## External Integrations
None.
## Security
None.
## Tests
No tests found.
+68
View File
@@ -0,0 +1,68 @@
# Module: user
## Purpose
Defines the authenticated user model and role enumeration for authorization decisions.
## Public Interface
### Enums
#### `RoleEnum` (cdef enum)
| Value | Numeric |
|------------------|---------|
| NONE | 0 |
| Operator | 10 |
| Validator | 20 |
| CompanionPC | 30 |
| Admin | 40 |
| ResourceUploader | 50 |
| ApiAdmin | 1000 |
### Classes
#### `User` (cdef class)
| Attribute | Type | Visibility |
|-----------|----------|------------|
| id | str | public |
| email | str | public |
| role | RoleEnum | public |
| Method | Signature | Description |
|------------|---------------------------------------------------|-------------|
| `__init__` | `(self, str id, str email, RoleEnum role)` | Constructor |
## Internal Logic
No logic — pure data class with enum.
## Dependencies
- **Internal**: none (leaf module)
- **External**: none
## Consumers
- `api_client` — creates `User` instances from JWT claims in `set_token()`, maps role strings to `RoleEnum`
## Data Models
`RoleEnum` + `User` are the data models.
## Configuration
None.
## External Integrations
None.
## Security
Role hierarchy is implicit in numeric values but no authorization enforcement logic exists here.
## Tests
No tests found.