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 +85 -0
- huitzo_sdk/command.py +180 -0
- huitzo_sdk/context.py +122 -0
- huitzo_sdk/errors.py +241 -0
- huitzo_sdk/integrations/__init__.py +26 -0
- huitzo_sdk/integrations/email.py +109 -0
- huitzo_sdk/integrations/files.py +188 -0
- huitzo_sdk/integrations/http.py +173 -0
- huitzo_sdk/integrations/llm.py +157 -0
- huitzo_sdk/integrations/telegram.py +117 -0
- huitzo_sdk/storage/__init__.py +21 -0
- huitzo_sdk/storage/memory.py +181 -0
- huitzo_sdk/storage/namespace.py +64 -0
- huitzo_sdk/storage/protocol.py +173 -0
- huitzo_sdk/types.py +47 -0
- huitzo_sdk-0.0.0.dist-info/METADATA +12 -0
- huitzo_sdk-0.0.0.dist-info/RECORD +18 -0
- huitzo_sdk-0.0.0.dist-info/WHEEL +4 -0
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
|
+
]
|