k9o 1.0.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.
k9o/__init__.py ADDED
@@ -0,0 +1,143 @@
1
+ """
2
+ k9o — Kilimanjaro platform SDK (Python; idiomatic).
3
+
4
+ Canonical usage:
5
+
6
+ from k9o import create_client, KError
7
+
8
+ k9o = create_client(
9
+ base_url="https://acme.api.k9o.io",
10
+ api_key=os.environ["K9O_API_KEY"],
11
+ org_slug="acme",
12
+ project_slug="member-portal",
13
+ )
14
+
15
+ try:
16
+ response = k9o.from_("posts").select().limit(10).execute()
17
+ for row in response.data:
18
+ print(row)
19
+ session = k9o.gate.sign_in(email="user@example.com", password="…")
20
+ print("signed in:", session.data["user"]["id"])
21
+ except KError as e:
22
+ print(f"[{e.code}] {e.message}: {e.recommendation}")
23
+
24
+ Architectural posture: hand-authored idiomatic SDK against the platform API specification
25
+ + services/gateway/openapi.json contract source; replaces the prior
26
+ OpenAPI-generator output (`k9o_gate` raw client) with ch13 idiomatic
27
+ shape (snake_case methods + raise KError + envelope dataclass with
28
+ typed meta accessor sugar + data DSL).
29
+ """
30
+
31
+ from __future__ import annotations
32
+
33
+ from .audit import AuditNamespace
34
+ from .client import K9oClient, create_client
35
+ from .compute import ComputeNamespace, LlmsNamespace, LLM_MODEL_IDS, LLM_ERROR_CODES
36
+ from .data import FromBuilder, SelectBuilder, UpdateBuilder, DeleteBuilder
37
+ from .functions import FunctionsNamespace
38
+ from .kibo_vault import KiboNamespace, SecretsNamespace, VaultNamespace
39
+ from .me import MeNamespace
40
+ from .migrations import MigrationsNamespace
41
+ from .org import OrgNamespace
42
+ from .orgs import OrgsNamespace
43
+ from .policies import PoliciesNamespace
44
+ from .realtime import RealtimeNamespace, realtime_topic, REALTIME_TOPIC_FAMILIES
45
+ from .studio import (
46
+ StudioNamespace,
47
+ WorkflowsNamespace,
48
+ DashboardsNamespace,
49
+ WORKFLOW_ERROR_CODES,
50
+ DASHBOARD_ERROR_CODES,
51
+ WORKFLOW_TRIGGER_TYPES,
52
+ WORKFLOW_ACTION_TYPES,
53
+ WIDGET_TYPES,
54
+ )
55
+ from .envelope import (
56
+ Envelope,
57
+ Meta,
58
+ MetaContext,
59
+ MetaCursor,
60
+ MetaIdempotency,
61
+ MetaSubscription,
62
+ MetaTiming,
63
+ MetaWarning,
64
+ )
65
+ from .errors import KError, KErrorWire, kerror_from_wire
66
+ from .gate import GateNamespace
67
+ from .http import HttpClientConfig, RetryConfig
68
+ from .projects import Project, ProjectsNamespace
69
+ from .url_grammar import (
70
+ org_path,
71
+ platform_path,
72
+ project_path,
73
+ resolve_base_url,
74
+ validate_base_url,
75
+ validate_slug,
76
+ )
77
+
78
+ __version__ = "0.3.0"
79
+
80
+ __all__ = [
81
+ "__version__",
82
+ # Factory + client
83
+ "create_client",
84
+ "K9oClient",
85
+ # Error
86
+ "KError",
87
+ "KErrorWire",
88
+ "kerror_from_wire",
89
+ # Envelope + meta
90
+ "Envelope",
91
+ "Meta",
92
+ "MetaContext",
93
+ "MetaCursor",
94
+ "MetaIdempotency",
95
+ "MetaSubscription",
96
+ "MetaTiming",
97
+ "MetaWarning",
98
+ # Data DSL
99
+ "FromBuilder",
100
+ "SelectBuilder",
101
+ "UpdateBuilder",
102
+ "DeleteBuilder",
103
+ # Namespaces
104
+ "ProjectsNamespace",
105
+ "Project",
106
+ "GateNamespace",
107
+ "StudioNamespace",
108
+ "WorkflowsNamespace",
109
+ "DashboardsNamespace",
110
+ "ComputeNamespace",
111
+ "LlmsNamespace",
112
+ "RealtimeNamespace",
113
+ "realtime_topic",
114
+ "OrgNamespace",
115
+ "OrgsNamespace",
116
+ "MigrationsNamespace",
117
+ "FunctionsNamespace",
118
+ "PoliciesNamespace",
119
+ "AuditNamespace",
120
+ "MeNamespace",
121
+ "KiboNamespace",
122
+ "VaultNamespace",
123
+ "SecretsNamespace",
124
+ # P6 closed enums + error-code maps
125
+ "WORKFLOW_ERROR_CODES",
126
+ "DASHBOARD_ERROR_CODES",
127
+ "LLM_ERROR_CODES",
128
+ "WORKFLOW_TRIGGER_TYPES",
129
+ "WORKFLOW_ACTION_TYPES",
130
+ "WIDGET_TYPES",
131
+ "LLM_MODEL_IDS",
132
+ "REALTIME_TOPIC_FAMILIES",
133
+ # URL grammar
134
+ "validate_base_url",
135
+ "resolve_base_url",
136
+ "validate_slug",
137
+ "project_path",
138
+ "org_path",
139
+ "platform_path",
140
+ # HTTP config (advanced use)
141
+ "HttpClientConfig",
142
+ "RetryConfig",
143
+ ]
k9o/ai.py ADDED
@@ -0,0 +1,45 @@
1
+ """
2
+ AI namespace — `client.ai.*` (platform AI primitives).
3
+
4
+ Wire mapping (project-plane):
5
+ - embed(input, model=None) -> POST /@<slug>/ai/embeddings
6
+ body {model?, input: str | list[str]}
7
+ -> Envelope(data={"embeddings": [[float, ...], ...], "model": str, "dimensions": int})
8
+
9
+ Bounds (non-empty inputs, count, size, concurrency) are enforced by the
10
+ gateway, not here. The SDK sends `input` exactly as given and never puts it
11
+ in an error it constructs.
12
+ """
13
+
14
+ from __future__ import annotations
15
+
16
+ from typing import Sequence
17
+
18
+ from .envelope import Envelope
19
+ from .http import HttpClientConfig, request
20
+ from .url_grammar import project_path
21
+
22
+
23
+ def _require_project_slug(config: HttpClientConfig) -> str:
24
+ if not config.project_slug:
25
+ raise ValueError("ai operations require project_slug bound on the client")
26
+ return config.project_slug
27
+
28
+
29
+ class AiNamespace:
30
+ """`client.ai.*`."""
31
+
32
+ def __init__(self, config: HttpClientConfig) -> None:
33
+ self._config = config
34
+
35
+ def embed(self, input: str | Sequence[str], *, model: str | None = None) -> Envelope:
36
+ """POST /@<slug>/ai/embeddings — embed one string or a list of strings.
37
+
38
+ `model` is omitted from the body when None (server default). A single
39
+ string stays a string; a sequence is sent as a list, in order.
40
+ """
41
+ slug = _require_project_slug(self._config)
42
+ body: dict = {"input": input if isinstance(input, str) else list(input)}
43
+ if model is not None:
44
+ body["model"] = model
45
+ return request(self._config, method="POST", path=project_path(slug, "ai/embeddings"), body=body)
k9o/audit.py ADDED
@@ -0,0 +1,48 @@
1
+ """
2
+ Project-plane audit log namespace — `client.audit.*`.
3
+
4
+ Distinct from `client.org.audit_log` (org-plane) and
5
+ `client.me.audit_log` (operator-self). Python idiomatic per ch13 §2.1.1.
6
+ """
7
+
8
+ from __future__ import annotations
9
+
10
+ from datetime import datetime, timedelta, timezone
11
+ from typing import Any
12
+ from urllib.parse import quote
13
+
14
+ from .envelope import Envelope
15
+ from .http import HttpClientConfig, request
16
+ from .url_grammar import project_path
17
+
18
+
19
+ def _require_project_slug(config: HttpClientConfig) -> str:
20
+ if not config.project_slug:
21
+ raise ValueError("audit operations require project_slug bound on the client")
22
+ return config.project_slug
23
+
24
+
25
+ def _clean(d: dict[str, Any]) -> dict[str, Any]:
26
+ return {k: v for k, v in d.items() if v is not None}
27
+
28
+
29
+ class AuditNamespace:
30
+ def __init__(self, config: HttpClientConfig) -> None:
31
+ self._config = config
32
+
33
+ def query(self, *, since: str | None = None, until: str | None = None, type: str | None = None,
34
+ actor_id: str | None = None, target_kind: str | None = None, target_id: str | None = None,
35
+ limit: int | None = None, cursor: str | None = None) -> Envelope:
36
+ """Query the audit log (reverse-chronological; filters AND-compose)."""
37
+ slug = _require_project_slug(self._config)
38
+ return request(self._config, method="GET", path=project_path(slug, "audit-log"),
39
+ query=_clean({"since": since, "until": until, "type": type, "actorId": actor_id,
40
+ "targetKind": target_kind, "targetId": target_id,
41
+ "limit": limit, "cursor": cursor}))
42
+
43
+ def tail(self, *, since_minutes: int = 1, limit: int = 100) -> Envelope:
44
+ """Convenience: recent events within a since-window."""
45
+ slug = _require_project_slug(self._config)
46
+ since = (datetime.now(timezone.utc) - timedelta(minutes=since_minutes)).isoformat()
47
+ return request(self._config, method="GET", path=project_path(slug, "audit-log"),
48
+ query={"since": since, "limit": limit})
@@ -0,0 +1,20 @@
1
+ """``k9o.barbican`` — free, embeddable L2 Keep guardrail (Python).
2
+
3
+ Unicode-normalize -> Aho-Corasick deny over a signed ruleset -> buffer-and-
4
+ scan output stream-filter. A thin wrapper over the audited ``k9o-keep-core``
5
+ engine, compiled to a native lib via uniffi (mirrors the ``k9o.kibo`` crypto
6
+ wrapper's FFI shape). ZERO security logic here: every normalization pass,
7
+ deny match, verdict, and output redaction is the core's; this layer only
8
+ forwards typed calls to the generated binding and maps its error -> ``KError``.
9
+
10
+ CLAIM BOUNDARY (security-claims-non-negotiable): Barbican is advisory /
11
+ defense-in-depth (Edge Filtration). "Un-bypassable" attaches ONLY to the
12
+ Enforced Boundary (Warden as the sole sandbox egress) -- never this SDK.
13
+
14
+ The native lib + generated uniffi binding under ``_ffi/`` are build artifacts
15
+ (generated per platform; gitignored) -- same convention as ``k9o.kibo``.
16
+ """
17
+
18
+ from .barbican import Barbican, OutputFilter
19
+
20
+ __all__ = ["Barbican", "OutputFilter"]
@@ -0,0 +1,5 @@
1
+ # Generated uniffi binding + native lib live here (build artifacts, gitignored).
2
+ # Regenerate: cargo build -p k9o-keep-core-ffi-uniffi --release --features cli
3
+ # + cargo run --bin uniffi-bindgen --features cli -- generate \
4
+ # --library target/release/libk9o_keep_core_ffi_uniffi.dylib \
5
+ # --language python --out-dir .
@@ -0,0 +1,218 @@
1
+ """Thin wrapper over the bound ``k9o-keep-core`` engine (native, via uniffi).
2
+
3
+ Sync throughout -- the core has no async fn (unlike ``k9o.kibo``'s crypto,
4
+ which calls a network KMS), and the rest of the ``k9o`` Python SDK is sync
5
+ (``requests``/``httpx``-style), so there is no async-for-future-proofing need
6
+ here the way there is in the TS SDK (JS network calls force async; Python
7
+ doesn't have that constraint -- a future hosted-tier escalation can stay sync).
8
+ """
9
+
10
+ from __future__ import annotations
11
+
12
+ import time
13
+ from typing import Any
14
+
15
+ from ..errors import KError
16
+ from ._ffi import k9o_keep_core_ffi_uniffi as _ffi
17
+
18
+
19
+ def _map_error(e: Exception, stage: str = "ruleset") -> KError:
20
+ """Map the bound core's ``KeepError`` -> ``KError``. ``KeepError.RulesetInvalid``
21
+ is the only variant today (a refused ruleset, manifest or key manifest); the
22
+ core's closed reason token (``unknown_kid`` / ``signature_invalid`` /
23
+ ``signature_non_canonical`` / ``version_stale`` / ``malformed`` / ... or an
24
+ unknown-severity message) rides in ``context.reason``, and ``context.stage``
25
+ says whether it came from the ruleset or the key manifest.
26
+ """
27
+ reason = getattr(e, "reason", None)
28
+ if reason is not None:
29
+ return KError(
30
+ code="K9O_BARBICAN_RULESET_INVALID",
31
+ message=str(e),
32
+ context={"reason": reason, "stage": stage},
33
+ )
34
+ return KError(code="K9O_BARBICAN_WASM_ERROR", message=str(e))
35
+
36
+
37
+ class OutputFilter:
38
+ """A streaming output redactor. Buffer-and-scan-before-flush: never emits a
39
+ byte the core has not cleared (no RST-after-leak)."""
40
+
41
+ def __init__(self, handle: Any) -> None:
42
+ self._handle = handle
43
+
44
+ def push(self, chunk: str) -> str:
45
+ """Feed a chunk; returns the cleared (redacted) prefix safe to emit now."""
46
+ return self._handle.push(chunk)
47
+
48
+ def finish(self) -> str:
49
+ """Flush the residual buffer (also scanned) at end of stream."""
50
+ return self._handle.finish()
51
+
52
+ def report(self) -> Any:
53
+ """Leak report (counts only -- never the leaked values): an object with
54
+ ``emails``, ``secrets``, ``markers``, ``total``."""
55
+ return self._handle.report()
56
+
57
+
58
+ class Barbican:
59
+ """The free, embeddable L2 guardrail. See the package docstring for the
60
+ claim boundary."""
61
+
62
+ def __init__(
63
+ self,
64
+ *,
65
+ fail_open: bool = False,
66
+ leet: bool = True,
67
+ decode_depth: int = 0,
68
+ ) -> None:
69
+ """``decode_depth`` of 0 uses the core default (3) -- a bounded base64/
70
+ leetspeak de-obfuscation depth (decode-bomb guard)."""
71
+ self._core = _ffi.KeepCore(
72
+ _ffi.KeepConfig(fail_open=fail_open, leet=leet, decode_depth=decode_depth)
73
+ )
74
+
75
+ def load_ruleset(self, ruleset_version: int, rules: list[dict[str, str]]) -> int:
76
+ """Load a static ruleset bundled with your package (Mode-A path: provenance
77
+ is vouched for by your package, so signature verification is skipped --
78
+ the core still enforces anti-rollback, never loading a version <= the
79
+ current one). ``rules`` = ``[{"id", "pattern", "severity"}]``
80
+ (severity one of low|medium|high|critical). Returns the accepted version.
81
+ """
82
+ blob = _ffi.KeepRulesetBlob(
83
+ ruleset_version=ruleset_version,
84
+ rules=[
85
+ _ffi.KeepRule(id=r["id"], pattern=r["pattern"], severity=r["severity"])
86
+ for r in rules
87
+ ],
88
+ )
89
+ try:
90
+ return self._core.load_ruleset_unsigned(blob)
91
+ except _ffi.KeepError as e: # type: ignore[attr-defined]
92
+ raise _map_error(e) from e
93
+
94
+ def load_key_manifest(self, jws: str, now_secs: int | None = None) -> int:
95
+ """Load a Barbican key manifest (compact JWS, ``typ k9o.barbican.keys.v1``).
96
+ The core checks it against the root key compiled into it and, only if it
97
+ verifies, REPLACES the trusted ruleset-signing keys with the ones it lists.
98
+ A refusal (wrong signer, rollback, a key changing its public key, a
99
+ malformed entry) raises ``K9O_BARBICAN_RULESET_INVALID`` with
100
+ ``context.stage == "key_manifest"`` and leaves the trusted keys unchanged.
101
+ Returns the manifest ``sequence``."""
102
+ try:
103
+ return self._core.load_key_manifest(
104
+ jws, int(time.time()) if now_secs is None else now_secs
105
+ )
106
+ except _ffi.KeepError as e: # type: ignore[attr-defined]
107
+ raise _map_error(e, "key_manifest") from e
108
+
109
+ def verify_manifest(self, jws: str) -> Any:
110
+ """Verify a ruleset manifest (compact JWS) under a trusted key and return
111
+ its authenticated fields (``ruleset_version``, ``signing_key_kid``,
112
+ ``artifact_hash``, ``artifact_url``, ``valid_until``)."""
113
+ try:
114
+ return self._core.verify_manifest(jws)
115
+ except _ffi.KeepError as e: # type: ignore[attr-defined]
116
+ raise _map_error(e) from e
117
+
118
+ def load_ruleset_signed(
119
+ self,
120
+ ruleset_version: int,
121
+ rules: list[dict[str, str]],
122
+ signing_key_kid: str,
123
+ signature: bytes,
124
+ created_at: int = 0,
125
+ ) -> int:
126
+ """Load a signed ruleset. The core verifies the Ed25519 signature ITSELF,
127
+ over its own canonical bytes, under a key from the loaded key manifest:
128
+ there is no key or verifier to pass. Any failure (no key manifest loaded,
129
+ unknown key, bad or non-canonical signature, stale version, malformed
130
+ rules) raises ``K9O_BARBICAN_RULESET_INVALID`` with ``context.reason`` and
131
+ leaves the current ruleset running. Returns the accepted version."""
132
+ try:
133
+ return self._core.load_ruleset_signed(self._signed_blob(ruleset_version, rules, signing_key_kid, signature, created_at))
134
+ except _ffi.KeepError as e: # type: ignore[attr-defined]
135
+ raise _map_error(e) from e
136
+
137
+ def ruleset_digest(
138
+ self,
139
+ ruleset_version: int,
140
+ rules: list[dict[str, str]],
141
+ signing_key_kid: str,
142
+ signature: bytes,
143
+ created_at: int = 0,
144
+ ) -> str:
145
+ """Lowercase hex SHA-256 of a ruleset's canonical signing bytes -- the value
146
+ a manifest's ``artifact_hash`` names. A digest only; it decides no trust."""
147
+ try:
148
+ return self._core.ruleset_digest(self._signed_blob(ruleset_version, rules, signing_key_kid, signature, created_at))
149
+ except _ffi.KeepError as e: # type: ignore[attr-defined]
150
+ raise _map_error(e) from e
151
+
152
+ @staticmethod
153
+ def _signed_blob(ruleset_version, rules, signing_key_kid, signature, created_at) -> Any:
154
+ return _ffi.KeepSignedRulesetBlob(
155
+ ruleset_version=ruleset_version,
156
+ rules=[
157
+ _ffi.KeepRule(id=r["id"], pattern=r["pattern"], severity=r["severity"])
158
+ for r in rules
159
+ ],
160
+ signing_key_kid=signing_key_kid,
161
+ signature=bytes(signature),
162
+ created_at=created_at,
163
+ )
164
+
165
+ @property
166
+ def key_manifest_sequence(self) -> int:
167
+ """The ``sequence`` of the key manifest in force (0 before one is loaded)."""
168
+ return self._core.key_manifest_sequence()
169
+
170
+ @property
171
+ def signed_floor(self) -> int:
172
+ """The highest VERIFIED ruleset version loaded (0 = none): the anti-rollback floor for signed
173
+ updates. A refresh compares a published version against this, not the version in force."""
174
+ return self._core.signed_floor()
175
+
176
+ @property
177
+ def active_ruleset_is_signed(self) -> bool:
178
+ """Whether the ruleset in force came from a verified load. False when none is loaded, or when
179
+ ``load_ruleset`` (unsigned, host-vouched) replaced a verified one."""
180
+ return self._core.active_ruleset_is_signed()
181
+
182
+ def trusted_kids(self) -> list[str]:
183
+ """The signing ``kid`` s the loaded key manifest currently trusts, sorted."""
184
+ return self._core.trusted_kids()
185
+
186
+ def deprecated_kids(self) -> list[str]:
187
+ """The trusted ``kid`` s the key manifest flags ``deprecated``, sorted."""
188
+ return self._core.deprecated_kids()
189
+
190
+ @staticmethod
191
+ def uses_test_trust_root() -> bool:
192
+ """``False`` on every shipped build. ``True`` means this binding trusts the
193
+ PUBLISHED TEST root and must never be used outside tests."""
194
+ return _ffi.uses_test_trust_root()
195
+
196
+ def inspect(self, text: str, *, waypoint_class: str | None = None) -> Any:
197
+ """Inspect an input (normalize -> deny -> local verdict). Obfuscation is
198
+ peeled BEFORE matching, so static deny-lists aren't defeated by trivial
199
+ tricks. ``waypoint_class`` is the manifest's meter hint -- it NEVER gates
200
+ the decision. Returns the verdict object (``decision``, ``reason``,
201
+ ``matched``, ``schema_valid``, ``waypoint_class_hint``, ``fail_mode``,
202
+ ``ruleset_version``, ``core_version``)."""
203
+ return self._core.inspect_input(text, waypoint_class)
204
+
205
+ def classify(self, text: str) -> str:
206
+ """Local L3 stand-in classification (``"clear"|"suspicious"|"malicious"``).
207
+ No receipt -- a classification is not an enforcement decision."""
208
+ return self._core.classify_local(text)
209
+
210
+ def create_output_filter(self) -> OutputFilter:
211
+ """Open an output stream-filter bound to the active ruleset's exfil
212
+ markers."""
213
+ return OutputFilter(self._core.output_filter())
214
+
215
+ @property
216
+ def core_version(self) -> str:
217
+ """The bound core's semver (audit anchor; rides every verdict too)."""
218
+ return self._core.core_version()
k9o/client.py ADDED
@@ -0,0 +1,132 @@
1
+ """
2
+ `create_client(...)` factory + `K9oClient`.
3
+
4
+ Canonical usage:
5
+
6
+ from k9o import create_client, KError
7
+
8
+ k9o = create_client(
9
+ base_url="https://acme.api.k9o.io",
10
+ api_key=os.environ["K9O_API_KEY"],
11
+ org_slug="acme",
12
+ project_slug="member-portal",
13
+ )
14
+
15
+ try:
16
+ response = k9o.from_("posts").select().limit(10).execute()
17
+ for row in response.data:
18
+ print(row)
19
+ if response.meta.cursor:
20
+ print("next cursor:", response.meta.cursor.next)
21
+ except KError as e:
22
+ print(f"[{e.code}] {e.message}: {e.recommendation}")
23
+ raise
24
+ """
25
+
26
+ from __future__ import annotations
27
+
28
+ from .ai import AiNamespace
29
+ from .audit import AuditNamespace
30
+ from .compute import ComputeNamespace
31
+ from .data import FromBuilder
32
+ from .functions import FunctionsNamespace
33
+ from .gate import GateNamespace
34
+ from .http import HttpClientConfig, RetryConfig
35
+ from .kibo_vault import KiboNamespace
36
+ from .me import MeNamespace
37
+ from .migrations import MigrationsNamespace
38
+ from .org import OrgNamespace
39
+ from .orgs import OrgsNamespace
40
+ from .policies import PoliciesNamespace
41
+ from .projects import ProjectsNamespace
42
+ from .realtime import RealtimeNamespace
43
+ from .storage import StorageNamespace
44
+ from .studio import StudioNamespace
45
+ from .url_grammar import resolve_base_url
46
+
47
+
48
+ class K9oClient:
49
+ """The client returned by `create_client(...)`.
50
+
51
+ Namespaces hang off the instance — composed from per-resource
52
+ factories sharing the bound HTTP config.
53
+
54
+ Attributes:
55
+ projects — `client.projects.list()` / `.create(...)` / `.get(slug)` / `.update(slug, ...)`
56
+ gate — `client.gate.sign_in(...)` / `.sign_up(...)` / `.whoami()` / etc. (requires project_slug)
57
+ kibo — `client.kibo.vault.secrets.{list,get,create,reveal_challenge,reveal,update,rotate,move,delete}`
58
+ (ZK vault-secrets surface, ch08 §14; requires project_slug)
59
+ storage — `client.storage.{create_bucket,get_bucket,list_buckets,update_bucket,delete_bucket}` and
60
+ `client.storage.from_(bucket).{upload,create_signed_url,list,remove}` (each with an `_async`
61
+ twin; requires project_slug)
62
+
63
+ Methods:
64
+ from_(collection) — data-plane DSL; returns a FromBuilder
65
+ """
66
+
67
+ def __init__(self, config: HttpClientConfig) -> None:
68
+ self._config = config
69
+ self.projects = ProjectsNamespace(config)
70
+ self.gate = GateNamespace(config)
71
+ self.migrations = MigrationsNamespace(config)
72
+ self.functions = FunctionsNamespace(config)
73
+ self.policies = PoliciesNamespace(config)
74
+ self.audit = AuditNamespace(config)
75
+ self.orgs = OrgsNamespace(config)
76
+ self.me = MeNamespace(config)
77
+ self.studio = StudioNamespace(config)
78
+ self.compute = ComputeNamespace(config)
79
+ self.ai = AiNamespace(config)
80
+ self.realtime = RealtimeNamespace(config)
81
+ self.org = OrgNamespace(config)
82
+ self.kibo = KiboNamespace(config)
83
+ self.storage = StorageNamespace(config)
84
+
85
+ @property
86
+ def config(self) -> HttpClientConfig:
87
+ """Bound config (read-only access for advanced use)."""
88
+ return self._config
89
+
90
+ def from_(self, collection: str) -> FromBuilder:
91
+ """Data-plane DSL entry point. `from` is a Python keyword so we use `from_`."""
92
+ return FromBuilder(self._config, collection)
93
+
94
+
95
+ def create_client(
96
+ *,
97
+ base_url: str | None = None,
98
+ api_key: str | None = None,
99
+ org_slug: str | None = None,
100
+ project_slug: str | None = None,
101
+ default_headers: dict[str, str] | None = None,
102
+ retry_config: RetryConfig | None = None,
103
+ ) -> K9oClient:
104
+ """Construct a K9oClient bound to the supplied gateway baseUrl + auth + context.
105
+
106
+ base_url is OPTIONAL (ch13 §7.1): when omitted it defaults to the platform
107
+ apex 'https://api.k9o.io'; when the host is the bare apex and org_slug is
108
+ set, the SDK derives the canonical org-subdomain root
109
+ 'https://<orgSlug>.api.k9o.io'. Any other host (explicit subdomain,
110
+ self-hosted, dev) is used unchanged. Validates baseUrl at construct time
111
+ per ch13 §7.2 — path-bearing URLs raise KError(K9O_INVALID_BASE_URL).
112
+
113
+ Args:
114
+ base_url: optional scheme + host (e.g. 'https://acme.api.k9o.io' or
115
+ 'https://api.k9o.io'); defaults to 'https://api.k9o.io'
116
+ api_key: bearer token for default auth (per-call override is supported)
117
+ org_slug: active org context; sent as 'x-k9o-org-slug' header
118
+ project_slug: active project context; sent as 'x-k9o-project-slug' header
119
+ default_headers: headers merged into every request
120
+ retry_config: retry/backoff policy (ch13 §7.1); None ⇒ defaults
121
+ (3 retries, exponential) on idempotent methods + transient failures
122
+ """
123
+ resolved = resolve_base_url(base_url, org_slug)
124
+ config = HttpClientConfig(
125
+ base_url=resolved,
126
+ api_key=api_key,
127
+ org_slug=org_slug,
128
+ project_slug=project_slug,
129
+ default_headers=dict(default_headers or {}),
130
+ retry_config=retry_config,
131
+ )
132
+ return K9oClient(config)