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.
@@ -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>`.
@@ -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'
@@ -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)