causality-engine 1.0.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.
Files changed (25) hide show
  1. causality_engine-1.0.0/LICENSE +21 -0
  2. causality_engine-1.0.0/PKG-INFO +216 -0
  3. causality_engine-1.0.0/README.md +181 -0
  4. causality_engine-1.0.0/causality/__init__.py +32 -0
  5. causality_engine-1.0.0/causality_engine/__init__.py +45 -0
  6. causality_engine-1.0.0/causality_engine/_http.py +171 -0
  7. causality_engine-1.0.0/causality_engine/client.py +111 -0
  8. causality_engine-1.0.0/causality_engine/exceptions.py +67 -0
  9. causality_engine-1.0.0/causality_engine/models.py +52 -0
  10. causality_engine-1.0.0/causality_engine/py.typed +1 -0
  11. causality_engine-1.0.0/causality_engine/resources/__init__.py +27 -0
  12. causality_engine-1.0.0/causality_engine/resources/_base.py +31 -0
  13. causality_engine-1.0.0/causality_engine/resources/agents.py +67 -0
  14. causality_engine-1.0.0/causality_engine/resources/attribution.py +102 -0
  15. causality_engine-1.0.0/causality_engine/resources/auth.py +59 -0
  16. causality_engine-1.0.0/causality_engine/resources/billing.py +50 -0
  17. causality_engine-1.0.0/causality_engine/resources/brand.py +42 -0
  18. causality_engine-1.0.0/causality_engine/resources/campaigns.py +41 -0
  19. causality_engine-1.0.0/causality_engine/resources/channels.py +66 -0
  20. causality_engine-1.0.0/causality_engine/resources/commissions.py +61 -0
  21. causality_engine-1.0.0/causality_engine/resources/health.py +40 -0
  22. causality_engine-1.0.0/causality_engine/resources/journeys.py +68 -0
  23. causality_engine-1.0.0/causality_engine/resources/referrals.py +37 -0
  24. causality_engine-1.0.0/pyproject.toml +60 -0
  25. causality_engine-1.0.0/tests/test_sdk.py +476 -0
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Causality Engine
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.
@@ -0,0 +1,216 @@
1
+ Metadata-Version: 2.4
2
+ Name: causality-engine
3
+ Version: 1.0.0
4
+ Summary: Official Python SDK for the Causality Engine API — causal attribution for e-commerce.
5
+ Project-URL: Homepage, https://causalityengine.ai
6
+ Project-URL: Documentation, https://developers.causalityengine.ai
7
+ Project-URL: Repository, https://github.com/causalityengine/causality-engine-python-sdk
8
+ Project-URL: Bug Tracker, https://github.com/causalityengine/causality-engine-python-sdk/issues
9
+ Project-URL: API Reference, https://developers.causalityengine.ai/api-reference
10
+ Author-email: Causality Engine <dev@causalityengine.ai>
11
+ License-Expression: MIT
12
+ License-File: LICENSE
13
+ Keywords: analytics,attribution,causal-inference,causality,ecommerce,marketing,marketing-attribution,shopify
14
+ Classifier: Development Status :: 5 - Production/Stable
15
+ Classifier: Intended Audience :: Developers
16
+ Classifier: License :: OSI Approved :: MIT License
17
+ Classifier: Operating System :: OS Independent
18
+ Classifier: Programming Language :: Python :: 3
19
+ Classifier: Programming Language :: Python :: 3.8
20
+ Classifier: Programming Language :: Python :: 3.9
21
+ Classifier: Programming Language :: Python :: 3.10
22
+ Classifier: Programming Language :: Python :: 3.11
23
+ Classifier: Programming Language :: Python :: 3.12
24
+ Classifier: Programming Language :: Python :: 3.13
25
+ Classifier: Topic :: Scientific/Engineering
26
+ Classifier: Topic :: Software Development :: Libraries :: Python Modules
27
+ Classifier: Typing :: Typed
28
+ Requires-Python: >=3.8
29
+ Requires-Dist: httpx<1.0.0,>=0.24.0
30
+ Provides-Extra: dev
31
+ Requires-Dist: pytest-asyncio>=0.21; extra == 'dev'
32
+ Requires-Dist: pytest>=7.0; extra == 'dev'
33
+ Requires-Dist: respx>=0.20; extra == 'dev'
34
+ Description-Content-Type: text/markdown
35
+
36
+ # Causality Engine Python SDK
37
+
38
+ The official Python SDK for the [Causality Engine](https://causalityengine.ai) API — causal attribution for e-commerce. Replace last-click with math.
39
+
40
+ [![PyPI version](https://img.shields.io/pypi/v/causality-engine.svg)](https://pypi.org/project/causality-engine/)
41
+ [![Python 3.8+](https://img.shields.io/pypi/pyversions/causality-engine.svg)](https://pypi.org/project/causality-engine/)
42
+ [![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](https://opensource.org/licenses/MIT)
43
+
44
+ ## Installation
45
+
46
+ ```bash
47
+ pip install causality-engine
48
+ ```
49
+
50
+ ## Quick Start
51
+
52
+ ```python
53
+ import causality
54
+
55
+ ce = causality.CausalityEngine(api_key="ce_live_sk_...")
56
+
57
+ # Run causal attribution analysis
58
+ result = ce.attribution.analyze(
59
+ store="my-store.myshopify.com",
60
+ date_range=["2026-01-01", "2026-01-31"],
61
+ )
62
+
63
+ for ch in result.data["channel_impact"]:
64
+ print(f"{ch['channel']}: {ch['causal_lift']:.0%} lift, ROI {ch['roi']:.1f}x")
65
+ ```
66
+
67
+ ## Authentication
68
+
69
+ Get your API key at [developers.causalityengine.ai/api-keys](https://developers.causalityengine.ai/api-keys).
70
+
71
+ ```python
72
+ # Option 1: Pass directly
73
+ ce = causality.CausalityEngine(api_key="ce_live_sk_...")
74
+
75
+ # Option 2: Environment variable
76
+ # export CAUSALITY_ENGINE_API_KEY="ce_live_sk_..."
77
+ ce = causality.CausalityEngine()
78
+ ```
79
+
80
+ ## API Coverage
81
+
82
+ | Resource | Methods | Description |
83
+ |---|---|---|
84
+ | `ce.attribution` | `analyze()`, `retrieve()`, `list()` | Causal attribution analysis |
85
+ | `ce.channels` | `performance()`, `amplification()` | Cross-channel performance |
86
+ | `ce.journeys` | `flow()`, `leakage()` | Customer journey mapping |
87
+ | `ce.campaigns` | `overview()` | Campaign-level intelligence |
88
+ | `ce.health` | `score()` | Marketing health diagnostics |
89
+ | `ce.brand` | `decompose()` | Brand awareness decomposition |
90
+ | `ce.agents` | `register()`, `me()`, `usage()` | AI agent management |
91
+ | `ce.referrals` | `list()` | Referral chain tracking |
92
+ | `ce.commissions` | `list()`, `verify()` | Commission ledger |
93
+ | `ce.billing` | `summary()`, `value_proofs()` | Billing & Delta_R tracking |
94
+ | `ce.auth` | `token()`, `rotate_key()` | OAuth & key management |
95
+
96
+ ## Examples
97
+
98
+ ### Channel Performance
99
+
100
+ ```python
101
+ perf = ce.channels.performance(
102
+ data_source_id="ds_abc123",
103
+ date_range=["2026-01-01", "2026-01-31"],
104
+ )
105
+ for ch in perf.data["channels"]:
106
+ print(f"{ch['name']}: score {ch['performance_score']}")
107
+ ```
108
+
109
+ ### Cross-Channel Amplification
110
+
111
+ ```python
112
+ amp = ce.channels.amplification(
113
+ data_source_id="ds_abc123",
114
+ date_range=["2026-01-01", "2026-01-31"],
115
+ )
116
+ print(f"Strongest pair: {amp.data['strongest_pair']}")
117
+ print(f"Total synergy: {amp.data['total_synergy_score']}")
118
+ ```
119
+
120
+ ### Customer Journey Flow
121
+
122
+ ```python
123
+ flow = ce.journeys.flow(
124
+ data_source_id="ds_abc123",
125
+ date_range=["2026-01-01", "2026-01-31"],
126
+ min_conversions=10,
127
+ )
128
+ for path in flow.data["top_paths"]:
129
+ print(f"{' → '.join(path['path'])}: {path['conversions']} conversions")
130
+ ```
131
+
132
+ ### Register an AI Agent
133
+
134
+ ```python
135
+ agent = ce.agents.register(
136
+ name="my-attribution-agent",
137
+ operator_email="dev@yourcompany.com",
138
+ capabilities=["attribution", "channel_analysis"],
139
+ )
140
+ print(f"Agent ID: {agent.data['agent_id']}")
141
+ print(f"Referral code: {agent.data['referral_code']}")
142
+ ```
143
+
144
+ ### Marketing Health Score
145
+
146
+ ```python
147
+ health = ce.health.score(
148
+ data_source_id="ds_abc123",
149
+ date_range=["2026-01-01", "2026-01-31"],
150
+ )
151
+ print(f"Score: {health.data['score']}/100 (Grade: {health.data['grade']})")
152
+ print(f"Recommendation: {health.data['top_recommendation']}")
153
+ ```
154
+
155
+ ## Error Handling
156
+
157
+ ```python
158
+ from causality_engine import (
159
+ AuthenticationError,
160
+ RateLimitError,
161
+ NotFoundError,
162
+ ValidationError,
163
+ CausalityEngineError,
164
+ )
165
+
166
+ try:
167
+ result = ce.attribution.analyze(
168
+ data_source_id="ds_abc123",
169
+ date_range=["2026-01-01", "2026-01-31"],
170
+ )
171
+ except AuthenticationError:
172
+ print("Invalid API key")
173
+ except RateLimitError as e:
174
+ print(f"Rate limited. Retry after {e.retry_after}s")
175
+ except NotFoundError:
176
+ print("Resource not found")
177
+ except ValidationError as e:
178
+ print(f"Invalid request: {e.message}")
179
+ except CausalityEngineError as e:
180
+ print(f"API error: {e.message} (status {e.status_code})")
181
+ ```
182
+
183
+ ## Configuration
184
+
185
+ ```python
186
+ ce = causality.CausalityEngine(
187
+ api_key="ce_live_sk_...",
188
+ base_url="https://api.causalityengine.ai", # default
189
+ timeout=30.0, # request timeout in seconds
190
+ max_retries=3, # retries on 429/5xx errors
191
+ )
192
+ ```
193
+
194
+ The SDK automatically retries on rate limits (429) and server errors (5xx) with exponential backoff.
195
+
196
+ ## Context Manager
197
+
198
+ ```python
199
+ with causality.CausalityEngine(api_key="ce_live_sk_...") as ce:
200
+ result = ce.attribution.analyze(
201
+ store="demo.myshopify.com",
202
+ date_range=["2026-01-01", "2026-01-31"],
203
+ )
204
+ # Transport is automatically closed
205
+ ```
206
+
207
+ ## Documentation
208
+
209
+ - [Developer Portal](https://developers.causalityengine.ai)
210
+ - [API Reference](https://developers.causalityengine.ai/api-reference)
211
+ - [Quickstart Guide](https://developers.causalityengine.ai/quickstart)
212
+ - [Agent Partner Program](https://developers.causalityengine.ai/agent-program)
213
+
214
+ ## License
215
+
216
+ MIT
@@ -0,0 +1,181 @@
1
+ # Causality Engine Python SDK
2
+
3
+ The official Python SDK for the [Causality Engine](https://causalityengine.ai) API — causal attribution for e-commerce. Replace last-click with math.
4
+
5
+ [![PyPI version](https://img.shields.io/pypi/v/causality-engine.svg)](https://pypi.org/project/causality-engine/)
6
+ [![Python 3.8+](https://img.shields.io/pypi/pyversions/causality-engine.svg)](https://pypi.org/project/causality-engine/)
7
+ [![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](https://opensource.org/licenses/MIT)
8
+
9
+ ## Installation
10
+
11
+ ```bash
12
+ pip install causality-engine
13
+ ```
14
+
15
+ ## Quick Start
16
+
17
+ ```python
18
+ import causality
19
+
20
+ ce = causality.CausalityEngine(api_key="ce_live_sk_...")
21
+
22
+ # Run causal attribution analysis
23
+ result = ce.attribution.analyze(
24
+ store="my-store.myshopify.com",
25
+ date_range=["2026-01-01", "2026-01-31"],
26
+ )
27
+
28
+ for ch in result.data["channel_impact"]:
29
+ print(f"{ch['channel']}: {ch['causal_lift']:.0%} lift, ROI {ch['roi']:.1f}x")
30
+ ```
31
+
32
+ ## Authentication
33
+
34
+ Get your API key at [developers.causalityengine.ai/api-keys](https://developers.causalityengine.ai/api-keys).
35
+
36
+ ```python
37
+ # Option 1: Pass directly
38
+ ce = causality.CausalityEngine(api_key="ce_live_sk_...")
39
+
40
+ # Option 2: Environment variable
41
+ # export CAUSALITY_ENGINE_API_KEY="ce_live_sk_..."
42
+ ce = causality.CausalityEngine()
43
+ ```
44
+
45
+ ## API Coverage
46
+
47
+ | Resource | Methods | Description |
48
+ |---|---|---|
49
+ | `ce.attribution` | `analyze()`, `retrieve()`, `list()` | Causal attribution analysis |
50
+ | `ce.channels` | `performance()`, `amplification()` | Cross-channel performance |
51
+ | `ce.journeys` | `flow()`, `leakage()` | Customer journey mapping |
52
+ | `ce.campaigns` | `overview()` | Campaign-level intelligence |
53
+ | `ce.health` | `score()` | Marketing health diagnostics |
54
+ | `ce.brand` | `decompose()` | Brand awareness decomposition |
55
+ | `ce.agents` | `register()`, `me()`, `usage()` | AI agent management |
56
+ | `ce.referrals` | `list()` | Referral chain tracking |
57
+ | `ce.commissions` | `list()`, `verify()` | Commission ledger |
58
+ | `ce.billing` | `summary()`, `value_proofs()` | Billing & Delta_R tracking |
59
+ | `ce.auth` | `token()`, `rotate_key()` | OAuth & key management |
60
+
61
+ ## Examples
62
+
63
+ ### Channel Performance
64
+
65
+ ```python
66
+ perf = ce.channels.performance(
67
+ data_source_id="ds_abc123",
68
+ date_range=["2026-01-01", "2026-01-31"],
69
+ )
70
+ for ch in perf.data["channels"]:
71
+ print(f"{ch['name']}: score {ch['performance_score']}")
72
+ ```
73
+
74
+ ### Cross-Channel Amplification
75
+
76
+ ```python
77
+ amp = ce.channels.amplification(
78
+ data_source_id="ds_abc123",
79
+ date_range=["2026-01-01", "2026-01-31"],
80
+ )
81
+ print(f"Strongest pair: {amp.data['strongest_pair']}")
82
+ print(f"Total synergy: {amp.data['total_synergy_score']}")
83
+ ```
84
+
85
+ ### Customer Journey Flow
86
+
87
+ ```python
88
+ flow = ce.journeys.flow(
89
+ data_source_id="ds_abc123",
90
+ date_range=["2026-01-01", "2026-01-31"],
91
+ min_conversions=10,
92
+ )
93
+ for path in flow.data["top_paths"]:
94
+ print(f"{' → '.join(path['path'])}: {path['conversions']} conversions")
95
+ ```
96
+
97
+ ### Register an AI Agent
98
+
99
+ ```python
100
+ agent = ce.agents.register(
101
+ name="my-attribution-agent",
102
+ operator_email="dev@yourcompany.com",
103
+ capabilities=["attribution", "channel_analysis"],
104
+ )
105
+ print(f"Agent ID: {agent.data['agent_id']}")
106
+ print(f"Referral code: {agent.data['referral_code']}")
107
+ ```
108
+
109
+ ### Marketing Health Score
110
+
111
+ ```python
112
+ health = ce.health.score(
113
+ data_source_id="ds_abc123",
114
+ date_range=["2026-01-01", "2026-01-31"],
115
+ )
116
+ print(f"Score: {health.data['score']}/100 (Grade: {health.data['grade']})")
117
+ print(f"Recommendation: {health.data['top_recommendation']}")
118
+ ```
119
+
120
+ ## Error Handling
121
+
122
+ ```python
123
+ from causality_engine import (
124
+ AuthenticationError,
125
+ RateLimitError,
126
+ NotFoundError,
127
+ ValidationError,
128
+ CausalityEngineError,
129
+ )
130
+
131
+ try:
132
+ result = ce.attribution.analyze(
133
+ data_source_id="ds_abc123",
134
+ date_range=["2026-01-01", "2026-01-31"],
135
+ )
136
+ except AuthenticationError:
137
+ print("Invalid API key")
138
+ except RateLimitError as e:
139
+ print(f"Rate limited. Retry after {e.retry_after}s")
140
+ except NotFoundError:
141
+ print("Resource not found")
142
+ except ValidationError as e:
143
+ print(f"Invalid request: {e.message}")
144
+ except CausalityEngineError as e:
145
+ print(f"API error: {e.message} (status {e.status_code})")
146
+ ```
147
+
148
+ ## Configuration
149
+
150
+ ```python
151
+ ce = causality.CausalityEngine(
152
+ api_key="ce_live_sk_...",
153
+ base_url="https://api.causalityengine.ai", # default
154
+ timeout=30.0, # request timeout in seconds
155
+ max_retries=3, # retries on 429/5xx errors
156
+ )
157
+ ```
158
+
159
+ The SDK automatically retries on rate limits (429) and server errors (5xx) with exponential backoff.
160
+
161
+ ## Context Manager
162
+
163
+ ```python
164
+ with causality.CausalityEngine(api_key="ce_live_sk_...") as ce:
165
+ result = ce.attribution.analyze(
166
+ store="demo.myshopify.com",
167
+ date_range=["2026-01-01", "2026-01-31"],
168
+ )
169
+ # Transport is automatically closed
170
+ ```
171
+
172
+ ## Documentation
173
+
174
+ - [Developer Portal](https://developers.causalityengine.ai)
175
+ - [API Reference](https://developers.causalityengine.ai/api-reference)
176
+ - [Quickstart Guide](https://developers.causalityengine.ai/quickstart)
177
+ - [Agent Partner Program](https://developers.causalityengine.ai/agent-program)
178
+
179
+ ## License
180
+
181
+ MIT
@@ -0,0 +1,32 @@
1
+ """
2
+ Alias module so ``import causality`` works.
3
+
4
+ This re-exports everything from ``causality_engine`` for convenience,
5
+ matching the documented usage pattern::
6
+
7
+ import causality
8
+ ce = causality.CausalityEngine(api_key="ce_live_sk_...")
9
+ """
10
+
11
+ from causality_engine import * # noqa: F401, F403
12
+ from causality_engine import (
13
+ CausalityEngine,
14
+ CausalityEngineError,
15
+ AuthenticationError,
16
+ RateLimitError,
17
+ NotFoundError,
18
+ ValidationError,
19
+ APIResponse,
20
+ __version__,
21
+ )
22
+
23
+ __all__ = [
24
+ "CausalityEngine",
25
+ "CausalityEngineError",
26
+ "AuthenticationError",
27
+ "RateLimitError",
28
+ "NotFoundError",
29
+ "ValidationError",
30
+ "APIResponse",
31
+ "__version__",
32
+ ]
@@ -0,0 +1,45 @@
1
+ """
2
+ Causality Engine Python SDK
3
+ ============================
4
+
5
+ Official Python client for the Causality Engine API.
6
+ Causal attribution for e-commerce — replace last-click with math.
7
+
8
+ Quick start::
9
+
10
+ import causality_engine as ce
11
+
12
+ client = ce.CausalityEngine(api_key="ce_live_sk_...")
13
+ result = client.attribution.analyze(
14
+ data_source_id="ds_abc123",
15
+ date_range=["2026-01-01", "2026-01-31"],
16
+ )
17
+ print(result.data["channel_impact"])
18
+
19
+ Or use the top-level ``causality`` alias::
20
+
21
+ import causality
22
+ ce = causality.CausalityEngine(api_key="ce_live_sk_...")
23
+
24
+ """
25
+
26
+ __version__ = "1.0.0"
27
+ __all__ = [
28
+ "CausalityEngine",
29
+ "CausalityEngineError",
30
+ "AuthenticationError",
31
+ "RateLimitError",
32
+ "NotFoundError",
33
+ "ValidationError",
34
+ "APIResponse",
35
+ ]
36
+
37
+ from causality_engine.client import CausalityEngine
38
+ from causality_engine.exceptions import (
39
+ CausalityEngineError,
40
+ AuthenticationError,
41
+ RateLimitError,
42
+ NotFoundError,
43
+ ValidationError,
44
+ )
45
+ from causality_engine.models import APIResponse
@@ -0,0 +1,171 @@
1
+ """
2
+ Low-level HTTP transport for the Causality Engine SDK.
3
+
4
+ Handles authentication, retries with exponential backoff, and
5
+ error-to-exception mapping. Uses httpx for both sync and async.
6
+ """
7
+
8
+ from __future__ import annotations
9
+
10
+ import time
11
+ import logging
12
+ from typing import Any, Dict, Optional, Union
13
+
14
+ import httpx
15
+
16
+ from causality_engine.exceptions import (
17
+ CausalityEngineError,
18
+ AuthenticationError,
19
+ RateLimitError,
20
+ NotFoundError,
21
+ ValidationError,
22
+ ServerError,
23
+ )
24
+ from causality_engine.models import APIResponse
25
+
26
+ logger = logging.getLogger("causality_engine")
27
+
28
+ DEFAULT_BASE_URL = "https://api.causalityengine.ai"
29
+ DEFAULT_TIMEOUT = 30.0
30
+ MAX_RETRIES = 3
31
+ RETRY_BACKOFF = 0.5 # seconds, doubles each retry
32
+
33
+
34
+ def _raise_for_status(response: httpx.Response) -> None:
35
+ """Map HTTP error codes to typed SDK exceptions."""
36
+ status = response.status_code
37
+ request_id = response.headers.get("x-request-id")
38
+
39
+ if status < 400:
40
+ return
41
+
42
+ try:
43
+ body = response.json()
44
+ except Exception:
45
+ body = {"error": response.text}
46
+
47
+ message = body.get("error", body.get("message", f"HTTP {status}"))
48
+ if isinstance(message, dict):
49
+ message = message.get("message", str(message))
50
+
51
+ kwargs = dict(
52
+ message=str(message),
53
+ status_code=status,
54
+ body=body,
55
+ request_id=request_id,
56
+ )
57
+
58
+ if status == 401:
59
+ raise AuthenticationError(**kwargs)
60
+ elif status == 404:
61
+ raise NotFoundError(**kwargs)
62
+ elif status == 422:
63
+ raise ValidationError(**kwargs)
64
+ elif status == 429:
65
+ retry_after = response.headers.get("retry-after")
66
+ raise RateLimitError(
67
+ retry_after=float(retry_after) if retry_after else None,
68
+ **kwargs,
69
+ )
70
+ elif status >= 500:
71
+ raise ServerError(**kwargs)
72
+ else:
73
+ raise CausalityEngineError(**kwargs)
74
+
75
+
76
+ class Transport:
77
+ """Synchronous HTTP transport with retry logic."""
78
+
79
+ def __init__(
80
+ self,
81
+ api_key: str,
82
+ base_url: str = DEFAULT_BASE_URL,
83
+ timeout: float = DEFAULT_TIMEOUT,
84
+ max_retries: int = MAX_RETRIES,
85
+ ) -> None:
86
+ self.api_key = api_key
87
+ self.base_url = base_url.rstrip("/")
88
+ self.max_retries = max_retries
89
+ self._client = httpx.Client(
90
+ base_url=self.base_url,
91
+ timeout=timeout,
92
+ headers={
93
+ "Authorization": f"Bearer {api_key}",
94
+ "Content-Type": "application/json",
95
+ "User-Agent": "causality-engine-python/1.0.0",
96
+ "Accept": "application/json",
97
+ },
98
+ )
99
+
100
+ def request(
101
+ self,
102
+ method: str,
103
+ path: str,
104
+ *,
105
+ json: Optional[Dict[str, Any]] = None,
106
+ params: Optional[Dict[str, Any]] = None,
107
+ ) -> APIResponse:
108
+ """Execute an HTTP request with automatic retries on 429/5xx."""
109
+ last_exc: Optional[Exception] = None
110
+
111
+ for attempt in range(self.max_retries + 1):
112
+ try:
113
+ response = self._client.request(
114
+ method,
115
+ path,
116
+ json=json,
117
+ params=_clean_params(params),
118
+ )
119
+ _raise_for_status(response)
120
+
121
+ data = response.json() if response.content else {}
122
+ return APIResponse(
123
+ data=data,
124
+ status_code=response.status_code,
125
+ request_id=response.headers.get("x-request-id"),
126
+ headers=dict(response.headers),
127
+ )
128
+
129
+ except (RateLimitError, ServerError) as exc:
130
+ last_exc = exc
131
+ if attempt < self.max_retries:
132
+ wait = getattr(exc, "retry_after", None)
133
+ if wait is None:
134
+ wait = RETRY_BACKOFF * (2 ** attempt)
135
+ logger.warning(
136
+ "Retrying %s %s (attempt %d/%d, wait %.1fs): %s",
137
+ method, path, attempt + 1, self.max_retries, wait, exc,
138
+ )
139
+ time.sleep(wait)
140
+ else:
141
+ raise
142
+
143
+ except httpx.TransportError as exc:
144
+ last_exc = exc
145
+ if attempt < self.max_retries:
146
+ wait = RETRY_BACKOFF * (2 ** attempt)
147
+ logger.warning(
148
+ "Transport error on %s %s (attempt %d/%d): %s",
149
+ method, path, attempt + 1, self.max_retries, exc,
150
+ )
151
+ time.sleep(wait)
152
+ else:
153
+ raise CausalityEngineError(
154
+ message=f"Transport error after {self.max_retries} retries: {exc}",
155
+ ) from exc
156
+
157
+ # Should not reach here, but just in case
158
+ raise CausalityEngineError(
159
+ message=f"Request failed after {self.max_retries} retries",
160
+ )
161
+
162
+ def close(self) -> None:
163
+ """Close the underlying HTTP client."""
164
+ self._client.close()
165
+
166
+
167
+ def _clean_params(params: Optional[Dict[str, Any]]) -> Optional[Dict[str, Any]]:
168
+ """Remove None values from query parameters."""
169
+ if params is None:
170
+ return None
171
+ return {k: v for k, v in params.items() if v is not None}