hebbrix 2.3.2__tar.gz → 2.4.1__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.
Files changed (30) hide show
  1. {hebbrix-2.3.2 → hebbrix-2.4.1}/CHANGELOG.md +22 -0
  2. hebbrix-2.4.1/PKG-INFO +169 -0
  3. hebbrix-2.4.1/README.md +127 -0
  4. hebbrix-2.4.1/hebbrix/__init__.py +42 -0
  5. {hebbrix-2.3.2 → hebbrix-2.4.1}/hebbrix/client.py +76 -12
  6. hebbrix-2.4.1/hebbrix/exceptions.py +135 -0
  7. {hebbrix-2.3.2 → hebbrix-2.4.1}/hebbrix/resources.py +204 -144
  8. {hebbrix-2.3.2 → hebbrix-2.4.1}/hebbrix/sync_client.py +136 -26
  9. hebbrix-2.4.1/hebbrix.egg-info/PKG-INFO +169 -0
  10. {hebbrix-2.3.2 → hebbrix-2.4.1}/hebbrix.egg-info/SOURCES.txt +1 -0
  11. {hebbrix-2.3.2 → hebbrix-2.4.1}/pyproject.toml +8 -6
  12. hebbrix-2.4.1/tests/test_advanced_resources.py +96 -0
  13. {hebbrix-2.3.2 → hebbrix-2.4.1}/tests/test_memories_resource.py +200 -1
  14. {hebbrix-2.3.2 → hebbrix-2.4.1}/tests/test_openapi_parity.py +26 -0
  15. hebbrix-2.4.1/tests/test_sync_client.py +305 -0
  16. hebbrix-2.3.2/PKG-INFO +0 -205
  17. hebbrix-2.3.2/README.md +0 -143
  18. hebbrix-2.3.2/hebbrix/__init__.py +0 -74
  19. hebbrix-2.3.2/hebbrix/exceptions.py +0 -48
  20. hebbrix-2.3.2/hebbrix.egg-info/PKG-INFO +0 -205
  21. hebbrix-2.3.2/tests/test_sync_client.py +0 -130
  22. {hebbrix-2.3.2 → hebbrix-2.4.1}/LICENSE +0 -0
  23. {hebbrix-2.3.2 → hebbrix-2.4.1}/MANIFEST.in +0 -0
  24. {hebbrix-2.3.2 → hebbrix-2.4.1}/hebbrix/chat.py +0 -0
  25. {hebbrix-2.3.2 → hebbrix-2.4.1}/hebbrix/models.py +0 -0
  26. {hebbrix-2.3.2 → hebbrix-2.4.1}/hebbrix.egg-info/dependency_links.txt +0 -0
  27. {hebbrix-2.3.2 → hebbrix-2.4.1}/hebbrix.egg-info/requires.txt +0 -0
  28. {hebbrix-2.3.2 → hebbrix-2.4.1}/hebbrix.egg-info/top_level.txt +0 -0
  29. {hebbrix-2.3.2 → hebbrix-2.4.1}/setup.cfg +0 -0
  30. {hebbrix-2.3.2 → hebbrix-2.4.1}/tests/test_procedural_resource.py +0 -0
@@ -1,5 +1,27 @@
1
1
  # Changelog
2
2
 
3
+ ## 2.4.1 — 2026-08-27
4
+
5
+ - Make every async and sync readiness deadline preserve the original durable
6
+ receipt and raise top-level `IndexingTimeoutError`, including single writes,
7
+ batches, inference jobs, and direct updates.
8
+ - Normalize memory/job/status/request/outbox/idempotency recovery metadata and
9
+ preserve transport-only `Location`, request, retry, event, and replay headers.
10
+
11
+ ## 2.4.0 — 2026-08-27
12
+
13
+ - Reconcile every exported advanced method with the canonical public OpenAPI,
14
+ including temporal, working-memory, consolidation, memory-tool, and RL routes.
15
+ - Add a route-manifest release gate and clean-wheel installation verification.
16
+ - Treat durable-but-indexing batch results as `202`, poll them through the SDK,
17
+ and raise `IndexingTimeoutError` with the durable receipt on client deadline.
18
+ - Preserve structured entitlement metadata in `EntitlementError`.
19
+ - Keep advanced resources async-only and document the sync-client boundary.
20
+ - Withdraw the experimental World Model from the public SDK until a trained,
21
+ versioned production model artifact and serving contract exist.
22
+ - Publish compatibility through `GET /v1/release`; replace the broken public
23
+ repository link with valid artifact and support links.
24
+
3
25
  ## 2.3.2 — 2026-08-27
4
26
 
5
27
  - Align procedure create/list/get/update/execute/delete with the canonical
hebbrix-2.4.1/PKG-INFO ADDED
@@ -0,0 +1,169 @@
1
+ Metadata-Version: 2.4
2
+ Name: hebbrix
3
+ Version: 2.4.1
4
+ Summary: Typed Python client for Hebbrix memory, retrieval, and outcome-learning APIs
5
+ Author-email: Hebbrix Team <support@hebbrix.com>
6
+ Maintainer-email: Hebbrix Team <support@hebbrix.com>
7
+ License-Expression: MIT
8
+ Project-URL: Homepage, https://hebbrix.com
9
+ Project-URL: Documentation, https://docs.hebbrix.com
10
+ Project-URL: Source, https://github.com/Hebbrix/hebbrix-python
11
+ Project-URL: Issues, https://github.com/Hebbrix/hebbrix-python/issues
12
+ Project-URL: Support, https://www.hebbrix.com/contact
13
+ Project-URL: API Reference, https://api.hebbrix.com/docs
14
+ Keywords: ai,memory,agents,llm,chatbot,assistant,ml,reinforcement-learning,knowledge-graph,vector-search,rag,temporal,procedural-memory,working-memory
15
+ Classifier: Development Status :: 4 - Beta
16
+ Classifier: Intended Audience :: Developers
17
+ Classifier: Topic :: Software Development :: Libraries :: Python Modules
18
+ Classifier: Topic :: Scientific/Engineering :: Artificial Intelligence
19
+ Classifier: Topic :: Database
20
+ Classifier: Programming Language :: Python :: 3
21
+ Classifier: Programming Language :: Python :: 3.8
22
+ Classifier: Programming Language :: Python :: 3.9
23
+ Classifier: Programming Language :: Python :: 3.10
24
+ Classifier: Programming Language :: Python :: 3.11
25
+ Classifier: Programming Language :: Python :: 3.12
26
+ Classifier: Operating System :: OS Independent
27
+ Classifier: Framework :: AsyncIO
28
+ Requires-Python: >=3.8
29
+ Description-Content-Type: text/markdown
30
+ License-File: LICENSE
31
+ Requires-Dist: httpx>=0.25.0
32
+ Requires-Dist: requests>=2.31.0
33
+ Provides-Extra: dev
34
+ Requires-Dist: pytest>=7.0.0; extra == "dev"
35
+ Requires-Dist: pytest-asyncio>=0.21.0; extra == "dev"
36
+ Requires-Dist: black>=23.0.0; extra == "dev"
37
+ Requires-Dist: flake8<6.0.0,>=5.0.4; python_full_version < "3.8.1" and extra == "dev"
38
+ Requires-Dist: flake8>=6.0.0; python_full_version >= "3.8.1" and extra == "dev"
39
+ Requires-Dist: mypy>=1.0.0; extra == "dev"
40
+ Requires-Dist: cryptography>=42.0.0; extra == "dev"
41
+ Dynamic: license-file
42
+
43
+ # Hebbrix Python SDK
44
+
45
+ Typed Python client for Hebbrix memory, retrieval, and outcome-learning APIs.
46
+
47
+ ## Install
48
+
49
+ ```bash
50
+ pip install hebbrix==2.4.1
51
+ ```
52
+
53
+ Python 3.8+ is supported. `MemoryClient` is asynchronous. `SyncMemoryClient`
54
+ supports the core collection, memory, search, correction, procedure, and
55
+ ProofLoop workflows; advanced temporal, working-memory, consolidation,
56
+ memory-tool, and RL resources are currently async-only.
57
+
58
+ ## Quick start
59
+
60
+ ```python
61
+ import asyncio
62
+ from hebbrix import MemoryClient
63
+
64
+ async def main():
65
+ async with MemoryClient(api_key="hbx_your_api_key") as client:
66
+ collection = await client.collections.create(name="Support memory")
67
+ memory = await client.memories.create(
68
+ collection_id=collection["id"],
69
+ content="Customer prefers concise replies",
70
+ wait_for_index=True,
71
+ idempotency_key="customer-42-preference-v1",
72
+ )
73
+ results = await client.search(
74
+ "How should replies be formatted?",
75
+ collection_id=collection["id"],
76
+ )
77
+ print(memory, results)
78
+
79
+ asyncio.run(main())
80
+ ```
81
+
82
+ ## Durable readiness
83
+
84
+ Memory writes return either a searchable completion or a durable `202` receipt.
85
+ A durable receipt means the database commit succeeded while indexing is still
86
+ converging; it is not a failure and does not justify a duplicate write.
87
+
88
+ When `wait_for_index=True`, the SDK accepts that receipt and polls the documented
89
+ status URL. It returns only after `searchable=true`. If the caller's deadline
90
+ expires, it raises `IndexingTimeoutError`; the exception retains the original
91
+ receipt plus normalized `memory_ids`, `job_id`, `status_url`, `request_id`,
92
+ `outbox_event_id`, retry timing, and idempotency replay metadata when available.
93
+ The synchronous and asynchronous single, batch, inference-job, and update
94
+ readiness paths share this behavior. The SDK never repeats the write while it
95
+ polls.
96
+
97
+ Catch the typed deadline without discarding the durable acceptance:
98
+
99
+ ```python
100
+ from hebbrix import IndexingTimeoutError
101
+
102
+ try:
103
+ created = await client.memories.create(
104
+ content="Customer prefers concise replies",
105
+ wait_for_index=True,
106
+ idempotency_key="customer-42-preference-v1",
107
+ index_timeout=5,
108
+ )
109
+ except IndexingTimeoutError as exc:
110
+ # Resume observation; do not submit an unrelated second write.
111
+ if exc.memory_ids:
112
+ created = await client.memories.wait_until_searchable(exc.memory_ids[0])
113
+ elif exc.job_id:
114
+ created = await client.memory_jobs.wait(exc.job_id)
115
+ ```
116
+
117
+ For a timed-out batch, pass `exc.receipt` to
118
+ `memories.wait_batch_until_searchable(...)`. Alternatively, replay the exact
119
+ same body with `exc.idempotency_key`; a changed body with the same key is
120
+ rejected by the API rather than creating a second logical write. For an update,
121
+ resume polling `exc.memory_ids[0]` because the relational edit already committed.
122
+
123
+ For an asynchronous batch receipt:
124
+
125
+ ```python
126
+ receipt = await client.memories.create_batch(
127
+ [{"content": "First fact"}, {"content": "Second fact"}],
128
+ collection_id="collection-42",
129
+ wait_for_index=False,
130
+ idempotency_key="import-42",
131
+ )
132
+ completed = await client.memories.wait_batch_until_searchable(receipt)
133
+ ```
134
+
135
+ ## Pagination
136
+
137
+ `collections.list()` returns the current page's collection items for backward
138
+ compatibility. Use `collections.list_page()` when cursor metadata is required.
139
+ Memory resources provide the same `list()`/`list_page()` distinction.
140
+
141
+ ## Advanced capabilities and entitlements
142
+
143
+ The async client exposes the canonical `/v1` temporal, working-memory,
144
+ consolidation, memory-tool, and RL contracts. RL metrics and evaluation require
145
+ the Pro plan. Process-wide RL training and checkpoint mutation require an admin
146
+ role. Entitlement failures raise `EntitlementError` and preserve the stable
147
+ error code, current/required plan, request ID, and support action.
148
+
149
+ The experimental World Model is intentionally not exported by this public SDK.
150
+ It remains withdrawn until a trained, versioned production model artifact and
151
+ an end-to-end public serving contract are available.
152
+
153
+ The authoritative account capability matrix is available from
154
+ `GET /v1/users/me/capabilities`.
155
+
156
+ ## Release compatibility
157
+
158
+ The production API publishes exact build and artifact compatibility at
159
+ [`GET /v1/release`](https://api.hebbrix.com/v1/release). The public OpenAPI is
160
+ [`/openapi.json`](https://api.hebbrix.com/openapi.json).
161
+
162
+ - [Documentation](https://docs.hebbrix.com)
163
+ - [API reference](https://api.hebbrix.com/docs)
164
+ - [PyPI files](https://pypi.org/project/hebbrix/#files)
165
+ - [Support](https://www.hebbrix.com/contact)
166
+
167
+ ## License
168
+
169
+ MIT. See `LICENSE` in the distribution.
@@ -0,0 +1,127 @@
1
+ # Hebbrix Python SDK
2
+
3
+ Typed Python client for Hebbrix memory, retrieval, and outcome-learning APIs.
4
+
5
+ ## Install
6
+
7
+ ```bash
8
+ pip install hebbrix==2.4.1
9
+ ```
10
+
11
+ Python 3.8+ is supported. `MemoryClient` is asynchronous. `SyncMemoryClient`
12
+ supports the core collection, memory, search, correction, procedure, and
13
+ ProofLoop workflows; advanced temporal, working-memory, consolidation,
14
+ memory-tool, and RL resources are currently async-only.
15
+
16
+ ## Quick start
17
+
18
+ ```python
19
+ import asyncio
20
+ from hebbrix import MemoryClient
21
+
22
+ async def main():
23
+ async with MemoryClient(api_key="hbx_your_api_key") as client:
24
+ collection = await client.collections.create(name="Support memory")
25
+ memory = await client.memories.create(
26
+ collection_id=collection["id"],
27
+ content="Customer prefers concise replies",
28
+ wait_for_index=True,
29
+ idempotency_key="customer-42-preference-v1",
30
+ )
31
+ results = await client.search(
32
+ "How should replies be formatted?",
33
+ collection_id=collection["id"],
34
+ )
35
+ print(memory, results)
36
+
37
+ asyncio.run(main())
38
+ ```
39
+
40
+ ## Durable readiness
41
+
42
+ Memory writes return either a searchable completion or a durable `202` receipt.
43
+ A durable receipt means the database commit succeeded while indexing is still
44
+ converging; it is not a failure and does not justify a duplicate write.
45
+
46
+ When `wait_for_index=True`, the SDK accepts that receipt and polls the documented
47
+ status URL. It returns only after `searchable=true`. If the caller's deadline
48
+ expires, it raises `IndexingTimeoutError`; the exception retains the original
49
+ receipt plus normalized `memory_ids`, `job_id`, `status_url`, `request_id`,
50
+ `outbox_event_id`, retry timing, and idempotency replay metadata when available.
51
+ The synchronous and asynchronous single, batch, inference-job, and update
52
+ readiness paths share this behavior. The SDK never repeats the write while it
53
+ polls.
54
+
55
+ Catch the typed deadline without discarding the durable acceptance:
56
+
57
+ ```python
58
+ from hebbrix import IndexingTimeoutError
59
+
60
+ try:
61
+ created = await client.memories.create(
62
+ content="Customer prefers concise replies",
63
+ wait_for_index=True,
64
+ idempotency_key="customer-42-preference-v1",
65
+ index_timeout=5,
66
+ )
67
+ except IndexingTimeoutError as exc:
68
+ # Resume observation; do not submit an unrelated second write.
69
+ if exc.memory_ids:
70
+ created = await client.memories.wait_until_searchable(exc.memory_ids[0])
71
+ elif exc.job_id:
72
+ created = await client.memory_jobs.wait(exc.job_id)
73
+ ```
74
+
75
+ For a timed-out batch, pass `exc.receipt` to
76
+ `memories.wait_batch_until_searchable(...)`. Alternatively, replay the exact
77
+ same body with `exc.idempotency_key`; a changed body with the same key is
78
+ rejected by the API rather than creating a second logical write. For an update,
79
+ resume polling `exc.memory_ids[0]` because the relational edit already committed.
80
+
81
+ For an asynchronous batch receipt:
82
+
83
+ ```python
84
+ receipt = await client.memories.create_batch(
85
+ [{"content": "First fact"}, {"content": "Second fact"}],
86
+ collection_id="collection-42",
87
+ wait_for_index=False,
88
+ idempotency_key="import-42",
89
+ )
90
+ completed = await client.memories.wait_batch_until_searchable(receipt)
91
+ ```
92
+
93
+ ## Pagination
94
+
95
+ `collections.list()` returns the current page's collection items for backward
96
+ compatibility. Use `collections.list_page()` when cursor metadata is required.
97
+ Memory resources provide the same `list()`/`list_page()` distinction.
98
+
99
+ ## Advanced capabilities and entitlements
100
+
101
+ The async client exposes the canonical `/v1` temporal, working-memory,
102
+ consolidation, memory-tool, and RL contracts. RL metrics and evaluation require
103
+ the Pro plan. Process-wide RL training and checkpoint mutation require an admin
104
+ role. Entitlement failures raise `EntitlementError` and preserve the stable
105
+ error code, current/required plan, request ID, and support action.
106
+
107
+ The experimental World Model is intentionally not exported by this public SDK.
108
+ It remains withdrawn until a trained, versioned production model artifact and
109
+ an end-to-end public serving contract are available.
110
+
111
+ The authoritative account capability matrix is available from
112
+ `GET /v1/users/me/capabilities`.
113
+
114
+ ## Release compatibility
115
+
116
+ The production API publishes exact build and artifact compatibility at
117
+ [`GET /v1/release`](https://api.hebbrix.com/v1/release). The public OpenAPI is
118
+ [`/openapi.json`](https://api.hebbrix.com/openapi.json).
119
+
120
+ - [Documentation](https://docs.hebbrix.com)
121
+ - [API reference](https://api.hebbrix.com/docs)
122
+ - [PyPI files](https://pypi.org/project/hebbrix/#files)
123
+ - [Support](https://www.hebbrix.com/contact)
124
+
125
+ ## License
126
+
127
+ MIT. See `LICENSE` in the distribution.
@@ -0,0 +1,42 @@
1
+ """Typed async-first client for Hebbrix memory and outcome-learning APIs.
2
+
3
+ The public surface and its plan/role restrictions are documented by the
4
+ production OpenAPI and ``GET /v1/users/me/capabilities``. The experimental
5
+ World Model is intentionally absent from this release.
6
+ """
7
+
8
+ __version__ = "2.4.1"
9
+ __author__ = "Hebbrix Team"
10
+ __license__ = "MIT"
11
+
12
+ from hebbrix.chat import MemoryChat
13
+ from hebbrix.client import MemoryClient
14
+ from hebbrix.exceptions import (
15
+ AuthenticationError,
16
+ EntitlementError,
17
+ HebbrixError,
18
+ IndexingTimeoutError,
19
+ NotFoundError,
20
+ RateLimitError,
21
+ ServerError,
22
+ ValidationError,
23
+ )
24
+ from hebbrix.models import EvidenceClaim, GroundingReceipt, SearchSafetyEnvelope
25
+ from hebbrix.sync_client import SyncMemoryClient
26
+
27
+ __all__ = [
28
+ "MemoryClient",
29
+ "SyncMemoryClient",
30
+ "MemoryChat",
31
+ "HebbrixError",
32
+ "AuthenticationError",
33
+ "EntitlementError",
34
+ "IndexingTimeoutError",
35
+ "ValidationError",
36
+ "NotFoundError",
37
+ "RateLimitError",
38
+ "ServerError",
39
+ "GroundingReceipt",
40
+ "EvidenceClaim",
41
+ "SearchSafetyEnvelope",
42
+ ]
@@ -10,6 +10,7 @@ from typing import Any, Dict, List, Optional
10
10
  import httpx
11
11
  from hebbrix.exceptions import (
12
12
  AuthenticationError,
13
+ EntitlementError,
13
14
  HebbrixError,
14
15
  NotFoundError,
15
16
  RateLimitError,
@@ -30,7 +31,6 @@ from hebbrix.resources import (
30
31
  SearchResource,
31
32
  TemporalResource,
32
33
  WorkingMemoryResource,
33
- WorldModelResource,
34
34
  )
35
35
 
36
36
 
@@ -85,13 +85,12 @@ class MemoryClient:
85
85
  self.working_memory = WorkingMemoryResource(self)
86
86
  self.consolidation = ConsolidationResource(self)
87
87
  self.memory_tools = MemoryToolsResource(self)
88
- self.world_model = WorldModelResource(self)
89
88
 
90
89
  def _get_headers(self) -> Dict[str, str]:
91
90
  """Get request headers."""
92
91
  headers = {
93
92
  "Content-Type": "application/json",
94
- "User-Agent": "hebbrix-python/2.3.2",
93
+ "User-Agent": "hebbrix-python/2.4.1",
95
94
  }
96
95
 
97
96
  if self.api_key:
@@ -108,23 +107,71 @@ class MemoryClient:
108
107
 
109
108
  try:
110
109
  error_data = response.json()
111
- message = error_data.get("error", {}).get("message", response.text)
112
110
  except Exception:
113
- message = response.text
111
+ error_data = {}
112
+
113
+ envelope = error_data.get("error") or error_data.get("detail") or {}
114
+ if not isinstance(envelope, dict):
115
+ envelope = {"message": str(envelope)}
116
+ nested = envelope.get("message")
117
+ details = nested if isinstance(nested, dict) else envelope
118
+ message = (
119
+ (details.get("message") if isinstance(details, dict) else None)
120
+ or (nested if isinstance(nested, str) else None)
121
+ or response.text
122
+ )
123
+ code = str(details.get("code") or envelope.get("code") or "") or None
124
+ request_id = (
125
+ details.get("request_id")
126
+ or envelope.get("request_id")
127
+ or response.headers.get("X-Request-ID")
128
+ )
114
129
 
115
130
  if status_code == 401:
116
- raise AuthenticationError(message)
131
+ raise AuthenticationError(
132
+ message, code=code, request_id=request_id, details=details
133
+ )
117
134
  elif status_code == 404:
118
- raise NotFoundError(message)
135
+ raise NotFoundError(
136
+ message, code=code, request_id=request_id, details=details
137
+ )
119
138
  elif status_code == 422:
120
139
  errors = error_data.get("error", {}).get("details", [])
121
- raise ValidationError(message, errors=errors)
140
+ raise ValidationError(
141
+ message,
142
+ errors=errors,
143
+ code=code,
144
+ request_id=request_id,
145
+ details=details,
146
+ )
122
147
  elif status_code == 429:
123
- raise RateLimitError(message)
148
+ raise RateLimitError(
149
+ message, code=code, request_id=request_id, details=details
150
+ )
124
151
  elif status_code >= 500:
125
- raise ServerError(message)
152
+ raise ServerError(
153
+ message, code=code, request_id=request_id, details=details
154
+ )
155
+ elif status_code in {402, 403} and (
156
+ "ENTITLEMENT" in str(code or "")
157
+ or details.get("error")
158
+ in {"feature_not_available", "tier_upgrade_required"}
159
+ ):
160
+ raise EntitlementError(
161
+ message,
162
+ status_code=status_code,
163
+ code=code,
164
+ request_id=request_id,
165
+ details=details,
166
+ )
126
167
  else:
127
- raise HebbrixError(message, status_code=status_code)
168
+ raise HebbrixError(
169
+ message,
170
+ status_code=status_code,
171
+ code=code,
172
+ request_id=request_id,
173
+ details=details,
174
+ )
128
175
 
129
176
  async def request(
130
177
  self,
@@ -153,7 +200,24 @@ class MemoryClient:
153
200
  if response.status_code >= 400:
154
201
  self._handle_error(response)
155
202
 
156
- return response.json() if response.text else {}
203
+ payload = response.json() if response.text else {}
204
+ if isinstance(payload, dict):
205
+ # Preserve transport-only recovery identifiers on durable 202
206
+ # receipts. The resource layer needs these values if its local
207
+ # readiness deadline expires after the write has committed.
208
+ recovery_headers = {
209
+ "request_id": response.headers.get("X-Request-ID"),
210
+ "status_url": response.headers.get("Location"),
211
+ "outbox_event_id": response.headers.get("X-Hebbrix-Index-Event"),
212
+ "retry_after": response.headers.get("Retry-After"),
213
+ }
214
+ for key, value in recovery_headers.items():
215
+ if value and not payload.get(key):
216
+ payload[key] = value
217
+ replay = response.headers.get("X-Idempotent-Replay")
218
+ if replay is not None and "idempotency_replay" not in payload:
219
+ payload["idempotency_replay"] = replay.casefold() == "true"
220
+ return payload
157
221
 
158
222
  async def get(self, path: str, **kwargs) -> Dict[str, Any]:
159
223
  """Make a GET request."""
@@ -0,0 +1,135 @@
1
+ """
2
+ Hebbrix SDK Exceptions
3
+ """
4
+
5
+
6
+ class HebbrixError(Exception):
7
+ """Base exception for Hebbrix SDK."""
8
+
9
+ def __init__(
10
+ self,
11
+ message: str,
12
+ status_code: int = None,
13
+ *,
14
+ code: str = None,
15
+ request_id: str = None,
16
+ details: dict = None,
17
+ ):
18
+ self.message = message
19
+ self.status_code = status_code
20
+ self.code = code
21
+ self.request_id = request_id
22
+ self.details = details or {}
23
+ super().__init__(self.message)
24
+
25
+
26
+ class EntitlementError(HebbrixError):
27
+ """The authenticated account lacks the plan or role for an operation."""
28
+
29
+ def __init__(self, message: str, *, status_code: int, **kwargs):
30
+ super().__init__(message, status_code=status_code, **kwargs)
31
+
32
+
33
+ class IndexingTimeoutError(TimeoutError):
34
+ """A durable write did not become searchable before the client deadline.
35
+
36
+ The write has already committed when this exception is raised. Recovery
37
+ metadata therefore lives on the exception so callers can resume polling or
38
+ safely replay the *same* request with the same idempotency key instead of
39
+ issuing an uncorrelated duplicate write.
40
+ """
41
+
42
+ def __init__(
43
+ self,
44
+ message: str,
45
+ receipt: dict,
46
+ *,
47
+ idempotency_key: str = None,
48
+ ):
49
+ self.receipt = dict(receipt or {})
50
+
51
+ candidates = [
52
+ *(self.receipt.get("memory_ids") or []),
53
+ self.receipt.get("memory_id"),
54
+ self.receipt.get("id"),
55
+ *(
56
+ item.get("memory_id") or item.get("id")
57
+ for item in (self.receipt.get("results") or [])
58
+ if isinstance(item, dict)
59
+ ),
60
+ ]
61
+ self.memory_ids = list(
62
+ dict.fromkeys(str(value) for value in candidates if value)
63
+ )
64
+ self.job_id = self.receipt.get("job_id")
65
+ self.status_url = self.receipt.get("status_url")
66
+ if not self.status_url and self.memory_ids:
67
+ self.status_url = f"/v1/memories/{self.memory_ids[0]}"
68
+ if not self.status_url and self.job_id:
69
+ self.status_url = f"/v1/memory-jobs/{self.job_id}"
70
+
71
+ self.request_id = self.receipt.get("request_id")
72
+ self.outbox_event_id = self.receipt.get("outbox_event_id")
73
+ self.indexing_event_id = (
74
+ self.receipt.get("indexing_event_id") or self.outbox_event_id
75
+ )
76
+ self.event_id = self.receipt.get("event_id") or self.indexing_event_id
77
+ self.idempotency_key = idempotency_key or self.receipt.get("idempotency_key")
78
+ self.idempotency_replay = self.receipt.get(
79
+ "idempotency_replay",
80
+ self.receipt.get("idempotency_replayed"),
81
+ )
82
+ self.retry_after = self.receipt.get("retry_after")
83
+ self.recovery = {
84
+ key: value
85
+ for key, value in {
86
+ "memory_ids": list(self.memory_ids),
87
+ "job_id": self.job_id,
88
+ "status_url": self.status_url,
89
+ "request_id": self.request_id,
90
+ "outbox_event_id": self.outbox_event_id,
91
+ "indexing_event_id": self.indexing_event_id,
92
+ "event_id": self.event_id,
93
+ "idempotency_key": self.idempotency_key,
94
+ "idempotency_replay": self.idempotency_replay,
95
+ "retry_after": self.retry_after,
96
+ }.items()
97
+ if value not in (None, "", [])
98
+ }
99
+ super().__init__(message)
100
+
101
+
102
+ class AuthenticationError(HebbrixError):
103
+ """Raised when authentication fails."""
104
+
105
+ def __init__(self, message: str = "Authentication failed", **kwargs):
106
+ super().__init__(message, status_code=401, **kwargs)
107
+
108
+
109
+ class ValidationError(HebbrixError):
110
+ """Raised when request validation fails."""
111
+
112
+ def __init__(self, message: str, errors: list = None, **kwargs):
113
+ self.errors = errors or []
114
+ super().__init__(message, status_code=422, **kwargs)
115
+
116
+
117
+ class NotFoundError(HebbrixError):
118
+ """Raised when a resource is not found."""
119
+
120
+ def __init__(self, message: str = "Resource not found", **kwargs):
121
+ super().__init__(message, status_code=404, **kwargs)
122
+
123
+
124
+ class RateLimitError(HebbrixError):
125
+ """Raised when rate limit is exceeded."""
126
+
127
+ def __init__(self, message: str = "Rate limit exceeded", **kwargs):
128
+ super().__init__(message, status_code=429, **kwargs)
129
+
130
+
131
+ class ServerError(HebbrixError):
132
+ """Raised when server returns 5xx error."""
133
+
134
+ def __init__(self, message: str = "Internal server error", **kwargs):
135
+ super().__init__(message, status_code=500, **kwargs)