ntro-cli 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_cli-0.1.0/CLAUDE-ntro-cli.md +421 -0
- ntro_cli-0.1.0/PKG-INFO +12 -0
- ntro_cli-0.1.0/README.md +1 -0
- ntro_cli-0.1.0/pyproject.toml +35 -0
- ntro_cli-0.1.0/src/ntro_cli/__init__.py +0 -0
- ntro_cli-0.1.0/src/ntro_cli/commands/__init__.py +0 -0
- ntro_cli-0.1.0/src/ntro_cli/commands/auth.py +107 -0
- ntro_cli-0.1.0/src/ntro_cli/commands/entity.py +93 -0
- ntro_cli-0.1.0/src/ntro_cli/commands/integration.py +131 -0
- ntro_cli-0.1.0/src/ntro_cli/commands/run.py +84 -0
- ntro_cli-0.1.0/src/ntro_cli/commands/tenant.py +62 -0
- ntro_cli-0.1.0/src/ntro_cli/commands/workflow.py +205 -0
- ntro_cli-0.1.0/src/ntro_cli/context.py +33 -0
- ntro_cli-0.1.0/src/ntro_cli/helpers.py +21 -0
- ntro_cli-0.1.0/src/ntro_cli/main.py +70 -0
- ntro_cli-0.1.0/src/ntro_cli/output.py +104 -0
- ntro_cli-0.1.0/tests/__init__.py +0 -0
- ntro_cli-0.1.0/tests/integration/__init__.py +0 -0
- ntro_cli-0.1.0/tests/unit/__init__.py +0 -0
|
@@ -0,0 +1,421 @@
|
|
|
1
|
+
# ntro-cli — Command-line interface for the ntro platform
|
|
2
|
+
|
|
3
|
+
## Project Overview
|
|
4
|
+
|
|
5
|
+
This is the CLI for the **ntro** platform. It installs as `ntro-cli` on PyPI and registers the `ntro` binary on PATH.
|
|
6
|
+
|
|
7
|
+
| Install | Binary | What you get |
|
|
8
|
+
|---------|--------|-------------|
|
|
9
|
+
| `pip install ntro-cli` | `ntro` | CLI tool for managing the ntro platform |
|
|
10
|
+
|
|
11
|
+
The CLI is a thin interface layer over the `ntro` SDK package (`pip install ntro`). All API interaction goes through `ntro.workspace.Client`. The CLI handles argument parsing, output formatting, and interactive flows.
|
|
12
|
+
|
|
13
|
+
**Design principle:** Never put business logic or HTTP calls in the CLI. The SDK is the single source of truth for API interaction. Every CLI command is: parse args → call SDK method → format output.
|
|
14
|
+
|
|
15
|
+
---
|
|
16
|
+
|
|
17
|
+
## Architecture
|
|
18
|
+
|
|
19
|
+
```
|
|
20
|
+
┌─────────────────────────────┐
|
|
21
|
+
│ ntro-cli │ ← This repo (Typer + Rich)
|
|
22
|
+
│ binary: ntro │
|
|
23
|
+
└──────────────┬──────────────┘
|
|
24
|
+
│ imports
|
|
25
|
+
▼
|
|
26
|
+
┌─────────────────────────────┐
|
|
27
|
+
│ ntro (separate repo) │ ← ntro-python repo, pip install ntro
|
|
28
|
+
│ ntro.workspace.Client │
|
|
29
|
+
└──────────────┬──────────────┘
|
|
30
|
+
│ HTTP/REST
|
|
31
|
+
▼
|
|
32
|
+
┌─────────────────────────────┐
|
|
33
|
+
│ Workspace API (TypeScript) │ ← ntro-workspace-api repo
|
|
34
|
+
└─────────────────────────────┘
|
|
35
|
+
```
|
|
36
|
+
|
|
37
|
+
### Related repos
|
|
38
|
+
|
|
39
|
+
| Repo | PyPI | Binary | Purpose |
|
|
40
|
+
|------|------|--------|---------|
|
|
41
|
+
| ntro-python | `ntro` | — | Python SDK (dependency of this repo) |
|
|
42
|
+
| **ntro-cli** (this) | `ntro-cli` | `ntro` | CLI tool |
|
|
43
|
+
| ntro-mcp | `ntro-mcp` | `ntro-mcp` | MCP server for Claude (future, also depends on `ntro`) |
|
|
44
|
+
| ntro-workspace-api | — | — | TypeScript/NestJS backend |
|
|
45
|
+
|
|
46
|
+
---
|
|
47
|
+
|
|
48
|
+
## Repository Structure
|
|
49
|
+
|
|
50
|
+
```
|
|
51
|
+
ntro-cli/
|
|
52
|
+
├── CLAUDE.md # ← This file
|
|
53
|
+
├── pyproject.toml
|
|
54
|
+
├── README.md
|
|
55
|
+
│
|
|
56
|
+
├── src/
|
|
57
|
+
│ └── ntro_cli/
|
|
58
|
+
│ ├── __init__.py
|
|
59
|
+
│ ├── main.py # Root Typer app, global flags callback, mounts groups
|
|
60
|
+
│ ├── context.py # Connection-aware SDK client init, output format state
|
|
61
|
+
│ ├── output.py # Output formatters (Rich tables, JSON)
|
|
62
|
+
│ ├── helpers.py # load_json_input (@file or inline), common validators
|
|
63
|
+
│ │
|
|
64
|
+
│ └── commands/ # One module per command group
|
|
65
|
+
│ ├── __init__.py
|
|
66
|
+
│ ├── auth.py # ntro auth login|list|test|set-default|whoami
|
|
67
|
+
│ ├── integration.py # ntro integration add|list|info|test|discover|tenants
|
|
68
|
+
│ ├── tenant.py # ntro tenant create|list|info
|
|
69
|
+
│ ├── entity.py # ntro entity create|list
|
|
70
|
+
│ ├── workflow.py # ntro workflow create|list|info|push|deploy|deploy-status|run
|
|
71
|
+
│ └── run.py # ntro run status|list|history|incoming|pending
|
|
72
|
+
│
|
|
73
|
+
├── tests/
|
|
74
|
+
│ ├── unit/ # Mocked SDK
|
|
75
|
+
│ └── integration/ # Against running API
|
|
76
|
+
│
|
|
77
|
+
└── docs/
|
|
78
|
+
└── cli-reference.md
|
|
79
|
+
```
|
|
80
|
+
|
|
81
|
+
---
|
|
82
|
+
|
|
83
|
+
## Package Configuration
|
|
84
|
+
|
|
85
|
+
```toml
|
|
86
|
+
# pyproject.toml
|
|
87
|
+
[project]
|
|
88
|
+
name = "ntro-cli"
|
|
89
|
+
version = "0.1.0"
|
|
90
|
+
description = "CLI for the ntro platform"
|
|
91
|
+
requires-python = ">=3.11"
|
|
92
|
+
dependencies = [
|
|
93
|
+
"ntro>=0.1.0",
|
|
94
|
+
"typer>=0.12",
|
|
95
|
+
"rich>=13.0",
|
|
96
|
+
]
|
|
97
|
+
|
|
98
|
+
[project.scripts]
|
|
99
|
+
ntro = "ntro_cli.main:app"
|
|
100
|
+
|
|
101
|
+
[project.optional-dependencies]
|
|
102
|
+
dev = [
|
|
103
|
+
"pytest>=8.0",
|
|
104
|
+
"ruff>=0.4",
|
|
105
|
+
"mypy>=1.10",
|
|
106
|
+
]
|
|
107
|
+
|
|
108
|
+
[build-system]
|
|
109
|
+
requires = ["hatchling"]
|
|
110
|
+
build-backend = "hatchling.build"
|
|
111
|
+
|
|
112
|
+
[tool.hatch.build.targets.wheel]
|
|
113
|
+
packages = ["src/ntro_cli"]
|
|
114
|
+
```
|
|
115
|
+
|
|
116
|
+
---
|
|
117
|
+
|
|
118
|
+
## CLI Design
|
|
119
|
+
|
|
120
|
+
### Design Reference: Databricks CLI + Snowflake CLI
|
|
121
|
+
|
|
122
|
+
The ntro CLI borrows conventions from both CLIs that data engineers use daily:
|
|
123
|
+
|
|
124
|
+
- **From Snowflake CLI:** TOML config (`~/.ntro/config.toml`), `--connection` / `-c` flag,
|
|
125
|
+
interactive `login` flow with `--no-interactive`, `NTRO_CONNECTIONS_<n>_<FIELD>` env var pattern
|
|
126
|
+
- **From Databricks CLI:** `--json` flag for complex payloads (inline or `@path/to/file.json`),
|
|
127
|
+
`--output` for format switching (text/json), `--debug` flag
|
|
128
|
+
- **Common:** Command structure `ntro <group> <command> [args] [--flags]`, output defaults to
|
|
129
|
+
text tables, JSON for piping to `jq`
|
|
130
|
+
|
|
131
|
+
### Global Flags
|
|
132
|
+
|
|
133
|
+
Every command accepts these flags (via Typer root callback):
|
|
134
|
+
|
|
135
|
+
```python
|
|
136
|
+
# src/ntro_cli/main.py
|
|
137
|
+
import typer
|
|
138
|
+
from typing import Optional
|
|
139
|
+
from ntro_cli.commands import auth, integration, tenant, entity, workflow, run
|
|
140
|
+
|
|
141
|
+
app = typer.Typer(
|
|
142
|
+
name="ntro",
|
|
143
|
+
help="ntro platform CLI",
|
|
144
|
+
no_args_is_help=True,
|
|
145
|
+
)
|
|
146
|
+
|
|
147
|
+
@app.callback()
|
|
148
|
+
def main(
|
|
149
|
+
connection: Optional[str] = typer.Option(None, "-c", "--connection", help="Connection name from config.toml", envvar="NTRO_DEFAULT_CONNECTION_NAME"),
|
|
150
|
+
host: Optional[str] = typer.Option(None, help="API host URL", envvar="NTRO_HOST"),
|
|
151
|
+
output: str = typer.Option("text", "-o", "--output", help="Output format: text or json"),
|
|
152
|
+
debug: bool = typer.Option(False, "--debug", help="Enable debug logging"),
|
|
153
|
+
log_level: Optional[str] = typer.Option(None, "--log-level", help="Log level: DEBUG, INFO, WARN, ERROR"),
|
|
154
|
+
log_file: Optional[str] = typer.Option(None, "--log-file", help="Write logs to file"),
|
|
155
|
+
):
|
|
156
|
+
"""ntro platform CLI."""
|
|
157
|
+
# Store in Typer context for subcommands
|
|
158
|
+
...
|
|
159
|
+
|
|
160
|
+
app.add_typer(auth.app, name="auth")
|
|
161
|
+
app.add_typer(integration.app, name="integration")
|
|
162
|
+
app.add_typer(tenant.app, name="tenant")
|
|
163
|
+
app.add_typer(entity.app, name="entity")
|
|
164
|
+
app.add_typer(workflow.app, name="workflow")
|
|
165
|
+
app.add_typer(run.app, name="run")
|
|
166
|
+
```
|
|
167
|
+
|
|
168
|
+
Usage:
|
|
169
|
+
```bash
|
|
170
|
+
ntro -c staging tenant list --output json
|
|
171
|
+
ntro -c production -o json workflow list --tenant acme-fund-admin
|
|
172
|
+
ntro --debug integration test dpc_acme_dbx
|
|
173
|
+
```
|
|
174
|
+
|
|
175
|
+
### The `--json` Pattern
|
|
176
|
+
|
|
177
|
+
For write commands, support `--json` alongside individual flags. Accepts inline JSON or `@path/to/file.json` (Databricks convention):
|
|
178
|
+
|
|
179
|
+
```bash
|
|
180
|
+
# Individual flags
|
|
181
|
+
ntro tenant create --name "Acme Fund Admin" --slug acme --integration dpc_123
|
|
182
|
+
|
|
183
|
+
# Inline JSON
|
|
184
|
+
ntro tenant create --json '{"name": "Acme Fund Admin", "slug": "acme", "data_platform_config_id": "dpc_123"}'
|
|
185
|
+
|
|
186
|
+
# From file
|
|
187
|
+
ntro tenant create --json @./tenant-config.json
|
|
188
|
+
```
|
|
189
|
+
|
|
190
|
+
Essential for `ntro integration add databricks` which has many nested config fields.
|
|
191
|
+
|
|
192
|
+
### Command Pattern
|
|
193
|
+
|
|
194
|
+
Every command: parse args/json → resolve connection → get SDK client → call SDK method → format output.
|
|
195
|
+
|
|
196
|
+
```python
|
|
197
|
+
# src/ntro_cli/commands/tenant.py
|
|
198
|
+
import typer
|
|
199
|
+
from typing import Optional
|
|
200
|
+
from ntro_cli.context import get_client, output, load_json_input
|
|
201
|
+
|
|
202
|
+
app = typer.Typer(help="Manage tenant cells (clients)")
|
|
203
|
+
|
|
204
|
+
@app.command()
|
|
205
|
+
def create(
|
|
206
|
+
name: Optional[str] = typer.Option(None, help="Tenant name"),
|
|
207
|
+
slug: Optional[str] = typer.Option(None, help="URL-safe identifier"),
|
|
208
|
+
integration: Optional[str] = typer.Option(None, "--integration", help="Data platform config ID"),
|
|
209
|
+
json_input: Optional[str] = typer.Option(None, "--json", help="JSON payload (inline or @file)"),
|
|
210
|
+
):
|
|
211
|
+
"""Create a new tenant (client cell)."""
|
|
212
|
+
if json_input:
|
|
213
|
+
payload = load_json_input(json_input)
|
|
214
|
+
else:
|
|
215
|
+
if not all([name, slug, integration]):
|
|
216
|
+
raise typer.BadParameter("Provide --name, --slug, --integration or use --json")
|
|
217
|
+
payload = {"name": name, "slug": slug, "data_platform_config_id": integration}
|
|
218
|
+
|
|
219
|
+
client = get_client()
|
|
220
|
+
tenant = client.tenants.create_sync(**payload)
|
|
221
|
+
output(tenant, title=f"Tenant created: {tenant.slug}")
|
|
222
|
+
|
|
223
|
+
@app.command("list")
|
|
224
|
+
def list_tenants():
|
|
225
|
+
"""List all tenants."""
|
|
226
|
+
client = get_client()
|
|
227
|
+
tenants = client.tenants.list_sync()
|
|
228
|
+
output(tenants, columns=["slug", "name", "status", "region", "entityCount"])
|
|
229
|
+
|
|
230
|
+
@app.command()
|
|
231
|
+
def info(id: str = typer.Argument(help="Tenant slug or ID")):
|
|
232
|
+
"""Show tenant details."""
|
|
233
|
+
client = get_client()
|
|
234
|
+
tenant = client.tenants.get_sync(id)
|
|
235
|
+
output(tenant)
|
|
236
|
+
```
|
|
237
|
+
|
|
238
|
+
### Output Formatting
|
|
239
|
+
|
|
240
|
+
The `--output` / `-o` global flag controls format:
|
|
241
|
+
|
|
242
|
+
- **text** (default): Rich tables for lists, key-value panels for single objects
|
|
243
|
+
- **json**: Raw JSON for piping to `jq` or scripting
|
|
244
|
+
|
|
245
|
+
When `--output json` is set, output is the raw API response JSON — no wrapping, no decoration:
|
|
246
|
+
`ntro tenant list -o json | jq '.[].slug'`
|
|
247
|
+
|
|
248
|
+
### Auth Commands
|
|
249
|
+
|
|
250
|
+
| Command | What it does |
|
|
251
|
+
|---|---|
|
|
252
|
+
| `ntro auth login` | Interactively add/update a connection. Prompts for host, API key, default tenant. `--no-interactive` for CI/CD. Writes to `~/.ntro/config.toml`. |
|
|
253
|
+
| `ntro auth list` | List all configured connections with status |
|
|
254
|
+
| `ntro auth test` | Test the active connection (or `-c staging` to test a specific one) |
|
|
255
|
+
| `ntro auth set-default <n>` | Change the default connection |
|
|
256
|
+
| `ntro auth whoami` | Verify identity, calls `GET /me` |
|
|
257
|
+
|
|
258
|
+
### `--tenant` and `--entity` Default Resolution
|
|
259
|
+
|
|
260
|
+
Many commands require `--tenant`. Resolution order:
|
|
261
|
+
1. `--tenant` flag (explicit)
|
|
262
|
+
2. `NTRO_TENANT` env var
|
|
263
|
+
3. `default_tenant` in active connection (`~/.ntro/config.toml`)
|
|
264
|
+
|
|
265
|
+
Same for `--entity`: `--entity` flag > `NTRO_ENTITY` > connection's `default_entity`.
|
|
266
|
+
|
|
267
|
+
After `ntro auth login`, most commands work without flags: `ntro entity list`, `ntro workflow list`.
|
|
268
|
+
|
|
269
|
+
### `ntro integration add` is Polymorphic
|
|
270
|
+
|
|
271
|
+
```python
|
|
272
|
+
# src/ntro_cli/commands/integration.py
|
|
273
|
+
app = typer.Typer(help="Manage data platforms and integrations")
|
|
274
|
+
add_app = typer.Typer(help="Add a new integration")
|
|
275
|
+
app.add_typer(add_app, name="add")
|
|
276
|
+
|
|
277
|
+
@add_app.command()
|
|
278
|
+
def databricks(
|
|
279
|
+
name: str = typer.Option(None),
|
|
280
|
+
workspace_url: str = typer.Option(None),
|
|
281
|
+
catalog: str = typer.Option(None),
|
|
282
|
+
json_input: Optional[str] = typer.Option(None, "--json"),
|
|
283
|
+
# ...
|
|
284
|
+
):
|
|
285
|
+
"""Register a Databricks data platform."""
|
|
286
|
+
...
|
|
287
|
+
|
|
288
|
+
@add_app.command()
|
|
289
|
+
def email(
|
|
290
|
+
tenant: str = typer.Option(None),
|
|
291
|
+
provider: str = typer.Option("microsoft-graph"),
|
|
292
|
+
json_input: Optional[str] = typer.Option(None, "--json"),
|
|
293
|
+
# ...
|
|
294
|
+
):
|
|
295
|
+
"""Add an email integration."""
|
|
296
|
+
...
|
|
297
|
+
```
|
|
298
|
+
|
|
299
|
+
### Workflow Run — Special Case
|
|
300
|
+
|
|
301
|
+
`ntro workflow run` handles file uploads and optional polling:
|
|
302
|
+
|
|
303
|
+
```python
|
|
304
|
+
@app.command("run")
|
|
305
|
+
def run_workflow(
|
|
306
|
+
name: str = typer.Argument(help="Workflow name (e.g., nav-monthly, coa-import)"),
|
|
307
|
+
tenant: str = typer.Option(None, help="Tenant slug"),
|
|
308
|
+
entity: str = typer.Option(None, help="Entity slug"),
|
|
309
|
+
period: str = typer.Option(None, help="Accounting period (e.g., 2026-03)"),
|
|
310
|
+
file: Path = typer.Option(None, exists=True, help="File to upload"),
|
|
311
|
+
dry_run: bool = typer.Option(False, help="Validate without committing"),
|
|
312
|
+
wait: bool = typer.Option(False, help="Poll until completion"),
|
|
313
|
+
):
|
|
314
|
+
"""Trigger a workflow run."""
|
|
315
|
+
client = get_client()
|
|
316
|
+
task = client.tasks.create_sync(
|
|
317
|
+
tenant_id=tenant, entity_id=entity, workflow_id=name,
|
|
318
|
+
context={"period": period, "dry_run": dry_run}, file=file,
|
|
319
|
+
)
|
|
320
|
+
output(task, title=f"Workflow run started: {task.id}")
|
|
321
|
+
if wait:
|
|
322
|
+
poll_task(client, task.id)
|
|
323
|
+
```
|
|
324
|
+
|
|
325
|
+
---
|
|
326
|
+
|
|
327
|
+
## CLI Command Groups
|
|
328
|
+
|
|
329
|
+
| Group | Purpose | Key commands |
|
|
330
|
+
|-------|---------|-------------|
|
|
331
|
+
| `ntro auth` | Connection management + identity | `login`, `list`, `test`, `set-default`, `whoami` |
|
|
332
|
+
| `ntro integration` | Data platforms + email sources | `add databricks`, `add email`, `list`, `info`, `test`, `discover` |
|
|
333
|
+
| `ntro tenant` | Client cells | `create`, `list`, `info` |
|
|
334
|
+
| `ntro entity` | SPVs/funds within tenants | `create`, `list` |
|
|
335
|
+
| `ntro workflow` | Definition, deployment, triggering | `create`, `list`, `info`, `push`, `deploy`, `deploy-status`, `run` |
|
|
336
|
+
| `ntro run` | Inspecting executions | `status`, `list`, `history`, `incoming`, `pending` |
|
|
337
|
+
|
|
338
|
+
**Key pattern:** `ntro workflow run` = verb ("run this workflow"), `ntro run` = noun ("show me this run").
|
|
339
|
+
|
|
340
|
+
---
|
|
341
|
+
|
|
342
|
+
## Full CLI → SDK → API Mapping
|
|
343
|
+
|
|
344
|
+
| CLI Command | SDK Method | API Endpoint |
|
|
345
|
+
|---|---|---|
|
|
346
|
+
| `ntro auth whoami` | `client.identity.whoami()` | `GET /me` |
|
|
347
|
+
| `ntro integration add databricks` | `client.integrations.create_data_platform()` | `POST /workspace/data` |
|
|
348
|
+
| `ntro integration list` | `client.integrations.list_data_platforms()` | `GET /workspace/data` |
|
|
349
|
+
| `ntro integration info <id>` | `client.integrations.get_data_platform(id)` | `GET /workspace/data/{id}` |
|
|
350
|
+
| `ntro integration test <id>` | `client.integrations.test_connection(id)` | `POST /workspace/data/{id}/test` |
|
|
351
|
+
| `ntro integration discover <id>` | `client.integrations.discover_schemas(id)` | `GET /workspace/data/{id}/schemas` |
|
|
352
|
+
| `ntro tenant create` | `client.tenants.create()` | `POST /workspace/tenants` |
|
|
353
|
+
| `ntro tenant list` | `client.tenants.list()` | `GET /workspace/tenants` |
|
|
354
|
+
| `ntro tenant info <id>` | `client.tenants.get(id)` | `GET /workspace/tenants/{id}` |
|
|
355
|
+
| `ntro entity create` | `client.entities.create(tenant_id, ...)` | `POST /workspace/tenants/{id}/entities` |
|
|
356
|
+
| `ntro entity list` | `client.entities.list()` | `GET /workspace/entities` |
|
|
357
|
+
| `ntro workflow create` | `client.workflows.create()` | `POST /workspace/registry/workflows` |
|
|
358
|
+
| `ntro workflow list` | `client.workflows.list()` | `GET /workspace/registry/workflows` |
|
|
359
|
+
| `ntro workflow info <id>` | `client.workflows.get(id)` | `GET /workspace/registry/workflows/{id}` |
|
|
360
|
+
| `ntro workflow push <id>` | `client.workflows.push(id, artifact)` | `POST /workspace/registry/workflows/{id}/versions` |
|
|
361
|
+
| `ntro workflow deploy` | `client.deployments.create()` | `POST /workspace/registry/deployments` |
|
|
362
|
+
| `ntro workflow deploy-status <id>` | `client.deployments.get(id)` | `GET /workspace/registry/deployments/{id}` |
|
|
363
|
+
| `ntro workflow run <n>` | `client.tasks.create()` | `POST /workspace/tasks` |
|
|
364
|
+
| `ntro run status <id>` | `client.tasks.get(id)` | `GET /workspace/tasks/{id}` |
|
|
365
|
+
| `ntro run list` | `client.tasks.list_schedule()` | `GET /workspace/schedule` |
|
|
366
|
+
| `ntro run history` | `client.tasks.history(t, e)` | `GET /workspace/tenants/{t}/entities/{e}/tasks` |
|
|
367
|
+
|
|
368
|
+
---
|
|
369
|
+
|
|
370
|
+
## Dependencies
|
|
371
|
+
|
|
372
|
+
- **ntro** >= 0.1.0 — the SDK (from ntro-python repo)
|
|
373
|
+
- **typer** >= 0.12 — CLI framework
|
|
374
|
+
- **rich** >= 13.0 — terminal formatting (tables, panels, spinners)
|
|
375
|
+
|
|
376
|
+
Dev: pytest, ruff, mypy
|
|
377
|
+
|
|
378
|
+
---
|
|
379
|
+
|
|
380
|
+
## Build Order
|
|
381
|
+
|
|
382
|
+
1. **Scaffold** — pyproject.toml, directory structure, dev tooling
|
|
383
|
+
2. **CLI core** — `main.py` (root app + global flags), `context.py` (get_client), `output.py` (Rich), `helpers.py` (json loading)
|
|
384
|
+
3. **Phase 0** — `commands/auth.py` → `ntro auth whoami` (validates full stack: CLI → SDK → API)
|
|
385
|
+
4. **Phase 1** — `commands/integration.py` → `ntro integration add|list|info|test|discover`
|
|
386
|
+
5. **Phase 2** — `commands/tenant.py` + `commands/entity.py`
|
|
387
|
+
6. **Phase 3** — `commands/workflow.py` → `create|list|info|push`
|
|
388
|
+
7. **Phase 4** — `commands/workflow.py` → `deploy|deploy-status`
|
|
389
|
+
8. **Phase 5** — `commands/workflow.py` → `run` + `commands/run.py` → `status|list|history`
|
|
390
|
+
|
|
391
|
+
Each phase: CLI commands → tests (mocked SDK).
|
|
392
|
+
|
|
393
|
+
**Important:** The SDK (`ntro` package) must be built first. During development, install it in editable mode: `pip install -e ../ntro-python` or point to a local path in pyproject.toml.
|
|
394
|
+
|
|
395
|
+
---
|
|
396
|
+
|
|
397
|
+
## PoC Scope
|
|
398
|
+
|
|
399
|
+
### In scope
|
|
400
|
+
- All P0 CLI commands (18 endpoints)
|
|
401
|
+
- `ntro auth login` (interactive + `--no-interactive`)
|
|
402
|
+
- `ntro auth whoami`
|
|
403
|
+
- TOML config support (`~/.ntro/config.toml`)
|
|
404
|
+
- Table + JSON output
|
|
405
|
+
- `--json` for write commands
|
|
406
|
+
- Basic error handling with user-friendly messages
|
|
407
|
+
|
|
408
|
+
### Excluded
|
|
409
|
+
- Shell completion
|
|
410
|
+
- Ledger commands (`ntro ledger` — deferred)
|
|
411
|
+
- PyPI publishing
|
|
412
|
+
|
|
413
|
+
---
|
|
414
|
+
|
|
415
|
+
## Domain Context
|
|
416
|
+
|
|
417
|
+
- **Tenant** = client organisation. Contains entities. `--tenant` flag or config default.
|
|
418
|
+
- **Entity** = SPV or fund within a tenant. `--entity` flag or config default.
|
|
419
|
+
- **Workflow** = repeatable process. `ntro workflow` manages definitions, `ntro workflow run` triggers execution.
|
|
420
|
+
- **Task** = running workflow instance. `ntro run` inspects execution status.
|
|
421
|
+
- **Built-in workflows** = `coa-import`, `document-ingest`, `nav-monthly`, `period-close`. Triggered via `ntro workflow run <name>`.
|
ntro_cli-0.1.0/PKG-INFO
ADDED
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: ntro-cli
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: CLI for the ntro platform
|
|
5
|
+
Requires-Python: >=3.11
|
|
6
|
+
Requires-Dist: ntro>=0.1.0
|
|
7
|
+
Requires-Dist: rich>=13.0
|
|
8
|
+
Requires-Dist: typer>=0.12
|
|
9
|
+
Provides-Extra: dev
|
|
10
|
+
Requires-Dist: mypy>=1.10; extra == 'dev'
|
|
11
|
+
Requires-Dist: pytest>=8.0; extra == 'dev'
|
|
12
|
+
Requires-Dist: ruff>=0.4; extra == 'dev'
|
ntro_cli-0.1.0/README.md
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
# ntro-cli
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
[project]
|
|
2
|
+
name = "ntro-cli"
|
|
3
|
+
version = "0.1.0"
|
|
4
|
+
description = "CLI for the ntro platform"
|
|
5
|
+
requires-python = ">=3.11"
|
|
6
|
+
dependencies = [
|
|
7
|
+
"ntro>=0.1.0",
|
|
8
|
+
"typer>=0.12",
|
|
9
|
+
"rich>=13.0",
|
|
10
|
+
]
|
|
11
|
+
|
|
12
|
+
[project.scripts]
|
|
13
|
+
ntro = "ntro_cli.main:app"
|
|
14
|
+
|
|
15
|
+
[project.optional-dependencies]
|
|
16
|
+
dev = [
|
|
17
|
+
"pytest>=8.0",
|
|
18
|
+
"ruff>=0.4",
|
|
19
|
+
"mypy>=1.10",
|
|
20
|
+
]
|
|
21
|
+
|
|
22
|
+
[build-system]
|
|
23
|
+
requires = ["hatchling"]
|
|
24
|
+
build-backend = "hatchling.build"
|
|
25
|
+
|
|
26
|
+
[tool.hatch.build.targets.wheel]
|
|
27
|
+
packages = ["src/ntro_cli"]
|
|
28
|
+
|
|
29
|
+
[tool.ruff]
|
|
30
|
+
line-length = 100
|
|
31
|
+
target-version = "py311"
|
|
32
|
+
|
|
33
|
+
[tool.mypy]
|
|
34
|
+
python_version = "3.11"
|
|
35
|
+
strict = true
|
|
File without changes
|
|
File without changes
|
|
@@ -0,0 +1,107 @@
|
|
|
1
|
+
"""ntro auth — connection management and identity."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from typing import Optional
|
|
6
|
+
|
|
7
|
+
import typer
|
|
8
|
+
|
|
9
|
+
from ntro.workspace.config import load_config, save_config, write_default_config
|
|
10
|
+
from ntro.workspace.exceptions import NtroError
|
|
11
|
+
from ntro_cli import output as out
|
|
12
|
+
from ntro_cli.context import get_client
|
|
13
|
+
|
|
14
|
+
app = typer.Typer(help="Manage connections and verify identity")
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
@app.command()
|
|
18
|
+
def login(
|
|
19
|
+
name: str = typer.Option("local", "--name", "-n", help="Connection name"),
|
|
20
|
+
host: str = typer.Option("http://localhost:3000/v1", "--host", help="API host URL"),
|
|
21
|
+
api_key: Optional[str] = typer.Option(None, "--api-key", help="API key"),
|
|
22
|
+
default_tenant: Optional[str] = typer.Option(None, "--tenant", help="Default tenant slug"),
|
|
23
|
+
set_default: bool = typer.Option(True, "--set-default/--no-set-default", help="Set as default connection"),
|
|
24
|
+
no_interactive: bool = typer.Option(False, "--no-interactive", help="Skip prompts (use flags only)"),
|
|
25
|
+
) -> None:
|
|
26
|
+
"""Add or update a connection in ~/.ntro/config.toml."""
|
|
27
|
+
write_default_config()
|
|
28
|
+
config = load_config()
|
|
29
|
+
|
|
30
|
+
if not no_interactive:
|
|
31
|
+
name = typer.prompt("Connection name", default=name)
|
|
32
|
+
host = typer.prompt("API host", default=host)
|
|
33
|
+
api_key = typer.prompt("API key", default=api_key or "", hide_input=True) or None
|
|
34
|
+
default_tenant = typer.prompt("Default tenant (optional)", default=default_tenant or "", show_default=False) or None
|
|
35
|
+
|
|
36
|
+
from ntro.workspace.config import ConnectionConfig
|
|
37
|
+
config.connections[name] = ConnectionConfig(
|
|
38
|
+
name=name,
|
|
39
|
+
host=host,
|
|
40
|
+
api_key=api_key or "",
|
|
41
|
+
default_tenant=default_tenant,
|
|
42
|
+
)
|
|
43
|
+
|
|
44
|
+
if set_default:
|
|
45
|
+
config.default_connection_name = name
|
|
46
|
+
|
|
47
|
+
save_config(config)
|
|
48
|
+
out.print_success(f"Connection '{name}' saved → {host}")
|
|
49
|
+
if set_default:
|
|
50
|
+
out.print_success(f"Default connection set to '{name}'")
|
|
51
|
+
|
|
52
|
+
|
|
53
|
+
@app.command("list")
|
|
54
|
+
def list_connections() -> None:
|
|
55
|
+
"""List all configured connections."""
|
|
56
|
+
config = load_config()
|
|
57
|
+
if not config.connections:
|
|
58
|
+
out.print_warning("No connections configured. Run 'ntro auth login' to add one.")
|
|
59
|
+
return
|
|
60
|
+
|
|
61
|
+
rows = []
|
|
62
|
+
for name, conn in config.connections.items():
|
|
63
|
+
rows.append({
|
|
64
|
+
"name": name,
|
|
65
|
+
"host": conn.host,
|
|
66
|
+
"default_tenant": conn.default_tenant or "",
|
|
67
|
+
"active": "●" if name == config.default_connection_name else "",
|
|
68
|
+
})
|
|
69
|
+
out.output(rows, columns=["active", "name", "host", "default_tenant"], title="Connections")
|
|
70
|
+
|
|
71
|
+
|
|
72
|
+
@app.command()
|
|
73
|
+
def test(
|
|
74
|
+
connection: Optional[str] = typer.Option(None, "-c", "--connection", help="Connection to test"),
|
|
75
|
+
) -> None:
|
|
76
|
+
"""Test a connection by calling GET /me."""
|
|
77
|
+
try:
|
|
78
|
+
client = get_client()
|
|
79
|
+
profile = client.identity.whoami_sync()
|
|
80
|
+
out.print_success(f"Connected as {profile.email} (org: {profile.orgId})")
|
|
81
|
+
except NtroError as e:
|
|
82
|
+
out.print_error(str(e))
|
|
83
|
+
raise typer.Exit(1)
|
|
84
|
+
|
|
85
|
+
|
|
86
|
+
@app.command("set-default")
|
|
87
|
+
def set_default(name: str = typer.Argument(help="Connection name to set as default")) -> None:
|
|
88
|
+
"""Set the default connection."""
|
|
89
|
+
config = load_config()
|
|
90
|
+
if name not in config.connections:
|
|
91
|
+
out.print_error(f"Connection '{name}' not found. Available: {list(config.connections.keys())}")
|
|
92
|
+
raise typer.Exit(1)
|
|
93
|
+
config.default_connection_name = name
|
|
94
|
+
save_config(config)
|
|
95
|
+
out.print_success(f"Default connection set to '{name}'")
|
|
96
|
+
|
|
97
|
+
|
|
98
|
+
@app.command()
|
|
99
|
+
def whoami() -> None:
|
|
100
|
+
"""Show current user identity (calls GET /me)."""
|
|
101
|
+
try:
|
|
102
|
+
client = get_client()
|
|
103
|
+
profile = client.identity.whoami_sync()
|
|
104
|
+
out.output(profile, title="Current User")
|
|
105
|
+
except NtroError as e:
|
|
106
|
+
out.print_error(str(e))
|
|
107
|
+
raise typer.Exit(1)
|
|
@@ -0,0 +1,93 @@
|
|
|
1
|
+
"""ntro entity — manage SPVs/funds within a tenant."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import os
|
|
6
|
+
from typing import Optional
|
|
7
|
+
|
|
8
|
+
import click
|
|
9
|
+
import typer
|
|
10
|
+
|
|
11
|
+
from ntro.workspace.config import load_config
|
|
12
|
+
from ntro.workspace.exceptions import NtroError
|
|
13
|
+
from ntro_cli import output as out
|
|
14
|
+
from ntro_cli.context import get_client
|
|
15
|
+
from ntro_cli.helpers import load_json_input
|
|
16
|
+
|
|
17
|
+
app = typer.Typer(help="Manage entities (SPVs/funds within a tenant)")
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
def _resolve_tenant(tenant_flag: Optional[str]) -> str:
|
|
21
|
+
"""Resolve tenant from flag > env var > config default."""
|
|
22
|
+
if tenant_flag:
|
|
23
|
+
return tenant_flag
|
|
24
|
+
env = os.environ.get("NTRO_TENANT")
|
|
25
|
+
if env:
|
|
26
|
+
return env
|
|
27
|
+
try:
|
|
28
|
+
ctx = click.get_current_context()
|
|
29
|
+
conn_name = ctx.obj.get("connection") if ctx.obj else None
|
|
30
|
+
config = load_config()
|
|
31
|
+
conn = config.get_connection(conn_name)
|
|
32
|
+
if conn.default_tenant:
|
|
33
|
+
return conn.default_tenant
|
|
34
|
+
except Exception:
|
|
35
|
+
pass
|
|
36
|
+
raise typer.BadParameter(
|
|
37
|
+
"Tenant required. Use --tenant, set NTRO_TENANT, or set default_tenant in config."
|
|
38
|
+
)
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
@app.command()
|
|
42
|
+
def create(
|
|
43
|
+
name: Optional[str] = typer.Option(None, help="Entity display name"),
|
|
44
|
+
slug: Optional[str] = typer.Option(None, help="URL-safe identifier"),
|
|
45
|
+
tenant: Optional[str] = typer.Option(None, "--tenant", help="Tenant slug or ID", envvar="NTRO_TENANT"),
|
|
46
|
+
entity_type: Optional[str] = typer.Option(None, "--type", help="Entity type (e.g. real-estate-spv)"),
|
|
47
|
+
jurisdiction: Optional[str] = typer.Option(None, help="Legal jurisdiction"),
|
|
48
|
+
currency: Optional[str] = typer.Option(None, help="Base currency (e.g. GBP)"),
|
|
49
|
+
schema: Optional[str] = typer.Option(None, help="Databricks schema name"),
|
|
50
|
+
json_input: Optional[str] = typer.Option(None, "--json", help="JSON payload (inline or @file)"),
|
|
51
|
+
) -> None:
|
|
52
|
+
"""Create a new entity within a tenant."""
|
|
53
|
+
try:
|
|
54
|
+
tenant_id = _resolve_tenant(tenant)
|
|
55
|
+
|
|
56
|
+
if json_input:
|
|
57
|
+
payload = load_json_input(json_input)
|
|
58
|
+
else:
|
|
59
|
+
if not name or not slug:
|
|
60
|
+
raise typer.BadParameter("Provide --name and --slug (or use --json)")
|
|
61
|
+
payload = {
|
|
62
|
+
"name": name,
|
|
63
|
+
"slug": slug,
|
|
64
|
+
"type": entity_type,
|
|
65
|
+
"jurisdiction": jurisdiction,
|
|
66
|
+
"currency": currency,
|
|
67
|
+
"schema": schema,
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
client = get_client()
|
|
71
|
+
entity = client.entities.create_sync(tenant_id=tenant_id, **payload)
|
|
72
|
+
out.output(entity, title=f"Entity created: {entity.slug}")
|
|
73
|
+
except NtroError as e:
|
|
74
|
+
out.print_error(str(e))
|
|
75
|
+
raise typer.Exit(1)
|
|
76
|
+
|
|
77
|
+
|
|
78
|
+
@app.command("list")
|
|
79
|
+
def list_entities(
|
|
80
|
+
tenant: Optional[str] = typer.Option(None, "--tenant", help="Filter by tenant", envvar="NTRO_TENANT"),
|
|
81
|
+
) -> None:
|
|
82
|
+
"""List entities (optionally filtered by tenant)."""
|
|
83
|
+
try:
|
|
84
|
+
client = get_client()
|
|
85
|
+
entities = client.entities.list_sync(tenant_id=tenant)
|
|
86
|
+
out.output(
|
|
87
|
+
entities,
|
|
88
|
+
columns=["id", "slug", "name", "tenantId", "type", "currency"],
|
|
89
|
+
title="Entities",
|
|
90
|
+
)
|
|
91
|
+
except NtroError as e:
|
|
92
|
+
out.print_error(str(e))
|
|
93
|
+
raise typer.Exit(1)
|