to11ai-sdk 0.0.2__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,51 @@
1
+ .worktrees/
2
+ .worktree/
3
+ .claude/worktrees/
4
+ node_modules/
5
+ dist/
6
+ .turbo/
7
+ test-results/
8
+ package-lock.json
9
+ yarn.lock
10
+ pnpm-lock.yaml
11
+ *.log
12
+ *.tsbuildinfo
13
+ .env
14
+ .env.local
15
+ # Per-environment overrides — never commit; secrets land in these.
16
+ # (`.env.example` is allowlisted by being explicitly tracked.)
17
+ .env.stg
18
+ .env.staging
19
+ .env.production
20
+ .vercel/
21
+
22
+ # Agent skills caches (regenerated from skills-lock.json)
23
+ **/.junie/
24
+ **/.windsurf/
25
+
26
+ # Agent local settings
27
+ .claude/settings.local.json
28
+ .claude/worktrees
29
+ .claude/scheduled_tasks.lock
30
+
31
+ # Entire.io (AI session tracking)
32
+ .entire/settings.local.json
33
+ .entire/metadata/
34
+ .entire/tmp/
35
+ .entire/logs/
36
+ .entire/current_session
37
+ # Husky hook backups the entire plugin writes when it overlays its own
38
+ # wrappers — local-only, regenerated on plugin (re)install.
39
+ **/*.pre-entire
40
+
41
+ # Stray per-example lockfiles. The examples/ workspace ships a single
42
+ # `examples/bun.lock`; per-subfolder lockfiles only appear when someone
43
+ # accidentally runs `bun install` inside an example dir, and they shadow
44
+ # the workspace resolution.
45
+ examples/*/bun.lock
46
+
47
+ # Not tracked: ClickHouse entrypoint injects a plaintext password into this file
48
+ # on every container start, and we don't want those changes committed to git.
49
+ docker/clickhouse/users.d/default-user.xml
50
+ .notes.md
51
+ .playwright-mcp/
@@ -0,0 +1,8 @@
1
+ # Changelog
2
+
3
+ ## [0.0.2](https://github.com/to11ai/platform/compare/python-sdk-v0.0.1...python-sdk-v0.0.2) (2026-05-06)
4
+
5
+
6
+ ### Documentation
7
+
8
+ * **python-sdk:** add README + wire into pyproject ([#552](https://github.com/to11ai/platform/issues/552)) ([0e0d48f](https://github.com/to11ai/platform/commit/0e0d48febbdbcaa229bc0d076111c0f7d782d5bf))
@@ -0,0 +1,55 @@
1
+ Metadata-Version: 2.4
2
+ Name: to11ai-sdk
3
+ Version: 0.0.2
4
+ Summary: Python SDK for the to11.ai platform
5
+ Project-URL: Homepage, https://to11.ai
6
+ Project-URL: Repository, https://github.com/to11ai/platform
7
+ License-Expression: MIT
8
+ Requires-Python: >=3.10
9
+ Description-Content-Type: text/markdown
10
+
11
+ # to11ai-sdk
12
+
13
+ Python SDK for [to11.ai](https://to11.ai) — the AI engineering platform for prompt management, tracing, and evals.
14
+
15
+ ## Installation
16
+
17
+ ```bash
18
+ pip install to11ai-sdk
19
+ # or with uv
20
+ uv add to11ai-sdk
21
+ ```
22
+
23
+ ## Getting started
24
+
25
+ ```python
26
+ from to11ai_sdk import create_client
27
+
28
+ client = create_client(
29
+ base_url="https://api.to11.ai",
30
+ api_key="...",
31
+ )
32
+
33
+ result = client.prompts.resolve(
34
+ project_id="proj_abc123",
35
+ slug="chat-system-prompt",
36
+ labels=["production"],
37
+ variables={"user_name": "Alice"},
38
+ )
39
+
40
+ print(result.rendered.text) # "Hello, Alice!"
41
+ ```
42
+
43
+ ## Features
44
+
45
+ - Prompt resolution with label-based version selection and Liquid template rendering
46
+ - Typed errors for auth, rate limiting, validation, and network failures
47
+ - Compatible with Python 3.10+
48
+
49
+ ## Documentation
50
+
51
+ Full API reference and guides: <https://docs.to11.ai>
52
+
53
+ ## License
54
+
55
+ MIT
@@ -0,0 +1,45 @@
1
+ # to11ai-sdk
2
+
3
+ Python SDK for [to11.ai](https://to11.ai) — the AI engineering platform for prompt management, tracing, and evals.
4
+
5
+ ## Installation
6
+
7
+ ```bash
8
+ pip install to11ai-sdk
9
+ # or with uv
10
+ uv add to11ai-sdk
11
+ ```
12
+
13
+ ## Getting started
14
+
15
+ ```python
16
+ from to11ai_sdk import create_client
17
+
18
+ client = create_client(
19
+ base_url="https://api.to11.ai",
20
+ api_key="...",
21
+ )
22
+
23
+ result = client.prompts.resolve(
24
+ project_id="proj_abc123",
25
+ slug="chat-system-prompt",
26
+ labels=["production"],
27
+ variables={"user_name": "Alice"},
28
+ )
29
+
30
+ print(result.rendered.text) # "Hello, Alice!"
31
+ ```
32
+
33
+ ## Features
34
+
35
+ - Prompt resolution with label-based version selection and Liquid template rendering
36
+ - Typed errors for auth, rate limiting, validation, and network failures
37
+ - Compatible with Python 3.10+
38
+
39
+ ## Documentation
40
+
41
+ Full API reference and guides: <https://docs.to11.ai>
42
+
43
+ ## License
44
+
45
+ MIT
@@ -0,0 +1,24 @@
1
+ [build-system]
2
+ requires = ["hatchling"]
3
+ build-backend = "hatchling.build"
4
+
5
+ [project]
6
+ name = "to11ai-sdk"
7
+ version = "0.0.2"
8
+ description = "Python SDK for the to11.ai platform"
9
+ readme = "README.md"
10
+ license = "MIT"
11
+ requires-python = ">=3.10"
12
+ dependencies = []
13
+
14
+ [project.urls]
15
+ Homepage = "https://to11.ai"
16
+ Repository = "https://github.com/to11ai/platform"
17
+
18
+ [tool.hatch.build.targets.wheel]
19
+ packages = ["src/to11ai_sdk"]
20
+
21
+ [tool.uv]
22
+ dev-dependencies = [
23
+ "pytest>=8.0",
24
+ ]
@@ -0,0 +1,47 @@
1
+ from .client import To11aiClient, create_client
2
+ from .errors import (
3
+ To11aiApiError,
4
+ To11aiAuthError,
5
+ To11aiError,
6
+ To11aiNetworkError,
7
+ To11aiRateLimitError,
8
+ To11aiValidationError,
9
+ )
10
+ from .gateway import gateway_auth_headers, gateway_prompt_headers
11
+ from .prompts.resource import PromptResource
12
+ from .prompts.types import (
13
+ ChatMessage,
14
+ ChatRendered,
15
+ PaginatedResult,
16
+ PaginationInfo,
17
+ Prompt,
18
+ PromptLabelPointer,
19
+ PromptVersion,
20
+ RenderedPrompt,
21
+ ResolvedPrompt,
22
+ TextRendered,
23
+ )
24
+
25
+ __all__ = [
26
+ "create_client",
27
+ "To11aiClient",
28
+ "PromptResource",
29
+ "gateway_auth_headers",
30
+ "gateway_prompt_headers",
31
+ "ResolvedPrompt",
32
+ "Prompt",
33
+ "PromptVersion",
34
+ "PromptLabelPointer",
35
+ "PaginatedResult",
36
+ "PaginationInfo",
37
+ "ChatMessage",
38
+ "TextRendered",
39
+ "ChatRendered",
40
+ "RenderedPrompt",
41
+ "To11aiError",
42
+ "To11aiApiError",
43
+ "To11aiAuthError",
44
+ "To11aiNetworkError",
45
+ "To11aiRateLimitError",
46
+ "To11aiValidationError",
47
+ ]
@@ -0,0 +1,28 @@
1
+ from __future__ import annotations
2
+
3
+ from dataclasses import dataclass
4
+
5
+ from .prompts.resource import PromptResource
6
+ from .transport import Transport, TransportConfig
7
+
8
+
9
+ @dataclass
10
+ class To11aiClient:
11
+ prompts: PromptResource
12
+
13
+
14
+ def create_client(
15
+ *,
16
+ api_key: str,
17
+ base_url: str,
18
+ timeout_s: float = 30.0,
19
+ max_retries: int = 3,
20
+ ttl_seconds: int = 300,
21
+ max_cache_entries: int = 100,
22
+ ) -> To11aiClient:
23
+ if not api_key:
24
+ raise ValueError("api_key must not be empty")
25
+ if not base_url:
26
+ raise ValueError("base_url must not be empty")
27
+ transport = Transport(TransportConfig(base_url=base_url, api_key=api_key, timeout_s=timeout_s, max_retries=max_retries))
28
+ return To11aiClient(prompts=PromptResource(transport, ttl_seconds=ttl_seconds, max_cache_entries=max_cache_entries))
@@ -0,0 +1,75 @@
1
+ from __future__ import annotations
2
+
3
+ from typing import Any, Optional
4
+
5
+
6
+ class To11aiError(Exception):
7
+ """Base class for all to11ai SDK errors."""
8
+
9
+
10
+ class To11aiApiError(To11aiError):
11
+ """Raised when the API returns a non-2xx response."""
12
+
13
+ def __init__(
14
+ self,
15
+ message: str,
16
+ *,
17
+ status_code: int,
18
+ error_code: Optional[str] = None,
19
+ details: Any = None,
20
+ ) -> None:
21
+ super().__init__(message)
22
+ self.status_code = status_code
23
+ self.error_code = error_code
24
+ self.details = details
25
+
26
+
27
+ class To11aiAuthError(To11aiApiError):
28
+ """Raised on 401 or 403 responses."""
29
+
30
+ def __init__(
31
+ self,
32
+ message: str,
33
+ *,
34
+ status_code: int,
35
+ error_code: Optional[str] = None,
36
+ details: Any = None,
37
+ ) -> None:
38
+ super().__init__(message, status_code=status_code, error_code=error_code, details=details)
39
+
40
+
41
+ class To11aiRateLimitError(To11aiApiError):
42
+ """Raised on 429 responses."""
43
+
44
+ def __init__(
45
+ self,
46
+ message: str,
47
+ *,
48
+ retry_after_seconds: Optional[float] = None,
49
+ error_code: Optional[str] = None,
50
+ details: Any = None,
51
+ ) -> None:
52
+ super().__init__(message, status_code=429, error_code=error_code, details=details)
53
+ self.retry_after_seconds = retry_after_seconds
54
+
55
+
56
+ class To11aiValidationError(To11aiApiError):
57
+ """Raised on 400 responses."""
58
+
59
+ def __init__(
60
+ self,
61
+ message: str,
62
+ *,
63
+ error_code: Optional[str] = None,
64
+ details: Any = None,
65
+ ) -> None:
66
+ super().__init__(message, status_code=400, error_code=error_code, details=details)
67
+
68
+
69
+ class To11aiNetworkError(To11aiError):
70
+ """Raised on network failure or timeout."""
71
+
72
+ def __init__(self, message: str, *, cause: Optional[BaseException] = None) -> None:
73
+ super().__init__(message)
74
+ if cause is not None:
75
+ self.__cause__ = cause
@@ -0,0 +1,40 @@
1
+ from __future__ import annotations
2
+
3
+ from .prompts.types import ResolvedPrompt
4
+
5
+
6
+ def gateway_auth_headers(*, api_key: str, project_id: str) -> dict[str, str]:
7
+ """
8
+ Build auth headers required for every request through the to11 gateway.
9
+
10
+ Example::
11
+
12
+ headers = gateway_auth_headers(api_key=api_key, project_id=project_id)
13
+ response = anthropic_client.messages.create(..., extra_headers=headers)
14
+ """
15
+ return {
16
+ "x-to11-authorization": f"Bearer {api_key}",
17
+ "x-to11-project-id": project_id,
18
+ }
19
+
20
+
21
+ def gateway_prompt_headers(prompt: ResolvedPrompt) -> dict[str, str]:
22
+ """
23
+ Build prompt provenance headers from a resolved prompt.
24
+
25
+ Omits ``x-to11-prompt-labels`` when the prompt has no labels —
26
+ the gateway rejects empty header values.
27
+
28
+ Example::
29
+
30
+ prompt = client.prompts.resolve(project_id=..., slug="my-prompt", labels=["production"])
31
+ headers = gateway_prompt_headers(prompt)
32
+ response = anthropic_client.messages.create(..., extra_headers=headers)
33
+ """
34
+ headers: dict[str, str] = {
35
+ "x-to11-prompt-id": prompt.prompt_id,
36
+ "x-to11-prompt-version": str(prompt.version),
37
+ }
38
+ if prompt.labels:
39
+ headers["x-to11-prompt-labels"] = ",".join(prompt.labels)
40
+ return headers
File without changes
@@ -0,0 +1,174 @@
1
+ from __future__ import annotations
2
+
3
+ import copy
4
+ import json
5
+ import threading
6
+ import time as _time
7
+ from typing import Any, Optional
8
+ from urllib.parse import quote
9
+
10
+ from ..errors import To11aiNetworkError
11
+ from ..transport import Transport
12
+ from .types import (
13
+ PaginatedResult, PaginationInfo, Prompt, PromptLabelPointer,
14
+ PromptVersion, ResolvedPrompt,
15
+ )
16
+
17
+ DEFAULT_TTL_SECONDS = 300
18
+ DEFAULT_MAX_ENTRIES = 100
19
+
20
+
21
+ def _build_cache_key(project_id: str, slug: str, labels: list[str], variables: Optional[dict[str, Any]] = None) -> str:
22
+ def _sort(obj: Any) -> Any:
23
+ if isinstance(obj, dict):
24
+ return {k: _sort(v) for k, v in sorted(obj.items())}
25
+ if isinstance(obj, list):
26
+ return [_sort(v) for v in obj]
27
+ return obj
28
+ vars_part = json.dumps(_sort(variables), sort_keys=True) if variables is not None else None
29
+ return f"{json.dumps(project_id)}|{json.dumps(slug)}|{json.dumps(sorted(labels))}|{json.dumps(vars_part)}"
30
+
31
+
32
+ class _CacheEntry:
33
+ __slots__ = ("expires_at", "value")
34
+ def __init__(self, value: ResolvedPrompt, expires_at: float) -> None:
35
+ self.value = value
36
+ self.expires_at = expires_at
37
+
38
+
39
+ class _Cache:
40
+ def __init__(self, ttl_s: int, max_entries: int) -> None:
41
+ self._ttl = ttl_s
42
+ self._max = max_entries
43
+ self._store: dict[str, _CacheEntry] = {}
44
+ self._lock = threading.Lock()
45
+
46
+ def get(self, key: str) -> Optional[ResolvedPrompt]:
47
+ with self._lock:
48
+ entry = self._store.get(key)
49
+ if entry is None:
50
+ return None
51
+ if _time.time() > entry.expires_at:
52
+ self._store.pop(key, None)
53
+ return None
54
+ del self._store[key]
55
+ self._store[key] = entry
56
+ return copy.deepcopy(entry.value)
57
+
58
+ def set(self, key: str, value: ResolvedPrompt) -> None:
59
+ if self._max <= 0:
60
+ return
61
+ with self._lock:
62
+ self._store.pop(key, None)
63
+ while len(self._store) >= self._max:
64
+ del self._store[next(iter(self._store))]
65
+ self._store[key] = _CacheEntry(value=copy.deepcopy(value), expires_at=_time.time() + self._ttl)
66
+
67
+
68
+ def _prompts_path(project_id: str) -> str:
69
+ return f"/v1/projects/{quote(project_id, safe='')}/prompts"
70
+
71
+
72
+ def _prompt_path(project_id: str, prompt_id: str) -> str:
73
+ return f"{_prompts_path(project_id)}/{quote(prompt_id, safe='')}"
74
+
75
+
76
+ def _unwrap(envelope: dict[str, Any]) -> Any:
77
+ data = envelope.get("data")
78
+ if data is None:
79
+ raise RuntimeError("API returned success but data is null")
80
+ return data
81
+
82
+
83
+ class PromptResource:
84
+ def __init__(self, transport: Transport, ttl_seconds: int = DEFAULT_TTL_SECONDS, max_cache_entries: int = DEFAULT_MAX_ENTRIES) -> None:
85
+ self._transport = transport
86
+ self._cache = _Cache(ttl_s=ttl_seconds, max_entries=max_cache_entries)
87
+
88
+ def create(self, *, project_id: str, name: str, slug: str, description: Optional[str] = None, tags: Optional[list[str]] = None) -> Prompt:
89
+ body: dict[str, Any] = {"name": name, "slug": slug}
90
+ if description is not None:
91
+ body["description"] = description
92
+ if tags is not None:
93
+ body["tags"] = tags
94
+ return Prompt.from_dict(_unwrap(self._transport.request("POST", _prompts_path(project_id), body=body)))
95
+
96
+ def list(self, *, project_id: str, status: Optional[str] = None, sort_by: Optional[str] = None, tags: Optional[str] = None, page: Optional[int] = None, limit: Optional[int] = None) -> PaginatedResult:
97
+ query: dict[str, Any] = {}
98
+ if status is not None:
99
+ query["status"] = status
100
+ if sort_by is not None:
101
+ query["sortBy"] = sort_by
102
+ if tags is not None:
103
+ query["tags"] = tags
104
+ if page is not None:
105
+ query["page"] = page
106
+ if limit is not None:
107
+ query["limit"] = limit
108
+ envelope = self._transport.request("GET", _prompts_path(project_id), query=query)
109
+ raw = _unwrap(envelope)
110
+ if isinstance(raw, list):
111
+ items = [Prompt.from_dict(d) for d in raw]
112
+ pag_data = envelope.get("pagination") or {"page": 1, "limit": len(items), "total": len(items), "totalPages": 1}
113
+ else:
114
+ items = [Prompt.from_dict(d) for d in raw.get("items", [])]
115
+ pag_data = envelope.get("pagination") or raw.get("pagination") or {"page": 1, "limit": len(items), "total": len(items), "totalPages": 1}
116
+ return PaginatedResult(items=items, pagination=PaginationInfo.from_dict(pag_data))
117
+
118
+ def get(self, *, project_id: str, prompt_id: str) -> Prompt:
119
+ return Prompt.from_dict(_unwrap(self._transport.request("GET", _prompt_path(project_id, prompt_id))))
120
+
121
+ def update(self, *, project_id: str, prompt_id: str, name: Optional[str] = None, description: Optional[str] = None, tags: Optional[list[str]] = None) -> Prompt:
122
+ body: dict[str, Any] = {}
123
+ if name is not None:
124
+ body["name"] = name
125
+ if description is not None:
126
+ body["description"] = description
127
+ if tags is not None:
128
+ body["tags"] = tags
129
+ return Prompt.from_dict(_unwrap(self._transport.request("PATCH", _prompt_path(project_id, prompt_id), body=body)))
130
+
131
+ def archive(self, *, project_id: str, prompt_id: str) -> Prompt:
132
+ return Prompt.from_dict(_unwrap(self._transport.request("DELETE", _prompt_path(project_id, prompt_id))))
133
+
134
+ def create_version(self, *, project_id: str, prompt_id: str, format: str, template_json: Any, variables_schema: Any, model_config: Any = None, changelog: Optional[str] = None) -> PromptVersion: # noqa: A002
135
+ body: dict[str, Any] = {"format": format, "templateJson": template_json, "variablesSchema": variables_schema}
136
+ if model_config is not None:
137
+ body["modelConfig"] = model_config
138
+ if changelog is not None:
139
+ body["changelog"] = changelog
140
+ return PromptVersion.from_dict(_unwrap(self._transport.request("POST", f"{_prompt_path(project_id, prompt_id)}/versions", body=body)))
141
+
142
+ def list_versions(self, *, project_id: str, prompt_id: str) -> list[PromptVersion]:
143
+ return [PromptVersion.from_dict(d) for d in _unwrap(self._transport.request("GET", f"{_prompt_path(project_id, prompt_id)}/versions"))]
144
+
145
+ def get_version(self, *, project_id: str, prompt_id: str, version_number: int) -> PromptVersion:
146
+ return PromptVersion.from_dict(_unwrap(self._transport.request("GET", f"{_prompt_path(project_id, prompt_id)}/versions/{quote(str(version_number), safe='')}")))
147
+
148
+ def move_label(self, *, project_id: str, prompt_id: str, label: str, version_id: str, reason: Optional[str] = None) -> PromptLabelPointer:
149
+ body: dict[str, Any] = {"versionId": version_id}
150
+ if reason is not None:
151
+ body["reason"] = reason
152
+ return PromptLabelPointer.from_dict(_unwrap(self._transport.request("PUT", f"{_prompt_path(project_id, prompt_id)}/labels/{quote(label, safe='')}", body=body)))
153
+
154
+ def list_labels(self, *, project_id: str, prompt_id: str) -> list[PromptLabelPointer]:
155
+ return [PromptLabelPointer.from_dict(d) for d in _unwrap(self._transport.request("GET", f"{_prompt_path(project_id, prompt_id)}/labels"))]
156
+
157
+ def resolve(self, *, project_id: str, slug: str, labels: list[str], variables: Optional[dict[str, Any]] = None, cache: bool = True, fallback: Optional[ResolvedPrompt] = None) -> ResolvedPrompt:
158
+ key = _build_cache_key(project_id, slug, labels, variables)
159
+ if cache:
160
+ cached = self._cache.get(key)
161
+ if cached is not None:
162
+ return cached
163
+ try:
164
+ body: dict[str, Any] = {"slug": slug, "labels": labels}
165
+ if variables is not None:
166
+ body["variables"] = variables
167
+ result = ResolvedPrompt.from_dict(_unwrap(self._transport.request("POST", f"{_prompts_path(project_id)}:resolve", body=body)))
168
+ if cache:
169
+ self._cache.set(key, result)
170
+ return result
171
+ except To11aiNetworkError:
172
+ if fallback is not None:
173
+ return fallback
174
+ raise