breeth 0.1.0__tar.gz
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-0.1.0/.gitignore +42 -0
- breeth-0.1.0/LICENSE +21 -0
- breeth-0.1.0/PKG-INFO +130 -0
- breeth-0.1.0/README.md +99 -0
- breeth-0.1.0/pyproject.toml +57 -0
- breeth-0.1.0/src/breeth/__init__.py +73 -0
- breeth-0.1.0/src/breeth/_base.py +103 -0
- breeth-0.1.0/src/breeth/client.py +369 -0
- breeth-0.1.0/src/breeth/errors.py +38 -0
- breeth-0.1.0/src/breeth/types.py +237 -0
- breeth-0.1.0/tests/__init__.py +0 -0
- breeth-0.1.0/tests/test_async_client.py +93 -0
- breeth-0.1.0/tests/test_client.py +314 -0
breeth-0.1.0/.gitignore
ADDED
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
# Byte-compiled / optimized / DLL files
|
|
2
|
+
__pycache__/
|
|
3
|
+
*.py[cod]
|
|
4
|
+
*$py.class
|
|
5
|
+
|
|
6
|
+
# Distribution / packaging
|
|
7
|
+
.Python
|
|
8
|
+
build/
|
|
9
|
+
develop-eggs/
|
|
10
|
+
dist/
|
|
11
|
+
downloads/
|
|
12
|
+
eggs/
|
|
13
|
+
.eggs/
|
|
14
|
+
lib/
|
|
15
|
+
lib64/
|
|
16
|
+
parts/
|
|
17
|
+
sdist/
|
|
18
|
+
var/
|
|
19
|
+
wheels/
|
|
20
|
+
*.egg-info/
|
|
21
|
+
.installed.cfg
|
|
22
|
+
*.egg
|
|
23
|
+
MANIFEST
|
|
24
|
+
|
|
25
|
+
# Environments
|
|
26
|
+
.venv/
|
|
27
|
+
venv/
|
|
28
|
+
env/
|
|
29
|
+
ENV/
|
|
30
|
+
|
|
31
|
+
# Test / coverage
|
|
32
|
+
.pytest_cache/
|
|
33
|
+
.coverage
|
|
34
|
+
htmlcov/
|
|
35
|
+
.tox/
|
|
36
|
+
.cache
|
|
37
|
+
|
|
38
|
+
# Editors
|
|
39
|
+
.vscode/
|
|
40
|
+
.idea/
|
|
41
|
+
*.swp
|
|
42
|
+
.DS_Store
|
breeth-0.1.0/LICENSE
ADDED
|
@@ -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.
|
breeth-0.1.0/PKG-INFO
ADDED
|
@@ -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).
|
breeth-0.1.0/README.md
ADDED
|
@@ -0,0 +1,99 @@
|
|
|
1
|
+
# breeth
|
|
2
|
+
|
|
3
|
+
Official Python SDK for [Breeth](https://thebreeth.com), the memory layer for agents.
|
|
4
|
+
|
|
5
|
+
`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`.
|
|
6
|
+
|
|
7
|
+
## Install
|
|
8
|
+
|
|
9
|
+
```bash
|
|
10
|
+
pip install breeth
|
|
11
|
+
```
|
|
12
|
+
|
|
13
|
+
## Quickstart (sync)
|
|
14
|
+
|
|
15
|
+
```python
|
|
16
|
+
from breeth import BreethClient
|
|
17
|
+
|
|
18
|
+
with BreethClient(api_key="ck_live_...") as breeth:
|
|
19
|
+
# Write a memory
|
|
20
|
+
write_resp = breeth.write(
|
|
21
|
+
"Candidate Jane Doe has 4 years HR-tech experience at Workday.",
|
|
22
|
+
group_id="recruiting",
|
|
23
|
+
)
|
|
24
|
+
print(write_resp.episode_name, write_resp.extracted.entities)
|
|
25
|
+
|
|
26
|
+
# Retrieve
|
|
27
|
+
results = breeth.retrieve("HR-tech background", group_id="recruiting", limit=5)
|
|
28
|
+
for edge in results.edges:
|
|
29
|
+
print(edge.fact)
|
|
30
|
+
|
|
31
|
+
# Inspect an entity
|
|
32
|
+
entity = breeth.entity("Workday", mode="narrative")
|
|
33
|
+
|
|
34
|
+
# Browse the graph
|
|
35
|
+
nodes = breeth.graph.list_entities(query="Jane", limit=25)
|
|
36
|
+
edges = breeth.graph.list_edges(limit=50)
|
|
37
|
+
eps = breeth.graph.list_episodes()
|
|
38
|
+
details = breeth.graph.node_details(nodes.entities[0].uuid)
|
|
39
|
+
|
|
40
|
+
# List groups visible to the team
|
|
41
|
+
groups = breeth.groups()
|
|
42
|
+
```
|
|
43
|
+
|
|
44
|
+
## Quickstart (async)
|
|
45
|
+
|
|
46
|
+
```python
|
|
47
|
+
import asyncio
|
|
48
|
+
from breeth import AsyncBreethClient
|
|
49
|
+
|
|
50
|
+
async def main():
|
|
51
|
+
async with AsyncBreethClient(api_key="ck_live_...") as breeth:
|
|
52
|
+
await breeth.write("Recruiter prefers iterative sourcing.")
|
|
53
|
+
results = await breeth.retrieve("recruiter preferences")
|
|
54
|
+
print(results.edges)
|
|
55
|
+
|
|
56
|
+
asyncio.run(main())
|
|
57
|
+
```
|
|
58
|
+
|
|
59
|
+
## Configuration
|
|
60
|
+
|
|
61
|
+
| Setting | Source |
|
|
62
|
+
| --- | --- |
|
|
63
|
+
| API key | `api_key=` argument, then `BREETH_API_KEY` env var, then `COGRAM_API_KEY` |
|
|
64
|
+
| Base URL | `base_url=` argument, then `COGRAM_API_URL` env var, default `https://api.thebreeth.com` |
|
|
65
|
+
| End user passthrough | `end_user_id=` argument, sent as `X-End-User-Id` |
|
|
66
|
+
|
|
67
|
+
## Errors
|
|
68
|
+
|
|
69
|
+
Every non-2xx response is raised as a `BreethError`:
|
|
70
|
+
|
|
71
|
+
```python
|
|
72
|
+
from breeth import BreethClient, BreethError
|
|
73
|
+
|
|
74
|
+
try:
|
|
75
|
+
with BreethClient(api_key="ck_live_...") as breeth:
|
|
76
|
+
breeth.write("...")
|
|
77
|
+
except BreethError as err:
|
|
78
|
+
print(err.status) # 429
|
|
79
|
+
print(err.slug) # "quota_exceeded"
|
|
80
|
+
print(err.message) # "Monthly write quota exceeded."
|
|
81
|
+
print(err.body) # raw JSON payload
|
|
82
|
+
```
|
|
83
|
+
|
|
84
|
+
The `slug` field is stable across API versions, so it is safe to branch on it.
|
|
85
|
+
|
|
86
|
+
## Roadmap
|
|
87
|
+
|
|
88
|
+
- v0.1.0: sync + async clients for write, retrieve, entity, graph, groups.
|
|
89
|
+
- v0.2.0: NDJSON streaming endpoints (`/v1/graph/nodes`, `/v1/graph/links`).
|
|
90
|
+
|
|
91
|
+
## Links
|
|
92
|
+
|
|
93
|
+
- Product: https://thebreeth.com
|
|
94
|
+
- Docs: https://docs.thebreeth.com
|
|
95
|
+
- Issues: https://github.com/Gramies/cogram-sdk-python/issues
|
|
96
|
+
|
|
97
|
+
## License
|
|
98
|
+
|
|
99
|
+
MIT. See [LICENSE](LICENSE).
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
[build-system]
|
|
2
|
+
requires = ["hatchling"]
|
|
3
|
+
build-backend = "hatchling.build"
|
|
4
|
+
|
|
5
|
+
[project]
|
|
6
|
+
name = "breeth"
|
|
7
|
+
version = "0.1.0"
|
|
8
|
+
description = "Official Python SDK for Breeth, a memory layer for agents."
|
|
9
|
+
readme = "README.md"
|
|
10
|
+
license = { text = "MIT" }
|
|
11
|
+
requires-python = ">=3.10"
|
|
12
|
+
authors = [{ name = "Breeth", email = "team@thebreeth.com" }]
|
|
13
|
+
keywords = ["breeth", "memory", "agents", "llm", "graph", "knowledge"]
|
|
14
|
+
classifiers = [
|
|
15
|
+
"Development Status :: 4 - Beta",
|
|
16
|
+
"Intended Audience :: Developers",
|
|
17
|
+
"License :: OSI Approved :: MIT License",
|
|
18
|
+
"Operating System :: OS Independent",
|
|
19
|
+
"Programming Language :: Python :: 3",
|
|
20
|
+
"Programming Language :: Python :: 3.10",
|
|
21
|
+
"Programming Language :: Python :: 3.11",
|
|
22
|
+
"Programming Language :: Python :: 3.12",
|
|
23
|
+
"Programming Language :: Python :: 3.13",
|
|
24
|
+
"Topic :: Software Development :: Libraries :: Python Modules",
|
|
25
|
+
]
|
|
26
|
+
dependencies = [
|
|
27
|
+
"httpx>=0.27",
|
|
28
|
+
"pydantic>=2",
|
|
29
|
+
]
|
|
30
|
+
|
|
31
|
+
[project.optional-dependencies]
|
|
32
|
+
dev = [
|
|
33
|
+
"pytest>=8",
|
|
34
|
+
"pytest-asyncio>=0.23",
|
|
35
|
+
"respx>=0.21",
|
|
36
|
+
]
|
|
37
|
+
|
|
38
|
+
[project.urls]
|
|
39
|
+
Homepage = "https://thebreeth.com"
|
|
40
|
+
Documentation = "https://docs.thebreeth.com"
|
|
41
|
+
Source = "https://github.com/Gramies/cogram-sdk-python"
|
|
42
|
+
Issues = "https://github.com/Gramies/cogram-sdk-python/issues"
|
|
43
|
+
|
|
44
|
+
[tool.hatch.build.targets.wheel]
|
|
45
|
+
packages = ["src/breeth"]
|
|
46
|
+
|
|
47
|
+
[tool.hatch.build.targets.sdist]
|
|
48
|
+
include = [
|
|
49
|
+
"/src",
|
|
50
|
+
"/tests",
|
|
51
|
+
"/README.md",
|
|
52
|
+
"/LICENSE",
|
|
53
|
+
]
|
|
54
|
+
|
|
55
|
+
[tool.pytest.ini_options]
|
|
56
|
+
asyncio_mode = "auto"
|
|
57
|
+
testpaths = ["tests"]
|
|
@@ -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
|
+
]
|
|
@@ -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
|