breeth 0.1.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.
- breeth/__init__.py +73 -0
- breeth/_base.py +103 -0
- breeth/client.py +369 -0
- breeth/errors.py +38 -0
- breeth/types.py +237 -0
- breeth-0.1.0.dist-info/METADATA +130 -0
- breeth-0.1.0.dist-info/RECORD +9 -0
- breeth-0.1.0.dist-info/WHEEL +4 -0
- breeth-0.1.0.dist-info/licenses/LICENSE +21 -0
breeth/__init__.py
ADDED
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
"""Breeth: the official Python SDK for the Breeth memory API.
|
|
2
|
+
|
|
3
|
+
from breeth import BreethClient
|
|
4
|
+
|
|
5
|
+
breeth = BreethClient(api_key="ck_live_...")
|
|
6
|
+
breeth.write("Recruiter prefers candidates with prior HR-tech roles.")
|
|
7
|
+
results = breeth.retrieve("HR-tech background")
|
|
8
|
+
"""
|
|
9
|
+
from __future__ import annotations
|
|
10
|
+
|
|
11
|
+
from .client import AsyncBreethClient, BreethClient
|
|
12
|
+
from .errors import BreethError
|
|
13
|
+
from .types import (
|
|
14
|
+
CacheStats,
|
|
15
|
+
CogramTaskEnvelope,
|
|
16
|
+
DirectorProfileBlock,
|
|
17
|
+
EdgeHit,
|
|
18
|
+
EntityEdgeRow,
|
|
19
|
+
EntityEpisodeRow,
|
|
20
|
+
EntityMode,
|
|
21
|
+
EntityNarrativeRow,
|
|
22
|
+
EntityResponse,
|
|
23
|
+
EpisodeMentionRow,
|
|
24
|
+
ExtractedCounts,
|
|
25
|
+
GraphEdgeListResponse,
|
|
26
|
+
GraphEdgeRow,
|
|
27
|
+
GraphEntityListResponse,
|
|
28
|
+
GraphEntityRow,
|
|
29
|
+
GraphEpisodeListResponse,
|
|
30
|
+
GraphEpisodeRow,
|
|
31
|
+
GroupRow,
|
|
32
|
+
GroupsListResponse,
|
|
33
|
+
IntentSuggestion,
|
|
34
|
+
NeighborRow,
|
|
35
|
+
NodeDetailsResponse,
|
|
36
|
+
PatternEvidence,
|
|
37
|
+
RetrieveResponse,
|
|
38
|
+
WriteResponse,
|
|
39
|
+
)
|
|
40
|
+
|
|
41
|
+
__version__ = "0.1.0"
|
|
42
|
+
|
|
43
|
+
__all__ = [
|
|
44
|
+
"AsyncBreethClient",
|
|
45
|
+
"BreethClient",
|
|
46
|
+
"BreethError",
|
|
47
|
+
"CacheStats",
|
|
48
|
+
"CogramTaskEnvelope",
|
|
49
|
+
"DirectorProfileBlock",
|
|
50
|
+
"EdgeHit",
|
|
51
|
+
"EntityEdgeRow",
|
|
52
|
+
"EntityEpisodeRow",
|
|
53
|
+
"EntityMode",
|
|
54
|
+
"EntityNarrativeRow",
|
|
55
|
+
"EntityResponse",
|
|
56
|
+
"EpisodeMentionRow",
|
|
57
|
+
"ExtractedCounts",
|
|
58
|
+
"GraphEdgeListResponse",
|
|
59
|
+
"GraphEdgeRow",
|
|
60
|
+
"GraphEntityListResponse",
|
|
61
|
+
"GraphEntityRow",
|
|
62
|
+
"GraphEpisodeListResponse",
|
|
63
|
+
"GraphEpisodeRow",
|
|
64
|
+
"GroupRow",
|
|
65
|
+
"GroupsListResponse",
|
|
66
|
+
"IntentSuggestion",
|
|
67
|
+
"NeighborRow",
|
|
68
|
+
"NodeDetailsResponse",
|
|
69
|
+
"PatternEvidence",
|
|
70
|
+
"RetrieveResponse",
|
|
71
|
+
"WriteResponse",
|
|
72
|
+
"__version__",
|
|
73
|
+
]
|
breeth/_base.py
ADDED
|
@@ -0,0 +1,103 @@
|
|
|
1
|
+
"""Shared base utilities for the sync and async Breeth clients.
|
|
2
|
+
|
|
3
|
+
Both `BreethClient` and `AsyncBreethClient` are thin wrappers around
|
|
4
|
+
``httpx.Client`` / ``httpx.AsyncClient``. This module centralises:
|
|
5
|
+
|
|
6
|
+
* Base URL resolution (env var ``COGRAM_API_URL`` overrides the default).
|
|
7
|
+
* Authorization header construction.
|
|
8
|
+
* Optional ``X-End-User-Id`` passthrough for multi-end-user apps.
|
|
9
|
+
* HTTP error to ``BreethError`` mapping.
|
|
10
|
+
|
|
11
|
+
Note: ``X-Cogram-Team-Id`` is intentionally NOT set. The server resolves
|
|
12
|
+
the team from the ``ck_live_`` API key.
|
|
13
|
+
"""
|
|
14
|
+
from __future__ import annotations
|
|
15
|
+
|
|
16
|
+
import os
|
|
17
|
+
from typing import Any, Mapping, Optional
|
|
18
|
+
|
|
19
|
+
import httpx
|
|
20
|
+
|
|
21
|
+
from .errors import BreethError
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
DEFAULT_BASE_URL = "https://api.thebreeth.com"
|
|
25
|
+
API_PREFIX = "/v1"
|
|
26
|
+
DEFAULT_TIMEOUT = 30.0
|
|
27
|
+
USER_AGENT = "breeth-python/0.1.0"
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
def resolve_base_url(explicit: Optional[str]) -> str:
|
|
31
|
+
"""Pick the base URL in order: explicit arg, ``COGRAM_API_URL``,
|
|
32
|
+
SDK default. Trailing slashes are stripped so URL joining is
|
|
33
|
+
predictable."""
|
|
34
|
+
raw = explicit or os.environ.get("COGRAM_API_URL") or DEFAULT_BASE_URL
|
|
35
|
+
return raw.rstrip("/")
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
def resolve_api_key(explicit: Optional[str]) -> str:
|
|
39
|
+
key = explicit or os.environ.get("BREETH_API_KEY") or os.environ.get("COGRAM_API_KEY")
|
|
40
|
+
if not key:
|
|
41
|
+
raise BreethError(
|
|
42
|
+
"Missing API key. Pass api_key=... or set BREETH_API_KEY.",
|
|
43
|
+
status=0,
|
|
44
|
+
slug="missing_api_key",
|
|
45
|
+
)
|
|
46
|
+
return key
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
def build_headers(api_key: str, end_user_id: Optional[str]) -> dict[str, str]:
|
|
50
|
+
headers = {
|
|
51
|
+
"Authorization": f"Bearer {api_key}",
|
|
52
|
+
"User-Agent": USER_AGENT,
|
|
53
|
+
"Accept": "application/json",
|
|
54
|
+
}
|
|
55
|
+
if end_user_id:
|
|
56
|
+
headers["X-End-User-Id"] = end_user_id
|
|
57
|
+
return headers
|
|
58
|
+
|
|
59
|
+
|
|
60
|
+
def build_url(base_url: str, path: str) -> str:
|
|
61
|
+
"""Join the base URL, the /v1 prefix, and a route path. The path
|
|
62
|
+
can be supplied with or without a leading slash."""
|
|
63
|
+
suffix = path if path.startswith("/") else f"/{path}"
|
|
64
|
+
return f"{base_url}{API_PREFIX}{suffix}"
|
|
65
|
+
|
|
66
|
+
|
|
67
|
+
def raise_for_status(response: httpx.Response) -> None:
|
|
68
|
+
"""Map any 4xx / 5xx response to a ``BreethError``.
|
|
69
|
+
|
|
70
|
+
The API returns JSON shaped like
|
|
71
|
+
``{"detail": {"error": "<slug>", "message": "..."}}`` for handled
|
|
72
|
+
errors, and FastAPI's default ``{"detail": "..."}`` for unhandled
|
|
73
|
+
validation issues. We accept both and extract whatever we can.
|
|
74
|
+
"""
|
|
75
|
+
if response.is_success:
|
|
76
|
+
return
|
|
77
|
+
|
|
78
|
+
slug: Optional[str] = None
|
|
79
|
+
message: str = response.reason_phrase or "Request failed"
|
|
80
|
+
body: Any = None
|
|
81
|
+
try:
|
|
82
|
+
body = response.json()
|
|
83
|
+
except Exception:
|
|
84
|
+
body = response.text or None
|
|
85
|
+
|
|
86
|
+
if isinstance(body, dict):
|
|
87
|
+
detail = body.get("detail", body)
|
|
88
|
+
if isinstance(detail, dict):
|
|
89
|
+
slug = detail.get("error") or detail.get("slug") or slug
|
|
90
|
+
message = detail.get("message") or detail.get("detail") or message
|
|
91
|
+
elif isinstance(detail, str):
|
|
92
|
+
message = detail
|
|
93
|
+
|
|
94
|
+
raise BreethError(message, status=response.status_code, slug=slug, body=body)
|
|
95
|
+
|
|
96
|
+
|
|
97
|
+
def merge_query(params: Optional[Mapping[str, Any]]) -> Optional[dict[str, Any]]:
|
|
98
|
+
"""Drop None-valued query parameters so callers can pass them
|
|
99
|
+
unconditionally without polluting the URL."""
|
|
100
|
+
if not params:
|
|
101
|
+
return None
|
|
102
|
+
out = {k: v for k, v in params.items() if v is not None}
|
|
103
|
+
return out or None
|
breeth/client.py
ADDED
|
@@ -0,0 +1,369 @@
|
|
|
1
|
+
"""Sync and async clients for the Breeth API.
|
|
2
|
+
|
|
3
|
+
Usage:
|
|
4
|
+
|
|
5
|
+
from breeth import BreethClient
|
|
6
|
+
|
|
7
|
+
with BreethClient(api_key="ck_live_...") as breeth:
|
|
8
|
+
breeth.write("Recruiter prefers candidates with HR-tech experience.")
|
|
9
|
+
hits = breeth.retrieve("HR-tech experience")
|
|
10
|
+
|
|
11
|
+
# Async
|
|
12
|
+
from breeth import AsyncBreethClient
|
|
13
|
+
|
|
14
|
+
async with AsyncBreethClient(api_key="ck_live_...") as breeth:
|
|
15
|
+
await breeth.write("...")
|
|
16
|
+
"""
|
|
17
|
+
from __future__ import annotations
|
|
18
|
+
|
|
19
|
+
from typing import Any, Optional
|
|
20
|
+
|
|
21
|
+
import httpx
|
|
22
|
+
|
|
23
|
+
from ._base import (
|
|
24
|
+
DEFAULT_TIMEOUT,
|
|
25
|
+
build_headers,
|
|
26
|
+
build_url,
|
|
27
|
+
merge_query,
|
|
28
|
+
raise_for_status,
|
|
29
|
+
resolve_api_key,
|
|
30
|
+
resolve_base_url,
|
|
31
|
+
)
|
|
32
|
+
from .types import (
|
|
33
|
+
EntityMode,
|
|
34
|
+
EntityResponse,
|
|
35
|
+
GraphEdgeListResponse,
|
|
36
|
+
GraphEntityListResponse,
|
|
37
|
+
GraphEpisodeListResponse,
|
|
38
|
+
GroupsListResponse,
|
|
39
|
+
NodeDetailsResponse,
|
|
40
|
+
RetrieveResponse,
|
|
41
|
+
WriteResponse,
|
|
42
|
+
)
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
# ---------------------------------------------------------------------------
|
|
46
|
+
# Sync client
|
|
47
|
+
# ---------------------------------------------------------------------------
|
|
48
|
+
|
|
49
|
+
class _GraphNamespace:
|
|
50
|
+
"""Nested namespace exposed as ``client.graph``. Reuses the parent
|
|
51
|
+
client's httpx.Client so we don't open extra connections."""
|
|
52
|
+
|
|
53
|
+
def __init__(self, client: "BreethClient") -> None:
|
|
54
|
+
self._client = client
|
|
55
|
+
|
|
56
|
+
def node_details(self, identifier: str) -> NodeDetailsResponse:
|
|
57
|
+
data = self._client._get(f"/graph/nodes/{identifier}/details")
|
|
58
|
+
return NodeDetailsResponse.model_validate(data)
|
|
59
|
+
|
|
60
|
+
def list_entities(
|
|
61
|
+
self,
|
|
62
|
+
*,
|
|
63
|
+
query: Optional[str] = None,
|
|
64
|
+
limit: Optional[int] = None,
|
|
65
|
+
offset: Optional[int] = None,
|
|
66
|
+
) -> GraphEntityListResponse:
|
|
67
|
+
params = merge_query({"query": query, "limit": limit, "offset": offset})
|
|
68
|
+
data = self._client._get("/graph/entities", params=params)
|
|
69
|
+
return GraphEntityListResponse.model_validate(data)
|
|
70
|
+
|
|
71
|
+
def list_edges(
|
|
72
|
+
self,
|
|
73
|
+
*,
|
|
74
|
+
query: Optional[str] = None,
|
|
75
|
+
limit: Optional[int] = None,
|
|
76
|
+
offset: Optional[int] = None,
|
|
77
|
+
) -> GraphEdgeListResponse:
|
|
78
|
+
params = merge_query({"query": query, "limit": limit, "offset": offset})
|
|
79
|
+
data = self._client._get("/graph/edges", params=params)
|
|
80
|
+
return GraphEdgeListResponse.model_validate(data)
|
|
81
|
+
|
|
82
|
+
def list_episodes(
|
|
83
|
+
self,
|
|
84
|
+
*,
|
|
85
|
+
query: Optional[str] = None,
|
|
86
|
+
limit: Optional[int] = None,
|
|
87
|
+
offset: Optional[int] = None,
|
|
88
|
+
) -> GraphEpisodeListResponse:
|
|
89
|
+
params = merge_query({"query": query, "limit": limit, "offset": offset})
|
|
90
|
+
data = self._client._get("/graph/episodes", params=params)
|
|
91
|
+
return GraphEpisodeListResponse.model_validate(data)
|
|
92
|
+
|
|
93
|
+
|
|
94
|
+
class BreethClient:
|
|
95
|
+
"""Synchronous client for the Breeth API.
|
|
96
|
+
|
|
97
|
+
Args:
|
|
98
|
+
api_key: ``ck_live_...`` token. Falls back to ``BREETH_API_KEY``
|
|
99
|
+
then ``COGRAM_API_KEY``.
|
|
100
|
+
base_url: Override the API host. Falls back to ``COGRAM_API_URL``
|
|
101
|
+
then the SDK default (``https://api.thebreeth.com``).
|
|
102
|
+
end_user_id: Optional ``X-End-User-Id`` header forwarded on
|
|
103
|
+
every request. Useful when one API key fronts many end users.
|
|
104
|
+
timeout: httpx timeout in seconds.
|
|
105
|
+
http_client: Inject a pre-configured ``httpx.Client`` (for
|
|
106
|
+
advanced transport, proxies, custom TLS, etc.). When supplied
|
|
107
|
+
the SDK does not close it on ``__exit__``.
|
|
108
|
+
"""
|
|
109
|
+
|
|
110
|
+
def __init__(
|
|
111
|
+
self,
|
|
112
|
+
api_key: Optional[str] = None,
|
|
113
|
+
*,
|
|
114
|
+
base_url: Optional[str] = None,
|
|
115
|
+
end_user_id: Optional[str] = None,
|
|
116
|
+
timeout: float = DEFAULT_TIMEOUT,
|
|
117
|
+
http_client: Optional[httpx.Client] = None,
|
|
118
|
+
) -> None:
|
|
119
|
+
self._api_key = resolve_api_key(api_key)
|
|
120
|
+
self._base_url = resolve_base_url(base_url)
|
|
121
|
+
self._end_user_id = end_user_id
|
|
122
|
+
self._headers = build_headers(self._api_key, end_user_id)
|
|
123
|
+
self._owns_http = http_client is None
|
|
124
|
+
self._http = http_client or httpx.Client(timeout=timeout)
|
|
125
|
+
self.graph = _GraphNamespace(self)
|
|
126
|
+
|
|
127
|
+
# ---- context manager ------------------------------------------------
|
|
128
|
+
|
|
129
|
+
def __enter__(self) -> "BreethClient":
|
|
130
|
+
return self
|
|
131
|
+
|
|
132
|
+
def __exit__(self, exc_type, exc, tb) -> None:
|
|
133
|
+
self.close()
|
|
134
|
+
|
|
135
|
+
def close(self) -> None:
|
|
136
|
+
if self._owns_http:
|
|
137
|
+
self._http.close()
|
|
138
|
+
|
|
139
|
+
# ---- low-level helpers ---------------------------------------------
|
|
140
|
+
|
|
141
|
+
def _get(self, path: str, *, params: Optional[dict[str, Any]] = None) -> Any:
|
|
142
|
+
response = self._http.get(
|
|
143
|
+
build_url(self._base_url, path),
|
|
144
|
+
headers=self._headers,
|
|
145
|
+
params=params,
|
|
146
|
+
)
|
|
147
|
+
raise_for_status(response)
|
|
148
|
+
return response.json()
|
|
149
|
+
|
|
150
|
+
def _post(self, path: str, *, json: dict[str, Any]) -> Any:
|
|
151
|
+
response = self._http.post(
|
|
152
|
+
build_url(self._base_url, path),
|
|
153
|
+
headers=self._headers,
|
|
154
|
+
json=json,
|
|
155
|
+
)
|
|
156
|
+
raise_for_status(response)
|
|
157
|
+
return response.json()
|
|
158
|
+
|
|
159
|
+
# ---- public methods ------------------------------------------------
|
|
160
|
+
|
|
161
|
+
def write(
|
|
162
|
+
self,
|
|
163
|
+
content: str,
|
|
164
|
+
*,
|
|
165
|
+
group_id: str = "default",
|
|
166
|
+
source_description: str = "api",
|
|
167
|
+
extract_intent: bool = False,
|
|
168
|
+
) -> WriteResponse:
|
|
169
|
+
"""Ingest a prose episode. The graph pipeline runs in the
|
|
170
|
+
background server-side; this returns once the episode is
|
|
171
|
+
persisted."""
|
|
172
|
+
payload = {
|
|
173
|
+
"content": content,
|
|
174
|
+
"group_id": group_id,
|
|
175
|
+
"source_description": source_description,
|
|
176
|
+
"extract_intent": extract_intent,
|
|
177
|
+
}
|
|
178
|
+
data = self._post("/episodes", json=payload)
|
|
179
|
+
return WriteResponse.model_validate(data)
|
|
180
|
+
|
|
181
|
+
def retrieve(
|
|
182
|
+
self,
|
|
183
|
+
query: str,
|
|
184
|
+
*,
|
|
185
|
+
group_id: str = "default",
|
|
186
|
+
limit: int = 10,
|
|
187
|
+
) -> RetrieveResponse:
|
|
188
|
+
"""Hybrid semantic + graph search. Returns edges plus, on
|
|
189
|
+
first-person queries, the director profile block."""
|
|
190
|
+
payload = {"query": query, "group_id": group_id, "limit": limit}
|
|
191
|
+
data = self._post("/search", json=payload)
|
|
192
|
+
return RetrieveResponse.model_validate(data)
|
|
193
|
+
|
|
194
|
+
def entity(
|
|
195
|
+
self,
|
|
196
|
+
name: str,
|
|
197
|
+
*,
|
|
198
|
+
mode: Optional[EntityMode] = None,
|
|
199
|
+
limit: Optional[int] = None,
|
|
200
|
+
) -> EntityResponse:
|
|
201
|
+
"""Look up an entity by name substring. ``mode`` selects which
|
|
202
|
+
view to return (``narrative`` is the server default)."""
|
|
203
|
+
params = merge_query({"mode": mode, "limit": limit})
|
|
204
|
+
data = self._get(f"/entities/{name}", params=params)
|
|
205
|
+
return EntityResponse.model_validate(data)
|
|
206
|
+
|
|
207
|
+
def groups(
|
|
208
|
+
self,
|
|
209
|
+
*,
|
|
210
|
+
query: Optional[str] = None,
|
|
211
|
+
limit: Optional[int] = None,
|
|
212
|
+
offset: Optional[int] = None,
|
|
213
|
+
) -> GroupsListResponse:
|
|
214
|
+
"""List the memory groups visible to the current team / member."""
|
|
215
|
+
params = merge_query({"query": query, "limit": limit, "offset": offset})
|
|
216
|
+
data = self._get("/groups", params=params)
|
|
217
|
+
return GroupsListResponse.model_validate(data)
|
|
218
|
+
|
|
219
|
+
|
|
220
|
+
# ---------------------------------------------------------------------------
|
|
221
|
+
# Async client
|
|
222
|
+
# ---------------------------------------------------------------------------
|
|
223
|
+
|
|
224
|
+
class _AsyncGraphNamespace:
|
|
225
|
+
def __init__(self, client: "AsyncBreethClient") -> None:
|
|
226
|
+
self._client = client
|
|
227
|
+
|
|
228
|
+
async def node_details(self, identifier: str) -> NodeDetailsResponse:
|
|
229
|
+
data = await self._client._get(f"/graph/nodes/{identifier}/details")
|
|
230
|
+
return NodeDetailsResponse.model_validate(data)
|
|
231
|
+
|
|
232
|
+
async def list_entities(
|
|
233
|
+
self,
|
|
234
|
+
*,
|
|
235
|
+
query: Optional[str] = None,
|
|
236
|
+
limit: Optional[int] = None,
|
|
237
|
+
offset: Optional[int] = None,
|
|
238
|
+
) -> GraphEntityListResponse:
|
|
239
|
+
params = merge_query({"query": query, "limit": limit, "offset": offset})
|
|
240
|
+
data = await self._client._get("/graph/entities", params=params)
|
|
241
|
+
return GraphEntityListResponse.model_validate(data)
|
|
242
|
+
|
|
243
|
+
async def list_edges(
|
|
244
|
+
self,
|
|
245
|
+
*,
|
|
246
|
+
query: Optional[str] = None,
|
|
247
|
+
limit: Optional[int] = None,
|
|
248
|
+
offset: Optional[int] = None,
|
|
249
|
+
) -> GraphEdgeListResponse:
|
|
250
|
+
params = merge_query({"query": query, "limit": limit, "offset": offset})
|
|
251
|
+
data = await self._client._get("/graph/edges", params=params)
|
|
252
|
+
return GraphEdgeListResponse.model_validate(data)
|
|
253
|
+
|
|
254
|
+
async def list_episodes(
|
|
255
|
+
self,
|
|
256
|
+
*,
|
|
257
|
+
query: Optional[str] = None,
|
|
258
|
+
limit: Optional[int] = None,
|
|
259
|
+
offset: Optional[int] = None,
|
|
260
|
+
) -> GraphEpisodeListResponse:
|
|
261
|
+
params = merge_query({"query": query, "limit": limit, "offset": offset})
|
|
262
|
+
data = await self._client._get("/graph/episodes", params=params)
|
|
263
|
+
return GraphEpisodeListResponse.model_validate(data)
|
|
264
|
+
|
|
265
|
+
|
|
266
|
+
class AsyncBreethClient:
|
|
267
|
+
"""Asynchronous client for the Breeth API. Same surface as
|
|
268
|
+
``BreethClient`` but every method is a coroutine."""
|
|
269
|
+
|
|
270
|
+
def __init__(
|
|
271
|
+
self,
|
|
272
|
+
api_key: Optional[str] = None,
|
|
273
|
+
*,
|
|
274
|
+
base_url: Optional[str] = None,
|
|
275
|
+
end_user_id: Optional[str] = None,
|
|
276
|
+
timeout: float = DEFAULT_TIMEOUT,
|
|
277
|
+
http_client: Optional[httpx.AsyncClient] = None,
|
|
278
|
+
) -> None:
|
|
279
|
+
self._api_key = resolve_api_key(api_key)
|
|
280
|
+
self._base_url = resolve_base_url(base_url)
|
|
281
|
+
self._end_user_id = end_user_id
|
|
282
|
+
self._headers = build_headers(self._api_key, end_user_id)
|
|
283
|
+
self._owns_http = http_client is None
|
|
284
|
+
self._http = http_client or httpx.AsyncClient(timeout=timeout)
|
|
285
|
+
self.graph = _AsyncGraphNamespace(self)
|
|
286
|
+
|
|
287
|
+
# ---- context manager ------------------------------------------------
|
|
288
|
+
|
|
289
|
+
async def __aenter__(self) -> "AsyncBreethClient":
|
|
290
|
+
return self
|
|
291
|
+
|
|
292
|
+
async def __aexit__(self, exc_type, exc, tb) -> None:
|
|
293
|
+
await self.aclose()
|
|
294
|
+
|
|
295
|
+
async def aclose(self) -> None:
|
|
296
|
+
if self._owns_http:
|
|
297
|
+
await self._http.aclose()
|
|
298
|
+
|
|
299
|
+
# ---- low-level helpers ---------------------------------------------
|
|
300
|
+
|
|
301
|
+
async def _get(self, path: str, *, params: Optional[dict[str, Any]] = None) -> Any:
|
|
302
|
+
response = await self._http.get(
|
|
303
|
+
build_url(self._base_url, path),
|
|
304
|
+
headers=self._headers,
|
|
305
|
+
params=params,
|
|
306
|
+
)
|
|
307
|
+
raise_for_status(response)
|
|
308
|
+
return response.json()
|
|
309
|
+
|
|
310
|
+
async def _post(self, path: str, *, json: dict[str, Any]) -> Any:
|
|
311
|
+
response = await self._http.post(
|
|
312
|
+
build_url(self._base_url, path),
|
|
313
|
+
headers=self._headers,
|
|
314
|
+
json=json,
|
|
315
|
+
)
|
|
316
|
+
raise_for_status(response)
|
|
317
|
+
return response.json()
|
|
318
|
+
|
|
319
|
+
# ---- public methods ------------------------------------------------
|
|
320
|
+
|
|
321
|
+
async def write(
|
|
322
|
+
self,
|
|
323
|
+
content: str,
|
|
324
|
+
*,
|
|
325
|
+
group_id: str = "default",
|
|
326
|
+
source_description: str = "api",
|
|
327
|
+
extract_intent: bool = False,
|
|
328
|
+
) -> WriteResponse:
|
|
329
|
+
payload = {
|
|
330
|
+
"content": content,
|
|
331
|
+
"group_id": group_id,
|
|
332
|
+
"source_description": source_description,
|
|
333
|
+
"extract_intent": extract_intent,
|
|
334
|
+
}
|
|
335
|
+
data = await self._post("/episodes", json=payload)
|
|
336
|
+
return WriteResponse.model_validate(data)
|
|
337
|
+
|
|
338
|
+
async def retrieve(
|
|
339
|
+
self,
|
|
340
|
+
query: str,
|
|
341
|
+
*,
|
|
342
|
+
group_id: str = "default",
|
|
343
|
+
limit: int = 10,
|
|
344
|
+
) -> RetrieveResponse:
|
|
345
|
+
payload = {"query": query, "group_id": group_id, "limit": limit}
|
|
346
|
+
data = await self._post("/search", json=payload)
|
|
347
|
+
return RetrieveResponse.model_validate(data)
|
|
348
|
+
|
|
349
|
+
async def entity(
|
|
350
|
+
self,
|
|
351
|
+
name: str,
|
|
352
|
+
*,
|
|
353
|
+
mode: Optional[EntityMode] = None,
|
|
354
|
+
limit: Optional[int] = None,
|
|
355
|
+
) -> EntityResponse:
|
|
356
|
+
params = merge_query({"mode": mode, "limit": limit})
|
|
357
|
+
data = await self._get(f"/entities/{name}", params=params)
|
|
358
|
+
return EntityResponse.model_validate(data)
|
|
359
|
+
|
|
360
|
+
async def groups(
|
|
361
|
+
self,
|
|
362
|
+
*,
|
|
363
|
+
query: Optional[str] = None,
|
|
364
|
+
limit: Optional[int] = None,
|
|
365
|
+
offset: Optional[int] = None,
|
|
366
|
+
) -> GroupsListResponse:
|
|
367
|
+
params = merge_query({"query": query, "limit": limit, "offset": offset})
|
|
368
|
+
data = await self._get("/groups", params=params)
|
|
369
|
+
return GroupsListResponse.model_validate(data)
|
breeth/errors.py
ADDED
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
"""Exception types raised by the Breeth SDK."""
|
|
2
|
+
from __future__ import annotations
|
|
3
|
+
|
|
4
|
+
from typing import Any, Optional
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
class BreethError(Exception):
|
|
8
|
+
"""Base error for every non-2xx response from the Breeth API.
|
|
9
|
+
|
|
10
|
+
Attributes:
|
|
11
|
+
status: HTTP status code (e.g. 401, 429, 500).
|
|
12
|
+
slug: Stable machine-readable error code from the API
|
|
13
|
+
(e.g. "quota_exceeded", "invalid_token"). May be None if
|
|
14
|
+
the server returned a non-standard payload.
|
|
15
|
+
message: Human-readable message from the API, falls back to a
|
|
16
|
+
generic string.
|
|
17
|
+
body: The full parsed response body for advanced inspection.
|
|
18
|
+
"""
|
|
19
|
+
|
|
20
|
+
def __init__(
|
|
21
|
+
self,
|
|
22
|
+
message: str,
|
|
23
|
+
*,
|
|
24
|
+
status: int,
|
|
25
|
+
slug: Optional[str] = None,
|
|
26
|
+
body: Any = None,
|
|
27
|
+
) -> None:
|
|
28
|
+
super().__init__(message)
|
|
29
|
+
self.status = status
|
|
30
|
+
self.slug = slug
|
|
31
|
+
self.message = message
|
|
32
|
+
self.body = body
|
|
33
|
+
|
|
34
|
+
def __repr__(self) -> str:
|
|
35
|
+
return (
|
|
36
|
+
f"BreethError(status={self.status}, slug={self.slug!r}, "
|
|
37
|
+
f"message={self.message!r})"
|
|
38
|
+
)
|
breeth/types.py
ADDED
|
@@ -0,0 +1,237 @@
|
|
|
1
|
+
"""Pydantic v2 request and response models for the Breeth API.
|
|
2
|
+
|
|
3
|
+
These mirror the server schemas in cogram-core. Fields with a leading
|
|
4
|
+
underscore on the wire (e.g. ``_cache``, ``_tier``, ``_source``,
|
|
5
|
+
``_note``) use aliases so Python users can access them as normal
|
|
6
|
+
attributes (``response.cache`` rather than ``response["_cache"]``).
|
|
7
|
+
"""
|
|
8
|
+
from __future__ import annotations
|
|
9
|
+
|
|
10
|
+
from typing import Any, Literal, Optional
|
|
11
|
+
|
|
12
|
+
from pydantic import BaseModel, ConfigDict, Field
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
# ---------------------------------------------------------------------------
|
|
16
|
+
# Write (POST /v1/episodes)
|
|
17
|
+
# ---------------------------------------------------------------------------
|
|
18
|
+
|
|
19
|
+
class ExtractedCounts(BaseModel):
|
|
20
|
+
entities: int
|
|
21
|
+
edges: int
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
class CogramTaskEnvelope(BaseModel):
|
|
25
|
+
mode: str
|
|
26
|
+
status: str
|
|
27
|
+
task_id: Optional[str] = None
|
|
28
|
+
note: Optional[str] = None
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
class IntentSuggestion(BaseModel):
|
|
32
|
+
should_extract: bool = True
|
|
33
|
+
confidence: float
|
|
34
|
+
reason: str
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
class WriteResponse(BaseModel):
|
|
38
|
+
ok: bool = True
|
|
39
|
+
episode_name: str
|
|
40
|
+
extracted: ExtractedCounts
|
|
41
|
+
group_id: str
|
|
42
|
+
warning: Optional[str] = None
|
|
43
|
+
cogram: Optional[CogramTaskEnvelope] = None
|
|
44
|
+
intent_suggestion: Optional[IntentSuggestion] = None
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
# ---------------------------------------------------------------------------
|
|
48
|
+
# Retrieve (POST /v1/search)
|
|
49
|
+
# ---------------------------------------------------------------------------
|
|
50
|
+
|
|
51
|
+
class EdgeHit(BaseModel):
|
|
52
|
+
model_config = ConfigDict(populate_by_name=True)
|
|
53
|
+
|
|
54
|
+
edge_uuid: Optional[str] = None
|
|
55
|
+
source_node: str
|
|
56
|
+
target_node: str
|
|
57
|
+
fact: str
|
|
58
|
+
name: Optional[str] = None
|
|
59
|
+
intent_meta: Any = None
|
|
60
|
+
tier: str = Field(alias="_tier")
|
|
61
|
+
|
|
62
|
+
|
|
63
|
+
class CacheStats(BaseModel):
|
|
64
|
+
tier: str
|
|
65
|
+
hot_hits: int
|
|
66
|
+
cold_hits: int
|
|
67
|
+
group_id: str
|
|
68
|
+
|
|
69
|
+
|
|
70
|
+
class PatternEvidence(BaseModel):
|
|
71
|
+
name: str
|
|
72
|
+
stored_confidence: float
|
|
73
|
+
effective_confidence: float
|
|
74
|
+
examples: list[dict] = Field(default_factory=list)
|
|
75
|
+
|
|
76
|
+
|
|
77
|
+
class DirectorProfileBlock(BaseModel):
|
|
78
|
+
model_config = ConfigDict(populate_by_name=True)
|
|
79
|
+
|
|
80
|
+
summary: str
|
|
81
|
+
visions: list[str] = Field(default_factory=list)
|
|
82
|
+
top_patterns: list[PatternEvidence] = Field(default_factory=list)
|
|
83
|
+
source: str = Field(alias="_source")
|
|
84
|
+
note: str = Field(alias="_note")
|
|
85
|
+
|
|
86
|
+
|
|
87
|
+
class RetrieveResponse(BaseModel):
|
|
88
|
+
model_config = ConfigDict(populate_by_name=True)
|
|
89
|
+
|
|
90
|
+
director_profile: Optional[DirectorProfileBlock] = None
|
|
91
|
+
edges: list[EdgeHit] = Field(default_factory=list)
|
|
92
|
+
cache: CacheStats = Field(alias="_cache")
|
|
93
|
+
note: Optional[str] = None
|
|
94
|
+
|
|
95
|
+
|
|
96
|
+
# ---------------------------------------------------------------------------
|
|
97
|
+
# Entity (GET /v1/entities/{name})
|
|
98
|
+
# ---------------------------------------------------------------------------
|
|
99
|
+
|
|
100
|
+
EntityMode = Literal["narrative", "edges", "episodes", "all"]
|
|
101
|
+
|
|
102
|
+
|
|
103
|
+
class EntityNarrativeRow(BaseModel):
|
|
104
|
+
uuid: str
|
|
105
|
+
name: str
|
|
106
|
+
degree: int
|
|
107
|
+
summary: str
|
|
108
|
+
narrative: Any
|
|
109
|
+
confidence_stored: float
|
|
110
|
+
confidence_decayed: float
|
|
111
|
+
confidence_label: str
|
|
112
|
+
|
|
113
|
+
|
|
114
|
+
class EntityEdgeRow(BaseModel):
|
|
115
|
+
a: str
|
|
116
|
+
b: str
|
|
117
|
+
fact: str
|
|
118
|
+
intent_meta: Any = None
|
|
119
|
+
|
|
120
|
+
|
|
121
|
+
class EntityEpisodeRow(BaseModel):
|
|
122
|
+
uuid: str
|
|
123
|
+
name: str
|
|
124
|
+
content_excerpt: str
|
|
125
|
+
timestamp: Any = None
|
|
126
|
+
|
|
127
|
+
|
|
128
|
+
class EntityResponse(BaseModel):
|
|
129
|
+
entity_name: str
|
|
130
|
+
mode: str
|
|
131
|
+
narrative: Optional[list[EntityNarrativeRow]] = None
|
|
132
|
+
edges: Optional[list[EntityEdgeRow]] = None
|
|
133
|
+
episodes: Optional[list[EntityEpisodeRow]] = None
|
|
134
|
+
note: Optional[str] = None
|
|
135
|
+
|
|
136
|
+
|
|
137
|
+
# ---------------------------------------------------------------------------
|
|
138
|
+
# Graph (GET /v1/graph/*)
|
|
139
|
+
# ---------------------------------------------------------------------------
|
|
140
|
+
|
|
141
|
+
class GraphEntityRow(BaseModel):
|
|
142
|
+
uuid: str
|
|
143
|
+
name: str
|
|
144
|
+
labels: list[str] = Field(default_factory=list)
|
|
145
|
+
summary: Optional[str] = None
|
|
146
|
+
edge_count: int = 0
|
|
147
|
+
episode_count: int = 0
|
|
148
|
+
space: str = "individual"
|
|
149
|
+
member_id: Optional[str] = None
|
|
150
|
+
created_at: Optional[str] = None
|
|
151
|
+
knot_narrative: Optional[str] = None
|
|
152
|
+
knot_score: Optional[float] = None
|
|
153
|
+
knot_synthesized_at: Optional[float] = None
|
|
154
|
+
knot_model: Optional[str] = None
|
|
155
|
+
|
|
156
|
+
|
|
157
|
+
class GraphEntityListResponse(BaseModel):
|
|
158
|
+
entities: list[GraphEntityRow]
|
|
159
|
+
count: int
|
|
160
|
+
|
|
161
|
+
|
|
162
|
+
class GraphEdgeRow(BaseModel):
|
|
163
|
+
uuid: str
|
|
164
|
+
fact: str
|
|
165
|
+
source_name: str
|
|
166
|
+
target_name: str
|
|
167
|
+
cognitive_pattern: Optional[str] = None
|
|
168
|
+
intent_meta: Optional[dict] = None
|
|
169
|
+
retracted_at: Optional[str] = None
|
|
170
|
+
space: str = "individual"
|
|
171
|
+
member_id: Optional[str] = None
|
|
172
|
+
created_at: Optional[str] = None
|
|
173
|
+
|
|
174
|
+
|
|
175
|
+
class GraphEdgeListResponse(BaseModel):
|
|
176
|
+
edges: list[GraphEdgeRow]
|
|
177
|
+
count: int
|
|
178
|
+
|
|
179
|
+
|
|
180
|
+
class GraphEpisodeRow(BaseModel):
|
|
181
|
+
uuid: str
|
|
182
|
+
name: str
|
|
183
|
+
source_description: Optional[str] = None
|
|
184
|
+
content_excerpt: str = ""
|
|
185
|
+
group_id: str = ""
|
|
186
|
+
valid_at: Optional[str] = None
|
|
187
|
+
space: str = "individual"
|
|
188
|
+
member_id: Optional[str] = None
|
|
189
|
+
|
|
190
|
+
|
|
191
|
+
class GraphEpisodeListResponse(BaseModel):
|
|
192
|
+
episodes: list[GraphEpisodeRow]
|
|
193
|
+
count: int
|
|
194
|
+
|
|
195
|
+
|
|
196
|
+
class NeighborRow(BaseModel):
|
|
197
|
+
peer: str
|
|
198
|
+
direction: str
|
|
199
|
+
fact: Optional[str] = None
|
|
200
|
+
cognitive_pattern: Optional[str] = None
|
|
201
|
+
intent_meta: Optional[dict] = None
|
|
202
|
+
edge_uuid: Optional[str] = None
|
|
203
|
+
|
|
204
|
+
|
|
205
|
+
class EpisodeMentionRow(BaseModel):
|
|
206
|
+
uuid: str
|
|
207
|
+
name: str
|
|
208
|
+
valid_at: Optional[str] = None
|
|
209
|
+
|
|
210
|
+
|
|
211
|
+
class NodeDetailsResponse(BaseModel):
|
|
212
|
+
entity: GraphEntityRow
|
|
213
|
+
neighbors: list[NeighborRow]
|
|
214
|
+
episodes: list[EpisodeMentionRow]
|
|
215
|
+
|
|
216
|
+
|
|
217
|
+
# ---------------------------------------------------------------------------
|
|
218
|
+
# Groups (GET /v1/groups)
|
|
219
|
+
# ---------------------------------------------------------------------------
|
|
220
|
+
|
|
221
|
+
class GroupRow(BaseModel):
|
|
222
|
+
group_id: str
|
|
223
|
+
episodes: int
|
|
224
|
+
entities: int
|
|
225
|
+
patterns: int
|
|
226
|
+
knots: int
|
|
227
|
+
has_director_profile: bool
|
|
228
|
+
|
|
229
|
+
|
|
230
|
+
class GroupsListResponse(BaseModel):
|
|
231
|
+
groups: list[GroupRow]
|
|
232
|
+
total: int
|
|
233
|
+
returned: int
|
|
234
|
+
query: Optional[str] = None
|
|
235
|
+
next_offset: Optional[int] = None
|
|
236
|
+
hint: Optional[str] = None
|
|
237
|
+
note: Optional[str] = None
|
|
@@ -0,0 +1,130 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: breeth
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: Official Python SDK for Breeth, a memory layer for agents.
|
|
5
|
+
Project-URL: Homepage, https://thebreeth.com
|
|
6
|
+
Project-URL: Documentation, https://docs.thebreeth.com
|
|
7
|
+
Project-URL: Source, https://github.com/Gramies/cogram-sdk-python
|
|
8
|
+
Project-URL: Issues, https://github.com/Gramies/cogram-sdk-python/issues
|
|
9
|
+
Author-email: Breeth <team@thebreeth.com>
|
|
10
|
+
License: MIT
|
|
11
|
+
License-File: LICENSE
|
|
12
|
+
Keywords: agents,breeth,graph,knowledge,llm,memory
|
|
13
|
+
Classifier: Development Status :: 4 - Beta
|
|
14
|
+
Classifier: Intended Audience :: Developers
|
|
15
|
+
Classifier: License :: OSI Approved :: MIT License
|
|
16
|
+
Classifier: Operating System :: OS Independent
|
|
17
|
+
Classifier: Programming Language :: Python :: 3
|
|
18
|
+
Classifier: Programming Language :: Python :: 3.10
|
|
19
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
20
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
21
|
+
Classifier: Programming Language :: Python :: 3.13
|
|
22
|
+
Classifier: Topic :: Software Development :: Libraries :: Python Modules
|
|
23
|
+
Requires-Python: >=3.10
|
|
24
|
+
Requires-Dist: httpx>=0.27
|
|
25
|
+
Requires-Dist: pydantic>=2
|
|
26
|
+
Provides-Extra: dev
|
|
27
|
+
Requires-Dist: pytest-asyncio>=0.23; extra == 'dev'
|
|
28
|
+
Requires-Dist: pytest>=8; extra == 'dev'
|
|
29
|
+
Requires-Dist: respx>=0.21; extra == 'dev'
|
|
30
|
+
Description-Content-Type: text/markdown
|
|
31
|
+
|
|
32
|
+
# breeth
|
|
33
|
+
|
|
34
|
+
Official Python SDK for [Breeth](https://thebreeth.com), the memory layer for agents.
|
|
35
|
+
|
|
36
|
+
`breeth` is a thin, type-safe wrapper around the Breeth REST API. It ships with both a synchronous and an asynchronous client, runs on Python 3.10+, and depends only on `httpx` and `pydantic`.
|
|
37
|
+
|
|
38
|
+
## Install
|
|
39
|
+
|
|
40
|
+
```bash
|
|
41
|
+
pip install breeth
|
|
42
|
+
```
|
|
43
|
+
|
|
44
|
+
## Quickstart (sync)
|
|
45
|
+
|
|
46
|
+
```python
|
|
47
|
+
from breeth import BreethClient
|
|
48
|
+
|
|
49
|
+
with BreethClient(api_key="ck_live_...") as breeth:
|
|
50
|
+
# Write a memory
|
|
51
|
+
write_resp = breeth.write(
|
|
52
|
+
"Candidate Jane Doe has 4 years HR-tech experience at Workday.",
|
|
53
|
+
group_id="recruiting",
|
|
54
|
+
)
|
|
55
|
+
print(write_resp.episode_name, write_resp.extracted.entities)
|
|
56
|
+
|
|
57
|
+
# Retrieve
|
|
58
|
+
results = breeth.retrieve("HR-tech background", group_id="recruiting", limit=5)
|
|
59
|
+
for edge in results.edges:
|
|
60
|
+
print(edge.fact)
|
|
61
|
+
|
|
62
|
+
# Inspect an entity
|
|
63
|
+
entity = breeth.entity("Workday", mode="narrative")
|
|
64
|
+
|
|
65
|
+
# Browse the graph
|
|
66
|
+
nodes = breeth.graph.list_entities(query="Jane", limit=25)
|
|
67
|
+
edges = breeth.graph.list_edges(limit=50)
|
|
68
|
+
eps = breeth.graph.list_episodes()
|
|
69
|
+
details = breeth.graph.node_details(nodes.entities[0].uuid)
|
|
70
|
+
|
|
71
|
+
# List groups visible to the team
|
|
72
|
+
groups = breeth.groups()
|
|
73
|
+
```
|
|
74
|
+
|
|
75
|
+
## Quickstart (async)
|
|
76
|
+
|
|
77
|
+
```python
|
|
78
|
+
import asyncio
|
|
79
|
+
from breeth import AsyncBreethClient
|
|
80
|
+
|
|
81
|
+
async def main():
|
|
82
|
+
async with AsyncBreethClient(api_key="ck_live_...") as breeth:
|
|
83
|
+
await breeth.write("Recruiter prefers iterative sourcing.")
|
|
84
|
+
results = await breeth.retrieve("recruiter preferences")
|
|
85
|
+
print(results.edges)
|
|
86
|
+
|
|
87
|
+
asyncio.run(main())
|
|
88
|
+
```
|
|
89
|
+
|
|
90
|
+
## Configuration
|
|
91
|
+
|
|
92
|
+
| Setting | Source |
|
|
93
|
+
| --- | --- |
|
|
94
|
+
| API key | `api_key=` argument, then `BREETH_API_KEY` env var, then `COGRAM_API_KEY` |
|
|
95
|
+
| Base URL | `base_url=` argument, then `COGRAM_API_URL` env var, default `https://api.thebreeth.com` |
|
|
96
|
+
| End user passthrough | `end_user_id=` argument, sent as `X-End-User-Id` |
|
|
97
|
+
|
|
98
|
+
## Errors
|
|
99
|
+
|
|
100
|
+
Every non-2xx response is raised as a `BreethError`:
|
|
101
|
+
|
|
102
|
+
```python
|
|
103
|
+
from breeth import BreethClient, BreethError
|
|
104
|
+
|
|
105
|
+
try:
|
|
106
|
+
with BreethClient(api_key="ck_live_...") as breeth:
|
|
107
|
+
breeth.write("...")
|
|
108
|
+
except BreethError as err:
|
|
109
|
+
print(err.status) # 429
|
|
110
|
+
print(err.slug) # "quota_exceeded"
|
|
111
|
+
print(err.message) # "Monthly write quota exceeded."
|
|
112
|
+
print(err.body) # raw JSON payload
|
|
113
|
+
```
|
|
114
|
+
|
|
115
|
+
The `slug` field is stable across API versions, so it is safe to branch on it.
|
|
116
|
+
|
|
117
|
+
## Roadmap
|
|
118
|
+
|
|
119
|
+
- v0.1.0: sync + async clients for write, retrieve, entity, graph, groups.
|
|
120
|
+
- v0.2.0: NDJSON streaming endpoints (`/v1/graph/nodes`, `/v1/graph/links`).
|
|
121
|
+
|
|
122
|
+
## Links
|
|
123
|
+
|
|
124
|
+
- Product: https://thebreeth.com
|
|
125
|
+
- Docs: https://docs.thebreeth.com
|
|
126
|
+
- Issues: https://github.com/Gramies/cogram-sdk-python/issues
|
|
127
|
+
|
|
128
|
+
## License
|
|
129
|
+
|
|
130
|
+
MIT. See [LICENSE](LICENSE).
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
breeth/__init__.py,sha256=Nv8BnkurpD9FD5XuCHWDIC0oNd0kBCwjpRG3aUJfAc8,1660
|
|
2
|
+
breeth/_base.py,sha256=ysb6EwCTHSKY5iH6sk3nRteIujOUUSE7keaNrn0AtvI,3396
|
|
3
|
+
breeth/client.py,sha256=a8yGlX3cmqQiW2QiCrXGdCkoguZORQhoPSeuRVrN5Ww,12375
|
|
4
|
+
breeth/errors.py,sha256=kVW9vXnufRCSZ_frm3PTcXjdyhZJYdKY5rIVIuP0m-4,1127
|
|
5
|
+
breeth/types.py,sha256=IdMruQiMjbHvhTMLh_k2Y0OwtpqryeHkbIzBuA1Hmf4,6010
|
|
6
|
+
breeth-0.1.0.dist-info/METADATA,sha256=_ox-dSbAi_b6N0tHsIW-XtOr6kvanKfFlN4_J8bh3-Q,4020
|
|
7
|
+
breeth-0.1.0.dist-info/WHEEL,sha256=QccIxa26bgl1E6uMy58deGWi-0aeIkkangHcxk2kWfw,87
|
|
8
|
+
breeth-0.1.0.dist-info/licenses/LICENSE,sha256=HLhqMVIwZmy5mQwhOgcZSCBUYBTsLcrBjIed2C_2Z-g,1063
|
|
9
|
+
breeth-0.1.0.dist-info/RECORD,,
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Breeth
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|