ntro-cli 0.1.0__py3-none-any.whl
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/__init__.py +0 -0
- ntro_cli/commands/__init__.py +0 -0
- ntro_cli/commands/auth.py +107 -0
- ntro_cli/commands/entity.py +93 -0
- ntro_cli/commands/integration.py +131 -0
- ntro_cli/commands/run.py +84 -0
- ntro_cli/commands/tenant.py +62 -0
- ntro_cli/commands/workflow.py +205 -0
- ntro_cli/context.py +33 -0
- ntro_cli/helpers.py +21 -0
- ntro_cli/main.py +70 -0
- ntro_cli/output.py +104 -0
- ntro_cli-0.1.0.dist-info/METADATA +12 -0
- ntro_cli-0.1.0.dist-info/RECORD +16 -0
- ntro_cli-0.1.0.dist-info/WHEEL +4 -0
- ntro_cli-0.1.0.dist-info/entry_points.txt +2 -0
ntro_cli/__init__.py
ADDED
|
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)
|
|
@@ -0,0 +1,131 @@
|
|
|
1
|
+
"""ntro integration — data platforms and email sources."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from typing import Optional
|
|
6
|
+
|
|
7
|
+
import typer
|
|
8
|
+
|
|
9
|
+
from ntro.workspace.exceptions import NtroError
|
|
10
|
+
from ntro_cli import output as out
|
|
11
|
+
from ntro_cli.context import get_client
|
|
12
|
+
from ntro_cli.helpers import load_json_input
|
|
13
|
+
|
|
14
|
+
app = typer.Typer(help="Manage data platform integrations and email sources")
|
|
15
|
+
add_app = typer.Typer(help="Add a new integration")
|
|
16
|
+
app.add_typer(add_app, name="add")
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
@add_app.command()
|
|
20
|
+
def databricks(
|
|
21
|
+
name: Optional[str] = typer.Option(None, help="Display name"),
|
|
22
|
+
workspace_url: Optional[str] = typer.Option(None, "--workspace-url", help="Databricks workspace URL"),
|
|
23
|
+
catalog: Optional[str] = typer.Option(None, help="Unity Catalog name"),
|
|
24
|
+
client_id: Optional[str] = typer.Option(None, "--client-id", help="Service principal client ID"),
|
|
25
|
+
client_secret: Optional[str] = typer.Option(None, "--client-secret", help="Service principal secret", hide_input=True),
|
|
26
|
+
region: Optional[str] = typer.Option(None, help="Azure region (e.g. UK-South)"),
|
|
27
|
+
json_input: Optional[str] = typer.Option(None, "--json", help="JSON payload (inline or @file)"),
|
|
28
|
+
) -> None:
|
|
29
|
+
"""Register a Databricks data platform."""
|
|
30
|
+
try:
|
|
31
|
+
if json_input:
|
|
32
|
+
payload = load_json_input(json_input)
|
|
33
|
+
else:
|
|
34
|
+
if not all([name, workspace_url, catalog]):
|
|
35
|
+
raise typer.BadParameter("Provide --name, --workspace-url, --catalog (or use --json)")
|
|
36
|
+
payload = {
|
|
37
|
+
"name": name,
|
|
38
|
+
"provider": "DATABRICKS",
|
|
39
|
+
"region": region,
|
|
40
|
+
"config": {
|
|
41
|
+
"workspaceUrl": workspace_url,
|
|
42
|
+
"catalog": catalog,
|
|
43
|
+
"authType": "service-principal",
|
|
44
|
+
"clientId": client_id,
|
|
45
|
+
"clientSecret": client_secret,
|
|
46
|
+
},
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
client = get_client()
|
|
50
|
+
result = client.integrations.create_data_platform_sync(**payload)
|
|
51
|
+
out.output(result, title=f"Integration created: {result.name}")
|
|
52
|
+
except NtroError as e:
|
|
53
|
+
out.print_error(str(e))
|
|
54
|
+
raise typer.Exit(1)
|
|
55
|
+
|
|
56
|
+
|
|
57
|
+
@add_app.command()
|
|
58
|
+
def email(
|
|
59
|
+
tenant_id: str = typer.Option(..., "--tenant", help="Tenant ID or slug"),
|
|
60
|
+
provider: str = typer.Option("microsoft-graph", help="Email provider"),
|
|
61
|
+
address: Optional[str] = typer.Option(None, help="Email address"),
|
|
62
|
+
json_input: Optional[str] = typer.Option(None, "--json", help="JSON payload (inline or @file)"),
|
|
63
|
+
) -> None:
|
|
64
|
+
"""Add an email integration."""
|
|
65
|
+
try:
|
|
66
|
+
if json_input:
|
|
67
|
+
payload = load_json_input(json_input)
|
|
68
|
+
else:
|
|
69
|
+
payload = {"tenantId": tenant_id, "provider": provider, "address": address}
|
|
70
|
+
|
|
71
|
+
client = get_client()
|
|
72
|
+
result = client.integrations.create_email_sync(**payload)
|
|
73
|
+
out.output(result, title="Email integration created")
|
|
74
|
+
except NtroError as e:
|
|
75
|
+
out.print_error(str(e))
|
|
76
|
+
raise typer.Exit(1)
|
|
77
|
+
|
|
78
|
+
|
|
79
|
+
@app.command("list")
|
|
80
|
+
def list_integrations() -> None:
|
|
81
|
+
"""List all data platform configs."""
|
|
82
|
+
try:
|
|
83
|
+
client = get_client()
|
|
84
|
+
items = client.integrations.list_data_platforms_sync()
|
|
85
|
+
out.output(items, columns=["id", "name", "provider", "region", "ownership"], title="Data Platforms")
|
|
86
|
+
except NtroError as e:
|
|
87
|
+
out.print_error(str(e))
|
|
88
|
+
raise typer.Exit(1)
|
|
89
|
+
|
|
90
|
+
|
|
91
|
+
@app.command()
|
|
92
|
+
def info(id: str = typer.Argument(help="Data platform config ID")) -> None:
|
|
93
|
+
"""Show data platform details."""
|
|
94
|
+
try:
|
|
95
|
+
client = get_client()
|
|
96
|
+
result = client.integrations.get_data_platform_sync(id)
|
|
97
|
+
out.output(result, title=f"Integration: {result.name}")
|
|
98
|
+
except NtroError as e:
|
|
99
|
+
out.print_error(str(e))
|
|
100
|
+
raise typer.Exit(1)
|
|
101
|
+
|
|
102
|
+
|
|
103
|
+
@app.command()
|
|
104
|
+
def test(id: str = typer.Argument(help="Data platform config ID")) -> None:
|
|
105
|
+
"""Test a data platform connection."""
|
|
106
|
+
try:
|
|
107
|
+
from rich.console import Console
|
|
108
|
+
console = Console()
|
|
109
|
+
client = get_client()
|
|
110
|
+
with console.status("Testing connection..."):
|
|
111
|
+
result = client.integrations.test_connection_sync(id)
|
|
112
|
+
if result.success:
|
|
113
|
+
out.print_success(f"{result.message}" + (f" ({result.latencyMs}ms)" if result.latencyMs else ""))
|
|
114
|
+
else:
|
|
115
|
+
out.print_error(result.message)
|
|
116
|
+
raise typer.Exit(1)
|
|
117
|
+
except NtroError as e:
|
|
118
|
+
out.print_error(str(e))
|
|
119
|
+
raise typer.Exit(1)
|
|
120
|
+
|
|
121
|
+
|
|
122
|
+
@app.command()
|
|
123
|
+
def discover(id: str = typer.Argument(help="Data platform config ID")) -> None:
|
|
124
|
+
"""Discover schemas in a data platform."""
|
|
125
|
+
try:
|
|
126
|
+
client = get_client()
|
|
127
|
+
schemas = client.integrations.discover_schemas_sync(id)
|
|
128
|
+
out.output(schemas, columns=["name"], title=f"Schemas in {id}")
|
|
129
|
+
except NtroError as e:
|
|
130
|
+
out.print_error(str(e))
|
|
131
|
+
raise typer.Exit(1)
|
ntro_cli/commands/run.py
ADDED
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
"""ntro run — inspect workflow executions."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from typing import Optional
|
|
6
|
+
|
|
7
|
+
import typer
|
|
8
|
+
|
|
9
|
+
from ntro.workspace.exceptions import NtroError
|
|
10
|
+
from ntro_cli import output as out
|
|
11
|
+
from ntro_cli.context import get_client
|
|
12
|
+
|
|
13
|
+
app = typer.Typer(help="Inspect workflow runs and execution history")
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
@app.command()
|
|
17
|
+
def status(id: str = typer.Argument(help="Task ID")) -> None:
|
|
18
|
+
"""Show the status of a workflow run."""
|
|
19
|
+
try:
|
|
20
|
+
client = get_client()
|
|
21
|
+
task = client.tasks.get_sync(id)
|
|
22
|
+
out.output(task, title=f"Run: {task.id}")
|
|
23
|
+
except NtroError as e:
|
|
24
|
+
out.print_error(str(e))
|
|
25
|
+
raise typer.Exit(1)
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
@app.command("list")
|
|
29
|
+
def list_runs() -> None:
|
|
30
|
+
"""List scheduled / upcoming runs."""
|
|
31
|
+
try:
|
|
32
|
+
client = get_client()
|
|
33
|
+
tasks = client.tasks.list_schedule_sync()
|
|
34
|
+
out.output(
|
|
35
|
+
tasks,
|
|
36
|
+
columns=["id", "title", "workflowId", "status", "dueDate"],
|
|
37
|
+
title="Schedule",
|
|
38
|
+
)
|
|
39
|
+
except NtroError as e:
|
|
40
|
+
out.print_error(str(e))
|
|
41
|
+
raise typer.Exit(1)
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
@app.command()
|
|
45
|
+
def history(
|
|
46
|
+
tenant: str = typer.Option(..., "--tenant", help="Tenant ID", envvar="NTRO_TENANT"),
|
|
47
|
+
entity: str = typer.Option(..., "--entity", help="Entity ID", envvar="NTRO_ENTITY"),
|
|
48
|
+
) -> None:
|
|
49
|
+
"""Show run history for a tenant/entity."""
|
|
50
|
+
try:
|
|
51
|
+
client = get_client()
|
|
52
|
+
tasks = client.tasks.history_sync(tenant, entity)
|
|
53
|
+
out.output(
|
|
54
|
+
tasks,
|
|
55
|
+
columns=["id", "title", "workflowId", "status", "createdAt"],
|
|
56
|
+
title=f"History — {entity}",
|
|
57
|
+
)
|
|
58
|
+
except NtroError as e:
|
|
59
|
+
out.print_error(str(e))
|
|
60
|
+
raise typer.Exit(1)
|
|
61
|
+
|
|
62
|
+
|
|
63
|
+
@app.command()
|
|
64
|
+
def incoming() -> None:
|
|
65
|
+
"""List incoming (queued) runs."""
|
|
66
|
+
try:
|
|
67
|
+
client = get_client()
|
|
68
|
+
items = client.tasks.list_incoming_sync()
|
|
69
|
+
out.output(items, title="Incoming")
|
|
70
|
+
except NtroError as e:
|
|
71
|
+
out.print_error(str(e))
|
|
72
|
+
raise typer.Exit(1)
|
|
73
|
+
|
|
74
|
+
|
|
75
|
+
@app.command()
|
|
76
|
+
def pending() -> None:
|
|
77
|
+
"""List pending (awaiting action) runs."""
|
|
78
|
+
try:
|
|
79
|
+
client = get_client()
|
|
80
|
+
items = client.tasks.list_pending_sync()
|
|
81
|
+
out.output(items, title="Pending")
|
|
82
|
+
except NtroError as e:
|
|
83
|
+
out.print_error(str(e))
|
|
84
|
+
raise typer.Exit(1)
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
"""ntro tenant — manage client cells."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from typing import Optional
|
|
6
|
+
|
|
7
|
+
import typer
|
|
8
|
+
|
|
9
|
+
from ntro.workspace.exceptions import NtroError
|
|
10
|
+
from ntro_cli import output as out
|
|
11
|
+
from ntro_cli.context import get_client
|
|
12
|
+
from ntro_cli.helpers import load_json_input
|
|
13
|
+
|
|
14
|
+
app = typer.Typer(help="Manage tenants (client cells)")
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
@app.command()
|
|
18
|
+
def create(
|
|
19
|
+
name: Optional[str] = typer.Option(None, help="Tenant display name"),
|
|
20
|
+
slug: Optional[str] = typer.Option(None, help="URL-safe identifier"),
|
|
21
|
+
integration: Optional[str] = typer.Option(None, "--integration", help="Data platform config ID"),
|
|
22
|
+
json_input: Optional[str] = typer.Option(None, "--json", help="JSON payload (inline or @file)"),
|
|
23
|
+
) -> None:
|
|
24
|
+
"""Create a new tenant."""
|
|
25
|
+
try:
|
|
26
|
+
if json_input:
|
|
27
|
+
payload = load_json_input(json_input)
|
|
28
|
+
else:
|
|
29
|
+
if not name or not slug:
|
|
30
|
+
raise typer.BadParameter("Provide --name and --slug (or use --json)")
|
|
31
|
+
payload = {"name": name, "slug": slug, "dataPlatformConfigId": integration}
|
|
32
|
+
|
|
33
|
+
client = get_client()
|
|
34
|
+
tenant = client.tenants.create_sync(**payload)
|
|
35
|
+
out.output(tenant, title=f"Tenant created: {tenant.slug}")
|
|
36
|
+
except NtroError as e:
|
|
37
|
+
out.print_error(str(e))
|
|
38
|
+
raise typer.Exit(1)
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
@app.command("list")
|
|
42
|
+
def list_tenants() -> None:
|
|
43
|
+
"""List all tenants."""
|
|
44
|
+
try:
|
|
45
|
+
client = get_client()
|
|
46
|
+
tenants = client.tenants.list_sync()
|
|
47
|
+
out.output(tenants, columns=["id", "slug", "name", "status", "entityCount"], title="Tenants")
|
|
48
|
+
except NtroError as e:
|
|
49
|
+
out.print_error(str(e))
|
|
50
|
+
raise typer.Exit(1)
|
|
51
|
+
|
|
52
|
+
|
|
53
|
+
@app.command()
|
|
54
|
+
def info(id: str = typer.Argument(help="Tenant slug or ID")) -> None:
|
|
55
|
+
"""Show tenant details."""
|
|
56
|
+
try:
|
|
57
|
+
client = get_client()
|
|
58
|
+
tenant = client.tenants.get_sync(id)
|
|
59
|
+
out.output(tenant, title=f"Tenant: {tenant.slug}")
|
|
60
|
+
except NtroError as e:
|
|
61
|
+
out.print_error(str(e))
|
|
62
|
+
raise typer.Exit(1)
|
|
@@ -0,0 +1,205 @@
|
|
|
1
|
+
"""ntro workflow — definition, deployment, and triggering."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import time
|
|
6
|
+
from pathlib import Path
|
|
7
|
+
from typing import Optional
|
|
8
|
+
|
|
9
|
+
import typer
|
|
10
|
+
|
|
11
|
+
from ntro.workspace.exceptions import NtroError
|
|
12
|
+
from ntro.workspace.models.common import TaskStatus
|
|
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 workflow definitions, deployments, and runs")
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
@app.command()
|
|
21
|
+
def create(
|
|
22
|
+
name: Optional[str] = typer.Option(None, help="Workflow name (slug)"),
|
|
23
|
+
description: Optional[str] = typer.Option(None, help="Description"),
|
|
24
|
+
tenant: Optional[str] = typer.Option(None, "--tenant", help="Tenant ID"),
|
|
25
|
+
schedule: Optional[str] = typer.Option(None, help="Cron schedule (e.g. '0 8 5 * *')"),
|
|
26
|
+
timezone: Optional[str] = typer.Option(None, help="Timezone (e.g. Europe/London)"),
|
|
27
|
+
json_input: Optional[str] = typer.Option(None, "--json", help="JSON payload (inline or @file)"),
|
|
28
|
+
) -> None:
|
|
29
|
+
"""Register a new workflow definition."""
|
|
30
|
+
try:
|
|
31
|
+
if json_input:
|
|
32
|
+
payload = load_json_input(json_input)
|
|
33
|
+
else:
|
|
34
|
+
if not name:
|
|
35
|
+
raise typer.BadParameter("Provide --name (or use --json)")
|
|
36
|
+
payload = {
|
|
37
|
+
"name": name,
|
|
38
|
+
"description": description,
|
|
39
|
+
"tenantId": tenant,
|
|
40
|
+
"schedule": schedule,
|
|
41
|
+
"timezone": timezone,
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
client = get_client()
|
|
45
|
+
wf = client.workflows.create_sync(**payload)
|
|
46
|
+
out.output(wf, title=f"Workflow created: {wf.name}")
|
|
47
|
+
except NtroError as e:
|
|
48
|
+
out.print_error(str(e))
|
|
49
|
+
raise typer.Exit(1)
|
|
50
|
+
|
|
51
|
+
|
|
52
|
+
@app.command("list")
|
|
53
|
+
def list_workflows() -> None:
|
|
54
|
+
"""List all registered workflows."""
|
|
55
|
+
try:
|
|
56
|
+
client = get_client()
|
|
57
|
+
workflows = client.workflows.list_sync()
|
|
58
|
+
out.output(
|
|
59
|
+
workflows,
|
|
60
|
+
columns=["id", "name", "description", "schedule", "latestVersion"],
|
|
61
|
+
title="Workflows",
|
|
62
|
+
)
|
|
63
|
+
except NtroError as e:
|
|
64
|
+
out.print_error(str(e))
|
|
65
|
+
raise typer.Exit(1)
|
|
66
|
+
|
|
67
|
+
|
|
68
|
+
@app.command()
|
|
69
|
+
def info(id: str = typer.Argument(help="Workflow ID or name")) -> None:
|
|
70
|
+
"""Show workflow details."""
|
|
71
|
+
try:
|
|
72
|
+
client = get_client()
|
|
73
|
+
wf = client.workflows.get_sync(id)
|
|
74
|
+
out.output(wf, title=f"Workflow: {wf.name}")
|
|
75
|
+
except NtroError as e:
|
|
76
|
+
out.print_error(str(e))
|
|
77
|
+
raise typer.Exit(1)
|
|
78
|
+
|
|
79
|
+
|
|
80
|
+
@app.command()
|
|
81
|
+
def push(
|
|
82
|
+
id: str = typer.Argument(help="Workflow ID"),
|
|
83
|
+
file: Path = typer.Argument(help="Artifact file to upload (.zip or .tar.gz)"),
|
|
84
|
+
) -> None:
|
|
85
|
+
"""Upload a new workflow version artifact."""
|
|
86
|
+
try:
|
|
87
|
+
if not file.exists():
|
|
88
|
+
out.print_error(f"File not found: {file}")
|
|
89
|
+
raise typer.Exit(1)
|
|
90
|
+
|
|
91
|
+
client = get_client()
|
|
92
|
+
version = client.workflows.push_sync(id, file)
|
|
93
|
+
out.output(version, title=f"Version pushed: {version.version}")
|
|
94
|
+
except NtroError as e:
|
|
95
|
+
out.print_error(str(e))
|
|
96
|
+
raise typer.Exit(1)
|
|
97
|
+
|
|
98
|
+
|
|
99
|
+
@app.command()
|
|
100
|
+
def deploy(
|
|
101
|
+
workflow_id: Optional[str] = typer.Option(None, "--workflow", help="Workflow ID"),
|
|
102
|
+
version_id: Optional[str] = typer.Option(None, "--version", help="Workflow version ID"),
|
|
103
|
+
tenant: Optional[str] = typer.Option(None, "--tenant", help="Tenant ID"),
|
|
104
|
+
entity: Optional[str] = typer.Option(None, "--entity", help="Entity ID"),
|
|
105
|
+
json_input: Optional[str] = typer.Option(None, "--json", help="JSON payload (inline or @file)"),
|
|
106
|
+
) -> None:
|
|
107
|
+
"""Deploy a workflow version to a tenant/entity."""
|
|
108
|
+
try:
|
|
109
|
+
if json_input:
|
|
110
|
+
payload = load_json_input(json_input)
|
|
111
|
+
else:
|
|
112
|
+
if not workflow_id or not version_id:
|
|
113
|
+
raise typer.BadParameter("Provide --workflow and --version (or use --json)")
|
|
114
|
+
payload = {
|
|
115
|
+
"workflowId": workflow_id,
|
|
116
|
+
"workflowVersionId": version_id,
|
|
117
|
+
"tenantId": tenant,
|
|
118
|
+
"entityId": entity,
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
client = get_client()
|
|
122
|
+
deployment = client.deployments.create_sync(**payload)
|
|
123
|
+
out.output(deployment, title=f"Deployment created: {deployment.id}")
|
|
124
|
+
except NtroError as e:
|
|
125
|
+
out.print_error(str(e))
|
|
126
|
+
raise typer.Exit(1)
|
|
127
|
+
|
|
128
|
+
|
|
129
|
+
@app.command("deploy-status")
|
|
130
|
+
def deploy_status(id: str = typer.Argument(help="Deployment ID")) -> None:
|
|
131
|
+
"""Check deployment status."""
|
|
132
|
+
try:
|
|
133
|
+
client = get_client()
|
|
134
|
+
deployment = client.deployments.get_sync(id)
|
|
135
|
+
out.output(deployment, title=f"Deployment: {deployment.id}")
|
|
136
|
+
except NtroError as e:
|
|
137
|
+
out.print_error(str(e))
|
|
138
|
+
raise typer.Exit(1)
|
|
139
|
+
|
|
140
|
+
|
|
141
|
+
@app.command("run")
|
|
142
|
+
def run_workflow(
|
|
143
|
+
name: str = typer.Argument(help="Workflow name (e.g. nav-monthly)"),
|
|
144
|
+
tenant: Optional[str] = typer.Option(None, "--tenant", help="Tenant ID", envvar="NTRO_TENANT"),
|
|
145
|
+
entity: Optional[str] = typer.Option(None, "--entity", help="Entity ID", envvar="NTRO_ENTITY"),
|
|
146
|
+
period: Optional[str] = typer.Option(None, "--period", help="Accounting period (e.g. 2026-03)"),
|
|
147
|
+
priority: Optional[str] = typer.Option(None, "--priority", help="Priority: LOW, NORMAL, HIGH"),
|
|
148
|
+
dry_run: bool = typer.Option(False, "--dry-run", help="Validate without committing"),
|
|
149
|
+
wait: bool = typer.Option(False, "--wait", help="Poll until task completes"),
|
|
150
|
+
json_input: Optional[str] = typer.Option(None, "--json", help="JSON payload (inline or @file)"),
|
|
151
|
+
) -> None:
|
|
152
|
+
"""Trigger a workflow run."""
|
|
153
|
+
try:
|
|
154
|
+
if json_input:
|
|
155
|
+
payload = load_json_input(json_input)
|
|
156
|
+
else:
|
|
157
|
+
if not tenant:
|
|
158
|
+
raise typer.BadParameter("Provide --tenant or set NTRO_TENANT / default_tenant in config")
|
|
159
|
+
context: dict = {}
|
|
160
|
+
if period:
|
|
161
|
+
context["period"] = period
|
|
162
|
+
if priority:
|
|
163
|
+
context["priority"] = priority
|
|
164
|
+
if dry_run:
|
|
165
|
+
context["dry_run"] = True
|
|
166
|
+
payload = {
|
|
167
|
+
"tenantId": tenant,
|
|
168
|
+
"entityId": entity,
|
|
169
|
+
"workflowId": name,
|
|
170
|
+
"context": context,
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
client = get_client()
|
|
174
|
+
task = client.tasks.create_sync(**payload)
|
|
175
|
+
out.output(task, title=f"Run started: {task.id}")
|
|
176
|
+
|
|
177
|
+
if wait:
|
|
178
|
+
_poll_task(client, task.id)
|
|
179
|
+
|
|
180
|
+
except NtroError as e:
|
|
181
|
+
out.print_error(str(e))
|
|
182
|
+
raise typer.Exit(1)
|
|
183
|
+
|
|
184
|
+
|
|
185
|
+
def _poll_task(client, task_id: str, interval: int = 3, timeout: int = 300) -> None:
|
|
186
|
+
from rich.console import Console
|
|
187
|
+
console = Console()
|
|
188
|
+
deadline = time.monotonic() + timeout
|
|
189
|
+
|
|
190
|
+
with console.status(f"Waiting for task {task_id}...") as status:
|
|
191
|
+
while time.monotonic() < deadline:
|
|
192
|
+
try:
|
|
193
|
+
task = client.tasks.get_sync(task_id)
|
|
194
|
+
status.update(f"[dim]{task.status}[/dim] — {task.id}")
|
|
195
|
+
if task.status in (TaskStatus.COMPLETED, TaskStatus.FAILED, TaskStatus.CANCELLED):
|
|
196
|
+
break
|
|
197
|
+
time.sleep(interval)
|
|
198
|
+
except NtroError:
|
|
199
|
+
time.sleep(interval)
|
|
200
|
+
|
|
201
|
+
try:
|
|
202
|
+
task = client.tasks.get_sync(task_id)
|
|
203
|
+
out.output(task, title=f"Final status: {task.status}")
|
|
204
|
+
except NtroError as e:
|
|
205
|
+
out.print_error(str(e))
|
ntro_cli/context.py
ADDED
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
"""CLI context — SDK client initialisation and shared state."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import click
|
|
6
|
+
|
|
7
|
+
from ntro.workspace import Client
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
def get_client() -> Client:
|
|
11
|
+
"""Build and return a Client from the current Typer context."""
|
|
12
|
+
ctx = click.get_current_context()
|
|
13
|
+
obj: dict = ctx.ensure_object(dict)
|
|
14
|
+
|
|
15
|
+
if "client" not in obj:
|
|
16
|
+
connection: str | None = obj.get("connection")
|
|
17
|
+
host: str | None = obj.get("host")
|
|
18
|
+
debug: bool = obj.get("debug", False)
|
|
19
|
+
|
|
20
|
+
if host:
|
|
21
|
+
# Explicit host overrides config entirely
|
|
22
|
+
from ntro.workspace.config import load_config
|
|
23
|
+
conn_name = connection or None
|
|
24
|
+
try:
|
|
25
|
+
cfg = load_config().get_connection(conn_name)
|
|
26
|
+
api_key = cfg.api_key
|
|
27
|
+
except Exception:
|
|
28
|
+
api_key = ""
|
|
29
|
+
obj["client"] = Client(host=host, api_key=api_key, debug=debug)
|
|
30
|
+
else:
|
|
31
|
+
obj["client"] = Client.from_config(connection=connection, debug=debug)
|
|
32
|
+
|
|
33
|
+
return obj["client"]
|
ntro_cli/helpers.py
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
"""Shared CLI helpers."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import json
|
|
6
|
+
from pathlib import Path
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
def load_json_input(value: str) -> dict:
|
|
10
|
+
"""Parse a --json argument.
|
|
11
|
+
|
|
12
|
+
Accepts:
|
|
13
|
+
- Inline JSON string: '{"name": "Acme"}'
|
|
14
|
+
- File reference: @./config.json or @/abs/path.json
|
|
15
|
+
"""
|
|
16
|
+
if value.startswith("@"):
|
|
17
|
+
path = Path(value[1:]).expanduser()
|
|
18
|
+
if not path.exists():
|
|
19
|
+
raise FileNotFoundError(f"JSON file not found: {path}")
|
|
20
|
+
return json.loads(path.read_text())
|
|
21
|
+
return json.loads(value)
|
ntro_cli/main.py
ADDED
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
"""ntro CLI — root Typer app."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import logging
|
|
6
|
+
from typing import Optional
|
|
7
|
+
|
|
8
|
+
import typer
|
|
9
|
+
|
|
10
|
+
from ntro_cli import output as out
|
|
11
|
+
from ntro_cli.commands import auth, entity, integration, run, tenant, workflow
|
|
12
|
+
|
|
13
|
+
app = typer.Typer(
|
|
14
|
+
name="ntro",
|
|
15
|
+
help="ntro platform CLI",
|
|
16
|
+
no_args_is_help=True,
|
|
17
|
+
pretty_exceptions_show_locals=False,
|
|
18
|
+
)
|
|
19
|
+
|
|
20
|
+
app.add_typer(auth.app, name="auth")
|
|
21
|
+
app.add_typer(integration.app, name="integration")
|
|
22
|
+
app.add_typer(tenant.app, name="tenant")
|
|
23
|
+
app.add_typer(entity.app, name="entity")
|
|
24
|
+
app.add_typer(workflow.app, name="workflow")
|
|
25
|
+
app.add_typer(run.app, name="run")
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
@app.callback()
|
|
29
|
+
def main(
|
|
30
|
+
ctx: typer.Context,
|
|
31
|
+
connection: Optional[str] = typer.Option(
|
|
32
|
+
None, "-c", "--connection",
|
|
33
|
+
help="Connection name from ~/.ntro/config.toml",
|
|
34
|
+
envvar="NTRO_DEFAULT_CONNECTION_NAME",
|
|
35
|
+
),
|
|
36
|
+
host: Optional[str] = typer.Option(
|
|
37
|
+
None, "--host",
|
|
38
|
+
help="Override API host URL (e.g. http://localhost:3000/v1)",
|
|
39
|
+
envvar="NTRO_HOST",
|
|
40
|
+
),
|
|
41
|
+
output: str = typer.Option(
|
|
42
|
+
"text", "-o", "--output",
|
|
43
|
+
help="Output format: text or json",
|
|
44
|
+
),
|
|
45
|
+
debug: bool = typer.Option(False, "--debug", help="Enable debug logging"),
|
|
46
|
+
log_level: Optional[str] = typer.Option(
|
|
47
|
+
None, "--log-level",
|
|
48
|
+
help="Log level: DEBUG, INFO, WARN, ERROR",
|
|
49
|
+
),
|
|
50
|
+
) -> None:
|
|
51
|
+
"""ntro platform CLI.
|
|
52
|
+
|
|
53
|
+
Default API: https://api.ntropii.com/v1
|
|
54
|
+
Local dev: http://localhost:3000/v1 (use -c local or --host)
|
|
55
|
+
|
|
56
|
+
Quickstart:
|
|
57
|
+
ntro auth login # configure a connection
|
|
58
|
+
ntro auth whoami # verify identity
|
|
59
|
+
ntro tenant list # list tenants
|
|
60
|
+
"""
|
|
61
|
+
ctx.ensure_object(dict)
|
|
62
|
+
ctx.obj["connection"] = connection
|
|
63
|
+
ctx.obj["host"] = host
|
|
64
|
+
ctx.obj["debug"] = debug
|
|
65
|
+
|
|
66
|
+
out.set_format(output)
|
|
67
|
+
|
|
68
|
+
if debug or log_level:
|
|
69
|
+
level = getattr(logging, (log_level or "DEBUG").upper(), logging.DEBUG)
|
|
70
|
+
logging.basicConfig(level=level, format="%(name)s %(levelname)s %(message)s")
|
ntro_cli/output.py
ADDED
|
@@ -0,0 +1,104 @@
|
|
|
1
|
+
"""Output formatters — Rich tables/panels for text mode, raw JSON for json mode."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import json
|
|
6
|
+
from typing import Any
|
|
7
|
+
|
|
8
|
+
from pydantic import BaseModel
|
|
9
|
+
from rich.console import Console
|
|
10
|
+
from rich.panel import Panel
|
|
11
|
+
from rich.table import Table
|
|
12
|
+
|
|
13
|
+
console = Console()
|
|
14
|
+
err_console = Console(stderr=True)
|
|
15
|
+
|
|
16
|
+
# Current output format — set by main.py callback from --output flag
|
|
17
|
+
_output_format: str = "text"
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
def set_format(fmt: str) -> None:
|
|
21
|
+
global _output_format
|
|
22
|
+
_output_format = fmt
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
def get_format() -> str:
|
|
26
|
+
return _output_format
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
def _to_dict(obj: Any) -> dict:
|
|
30
|
+
if isinstance(obj, BaseModel):
|
|
31
|
+
return obj.model_dump(mode="json")
|
|
32
|
+
if isinstance(obj, dict):
|
|
33
|
+
return obj
|
|
34
|
+
return {"value": str(obj)}
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
def _to_dict_list(items: list) -> list[dict]:
|
|
38
|
+
return [_to_dict(i) for i in items]
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
def output(
|
|
42
|
+
data: Any,
|
|
43
|
+
*,
|
|
44
|
+
columns: list[str] | None = None,
|
|
45
|
+
title: str | None = None,
|
|
46
|
+
) -> None:
|
|
47
|
+
"""Print data to stdout in the current output format."""
|
|
48
|
+
if _output_format == "json":
|
|
49
|
+
_print_json(data)
|
|
50
|
+
else:
|
|
51
|
+
if isinstance(data, list):
|
|
52
|
+
_print_table(data, columns=columns, title=title)
|
|
53
|
+
else:
|
|
54
|
+
_print_panel(data, title=title)
|
|
55
|
+
|
|
56
|
+
|
|
57
|
+
def _print_json(data: Any) -> None:
|
|
58
|
+
if isinstance(data, list):
|
|
59
|
+
raw = [_to_dict(d) for d in data]
|
|
60
|
+
elif isinstance(data, BaseModel):
|
|
61
|
+
raw = data.model_dump()
|
|
62
|
+
else:
|
|
63
|
+
raw = data
|
|
64
|
+
console.print_json(json.dumps(raw, default=str))
|
|
65
|
+
|
|
66
|
+
|
|
67
|
+
def _print_table(items: list, columns: list[str] | None, title: str | None) -> None:
|
|
68
|
+
if not items:
|
|
69
|
+
console.print("[dim]No results.[/dim]")
|
|
70
|
+
return
|
|
71
|
+
|
|
72
|
+
rows = _to_dict_list(items)
|
|
73
|
+
|
|
74
|
+
# Determine columns: explicit list, or all keys from first row
|
|
75
|
+
cols = columns or list(rows[0].keys())
|
|
76
|
+
|
|
77
|
+
table = Table(title=title, show_header=True, header_style="bold cyan")
|
|
78
|
+
for col in cols:
|
|
79
|
+
table.add_column(col)
|
|
80
|
+
|
|
81
|
+
for row in rows:
|
|
82
|
+
table.add_row(*[str(row.get(col, "")) for col in cols])
|
|
83
|
+
|
|
84
|
+
console.print(table)
|
|
85
|
+
|
|
86
|
+
|
|
87
|
+
def _print_panel(data: Any, title: str | None) -> None:
|
|
88
|
+
row = _to_dict(data)
|
|
89
|
+
lines = "\n".join(
|
|
90
|
+
f"[bold]{k}[/bold]: {v}" for k, v in row.items() if v not in (None, "", [])
|
|
91
|
+
)
|
|
92
|
+
console.print(Panel(lines, title=title or "", expand=False))
|
|
93
|
+
|
|
94
|
+
|
|
95
|
+
def print_success(message: str) -> None:
|
|
96
|
+
console.print(f"[green]✓[/green] {message}")
|
|
97
|
+
|
|
98
|
+
|
|
99
|
+
def print_error(message: str) -> None:
|
|
100
|
+
err_console.print(f"[red]✗[/red] {message}")
|
|
101
|
+
|
|
102
|
+
|
|
103
|
+
def print_warning(message: str) -> None:
|
|
104
|
+
console.print(f"[yellow]![/yellow] {message}")
|
|
@@ -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,16 @@
|
|
|
1
|
+
ntro_cli/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
2
|
+
ntro_cli/context.py,sha256=Fvq6YV4GUH2q12nnyVBKnkOHPV6UN9ebxPs6LtMh2h8,1048
|
|
3
|
+
ntro_cli/helpers.py,sha256=wuEOOMZyy2hiexk9-BphLqVK8mDZ8kbPoOs2JiQlo1A,555
|
|
4
|
+
ntro_cli/main.py,sha256=9bv85ayEdBLkcuHZQLfag5Z8h_KtybmbGg-AIFMLB7U,2045
|
|
5
|
+
ntro_cli/output.py,sha256=giP8028Ezz2mpHddBNcCP0jgRqHnqRDpGaErlZlPiYU,2612
|
|
6
|
+
ntro_cli/commands/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
7
|
+
ntro_cli/commands/auth.py,sha256=qspNRBzMrIAngTU9YUn1n8yOVdqfkUdmUvYGVPRGoWg,3833
|
|
8
|
+
ntro_cli/commands/entity.py,sha256=cqfUNvfB2KvtozvgfE13epgpRn1ygtL5MytIe2tVp4Y,3251
|
|
9
|
+
ntro_cli/commands/integration.py,sha256=37J7Xl5Ne8u9JMp1Dhc-BmAfUU7eGzEcAlTtUhoGItY,4880
|
|
10
|
+
ntro_cli/commands/run.py,sha256=Fcvh27lKuYPd1CN6lhVRzU_SPWHo36XFJ2RjOFiSd4A,2302
|
|
11
|
+
ntro_cli/commands/tenant.py,sha256=I5qyyQLgLD9QjLB9nixRtHL_QAjORgbZyXQiAY-JjJk,2017
|
|
12
|
+
ntro_cli/commands/workflow.py,sha256=hM3stg47bp_sCqbGNnI76bhyecpNoiIAnYiW6B9Iw2A,7333
|
|
13
|
+
ntro_cli-0.1.0.dist-info/METADATA,sha256=DgKliN1BOO978soIXeEDEAhW-TYdx3o1WZ4TIjYqGIY,337
|
|
14
|
+
ntro_cli-0.1.0.dist-info/WHEEL,sha256=QccIxa26bgl1E6uMy58deGWi-0aeIkkangHcxk2kWfw,87
|
|
15
|
+
ntro_cli-0.1.0.dist-info/entry_points.txt,sha256=u6Q6GPMkr87NZjSc8GqeHFwa7XW3Yk0hREwqQWKyPbQ,43
|
|
16
|
+
ntro_cli-0.1.0.dist-info/RECORD,,
|