ntro
Official Python SDK for the ntro platform — fund operations automation for private markets firms (real estate, infrastructure, private credit).
pip install ntro
Overview
The ntro SDK is the Python interface to the Workspace API — the control plane that manages tenants, entities, workflows, and task execution for the ntro platform.
┌─────────────────────────────┐
│ ntro-cli / ntro-mcp │ ← Thin interface layers built on this SDK
└──────────────┬──────────────┘
│ imports
▼
┌─────────────────────────────┐
│ ntro (this package) │ ← httpx + Pydantic, async-first
│ ntro.workspace.Client │
└──────────────┬──────────────┘
│ HTTP/REST
▼
┌─────────────────────────────┐
│ Workspace API │ ← https://api.ntropii.com/v1
└─────────────────────────────┘
Quick start
from ntro.workspace import Client
# From config file (~/.ntro/config.toml)
client = Client.from_config() # default connection
client = Client.from_config(connection="staging") # named connection
# Explicit (scripting / testing)
client = Client(
host="http://localhost:3000/v1",
api_key="ntro_dev_key",
)
Configuration
The SDK reads ~/.ntro/config.toml (or $NTRO_HOME/config.toml):
default_connection_name = "local"
[connections.local]
host = "http://localhost:3000/v1"
api_key = "ntro_dev_key"
default_tenant = "acme-fund-admin"
[connections.production]
host = "https://api.ntropii.com/v1"
api_key = "your-api-key-here"
Resolution priority:
- Explicit constructor args (
host=,api_key=) NTRO_HOST/NTRO_API_KEYenvironment variables- Named connection in config (
--connection staging) default_connection_namein config
Usage
All resources have async methods and _sync wrappers for CLI / scripting use.
Identity
# Async
profile = await client.identity.whoami()
# Sync
profile = client.identity.whoami_sync()
print(profile.email, profile.orgId)
Data platform integrations
# Register a Databricks integration
integration = client.integrations.create_data_platform_sync(
name="Acme Databricks UK",
provider="DATABRICKS",
region="UK-South",
config={
"workspaceUrl": "https://adb-1234567890.12.azuredatabricks.net",
"catalog": "fund_ops",
"authType": "service-principal",
"clientId": "...",
"clientSecret": "...",
},
)
# Test connectivity
result = client.integrations.test_connection_sync(integration.id)
print(result.success, result.latencyMs)
# List all
platforms = client.integrations.list_data_platforms_sync()
# Discover schemas
schemas = client.integrations.discover_schemas_sync(integration.id)
Tenants
tenant = client.tenants.create_sync(
name="Acme Fund Administration",
slug="acme-fund-admin",
dataPlatformConfigId=integration.id,
)
tenants = client.tenants.list_sync()
tenant = client.tenants.get_sync("acme-fund-admin")
Entities
entity = client.entities.create_sync(
tenant_id="acme-fund-admin",
name="Acme Commercial SPV 1",
slug="acme-commercial-spv1",
type="real-estate-spv",
jurisdiction="Jersey",
currency="GBP",
schema_="nav_pipeline_spv1",
)
entities = client.entities.list_sync()
entities = client.entities.list_sync(tenant_id="acme-fund-admin")
Workflows
workflow = client.workflows.create_sync(
name="nav-monthly",
description="Monthly NAV pipeline",
tenantId="acme-fund-admin",
schedule="0 8 5 * *",
timezone="Europe/London",
)
workflows = client.workflows.list_sync()
workflow = client.workflows.get_sync("nav-monthly")
Deployments
deployment = client.deployments.create_sync(
workflowId=workflow.id,
workflowVersionId=version.id,
tenantId="acme-fund-admin",
)
status = client.deployments.get_sync(deployment.id)
Tasks (workflow runs)
# Trigger a run
task = client.tasks.create_sync(
tenantId="acme-fund-admin",
entityId="acme-commercial-spv1",
workflowId="nav-monthly",
context={"period": "2026-03", "priority": "HIGH"},
)
# Poll status
import time
while True:
task = client.tasks.get_sync(task.id)
print(task.status, [s.name for s in task.steps if s.status == "IN_PROGRESS"])
if task.status in ("COMPLETED", "FAILED", "CANCELLED"):
break
time.sleep(5)
# History
history = client.tasks.history_sync("acme-fund-admin", "acme-commercial-spv1")
Async usage
All methods are async by default, with _sync wrappers that call asyncio.run():
import asyncio
from ntro.workspace import Client
async def main():
client = Client.from_config()
# Run multiple requests concurrently
tenants, workflows = await asyncio.gather(
client.tenants.list(),
client.workflows.list(),
)
await client.close()
asyncio.run(main())
Error handling
from ntro.workspace.exceptions import (
NotFoundError,
AuthenticationError,
ValidationError,
NtroConnectionError,
)
try:
tenant = client.tenants.get_sync("unknown-slug")
except NotFoundError:
print("Tenant not found")
except AuthenticationError:
print("Invalid API key")
except NtroConnectionError as e:
print(f"Could not reach API: {e}")
| Exception | HTTP status | When |
|---|---|---|
AuthenticationError |
401 | Invalid or missing API key |
AuthorizationError |
403 | Insufficient permissions |
NotFoundError |
404 | Resource does not exist |
ConflictError |
409 | Resource already exists |
ValidationError |
422 | Request payload invalid |
NtroConnectionError |
— | Network / timeout error |
Domain model
| Concept | Description |
|---|---|
| Tenant | A client organisation (fund admin or asset manager). Contains entities. |
| Entity | An SPV or fund within a tenant. Gets its own schema in the customer's data platform. |
| Workflow | A repeatable process: NAV calculation, document ingestion, period close. |
| Task | A running instance of a workflow, scoped to a tenant/entity. |
| Integration | A data platform connection (Databricks, Snowflake, Microsoft Fabric). |
Authoring workflows (ntro.workflow)
Runbook authors subclass NtroWorkflow and decorate per-phase methods with @ui_step. The decorator both attaches metadata for the UI breadcrumb AND wraps the method so step lifecycle (_current_step_id, _steps_completed) tracks automatically.
from datetime import timedelta
from temporalio import workflow
from ntro.workflow import NtroWorkflow, ui_step
from .activities import open_period, emit_period_summary
from .models import NavMonthlyContext, PeriodSummary
@workflow.defn
class NavMonthlyWorkflow(NtroWorkflow):
@ui_step(name="period_open", title="Open period", icon="Calendar")
async def _step_period_open(self, ctx: NavMonthlyContext):
return await workflow.execute_activity(
open_period, ctx, start_to_close_timeout=timedelta(minutes=5),
)
@ui_step(name="summary", title="Period summary", icon="CheckCircle2")
async def _step_summary(self, ctx: NavMonthlyContext, ...) -> PeriodSummary:
return await workflow.execute_activity(
emit_period_summary, ..., start_to_close_timeout=timedelta(minutes=5),
)
@workflow.run
async def run(self, ctx: NavMonthlyContext) -> PeriodSummary:
await self._step_period_open(ctx)
...
return await self._step_summary(ctx, ...)
Class-definition order = breadcrumb order (Python preserves __dict__ insertion order). Icons are Lucide names rendered by ui-tenant.
NtroWorkflow also provides:
await self.wait_for_action(payload, display_hint, reason)— block on a HITL approve/reject signal; surfaces the richdisplay_hintto ui-tenant.await self.await_signal_with_action(predicate, action, display_hint)— block until an external signal satisfiespredicate, advertising what's needed via thecurrent_pending_actionquery.await self.run_child_workflow(slug=..., input=..., step_id=...)— dispatch a child runbook by slug.- Standard queries
current_pending_action,current_steps,current_ui_stateand theuser_actionsignal handler — wired automatically.
Install with the workflow extra: pip install 'ntro[workflow]'.
Testing workflows (ntro.testing)
Inner-loop test harness for runbook authors. Boots an in-memory Temporal (temporalio.testing.WorkflowEnvironment), registers your workflow + child workflows with auto-mocked activities, and drives the agent loop per scenario. Sub-second startup, no docker, no deploy cycle.
import asyncio
from ntro.testing import WorkflowHarness, HAPPY, REJECT_ALL, load_runbook, report
async def main():
nav, _ = load_runbook("./runbooks/nav-monthly")
di, _ = load_runbook("./runbooks/document-ingest")
nj, _ = load_runbook("./runbooks/nav-monthly-journals")
results = []
for scenario in [HAPPY, REJECT_ALL]:
async with WorkflowHarness(nav, child_workflows=[di, nj]) as h:
results.append(await h.run(input=ctx, scenario=scenario))
print(report.human(results))
asyncio.run(main())
Output:
✓ happy (0.86s)
[ 0.13s] submit_file hly-7a820232 signal=tb_submitted, source=xero-trial-balance
[ 0.24s] drill_down hly-7a820232 children=[...:document-ingest]
[ 0.36s] review ument-ingest response=approved
...
✓ reject_all (0.45s)
summary: 2 passed, 0 failed (of 2)
What's auto-mocked
- Activity returns — derived from each
@activity.defn's return type via Pydantic introspection. Required fields get type-conformant fakes (str →"auto-mock",Decimal→0, nestedBaseModel→ recursive). Defaulted fields are left alone — the runbook author's defaults are usually the most realistic value. - HITL responses — controlled by the
Scenario(HAPPY approves everything; REJECT_ALL rejects everything). Per-step overrides viaScenario(review_overrides={"step_id": "rejected"}). - submit_file signals — the harness sends a fake
document_ref+tenant_slug/entity_slugderived from the workflow's advertised args.
Built-in scenarios
| Name | Behaviour |
|---|---|
HAPPY |
Auto-approve every review; fakes for everything. |
REJECT_ALL |
Auto-reject every review (verifies clean termination). |
Build custom scenarios by instantiating Scenario(name="...", approve_reviews=False, review_overrides={...}, corrections={...}).
CLI
# Single workflow (no children)
ntro workflow test ./runbooks/document-ingest
# Parent + children
ntro workflow test ./runbooks/nav-monthly \
--child ./runbooks/document-ingest \
--child ./runbooks/nav-monthly-journals
# Specific scenarios + JSON output for CI
ntro workflow test ./runbooks/nav-monthly --scenario happy --json
Install with the testing extra: pip install 'ntro[testing]'.
Related packages
| Package | PyPI | Description |
|---|---|---|
ntro |
pypi.org/project/ntro | This SDK |
ntro-cli |
pypi.org/project/ntro-cli | CLI — ntro tenant list, ntro workflow run, ntro workflow test |
ntro-mcp |
pypi.org/project/ntro-mcp | MCP server for Claude Desktop / claude.ai |
Metadata
Release files for ntro 0.1.2
For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.
Source distribution (sdist)
| File | Size | Uploaded | Provenance | |
|---|---|---|---|---|
| ntro-0.1.2.tar.gz | 280.7 kB | Without provenance | Details |
Built distribution (wheel)
| File | Interpreter | ABI | Platform | Reset |
|---|---|---|---|---|
| ntro-0.1.2-py3-none-any.whl | Python 3 | none | any | Details |
Total release size: 498.1 kB
Release files / ntro-0.1.2.tar.gz
| Download URL | ntro-0.1.2.tar.gz |
|---|---|
| Size | 280.7 kB |
| Tags | Source |
|
SHA-256 checksum How to use checksums |
df254c980ad152f663dfeaf05e2488cbc70480c93f414d2b4e221eb1d13eaa79
|
|
BLAKE2b-256 checksum How to use checksums |
46ca5f4d0a7c3d360152f6f0e24956775ce700de21fbb36ef7c67bd6250500fa
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
No |
| Uploaded via |
uv/0.10.9 {"installer":{"name":"uv","version":"0.10.9","subcommand":["publish"]},"python":null,"implementation":{"name":null,"version":null},"distro":{"name":"Ubuntu","version":"24.04","id":"noble","libc":null},"system":{"name":null,"release":null},"cpu":null,"openssl_version":null,"setuptools_version":null,"rustc_version":null,"ci":null}
|
Release files / ntro-0.1.2-py3-none-any.whl
| Download URL | ntro-0.1.2-py3-none-any.whl |
|---|---|
| Size | 217.4 kB |
| Tags | Python 3 |
|
SHA-256 checksum How to use checksums |
21ce6e5c1fb88af34bc07537e1c17b4ef3504a0f0f37180700d1978794be56ef
|
|
BLAKE2b-256 checksum How to use checksums |
d0e245c2b3592ca7f64356c3ce6a1881f5ad817126f7c45bf275a0008466cbb1
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
No |
| Uploaded via |
uv/0.10.9 {"installer":{"name":"uv","version":"0.10.9","subcommand":["publish"]},"python":null,"implementation":{"name":null,"version":null},"distro":{"name":"Ubuntu","version":"24.04","id":"noble","libc":null},"system":{"name":null,"release":null},"cpu":null,"openssl_version":null,"setuptools_version":null,"rustc_version":null,"ci":null}
|
Security for ntro 0.1.2
Provenance
Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.
No provenance found
None of the files in this release were uploaded with provenance metadata.
A note on package security
PyPI is a repository for distributing packages, not a service for auditing them. While third-party security researchers and our staff do their best to detect and remove malicious packages, PyPI cannot guarantee the quality, security or suitability of any given package.
To learn more about protecting yourself from malicious actors, visit our package security documentation.