c3po-sdk 0.4.0a1__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.
Files changed (63) hide show
  1. c3po_sdk-0.4.0a1/PKG-INFO +129 -0
  2. c3po_sdk-0.4.0a1/README.md +108 -0
  3. c3po_sdk-0.4.0a1/pyproject.toml +113 -0
  4. c3po_sdk-0.4.0a1/src/c3po_sdk/__init__.py +5 -0
  5. c3po_sdk-0.4.0a1/src/c3po_sdk/cli/__init__.py +5 -0
  6. c3po_sdk-0.4.0a1/src/c3po_sdk/cli/commands/__init__.py +1 -0
  7. c3po_sdk-0.4.0a1/src/c3po_sdk/cli/commands/cluster.py +90 -0
  8. c3po_sdk-0.4.0a1/src/c3po_sdk/cli/commands/configure.py +123 -0
  9. c3po_sdk-0.4.0a1/src/c3po_sdk/cli/commands/dataset.py +173 -0
  10. c3po_sdk-0.4.0a1/src/c3po_sdk/cli/commands/kb.py +101 -0
  11. c3po_sdk-0.4.0a1/src/c3po_sdk/cli/commands/node.py +592 -0
  12. c3po_sdk-0.4.0a1/src/c3po_sdk/cli/commands/program.py +90 -0
  13. c3po_sdk-0.4.0a1/src/c3po_sdk/cli/commands/tenant.py +143 -0
  14. c3po_sdk-0.4.0a1/src/c3po_sdk/cli/commands/user.py +210 -0
  15. c3po_sdk-0.4.0a1/src/c3po_sdk/cli/context.py +172 -0
  16. c3po_sdk-0.4.0a1/src/c3po_sdk/cli/main.py +88 -0
  17. c3po_sdk-0.4.0a1/src/c3po_sdk/cli/output/__init__.py +179 -0
  18. c3po_sdk-0.4.0a1/src/c3po_sdk/cli/pagination.py +69 -0
  19. c3po_sdk-0.4.0a1/src/c3po_sdk/cli/utils/__init__.py +1 -0
  20. c3po_sdk-0.4.0a1/src/c3po_sdk/cli/utils/download.py +56 -0
  21. c3po_sdk-0.4.0a1/src/c3po_sdk/cli/utils/tree.py +53 -0
  22. c3po_sdk-0.4.0a1/src/c3po_sdk/client/__init__.py +6 -0
  23. c3po_sdk-0.4.0a1/src/c3po_sdk/client/client.py +201 -0
  24. c3po_sdk-0.4.0a1/src/c3po_sdk/client/config/__init__.py +1 -0
  25. c3po_sdk-0.4.0a1/src/c3po_sdk/client/config/credentials.py +88 -0
  26. c3po_sdk-0.4.0a1/src/c3po_sdk/client/config/settings.py +119 -0
  27. c3po_sdk-0.4.0a1/src/c3po_sdk/client/exceptions.py +17 -0
  28. c3po_sdk-0.4.0a1/src/c3po_sdk/client/models/__init__.py +133 -0
  29. c3po_sdk-0.4.0a1/src/c3po_sdk/client/models/_enums.py +55 -0
  30. c3po_sdk-0.4.0a1/src/c3po_sdk/client/models/_utils.py +94 -0
  31. c3po_sdk-0.4.0a1/src/c3po_sdk/client/models/auth.py +19 -0
  32. c3po_sdk-0.4.0a1/src/c3po_sdk/client/models/cluster.py +28 -0
  33. c3po_sdk-0.4.0a1/src/c3po_sdk/client/models/conversation.py +52 -0
  34. c3po_sdk-0.4.0a1/src/c3po_sdk/client/models/dataset.py +42 -0
  35. c3po_sdk-0.4.0a1/src/c3po_sdk/client/models/invitation.py +33 -0
  36. c3po_sdk-0.4.0a1/src/c3po_sdk/client/models/kb.py +58 -0
  37. c3po_sdk-0.4.0a1/src/c3po_sdk/client/models/node.py +112 -0
  38. c3po_sdk-0.4.0a1/src/c3po_sdk/client/models/pipeline.py +58 -0
  39. c3po_sdk-0.4.0a1/src/c3po_sdk/client/models/program.py +52 -0
  40. c3po_sdk-0.4.0a1/src/c3po_sdk/client/models/tenant.py +41 -0
  41. c3po_sdk-0.4.0a1/src/c3po_sdk/client/models/user.py +52 -0
  42. c3po_sdk-0.4.0a1/src/c3po_sdk/client/namespaces/__init__.py +42 -0
  43. c3po_sdk-0.4.0a1/src/c3po_sdk/client/namespaces/_helpers.py +15 -0
  44. c3po_sdk-0.4.0a1/src/c3po_sdk/client/namespaces/auth.py +134 -0
  45. c3po_sdk-0.4.0a1/src/c3po_sdk/client/namespaces/dataset.py +64 -0
  46. c3po_sdk-0.4.0a1/src/c3po_sdk/client/namespaces/kb.py +134 -0
  47. c3po_sdk-0.4.0a1/src/c3po_sdk/client/namespaces/node.py +186 -0
  48. c3po_sdk-0.4.0a1/src/c3po_sdk/client/namespaces/tenant.py +73 -0
  49. c3po_sdk-0.4.0a1/src/c3po_sdk/client/namespaces/user.py +99 -0
  50. c3po_sdk-0.4.0a1/src/c3po_sdk/client/session.py +53 -0
  51. c3po_sdk-0.4.0a1/src/c3po_sdk/mcp/__init__.py +1 -0
  52. c3po_sdk-0.4.0a1/src/c3po_sdk/mcp/context.py +41 -0
  53. c3po_sdk-0.4.0a1/src/c3po_sdk/mcp/descriptions.py +152 -0
  54. c3po_sdk-0.4.0a1/src/c3po_sdk/mcp/middleware.py +18 -0
  55. c3po_sdk-0.4.0a1/src/c3po_sdk/mcp/server.py +46 -0
  56. c3po_sdk-0.4.0a1/src/c3po_sdk/mcp/tools/__init__.py +1 -0
  57. c3po_sdk-0.4.0a1/src/c3po_sdk/mcp/tools/cluster.py +30 -0
  58. c3po_sdk-0.4.0a1/src/c3po_sdk/mcp/tools/dataset.py +62 -0
  59. c3po_sdk-0.4.0a1/src/c3po_sdk/mcp/tools/kb.py +15 -0
  60. c3po_sdk-0.4.0a1/src/c3po_sdk/mcp/tools/node.py +111 -0
  61. c3po_sdk-0.4.0a1/src/c3po_sdk/mcp/tools/program.py +30 -0
  62. c3po_sdk-0.4.0a1/src/c3po_sdk/mcp/tools/tenant.py +52 -0
  63. c3po_sdk-0.4.0a1/src/c3po_sdk/mcp/tools/user.py +44 -0
@@ -0,0 +1,129 @@
1
+ Metadata-Version: 2.3
2
+ Name: c3po-sdk
3
+ Version: 0.4.0a1
4
+ Summary: Python SDK, CLI, and MCP server for the C3PO research platform
5
+ Classifier: Programming Language :: Python :: 3 :: Only
6
+ Classifier: Programming Language :: Python :: 3.11
7
+ Classifier: Programming Language :: Python :: 3.12
8
+ Classifier: Programming Language :: Python :: 3.13
9
+ Classifier: Programming Language :: Python :: 3.14
10
+ Requires-Dist: asyncer>=0.0.8
11
+ Requires-Dist: fastmcp>=2.0
12
+ Requires-Dist: httpx>=0.27
13
+ Requires-Dist: pydantic-settings>=2.0
14
+ Requires-Dist: rich>=13.0
15
+ Requires-Dist: sentry-sdk>=2.59
16
+ Requires-Dist: typer>=0.26
17
+ Requires-Dist: prompt-toolkit>=3.0 ; extra == 'interactive'
18
+ Requires-Python: >=3.11
19
+ Provides-Extra: interactive
20
+ Description-Content-Type: text/markdown
21
+
22
+ # c3po-sdk
23
+
24
+ Python SDK, CLI, and MCP server for the C3PO research platform.
25
+
26
+ ## Installation
27
+
28
+ ```bash
29
+ uv tool install c3po-sdk
30
+ ```
31
+
32
+ ## Modules
33
+
34
+ ### Client (`c3po_sdk.client`)
35
+
36
+ Python SDK for the C3PO API.
37
+
38
+ ```python
39
+ from c3po_sdk.client.client import C3poClient
40
+
41
+ async with C3poClient() as client:
42
+ cursor: str | None = None
43
+ while True:
44
+ page = await client.datasets.list(last_evaluated_key=cursor, limit=50)
45
+ for dataset in page.items:
46
+ ...
47
+ if not page.has_more:
48
+ break
49
+ cursor = page.last_evaluated_key
50
+ ```
51
+
52
+ ### CLI (`c3po_sdk.cli`)
53
+
54
+ ```bash
55
+ c3po --help
56
+ ```
57
+
58
+ #### CLI commands
59
+
60
+ Global options (where supported): `--output` / `-o` (`rich` | `plain` | `json`), `--profile`, `--version` / `-V`. Use `c3po <group> <command> --help` for flags and arguments.
61
+
62
+ **auth**
63
+
64
+ - `c3po auth status`
65
+
66
+ **configure**
67
+
68
+ - `c3po configure auth`
69
+
70
+ **datasets**
71
+
72
+ - `c3po datasets list`
73
+ - `c3po datasets get`
74
+ - `c3po datasets update`
75
+ - `c3po datasets tree`
76
+ - `c3po datasets assets`
77
+
78
+ **kb** (analytics knowledge base — `POST /v1/kb/query`: NL question → generated SQL and executed result rows in one response)
79
+
80
+ - `c3po kb query [QUESTION]` — pass the question as an argument or pipe text on stdin; use `--output json` for the raw `sql` / `columns` / `rows` object.
81
+ - **Timeouts:** In CDK this route uses the streaming API Lambda with a **15-minute** Lambda and API Gateway integration timeout (`c3po_backend/services/api/_constructs/api.py`). The SDK applies the same ceiling for the HTTP **read** timeout so long NL2SQL + Redshift runs are not cut off early by the default httpx limits used elsewhere.
82
+
83
+ **nodes** (all subcommands take a `NODE_ID` unless noted)
84
+
85
+ - `c3po nodes get NODE_ID` — node details; optional `--tree` for dataset subtree
86
+ - `c3po nodes update NODE_ID` — `--name`, `--description`
87
+ - `c3po nodes reset NODE_ID` — clear pipeline outputs (`--force` / `-f`)
88
+ - `c3po nodes cluster NODE_ID` — cell clustering (`--obs-variable`, repeatable `--obs-values`, `--run-pipeline`)
89
+ - `c3po nodes assets NODE_ID` — presigned asset URLs; `--download` to fetch files
90
+
91
+ **nodes pipeline**
92
+
93
+ - `c3po nodes pipeline run NODE_ID` — start execution (see `--help` for Leiden / IDS params); `--watch` / `--no-watch`
94
+ - `c3po nodes pipeline list NODE_ID` — paginated history (`--all` / `-a`, `--limit`)
95
+ - `c3po nodes pipeline status NODE_ID EXECUTION_ID` — Step Functions payload; `--watch` to poll until terminal
96
+
97
+ **nodes programs** (gene programs for a node)
98
+
99
+ - `c3po nodes programs list NODE_ID` — paginated (`--all` / `-a`, `--limit`)
100
+ - `c3po nodes programs get NODE_ID PROGRAM_ID` — full program record
101
+
102
+ **nodes clusters** (cell clusters for a node)
103
+
104
+ - `c3po nodes clusters list NODE_ID` — paginated (`--all` / `-a`, `--limit`)
105
+ - `c3po nodes clusters get NODE_ID CLUSTER_ID` — full cluster record
106
+
107
+ **tenants**
108
+
109
+ - `c3po tenants me`
110
+ - `c3po tenants users list`
111
+ - `c3po tenants credentials generate`
112
+ - `c3po tenants credentials regenerate`
113
+
114
+ **users**
115
+
116
+ - `c3po users me`
117
+ - `c3po users update`
118
+ - `c3po users tenants list`
119
+ - `c3po users tokens list`
120
+ - `c3po users tokens create`
121
+ - `c3po users tokens revoke`
122
+
123
+ ### MCP Server (`c3po_sdk.mcp`)
124
+
125
+ See [docs/MCP.md](docs/MCP.md) for detailed configuration instructions.
126
+
127
+ ```bash
128
+ c3po.mcp
129
+ ```
@@ -0,0 +1,108 @@
1
+ # c3po-sdk
2
+
3
+ Python SDK, CLI, and MCP server for the C3PO research platform.
4
+
5
+ ## Installation
6
+
7
+ ```bash
8
+ uv tool install c3po-sdk
9
+ ```
10
+
11
+ ## Modules
12
+
13
+ ### Client (`c3po_sdk.client`)
14
+
15
+ Python SDK for the C3PO API.
16
+
17
+ ```python
18
+ from c3po_sdk.client.client import C3poClient
19
+
20
+ async with C3poClient() as client:
21
+ cursor: str | None = None
22
+ while True:
23
+ page = await client.datasets.list(last_evaluated_key=cursor, limit=50)
24
+ for dataset in page.items:
25
+ ...
26
+ if not page.has_more:
27
+ break
28
+ cursor = page.last_evaluated_key
29
+ ```
30
+
31
+ ### CLI (`c3po_sdk.cli`)
32
+
33
+ ```bash
34
+ c3po --help
35
+ ```
36
+
37
+ #### CLI commands
38
+
39
+ Global options (where supported): `--output` / `-o` (`rich` | `plain` | `json`), `--profile`, `--version` / `-V`. Use `c3po <group> <command> --help` for flags and arguments.
40
+
41
+ **auth**
42
+
43
+ - `c3po auth status`
44
+
45
+ **configure**
46
+
47
+ - `c3po configure auth`
48
+
49
+ **datasets**
50
+
51
+ - `c3po datasets list`
52
+ - `c3po datasets get`
53
+ - `c3po datasets update`
54
+ - `c3po datasets tree`
55
+ - `c3po datasets assets`
56
+
57
+ **kb** (analytics knowledge base — `POST /v1/kb/query`: NL question → generated SQL and executed result rows in one response)
58
+
59
+ - `c3po kb query [QUESTION]` — pass the question as an argument or pipe text on stdin; use `--output json` for the raw `sql` / `columns` / `rows` object.
60
+ - **Timeouts:** In CDK this route uses the streaming API Lambda with a **15-minute** Lambda and API Gateway integration timeout (`c3po_backend/services/api/_constructs/api.py`). The SDK applies the same ceiling for the HTTP **read** timeout so long NL2SQL + Redshift runs are not cut off early by the default httpx limits used elsewhere.
61
+
62
+ **nodes** (all subcommands take a `NODE_ID` unless noted)
63
+
64
+ - `c3po nodes get NODE_ID` — node details; optional `--tree` for dataset subtree
65
+ - `c3po nodes update NODE_ID` — `--name`, `--description`
66
+ - `c3po nodes reset NODE_ID` — clear pipeline outputs (`--force` / `-f`)
67
+ - `c3po nodes cluster NODE_ID` — cell clustering (`--obs-variable`, repeatable `--obs-values`, `--run-pipeline`)
68
+ - `c3po nodes assets NODE_ID` — presigned asset URLs; `--download` to fetch files
69
+
70
+ **nodes pipeline**
71
+
72
+ - `c3po nodes pipeline run NODE_ID` — start execution (see `--help` for Leiden / IDS params); `--watch` / `--no-watch`
73
+ - `c3po nodes pipeline list NODE_ID` — paginated history (`--all` / `-a`, `--limit`)
74
+ - `c3po nodes pipeline status NODE_ID EXECUTION_ID` — Step Functions payload; `--watch` to poll until terminal
75
+
76
+ **nodes programs** (gene programs for a node)
77
+
78
+ - `c3po nodes programs list NODE_ID` — paginated (`--all` / `-a`, `--limit`)
79
+ - `c3po nodes programs get NODE_ID PROGRAM_ID` — full program record
80
+
81
+ **nodes clusters** (cell clusters for a node)
82
+
83
+ - `c3po nodes clusters list NODE_ID` — paginated (`--all` / `-a`, `--limit`)
84
+ - `c3po nodes clusters get NODE_ID CLUSTER_ID` — full cluster record
85
+
86
+ **tenants**
87
+
88
+ - `c3po tenants me`
89
+ - `c3po tenants users list`
90
+ - `c3po tenants credentials generate`
91
+ - `c3po tenants credentials regenerate`
92
+
93
+ **users**
94
+
95
+ - `c3po users me`
96
+ - `c3po users update`
97
+ - `c3po users tenants list`
98
+ - `c3po users tokens list`
99
+ - `c3po users tokens create`
100
+ - `c3po users tokens revoke`
101
+
102
+ ### MCP Server (`c3po_sdk.mcp`)
103
+
104
+ See [docs/MCP.md](docs/MCP.md) for detailed configuration instructions.
105
+
106
+ ```bash
107
+ c3po.mcp
108
+ ```
@@ -0,0 +1,113 @@
1
+ [build-system]
2
+ build-backend = "uv_build"
3
+ requires = [ "uv-build>=0.11.7,<0.12.0" ]
4
+
5
+ [project]
6
+ name = "c3po-sdk"
7
+ version = "0.4.0a1"
8
+ description = "Python SDK, CLI, and MCP server for the C3PO research platform"
9
+ readme = "README.md"
10
+ requires-python = ">=3.11"
11
+ classifiers = [
12
+ "Programming Language :: Python :: 3 :: Only",
13
+ "Programming Language :: Python :: 3.11",
14
+ "Programming Language :: Python :: 3.12",
15
+ "Programming Language :: Python :: 3.13",
16
+ "Programming Language :: Python :: 3.14",
17
+ ]
18
+ dependencies = [
19
+ "asyncer>=0.0.8",
20
+ "fastmcp>=2.0",
21
+ "httpx>=0.27",
22
+ "pydantic-settings>=2.0",
23
+ "rich>=13.0",
24
+ "sentry-sdk>=2.59",
25
+ "typer>=0.26",
26
+ ]
27
+ optional-dependencies.interactive = [ "prompt-toolkit>=3.0" ]
28
+ scripts."c3po.mcp" = "c3po_sdk.mcp.server:main"
29
+ scripts.c3po = "c3po_sdk.cli.main:app"
30
+
31
+ [dependency-groups]
32
+ dev = [
33
+ "polyfactory>=2.0",
34
+ "pre-commit>=4.5.1",
35
+ "pytest>=8.0",
36
+ "pytest-asyncio>=0.23",
37
+ "pytest-cov>=5.0",
38
+ "respx>=0.21",
39
+ "ruff>=0.15.12",
40
+ "ty>=0.0.30",
41
+ ]
42
+
43
+ [tool.uv]
44
+ exclude-newer = "P7D"
45
+ exclude-newer-package = { typer = "P0D" }
46
+
47
+ [tool.ruff]
48
+ extend-exclude = [ "uv.lock" ]
49
+ lint.extend-select = [
50
+ "B",
51
+ "C4",
52
+ "D",
53
+ "FLY",
54
+ "I",
55
+ "ICN",
56
+ "N",
57
+ "NPY",
58
+ "PERF",
59
+ "PTH",
60
+ "RET",
61
+ "RUF",
62
+ "SIM",
63
+ "UP",
64
+ ]
65
+ lint.ignore = [
66
+ "D100",
67
+ "D104",
68
+ "D107",
69
+ "D212",
70
+ "N802",
71
+ "N803",
72
+ "N806",
73
+ ]
74
+ lint.per-file-ignores."__init__.py" = [ "F401", "F403" ]
75
+ lint.per-file-ignores."tests/**" = [ "D100", "D101", "D102", "D103" ]
76
+ lint.pydocstyle.convention = "google"
77
+
78
+ [tool.pyproject-fmt]
79
+ keep_full_version = true
80
+
81
+ [tool.ty]
82
+ rules.possibly-missing-attribute = "ignore"
83
+ src.exclude = [
84
+ "**/site-packages",
85
+ "**/.venv",
86
+ "**/.ruff_cache",
87
+ "**/.pytest_cache",
88
+ "**/htmlcov",
89
+ "**/node_modules",
90
+ "**/__pycache__",
91
+ "**/.*",
92
+ ]
93
+
94
+ [tool.pytest]
95
+ ini_options.testpaths = [ "tests" ]
96
+ ini_options.asyncio_mode = "auto"
97
+ ini_options.addopts = "--import-mode=importlib --cov=. --cov-report=html"
98
+ ini_options.filterwarnings = [
99
+ "ignore:datetime.datetime.utcnow\\(\\) is deprecated",
100
+ "ignore::FutureWarning",
101
+ "ignore::DeprecationWarning",
102
+ ]
103
+ ini_options.python_files = [ "*_tests.py", "test_*.py", "tests.py" ]
104
+
105
+ [tool.coverage]
106
+ run.branch = true
107
+ run.omit = [ "*/tests*", "*/tests/*", "*__init__*" ]
108
+ run.relative_files = true
109
+ report.exclude_lines = [ "pragma: no cover" ]
110
+ report.include = [ "src/*" ]
111
+ report.include_namespace_packages = true
112
+ report.omit = [ "*/tests*", "*/tests/*", "*__init__*" ]
113
+ report.show_missing = true
@@ -0,0 +1,5 @@
1
+ """c3po_sdk — Python SDK, CLI, and MCP server for the C3PO research platform."""
2
+
3
+ from importlib.metadata import version
4
+
5
+ __version__ = version("c3po-sdk")
@@ -0,0 +1,5 @@
1
+ """c3po_sdk.cli package."""
2
+
3
+ from c3po_sdk import __version__
4
+
5
+ __all__ = ["__version__"]
@@ -0,0 +1 @@
1
+ """c3po_sdk.cli.commands package."""
@@ -0,0 +1,90 @@
1
+ """Cell cluster sub-commands under ``c3po nodes clusters``."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from functools import partial
6
+ from typing import Annotated
7
+
8
+ import typer
9
+ from asyncer import syncify
10
+
11
+ from c3po_sdk.cli.context import GlobalCommand, get_ctx
12
+ from c3po_sdk.cli.output import get_console
13
+ from c3po_sdk.cli.pagination import paginate
14
+ from c3po_sdk.client.exceptions import ApiError
15
+ from c3po_sdk.client.models import ClusterList, Page
16
+ from c3po_sdk.client.session import Session
17
+
18
+ clusters_app = typer.Typer(
19
+ name="clusters",
20
+ help="Cluster commands.",
21
+ no_args_is_help=True,
22
+ )
23
+
24
+
25
+ @clusters_app.command("list", cls=GlobalCommand)
26
+ @partial(syncify, raise_sync_error=False)
27
+ async def clusters_list(
28
+ node_id: Annotated[str, typer.Argument(help="Node ID")],
29
+ fetch_all: Annotated[
30
+ bool,
31
+ typer.Option(
32
+ "--all",
33
+ "-a",
34
+ help="Fetch every page (otherwise only the first page is shown).",
35
+ ),
36
+ ] = False,
37
+ limit: Annotated[
38
+ int,
39
+ typer.Option("--limit", min=1, max=100, help="Page size."),
40
+ ] = 10,
41
+ ) -> None:
42
+ """List cell clusters for a node."""
43
+ console = get_console()
44
+ ctx = get_ctx()
45
+
46
+ try:
47
+ async with Session(profile=ctx.profile).client() as client:
48
+
49
+ async def fetch(cursor: str | None) -> Page[ClusterList]:
50
+ return await client.nodes.clusters.list(
51
+ node_id,
52
+ last_evaluated_key=cursor,
53
+ limit=limit,
54
+ )
55
+
56
+ items = await paginate(fetch, fetch_all=fetch_all)
57
+ except ApiError as exc:
58
+ console.error(f"Failed to list clusters: {exc}")
59
+ raise typer.Exit(code=1) from None
60
+
61
+ console.rows_table(
62
+ items,
63
+ columns={
64
+ "id",
65
+ "label",
66
+ "created_at",
67
+ "updated_at",
68
+ },
69
+ title="Clusters",
70
+ )
71
+
72
+
73
+ @clusters_app.command("get", cls=GlobalCommand)
74
+ @partial(syncify, raise_sync_error=False)
75
+ async def clusters_get(
76
+ node_id: Annotated[str, typer.Argument(help="Node ID")],
77
+ cluster_id: Annotated[str, typer.Argument(help="Cluster ID")],
78
+ ) -> None:
79
+ """Show full details of a cell cluster."""
80
+ console = get_console()
81
+ ctx = get_ctx()
82
+
83
+ try:
84
+ async with Session(profile=ctx.profile).client() as client:
85
+ cluster = await client.nodes.clusters.get(node_id, cluster_id)
86
+ except ApiError as exc:
87
+ console.error(f"Failed to fetch cluster: {exc}")
88
+ raise typer.Exit(code=1) from None
89
+
90
+ console.kv_table(cluster, title="Cluster")
@@ -0,0 +1,123 @@
1
+ """Configure commands: one-time credential setup."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from functools import partial
6
+
7
+ import typer
8
+ from asyncer import syncify
9
+
10
+ from c3po_sdk.cli.context import GlobalCommand, get_ctx
11
+ from c3po_sdk.cli.output import get_console
12
+ from c3po_sdk.client.client import C3poClient
13
+ from c3po_sdk.client.config.credentials import (
14
+ _CREDENTIALS_PATH,
15
+ CredentialsFile,
16
+ Profile,
17
+ )
18
+ from c3po_sdk.client.config.settings import Settings
19
+ from c3po_sdk.client.exceptions import ApiError
20
+ from c3po_sdk.client.models._utils import RichModel
21
+
22
+
23
+ class _AuthResult(RichModel):
24
+ email: str
25
+ tenant_id: str
26
+ profile: str
27
+
28
+
29
+ configure_app = typer.Typer(
30
+ name="configure",
31
+ help="Configure the CLI.",
32
+ no_args_is_help=True,
33
+ )
34
+
35
+
36
+ @configure_app.command(name="auth", cls=GlobalCommand)
37
+ @partial(syncify, raise_sync_error=False)
38
+ async def configure_auth() -> None:
39
+ """Authenticate and store a user access token."""
40
+ console = get_console()
41
+ ctx = get_ctx()
42
+ profile = ctx.profile or "default"
43
+
44
+ creds = CredentialsFile.load(_CREDENTIALS_PATH)
45
+ existing = creds.get_profile(profile)
46
+ if (
47
+ existing
48
+ and existing.api_token
49
+ and not typer.confirm(
50
+ f"Profile '{profile}' already has credentials. Overwrite?",
51
+ default=False,
52
+ )
53
+ ):
54
+ raise typer.Exit()
55
+
56
+ email = typer.prompt("Email")
57
+
58
+ async with C3poClient(settings=Settings(api_token="dummy")) as client:
59
+ with console.status("Sending sign-in code…"):
60
+ try:
61
+ signin_resp = await client.auth.signin(email)
62
+ except ApiError as exc:
63
+ console.error(f"Sign-in failed: {exc}")
64
+ raise typer.Exit(code=1) from None
65
+ console.print("[dim]A sign-in code has been sent to your email.[/dim]")
66
+
67
+ code = typer.prompt("Code")
68
+
69
+ with console.status("Verifying code…"):
70
+ try:
71
+ token_resp = await client.auth.token(
72
+ "authorization_code",
73
+ code=code,
74
+ tenant_id=signin_resp.tenant_id,
75
+ )
76
+ except ApiError as exc:
77
+ console.error(f"Authentication failed: {exc}")
78
+ raise typer.Exit(code=1) from None
79
+
80
+ jwt_settings = Settings(api_token=token_resp.access_token)
81
+ async with C3poClient(jwt_settings) as jwt_client:
82
+ with console.status("Fetching tenants…"):
83
+ try:
84
+ tenants_page = await jwt_client.user.tenants.list()
85
+ except ApiError as exc:
86
+ console.error(f"Failed to list tenants: {exc}")
87
+ raise typer.Exit(code=1) from None
88
+ tenants = [{"id": t.id, "name": t.name} for t in tenants_page.items]
89
+
90
+ if not tenants:
91
+ console.error("No tenants available for this account.")
92
+ raise typer.Exit(code=1)
93
+
94
+ if len(tenants) == 1:
95
+ chosen = tenants[0]
96
+ console.print(f"Authenticated to tenant: [bold]{chosen['name']}[/bold]")
97
+ else:
98
+ chosen = console.select(
99
+ tenants,
100
+ heading="Available tenants:",
101
+ label="name",
102
+ detail="id",
103
+ prompt="Select a tenant",
104
+ )
105
+
106
+ with console.status("Creating access token…"):
107
+ try:
108
+ uat = await jwt_client.user.tokens.create(name=profile)
109
+ except ApiError as exc:
110
+ console.error(f"Failed to create access token: {exc}")
111
+ raise typer.Exit(code=1) from None
112
+
113
+ creds = CredentialsFile.load(_CREDENTIALS_PATH)
114
+ creds.set_profile(
115
+ Profile(api_token=uat.token, tenant_id=chosen["id"]),
116
+ name=profile,
117
+ )
118
+ creds.save(_CREDENTIALS_PATH)
119
+
120
+ console.kv_table(
121
+ _AuthResult(email=email, tenant_id=chosen["id"], profile=profile),
122
+ title="Authenticated",
123
+ )