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.
huitzo_sdk/__init__.py ADDED
@@ -0,0 +1,85 @@
1
+ """
2
+ Module: huitzo_sdk
3
+ Description: Huitzo SDK for building Intelligence Packs.
4
+
5
+ Implements:
6
+ - docs/sdk/overview.md
7
+ - docs/sdk/commands.md
8
+ - docs/sdk/context.md
9
+ - docs/sdk/error-handling.md
10
+
11
+ See Also:
12
+ - docs/sdk/storage.md
13
+ - docs/sdk/storage-backends.md
14
+ - docs/sdk/integrations.md
15
+ """
16
+
17
+ from huitzo_sdk.command import HuitzoCommand, command
18
+ from huitzo_sdk.context import Context
19
+ from huitzo_sdk.errors import (
20
+ CommandError,
21
+ ConfigurationError,
22
+ EmailError,
23
+ ExternalAPIError,
24
+ HTTPError,
25
+ HTTPSecurityError,
26
+ HuitzoError,
27
+ IntegrationError,
28
+ LLMError,
29
+ PackExecutionError,
30
+ PermissionError,
31
+ RateLimitError,
32
+ SecretsError,
33
+ StorageError,
34
+ TimeoutError,
35
+ ValidationError,
36
+ )
37
+ from huitzo_sdk.integrations import (
38
+ EmailClient,
39
+ FileClient,
40
+ FileStorageBackend,
41
+ HTTPClient,
42
+ LLMClient,
43
+ TelegramClient,
44
+ )
45
+ from huitzo_sdk.storage import InMemoryBackend, StorageBackend, StorageNamespace
46
+ from huitzo_sdk.types import CommandMetadata, DeploymentMode, Result
47
+
48
+ __all__ = [
49
+ # Core
50
+ "command",
51
+ "HuitzoCommand",
52
+ "Context",
53
+ # Integrations
54
+ "LLMClient",
55
+ "EmailClient",
56
+ "HTTPClient",
57
+ "TelegramClient",
58
+ "FileClient",
59
+ "FileStorageBackend",
60
+ # Storage
61
+ "StorageBackend",
62
+ "InMemoryBackend",
63
+ "StorageNamespace",
64
+ # Types
65
+ "CommandMetadata",
66
+ "DeploymentMode",
67
+ "Result",
68
+ # Errors
69
+ "HuitzoError",
70
+ "CommandError",
71
+ "ValidationError",
72
+ "TimeoutError",
73
+ "StorageError",
74
+ "SecretsError",
75
+ "ExternalAPIError",
76
+ "IntegrationError",
77
+ "LLMError",
78
+ "EmailError",
79
+ "HTTPError",
80
+ "HTTPSecurityError",
81
+ "PackExecutionError",
82
+ "PermissionError",
83
+ "ConfigurationError",
84
+ "RateLimitError",
85
+ ]
huitzo_sdk/command.py ADDED
@@ -0,0 +1,180 @@
1
+ """
2
+ Module: command
3
+ Description: @command decorator and HuitzoCommand base class for defining Intelligence Pack commands.
4
+
5
+ Implements:
6
+ - docs/sdk/commands.md#the-command-decorator
7
+ - docs/sdk/commands.md#decorator-parameters
8
+ - docs/sdk/commands.md#class-based-commands
9
+ - docs/sdk/commands.md#sync-vs-async-commands
10
+
11
+ See Also:
12
+ - docs/sdk/context.md (for Context API)
13
+ - docs/sdk/error-handling.md (for error patterns)
14
+ """
15
+
16
+ from __future__ import annotations
17
+
18
+ import asyncio
19
+ import functools
20
+ import inspect
21
+ from typing import Any, Callable, TypeVar, get_type_hints
22
+
23
+ from pydantic import BaseModel
24
+
25
+ from huitzo_sdk.context import Context
26
+ from huitzo_sdk.types import CommandMetadata, Result
27
+
28
+ F = TypeVar("F", bound=Callable[..., Any])
29
+
30
+
31
+ def command(
32
+ name: str,
33
+ namespace: str,
34
+ *,
35
+ version: str = "1.0.0",
36
+ timeout: int = 60,
37
+ retries: int = 3,
38
+ retry_backoff: float = 1.0,
39
+ retry_max_wait: int = 60,
40
+ queue: str = "default",
41
+ output_format: str = "auto",
42
+ description: str | None = None,
43
+ ) -> Callable[[F], F]:
44
+ """Decorator to register a function as a Huitzo command.
45
+
46
+ Stores CommandMetadata on the function, validates Pydantic args,
47
+ and supports both sync and async functions.
48
+ """
49
+ metadata = CommandMetadata(
50
+ name=name,
51
+ namespace=namespace,
52
+ version=version,
53
+ timeout=timeout,
54
+ retries=retries,
55
+ retry_backoff=retry_backoff,
56
+ retry_max_wait=retry_max_wait,
57
+ queue=queue,
58
+ output_format=output_format,
59
+ description=description,
60
+ )
61
+
62
+ def decorator(fn: F) -> F:
63
+ is_async = asyncio.iscoroutinefunction(fn)
64
+ hints = get_type_hints(fn)
65
+
66
+ # Find the Pydantic model type for args (first parameter)
67
+ params = list(inspect.signature(fn).parameters.keys())
68
+ args_type: type[BaseModel] | None = None
69
+ if params:
70
+ first_hint = hints.get(params[0])
71
+ if (
72
+ first_hint is not None
73
+ and isinstance(first_hint, type)
74
+ and issubclass(first_hint, BaseModel)
75
+ ):
76
+ args_type = first_hint
77
+
78
+ @functools.wraps(fn)
79
+ async def wrapper(args: Any, ctx: Context | None = None) -> Result:
80
+ # Validate args via Pydantic if type-annotated
81
+ validated_args = args
82
+ if args_type is not None and isinstance(args, dict):
83
+ validated_args = args_type.model_validate(args)
84
+
85
+ # Inject context if not provided
86
+ if ctx is None:
87
+ ctx = Context(
88
+ command_name=metadata.name,
89
+ namespace=metadata.namespace,
90
+ command_version=metadata.version,
91
+ )
92
+
93
+ if is_async:
94
+ result: Result = await fn(validated_args, ctx)
95
+ else:
96
+ result = await asyncio.to_thread(fn, validated_args, ctx)
97
+ return result
98
+
99
+ wrapper._huitzo_command = metadata # type: ignore[attr-defined]
100
+ return wrapper # type: ignore[return-value]
101
+
102
+ return decorator
103
+
104
+
105
+ class HuitzoCommand:
106
+ """Base class for class-based commands with lifecycle hooks.
107
+
108
+ Subclass and implement execute(). Optionally override lifecycle hooks.
109
+ """
110
+
111
+ name: str = ""
112
+ namespace: str = ""
113
+ version: str = "1.0.0"
114
+ timeout: int = 60
115
+ retries: int = 3
116
+ retry_backoff: float = 1.0
117
+ retry_max_wait: int = 60
118
+ queue: str = "default"
119
+ output_format: str = "auto"
120
+
121
+ def __init__(self) -> None:
122
+ self.configure()
123
+ self._metadata = CommandMetadata(
124
+ name=self.name,
125
+ namespace=self.namespace,
126
+ version=self.version,
127
+ timeout=self.timeout,
128
+ retries=self.retries,
129
+ retry_backoff=self.retry_backoff,
130
+ retry_max_wait=self.retry_max_wait,
131
+ queue=self.queue,
132
+ output_format=self.output_format,
133
+ )
134
+
135
+ @property
136
+ def metadata(self) -> CommandMetadata:
137
+ return self._metadata
138
+
139
+ def configure(self) -> None:
140
+ """Override to set options before execution."""
141
+
142
+ def on_start(self, args: Any) -> None:
143
+ """Called when command starts."""
144
+
145
+ def on_progress(self, percent: int, message: str) -> None:
146
+ """Called to report progress."""
147
+
148
+ def on_retry(self, attempt: int, error: Exception) -> None:
149
+ """Called before each retry."""
150
+
151
+ def on_complete(self, result: Any) -> None:
152
+ """Called after successful completion."""
153
+
154
+ def on_error(self, error: Exception) -> None:
155
+ """Called on failure."""
156
+
157
+ def update_state(self, **kwargs: Any) -> None:
158
+ """Update command state (e.g., progress). Stub for runtime integration."""
159
+
160
+ async def execute(self, args: Any, ctx: Context) -> Result:
161
+ """Main execution logic. Must be overridden."""
162
+ raise NotImplementedError("Subclasses must implement execute()")
163
+
164
+ async def run(self, args: Any, ctx: Context | None = None) -> Result:
165
+ """Execute the command with lifecycle hooks."""
166
+ if ctx is None:
167
+ ctx = Context(
168
+ command_name=self.name,
169
+ namespace=self.namespace,
170
+ command_version=self.version,
171
+ )
172
+
173
+ self.on_start(args)
174
+ try:
175
+ result = await self.execute(args, ctx)
176
+ self.on_complete(result)
177
+ return result
178
+ except Exception as e:
179
+ self.on_error(e)
180
+ raise
huitzo_sdk/context.py ADDED
@@ -0,0 +1,122 @@
1
+ """
2
+ Module: context
3
+ Description: Context object passed to every command with identity, metadata, and platform services.
4
+
5
+ Implements:
6
+ - docs/sdk/context.md#context-properties
7
+ - docs/sdk/context.md#services
8
+ - docs/sdk/integrations.md
9
+
10
+ See Also:
11
+ - docs/sdk/commands.md (for command patterns)
12
+ - docs/sdk/storage.md (for storage API)
13
+ """
14
+
15
+ from __future__ import annotations
16
+
17
+ from dataclasses import dataclass, field
18
+ from typing import Any
19
+ from uuid import UUID, uuid4
20
+
21
+ from huitzo_sdk.integrations.email import EmailClient
22
+ from huitzo_sdk.integrations.files import FileClient
23
+ from huitzo_sdk.integrations.http import HTTPClient
24
+ from huitzo_sdk.integrations.llm import LLMClient
25
+ from huitzo_sdk.integrations.telegram import TelegramClient
26
+ from huitzo_sdk.types import DeploymentMode
27
+
28
+
29
+ _NOT_WIRED = (
30
+ "not available. This integration is injected by the Huitzo backend at runtime. "
31
+ "If you see this in production, contact support."
32
+ )
33
+
34
+ # Services still stubbed for future PRs
35
+ _FUTURE_STUBS: dict[str, str] = {
36
+ "storage": "Storage implemented in PR-4.",
37
+ "log": "Logging planned for PR-6+.",
38
+ "env": "Environment access planned for PR-6+.",
39
+ "config": "Configuration access planned for PR-6+.",
40
+ "secrets": "Secrets access planned for PR-6+.",
41
+ }
42
+
43
+
44
+ @dataclass
45
+ class Context:
46
+ """Context object passed to every command execution.
47
+
48
+ Provides identity, metadata, and access to platform services.
49
+ """
50
+
51
+ # Identity
52
+ user_id: UUID = field(default_factory=uuid4)
53
+ tenant_id: UUID = field(default_factory=uuid4)
54
+ session_id: UUID = field(default_factory=uuid4)
55
+ correlation_id: str = field(default_factory=lambda: str(uuid4()))
56
+
57
+ # Command metadata
58
+ command_name: str = ""
59
+ namespace: str = ""
60
+ command_version: str = "1.0.0"
61
+ deployment_mode: DeploymentMode = DeploymentMode.CLOUD
62
+
63
+ # Integration clients (injected by backend at runtime)
64
+ _llm: LLMClient | None = field(default=None, repr=False)
65
+ _email: EmailClient | None = field(default=None, repr=False)
66
+ _http: HTTPClient | None = field(default=None, repr=False)
67
+ _telegram: TelegramClient | None = field(default=None, repr=False)
68
+ _files: FileClient | None = field(default=None, repr=False)
69
+
70
+ @property
71
+ def llm(self) -> LLMClient:
72
+ """LLM integration client."""
73
+ if self._llm is None:
74
+ raise NotImplementedError(f"ctx.llm is {_NOT_WIRED}")
75
+ return self._llm
76
+
77
+ @property
78
+ def email(self) -> EmailClient:
79
+ """Email integration client."""
80
+ if self._email is None:
81
+ raise NotImplementedError(f"ctx.email is {_NOT_WIRED}")
82
+ return self._email
83
+
84
+ @property
85
+ def http(self) -> HTTPClient:
86
+ """HTTP integration client."""
87
+ if self._http is None:
88
+ raise NotImplementedError(f"ctx.http is {_NOT_WIRED}")
89
+ return self._http
90
+
91
+ @property
92
+ def telegram(self) -> TelegramClient:
93
+ """Telegram integration client."""
94
+ if self._telegram is None:
95
+ raise NotImplementedError(f"ctx.telegram is {_NOT_WIRED}")
96
+ return self._telegram
97
+
98
+ @property
99
+ def files(self) -> FileClient:
100
+ """File integration client."""
101
+ if self._files is None:
102
+ raise NotImplementedError(f"ctx.files is {_NOT_WIRED}")
103
+ return self._files
104
+
105
+ def __getattr__(self, name: str) -> Any:
106
+ if name.startswith("_"):
107
+ raise AttributeError(f"'Context' has no attribute '{name}'")
108
+ if name in _FUTURE_STUBS:
109
+ raise NotImplementedError(f"ctx.{name} is not yet implemented. {_FUTURE_STUBS[name]}")
110
+ raise AttributeError(f"'Context' has no attribute '{name}'")
111
+
112
+ async def execute(
113
+ self,
114
+ pack: str,
115
+ command: str,
116
+ args: dict[str, Any],
117
+ timeout: int | None = None,
118
+ ) -> Any:
119
+ """Execute a command from another pack. Not yet implemented."""
120
+ raise NotImplementedError(
121
+ "ctx.execute() for cross-pack calls is not yet implemented. Implemented in PR-6."
122
+ )
huitzo_sdk/errors.py ADDED
@@ -0,0 +1,241 @@
1
+ """
2
+ Module: errors
3
+ Description: SDK exception hierarchy for structured error handling.
4
+
5
+ Implements:
6
+ - docs/sdk/error-handling.md#sdk-exception-hierarchy
7
+ - docs/sdk/error-handling.md#exception-reference
8
+
9
+ See Also:
10
+ - docs/sdk/commands.md (for command error patterns)
11
+ """
12
+
13
+ from __future__ import annotations
14
+
15
+ from typing import Any
16
+
17
+
18
+ class HuitzoError(Exception):
19
+ """Base exception for all Huitzo SDK errors."""
20
+
21
+ code: str = "HUITZO_ERROR"
22
+ http_status: int = 500
23
+ retryable: bool = False
24
+
25
+ def __init__(self, message: str, *, details: dict[str, Any] | None = None) -> None:
26
+ super().__init__(message)
27
+ self.message = message
28
+ self.details = details or {}
29
+
30
+
31
+ class CommandError(HuitzoError):
32
+ """General command execution failure."""
33
+
34
+ code = "COMMAND_FAILED"
35
+
36
+ def __init__(
37
+ self,
38
+ message: str,
39
+ *,
40
+ exit_code: int = 1,
41
+ details: dict[str, Any] | None = None,
42
+ ) -> None:
43
+ super().__init__(message, details=details)
44
+ self.exit_code = exit_code
45
+
46
+
47
+ class ValidationError(HuitzoError):
48
+ """Input validation failed."""
49
+
50
+ code = "VALIDATION_FAILED"
51
+ http_status = 400
52
+
53
+ def __init__(self, *, field: str, value: Any, message: str) -> None:
54
+ super().__init__(message)
55
+ self.field = field
56
+ self.value = value
57
+
58
+
59
+ class TimeoutError(HuitzoError):
60
+ """Command exceeded its configured timeout."""
61
+
62
+ code = "TIMEOUT"
63
+ http_status = 408
64
+ retryable = True
65
+
66
+ def __init__(self, *, timeout_seconds: int, elapsed_seconds: float) -> None:
67
+ super().__init__(
68
+ f"Command timed out after {elapsed_seconds:.1f}s (limit: {timeout_seconds}s)"
69
+ )
70
+ self.timeout_seconds = timeout_seconds
71
+ self.elapsed_seconds = elapsed_seconds
72
+
73
+
74
+ class StorageError(HuitzoError):
75
+ """Storage operation failed."""
76
+
77
+ code = "STORAGE_ERROR"
78
+
79
+ def __init__(self, *, operation: str, key: str | None = None, message: str) -> None:
80
+ super().__init__(message)
81
+ self.operation = operation
82
+ self.key = key
83
+
84
+
85
+ class SecretsError(HuitzoError):
86
+ """User secret access failure."""
87
+
88
+ code = "SECRET_MISSING"
89
+ http_status = 400
90
+
91
+ def __init__(self, *, secret_name: str, message: str) -> None:
92
+ super().__init__(message)
93
+ self.secret_name = secret_name
94
+
95
+
96
+ class ExternalAPIError(HuitzoError):
97
+ """User-configured external API failure."""
98
+
99
+ code = "EXTERNAL_API_ERROR"
100
+ http_status = 502
101
+
102
+ def __init__(self, *, service: str, message: str) -> None:
103
+ super().__init__(message)
104
+ self.service = service
105
+
106
+
107
+ class IntegrationError(HuitzoError):
108
+ """Platform service failure (base for LLM, Email, HTTP errors)."""
109
+
110
+ code = "INTEGRATION_ERROR"
111
+ http_status = 502
112
+ retryable = True
113
+
114
+ def __init__(
115
+ self,
116
+ *,
117
+ service: str,
118
+ message: str,
119
+ details: dict[str, Any] | None = None,
120
+ ) -> None:
121
+ super().__init__(message, details=details)
122
+ self.service = service
123
+
124
+
125
+ class LLMError(IntegrationError):
126
+ """LLM provider error."""
127
+
128
+ code = "LLM_ERROR"
129
+
130
+ def __init__(
131
+ self,
132
+ *,
133
+ provider: str,
134
+ model: str | None = None,
135
+ status_code: int | None = None,
136
+ message: str,
137
+ ) -> None:
138
+ super().__init__(service=f"llm:{provider}", message=message)
139
+ self.provider = provider
140
+ self.model = model
141
+ self.status_code = status_code
142
+
143
+
144
+ class EmailError(IntegrationError):
145
+ """Email sending error."""
146
+
147
+ code = "EMAIL_ERROR"
148
+
149
+ def __init__(self, *, message: str) -> None:
150
+ super().__init__(service="email", message=message)
151
+
152
+
153
+ class HTTPError(IntegrationError):
154
+ """HTTP request error."""
155
+
156
+ code = "HTTP_ERROR"
157
+
158
+ def __init__(
159
+ self,
160
+ *,
161
+ url: str,
162
+ method: str,
163
+ status_code: int | None = None,
164
+ response_body: str | None = None,
165
+ message: str,
166
+ ) -> None:
167
+ super().__init__(service="http", message=message)
168
+ self.url = url
169
+ self.method = method
170
+ self.status_code = status_code
171
+ self.response_body = response_body
172
+
173
+
174
+ class HTTPSecurityError(HuitzoError):
175
+ """Request to disallowed domain."""
176
+
177
+ code = "HTTP_SECURITY_ERROR"
178
+ http_status = 403
179
+
180
+ def __init__(self, *, domain: str, message: str | None = None) -> None:
181
+ super().__init__(message or f"Domain not allowed: {domain}")
182
+ self.domain = domain
183
+
184
+
185
+ class PackExecutionError(HuitzoError):
186
+ """Cross-pack command execution failed."""
187
+
188
+ code = "PACK_EXECUTION_ERROR"
189
+
190
+ def __init__(
191
+ self,
192
+ *,
193
+ pack: str,
194
+ command: str,
195
+ original_error: Exception | None = None,
196
+ message: str,
197
+ ) -> None:
198
+ super().__init__(message)
199
+ self.pack = pack
200
+ self.command = command
201
+ self.original_error = original_error
202
+
203
+
204
+ class PermissionError(HuitzoError):
205
+ """Insufficient permissions."""
206
+
207
+ code = "PERMISSION_DENIED"
208
+ http_status = 403
209
+
210
+ def __init__(self, *, message: str) -> None:
211
+ super().__init__(message)
212
+
213
+
214
+ class ConfigurationError(HuitzoError):
215
+ """Invalid configuration."""
216
+
217
+ code = "CONFIGURATION_ERROR"
218
+
219
+ def __init__(self, *, message: str) -> None:
220
+ super().__init__(message)
221
+
222
+
223
+ class RateLimitError(HuitzoError):
224
+ """Rate limit exceeded."""
225
+
226
+ code = "RATE_LIMITED"
227
+ http_status = 429
228
+ retryable = True
229
+
230
+ def __init__(
231
+ self,
232
+ *,
233
+ retry_after: float,
234
+ limit: str | None = None,
235
+ current: int | None = None,
236
+ message: str | None = None,
237
+ ) -> None:
238
+ super().__init__(message or f"Rate limit exceeded. Retry after {retry_after}s")
239
+ self.retry_after = retry_after
240
+ self.limit = limit
241
+ self.current = current
@@ -0,0 +1,26 @@
1
+ """
2
+ Module: integrations
3
+ Description: Built-in platform integration clients for the Huitzo SDK.
4
+
5
+ Implements:
6
+ - docs/sdk/integrations.md
7
+
8
+ See Also:
9
+ - docs/sdk/context.md (for Context wiring)
10
+ - docs/sdk/error-handling.md (for integration errors)
11
+ """
12
+
13
+ from huitzo_sdk.integrations.email import EmailClient
14
+ from huitzo_sdk.integrations.files import FileClient, FileStorageBackend
15
+ from huitzo_sdk.integrations.http import HTTPClient
16
+ from huitzo_sdk.integrations.llm import LLMClient
17
+ from huitzo_sdk.integrations.telegram import TelegramClient
18
+
19
+ __all__ = [
20
+ "LLMClient",
21
+ "EmailClient",
22
+ "HTTPClient",
23
+ "TelegramClient",
24
+ "FileClient",
25
+ "FileStorageBackend",
26
+ ]