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,109 @@
1
+ """
2
+ Module: integrations.email
3
+ Description: Email integration client for sending emails and templates.
4
+
5
+ Implements:
6
+ - docs/sdk/integrations.md#email-integration
7
+
8
+ See Also:
9
+ - docs/sdk/error-handling.md (for EmailError)
10
+ """
11
+
12
+ from __future__ import annotations
13
+
14
+ from dataclasses import dataclass
15
+ from typing import Any, Protocol, runtime_checkable
16
+
17
+
18
+ @runtime_checkable
19
+ class EmailBackend(Protocol):
20
+ """Backend protocol for email providers."""
21
+
22
+ async def send(
23
+ self,
24
+ *,
25
+ to: str | list[str],
26
+ subject: str,
27
+ body: str | None,
28
+ html: str | None,
29
+ cc: list[str] | None,
30
+ bcc: list[str] | None,
31
+ attachments: list[dict[str, Any]] | None,
32
+ ) -> None: ...
33
+
34
+ async def send_template(
35
+ self,
36
+ *,
37
+ to: str | list[str],
38
+ template_id: str,
39
+ template_data: dict[str, Any] | None,
40
+ ) -> None: ...
41
+
42
+
43
+ @dataclass
44
+ class EmailClient:
45
+ """Client for sending emails.
46
+
47
+ This is the SDK-side interface. The backend injects a real implementation
48
+ at runtime (SendGrid, SMTP, etc.).
49
+ """
50
+
51
+ _backend: EmailBackend
52
+
53
+ async def send(
54
+ self,
55
+ *,
56
+ to: str | list[str],
57
+ subject: str,
58
+ body: str | None = None,
59
+ html: str | None = None,
60
+ cc: list[str] | None = None,
61
+ bcc: list[str] | None = None,
62
+ attachments: list[dict[str, Any]] | None = None,
63
+ ) -> None:
64
+ """Send an email.
65
+
66
+ Args:
67
+ to: Recipient(s).
68
+ subject: Email subject.
69
+ body: Plain text body.
70
+ html: HTML body.
71
+ cc: CC recipients.
72
+ bcc: BCC recipients.
73
+ attachments: List of attachment dicts with filename, content, content_type.
74
+
75
+ Raises:
76
+ EmailError: If sending fails.
77
+ """
78
+ await self._backend.send(
79
+ to=to,
80
+ subject=subject,
81
+ body=body,
82
+ html=html,
83
+ cc=cc,
84
+ bcc=bcc,
85
+ attachments=attachments,
86
+ )
87
+
88
+ async def send_template(
89
+ self,
90
+ *,
91
+ to: str | list[str],
92
+ template_id: str,
93
+ template_data: dict[str, Any] | None = None,
94
+ ) -> None:
95
+ """Send a templated email.
96
+
97
+ Args:
98
+ to: Recipient(s).
99
+ template_id: Provider template ID.
100
+ template_data: Template variable substitutions.
101
+
102
+ Raises:
103
+ EmailError: If sending fails.
104
+ """
105
+ await self._backend.send_template(
106
+ to=to,
107
+ template_id=template_id,
108
+ template_data=template_data,
109
+ )
@@ -0,0 +1,188 @@
1
+ """
2
+ Module: integrations.files
3
+ Description: File integration client for reading and writing files with backend abstraction.
4
+
5
+ Implements:
6
+ - docs/sdk/integrations.md#files-integration
7
+
8
+ See Also:
9
+ - docs/sdk/context.md (for Context wiring)
10
+ """
11
+
12
+ from __future__ import annotations
13
+
14
+ import builtins
15
+ from dataclasses import dataclass
16
+ from typing import Any, Protocol, runtime_checkable
17
+
18
+
19
+ @runtime_checkable
20
+ class FileStorageBackend(Protocol):
21
+ """Backend protocol for file storage (local, S3, Azure, GCS)."""
22
+
23
+ async def read(self, path: str) -> bytes: ...
24
+
25
+ async def write(self, path: str, content: bytes) -> None: ...
26
+
27
+ async def info(self, path: str) -> dict[str, Any]: ...
28
+
29
+ async def list(self, prefix: str) -> builtins.list[dict[str, Any]]: ...
30
+
31
+ async def exists(self, path: str) -> bool: ...
32
+
33
+ async def get_url(self, path: str, *, expires: int) -> str: ...
34
+
35
+
36
+ @dataclass
37
+ class FileClient:
38
+ """Client for reading and writing files.
39
+
40
+ Backend-agnostic: works with local filesystem, S3, Azure Blob, or GCS.
41
+ Tenant path isolation is handled automatically by the backend.
42
+ """
43
+
44
+ _backend: FileStorageBackend
45
+
46
+ async def read_excel(
47
+ self,
48
+ path: str,
49
+ *,
50
+ sheet: str | None = None,
51
+ ) -> Any:
52
+ """Read an Excel file and return a DataFrame.
53
+
54
+ Args:
55
+ path: Relative file path.
56
+ sheet: Specific sheet name (default: first sheet).
57
+
58
+ Returns:
59
+ pandas DataFrame.
60
+
61
+ Raises:
62
+ ImportError: If pandas is not installed.
63
+ IntegrationError: If the backend read fails.
64
+ """
65
+ data = await self._backend.read(path)
66
+ import io
67
+
68
+ try:
69
+ import pandas as pd
70
+ except ImportError as exc:
71
+ raise ImportError("pandas is required for read_excel") from exc
72
+ kwargs: dict[str, Any] = {}
73
+ if sheet is not None:
74
+ kwargs["sheet_name"] = sheet
75
+ return pd.read_excel(io.BytesIO(data), **kwargs)
76
+
77
+ async def read_csv(
78
+ self,
79
+ path: str,
80
+ *,
81
+ delimiter: str = ",",
82
+ encoding: str = "utf-8",
83
+ ) -> Any:
84
+ """Read a CSV file and return a DataFrame.
85
+
86
+ Args:
87
+ path: Relative file path.
88
+ delimiter: Column delimiter.
89
+ encoding: File encoding.
90
+
91
+ Returns:
92
+ pandas DataFrame.
93
+
94
+ Raises:
95
+ ImportError: If pandas is not installed.
96
+ IntegrationError: If the backend read fails.
97
+ """
98
+ data = await self._backend.read(path)
99
+ import io
100
+
101
+ try:
102
+ import pandas as pd
103
+ except ImportError as exc:
104
+ raise ImportError("pandas is required for read_csv") from exc
105
+ return pd.read_csv(io.BytesIO(data), delimiter=delimiter, encoding=encoding)
106
+
107
+ async def read_json(self, path: str) -> Any:
108
+ """Read a JSON file.
109
+
110
+ Args:
111
+ path: Relative file path.
112
+
113
+ Returns:
114
+ Parsed JSON data.
115
+
116
+ Raises:
117
+ IntegrationError: If the backend read fails.
118
+ json.JSONDecodeError: If file content is not valid JSON.
119
+ """
120
+ import json
121
+
122
+ data = await self._backend.read(path)
123
+ return json.loads(data)
124
+
125
+ async def write(
126
+ self,
127
+ path: str,
128
+ content: str | bytes,
129
+ *,
130
+ binary: bool = False,
131
+ ) -> None:
132
+ """Write content to a file.
133
+
134
+ Args:
135
+ path: Relative file path.
136
+ content: File content (string or bytes).
137
+ binary: If True, content is bytes.
138
+
139
+ Raises:
140
+ IntegrationError: If the backend write fails.
141
+ """
142
+ if isinstance(content, str):
143
+ raw = content.encode("utf-8")
144
+ else:
145
+ raw = content
146
+ await self._backend.write(path, raw)
147
+
148
+ async def info(self, path: str) -> dict[str, Any]:
149
+ """Get file metadata.
150
+
151
+ Args:
152
+ path: Relative file path.
153
+
154
+ Returns:
155
+ Dict with size, created, modified, type.
156
+ """
157
+ return await self._backend.info(path)
158
+
159
+ async def list(self, prefix: str = "") -> builtins.list[dict[str, Any]]:
160
+ """List files matching a prefix.
161
+
162
+ Args:
163
+ prefix: Path prefix (e.g. "uploads/").
164
+
165
+ Returns:
166
+ List of file info dicts.
167
+ """
168
+ return await self._backend.list(prefix)
169
+
170
+ async def exists(self, path: str) -> bool:
171
+ """Check if a file exists.
172
+
173
+ Args:
174
+ path: Relative file path.
175
+ """
176
+ return await self._backend.exists(path)
177
+
178
+ async def get_url(self, path: str, *, expires: int = 3600) -> str:
179
+ """Generate a download URL for a file.
180
+
181
+ Args:
182
+ path: Relative file path.
183
+ expires: URL expiration in seconds.
184
+
185
+ Returns:
186
+ Presigned download URL.
187
+ """
188
+ return await self._backend.get_url(path, expires=expires)
@@ -0,0 +1,173 @@
1
+ """
2
+ Module: integrations.http
3
+ Description: HTTP integration client for external API requests with domain restrictions.
4
+
5
+ Implements:
6
+ - docs/sdk/integrations.md#http-integration
7
+
8
+ See Also:
9
+ - docs/sdk/error-handling.md (for HTTPError, HTTPSecurityError)
10
+ """
11
+
12
+ from __future__ import annotations
13
+
14
+ from dataclasses import dataclass, field
15
+ from typing import Any, Protocol, runtime_checkable
16
+ from urllib.parse import urlparse
17
+
18
+ from huitzo_sdk.errors import HTTPSecurityError
19
+
20
+
21
+ @runtime_checkable
22
+ class HTTPBackend(Protocol):
23
+ """Backend protocol for HTTP requests."""
24
+
25
+ async def request(
26
+ self,
27
+ method: str,
28
+ url: str,
29
+ *,
30
+ headers: dict[str, str] | None,
31
+ params: dict[str, Any] | None,
32
+ json: Any | None,
33
+ data: dict[str, Any] | None,
34
+ files: dict[str, bytes] | None,
35
+ timeout: int | None,
36
+ ) -> Any: ...
37
+
38
+
39
+ @dataclass
40
+ class HTTPClient:
41
+ """Client for making HTTP requests to external APIs.
42
+
43
+ Supports domain restrictions to limit which external services
44
+ a pack can communicate with.
45
+ """
46
+
47
+ _backend: HTTPBackend
48
+ _allowed_domains: list[str] = field(default_factory=list)
49
+
50
+ def _check_domain(self, url: str) -> None:
51
+ """Validate the URL domain against allowed domains."""
52
+ if not self._allowed_domains:
53
+ return
54
+ parsed = urlparse(url)
55
+ domain = parsed.hostname or ""
56
+ for allowed in self._allowed_domains:
57
+ if allowed.startswith("*."):
58
+ base = allowed[2:] # e.g. "trusted-domain.org"
59
+ if domain.endswith("." + base):
60
+ return
61
+ elif domain == allowed:
62
+ return
63
+ raise HTTPSecurityError(domain=domain)
64
+
65
+ async def get(
66
+ self,
67
+ url: str,
68
+ *,
69
+ headers: dict[str, str] | None = None,
70
+ params: dict[str, Any] | None = None,
71
+ timeout: int | None = None,
72
+ ) -> Any:
73
+ """Send a GET request.
74
+
75
+ Raises:
76
+ HTTPError: If the request fails.
77
+ HTTPSecurityError: If domain not allowed.
78
+ """
79
+ self._check_domain(url)
80
+ return await self._backend.request(
81
+ "GET",
82
+ url,
83
+ headers=headers,
84
+ params=params,
85
+ json=None,
86
+ data=None,
87
+ files=None,
88
+ timeout=timeout,
89
+ )
90
+
91
+ async def post(
92
+ self,
93
+ url: str,
94
+ *,
95
+ headers: dict[str, str] | None = None,
96
+ params: dict[str, Any] | None = None,
97
+ json: Any | None = None,
98
+ data: dict[str, Any] | None = None,
99
+ files: dict[str, bytes] | None = None,
100
+ timeout: int | None = None,
101
+ ) -> Any:
102
+ """Send a POST request.
103
+
104
+ Raises:
105
+ HTTPError: If the request fails.
106
+ HTTPSecurityError: If domain not allowed.
107
+ """
108
+ self._check_domain(url)
109
+ return await self._backend.request(
110
+ "POST",
111
+ url,
112
+ headers=headers,
113
+ params=params,
114
+ json=json,
115
+ data=data,
116
+ files=files,
117
+ timeout=timeout,
118
+ )
119
+
120
+ async def put(
121
+ self,
122
+ url: str,
123
+ *,
124
+ headers: dict[str, str] | None = None,
125
+ params: dict[str, Any] | None = None,
126
+ json: Any | None = None,
127
+ data: dict[str, Any] | None = None,
128
+ files: dict[str, bytes] | None = None,
129
+ timeout: int | None = None,
130
+ ) -> Any:
131
+ """Send a PUT request.
132
+
133
+ Raises:
134
+ HTTPError: If the request fails.
135
+ HTTPSecurityError: If domain not allowed.
136
+ """
137
+ self._check_domain(url)
138
+ return await self._backend.request(
139
+ "PUT",
140
+ url,
141
+ headers=headers,
142
+ params=params,
143
+ json=json,
144
+ data=data,
145
+ files=files,
146
+ timeout=timeout,
147
+ )
148
+
149
+ async def delete(
150
+ self,
151
+ url: str,
152
+ *,
153
+ headers: dict[str, str] | None = None,
154
+ params: dict[str, Any] | None = None,
155
+ timeout: int | None = None,
156
+ ) -> Any:
157
+ """Send a DELETE request.
158
+
159
+ Raises:
160
+ HTTPError: If the request fails.
161
+ HTTPSecurityError: If domain not allowed.
162
+ """
163
+ self._check_domain(url)
164
+ return await self._backend.request(
165
+ "DELETE",
166
+ url,
167
+ headers=headers,
168
+ params=params,
169
+ json=None,
170
+ data=None,
171
+ files=None,
172
+ timeout=timeout,
173
+ )
@@ -0,0 +1,157 @@
1
+ """
2
+ Module: integrations.llm
3
+ Description: LLM integration client for AI completions and streaming.
4
+
5
+ Implements:
6
+ - docs/sdk/integrations.md#llm-integration
7
+
8
+ See Also:
9
+ - docs/sdk/error-handling.md (for LLMError)
10
+ """
11
+
12
+ from __future__ import annotations
13
+
14
+ from dataclasses import dataclass, field
15
+ from typing import Any, AsyncIterator, Protocol, runtime_checkable
16
+
17
+
18
+ @runtime_checkable
19
+ class LLMBackend(Protocol):
20
+ """Backend protocol for LLM providers."""
21
+
22
+ async def complete(
23
+ self,
24
+ *,
25
+ prompt: str,
26
+ system: str | None,
27
+ model: str,
28
+ temperature: float,
29
+ max_tokens: int | None,
30
+ top_p: float | None,
31
+ stop: list[str] | None,
32
+ response_format: str | None,
33
+ schema: type[Any] | None,
34
+ ) -> str: ...
35
+
36
+ def stream(
37
+ self,
38
+ *,
39
+ prompt: str,
40
+ system: str | None,
41
+ model: str,
42
+ temperature: float,
43
+ max_tokens: int | None,
44
+ top_p: float | None,
45
+ stop: list[str] | None,
46
+ ) -> AsyncIterator[str]: ...
47
+
48
+
49
+ @dataclass
50
+ class TokenUsage:
51
+ """Token usage tracking for a single LLM call."""
52
+
53
+ prompt_tokens: int = 0
54
+ completion_tokens: int = 0
55
+
56
+ @property
57
+ def total_tokens(self) -> int:
58
+ return self.prompt_tokens + self.completion_tokens
59
+
60
+
61
+ @dataclass
62
+ class LLMClient:
63
+ """Client for LLM completions and streaming.
64
+
65
+ This is the SDK-side interface. The backend injects a real implementation
66
+ at runtime that handles actual API calls.
67
+ """
68
+
69
+ _backend: LLMBackend
70
+ _usage: list[TokenUsage] = field(default_factory=list, init=False)
71
+
72
+ async def complete(
73
+ self,
74
+ prompt: str,
75
+ *,
76
+ model: str = "gpt-4o-mini",
77
+ system: str | None = None,
78
+ temperature: float = 1.0,
79
+ max_tokens: int | None = None,
80
+ top_p: float | None = None,
81
+ stop: list[str] | None = None,
82
+ response_format: str | None = None,
83
+ schema: type[Any] | None = None,
84
+ ) -> str:
85
+ """Generate a completion from an LLM.
86
+
87
+ Args:
88
+ prompt: The user prompt.
89
+ model: Model identifier (e.g. "gpt-4o", "claude-3-5-sonnet").
90
+ system: Optional system message.
91
+ temperature: Sampling temperature (0.0-2.0).
92
+ max_tokens: Maximum response tokens.
93
+ top_p: Nucleus sampling parameter.
94
+ stop: Stop sequences.
95
+ response_format: "json" for structured output.
96
+ schema: Pydantic model for JSON mode validation.
97
+
98
+ Returns:
99
+ The completion text.
100
+
101
+ Raises:
102
+ LLMError: If the LLM call fails.
103
+ """
104
+ return await self._backend.complete(
105
+ prompt=prompt,
106
+ system=system,
107
+ model=model,
108
+ temperature=temperature,
109
+ max_tokens=max_tokens,
110
+ top_p=top_p,
111
+ stop=stop,
112
+ response_format=response_format,
113
+ schema=schema,
114
+ )
115
+
116
+ def stream(
117
+ self,
118
+ prompt: str,
119
+ *,
120
+ model: str = "gpt-4o-mini",
121
+ system: str | None = None,
122
+ temperature: float = 1.0,
123
+ max_tokens: int | None = None,
124
+ top_p: float | None = None,
125
+ stop: list[str] | None = None,
126
+ ) -> AsyncIterator[str]:
127
+ """Stream a completion from an LLM.
128
+
129
+ Args:
130
+ prompt: The user prompt.
131
+ model: Model identifier.
132
+ system: Optional system message.
133
+ temperature: Sampling temperature.
134
+ max_tokens: Maximum response tokens.
135
+ top_p: Nucleus sampling parameter.
136
+ stop: Stop sequences.
137
+
138
+ Returns:
139
+ An async iterator yielding text chunks.
140
+
141
+ Raises:
142
+ LLMError: If the LLM call fails.
143
+ """
144
+ return self._backend.stream(
145
+ prompt=prompt,
146
+ system=system,
147
+ model=model,
148
+ temperature=temperature,
149
+ max_tokens=max_tokens,
150
+ top_p=top_p,
151
+ stop=stop,
152
+ )
153
+
154
+ @property
155
+ def usage(self) -> list[TokenUsage]:
156
+ """Token usage records for this client."""
157
+ return list(self._usage)