latitude-sdk 0.1.0b1__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.
- latitude_sdk-0.1.0b1/.gitignore +141 -0
- latitude_sdk-0.1.0b1/.python-version +1 -0
- latitude_sdk-0.1.0b1/PKG-INFO +63 -0
- latitude_sdk-0.1.0b1/README.md +46 -0
- latitude_sdk-0.1.0b1/examples/all.py +120 -0
- latitude_sdk-0.1.0b1/pyproject.toml +64 -0
- latitude_sdk-0.1.0b1/scripts/format.py +8 -0
- latitude_sdk-0.1.0b1/scripts/lint.py +9 -0
- latitude_sdk-0.1.0b1/scripts/test.py +7 -0
- latitude_sdk-0.1.0b1/src/latitude_sdk/__init__.py +1 -0
- latitude_sdk-0.1.0b1/src/latitude_sdk/client/__init__.py +3 -0
- latitude_sdk-0.1.0b1/src/latitude_sdk/client/client.py +140 -0
- latitude_sdk-0.1.0b1/src/latitude_sdk/client/payloads.py +117 -0
- latitude_sdk-0.1.0b1/src/latitude_sdk/client/router.py +117 -0
- latitude_sdk-0.1.0b1/src/latitude_sdk/env/__init__.py +1 -0
- latitude_sdk-0.1.0b1/src/latitude_sdk/env/env.py +18 -0
- latitude_sdk-0.1.0b1/src/latitude_sdk/py.typed +0 -0
- latitude_sdk-0.1.0b1/src/latitude_sdk/sdk/__init__.py +6 -0
- latitude_sdk-0.1.0b1/src/latitude_sdk/sdk/errors.py +59 -0
- latitude_sdk-0.1.0b1/src/latitude_sdk/sdk/evaluations.py +69 -0
- latitude_sdk-0.1.0b1/src/latitude_sdk/sdk/latitude.py +76 -0
- latitude_sdk-0.1.0b1/src/latitude_sdk/sdk/logs.py +66 -0
- latitude_sdk-0.1.0b1/src/latitude_sdk/sdk/prompts.py +297 -0
- latitude_sdk-0.1.0b1/src/latitude_sdk/sdk/types.py +296 -0
- latitude_sdk-0.1.0b1/src/latitude_sdk/util/__init__.py +1 -0
- latitude_sdk-0.1.0b1/src/latitude_sdk/util/utils.py +87 -0
- latitude_sdk-0.1.0b1/tests/prompts/get_test.py +32 -0
- latitude_sdk-0.1.0b1/uv.lock +527 -0
@@ -0,0 +1,141 @@
|
|
1
|
+
# Custom
|
2
|
+
.vscode/
|
3
|
+
.superinvoke_cache/
|
4
|
+
.idea/
|
5
|
+
*.tmlanguage.cache
|
6
|
+
*.tmPreferences.cache
|
7
|
+
*.stTheme.cache
|
8
|
+
*.sublime-workspace
|
9
|
+
.DS_Store
|
10
|
+
*.env
|
11
|
+
|
12
|
+
# Byte-compiled / optimized / DLL files
|
13
|
+
__pycache__/
|
14
|
+
*.py[cod]
|
15
|
+
*$py.class
|
16
|
+
|
17
|
+
# C extensions
|
18
|
+
*.so
|
19
|
+
|
20
|
+
# Distribution / packaging
|
21
|
+
.Python
|
22
|
+
build/
|
23
|
+
develop-eggs/
|
24
|
+
dist/
|
25
|
+
downloads/
|
26
|
+
eggs/
|
27
|
+
.eggs/
|
28
|
+
lib/
|
29
|
+
lib64/
|
30
|
+
parts/
|
31
|
+
sdist/
|
32
|
+
var/
|
33
|
+
wheels/
|
34
|
+
share/python-wheels/
|
35
|
+
*.egg-info/
|
36
|
+
.installed.cfg
|
37
|
+
*.egg
|
38
|
+
MANIFEST
|
39
|
+
|
40
|
+
# PyInstaller
|
41
|
+
*.manifest
|
42
|
+
*.spec
|
43
|
+
|
44
|
+
# Installer logs
|
45
|
+
pip-log.txt
|
46
|
+
pip-delete-this-directory.txt
|
47
|
+
|
48
|
+
# Unit test / coverage reports
|
49
|
+
htmlcov/
|
50
|
+
.tox/
|
51
|
+
.nox/
|
52
|
+
.coverage
|
53
|
+
.coverage.*
|
54
|
+
.cache
|
55
|
+
nosetests.xml
|
56
|
+
coverage.xml
|
57
|
+
*.cover
|
58
|
+
*.py,cover
|
59
|
+
.hypothesis/
|
60
|
+
.pytest_cache/
|
61
|
+
cover/
|
62
|
+
|
63
|
+
# Translations
|
64
|
+
*.mo
|
65
|
+
*.pot
|
66
|
+
|
67
|
+
# Django stuff:
|
68
|
+
*.log
|
69
|
+
local_settings.py
|
70
|
+
db.sqlite3
|
71
|
+
db.sqlite3-journal
|
72
|
+
|
73
|
+
# Flask stuff:
|
74
|
+
instance/
|
75
|
+
.webassets-cache
|
76
|
+
|
77
|
+
# Scrapy stuff:
|
78
|
+
.scrapy
|
79
|
+
|
80
|
+
# Sphinx documentation
|
81
|
+
docs/_build/
|
82
|
+
|
83
|
+
# PyBuilder
|
84
|
+
.pybuilder/
|
85
|
+
target/
|
86
|
+
|
87
|
+
# Jupyter Notebook
|
88
|
+
.ipynb_checkpoints
|
89
|
+
|
90
|
+
# IPython
|
91
|
+
profile_default/
|
92
|
+
ipython_config.py
|
93
|
+
|
94
|
+
# pyenv
|
95
|
+
-
|
96
|
+
|
97
|
+
# pdm
|
98
|
+
.pdm.toml
|
99
|
+
|
100
|
+
# PEP 582
|
101
|
+
__pypackages__/
|
102
|
+
|
103
|
+
# Celery stuff
|
104
|
+
celerybeat-schedule
|
105
|
+
celerybeat.pid
|
106
|
+
|
107
|
+
# SageMath parsed files
|
108
|
+
*.sage.py
|
109
|
+
|
110
|
+
# Environments
|
111
|
+
.env
|
112
|
+
.venv
|
113
|
+
venv/
|
114
|
+
venv.bak/
|
115
|
+
|
116
|
+
# Spyder project settings
|
117
|
+
.spyderproject
|
118
|
+
.spyproject
|
119
|
+
|
120
|
+
# Rope project settings
|
121
|
+
.ropeproject
|
122
|
+
|
123
|
+
# mkdocs documentation
|
124
|
+
/site
|
125
|
+
|
126
|
+
# mypy
|
127
|
+
.mypy_cache/
|
128
|
+
.dmypy.json
|
129
|
+
dmypy.json
|
130
|
+
|
131
|
+
# Pyre type checker
|
132
|
+
.pyre/
|
133
|
+
|
134
|
+
# pytype static type analyzer
|
135
|
+
.pytype/
|
136
|
+
|
137
|
+
# Cython debug symbols
|
138
|
+
cython_debug/
|
139
|
+
|
140
|
+
# Ruff
|
141
|
+
.ruff_cache/
|
@@ -0,0 +1 @@
|
|
1
|
+
3.8.20
|
@@ -0,0 +1,63 @@
|
|
1
|
+
Metadata-Version: 2.4
|
2
|
+
Name: latitude-sdk
|
3
|
+
Version: 0.1.0b1
|
4
|
+
Summary: Latitude SDK for Python
|
5
|
+
Project-URL: repository, https://github.com/latitude-dev/latitude-llm/tree/main/packages/sdks/python
|
6
|
+
Project-URL: homepage, https://github.com/latitude-dev/latitude-llm/tree/main/packages/sdks/python#readme
|
7
|
+
Project-URL: documentation, https://github.com/latitude-dev/latitude-llm/tree/main/packages/sdks/python#readme
|
8
|
+
Author-email: Latitude Data SL <hello@latitude.so>
|
9
|
+
Maintainer-email: Latitude Data SL <hello@latitude.so>
|
10
|
+
License-Expression: LGPL-3.0
|
11
|
+
Requires-Python: >=3.8
|
12
|
+
Requires-Dist: httpx-sse>=0.4.0
|
13
|
+
Requires-Dist: httpx>=0.28.1
|
14
|
+
Requires-Dist: pydantic>=2.10.3
|
15
|
+
Requires-Dist: typing-extensions>=4.12.2
|
16
|
+
Description-Content-Type: text/markdown
|
17
|
+
|
18
|
+
# Latitude SDK for Python
|
19
|
+
|
20
|
+
```sh
|
21
|
+
pip install latitude-sdk
|
22
|
+
```
|
23
|
+
|
24
|
+
Requires Python `3.8` or higher.
|
25
|
+
|
26
|
+
Go to the [documentation](https://docs.latitude.so/guides/sdk/python) to learn more.
|
27
|
+
|
28
|
+
## Usage
|
29
|
+
|
30
|
+
```python
|
31
|
+
from latitude_sdk import Latitude, LatitudeOptions, RunPromptOptions
|
32
|
+
|
33
|
+
sdk = Latitude("my-api-key", LatitudeOptions(
|
34
|
+
project_id="my-project-id",
|
35
|
+
version_uuid="my-version-uuid",
|
36
|
+
))
|
37
|
+
|
38
|
+
await sdk.prompts.run("joke-teller", RunPromptOptions(
|
39
|
+
parameters={"topic": "Python"},
|
40
|
+
on_event=lambda event: print(event),
|
41
|
+
on_finished=lambda event: print(event),
|
42
|
+
on_error=lambda error: print(error),
|
43
|
+
stream=True,
|
44
|
+
))
|
45
|
+
```
|
46
|
+
|
47
|
+
Find more examples [here](https://github.com/latitude-dev/latitude-llm/tree/main/examples/sdks/python).
|
48
|
+
|
49
|
+
## Development
|
50
|
+
|
51
|
+
Requires uv `0.5.10` or higher.
|
52
|
+
|
53
|
+
- Install dependencies: `uv venv && uv sync --all-extras --all-groups`
|
54
|
+
- Add [dev] dependencies: `uv add [--dev] <package>`
|
55
|
+
- Run linter: `uv run scripts/lint.py`
|
56
|
+
- Run formatter: `uv run scripts/format.py`
|
57
|
+
- Run tests: `uv run scripts/test.py`
|
58
|
+
- Build package: `uv build`
|
59
|
+
- Publish package: `uv publish`
|
60
|
+
|
61
|
+
## License
|
62
|
+
|
63
|
+
The SDK is licensed under the [LGPL-3.0 License](https://opensource.org/licenses/LGPL-3.0) - read the [LICENSE](/LICENSE) file for details.
|
@@ -0,0 +1,46 @@
|
|
1
|
+
# Latitude SDK for Python
|
2
|
+
|
3
|
+
```sh
|
4
|
+
pip install latitude-sdk
|
5
|
+
```
|
6
|
+
|
7
|
+
Requires Python `3.8` or higher.
|
8
|
+
|
9
|
+
Go to the [documentation](https://docs.latitude.so/guides/sdk/python) to learn more.
|
10
|
+
|
11
|
+
## Usage
|
12
|
+
|
13
|
+
```python
|
14
|
+
from latitude_sdk import Latitude, LatitudeOptions, RunPromptOptions
|
15
|
+
|
16
|
+
sdk = Latitude("my-api-key", LatitudeOptions(
|
17
|
+
project_id="my-project-id",
|
18
|
+
version_uuid="my-version-uuid",
|
19
|
+
))
|
20
|
+
|
21
|
+
await sdk.prompts.run("joke-teller", RunPromptOptions(
|
22
|
+
parameters={"topic": "Python"},
|
23
|
+
on_event=lambda event: print(event),
|
24
|
+
on_finished=lambda event: print(event),
|
25
|
+
on_error=lambda error: print(error),
|
26
|
+
stream=True,
|
27
|
+
))
|
28
|
+
```
|
29
|
+
|
30
|
+
Find more examples [here](https://github.com/latitude-dev/latitude-llm/tree/main/examples/sdks/python).
|
31
|
+
|
32
|
+
## Development
|
33
|
+
|
34
|
+
Requires uv `0.5.10` or higher.
|
35
|
+
|
36
|
+
- Install dependencies: `uv venv && uv sync --all-extras --all-groups`
|
37
|
+
- Add [dev] dependencies: `uv add [--dev] <package>`
|
38
|
+
- Run linter: `uv run scripts/lint.py`
|
39
|
+
- Run formatter: `uv run scripts/format.py`
|
40
|
+
- Run tests: `uv run scripts/test.py`
|
41
|
+
- Build package: `uv build`
|
42
|
+
- Publish package: `uv publish`
|
43
|
+
|
44
|
+
## License
|
45
|
+
|
46
|
+
The SDK is licensed under the [LGPL-3.0 License](https://opensource.org/licenses/LGPL-3.0) - read the [LICENSE](/LICENSE) file for details.
|
@@ -0,0 +1,120 @@
|
|
1
|
+
import asyncio
|
2
|
+
from pprint import pp # type: ignore
|
3
|
+
|
4
|
+
from latitude_sdk import (
|
5
|
+
ApiError,
|
6
|
+
ChatPromptOptions,
|
7
|
+
CreateEvaluationResultOptions,
|
8
|
+
CreateLogOptions,
|
9
|
+
GatewayOptions,
|
10
|
+
GetOrCreatePromptOptions,
|
11
|
+
GetPromptOptions,
|
12
|
+
InternalOptions,
|
13
|
+
Latitude,
|
14
|
+
LatitudeOptions,
|
15
|
+
RunPromptOptions,
|
16
|
+
TextContent,
|
17
|
+
TriggerEvaluationOptions,
|
18
|
+
UserMessage,
|
19
|
+
)
|
20
|
+
|
21
|
+
|
22
|
+
# TODO Move to root/examples when latitude-sdk is published
|
23
|
+
async def main():
|
24
|
+
sdk = Latitude(
|
25
|
+
api_key="6f67407c-da6c-4a4d-9615-a3eb59e51d29",
|
26
|
+
options=LatitudeOptions(
|
27
|
+
project_id=3,
|
28
|
+
version_uuid="57502e00-20c2-4411-8b4b-44bc9008079e",
|
29
|
+
internal=InternalOptions(gateway=GatewayOptions(host="localhost", port=8787, ssl=False, api_version="v2")),
|
30
|
+
),
|
31
|
+
)
|
32
|
+
|
33
|
+
try:
|
34
|
+
print("Getting prompt...")
|
35
|
+
get_prompt_result = await sdk.prompts.get("prompt", GetPromptOptions())
|
36
|
+
pp(get_prompt_result.model_dump())
|
37
|
+
print("\n" * 2)
|
38
|
+
print("-" * 100)
|
39
|
+
|
40
|
+
print("Getting or creating prompt...")
|
41
|
+
get_or_create_prompt_result = await sdk.prompts.get_or_create("prompt3", GetOrCreatePromptOptions())
|
42
|
+
pp(get_or_create_prompt_result.model_dump())
|
43
|
+
print("\n" * 2)
|
44
|
+
print("-" * 100)
|
45
|
+
|
46
|
+
print("Running prompt...")
|
47
|
+
run_prompt_result = await sdk.prompts.run(
|
48
|
+
"prompt",
|
49
|
+
RunPromptOptions(
|
50
|
+
on_event=lambda event: print(event, "\n" * 2),
|
51
|
+
on_finished=lambda event: print(event, "\n" * 2),
|
52
|
+
on_error=lambda error: print(error, "\n" * 2),
|
53
|
+
custom_identifier="custom!",
|
54
|
+
parameters={"topic": "Python"},
|
55
|
+
stream=True,
|
56
|
+
),
|
57
|
+
)
|
58
|
+
assert run_prompt_result is not None
|
59
|
+
pp(run_prompt_result.model_dump())
|
60
|
+
print("\n" * 2)
|
61
|
+
print("-" * 100)
|
62
|
+
|
63
|
+
print("Chat prompt...")
|
64
|
+
chat_prompt_result = await sdk.prompts.chat(
|
65
|
+
run_prompt_result.uuid,
|
66
|
+
[
|
67
|
+
UserMessage(content=[TextContent(text="Hello, how are you?")]),
|
68
|
+
UserMessage(content="I'm fine btw"),
|
69
|
+
],
|
70
|
+
ChatPromptOptions(
|
71
|
+
on_event=lambda event: print(event, "\n" * 2),
|
72
|
+
on_finished=lambda event: print(event, "\n" * 2),
|
73
|
+
on_error=lambda error: print(error, "\n" * 2),
|
74
|
+
stream=True,
|
75
|
+
),
|
76
|
+
)
|
77
|
+
assert chat_prompt_result is not None
|
78
|
+
pp(chat_prompt_result.model_dump())
|
79
|
+
print("\n" * 2)
|
80
|
+
print("-" * 100)
|
81
|
+
|
82
|
+
print("Create log...")
|
83
|
+
create_log_result = await sdk.logs.create(
|
84
|
+
"prompt",
|
85
|
+
[
|
86
|
+
UserMessage(content=[TextContent(text="Hello, how are you?")]),
|
87
|
+
UserMessage(content=[TextContent(text="I'm fine btw")]),
|
88
|
+
],
|
89
|
+
CreateLogOptions(),
|
90
|
+
)
|
91
|
+
pp(create_log_result.model_dump())
|
92
|
+
print("\n" * 2)
|
93
|
+
print("-" * 100)
|
94
|
+
|
95
|
+
print("Trigger evaluation...")
|
96
|
+
trigger_evaluation_result = await sdk.evaluations.trigger(
|
97
|
+
chat_prompt_result.uuid,
|
98
|
+
TriggerEvaluationOptions(evaluation_uuids=["46d29f2d-7086-44b8-9220-af1dea1e3692"]),
|
99
|
+
)
|
100
|
+
pp(trigger_evaluation_result.model_dump())
|
101
|
+
print("\n" * 2)
|
102
|
+
print("-" * 100)
|
103
|
+
|
104
|
+
print("Create evaluation result...")
|
105
|
+
create_evaluation_result_result = await sdk.evaluations.create_result(
|
106
|
+
chat_prompt_result.uuid,
|
107
|
+
"d7a04129-9df8-4047-ba93-6349029a1000",
|
108
|
+
CreateEvaluationResultOptions(result="I like it!", reason="Because I like it!"),
|
109
|
+
)
|
110
|
+
pp(create_evaluation_result_result.model_dump())
|
111
|
+
print("\n" * 2)
|
112
|
+
print("-" * 100)
|
113
|
+
|
114
|
+
except ApiError as error:
|
115
|
+
pp(error.__dict__)
|
116
|
+
except Exception as e:
|
117
|
+
raise e
|
118
|
+
|
119
|
+
|
120
|
+
asyncio.run(main())
|
@@ -0,0 +1,64 @@
|
|
1
|
+
[project]
|
2
|
+
name = "latitude-sdk"
|
3
|
+
version = "0.1.0-beta.1"
|
4
|
+
description = "Latitude SDK for Python"
|
5
|
+
authors = [{ name = "Latitude Data SL", email = "hello@latitude.so" }]
|
6
|
+
maintainers = [{ name = "Latitude Data SL", email = "hello@latitude.so" }]
|
7
|
+
readme = "README.md"
|
8
|
+
license = "LGPL-3.0"
|
9
|
+
urls.repository = "https://github.com/latitude-dev/latitude-llm/tree/main/packages/sdks/python"
|
10
|
+
urls.homepage = "https://github.com/latitude-dev/latitude-llm/tree/main/packages/sdks/python#readme"
|
11
|
+
urls.documentation = "https://github.com/latitude-dev/latitude-llm/tree/main/packages/sdks/python#readme"
|
12
|
+
requires-python = ">=3.8"
|
13
|
+
dependencies = [
|
14
|
+
"httpx>=0.28.1",
|
15
|
+
"httpx-sse>=0.4.0",
|
16
|
+
"pydantic>=2.10.3",
|
17
|
+
"typing-extensions>=4.12.2",
|
18
|
+
]
|
19
|
+
|
20
|
+
[dependency-groups]
|
21
|
+
dev = [
|
22
|
+
"pytest-asyncio>=0.24.0",
|
23
|
+
"pytest-xdist>=3.6.1",
|
24
|
+
"pytest>=8.3.4",
|
25
|
+
"pyright>=1.1.391",
|
26
|
+
"ruff>=0.8.3",
|
27
|
+
"sh>=1.14.3",
|
28
|
+
]
|
29
|
+
|
30
|
+
[tool.pyright]
|
31
|
+
pythonVersion = "3.8"
|
32
|
+
typeCheckingMode = "strict"
|
33
|
+
reportMissingTypeStubs = false
|
34
|
+
reportUnnecessaryIsInstance = false
|
35
|
+
reportPrivateUsage = false
|
36
|
+
|
37
|
+
[tool.ruff]
|
38
|
+
target-version = "py38"
|
39
|
+
line-length = 120
|
40
|
+
indent-width = 4
|
41
|
+
|
42
|
+
[tool.ruff.lint]
|
43
|
+
select = ["B", "C4", "E", "F", "I", "W", "UP"]
|
44
|
+
ignore = [
|
45
|
+
"F401",
|
46
|
+
"F403",
|
47
|
+
# Needed because unnecessary str() on field aliases are needed
|
48
|
+
# https://docs.pydantic.dev/2.8/concepts/fields/#field-aliases
|
49
|
+
"UP018",
|
50
|
+
]
|
51
|
+
|
52
|
+
[tool.ruff.format]
|
53
|
+
quote-style = "double"
|
54
|
+
indent-style = "space"
|
55
|
+
|
56
|
+
[tool.pytest.ini_options]
|
57
|
+
addopts = "-p no:warnings -n auto"
|
58
|
+
xfail_strict = true
|
59
|
+
asyncio_mode = "auto"
|
60
|
+
asyncio_default_fixture_loop_scope = "function"
|
61
|
+
|
62
|
+
[build-system]
|
63
|
+
requires = ["hatchling"]
|
64
|
+
build-backend = "hatchling.build"
|
@@ -0,0 +1 @@
|
|
1
|
+
from .sdk import *
|
@@ -0,0 +1,140 @@
|
|
1
|
+
import asyncio
|
2
|
+
import json
|
3
|
+
from contextlib import asynccontextmanager
|
4
|
+
from typing import Any, AsyncGenerator, Optional
|
5
|
+
|
6
|
+
import httpx
|
7
|
+
import httpx_sse
|
8
|
+
|
9
|
+
from latitude_sdk.client.payloads import ErrorResponse, RequestBody, RequestHandler, RequestParams
|
10
|
+
from latitude_sdk.client.router import Router, RouterOptions
|
11
|
+
from latitude_sdk.sdk.errors import (
|
12
|
+
ApiError,
|
13
|
+
ApiErrorCodes,
|
14
|
+
ApiErrorDbRef,
|
15
|
+
)
|
16
|
+
from latitude_sdk.sdk.types import LogSources
|
17
|
+
from latitude_sdk.util import Model
|
18
|
+
|
19
|
+
RETRIABLE_STATUSES = [429, 500, 502, 503, 504]
|
20
|
+
|
21
|
+
ClientEvent = httpx_sse.ServerSentEvent
|
22
|
+
|
23
|
+
|
24
|
+
class ClientResponse(httpx.Response):
|
25
|
+
async def sse(self: httpx.Response) -> AsyncGenerator[ClientEvent, Any]:
|
26
|
+
source = httpx_sse.EventSource(self)
|
27
|
+
|
28
|
+
async for event in source.aiter_sse():
|
29
|
+
yield event
|
30
|
+
|
31
|
+
|
32
|
+
httpx.Response.sse = ClientResponse.sse # pyright: ignore [reportAttributeAccessIssue]
|
33
|
+
|
34
|
+
|
35
|
+
class ClientOptions(Model):
|
36
|
+
api_key: str
|
37
|
+
retries: int
|
38
|
+
delay: float
|
39
|
+
timeout: float
|
40
|
+
source: LogSources
|
41
|
+
router: RouterOptions
|
42
|
+
|
43
|
+
|
44
|
+
class Client:
|
45
|
+
options: ClientOptions
|
46
|
+
router: Router
|
47
|
+
|
48
|
+
def __init__(self, options: ClientOptions):
|
49
|
+
self.options = options
|
50
|
+
self.router = Router(options.router)
|
51
|
+
|
52
|
+
@asynccontextmanager
|
53
|
+
async def request(
|
54
|
+
self, handler: RequestHandler, params: RequestParams, body: Optional[RequestBody] = None
|
55
|
+
) -> AsyncGenerator[ClientResponse, Any]:
|
56
|
+
client = httpx.AsyncClient(
|
57
|
+
headers={
|
58
|
+
"Authorization": f"Bearer {self.options.api_key}",
|
59
|
+
"Content-Type": "application/json",
|
60
|
+
},
|
61
|
+
timeout=self.options.timeout,
|
62
|
+
follow_redirects=False,
|
63
|
+
max_redirects=0,
|
64
|
+
)
|
65
|
+
response = None
|
66
|
+
attempt = 1
|
67
|
+
|
68
|
+
try:
|
69
|
+
method, url = self.router.resolve(handler, params)
|
70
|
+
content = None
|
71
|
+
if body:
|
72
|
+
content = json.dumps(
|
73
|
+
{
|
74
|
+
**json.loads(body.model_dump_json()),
|
75
|
+
"__internal": {"source": self.options.source},
|
76
|
+
}
|
77
|
+
)
|
78
|
+
|
79
|
+
while attempt <= self.options.retries:
|
80
|
+
try:
|
81
|
+
response = await client.request(method=method, url=url, content=content)
|
82
|
+
response.raise_for_status()
|
83
|
+
|
84
|
+
yield response # pyright: ignore [reportReturnType]
|
85
|
+
break
|
86
|
+
|
87
|
+
except Exception as exception:
|
88
|
+
if isinstance(exception, ApiError):
|
89
|
+
raise exception
|
90
|
+
|
91
|
+
if attempt >= self.options.retries:
|
92
|
+
raise await self._exception(exception, response) from exception
|
93
|
+
|
94
|
+
if response and response.status_code in RETRIABLE_STATUSES:
|
95
|
+
await asyncio.sleep(self.options.delay * (2 ** (attempt - 1)))
|
96
|
+
else:
|
97
|
+
raise await self._exception(exception, response) from exception
|
98
|
+
|
99
|
+
finally:
|
100
|
+
if response:
|
101
|
+
await response.aclose()
|
102
|
+
|
103
|
+
attempt += 1
|
104
|
+
|
105
|
+
except Exception as exception:
|
106
|
+
if isinstance(exception, ApiError):
|
107
|
+
raise exception
|
108
|
+
|
109
|
+
raise await self._exception(exception, response) from exception
|
110
|
+
|
111
|
+
finally:
|
112
|
+
await client.aclose()
|
113
|
+
|
114
|
+
async def _exception(self, exception: Exception, response: Optional[httpx.Response] = None) -> ApiError:
|
115
|
+
if not response:
|
116
|
+
return ApiError(
|
117
|
+
status=500,
|
118
|
+
code=ApiErrorCodes.InternalServerError,
|
119
|
+
message=str(exception),
|
120
|
+
response=str(exception),
|
121
|
+
)
|
122
|
+
|
123
|
+
try:
|
124
|
+
error = ErrorResponse.model_validate_json(response.content)
|
125
|
+
|
126
|
+
return ApiError(
|
127
|
+
status=response.status_code,
|
128
|
+
code=error.code,
|
129
|
+
message=error.message,
|
130
|
+
response=response.text,
|
131
|
+
db_ref=ApiErrorDbRef(**dict(error.db_ref)) if error.db_ref else None,
|
132
|
+
)
|
133
|
+
|
134
|
+
except Exception:
|
135
|
+
return ApiError(
|
136
|
+
status=response.status_code,
|
137
|
+
code=ApiErrorCodes.InternalServerError,
|
138
|
+
message=str(exception),
|
139
|
+
response=response.text,
|
140
|
+
)
|