ntro 0.1.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 (30) hide show
  1. ntro-0.1.0/CLAUDE-ntro-python.md +378 -0
  2. ntro-0.1.0/PKG-INFO +14 -0
  3. ntro-0.1.0/README.md +2 -0
  4. ntro-0.1.0/pyproject.toml +34 -0
  5. ntro-0.1.0/src/ntro/__init__.py +5 -0
  6. ntro-0.1.0/src/ntro/workspace/__init__.py +5 -0
  7. ntro-0.1.0/src/ntro/workspace/client.py +68 -0
  8. ntro-0.1.0/src/ntro/workspace/config.py +145 -0
  9. ntro-0.1.0/src/ntro/workspace/exceptions.py +45 -0
  10. ntro-0.1.0/src/ntro/workspace/http.py +132 -0
  11. ntro-0.1.0/src/ntro/workspace/models/__init__.py +1 -0
  12. ntro-0.1.0/src/ntro/workspace/models/common.py +51 -0
  13. ntro-0.1.0/src/ntro/workspace/models/deployment.py +26 -0
  14. ntro-0.1.0/src/ntro/workspace/models/entity.py +31 -0
  15. ntro-0.1.0/src/ntro/workspace/models/identity.py +13 -0
  16. ntro-0.1.0/src/ntro/workspace/models/integration.py +61 -0
  17. ntro-0.1.0/src/ntro/workspace/models/task.py +43 -0
  18. ntro-0.1.0/src/ntro/workspace/models/tenant.py +24 -0
  19. ntro-0.1.0/src/ntro/workspace/models/workflow.py +47 -0
  20. ntro-0.1.0/src/ntro/workspace/resources/__init__.py +1 -0
  21. ntro-0.1.0/src/ntro/workspace/resources/deployments.py +31 -0
  22. ntro-0.1.0/src/ntro/workspace/resources/entities.py +36 -0
  23. ntro-0.1.0/src/ntro/workspace/resources/identity.py +20 -0
  24. ntro-0.1.0/src/ntro/workspace/resources/integrations.py +81 -0
  25. ntro-0.1.0/src/ntro/workspace/resources/tasks.py +61 -0
  26. ntro-0.1.0/src/ntro/workspace/resources/tenants.py +37 -0
  27. ntro-0.1.0/src/ntro/workspace/resources/workflows.py +70 -0
  28. ntro-0.1.0/tests/__init__.py +0 -0
  29. ntro-0.1.0/tests/integration/__init__.py +0 -0
  30. ntro-0.1.0/tests/unit/__init__.py +0 -0
@@ -0,0 +1,378 @@
1
+ # ntro-python — Official Python SDK for the ntro platform
2
+
3
+ ## Project Overview
4
+
5
+ This is the official Python SDK for the **ntro** platform. It provides the `ntro` package on PyPI.
6
+
7
+ | Install | Import | What you get |
8
+ |---------|--------|-------------|
9
+ | `pip install ntro` | `from ntro.workspace import Client` | Workspace API client (control plane) |
10
+ | (future) | `from ntro.workflow import workflow, activity` | Workflow framework (data plane) |
11
+
12
+ **Today this repo contains `ntro.workspace` — the Python client for the Workspace API.** The workflow framework will be added later as `ntro.workflow` with optional dependency groups.
13
+
14
+ The ntro platform automates fund administration (NAV calculation, document extraction, GL classification) for private markets firms. The Workspace API is the control plane backend (a separate TypeScript/NestJS service). This SDK is the Python interface to that API.
15
+
16
+ **Naming convention:** follows the `{brand}-python` pattern used by Stripe (`stripe-python`), Auth0 (`auth0-python`), and Twilio (`twilio-python`). The repo is `ntro-python`, the PyPI package is `ntro`, the import is `from ntro import ...`.
17
+
18
+ ---
19
+
20
+ ## Architecture
21
+
22
+ ```
23
+ ┌─────────────┐ ┌─────────────┐
24
+ │ ntro-cli │ │ ntro-mcp │ ← Separate repos, thin interface layers
25
+ │ (Typer) │ │ (future) │
26
+ └──────┬──────┘ └──────┬──────┘
27
+ │ │
28
+ ▼ ▼
29
+ ┌─────────────────────────────┐
30
+ │ ntro (this package) │ ← httpx + Pydantic, async-first
31
+ │ ntro.workspace │
32
+ └──────────────┬──────────────┘
33
+ │ HTTP/REST
34
+
35
+ ┌─────────────────────────────┐
36
+ │ Workspace API (TypeScript) │ ← Separate repo (NestJS)
37
+ │ https://api.ntropii.com/v1│
38
+ └─────────────────────────────┘
39
+ ```
40
+
41
+ ### Related repos
42
+
43
+ | Repo | PyPI package | Binary | Purpose |
44
+ |------|-------------|--------|---------|
45
+ | **ntro-python** (this) | `ntro` | — | Python SDK |
46
+ | ntro-cli | `ntro-cli` | `ntro` | CLI tool |
47
+ | ntro-mcp | `ntro-mcp` | `ntro-mcp` | MCP server for Claude (future) |
48
+ | ntro-workspace-api | — | — | TypeScript/NestJS backend |
49
+
50
+ ---
51
+
52
+ ## Repository Structure
53
+
54
+ ```
55
+ ntro-python/
56
+ ├── CLAUDE.md # ← This file
57
+ ├── pyproject.toml # Package metadata, dependencies, build config
58
+ ├── README.md
59
+
60
+ ├── src/
61
+ │ └── ntro/
62
+ │ ├── __init__.py # Top-level exports
63
+ │ │
64
+ │ └── workspace/ # Workspace API client (control plane)
65
+ │ ├── __init__.py # Exports Client
66
+ │ ├── client.py # Main client class, resource mounting
67
+ │ ├── config.py # TOML config loading (~/.ntro/config.toml, env vars)
68
+ │ ├── auth.py # Auth handling (API key for PoC, OAuth future)
69
+ │ ├── exceptions.py # Typed exceptions (NtroAPIError, NotFoundError, etc.)
70
+ │ ├── http.py # httpx wrapper, retry logic, error mapping
71
+ │ │
72
+ │ ├── models/ # Pydantic v2 models (request/response DTOs)
73
+ │ │ ├── __init__.py
74
+ │ │ ├── common.py # Pagination, enums (Provider, Status, etc.)
75
+ │ │ ├── identity.py # UserProfile
76
+ │ │ ├── integration.py # DataPlatformConfig, EmailIntegration, SchemaInfo
77
+ │ │ ├── tenant.py # Tenant, CreateTenantRequest
78
+ │ │ ├── entity.py # Entity, CreateEntityRequest
79
+ │ │ ├── workflow.py # Workflow, WorkflowVersion, Capability
80
+ │ │ ├── deployment.py # Deployment, DeploymentStatus
81
+ │ │ └── task.py # Task, TaskStep, TaskStatus
82
+ │ │
83
+ │ └── resources/ # API resource classes (one per domain)
84
+ │ ├── __init__.py
85
+ │ ├── identity.py # IdentityResource — GET /me
86
+ │ ├── integrations.py # IntegrationsResource — /workspace/data/*, /workspace/integrations/*
87
+ │ ├── tenants.py # TenantsResource — /workspace/tenants/*
88
+ │ ├── entities.py # EntitiesResource — /workspace/entities, /workspace/tenants/{id}/entities
89
+ │ ├── workflows.py # WorkflowsResource — /workspace/registry/workflows/*
90
+ │ ├── deployments.py # DeploymentsResource — /workspace/registry/deployments/*
91
+ │ └── tasks.py # TasksResource — /workspace/tasks/*, /workspace/schedule, etc.
92
+
93
+ ├── tests/
94
+ │ ├── unit/ # Mocked HTTP via respx
95
+ │ └── integration/ # Against running API
96
+
97
+ └── docs/
98
+ ```
99
+
100
+ ---
101
+
102
+ ## Package Configuration
103
+
104
+ ```toml
105
+ # pyproject.toml
106
+ [project]
107
+ name = "ntro"
108
+ version = "0.1.0"
109
+ description = "Official Python SDK for the ntro platform"
110
+ requires-python = ">=3.11"
111
+ dependencies = [
112
+ "httpx>=0.27",
113
+ "pydantic>=2.7",
114
+ "tomli-w>=1.0",
115
+ ]
116
+
117
+ [project.optional-dependencies]
118
+ # Future: workflow framework deps
119
+ # workflow = ["temporalio>=1.7", "pdfplumber>=0.11"]
120
+ dev = [
121
+ "pytest>=8.0",
122
+ "pytest-asyncio>=0.23",
123
+ "respx>=0.21",
124
+ "ruff>=0.4",
125
+ "mypy>=1.10",
126
+ ]
127
+
128
+ [build-system]
129
+ requires = ["hatchling"]
130
+ build-backend = "hatchling.build"
131
+
132
+ [tool.hatch.build.targets.wheel]
133
+ packages = ["src/ntro"]
134
+ ```
135
+
136
+ ---
137
+
138
+ ## SDK Design
139
+
140
+ ### Client
141
+
142
+ ```python
143
+ from ntro.workspace import Client
144
+
145
+ # From config file (reads ~/.ntro/config.toml)
146
+ client = Client.from_config() # uses default connection
147
+ client = Client.from_config(connection="staging") # specific connection
148
+
149
+ # Explicit (for scripting / testing)
150
+ client = Client(
151
+ host="http://localhost:3000/v1",
152
+ api_key="ntro_test_key",
153
+ )
154
+
155
+ # Async (MCP server, notebooks)
156
+ tenant = await client.tenants.create(name="Acme", slug="acme", data_platform_config_id="dpc_123")
157
+
158
+ # Sync (CLI, simple scripts)
159
+ tenant = client.tenants.create_sync(name="Acme", slug="acme", data_platform_config_id="dpc_123")
160
+ ```
161
+
162
+ ### Resource Pattern
163
+
164
+ ```python
165
+ # src/ntro/workspace/resources/tenants.py
166
+ from ntro.workspace.http import HttpClient
167
+ from ntro.workspace.models.tenant import Tenant, CreateTenantRequest
168
+
169
+ class TenantsResource:
170
+ def __init__(self, http: HttpClient):
171
+ self._http = http
172
+
173
+ async def create(self, *, name: str, slug: str, data_platform_config_id: str) -> Tenant:
174
+ dto = CreateTenantRequest(name=name, slug=slug, data_platform_config_id=data_platform_config_id)
175
+ response = await self._http.post("/workspace/tenants", json=dto.model_dump())
176
+ return Tenant.model_validate(response)
177
+
178
+ def create_sync(self, **kwargs) -> Tenant:
179
+ return asyncio.run(self.create(**kwargs))
180
+
181
+ async def list(self) -> list[Tenant]:
182
+ response = await self._http.get("/workspace/tenants")
183
+ return [Tenant.model_validate(t) for t in response]
184
+
185
+ def list_sync(self) -> list[Tenant]:
186
+ return asyncio.run(self.list())
187
+
188
+ async def get(self, id: str) -> Tenant:
189
+ response = await self._http.get(f"/workspace/tenants/{id}")
190
+ return Tenant.model_validate(response)
191
+
192
+ def get_sync(self, id: str) -> Tenant:
193
+ return asyncio.run(self.get(id))
194
+ ```
195
+
196
+ ### HTTP Client
197
+
198
+ Use **httpx** with:
199
+ - Async by default (`httpx.AsyncClient`)
200
+ - Sync wrappers using `asyncio.run()` for CLI consumers
201
+ - Base URL injection from config
202
+ - Auth header injection (`Authorization: Bearer <api_key>`)
203
+ - Automatic retry with exponential backoff for 5xx errors
204
+ - Error mapping: 404 → `NotFoundError`, 422 → `ValidationError`, 401 → `AuthenticationError`
205
+ - Request/response logging at DEBUG level
206
+
207
+ ### Pydantic Models
208
+
209
+ All request and response bodies are Pydantic v2 models:
210
+
211
+ ```python
212
+ # src/ntro/workspace/models/common.py
213
+ from enum import Enum
214
+
215
+ class Provider(str, Enum):
216
+ DATABRICKS = "DATABRICKS"
217
+ SNOWFLAKE = "SNOWFLAKE"
218
+ MICROSOFT_FABRIC = "MICROSOFT_FABRIC"
219
+ GCP_BIGQUERY = "GCP_BIGQUERY"
220
+
221
+ class TaskStatus(str, Enum):
222
+ PENDING = "PENDING"
223
+ IN_PROGRESS = "IN_PROGRESS"
224
+ BLOCKED = "BLOCKED"
225
+ COMPLETED = "COMPLETED"
226
+ FAILED = "FAILED"
227
+
228
+ class TenantStatus(str, Enum):
229
+ PROVISIONING = "PROVISIONING"
230
+ ACTIVE = "ACTIVE"
231
+ SUSPENDED = "SUSPENDED"
232
+ ```
233
+
234
+ ### Configuration (TOML, Snowflake CLI-style connections)
235
+
236
+ Config follows the Snowflake CLI convention (`config.toml` with `[connections.<n>]` sections).
237
+
238
+ **Config file:** `~/.ntro/config.toml`
239
+
240
+ ```toml
241
+ # ~/.ntro/config.toml
242
+
243
+ default_connection_name = "local"
244
+
245
+ [connections.local]
246
+ host = "http://localhost:3000/v1"
247
+ api_key = "ntro_dev_key"
248
+ default_tenant = "acme-fund-admin"
249
+
250
+ [connections.staging]
251
+ host = "https://staging.api.ntropii.com/v1"
252
+ api_key = "ntro_staging_key"
253
+ default_tenant = "test-fund-admin"
254
+
255
+ [connections.production]
256
+ host = "https://api.ntropii.com/v1"
257
+ api_key = "ntro_prod_key"
258
+ default_tenant = "acme-fund-admin"
259
+
260
+ [cli]
261
+ output_format = "table"
262
+
263
+ [cli.logs]
264
+ save_logs = true
265
+ level = "info"
266
+ path = "~/.ntro/logs"
267
+ ```
268
+
269
+ **Resolution priority:**
270
+ 1. Explicit constructor args
271
+ 2. Environment variables: `NTRO_HOST`, `NTRO_API_KEY`
272
+ 3. Per-connection env var overrides: `NTRO_CONNECTIONS_<n>_API_KEY`
273
+ 4. Named connection in config.toml
274
+ 5. `default_connection_name` in config.toml
275
+
276
+ **Config file location:**
277
+ 1. `NTRO_HOME` env var → `$NTRO_HOME/config.toml`
278
+ 2. `~/.ntro/config.toml`
279
+
280
+ Parsed with Python 3.11+ built-in `tomllib` (read) and `tomli_w` (write).
281
+
282
+ ---
283
+
284
+ ## Endpoint → SDK Method Mapping
285
+
286
+ ### Phase 0 — Bootstrap
287
+ | API Endpoint | SDK Method |
288
+ |---|---|
289
+ | `GET /me` | `client.identity.whoami()` |
290
+
291
+ ### Phase 1 — Data Platform & Integrations
292
+ | API Endpoint | SDK Method |
293
+ |---|---|
294
+ | `POST /workspace/data` | `client.integrations.create_data_platform()` |
295
+ | `GET /workspace/data` | `client.integrations.list_data_platforms()` |
296
+ | `GET /workspace/data/{id}` | `client.integrations.get_data_platform(id)` |
297
+ | `POST /workspace/data/{id}/test` | `client.integrations.test_connection(id)` |
298
+ | `GET /workspace/data/{id}/schemas` | `client.integrations.discover_schemas(id)` |
299
+ | `GET /workspace/data/{id}/tenants` | `client.integrations.list_platform_tenants(id)` |
300
+ | `POST /workspace/integrations/email` | `client.integrations.create_email(...)` |
301
+ | `GET /workspace/integrations` | `client.integrations.list_all()` |
302
+
303
+ ### Phase 2 — Tenant & Entity
304
+ | API Endpoint | SDK Method |
305
+ |---|---|
306
+ | `POST /workspace/tenants` | `client.tenants.create()` |
307
+ | `GET /workspace/tenants` | `client.tenants.list()` |
308
+ | `GET /workspace/tenants/{id}` | `client.tenants.get(id)` |
309
+ | `POST /workspace/tenants/{id}/entities` | `client.entities.create(tenant_id, ...)` |
310
+ | `GET /workspace/entities` | `client.entities.list()` |
311
+
312
+ ### Phase 3 — Workflow Registry
313
+ | API Endpoint | SDK Method |
314
+ |---|---|
315
+ | `POST /workspace/registry/workflows` | `client.workflows.create()` |
316
+ | `GET /workspace/registry/workflows` | `client.workflows.list()` |
317
+ | `GET /workspace/registry/workflows/{id}` | `client.workflows.get(id)` |
318
+ | `POST /workspace/registry/workflows/{id}/versions` | `client.workflows.push(id, artifact)` |
319
+ | `POST /workspace/registry/capabilities` | `client.workflows.create_capability()` |
320
+
321
+ ### Phase 4 — Deployment
322
+ | API Endpoint | SDK Method |
323
+ |---|---|
324
+ | `POST /workspace/registry/deployments` | `client.deployments.create()` |
325
+ | `GET /workspace/registry/deployments/{id}` | `client.deployments.get(id)` |
326
+
327
+ ### Phase 5 — Execution
328
+ | API Endpoint | SDK Method |
329
+ |---|---|
330
+ | `POST /workspace/tasks` | `client.tasks.create()` |
331
+ | `GET /workspace/tasks/{id}` | `client.tasks.get(id)` |
332
+ | `GET /workspace/schedule` | `client.tasks.list_schedule()` |
333
+ | `GET /workspace/tenants/{tid}/entities/{eid}/tasks` | `client.tasks.history(tenant_id, entity_id)` |
334
+ | `GET /workspace/incoming` | `client.tasks.list_incoming()` |
335
+ | `GET /workspace/pending` | `client.tasks.list_pending()` |
336
+
337
+ ---
338
+
339
+ ## Build Order
340
+
341
+ 1. **Scaffold** — pyproject.toml, directory structure, dev tooling
342
+ 2. **Core** — `http.py` (httpx wrapper), `config.py` (TOML loading), `auth.py`, `exceptions.py`
343
+ 3. **Phase 0** — `resources/identity.py` + `models/identity.py` → `client.identity.whoami()`
344
+ 4. **Phase 1** — `resources/integrations.py` + `models/integration.py`
345
+ 5. **Phase 2** — `resources/tenants.py` + `resources/entities.py` + models
346
+ 6. **Phase 3** — `resources/workflows.py` + models
347
+ 7. **Phase 4** — `resources/deployments.py` + models
348
+ 8. **Phase 5** — `resources/tasks.py` + models
349
+
350
+ Each phase: resource + models → unit tests (mocked HTTP via respx).
351
+
352
+ ---
353
+
354
+ ## PoC Scope
355
+
356
+ ### In scope
357
+ - All P0 SDK methods (18 endpoints)
358
+ - TOML config file support
359
+ - Async + sync wrappers
360
+ - Pydantic v2 models (hand-written)
361
+ - Unit tests with respx
362
+
363
+ ### Excluded
364
+ - `ntro.workflow` module (workflow framework — future)
365
+ - OAuth / OIDC auth (PoC uses API key)
366
+ - Auto-generated models from OpenAPI spec
367
+ - PyPI publishing
368
+
369
+ ---
370
+
371
+ ## Domain Context
372
+
373
+ - **Tenant** = client organisation (fund admin or asset manager). Contains entities.
374
+ - **Entity** = SPV or fund within a tenant. Own schema in customer's Databricks.
375
+ - **Workflow** = repeatable process (NAV calc, doc ingestion, period close). Registered, deployed, triggered.
376
+ - **Task** = running instance of a workflow, scoped to an entity.
377
+ - **Data Platform** = customer's Databricks/Snowflake/Fabric where financial data lives.
378
+ - **Built-in workflows** = `coa-import`, `document-ingest`, `nav-monthly`, `period-close`.
ntro-0.1.0/PKG-INFO ADDED
@@ -0,0 +1,14 @@
1
+ Metadata-Version: 2.4
2
+ Name: ntro
3
+ Version: 0.1.0
4
+ Summary: Official Python SDK for the ntro platform
5
+ Requires-Python: >=3.11
6
+ Requires-Dist: httpx>=0.27
7
+ Requires-Dist: pydantic>=2.7
8
+ Requires-Dist: tomli-w>=1.0
9
+ Provides-Extra: dev
10
+ Requires-Dist: mypy>=1.10; extra == 'dev'
11
+ Requires-Dist: pytest-asyncio>=0.23; extra == 'dev'
12
+ Requires-Dist: pytest>=8.0; extra == 'dev'
13
+ Requires-Dist: respx>=0.21; extra == 'dev'
14
+ Requires-Dist: ruff>=0.4; extra == 'dev'
ntro-0.1.0/README.md ADDED
@@ -0,0 +1,2 @@
1
+ # ntro-python
2
+ Python library for ntropii framework
@@ -0,0 +1,34 @@
1
+ [project]
2
+ name = "ntro"
3
+ version = "0.1.0"
4
+ description = "Official Python SDK for the ntro platform"
5
+ requires-python = ">=3.11"
6
+ dependencies = [
7
+ "httpx>=0.27",
8
+ "pydantic>=2.7",
9
+ "tomli-w>=1.0",
10
+ ]
11
+
12
+ [project.optional-dependencies]
13
+ dev = [
14
+ "pytest>=8.0",
15
+ "pytest-asyncio>=0.23",
16
+ "respx>=0.21",
17
+ "ruff>=0.4",
18
+ "mypy>=1.10",
19
+ ]
20
+
21
+ [build-system]
22
+ requires = ["hatchling"]
23
+ build-backend = "hatchling.build"
24
+
25
+ [tool.hatch.build.targets.wheel]
26
+ packages = ["src/ntro"]
27
+
28
+ [tool.ruff]
29
+ line-length = 100
30
+ target-version = "py311"
31
+
32
+ [tool.mypy]
33
+ python_version = "3.11"
34
+ strict = true
@@ -0,0 +1,5 @@
1
+ """ntro — Official Python SDK for the ntro platform."""
2
+
3
+ from ntro.workspace import Client
4
+
5
+ __all__ = ["Client"]
@@ -0,0 +1,5 @@
1
+ """ntro.workspace — Workspace API client (control plane)."""
2
+
3
+ from ntro.workspace.client import Client
4
+
5
+ __all__ = ["Client"]
@@ -0,0 +1,68 @@
1
+ """Main Client class for the ntro Workspace API."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import os
6
+
7
+ from ntro.workspace.config import ConnectionConfig, load_config
8
+ from ntro.workspace.http import HttpClient
9
+ from ntro.workspace.resources.deployments import DeploymentsResource
10
+ from ntro.workspace.resources.entities import EntitiesResource
11
+ from ntro.workspace.resources.identity import IdentityResource
12
+ from ntro.workspace.resources.integrations import IntegrationsResource
13
+ from ntro.workspace.resources.tasks import TasksResource
14
+ from ntro.workspace.resources.tenants import TenantsResource
15
+ from ntro.workspace.resources.workflows import WorkflowsResource
16
+
17
+
18
+ class Client:
19
+ """Workspace API client.
20
+
21
+ Usage:
22
+ # From config file (reads ~/.ntro/config.toml)
23
+ client = Client.from_config()
24
+ client = Client.from_config(connection="staging")
25
+
26
+ # Explicit (for scripting / testing)
27
+ client = Client(host="http://localhost:3000/v1", api_key="ntro_dev_key")
28
+ """
29
+
30
+ def __init__(
31
+ self,
32
+ host: str = "https://api.ntropii.com/v1",
33
+ api_key: str = "",
34
+ debug: bool = False,
35
+ ) -> None:
36
+ self._http = HttpClient(base_url=host, api_key=api_key, debug=debug)
37
+ self.identity = IdentityResource(self._http)
38
+ self.integrations = IntegrationsResource(self._http)
39
+ self.tenants = TenantsResource(self._http)
40
+ self.entities = EntitiesResource(self._http)
41
+ self.workflows = WorkflowsResource(self._http)
42
+ self.deployments = DeploymentsResource(self._http)
43
+ self.tasks = TasksResource(self._http)
44
+
45
+ @classmethod
46
+ def from_config(
47
+ cls,
48
+ connection: str | None = None,
49
+ debug: bool = False,
50
+ ) -> "Client":
51
+ """Create a Client from ~/.ntro/config.toml.
52
+
53
+ Resolution priority:
54
+ 1. NTRO_HOST / NTRO_API_KEY env vars (override config)
55
+ 2. Named connection (--connection flag or NTRO_DEFAULT_CONNECTION_NAME env var)
56
+ 3. default_connection_name in config.toml
57
+ """
58
+ conn_name = connection or os.environ.get("NTRO_DEFAULT_CONNECTION_NAME")
59
+ config = load_config()
60
+ conn: ConnectionConfig = config.get_connection(conn_name)
61
+
62
+ host = os.environ.get("NTRO_HOST") or conn.host
63
+ api_key = os.environ.get("NTRO_API_KEY") or conn.api_key
64
+
65
+ return cls(host=host, api_key=api_key, debug=debug)
66
+
67
+ async def close(self) -> None:
68
+ await self._http.close()
@@ -0,0 +1,145 @@
1
+ """TOML-based config loading for ntro SDK.
2
+
3
+ Config file: ~/.ntro/config.toml (or $NTRO_HOME/config.toml)
4
+
5
+ Example:
6
+ default_connection_name = "local"
7
+
8
+ [connections.local]
9
+ host = "http://localhost:3000/v1"
10
+ api_key = "ntro_dev_key"
11
+ default_tenant = "acme-fund-admin"
12
+
13
+ [connections.production]
14
+ host = "https://api.ntropii.com/v1"
15
+ api_key = "ntro_prod_key"
16
+ """
17
+
18
+ from __future__ import annotations
19
+
20
+ import os
21
+ import tomllib
22
+ from dataclasses import dataclass, field
23
+ from pathlib import Path
24
+
25
+ import tomli_w
26
+
27
+ from ntro.workspace.exceptions import ConfigError
28
+
29
+ DEFAULT_HOST = "https://api.ntropii.com/v1"
30
+
31
+
32
+ @dataclass
33
+ class ConnectionConfig:
34
+ name: str
35
+ host: str = DEFAULT_HOST
36
+ api_key: str = ""
37
+ default_tenant: str | None = None
38
+ default_entity: str | None = None
39
+
40
+
41
+ @dataclass
42
+ class NtroConfig:
43
+ default_connection_name: str = "production"
44
+ connections: dict[str, ConnectionConfig] = field(default_factory=dict)
45
+
46
+ def get_connection(self, name: str | None = None) -> ConnectionConfig:
47
+ target = name or self.default_connection_name
48
+ if target not in self.connections:
49
+ raise ConfigError(
50
+ f"Connection '{target}' not found in config. "
51
+ f"Available: {list(self.connections.keys()) or ['(none)']}\n"
52
+ f"Run 'ntro auth login' to configure a connection."
53
+ )
54
+ return self.connections[target]
55
+
56
+
57
+ def _config_path() -> Path:
58
+ ntro_home = os.environ.get("NTRO_HOME")
59
+ if ntro_home:
60
+ return Path(ntro_home) / "config.toml"
61
+ return Path.home() / ".ntro" / "config.toml"
62
+
63
+
64
+ def load_config() -> NtroConfig:
65
+ """Load config from disk. Returns an empty config (production defaults) if missing."""
66
+ path = _config_path()
67
+ if not path.exists():
68
+ return NtroConfig(
69
+ default_connection_name="production",
70
+ connections={
71
+ "production": ConnectionConfig(
72
+ name="production",
73
+ host=DEFAULT_HOST,
74
+ api_key="",
75
+ )
76
+ },
77
+ )
78
+
79
+ with open(path, "rb") as f:
80
+ data = tomllib.load(f)
81
+
82
+ connections: dict[str, ConnectionConfig] = {}
83
+ for conn_name, conn_data in data.get("connections", {}).items():
84
+ connections[conn_name] = ConnectionConfig(
85
+ name=conn_name,
86
+ host=conn_data.get("host", DEFAULT_HOST),
87
+ api_key=conn_data.get("api_key", ""),
88
+ default_tenant=conn_data.get("default_tenant"),
89
+ default_entity=conn_data.get("default_entity"),
90
+ )
91
+
92
+ return NtroConfig(
93
+ default_connection_name=data.get("default_connection_name", "production"),
94
+ connections=connections,
95
+ )
96
+
97
+
98
+ def save_config(config: NtroConfig) -> None:
99
+ """Write config back to disk."""
100
+ path = _config_path()
101
+ path.parent.mkdir(parents=True, exist_ok=True)
102
+
103
+ data: dict[str, object] = {
104
+ "default_connection_name": config.default_connection_name,
105
+ "connections": {},
106
+ }
107
+
108
+ conns: dict[str, dict[str, object]] = {}
109
+ for name, conn in config.connections.items():
110
+ entry: dict[str, object] = {"host": conn.host, "api_key": conn.api_key}
111
+ if conn.default_tenant:
112
+ entry["default_tenant"] = conn.default_tenant
113
+ if conn.default_entity:
114
+ entry["default_entity"] = conn.default_entity
115
+ conns[name] = entry
116
+
117
+ data["connections"] = conns
118
+
119
+ with open(path, "wb") as f:
120
+ tomli_w.dump(data, f)
121
+
122
+
123
+ def write_default_config() -> Path:
124
+ """Write the default config file (local + production connections) if it doesn't exist."""
125
+ path = _config_path()
126
+ if path.exists():
127
+ return path
128
+
129
+ config = NtroConfig(
130
+ default_connection_name="local",
131
+ connections={
132
+ "local": ConnectionConfig(
133
+ name="local",
134
+ host="http://localhost:3000/v1",
135
+ api_key="ntro_dev_key",
136
+ ),
137
+ "production": ConnectionConfig(
138
+ name="production",
139
+ host=DEFAULT_HOST,
140
+ api_key="",
141
+ ),
142
+ },
143
+ )
144
+ save_config(config)
145
+ return path