cadlag-sdk 1.0.4__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.
Potentially problematic release.
This version of cadlag-sdk might be problematic. Click here for more details.
- cadlag_sdk-1.0.4/LICENSE +17 -0
- cadlag_sdk-1.0.4/PKG-INFO +50 -0
- cadlag_sdk-1.0.4/README.md +19 -0
- cadlag_sdk-1.0.4/cadlag_sdk/__init__.py +25 -0
- cadlag_sdk-1.0.4/cadlag_sdk/cli.py +96 -0
- cadlag_sdk-1.0.4/cadlag_sdk/client/__init__.py +29 -0
- cadlag_sdk-1.0.4/cadlag_sdk/client/_transport.py +213 -0
- cadlag_sdk-1.0.4/cadlag_sdk/client/client.py +256 -0
- cadlag_sdk-1.0.4/cadlag_sdk/client/config.py +25 -0
- cadlag_sdk-1.0.4/cadlag_sdk/client/exceptions.py +90 -0
- cadlag_sdk-1.0.4/cadlag_sdk/client/models/__init__.py +97 -0
- cadlag_sdk-1.0.4/cadlag_sdk/client/models/audit.py +23 -0
- cadlag_sdk-1.0.4/cadlag_sdk/client/models/backtest.py +78 -0
- cadlag_sdk-1.0.4/cadlag_sdk/client/models/config.py +20 -0
- cadlag_sdk-1.0.4/cadlag_sdk/client/models/data.py +76 -0
- cadlag_sdk-1.0.4/cadlag_sdk/client/models/datasets.py +42 -0
- cadlag_sdk-1.0.4/cadlag_sdk/client/models/deployments.py +31 -0
- cadlag_sdk-1.0.4/cadlag_sdk/client/models/experiments.py +33 -0
- cadlag_sdk-1.0.4/cadlag_sdk/client/models/feature_sets.py +36 -0
- cadlag_sdk-1.0.4/cadlag_sdk/client/models/live.py +80 -0
- cadlag_sdk-1.0.4/cadlag_sdk/client/models/models.py +54 -0
- cadlag_sdk-1.0.4/cadlag_sdk/client/models/pipeline.py +30 -0
- cadlag_sdk-1.0.4/cadlag_sdk/client/models/risk.py +29 -0
- cadlag_sdk-1.0.4/cadlag_sdk/client/models/strategies.py +41 -0
- cadlag_sdk-1.0.4/cadlag_sdk/client/models/validation.py +55 -0
- cadlag_sdk-1.0.4/cadlag_sdk/client/modules/__init__.py +102 -0
- cadlag_sdk-1.0.4/cadlag_sdk/client/modules/audit.py +52 -0
- cadlag_sdk-1.0.4/cadlag_sdk/client/modules/backtest.py +122 -0
- cadlag_sdk-1.0.4/cadlag_sdk/client/modules/config.py +28 -0
- cadlag_sdk-1.0.4/cadlag_sdk/client/modules/data.py +116 -0
- cadlag_sdk-1.0.4/cadlag_sdk/client/modules/datasets.py +91 -0
- cadlag_sdk-1.0.4/cadlag_sdk/client/modules/deployments.py +62 -0
- cadlag_sdk-1.0.4/cadlag_sdk/client/modules/experiments.py +77 -0
- cadlag_sdk-1.0.4/cadlag_sdk/client/modules/feature_sets.py +85 -0
- cadlag_sdk-1.0.4/cadlag_sdk/client/modules/live.py +94 -0
- cadlag_sdk-1.0.4/cadlag_sdk/client/modules/models.py +101 -0
- cadlag_sdk-1.0.4/cadlag_sdk/client/modules/pipeline.py +68 -0
- cadlag_sdk-1.0.4/cadlag_sdk/client/modules/risk.py +42 -0
- cadlag_sdk-1.0.4/cadlag_sdk/client/modules/strategies.py +94 -0
- cadlag_sdk-1.0.4/cadlag_sdk/client/modules/validation.py +74 -0
- cadlag_sdk-1.0.4/cadlag_sdk/config_schema.py +194 -0
- cadlag_sdk-1.0.4/cadlag_sdk/data_process.py +2042 -0
- cadlag_sdk-1.0.4/cadlag_sdk/env_setting.py +53 -0
- cadlag_sdk-1.0.4/cadlag_sdk/feature_builder.py +1651 -0
- cadlag_sdk-1.0.4/cadlag_sdk/losses/__init__.py +4 -0
- cadlag_sdk-1.0.4/cadlag_sdk/losses/base.py +38 -0
- cadlag_sdk-1.0.4/cadlag_sdk/models/__init__.py +4 -0
- cadlag_sdk-1.0.4/cadlag_sdk/models/base.py +75 -0
- cadlag_sdk-1.0.4/cadlag_sdk/py.typed +1 -0
- cadlag_sdk-1.0.4/cadlag_sdk/strategies/__init__.py +4 -0
- cadlag_sdk-1.0.4/cadlag_sdk/strategies/base.py +129 -0
- cadlag_sdk-1.0.4/cadlag_sdk/symbol_alias.py +19 -0
- cadlag_sdk-1.0.4/cadlag_sdk/testing/__init__.py +103 -0
- cadlag_sdk-1.0.4/cadlag_sdk/trainers/__init__.py +4 -0
- cadlag_sdk-1.0.4/cadlag_sdk/trainers/base.py +149 -0
- cadlag_sdk-1.0.4/cadlag_sdk.egg-info/PKG-INFO +50 -0
- cadlag_sdk-1.0.4/cadlag_sdk.egg-info/SOURCES.txt +69 -0
- cadlag_sdk-1.0.4/cadlag_sdk.egg-info/dependency_links.txt +1 -0
- cadlag_sdk-1.0.4/cadlag_sdk.egg-info/entry_points.txt +5 -0
- cadlag_sdk-1.0.4/cadlag_sdk.egg-info/requires.txt +19 -0
- cadlag_sdk-1.0.4/cadlag_sdk.egg-info/top_level.txt +1 -0
- cadlag_sdk-1.0.4/pyproject.toml +61 -0
- cadlag_sdk-1.0.4/setup.cfg +4 -0
- cadlag_sdk-1.0.4/tests/test_base_classes.py +292 -0
- cadlag_sdk-1.0.4/tests/test_client.py +211 -0
- cadlag_sdk-1.0.4/tests/test_config_schema.py +136 -0
- cadlag_sdk-1.0.4/tests/test_data_process.py +556 -0
- cadlag_sdk-1.0.4/tests/test_env_setting.py +49 -0
- cadlag_sdk-1.0.4/tests/test_feature_builder.py +512 -0
- cadlag_sdk-1.0.4/tests/test_feature_builder_models.py +182 -0
- cadlag_sdk-1.0.4/tests/test_symbol_alias.py +20 -0
cadlag_sdk-1.0.4/LICENSE
ADDED
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
Apache License
|
|
2
|
+
Version 2.0, January 2004
|
|
3
|
+
http://www.apache.org/licenses/
|
|
4
|
+
|
|
5
|
+
Copyright (c) 2026 Cadlag
|
|
6
|
+
|
|
7
|
+
Licensed under the Apache License, Version 2.0 (the "License");
|
|
8
|
+
you may not use this file except in compliance with the License.
|
|
9
|
+
You may obtain a copy of the License at
|
|
10
|
+
|
|
11
|
+
http://www.apache.org/licenses/LICENSE-2.0
|
|
12
|
+
|
|
13
|
+
Unless required by applicable law or agreed to in writing, software
|
|
14
|
+
distributed under the License is distributed on an "AS IS" BASIS,
|
|
15
|
+
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
16
|
+
See the License for the specific language governing permissions and
|
|
17
|
+
limitations under the License.
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: cadlag-sdk
|
|
3
|
+
Version: 1.0.4
|
|
4
|
+
Summary: Cadlag SDK — Base classes, utilities, platform client, CLI, and testing fixtures
|
|
5
|
+
Maintainer-email: Cadlag <info@cadlag.pro>
|
|
6
|
+
License: Apache-2.0
|
|
7
|
+
Project-URL: Homepage, https://cadlag.pro
|
|
8
|
+
Project-URL: Repository, https://github.com/cadlag-quant/cadlag
|
|
9
|
+
Requires-Python: >=3.11
|
|
10
|
+
Description-Content-Type: text/markdown
|
|
11
|
+
License-File: LICENSE
|
|
12
|
+
Requires-Dist: pydantic>=2.5
|
|
13
|
+
Requires-Dist: polars>=0.20
|
|
14
|
+
Requires-Dist: numpy>=1.24
|
|
15
|
+
Requires-Dist: pyarrow>=14.0
|
|
16
|
+
Requires-Dist: httpx>=0.26
|
|
17
|
+
Requires-Dist: websockets>=12.0
|
|
18
|
+
Requires-Dist: structlog>=24.1
|
|
19
|
+
Requires-Dist: scikit-learn>=1.3
|
|
20
|
+
Requires-Dist: pandas>=2.0
|
|
21
|
+
Requires-Dist: scipy>=1.11
|
|
22
|
+
Requires-Dist: joblib>=1.3
|
|
23
|
+
Requires-Dist: TA-Lib>=0.4
|
|
24
|
+
Provides-Extra: dev
|
|
25
|
+
Requires-Dist: pytest>=7.4; extra == "dev"
|
|
26
|
+
Requires-Dist: pytest-asyncio>=0.23; extra == "dev"
|
|
27
|
+
Requires-Dist: pytest-cov>=4.1; extra == "dev"
|
|
28
|
+
Requires-Dist: ruff>=0.3; extra == "dev"
|
|
29
|
+
Requires-Dist: mypy>=1.8; extra == "dev"
|
|
30
|
+
Dynamic: license-file
|
|
31
|
+
|
|
32
|
+
# Cadlag SDK
|
|
33
|
+
|
|
34
|
+
Base classes, utilities, platform client, CLI and testing fixtures for Cadlag.
|
|
35
|
+
Requires Python 3.11 or later. Licensed under Apache-2.0.
|
|
36
|
+
|
|
37
|
+
```bash
|
|
38
|
+
python -m pip install cadlag-sdk
|
|
39
|
+
cadlag --help
|
|
40
|
+
```
|
|
41
|
+
|
|
42
|
+
```python
|
|
43
|
+
from cadlag_sdk import CadlagClient, AsyncCadlagClient
|
|
44
|
+
|
|
45
|
+
with CadlagClient(base_url="http://localhost:8000") as client:
|
|
46
|
+
print(client.health())
|
|
47
|
+
```
|
|
48
|
+
|
|
49
|
+
The SDK connects to a running Cadlag CE server. Install `cadlag-quant` to include the
|
|
50
|
+
server package and Starter examples. Import the SDK as `cadlag_sdk`.
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
# Cadlag SDK
|
|
2
|
+
|
|
3
|
+
Base classes, utilities, platform client, CLI and testing fixtures for Cadlag.
|
|
4
|
+
Requires Python 3.11 or later. Licensed under Apache-2.0.
|
|
5
|
+
|
|
6
|
+
```bash
|
|
7
|
+
python -m pip install cadlag-sdk
|
|
8
|
+
cadlag --help
|
|
9
|
+
```
|
|
10
|
+
|
|
11
|
+
```python
|
|
12
|
+
from cadlag_sdk import CadlagClient, AsyncCadlagClient
|
|
13
|
+
|
|
14
|
+
with CadlagClient(base_url="http://localhost:8000") as client:
|
|
15
|
+
print(client.health())
|
|
16
|
+
```
|
|
17
|
+
|
|
18
|
+
The SDK connects to a running Cadlag CE server. Install `cadlag-quant` to include the
|
|
19
|
+
server package and Starter examples. Import the SDK as `cadlag_sdk`.
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
# Copyright (c) 2026 Cadlag. Licensed under Apache 2.0.
|
|
2
|
+
"""Cadlag SDK — public surface re-exports."""
|
|
3
|
+
|
|
4
|
+
from importlib.metadata import PackageNotFoundError, version
|
|
5
|
+
|
|
6
|
+
from cadlag_sdk.client.client import AsyncCadlagClient, CadlagClient
|
|
7
|
+
from cadlag_sdk.losses.base import BaseLoss
|
|
8
|
+
from cadlag_sdk.models.base import BaseModel
|
|
9
|
+
from cadlag_sdk.strategies.base import BaseStrategy
|
|
10
|
+
from cadlag_sdk.trainers.base import BaseTrainer
|
|
11
|
+
|
|
12
|
+
try:
|
|
13
|
+
__version__ = version("cadlag-sdk")
|
|
14
|
+
except PackageNotFoundError:
|
|
15
|
+
__version__ = "0+unknown"
|
|
16
|
+
|
|
17
|
+
__all__ = [
|
|
18
|
+
"AsyncCadlagClient",
|
|
19
|
+
"BaseLoss",
|
|
20
|
+
"BaseModel",
|
|
21
|
+
"BaseStrategy",
|
|
22
|
+
"BaseTrainer",
|
|
23
|
+
"CadlagClient",
|
|
24
|
+
"__version__",
|
|
25
|
+
]
|
|
@@ -0,0 +1,96 @@
|
|
|
1
|
+
# Copyright (c) 2026 Cadlag. Licensed under Apache 2.0.
|
|
2
|
+
"""Cadlag SDK command-line interface.
|
|
3
|
+
|
|
4
|
+
Entry point: ``cadlag`` (declared in ``pyproject.toml`` ``[project.scripts]``).
|
|
5
|
+
|
|
6
|
+
The SDK owns the executable but discovers commands supplied by installed
|
|
7
|
+
Cadlag packages through the ``cadlag.cli`` entry-point group. This keeps
|
|
8
|
+
the SDK dependency-free with respect to Cadlag CE and optional extensions.
|
|
9
|
+
"""
|
|
10
|
+
|
|
11
|
+
from __future__ import annotations
|
|
12
|
+
|
|
13
|
+
import argparse
|
|
14
|
+
import subprocess
|
|
15
|
+
import sys
|
|
16
|
+
from collections.abc import Callable, Sequence
|
|
17
|
+
from importlib import metadata
|
|
18
|
+
from typing import Protocol, cast
|
|
19
|
+
|
|
20
|
+
CommandHandler = Callable[[argparse.Namespace], int]
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
class CommandRegistrar(Protocol):
|
|
24
|
+
"""Register package-owned commands on the root Cadlag CLI."""
|
|
25
|
+
|
|
26
|
+
def __call__(
|
|
27
|
+
self,
|
|
28
|
+
subparsers: argparse._SubParsersAction[argparse.ArgumentParser],
|
|
29
|
+
) -> None: ...
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
def _cmd_test(args: argparse.Namespace) -> int:
|
|
33
|
+
"""Run SDK tests using pytest.
|
|
34
|
+
|
|
35
|
+
Args:
|
|
36
|
+
args: Parsed CLI arguments (``args.markers``, ``args.verbose``).
|
|
37
|
+
|
|
38
|
+
Returns:
|
|
39
|
+
pytest exit code.
|
|
40
|
+
"""
|
|
41
|
+
cmd = [sys.executable, "-m", "pytest"]
|
|
42
|
+
if args.verbose:
|
|
43
|
+
cmd.append("-v")
|
|
44
|
+
if args.markers:
|
|
45
|
+
cmd.extend(["-m", args.markers])
|
|
46
|
+
# Run from the cadlag_sdk package directory so conftest is discovered.
|
|
47
|
+
import pathlib
|
|
48
|
+
|
|
49
|
+
import cadlag_sdk as _sdk
|
|
50
|
+
|
|
51
|
+
sdk_root = str(pathlib.Path(_sdk.__file__).parent.parent)
|
|
52
|
+
cmd.append(sdk_root)
|
|
53
|
+
result = subprocess.run(cmd, check=False)
|
|
54
|
+
return result.returncode
|
|
55
|
+
|
|
56
|
+
|
|
57
|
+
def build_parser() -> argparse.ArgumentParser:
|
|
58
|
+
"""Build the root parser and load commands from installed packages."""
|
|
59
|
+
parser = argparse.ArgumentParser(
|
|
60
|
+
prog="cadlag",
|
|
61
|
+
description="Cadlag SDK command-line interface.",
|
|
62
|
+
)
|
|
63
|
+
sub = parser.add_subparsers(dest="command", metavar="<command>")
|
|
64
|
+
|
|
65
|
+
test_parser = sub.add_parser("test", help="Run SDK contract tests via pytest.")
|
|
66
|
+
test_parser.add_argument("-v", "--verbose", action="store_true", help="Verbose pytest output.")
|
|
67
|
+
test_parser.add_argument("-m", "--markers", default="", help="pytest marker expression.")
|
|
68
|
+
test_parser.set_defaults(_cadlag_handler=_cmd_test)
|
|
69
|
+
|
|
70
|
+
entry_points = sorted(metadata.entry_points(group="cadlag.cli"), key=lambda entry_point: entry_point.name)
|
|
71
|
+
for entry_point in entry_points:
|
|
72
|
+
try:
|
|
73
|
+
registrar = cast("CommandRegistrar", entry_point.load())
|
|
74
|
+
registrar(sub)
|
|
75
|
+
except Exception as exc:
|
|
76
|
+
raise RuntimeError(
|
|
77
|
+
f"Unable to register Cadlag CLI provider {entry_point.name!r} ({entry_point.value}): {exc}"
|
|
78
|
+
) from exc
|
|
79
|
+
|
|
80
|
+
return parser
|
|
81
|
+
|
|
82
|
+
|
|
83
|
+
def main(argv: Sequence[str] | None = None) -> None:
|
|
84
|
+
"""Entry point for the ``cadlag`` CLI."""
|
|
85
|
+
parser = build_parser()
|
|
86
|
+
args = parser.parse_args(argv)
|
|
87
|
+
|
|
88
|
+
handler = cast("CommandHandler | None", getattr(args, "_cadlag_handler", None))
|
|
89
|
+
if handler is None:
|
|
90
|
+
parser.print_help()
|
|
91
|
+
raise SystemExit(0)
|
|
92
|
+
raise SystemExit(handler(args))
|
|
93
|
+
|
|
94
|
+
|
|
95
|
+
if __name__ == "__main__":
|
|
96
|
+
main()
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
# Copyright (c) 2026 Cadlag. Licensed under Apache 2.0.
|
|
2
|
+
from cadlag_sdk.client.client import AsyncCadlagClient, CadlagClient
|
|
3
|
+
from cadlag_sdk.client.config import ClientConfig
|
|
4
|
+
from cadlag_sdk.client.exceptions import (
|
|
5
|
+
BacktestFailedError,
|
|
6
|
+
ChainLimitExceededError,
|
|
7
|
+
ConnectionError,
|
|
8
|
+
NotFoundError,
|
|
9
|
+
PipelineFailedError,
|
|
10
|
+
ProFeatureRequiredError,
|
|
11
|
+
CadlagError,
|
|
12
|
+
TimeoutError,
|
|
13
|
+
ValidationError,
|
|
14
|
+
)
|
|
15
|
+
|
|
16
|
+
__all__ = [
|
|
17
|
+
"AsyncCadlagClient",
|
|
18
|
+
"BacktestFailedError",
|
|
19
|
+
"ChainLimitExceededError",
|
|
20
|
+
"ClientConfig",
|
|
21
|
+
"ConnectionError",
|
|
22
|
+
"NotFoundError",
|
|
23
|
+
"PipelineFailedError",
|
|
24
|
+
"ProFeatureRequiredError",
|
|
25
|
+
"CadlagClient",
|
|
26
|
+
"CadlagError",
|
|
27
|
+
"TimeoutError",
|
|
28
|
+
"ValidationError",
|
|
29
|
+
]
|
|
@@ -0,0 +1,213 @@
|
|
|
1
|
+
# Copyright (c) 2026 Cadlag. Licensed under Apache 2.0.
|
|
2
|
+
"""HTTP transport layers with shared retry and error mapping.
|
|
3
|
+
|
|
4
|
+
Provides sync and async transports that share timeout construction and
|
|
5
|
+
platform HTTP status-code → SDK exception mapping semantics.
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
from __future__ import annotations
|
|
9
|
+
|
|
10
|
+
import asyncio
|
|
11
|
+
import time
|
|
12
|
+
from typing import TYPE_CHECKING, Any
|
|
13
|
+
|
|
14
|
+
import httpx
|
|
15
|
+
import structlog
|
|
16
|
+
|
|
17
|
+
if TYPE_CHECKING:
|
|
18
|
+
from cadlag_sdk.client.config import ClientConfig
|
|
19
|
+
|
|
20
|
+
from cadlag_sdk.client.exceptions import (
|
|
21
|
+
ChainLimitExceededError,
|
|
22
|
+
ConnectionError,
|
|
23
|
+
NotFoundError,
|
|
24
|
+
ProFeatureRequiredError,
|
|
25
|
+
CadlagError,
|
|
26
|
+
TimeoutError,
|
|
27
|
+
ValidationError,
|
|
28
|
+
)
|
|
29
|
+
|
|
30
|
+
logger = structlog.get_logger(__name__)
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
def _build_timeout(config: ClientConfig) -> httpx.Timeout:
|
|
34
|
+
return httpx.Timeout(
|
|
35
|
+
connect=config.connect_timeout,
|
|
36
|
+
read=config.read_timeout,
|
|
37
|
+
write=config.write_timeout,
|
|
38
|
+
pool=config.connect_timeout,
|
|
39
|
+
)
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
class _BaseTransport:
|
|
43
|
+
"""Shared transport behavior for sync and async SDK clients.
|
|
44
|
+
|
|
45
|
+
Args:
|
|
46
|
+
config: :class:`~cadlag_sdk.client.config.ClientConfig` instance.
|
|
47
|
+
"""
|
|
48
|
+
|
|
49
|
+
def __init__(self, config: ClientConfig) -> None:
|
|
50
|
+
self._config = config
|
|
51
|
+
|
|
52
|
+
def _map_error(self, response: httpx.Response) -> httpx.Response:
|
|
53
|
+
"""Raise a typed exception for error status codes."""
|
|
54
|
+
if response.is_success:
|
|
55
|
+
return response
|
|
56
|
+
|
|
57
|
+
body: Any = None
|
|
58
|
+
try:
|
|
59
|
+
body = response.json()
|
|
60
|
+
except Exception:
|
|
61
|
+
body = response.text
|
|
62
|
+
|
|
63
|
+
status = response.status_code
|
|
64
|
+
if status == 402:
|
|
65
|
+
capability = body.get("capability", "unknown") if isinstance(body, dict) else "unknown"
|
|
66
|
+
docs_url_value = body.get("docs_url", body.get("docs", "")) if isinstance(body, dict) else ""
|
|
67
|
+
docs_url = docs_url_value if isinstance(docs_url_value, str) else ""
|
|
68
|
+
message = body.get("error", "Pro feature required") if isinstance(body, dict) else str(body)
|
|
69
|
+
raise ProFeatureRequiredError(
|
|
70
|
+
message,
|
|
71
|
+
status_code=status,
|
|
72
|
+
response=body,
|
|
73
|
+
capability=capability,
|
|
74
|
+
docs_url=docs_url,
|
|
75
|
+
)
|
|
76
|
+
if status == 404:
|
|
77
|
+
raise NotFoundError("Resource not found", status_code=status, response=body)
|
|
78
|
+
if status == 422:
|
|
79
|
+
if (
|
|
80
|
+
isinstance(body, dict)
|
|
81
|
+
and body.get("error") == "chain limit exceeded"
|
|
82
|
+
and isinstance(body.get("max_chains"), int)
|
|
83
|
+
and isinstance(body.get("requested"), int)
|
|
84
|
+
):
|
|
85
|
+
raise ChainLimitExceededError(
|
|
86
|
+
body.get("detail", "Chain limit exceeded"),
|
|
87
|
+
status_code=status,
|
|
88
|
+
response=body,
|
|
89
|
+
max_chains=body["max_chains"],
|
|
90
|
+
requested=body["requested"],
|
|
91
|
+
)
|
|
92
|
+
raise ValidationError("Validation error", status_code=status, response=body)
|
|
93
|
+
|
|
94
|
+
raise CadlagError(f"Unexpected HTTP {status}", status_code=status, response=body)
|
|
95
|
+
|
|
96
|
+
|
|
97
|
+
class SyncTransport(_BaseTransport):
|
|
98
|
+
"""Sync HTTP transport with retry + status-code → exception mapping."""
|
|
99
|
+
|
|
100
|
+
def __init__(self, config: ClientConfig) -> None:
|
|
101
|
+
super().__init__(config)
|
|
102
|
+
self._client: httpx.Client | None = None
|
|
103
|
+
|
|
104
|
+
def _get_client(self) -> httpx.Client:
|
|
105
|
+
if self._client is None or getattr(self._client, "is_closed", False):
|
|
106
|
+
self._client = httpx.Client(
|
|
107
|
+
base_url=self._config.base_url,
|
|
108
|
+
timeout=_build_timeout(self._config),
|
|
109
|
+
)
|
|
110
|
+
return self._client
|
|
111
|
+
|
|
112
|
+
def request(
|
|
113
|
+
self,
|
|
114
|
+
method: str,
|
|
115
|
+
path: str,
|
|
116
|
+
**kwargs: Any,
|
|
117
|
+
) -> httpx.Response:
|
|
118
|
+
"""Send a synchronous HTTP request with retry and backoff."""
|
|
119
|
+
client = self._get_client()
|
|
120
|
+
last_exc: Exception | None = None
|
|
121
|
+
|
|
122
|
+
for attempt in range(self._config.max_retries + 1):
|
|
123
|
+
if attempt > 0:
|
|
124
|
+
delay = self._config.backoff_factor * (2 ** (attempt - 1))
|
|
125
|
+
time.sleep(delay)
|
|
126
|
+
|
|
127
|
+
try:
|
|
128
|
+
response = client.request(method, path, **kwargs)
|
|
129
|
+
return self._map_error(response)
|
|
130
|
+
except httpx.TimeoutException as exc:
|
|
131
|
+
last_exc = exc
|
|
132
|
+
logger.warning("request_timeout", attempt=attempt, path=path)
|
|
133
|
+
except httpx.NetworkError as exc:
|
|
134
|
+
last_exc = exc
|
|
135
|
+
logger.warning("request_network_error", attempt=attempt, path=path)
|
|
136
|
+
|
|
137
|
+
if isinstance(last_exc, httpx.TimeoutException):
|
|
138
|
+
raise TimeoutError(str(last_exc)) from last_exc
|
|
139
|
+
raise ConnectionError(str(last_exc)) from last_exc
|
|
140
|
+
|
|
141
|
+
def close(self) -> None:
|
|
142
|
+
"""Close the underlying synchronous client."""
|
|
143
|
+
if self._client and not getattr(self._client, "is_closed", False):
|
|
144
|
+
self._client.close()
|
|
145
|
+
|
|
146
|
+
|
|
147
|
+
class AsyncTransport(_BaseTransport):
|
|
148
|
+
"""Async HTTP transport with retry + status-code → exception mapping."""
|
|
149
|
+
|
|
150
|
+
def __init__(self, config: ClientConfig) -> None:
|
|
151
|
+
super().__init__(config)
|
|
152
|
+
self._client: httpx.AsyncClient | None = None
|
|
153
|
+
|
|
154
|
+
async def _get_client(self) -> httpx.AsyncClient:
|
|
155
|
+
if self._client is None or self._client.is_closed:
|
|
156
|
+
self._client = httpx.AsyncClient(
|
|
157
|
+
base_url=self._config.base_url,
|
|
158
|
+
timeout=_build_timeout(self._config),
|
|
159
|
+
)
|
|
160
|
+
return self._client
|
|
161
|
+
|
|
162
|
+
async def request(
|
|
163
|
+
self,
|
|
164
|
+
method: str,
|
|
165
|
+
path: str,
|
|
166
|
+
**kwargs: Any,
|
|
167
|
+
) -> httpx.Response:
|
|
168
|
+
"""Send an HTTP request with retry and backoff.
|
|
169
|
+
|
|
170
|
+
Args:
|
|
171
|
+
method: HTTP verb (``"GET"``, ``"POST"``, etc.).
|
|
172
|
+
path: URL path relative to ``base_url``.
|
|
173
|
+
**kwargs: Forwarded to :meth:`httpx.AsyncClient.request`.
|
|
174
|
+
|
|
175
|
+
Returns:
|
|
176
|
+
The :class:`httpx.Response` on success.
|
|
177
|
+
|
|
178
|
+
Raises:
|
|
179
|
+
ConnectionError: Network-level failure.
|
|
180
|
+
TimeoutError: Request exceeded configured timeout.
|
|
181
|
+
Various :class:`CadlagError` subclasses on HTTP error statuses.
|
|
182
|
+
"""
|
|
183
|
+
client = await self._get_client()
|
|
184
|
+
last_exc: Exception | None = None
|
|
185
|
+
|
|
186
|
+
for attempt in range(self._config.max_retries + 1):
|
|
187
|
+
if attempt > 0:
|
|
188
|
+
delay = self._config.backoff_factor * (2 ** (attempt - 1))
|
|
189
|
+
await asyncio.sleep(delay)
|
|
190
|
+
|
|
191
|
+
try:
|
|
192
|
+
response = await client.request(method, path, **kwargs)
|
|
193
|
+
return self._map_error(response)
|
|
194
|
+
except httpx.TimeoutException as exc:
|
|
195
|
+
last_exc = exc
|
|
196
|
+
logger.warning("request_timeout", attempt=attempt, path=path)
|
|
197
|
+
except httpx.NetworkError as exc:
|
|
198
|
+
last_exc = exc
|
|
199
|
+
logger.warning("request_network_error", attempt=attempt, path=path)
|
|
200
|
+
|
|
201
|
+
# All retries exhausted
|
|
202
|
+
if isinstance(last_exc, httpx.TimeoutException):
|
|
203
|
+
raise TimeoutError(str(last_exc)) from last_exc
|
|
204
|
+
raise ConnectionError(str(last_exc)) from last_exc
|
|
205
|
+
|
|
206
|
+
async def aclose(self) -> None:
|
|
207
|
+
"""Close the underlying ``httpx.AsyncClient``."""
|
|
208
|
+
if self._client and not self._client.is_closed:
|
|
209
|
+
await self._client.aclose()
|
|
210
|
+
|
|
211
|
+
|
|
212
|
+
# Backward-compatible alias for the original async transport symbol.
|
|
213
|
+
Transport = AsyncTransport
|