olira 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 (43) hide show
  1. olira-1.0.0/.devcontainer/Dockerfile +14 -0
  2. olira-1.0.0/.devcontainer/README.md +19 -0
  3. olira-1.0.0/.devcontainer/devcontainer.json +74 -0
  4. olira-1.0.0/.github/workflows/ci.yml +36 -0
  5. olira-1.0.0/.github/workflows/publish.yml +53 -0
  6. olira-1.0.0/CHANGELOG.md +26 -0
  7. olira-1.0.0/LICENSE +17 -0
  8. olira-1.0.0/PKG-INFO +218 -0
  9. olira-1.0.0/README.md +191 -0
  10. olira-1.0.0/SDK_DOCUMENTATION.md +1933 -0
  11. olira-1.0.0/examples/.env.example +14 -0
  12. olira-1.0.0/examples/00_quickstart.py +61 -0
  13. olira-1.0.0/examples/01_patient_management.py +101 -0
  14. olira-1.0.0/examples/02_event_logging.py +156 -0
  15. olira-1.0.0/examples/03_historical_ingestion.py +172 -0
  16. olira-1.0.0/examples/04_logs_only_workflow.py +154 -0
  17. olira-1.0.0/examples/05_read_patient_state.py +113 -0
  18. olira-1.0.0/examples/README.md +28 -0
  19. olira-1.0.0/examples/pyproject.toml +10 -0
  20. olira-1.0.0/pyproject.toml +73 -0
  21. olira-1.0.0/scripts/check-version.sh +98 -0
  22. olira-1.0.0/scripts/install-dev.sh +20 -0
  23. olira-1.0.0/scripts/lint.sh +16 -0
  24. olira-1.0.0/scripts/pre-pr.sh +34 -0
  25. olira-1.0.0/scripts/test.sh +10 -0
  26. olira-1.0.0/scripts/uv.sh +6 -0
  27. olira-1.0.0/src/olira/__init__.py +535 -0
  28. olira-1.0.0/src/olira/client.py +1118 -0
  29. olira-1.0.0/src/olira/exceptions.py +33 -0
  30. olira-1.0.0/src/olira/http.py +557 -0
  31. olira-1.0.0/src/olira/models.py +629 -0
  32. olira-1.0.0/src/olira/py.typed +0 -0
  33. olira-1.0.0/src/olira/queue.py +142 -0
  34. olira-1.0.0/src/olira/validation.py +365 -0
  35. olira-1.0.0/src/olira/version.py +3 -0
  36. olira-1.0.0/tests/__init__.py +1 -0
  37. olira-1.0.0/tests/conftest.py +14 -0
  38. olira-1.0.0/tests/test_async_client.py +131 -0
  39. olira-1.0.0/tests/test_client.py +82 -0
  40. olira-1.0.0/tests/test_event_recorders.py +234 -0
  41. olira-1.0.0/tests/test_privacy.py +42 -0
  42. olira-1.0.0/tests/test_retry.py +52 -0
  43. olira-1.0.0/uv.lock +644 -0
@@ -0,0 +1,14 @@
1
+ # See here for image contents: https://github.com/microsoft/vscode-dev-containers/tree/v0.245.0/containers/python-3
2
+ ARG VARIANT=3.11-bookworm
3
+ FROM mcr.microsoft.com/devcontainers/python:${VARIANT}
4
+
5
+ # Install uv - fast Python package installer
6
+ RUN curl -LsSf https://astral.sh/uv/install.sh | sh && \
7
+ mkdir -p /usr/local/bin && \
8
+ if [ -f /root/.cargo/bin/uv ]; then \
9
+ mv /root/.cargo/bin/uv /usr/local/bin/uv; \
10
+ elif [ -f /root/.local/bin/uv ]; then \
11
+ mv /root/.local/bin/uv /usr/local/bin/uv; \
12
+ fi && \
13
+ chmod +x /usr/local/bin/uv && \
14
+ uv --version
@@ -0,0 +1,19 @@
1
+ # Development container
2
+
3
+ VS Code dev container for the Olira Python SDK.
4
+
5
+ ## Setup
6
+
7
+ 1. Open this repository in VS Code.
8
+ 2. Install the [Dev Containers](https://marketplace.visualstudio.com/items?itemName=ms-vscode-remote.remote-containers) extension.
9
+ 3. **Reopen in Container** (Command Palette → Dev Containers: Reopen in Container).
10
+ 4. After the container builds: `bash scripts/install-dev.sh` (PyPI only)
11
+
12
+ API reference: [https://olira.ai/api-docs](https://olira.ai/api-docs) (Python SDK tab).
13
+
14
+ ## Commands
15
+
16
+ - Pre-PR: `./scripts/pre-pr.sh`
17
+ - Lint: `./scripts/lint.sh`
18
+ - Tests: `./scripts/test.sh`
19
+ - Version check: `./scripts/check-version.sh`
@@ -0,0 +1,74 @@
1
+ {
2
+ "name": "Olira Python SDK",
3
+ "build": {
4
+ "dockerfile": "Dockerfile",
5
+ "context": "..",
6
+ "args": {
7
+ "VARIANT": "3.11-bookworm"
8
+ }
9
+ },
10
+ "customizations": {
11
+ "vscode": {
12
+ "settings": {
13
+ "python.defaultInterpreterPath": "/home/vscode/.venv/bin/python",
14
+ "python.linting.enabled": true,
15
+ "python.formatting.ruffPath": "/usr/local/py-utils/bin/ruff",
16
+ "python.linting.ruffPath": "/usr/local/py-utils/bin/ruff",
17
+ "python.linting.mypyPath": "/usr/local/py-utils/bin/mypy",
18
+ "files.watcherExclude": {
19
+ "**/__pycache__/**": true,
20
+ "**/.mypy_cache/**": true,
21
+ "**/.pytest_cache/**": true,
22
+ "**/.ruff_cache/**": true,
23
+ "**/.venv/**": true,
24
+ "**/venv/**": true
25
+ },
26
+ "files.watcherInclude": [
27
+ "**/*.py",
28
+ "**/*.yaml",
29
+ "**/*.yml",
30
+ "**/*.json",
31
+ "**/*.toml"
32
+ ]
33
+ },
34
+ "extensions": [
35
+ "ms-python.python",
36
+ "ms-python.vscode-pylance",
37
+ "charliermarsh.ruff",
38
+ "eamodio.gitlens",
39
+ "ms-python.mypy-type-checker"
40
+ ]
41
+ }
42
+ },
43
+ "remoteUser": "vscode",
44
+ "mounts": [
45
+ "source=${localEnv:HOME}/.ssh/,target=/home/vscode/.ssh/,type=bind,consistency=cached",
46
+ "source=olira-sdk-venv,target=/home/vscode/.venv,type=volume",
47
+ "source=olira-sdk-pycache,target=/tmp/pycache,type=volume",
48
+ "source=olira-sdk-mypy-cache,target=/tmp/.mypy_cache,type=volume",
49
+ "source=olira-sdk-pytest-cache,target=/tmp/.pytest_cache,type=volume",
50
+ "source=olira-sdk-ruff-cache,target=/tmp/.ruff_cache,type=volume",
51
+ "source=olira-sdk-uv-cache,target=/root/.cache/uv,type=volume"
52
+ ],
53
+ "containerEnv": {
54
+ "PYTHONPATH": "${workspaceFolder}/src",
55
+ "UV_PROJECT_ENVIRONMENT": "/home/vscode/.venv",
56
+ "PYTHONPYCACHEPREFIX": "/tmp/pycache",
57
+ "MYPY_CACHE_DIR": "/tmp/.mypy_cache",
58
+ "PYTEST_CACHE_DIR": "/tmp/.pytest_cache",
59
+ "RUFF_CACHE_DIR": "/tmp/.ruff_cache"
60
+ },
61
+ "features": {
62
+ "git": "latest"
63
+ },
64
+ "postCreateCommand": "bash -c 'sudo chown -R vscode:vscode /home/vscode/.venv /tmp/pycache /tmp/.mypy_cache /tmp/.pytest_cache /tmp/.ruff_cache 2>/dev/null || true; cd ${containerWorkspaceFolder} && bash scripts/install-dev.sh'",
65
+ "hostRequirements": {
66
+ "cpus": 2,
67
+ "memory": "4gb",
68
+ "storage": "8gb"
69
+ },
70
+ "runArgs": [
71
+ "--cpus=2",
72
+ "--memory=4g"
73
+ ]
74
+ }
@@ -0,0 +1,36 @@
1
+ name: CI
2
+
3
+ on:
4
+ pull_request:
5
+ branches:
6
+ - main
7
+
8
+ permissions:
9
+ contents: read
10
+
11
+ jobs:
12
+ ci:
13
+ runs-on: ubuntu-latest
14
+ steps:
15
+ - name: Checkout repository
16
+ uses: actions/checkout@v4
17
+ with:
18
+ fetch-depth: 0
19
+
20
+ - name: Set up Python
21
+ uses: actions/setup-python@v5
22
+ with:
23
+ python-version: "3.11"
24
+
25
+ - name: Install uv
26
+ uses: astral-sh/setup-uv@v4
27
+ with:
28
+ version: "latest"
29
+ enable-cache: true
30
+
31
+ - name: Pre-PR validation
32
+ env:
33
+ CI: "true"
34
+ GITHUB_BASE_REF: ${{ github.base_ref }}
35
+ UV_INDEX_URL: https://pypi.org/simple/
36
+ run: ./scripts/pre-pr.sh
@@ -0,0 +1,53 @@
1
+ name: Publish to PyPI
2
+
3
+ on:
4
+ push:
5
+ tags:
6
+ - "v*"
7
+
8
+ permissions:
9
+ id-token: write
10
+ contents: write
11
+
12
+ jobs:
13
+ publish:
14
+ runs-on: ubuntu-latest
15
+ environment: pypi
16
+
17
+ steps:
18
+ - name: Checkout repository
19
+ uses: actions/checkout@v4
20
+
21
+ - name: Set up Python
22
+ uses: actions/setup-python@v5
23
+ with:
24
+ python-version: "3.11"
25
+
26
+ - name: Install uv
27
+ uses: astral-sh/setup-uv@v4
28
+ with:
29
+ version: "latest"
30
+
31
+ - name: Read version
32
+ id: version
33
+ run: echo "version=$(grep -E '^version = ' pyproject.toml | sed 's/version = "\(.*\)"/\1/')" >> "$GITHUB_OUTPUT"
34
+
35
+ - name: Reject pre-release versions
36
+ run: |
37
+ VERSION="${{ steps.version.outputs.version }}"
38
+ if [[ "$VERSION" =~ -(alpha|beta|rc) ]] || [[ "$VERSION" == *a* ]] || [[ "$VERSION" == *b[0-9] ]]; then
39
+ echo "::error::Refusing to publish pre-release version ($VERSION) to PyPI."
40
+ exit 1
41
+ fi
42
+ echo "Publishing stable version $VERSION to PyPI."
43
+
44
+ - name: Build package
45
+ env:
46
+ UV_INDEX_URL: https://pypi.org/simple/
47
+ run: uv build
48
+
49
+ - name: Publish to PyPI
50
+ uses: pypa/gh-action-pypi-publish@release/v1
51
+ with:
52
+ packages-dir: dist/
53
+ print-hash: true
@@ -0,0 +1,26 @@
1
+ # Changelog
2
+
3
+ All notable changes to this project will be documented in this file.
4
+
5
+ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
6
+ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
7
+
8
+ ## [1.0.0] - 2026-05-20
9
+
10
+ First public release of the Olira Python SDK (`pip install olira`).
11
+
12
+ ### Added
13
+
14
+ - **Event logging** (`sdk:event-log`): `OliraClient.log()`, `log_batch()`, background queue with `flush()`, and module-level `olira.init()` / `olira.log()` / `olira.flush()`.
15
+ - **Patient management** (`api:manage-patients`): create, read, update, delete, list, and batch-create patients; `ExternalIdentifier` for linking to EMR or partner IDs.
16
+ - **Patient token** (`sdk:patient-token`): `get_patient_token()` for short-lived JWTs used with the [Olira MCP Patient State server](https://olira.ai/api-docs).
17
+ - **Patient state read** (`sdk:state-read`): stable modules, event state modules, views, logs, events, and memories — REST-backed access to compiled patient state from Python.
18
+ - **Historical data ingestion** (`sdk:historical-ingest`): JSONL file or inline record upload, two-phase confirm flow, job polling, and local pre-flight validation (`validate_ingestion_file`, `validate_ingestion_records`).
19
+ - **Async client**: `AsyncOliraClient` with the same surface as `OliraClient`.
20
+ - **Typed models**: `OliraLogType`, payload helpers (`EsasItem`, `LabResultItem`, …), and structured error types (`AuthError`, `ValidationError`, `RateLimitError`, `ServerError`).
21
+ - **Examples**: runnable scripts under `examples/` for quickstart, patients, logging, ingestion, and state read.
22
+
23
+ ### Documentation
24
+
25
+ - API reference: [https://olira.ai/api-docs](https://olira.ai/api-docs) (Python SDK tab)
26
+ - Local reference: `SDK_DOCUMENTATION.md`
olira-1.0.0/LICENSE ADDED
@@ -0,0 +1,17 @@
1
+ Apache License
2
+ Version 2.0, January 2004
3
+ http://www.apache.org/licenses/
4
+
5
+ Copyright 2026 Olira AI
6
+
7
+ Licensed under the Apache License, Version 2.0 (the "License");
8
+ you may not use this file except in compliance with the License.
9
+ You may obtain a copy of the License at
10
+
11
+ http://www.apache.org/licenses/LICENSE-2.0
12
+
13
+ Unless required by applicable law or agreed to in writing, software
14
+ distributed under the License is distributed on an "AS IS" BASIS,
15
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
16
+ See the License for the specific language governing permissions and
17
+ limitations under the License.
olira-1.0.0/PKG-INFO ADDED
@@ -0,0 +1,218 @@
1
+ Metadata-Version: 2.4
2
+ Name: olira
3
+ Version: 1.0.0
4
+ Summary: Olira Python SDK — event ingestion client for the Olira platform
5
+ Project-URL: Documentation, https://olira.ai/api-docs
6
+ Author-email: Olira AI <dev@olira.ai>
7
+ License: Apache-2.0
8
+ License-File: LICENSE
9
+ Keywords: events,health,ingestion,olira,sdk
10
+ Classifier: Development Status :: 5 - Production/Stable
11
+ Classifier: Intended Audience :: Developers
12
+ Classifier: License :: OSI Approved :: Apache Software License
13
+ Classifier: Programming Language :: Python :: 3
14
+ Classifier: Programming Language :: Python :: 3.11
15
+ Classifier: Programming Language :: Python :: 3.12
16
+ Classifier: Topic :: Software Development :: Libraries :: Python Modules
17
+ Requires-Python: >=3.11
18
+ Requires-Dist: httpx>=0.27
19
+ Requires-Dist: pydantic>=2.0
20
+ Provides-Extra: dev
21
+ Requires-Dist: mypy>=1.0; extra == 'dev'
22
+ Requires-Dist: pytest-asyncio>=0.23; extra == 'dev'
23
+ Requires-Dist: pytest-cov>=4.0; extra == 'dev'
24
+ Requires-Dist: pytest>=7.0; extra == 'dev'
25
+ Requires-Dist: ruff>=0.5; extra == 'dev'
26
+ Description-Content-Type: text/markdown
27
+
28
+ # Olira Python SDK
29
+
30
+ Log ingestion, patient management, and patient token client for the Olira platform.
31
+
32
+ ## Install
33
+
34
+ ```bash
35
+ pip install olira
36
+ ```
37
+
38
+ ## Documentation
39
+
40
+ Full API reference: [https://olira.ai/api-docs](https://olira.ai/api-docs) (Python SDK tab).
41
+
42
+ Local copy: [SDK_DOCUMENTATION.md](SDK_DOCUMENTATION.md).
43
+
44
+ ---
45
+
46
+ ## Authentication
47
+
48
+ All SDK methods authenticate with an **Olira API key** (`olira_prod_...`). Create keys from the Olira Console under **Settings → API Keys**, selecting the scopes you need:
49
+
50
+ | Scope | What it unlocks |
51
+ | --------------------- | ------------------------------------- |
52
+ | `sdk:event-log` | Log events |
53
+ | `api:manage-patients` | Create, read, update, delete patients |
54
+ | `sdk:patient-token` | Mint short-lived patient-scoped JWTs |
55
+
56
+ See [API key scopes](https://olira.ai/api-docs) for the full list.
57
+
58
+ Pass the key to `OliraClient` or to `olira.init()`:
59
+
60
+ ```python
61
+ import olira
62
+ olira.init(api_key="olira_prod_...") # or set OLIRA_API_KEY env var
63
+ ```
64
+
65
+ ---
66
+
67
+ ## Patient Management
68
+
69
+ Patients must exist before you can log events against them. Use the `api:manage-patients` scope.
70
+
71
+ Olira assigns a stable `id` to each patient at creation time. The `id` returned in the `Patient` object is what you use in all subsequent calls.
72
+
73
+ ```python
74
+ from olira import OliraClient
75
+
76
+ client = OliraClient(api_key="olira_prod_...")
77
+
78
+ # Create — Olira assigns the id; store it for future calls
79
+ patient = client.create_patient(
80
+ first_name="Jane",
81
+ last_name="Smith",
82
+ timezone="America/New_York",
83
+ primary_disease_site="breast",
84
+ disease_stage="II",
85
+ )
86
+ patient_id = patient.id
87
+
88
+ # Get
89
+ patient = client.get_patient(patient_id=patient_id)
90
+
91
+ # List (paginated, returns PatientListResult)
92
+ result = client.list_patients(limit=50, offset=0)
93
+ for p in result.patients:
94
+ print(p.id, p.first_name, p.last_name)
95
+
96
+ # Update (only supplied fields are changed)
97
+ patient = client.update_patient(patient_id=patient_id, disease_stage="III")
98
+
99
+ # Soft-delete
100
+ client.delete_patient(patient_id=patient_id)
101
+ ```
102
+
103
+ ---
104
+
105
+ ## Event Logging
106
+
107
+ Log a single event in the background (fire-and-forget):
108
+
109
+ ```python
110
+ import olira
111
+ from olira import OliraLogType
112
+
113
+ olira.init(api_key="olira_prod_...")
114
+
115
+ olira.log(
116
+ log_type=OliraLogType.USER_LOGIN,
117
+ patient_id=patient_id, # id from patient.id
118
+ )
119
+ olira.flush() # block until delivery
120
+ ```
121
+
122
+ Send a batch directly and get back a result:
123
+
124
+ ```python
125
+ from olira import OliraClient, LogSpec, OliraLogType, EsasItem
126
+
127
+ client = OliraClient(api_key="olira_prod_...")
128
+ result = client.log_batch([
129
+ LogSpec(log_type=OliraLogType.USER_LOGIN, patient_id=patient_id),
130
+ LogSpec(
131
+ log_type=OliraLogType.SYMPTOM_REPORT,
132
+ patient_id=patient_id,
133
+ payload={
134
+ "instrument": "esas_r",
135
+ "symptoms": [EsasItem(name="pain", score=4).model_dump()],
136
+ },
137
+ ),
138
+ ])
139
+ print(f"accepted={result.accepted}, failed={result.failed}")
140
+ client.close()
141
+ ```
142
+
143
+ ---
144
+
145
+ ## Patient Token
146
+
147
+ Mint a short-lived JWT scoped to a single patient. Requires the `sdk:patient-token` scope.
148
+
149
+ Use this when a patient device needs to communicate with the [Olira MCP Patient State server](https://olira.ai/api-docs) — pass the token as a Bearer header. The token expires after 15 minutes and is locked to the specified patient.
150
+
151
+ ```python
152
+ from olira import OliraClient
153
+
154
+ client = OliraClient(api_key="olira_prod_...")
155
+ token = client.get_patient_token(patient_id=patient_id)
156
+
157
+ print(token.access_token) # forward this to the patient device
158
+ print(token.expires_in) # 900 (seconds)
159
+ client.close()
160
+ ```
161
+
162
+ ---
163
+
164
+ ## Async client
165
+
166
+ All methods are available on `AsyncOliraClient` as coroutines:
167
+
168
+ ```python
169
+ import asyncio
170
+ from olira import AsyncOliraClient, OliraLogType
171
+
172
+ async def main():
173
+ async with AsyncOliraClient(api_key="olira_prod_...") as client:
174
+ patient = await client.create_patient(
175
+ first_name="Jane",
176
+ last_name="Smith",
177
+ )
178
+ await client.log(
179
+ log_type=OliraLogType.USER_LOGIN,
180
+ patient_id=patient.id,
181
+ )
182
+
183
+ asyncio.run(main())
184
+ ```
185
+
186
+ ---
187
+
188
+ ## Error handling
189
+
190
+ ```python
191
+ from olira import AuthError, ValidationError, RateLimitError, ServerError
192
+
193
+ try:
194
+ client.log_batch([...])
195
+ except AuthError:
196
+ # Invalid or revoked API key, or missing scope
197
+ ...
198
+ except ValidationError:
199
+ # Bad request (400/404/422) — e.g. unknown event type or missing required field
200
+ ...
201
+ except RateLimitError as e:
202
+ # Retry after e.retry_after seconds
203
+ ...
204
+ except ServerError:
205
+ # Transient server error after all retries exhausted
206
+ ...
207
+ ```
208
+
209
+ ---
210
+
211
+ ## Contributing
212
+
213
+ Dependencies are public PyPI packages only:
214
+
215
+ ```bash
216
+ bash scripts/install-dev.sh
217
+ ./scripts/pre-pr.sh
218
+ ```
olira-1.0.0/README.md ADDED
@@ -0,0 +1,191 @@
1
+ # Olira Python SDK
2
+
3
+ Log ingestion, patient management, and patient token client for the Olira platform.
4
+
5
+ ## Install
6
+
7
+ ```bash
8
+ pip install olira
9
+ ```
10
+
11
+ ## Documentation
12
+
13
+ Full API reference: [https://olira.ai/api-docs](https://olira.ai/api-docs) (Python SDK tab).
14
+
15
+ Local copy: [SDK_DOCUMENTATION.md](SDK_DOCUMENTATION.md).
16
+
17
+ ---
18
+
19
+ ## Authentication
20
+
21
+ All SDK methods authenticate with an **Olira API key** (`olira_prod_...`). Create keys from the Olira Console under **Settings → API Keys**, selecting the scopes you need:
22
+
23
+ | Scope | What it unlocks |
24
+ | --------------------- | ------------------------------------- |
25
+ | `sdk:event-log` | Log events |
26
+ | `api:manage-patients` | Create, read, update, delete patients |
27
+ | `sdk:patient-token` | Mint short-lived patient-scoped JWTs |
28
+
29
+ See [API key scopes](https://olira.ai/api-docs) for the full list.
30
+
31
+ Pass the key to `OliraClient` or to `olira.init()`:
32
+
33
+ ```python
34
+ import olira
35
+ olira.init(api_key="olira_prod_...") # or set OLIRA_API_KEY env var
36
+ ```
37
+
38
+ ---
39
+
40
+ ## Patient Management
41
+
42
+ Patients must exist before you can log events against them. Use the `api:manage-patients` scope.
43
+
44
+ Olira assigns a stable `id` to each patient at creation time. The `id` returned in the `Patient` object is what you use in all subsequent calls.
45
+
46
+ ```python
47
+ from olira import OliraClient
48
+
49
+ client = OliraClient(api_key="olira_prod_...")
50
+
51
+ # Create — Olira assigns the id; store it for future calls
52
+ patient = client.create_patient(
53
+ first_name="Jane",
54
+ last_name="Smith",
55
+ timezone="America/New_York",
56
+ primary_disease_site="breast",
57
+ disease_stage="II",
58
+ )
59
+ patient_id = patient.id
60
+
61
+ # Get
62
+ patient = client.get_patient(patient_id=patient_id)
63
+
64
+ # List (paginated, returns PatientListResult)
65
+ result = client.list_patients(limit=50, offset=0)
66
+ for p in result.patients:
67
+ print(p.id, p.first_name, p.last_name)
68
+
69
+ # Update (only supplied fields are changed)
70
+ patient = client.update_patient(patient_id=patient_id, disease_stage="III")
71
+
72
+ # Soft-delete
73
+ client.delete_patient(patient_id=patient_id)
74
+ ```
75
+
76
+ ---
77
+
78
+ ## Event Logging
79
+
80
+ Log a single event in the background (fire-and-forget):
81
+
82
+ ```python
83
+ import olira
84
+ from olira import OliraLogType
85
+
86
+ olira.init(api_key="olira_prod_...")
87
+
88
+ olira.log(
89
+ log_type=OliraLogType.USER_LOGIN,
90
+ patient_id=patient_id, # id from patient.id
91
+ )
92
+ olira.flush() # block until delivery
93
+ ```
94
+
95
+ Send a batch directly and get back a result:
96
+
97
+ ```python
98
+ from olira import OliraClient, LogSpec, OliraLogType, EsasItem
99
+
100
+ client = OliraClient(api_key="olira_prod_...")
101
+ result = client.log_batch([
102
+ LogSpec(log_type=OliraLogType.USER_LOGIN, patient_id=patient_id),
103
+ LogSpec(
104
+ log_type=OliraLogType.SYMPTOM_REPORT,
105
+ patient_id=patient_id,
106
+ payload={
107
+ "instrument": "esas_r",
108
+ "symptoms": [EsasItem(name="pain", score=4).model_dump()],
109
+ },
110
+ ),
111
+ ])
112
+ print(f"accepted={result.accepted}, failed={result.failed}")
113
+ client.close()
114
+ ```
115
+
116
+ ---
117
+
118
+ ## Patient Token
119
+
120
+ Mint a short-lived JWT scoped to a single patient. Requires the `sdk:patient-token` scope.
121
+
122
+ Use this when a patient device needs to communicate with the [Olira MCP Patient State server](https://olira.ai/api-docs) — pass the token as a Bearer header. The token expires after 15 minutes and is locked to the specified patient.
123
+
124
+ ```python
125
+ from olira import OliraClient
126
+
127
+ client = OliraClient(api_key="olira_prod_...")
128
+ token = client.get_patient_token(patient_id=patient_id)
129
+
130
+ print(token.access_token) # forward this to the patient device
131
+ print(token.expires_in) # 900 (seconds)
132
+ client.close()
133
+ ```
134
+
135
+ ---
136
+
137
+ ## Async client
138
+
139
+ All methods are available on `AsyncOliraClient` as coroutines:
140
+
141
+ ```python
142
+ import asyncio
143
+ from olira import AsyncOliraClient, OliraLogType
144
+
145
+ async def main():
146
+ async with AsyncOliraClient(api_key="olira_prod_...") as client:
147
+ patient = await client.create_patient(
148
+ first_name="Jane",
149
+ last_name="Smith",
150
+ )
151
+ await client.log(
152
+ log_type=OliraLogType.USER_LOGIN,
153
+ patient_id=patient.id,
154
+ )
155
+
156
+ asyncio.run(main())
157
+ ```
158
+
159
+ ---
160
+
161
+ ## Error handling
162
+
163
+ ```python
164
+ from olira import AuthError, ValidationError, RateLimitError, ServerError
165
+
166
+ try:
167
+ client.log_batch([...])
168
+ except AuthError:
169
+ # Invalid or revoked API key, or missing scope
170
+ ...
171
+ except ValidationError:
172
+ # Bad request (400/404/422) — e.g. unknown event type or missing required field
173
+ ...
174
+ except RateLimitError as e:
175
+ # Retry after e.retry_after seconds
176
+ ...
177
+ except ServerError:
178
+ # Transient server error after all retries exhausted
179
+ ...
180
+ ```
181
+
182
+ ---
183
+
184
+ ## Contributing
185
+
186
+ Dependencies are public PyPI packages only:
187
+
188
+ ```bash
189
+ bash scripts/install-dev.sh
190
+ ./scripts/pre-pr.sh
191
+ ```