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.
- ntro-0.1.0/CLAUDE-ntro-python.md +378 -0
- ntro-0.1.0/PKG-INFO +14 -0
- ntro-0.1.0/README.md +2 -0
- ntro-0.1.0/pyproject.toml +34 -0
- ntro-0.1.0/src/ntro/__init__.py +5 -0
- ntro-0.1.0/src/ntro/workspace/__init__.py +5 -0
- ntro-0.1.0/src/ntro/workspace/client.py +68 -0
- ntro-0.1.0/src/ntro/workspace/config.py +145 -0
- ntro-0.1.0/src/ntro/workspace/exceptions.py +45 -0
- ntro-0.1.0/src/ntro/workspace/http.py +132 -0
- ntro-0.1.0/src/ntro/workspace/models/__init__.py +1 -0
- ntro-0.1.0/src/ntro/workspace/models/common.py +51 -0
- ntro-0.1.0/src/ntro/workspace/models/deployment.py +26 -0
- ntro-0.1.0/src/ntro/workspace/models/entity.py +31 -0
- ntro-0.1.0/src/ntro/workspace/models/identity.py +13 -0
- ntro-0.1.0/src/ntro/workspace/models/integration.py +61 -0
- ntro-0.1.0/src/ntro/workspace/models/task.py +43 -0
- ntro-0.1.0/src/ntro/workspace/models/tenant.py +24 -0
- ntro-0.1.0/src/ntro/workspace/models/workflow.py +47 -0
- ntro-0.1.0/src/ntro/workspace/resources/__init__.py +1 -0
- ntro-0.1.0/src/ntro/workspace/resources/deployments.py +31 -0
- ntro-0.1.0/src/ntro/workspace/resources/entities.py +36 -0
- ntro-0.1.0/src/ntro/workspace/resources/identity.py +20 -0
- ntro-0.1.0/src/ntro/workspace/resources/integrations.py +81 -0
- ntro-0.1.0/src/ntro/workspace/resources/tasks.py +61 -0
- ntro-0.1.0/src/ntro/workspace/resources/tenants.py +37 -0
- ntro-0.1.0/src/ntro/workspace/resources/workflows.py +70 -0
- ntro-0.1.0/tests/__init__.py +0 -0
- ntro-0.1.0/tests/integration/__init__.py +0 -0
- 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,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,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
|