huitzo-sdk 0.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.
@@ -0,0 +1,117 @@
1
+ """
2
+ Module: integrations.telegram
3
+ Description: Telegram integration client for sending messages, documents, and photos.
4
+
5
+ Implements:
6
+ - docs/sdk/integrations.md#telegram-integration
7
+
8
+ See Also:
9
+ - docs/sdk/error-handling.md (for IntegrationError)
10
+ """
11
+
12
+ from __future__ import annotations
13
+
14
+ from dataclasses import dataclass
15
+ from typing import Protocol, runtime_checkable
16
+
17
+
18
+ @runtime_checkable
19
+ class TelegramBackend(Protocol):
20
+ """Backend protocol for Telegram bot API."""
21
+
22
+ async def send(
23
+ self,
24
+ *,
25
+ chat_id: str,
26
+ message: str,
27
+ parse_mode: str | None,
28
+ ) -> None: ...
29
+
30
+ async def send_document(
31
+ self,
32
+ *,
33
+ chat_id: str,
34
+ document: bytes,
35
+ filename: str,
36
+ caption: str | None,
37
+ ) -> None: ...
38
+
39
+ async def send_photo(
40
+ self,
41
+ *,
42
+ chat_id: str,
43
+ photo: bytes,
44
+ caption: str | None,
45
+ ) -> None: ...
46
+
47
+
48
+ @dataclass
49
+ class TelegramClient:
50
+ """Client for sending Telegram messages and media.
51
+
52
+ This is the SDK-side interface. The backend injects a real implementation
53
+ at runtime that calls the Telegram Bot API.
54
+ """
55
+
56
+ _backend: TelegramBackend
57
+
58
+ async def send(
59
+ self,
60
+ *,
61
+ chat_id: str,
62
+ message: str,
63
+ parse_mode: str | None = None,
64
+ ) -> None:
65
+ """Send a text message.
66
+
67
+ Args:
68
+ chat_id: Target chat ID.
69
+ message: Message text.
70
+ parse_mode: "Markdown" or "HTML".
71
+
72
+ Raises:
73
+ IntegrationError: If sending fails.
74
+ """
75
+ await self._backend.send(chat_id=chat_id, message=message, parse_mode=parse_mode)
76
+
77
+ async def send_document(
78
+ self,
79
+ *,
80
+ chat_id: str,
81
+ document: bytes,
82
+ filename: str,
83
+ caption: str | None = None,
84
+ ) -> None:
85
+ """Send a document.
86
+
87
+ Args:
88
+ chat_id: Target chat ID.
89
+ document: File bytes.
90
+ filename: Filename for the document.
91
+ caption: Optional caption.
92
+
93
+ Raises:
94
+ IntegrationError: If sending fails.
95
+ """
96
+ await self._backend.send_document(
97
+ chat_id=chat_id, document=document, filename=filename, caption=caption
98
+ )
99
+
100
+ async def send_photo(
101
+ self,
102
+ *,
103
+ chat_id: str,
104
+ photo: bytes,
105
+ caption: str | None = None,
106
+ ) -> None:
107
+ """Send a photo.
108
+
109
+ Args:
110
+ chat_id: Target chat ID.
111
+ photo: Image bytes.
112
+ caption: Optional caption.
113
+
114
+ Raises:
115
+ IntegrationError: If sending fails.
116
+ """
117
+ await self._backend.send_photo(chat_id=chat_id, photo=photo, caption=caption)
@@ -0,0 +1,21 @@
1
+ """
2
+ Module: storage
3
+ Description: Storage interface, backends, and key scoping for the Huitzo SDK.
4
+
5
+ Implements:
6
+ - docs/sdk/storage.md
7
+ - docs/sdk/storage-backends.md
8
+
9
+ See Also:
10
+ - docs/sdk/context.md#ctx-storage (for Context integration)
11
+ """
12
+
13
+ from huitzo_sdk.storage.memory import InMemoryBackend
14
+ from huitzo_sdk.storage.namespace import StorageNamespace
15
+ from huitzo_sdk.storage.protocol import StorageBackend
16
+
17
+ __all__ = [
18
+ "StorageBackend",
19
+ "InMemoryBackend",
20
+ "StorageNamespace",
21
+ ]
@@ -0,0 +1,181 @@
1
+ """
2
+ Module: storage.memory
3
+ Description: In-memory storage backend for testing and development.
4
+
5
+ Implements:
6
+ - docs/sdk/storage-backends.md#storage-backend-protocol
7
+ - docs/sdk/storage.md#core-methods
8
+ - docs/sdk/storage.md#ttl-time-to-live
9
+ - docs/sdk/storage.md#batch-operations
10
+ - docs/sdk/storage.md#transactions
11
+ - docs/sdk/storage.md#query-by-metadata
12
+
13
+ See Also:
14
+ - docs/sdk/storage.md (for high-level storage API)
15
+ """
16
+
17
+ from __future__ import annotations
18
+
19
+ import builtins
20
+ import copy
21
+ import time
22
+ from contextlib import asynccontextmanager
23
+ from dataclasses import dataclass, field
24
+ from typing import Any, AsyncIterator
25
+
26
+
27
+ @dataclass
28
+ class _Entry:
29
+ """Internal storage entry."""
30
+
31
+ value: Any
32
+ metadata: dict[str, Any] = field(default_factory=dict)
33
+ expires_at: float | None = None
34
+
35
+ def is_expired(self) -> bool:
36
+ return self.expires_at is not None and time.monotonic() >= self.expires_at
37
+
38
+
39
+ class InMemoryBackend:
40
+ """In-memory implementation of StorageBackend for testing.
41
+
42
+ Supports all protocol methods including TTL, batch operations,
43
+ metadata queries, and transactions.
44
+
45
+ Note: This backend has the following limitations:
46
+ - Expired entries are cleaned up lazily (only when accessed), which may
47
+ cause memory usage to grow with many TTL-based entries that are never
48
+ accessed again.
49
+ - Transaction implementation is not thread-safe for concurrent operations.
50
+ The _in_transaction flag check and setting are not atomic.
51
+ - Transactions create a deep copy of all data, which has O(n) time and
52
+ space overhead where n is the total number of keys in storage.
53
+ """
54
+
55
+ def __init__(self) -> None:
56
+ self._data: dict[str, _Entry] = {}
57
+ self._in_transaction: bool = False
58
+ self._snapshot: dict[str, _Entry] | None = None
59
+
60
+ def _clean_expired(self, key: str) -> None:
61
+ entry = self._data.get(key)
62
+ if entry is not None and entry.is_expired():
63
+ del self._data[key]
64
+
65
+ def _set_expiration_for_testing(self, key: str, expires_at: float) -> None:
66
+ """Set expiration time for a key (for testing purposes only).
67
+
68
+ Args:
69
+ key: The key to modify
70
+ expires_at: Expiration timestamp (from time.monotonic())
71
+ """
72
+ if key in self._data:
73
+ self._data[key].expires_at = expires_at
74
+
75
+ def _get_entry(self, key: str) -> _Entry | None:
76
+ self._clean_expired(key)
77
+ return self._data.get(key)
78
+
79
+ async def save(
80
+ self,
81
+ key: str,
82
+ value: Any,
83
+ *,
84
+ ttl: int | None = None,
85
+ metadata: dict[str, Any] | None = None,
86
+ ) -> str:
87
+ expires_at = (time.monotonic() + ttl) if ttl is not None else None
88
+ self._data[key] = _Entry(
89
+ value=value,
90
+ metadata=metadata or {},
91
+ expires_at=expires_at,
92
+ )
93
+ return key
94
+
95
+ async def get(self, key: str, *, default: Any = None) -> Any:
96
+ entry = self._get_entry(key)
97
+ if entry is None:
98
+ return default
99
+ return entry.value
100
+
101
+ async def delete(self, key: str) -> bool:
102
+ self._clean_expired(key)
103
+ if key in self._data:
104
+ del self._data[key]
105
+ return True
106
+ return False
107
+
108
+ async def exists(self, key: str) -> bool:
109
+ return self._get_entry(key) is not None
110
+
111
+ async def list(
112
+ self, prefix: str = "", *, limit: int = 100, offset: int = 0
113
+ ) -> builtins.list[str]:
114
+ # Clean expired keys lazily
115
+ keys = [k for k in self._data if k.startswith(prefix) and not self._data[k].is_expired()]
116
+ keys.sort()
117
+ return keys[offset : offset + limit]
118
+
119
+ async def save_many(self, items: dict[str, Any], *, ttl: int | None = None) -> None:
120
+ for key, value in items.items():
121
+ await self.save(key, value, ttl=ttl)
122
+
123
+ async def get_many(self, keys: builtins.list[str]) -> dict[str, Any]:
124
+ return {key: await self.get(key) for key in keys}
125
+
126
+ async def delete_many(self, keys: builtins.list[str]) -> int:
127
+ count = 0
128
+ for key in keys:
129
+ if await self.delete(key):
130
+ count += 1
131
+ return count
132
+
133
+ async def query(
134
+ self,
135
+ prefix: str = "",
136
+ *,
137
+ metadata: dict[str, Any] | None = None,
138
+ limit: int = 100,
139
+ ) -> builtins.list[dict[str, Any]]:
140
+ results: builtins.list[dict[str, Any]] = []
141
+ for key, entry in sorted(self._data.items()):
142
+ if entry.is_expired():
143
+ continue
144
+ if not key.startswith(prefix):
145
+ continue
146
+ if metadata:
147
+ if not all(entry.metadata.get(k) == v for k, v in metadata.items()):
148
+ continue
149
+ results.append(
150
+ {"key": key, "value": entry.value, "metadata": copy.deepcopy(entry.metadata)}
151
+ )
152
+ if len(results) >= limit:
153
+ break
154
+ return results
155
+
156
+ @asynccontextmanager
157
+ async def transaction(self) -> AsyncIterator[None]:
158
+ """Atomic transaction with snapshot-based rollback."""
159
+ if self._in_transaction:
160
+ # Nested transaction: just yield (no new snapshot)
161
+ yield
162
+ return
163
+
164
+ # Take snapshot
165
+ self._snapshot = {
166
+ k: _Entry(copy.deepcopy(v.value), copy.deepcopy(v.metadata), v.expires_at)
167
+ for k, v in self._data.items()
168
+ }
169
+ self._in_transaction = True
170
+ try:
171
+ yield
172
+ # Commit: discard snapshot
173
+ self._snapshot = None
174
+ except BaseException:
175
+ # Rollback: restore snapshot
176
+ if self._snapshot is not None:
177
+ self._data = self._snapshot
178
+ self._snapshot = None
179
+ raise
180
+ finally:
181
+ self._in_transaction = False
@@ -0,0 +1,64 @@
1
+ """
2
+ Module: storage.namespace
3
+ Description: Key scoping for tenant-isolated storage.
4
+
5
+ Implements:
6
+ - docs/sdk/storage.md#automatic-scoping
7
+ - docs/sdk/storage.md#scope-levels
8
+
9
+ See Also:
10
+ - docs/sdk/storage-backends.md (for backend protocol)
11
+ """
12
+
13
+ from __future__ import annotations
14
+
15
+ from dataclasses import dataclass
16
+ from uuid import UUID
17
+
18
+
19
+ @dataclass(frozen=True)
20
+ class StorageNamespace:
21
+ """Builds scoped storage keys for tenant isolation.
22
+
23
+ Keys are scoped as:
24
+ user scope: tenant:{tenant_id}:user:{user_id}:pack:{pack_id}:{key}
25
+ tenant scope: tenant:{tenant_id}:pack:{pack_id}:{key}
26
+ pack scope: pack:{pack_id}:{key}
27
+ """
28
+
29
+ tenant_id: UUID
30
+ user_id: UUID
31
+ pack_id: str
32
+
33
+ def scope_key(self, key: str, scope: str = "user") -> str:
34
+ """Build a fully-scoped storage key.
35
+
36
+ Args:
37
+ key: The user-provided key.
38
+ scope: Scope level - "user" (default), "tenant", or "pack".
39
+
40
+ Returns:
41
+ The scoped key string.
42
+
43
+ Raises:
44
+ ValueError: If scope is invalid.
45
+ """
46
+ if scope == "user":
47
+ return f"tenant:{self.tenant_id}:user:{self.user_id}:pack:{self.pack_id}:{key}"
48
+ if scope == "tenant":
49
+ return f"tenant:{self.tenant_id}:pack:{self.pack_id}:{key}"
50
+ if scope == "pack":
51
+ return f"pack:{self.pack_id}:{key}"
52
+ raise ValueError(f"Invalid scope: {scope!r}. Must be 'user', 'tenant', or 'pack'.")
53
+
54
+ def scope_prefix(self, prefix: str, scope: str = "user") -> str:
55
+ """Build a scoped prefix for listing keys.
56
+
57
+ Args:
58
+ prefix: The user-provided prefix.
59
+ scope: Scope level.
60
+
61
+ Returns:
62
+ The scoped prefix string.
63
+ """
64
+ return self.scope_key(prefix, scope)
@@ -0,0 +1,173 @@
1
+ """
2
+ Module: storage.protocol
3
+ Description: StorageBackend Protocol defining the abstract storage interface.
4
+
5
+ Implements:
6
+ - docs/sdk/storage-backends.md#storage-backend-protocol
7
+ - docs/sdk/storage.md#core-methods
8
+
9
+ See Also:
10
+ - docs/sdk/storage.md (for high-level storage API)
11
+ - docs/sdk/context.md#ctx-storage (for Context integration)
12
+ """
13
+
14
+ from __future__ import annotations
15
+
16
+ import builtins
17
+ from contextlib import AbstractAsyncContextManager
18
+ from typing import Any, Protocol, runtime_checkable
19
+
20
+
21
+ @runtime_checkable
22
+ class StorageBackend(Protocol):
23
+ """Abstract storage interface enabling multiple backends.
24
+
25
+ All storage backends must implement this protocol. Keys are pre-scoped
26
+ by StorageNamespace before reaching the backend.
27
+ """
28
+
29
+ async def save(
30
+ self,
31
+ key: str,
32
+ value: Any,
33
+ *,
34
+ ttl: int | None = None,
35
+ metadata: dict[str, Any] | None = None,
36
+ ) -> str:
37
+ """Save a value to storage.
38
+
39
+ Args:
40
+ key: Storage key (max 256 characters, pre-scoped).
41
+ value: JSON-serializable value.
42
+ ttl: Time-to-live in seconds.
43
+ metadata: Optional metadata for querying.
44
+
45
+ Returns:
46
+ The storage key.
47
+
48
+ Raises:
49
+ StorageError: If save operation fails.
50
+ """
51
+ ...
52
+
53
+ async def get(self, key: str, *, default: Any = None) -> Any:
54
+ """Retrieve a value from storage.
55
+
56
+ Args:
57
+ key: Storage key.
58
+ default: Value returned if key not found.
59
+
60
+ Returns:
61
+ The stored value, or default if not found.
62
+
63
+ Raises:
64
+ StorageError: If retrieval fails.
65
+ """
66
+ ...
67
+
68
+ async def delete(self, key: str) -> bool:
69
+ """Delete a value from storage.
70
+
71
+ Args:
72
+ key: Storage key.
73
+
74
+ Returns:
75
+ True if deleted, False if not found.
76
+
77
+ Raises:
78
+ StorageError: If deletion fails.
79
+ """
80
+ ...
81
+
82
+ async def exists(self, key: str) -> bool:
83
+ """Check if a key exists in storage.
84
+
85
+ Args:
86
+ key: Storage key.
87
+
88
+ Returns:
89
+ True if key exists and is not expired.
90
+ """
91
+ ...
92
+
93
+ async def list(
94
+ self, prefix: str = "", *, limit: int = 100, offset: int = 0
95
+ ) -> builtins.list[str]:
96
+ """List keys matching a prefix.
97
+
98
+ Args:
99
+ prefix: Key prefix to match.
100
+ limit: Maximum number of keys to return.
101
+ offset: Pagination offset.
102
+
103
+ Returns:
104
+ List of matching keys.
105
+
106
+ Raises:
107
+ StorageError: If listing fails.
108
+ """
109
+ ...
110
+
111
+ async def save_many(self, items: dict[str, Any], *, ttl: int | None = None) -> None:
112
+ """Save multiple key-value pairs.
113
+
114
+ Args:
115
+ items: Dictionary of key-value pairs.
116
+ ttl: Optional TTL applied to all items.
117
+
118
+ Raises:
119
+ StorageError: If any save fails.
120
+ """
121
+ ...
122
+
123
+ async def get_many(self, keys: builtins.list[str]) -> dict[str, Any]:
124
+ """Retrieve multiple values.
125
+
126
+ Args:
127
+ keys: List of keys to retrieve.
128
+
129
+ Returns:
130
+ Dictionary mapping keys to values (None for missing keys).
131
+ """
132
+ ...
133
+
134
+ async def delete_many(self, keys: builtins.list[str]) -> int:
135
+ """Delete multiple keys.
136
+
137
+ Args:
138
+ keys: List of keys to delete.
139
+
140
+ Returns:
141
+ Number of keys actually deleted.
142
+ """
143
+ ...
144
+
145
+ async def query(
146
+ self,
147
+ prefix: str = "",
148
+ *,
149
+ metadata: dict[str, Any] | None = None,
150
+ limit: int = 100,
151
+ ) -> builtins.list[dict[str, Any]]:
152
+ """Query storage by prefix and metadata filters.
153
+
154
+ Args:
155
+ prefix: Key prefix to match.
156
+ metadata: Metadata key-value filters (exact match).
157
+ limit: Maximum number of results.
158
+
159
+ Returns:
160
+ List of records with key, value, and metadata.
161
+ """
162
+ ...
163
+
164
+ def transaction(self) -> AbstractAsyncContextManager[None]:
165
+ """Begin an atomic transaction.
166
+
167
+ Usage:
168
+ async with backend.transaction():
169
+ await backend.save("k1", v1)
170
+ await backend.save("k2", v2)
171
+ # Both commit or both rollback.
172
+ """
173
+ ...
huitzo_sdk/types.py ADDED
@@ -0,0 +1,47 @@
1
+ """
2
+ Module: types
3
+ Description: Core type definitions for the Huitzo SDK.
4
+
5
+ Implements:
6
+ - docs/sdk/commands.md#decorator-parameters
7
+ - docs/sdk/context.md#deployment-mode
8
+
9
+ See Also:
10
+ - docs/sdk/overview.md (for SDK design philosophy)
11
+ """
12
+
13
+ from __future__ import annotations
14
+
15
+ from dataclasses import dataclass
16
+ from enum import Enum
17
+ from typing import Any, Union
18
+
19
+ from pydantic import BaseModel
20
+
21
+
22
+ class DeploymentMode(Enum):
23
+ """Deployment mode for the Huitzo platform."""
24
+
25
+ CLOUD = "cloud"
26
+ SELF_HOSTED = "self_hosted"
27
+ EDGE = "edge"
28
+
29
+
30
+ @dataclass(frozen=True)
31
+ class CommandMetadata:
32
+ """Metadata stored on decorated command functions."""
33
+
34
+ name: str
35
+ namespace: str
36
+ version: str = "1.0.0"
37
+ timeout: int = 60
38
+ retries: int = 3
39
+ retry_backoff: float = 1.0
40
+ retry_max_wait: int = 60
41
+ queue: str = "default"
42
+ output_format: str = "auto"
43
+ description: str | None = None
44
+
45
+
46
+ # Result can be a dict, Pydantic model, str, int, or None
47
+ Result = Union[dict[str, Any], BaseModel, str, int, None]
@@ -0,0 +1,12 @@
1
+ Metadata-Version: 2.4
2
+ Name: huitzo-sdk
3
+ Version: 0.0.0
4
+ Summary: Huitzo SDK for building Intelligence Packs
5
+ Requires-Python: >=3.11
6
+ Requires-Dist: pydantic>=2.12
7
+ Provides-Extra: dev
8
+ Requires-Dist: mypy>=1.19; extra == 'dev'
9
+ Requires-Dist: pytest-asyncio>=1.0; extra == 'dev'
10
+ Requires-Dist: pytest-cov>=7.0; extra == 'dev'
11
+ Requires-Dist: pytest>=9.0; extra == 'dev'
12
+ Requires-Dist: ruff>=0.14; extra == 'dev'
@@ -0,0 +1,18 @@
1
+ huitzo_sdk/__init__.py,sha256=3FeobaJAjqVkTA2r3mp0eeHGL8wZHea4zO-ZJ_7HvTo,1776
2
+ huitzo_sdk/command.py,sha256=ANsJ6pY4ADI6CNi2ZKtEdtjY8NqEO2n6d98hwtTk5HU,5512
3
+ huitzo_sdk/context.py,sha256=YXJfe2m79IlDE7_sGacPM6G-JJP6_T9XByDWKcuS_vE,4044
4
+ huitzo_sdk/errors.py,sha256=ZVXVH6a-mqiVvq8Ma5Uc1_7fcoAE7xdvXXQbmkqy758,5816
5
+ huitzo_sdk/types.py,sha256=2ZbkH_MKz2LAz8Dwnexv1pjTU-4uyWzh0R4p-xCPGLo,1036
6
+ huitzo_sdk/integrations/__init__.py,sha256=cx2U6Z0vOwkkEeli1IyGxE70NaY1b1n7tYjcRxHdR1w,685
7
+ huitzo_sdk/integrations/email.py,sha256=9hDSt5lgbh_w4P3sFTHfmaL15x0lCCeYVTVzSRP5G9o,2631
8
+ huitzo_sdk/integrations/files.py,sha256=c92RdjxGPUDpD6x1Mepi8sHfcugiziQTl_Pmy98eKYY,4961
9
+ huitzo_sdk/integrations/http.py,sha256=cBwOmTW9SyT9ltmIQB-ihemrHVy0OH4opbamcFD8rqU,4574
10
+ huitzo_sdk/integrations/llm.py,sha256=9EHBTX6piIFs2PNRQJDU2ruUpYZLc5Iop0pozdMlUW8,4178
11
+ huitzo_sdk/integrations/telegram.py,sha256=v6yjzuYADYLHBP0UokWqo95wvcbR61bSni1IPX9SuLQ,2716
12
+ huitzo_sdk/storage/__init__.py,sha256=D3M5-3JrGey3GDV-qenTqQKQtwutoiYArrMJWzkUsvQ,503
13
+ huitzo_sdk/storage/memory.py,sha256=dCPng8zqOHU5ucXK-zhgC2ctFEGfQWwKMxXwb8e8Giw,5855
14
+ huitzo_sdk/storage/namespace.py,sha256=24dSCIj7n4tVvFippqJSq0J7rMHpYvnzk6rye3xWLaQ,1803
15
+ huitzo_sdk/storage/protocol.py,sha256=xNPfdPsiOKG7ku0uGsUoJaN07GvO1_TlS9j-lmRwvAI,4363
16
+ huitzo_sdk-0.0.0.dist-info/METADATA,sha256=6DuYxD3GsdgzBBssIJHNK00IxEynnWCx1iEAnh1gDSQ,405
17
+ huitzo_sdk-0.0.0.dist-info/WHEEL,sha256=WLgqFyCfm_KASv4WHyYy0P3pM_m7J5L9k2skdKLirC8,87
18
+ huitzo_sdk-0.0.0.dist-info/RECORD,,
@@ -0,0 +1,4 @@
1
+ Wheel-Version: 1.0
2
+ Generator: hatchling 1.28.0
3
+ Root-Is-Purelib: true
4
+ Tag: py3-none-any