marpledata 3.2.0__tar.gz → 3.2.2__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.
- marpledata-3.2.2/CHANGELOG.md +39 -0
- {marpledata-3.2.0 → marpledata-3.2.2}/LICENSE +1 -1
- {marpledata-3.2.0 → marpledata-3.2.2}/PKG-INFO +3 -1
- {marpledata-3.2.0 → marpledata-3.2.2}/README.md +2 -0
- {marpledata-3.2.0 → marpledata-3.2.2}/pyproject.toml +1 -1
- {marpledata-3.2.0 → marpledata-3.2.2}/src/marple/__init__.py +1 -1
- {marpledata-3.2.0 → marpledata-3.2.2}/src/marple/db/datastream.py +10 -2
- {marpledata-3.2.0 → marpledata-3.2.2}/src/marple/insight.py +38 -1
- marpledata-3.2.2/src/marple/profile.json +95 -0
- {marpledata-3.2.0 → marpledata-3.2.2}/src/marple/utils.py +52 -1
- marpledata-3.2.2/tests/test_reliability.py +102 -0
- {marpledata-3.2.0 → marpledata-3.2.2}/uv.lock +1 -1
- marpledata-3.2.0/.uv-cache/sdists-v9/.git +0 -0
- {marpledata-3.2.0 → marpledata-3.2.2}/.gitignore +0 -0
- {marpledata-3.2.0 → marpledata-3.2.2}/.python-version +0 -0
- {marpledata-3.2.0 → marpledata-3.2.2}/.uv-cache/.gitignore +0 -0
- {marpledata-3.2.0 → marpledata-3.2.2}/.uv-cache/.lock +0 -0
- {marpledata-3.2.0 → marpledata-3.2.2}/.uv-cache/CACHEDIR.TAG +0 -0
- {marpledata-3.2.0 → marpledata-3.2.2}/.uv-cache/interpreter-v4/c2c2931c8a99aae1/588e1e7c128e2872.msgpack +0 -0
- {marpledata-3.2.0 → marpledata-3.2.2}/.uv-cache/sdists-v9/.gitignore +0 -0
- {marpledata-3.2.0 → marpledata-3.2.2}/AGENTS.md +0 -0
- {marpledata-3.2.0 → marpledata-3.2.2}/CONTRIBUTING.md +0 -0
- {marpledata-3.2.0 → marpledata-3.2.2}/docs/DEPLOYMENT.md +0 -0
- {marpledata-3.2.0 → marpledata-3.2.2}/docs/_static/custom.css +0 -0
- {marpledata-3.2.0 → marpledata-3.2.2}/docs/_static/favicon.png +0 -0
- {marpledata-3.2.0 → marpledata-3.2.2}/docs/_static/logo.png +0 -0
- {marpledata-3.2.0 → marpledata-3.2.2}/docs/api/db.rst +0 -0
- {marpledata-3.2.0 → marpledata-3.2.2}/docs/api/insight.rst +0 -0
- {marpledata-3.2.0 → marpledata-3.2.2}/docs/api/marple.Insight.rst +0 -0
- {marpledata-3.2.0 → marpledata-3.2.2}/docs/api/marple.db.DB.rst +0 -0
- {marpledata-3.2.0 → marpledata-3.2.2}/docs/api/marple.db.DataStream.rst +0 -0
- {marpledata-3.2.0 → marpledata-3.2.2}/docs/api/marple.db.Dataset.rst +0 -0
- {marpledata-3.2.0 → marpledata-3.2.2}/docs/api/marple.db.DatasetList.rst +0 -0
- {marpledata-3.2.0 → marpledata-3.2.2}/docs/api/marple.db.Signal.rst +0 -0
- {marpledata-3.2.0 → marpledata-3.2.2}/docs/api.rst +0 -0
- {marpledata-3.2.0 → marpledata-3.2.2}/docs/conf.py +0 -0
- {marpledata-3.2.0 → marpledata-3.2.2}/docs/getting-started.rst +0 -0
- {marpledata-3.2.0 → marpledata-3.2.2}/docs/index.rst +0 -0
- {marpledata-3.2.0 → marpledata-3.2.2}/docs/tutorials.rst +0 -0
- {marpledata-3.2.0 → marpledata-3.2.2}/example.py +0 -0
- {marpledata-3.2.0 → marpledata-3.2.2}/examples_race.csv +0 -0
- {marpledata-3.2.0 → marpledata-3.2.2}/pytest.xml +0 -0
- {marpledata-3.2.0 → marpledata-3.2.2}/src/marple/db/__init__.py +0 -0
- {marpledata-3.2.0 → marpledata-3.2.2}/src/marple/db/constants.py +0 -0
- {marpledata-3.2.0 → marpledata-3.2.2}/src/marple/db/dataset.py +0 -0
- {marpledata-3.2.0 → marpledata-3.2.2}/src/marple/db/signal.py +0 -0
- {marpledata-3.2.0 → marpledata-3.2.2}/src/marple/py.typed +0 -0
- {marpledata-3.2.0 → marpledata-3.2.2}/tests/test_sdk.py +0 -0
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
# Changelog
|
|
2
|
+
|
|
3
|
+
All notable changes to the Python SDK package `marpledata` will be documented in this file.
|
|
4
|
+
|
|
5
|
+
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
|
|
6
|
+
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
|
|
7
|
+
|
|
8
|
+
## [3.2.2] - 2026-06-01
|
|
9
|
+
|
|
10
|
+
### Changed
|
|
11
|
+
|
|
12
|
+
- Added default request timeouts and bounded retries to improve SDK reliability on spotty networks.
|
|
13
|
+
- Retried idempotent direct-storage uploads while keeping Marple API POST retries conservative.
|
|
14
|
+
|
|
15
|
+
## [3.2.1] - 2026-05-22
|
|
16
|
+
|
|
17
|
+
### Changed
|
|
18
|
+
|
|
19
|
+
- Included the SDK version in the `X-Request-Source` header sent by the Marple Insight client, matching the Marple DB client.
|
|
20
|
+
|
|
21
|
+
## [3.2.0] - 2026-05-22
|
|
22
|
+
|
|
23
|
+
### Added
|
|
24
|
+
|
|
25
|
+
- Added the current Marple DB ingestion flow to `DataStream.push_file`.
|
|
26
|
+
- Added automatic upload mode handling for server upload, Azure direct upload, single presigned upload, and multipart upload.
|
|
27
|
+
- Added `concurrency` to control parallel direct-storage uploads.
|
|
28
|
+
- Added `upload_mode="server"` to force uploads through the Marple DB API server when direct storage URLs are blocked.
|
|
29
|
+
- Added `azure-storage-blob` as a runtime dependency for Azure-backed direct uploads.
|
|
30
|
+
|
|
31
|
+
### Changed
|
|
32
|
+
|
|
33
|
+
- Updated upload error handling to abort failed ingestions with a reason.
|
|
34
|
+
- Undeprecated `DB.delete_stream` and `DB.delete_dataset` convenience methods.
|
|
35
|
+
- Expanded README and Sphinx documentation for large-file uploads, forced server uploads, and integration test configuration.
|
|
36
|
+
|
|
37
|
+
### Notes
|
|
38
|
+
|
|
39
|
+
- `DB.push_file`, `DB.download_signal`, and `DB.update_metadata` remain deprecated compatibility paths. Prefer `DataStream`, `Dataset`, and `Signal` methods for new code.
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: marpledata
|
|
3
|
-
Version: 3.2.
|
|
3
|
+
Version: 3.2.2
|
|
4
4
|
Summary: Marple SDK for Python
|
|
5
5
|
Project-URL: Homepage, https://www.marpledata.com/
|
|
6
6
|
Project-URL: Documentation, https://marpledata.gitlab.io/marple-sdk/
|
|
@@ -51,6 +51,8 @@ from marple import DB # Marple DB
|
|
|
51
51
|
from marple import Insight # Marple Insight
|
|
52
52
|
```
|
|
53
53
|
|
|
54
|
+
For release notes, see [`CHANGELOG.md`](CHANGELOG.md).
|
|
55
|
+
|
|
54
56
|
## Marple DB
|
|
55
57
|
|
|
56
58
|
To get started:
|
|
@@ -153,7 +153,11 @@ class DataStream(BaseModel):
|
|
|
153
153
|
if init.presigned_url is None:
|
|
154
154
|
raise ValueError("Single upload mode without presigned_url")
|
|
155
155
|
with path.open("rb") as file:
|
|
156
|
-
response =
|
|
156
|
+
response = self._client.storage_session.put(
|
|
157
|
+
init.presigned_url,
|
|
158
|
+
data=file,
|
|
159
|
+
headers={"Content-Length": str(file_size)},
|
|
160
|
+
)
|
|
157
161
|
_validate_storage_response(response, "Storage PUT failed")
|
|
158
162
|
|
|
159
163
|
def _upload_multipart(
|
|
@@ -183,7 +187,11 @@ class DataStream(BaseModel):
|
|
|
183
187
|
file.seek(offset)
|
|
184
188
|
chunk = file.read(part_len)
|
|
185
189
|
|
|
186
|
-
response =
|
|
190
|
+
response = self._client.storage_session.put(
|
|
191
|
+
part.url,
|
|
192
|
+
data=chunk,
|
|
193
|
+
headers={"Content-Length": str(part_len)},
|
|
194
|
+
)
|
|
187
195
|
_validate_storage_response(response, f"Part {part.part_number} storage PUT failed")
|
|
188
196
|
|
|
189
197
|
with ThreadPoolExecutor(max_workers=concurrency) as executor:
|
|
@@ -2,6 +2,7 @@ from pathlib import Path
|
|
|
2
2
|
from typing import Optional
|
|
3
3
|
from urllib import request
|
|
4
4
|
|
|
5
|
+
import marple
|
|
5
6
|
import requests
|
|
6
7
|
from marple.utils import validate_response
|
|
7
8
|
from requests import Response
|
|
@@ -25,7 +26,7 @@ class Insight:
|
|
|
25
26
|
bearer_token = f"Bearer {api_token}"
|
|
26
27
|
self.session = requests.Session()
|
|
27
28
|
self.session.headers.update({"Authorization": bearer_token})
|
|
28
|
-
self.session.headers.update({"X-Request-Source": "sdk/python"})
|
|
29
|
+
self.session.headers.update({"X-Request-Source": f"sdk/python:{marple.__version__}"})
|
|
29
30
|
self.check_connection()
|
|
30
31
|
|
|
31
32
|
# User functions #
|
|
@@ -181,3 +182,39 @@ class Insight:
|
|
|
181
182
|
|
|
182
183
|
request.urlretrieve(download_url, target_path)
|
|
183
184
|
return target_path
|
|
185
|
+
|
|
186
|
+
You generate Marple analysis plots from user questions.
|
|
187
|
+
|
|
188
|
+
Supported plot types:
|
|
189
|
+
- timeseries: use for one or more signals over time.
|
|
190
|
+
- scatter: use for relationships between signals. Signal order is [x, y, optional color].
|
|
191
|
+
- heatmap: use for histograms of x/y relationships. Signal order is [x, y].
|
|
192
|
+
- map: use for geographic trajectory data. Signal order is [latitude, longitude, optional color].
|
|
193
|
+
- frequency: use for frequency/FFT analysis of one or more signals.
|
|
194
|
+
- state: use for discrete or categorical state signals.
|
|
195
|
+
- message: use for message/log views. Use for sparse text-based data.
|
|
196
|
+
- metadata: use for dataset metadata views. Use an empty signals list.
|
|
197
|
+
|
|
198
|
+
Always call search_signals before add_plot. Only use signal keys returned by search_signals. Search results may be truncated.
|
|
199
|
+
This is a single-turn plot generation task that creates exactly one plot. You cannot ask the user follow-up questions.
|
|
200
|
+
If the request is ambiguous, make a reasonable best effort using search_signals.
|
|
201
|
+
Provide a short description in add_plot summarizing the plot for the user.
|
|
202
|
+
|
|
203
|
+
Examples:
|
|
204
|
+
User: "all speed signals"
|
|
205
|
+
search_signals({"text":"speed"})
|
|
206
|
+
search_signals({"text":"*speed*","glob":true})
|
|
207
|
+
|
|
208
|
+
|
|
209
|
+
User: "speed and rpm over time"
|
|
210
|
+
add_plot({"plotType":"timeseries","signals":["speed_key","rpm_key"],"description":"Created speed and rpm over time."})
|
|
211
|
+
|
|
212
|
+
User: "speed vs throttle colored by rpm"
|
|
213
|
+
add_plot({"plotType":"scatter","signals":["throttle_key","speed_key","rpm_key"],"description":"Created speed versus throttle, colored by rpm."})
|
|
214
|
+
|
|
215
|
+
User: "show me dataset metadata"
|
|
216
|
+
add_plot({"plotType":"metadata","signals":[],"description":"Created a metadata plot."})
|
|
217
|
+
|
|
218
|
+
Dataset profile:
|
|
219
|
+
|
|
220
|
+
|
|
@@ -0,0 +1,95 @@
|
|
|
1
|
+
{
|
|
2
|
+
"dataset": {
|
|
3
|
+
"id": "{\"dataset\": \"SH-01 2026-05-31 Spa\"}",
|
|
4
|
+
"name": "SH-01 2026-05-31 Spa",
|
|
5
|
+
"realtime": false,
|
|
6
|
+
"timestampStart": "31/05/2026 08:27",
|
|
7
|
+
"timestampStop": "31/05/2026 08:48",
|
|
8
|
+
"duration": "21m15s",
|
|
9
|
+
"metadata": {
|
|
10
|
+
"dataset": "SH-01 2026-05-31 Spa",
|
|
11
|
+
"session": "SH",
|
|
12
|
+
"outing": "Outing 1",
|
|
13
|
+
"car": "M42",
|
|
14
|
+
"driver": "JS",
|
|
15
|
+
"track": "Spa",
|
|
16
|
+
"acquisition type": "Cable"
|
|
17
|
+
}
|
|
18
|
+
},
|
|
19
|
+
"signalCount": 126,
|
|
20
|
+
"facets": {
|
|
21
|
+
"signalStatus": {
|
|
22
|
+
"FROZEN_TO_COLD": 0,
|
|
23
|
+
"COLD": 0,
|
|
24
|
+
"HOT": 0,
|
|
25
|
+
"COLD_TO_HOT": 0
|
|
26
|
+
},
|
|
27
|
+
"datatype": { "number": 121, "text": 5 },
|
|
28
|
+
"metadata.count_text": { "0": 121, "11486": 5 },
|
|
29
|
+
"metadata.count_value": {
|
|
30
|
+
"0": 5,
|
|
31
|
+
"1275": 55,
|
|
32
|
+
"2550": 42,
|
|
33
|
+
"6376": 11,
|
|
34
|
+
"11486": 13
|
|
35
|
+
},
|
|
36
|
+
"metadata.group": {
|
|
37
|
+
"acceleration": 3,
|
|
38
|
+
"velocity": 9,
|
|
39
|
+
"brakes": 7,
|
|
40
|
+
"controls": 4,
|
|
41
|
+
"position": 4,
|
|
42
|
+
"lap": 6,
|
|
43
|
+
"drs": 4,
|
|
44
|
+
"damage": 6,
|
|
45
|
+
"engine": 4,
|
|
46
|
+
"general": 2,
|
|
47
|
+
"fuel": 3,
|
|
48
|
+
"gear": 2,
|
|
49
|
+
"gforce": 3,
|
|
50
|
+
"gps": 2,
|
|
51
|
+
"pit": 4,
|
|
52
|
+
"spectating": 2,
|
|
53
|
+
"kers": 2,
|
|
54
|
+
"cars": 2,
|
|
55
|
+
"orientation": 3,
|
|
56
|
+
"lights": 1,
|
|
57
|
+
"sector": 3,
|
|
58
|
+
"session": 2,
|
|
59
|
+
"sli": 1,
|
|
60
|
+
"speed": 1,
|
|
61
|
+
"suspension": 12,
|
|
62
|
+
"team": 1,
|
|
63
|
+
"distance": 1,
|
|
64
|
+
"track": 2,
|
|
65
|
+
"traction": 1,
|
|
66
|
+
"tyres": 17,
|
|
67
|
+
"flags": 2,
|
|
68
|
+
"wheels": 4,
|
|
69
|
+
"displacement": 3,
|
|
70
|
+
"rotation": 3
|
|
71
|
+
},
|
|
72
|
+
"metadata.unit": {
|
|
73
|
+
"rad/s²": 3,
|
|
74
|
+
"rad/s": 3,
|
|
75
|
+
"enum": 10,
|
|
76
|
+
"%": 2,
|
|
77
|
+
"°C": 9,
|
|
78
|
+
"rpm": 3,
|
|
79
|
+
"L": 2,
|
|
80
|
+
"G": 2,
|
|
81
|
+
"g": 1,
|
|
82
|
+
"°": 2,
|
|
83
|
+
"J": 2,
|
|
84
|
+
"m": 9,
|
|
85
|
+
"s": 6,
|
|
86
|
+
"km/h": 1,
|
|
87
|
+
"rad": 7,
|
|
88
|
+
"m/s": 15,
|
|
89
|
+
"m/s²": 4,
|
|
90
|
+
"mm": 4,
|
|
91
|
+
"psi": 4
|
|
92
|
+
}
|
|
93
|
+
},
|
|
94
|
+
"metadataKeys": ["count_text", "count_value", "description", "group", "unit"]
|
|
95
|
+
}
|
|
@@ -15,6 +15,8 @@ from marple.db.constants import (
|
|
|
15
15
|
COL_VAL_TEXT,
|
|
16
16
|
COL_VAL_TEXT_IDX,
|
|
17
17
|
)
|
|
18
|
+
from requests.adapters import HTTPAdapter
|
|
19
|
+
from urllib3.util.retry import Retry
|
|
18
20
|
|
|
19
21
|
|
|
20
22
|
def validate_response(response: requests.Response, failure_message: str) -> dict:
|
|
@@ -34,7 +36,20 @@ def validate_response(response: requests.Response, failure_message: str) -> dict
|
|
|
34
36
|
return r_json
|
|
35
37
|
|
|
36
38
|
|
|
39
|
+
class TimeoutHTTPAdapter(HTTPAdapter):
|
|
40
|
+
def __init__(self, *args, timeout=None, **kwargs):
|
|
41
|
+
self.timeout = timeout
|
|
42
|
+
super().__init__(*args, **kwargs)
|
|
43
|
+
|
|
44
|
+
def send(self, request, **kwargs):
|
|
45
|
+
if kwargs.get("timeout") is None:
|
|
46
|
+
kwargs["timeout"] = self.timeout
|
|
47
|
+
return super().send(request, **kwargs)
|
|
48
|
+
|
|
49
|
+
|
|
37
50
|
class DBClient:
|
|
51
|
+
DEFAULT_TIMEOUT = (5, 300)
|
|
52
|
+
|
|
38
53
|
def __init__(self, api_token: str, api_url: str, datapool: str, cache_folder: str):
|
|
39
54
|
self.api_token = api_token
|
|
40
55
|
self.api_url = api_url
|
|
@@ -42,10 +57,46 @@ class DBClient:
|
|
|
42
57
|
self.cache_folder = cache_folder
|
|
43
58
|
self._signal_map: dict[str, int] | None = None
|
|
44
59
|
|
|
45
|
-
self.session =
|
|
60
|
+
self.session = self._create_api_session()
|
|
61
|
+
self.storage_session = self._create_storage_session()
|
|
46
62
|
self.session.headers.update({"Authorization": f"Bearer {self.api_token}"})
|
|
47
63
|
self.session.headers.update({"X-Request-Source": f"sdk/python:{marple.__version__}"})
|
|
48
64
|
|
|
65
|
+
def _create_api_session(self) -> requests.Session:
|
|
66
|
+
retry = Retry(
|
|
67
|
+
total=5,
|
|
68
|
+
connect=5,
|
|
69
|
+
read=2,
|
|
70
|
+
status=2,
|
|
71
|
+
backoff_factor=0.5,
|
|
72
|
+
status_forcelist=(429, 502, 503, 504),
|
|
73
|
+
allowed_methods=frozenset({"GET", "HEAD", "OPTIONS", "DELETE"}),
|
|
74
|
+
respect_retry_after_header=True,
|
|
75
|
+
raise_on_status=False,
|
|
76
|
+
)
|
|
77
|
+
return self._create_session(retry)
|
|
78
|
+
|
|
79
|
+
def _create_storage_session(self) -> requests.Session:
|
|
80
|
+
retry = Retry(
|
|
81
|
+
total=5,
|
|
82
|
+
connect=5,
|
|
83
|
+
read=5,
|
|
84
|
+
status=5,
|
|
85
|
+
backoff_factor=0.5,
|
|
86
|
+
status_forcelist=(429, 500, 502, 503, 504),
|
|
87
|
+
allowed_methods=frozenset({"PUT", "GET", "HEAD"}),
|
|
88
|
+
respect_retry_after_header=True,
|
|
89
|
+
raise_on_status=False,
|
|
90
|
+
)
|
|
91
|
+
return self._create_session(retry)
|
|
92
|
+
|
|
93
|
+
def _create_session(self, retry: Retry) -> requests.Session:
|
|
94
|
+
session = requests.Session()
|
|
95
|
+
adapter = TimeoutHTTPAdapter(max_retries=retry, timeout=self.DEFAULT_TIMEOUT)
|
|
96
|
+
session.mount("https://", adapter)
|
|
97
|
+
session.mount("http://", adapter)
|
|
98
|
+
return session
|
|
99
|
+
|
|
49
100
|
def get(self, url: str, *args, **kwargs) -> requests.Response:
|
|
50
101
|
return self.session.get(f"{self.api_url}{url}", *args, **kwargs)
|
|
51
102
|
|
|
@@ -0,0 +1,102 @@
|
|
|
1
|
+
from collections.abc import Iterator
|
|
2
|
+
from contextlib import contextmanager
|
|
3
|
+
from http.server import BaseHTTPRequestHandler, HTTPServer
|
|
4
|
+
from threading import Thread
|
|
5
|
+
|
|
6
|
+
import pytest
|
|
7
|
+
from marple.utils import DBClient
|
|
8
|
+
from requests import Response
|
|
9
|
+
from requests.adapters import HTTPAdapter
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
@contextmanager
|
|
13
|
+
def retry_test_server(statuses: list[int]) -> Iterator[tuple[str, dict[str, int]]]:
|
|
14
|
+
calls = {"GET": 0, "POST": 0, "PUT": 0}
|
|
15
|
+
|
|
16
|
+
class Handler(BaseHTTPRequestHandler):
|
|
17
|
+
def do_GET(self) -> None:
|
|
18
|
+
self._handle()
|
|
19
|
+
|
|
20
|
+
def do_POST(self) -> None:
|
|
21
|
+
self._handle()
|
|
22
|
+
|
|
23
|
+
def do_PUT(self) -> None:
|
|
24
|
+
self._handle()
|
|
25
|
+
|
|
26
|
+
def log_message(self, format: str, *args) -> None:
|
|
27
|
+
return
|
|
28
|
+
|
|
29
|
+
def _handle(self) -> None:
|
|
30
|
+
calls[self.command] += 1
|
|
31
|
+
length = int(self.headers.get("Content-Length", "0"))
|
|
32
|
+
if length:
|
|
33
|
+
self.rfile.read(length)
|
|
34
|
+
status = statuses[min(calls[self.command] - 1, len(statuses) - 1)]
|
|
35
|
+
self.send_response(status)
|
|
36
|
+
self.send_header("Content-Length", "0")
|
|
37
|
+
self.end_headers()
|
|
38
|
+
|
|
39
|
+
server = HTTPServer(("127.0.0.1", 0), Handler)
|
|
40
|
+
thread = Thread(target=server.serve_forever)
|
|
41
|
+
thread.start()
|
|
42
|
+
|
|
43
|
+
try:
|
|
44
|
+
host, port = server.server_address
|
|
45
|
+
yield f"http://{host}:{port}", calls
|
|
46
|
+
finally:
|
|
47
|
+
server.shutdown()
|
|
48
|
+
thread.join()
|
|
49
|
+
server.server_close()
|
|
50
|
+
|
|
51
|
+
|
|
52
|
+
def make_client(api_url: str = "http://example.test") -> DBClient:
|
|
53
|
+
return DBClient(
|
|
54
|
+
api_token="test-token",
|
|
55
|
+
api_url=api_url,
|
|
56
|
+
datapool="default",
|
|
57
|
+
cache_folder="./.test_cache",
|
|
58
|
+
)
|
|
59
|
+
|
|
60
|
+
|
|
61
|
+
def test_api_session_retries_retryable_get_status() -> None:
|
|
62
|
+
with retry_test_server([503, 200]) as (api_url, calls):
|
|
63
|
+
response = make_client(api_url).get("/health")
|
|
64
|
+
|
|
65
|
+
assert response.status_code == 200
|
|
66
|
+
assert calls["GET"] == 2
|
|
67
|
+
|
|
68
|
+
|
|
69
|
+
def test_default_timeout_applied(monkeypatch: pytest.MonkeyPatch) -> None:
|
|
70
|
+
timeouts = []
|
|
71
|
+
|
|
72
|
+
def fake_send(self, request, **kwargs) -> Response:
|
|
73
|
+
timeouts.append(kwargs["timeout"])
|
|
74
|
+
response = Response()
|
|
75
|
+
response.status_code = 200
|
|
76
|
+
response.url = request.url
|
|
77
|
+
response._content = b"{}"
|
|
78
|
+
return response
|
|
79
|
+
|
|
80
|
+
monkeypatch.setattr(HTTPAdapter, "send", fake_send)
|
|
81
|
+
|
|
82
|
+
client = make_client()
|
|
83
|
+
client.get("/health")
|
|
84
|
+
client.get("/health", timeout=(1, 2))
|
|
85
|
+
|
|
86
|
+
assert timeouts == [DBClient.DEFAULT_TIMEOUT, (1, 2)]
|
|
87
|
+
|
|
88
|
+
|
|
89
|
+
def test_storage_put_retries_retryable_status() -> None:
|
|
90
|
+
with retry_test_server([503, 200]) as (storage_url, calls):
|
|
91
|
+
response = make_client().storage_session.put(f"{storage_url}/blob", data=b"payload")
|
|
92
|
+
|
|
93
|
+
assert response.status_code == 200
|
|
94
|
+
assert calls["PUT"] == 2
|
|
95
|
+
|
|
96
|
+
|
|
97
|
+
def test_api_post_body_is_not_replayed_after_status_failure() -> None:
|
|
98
|
+
with retry_test_server([503, 200]) as (api_url, calls):
|
|
99
|
+
response = make_client(api_url).post("/ingestion", data=b"payload")
|
|
100
|
+
|
|
101
|
+
assert response.status_code == 503
|
|
102
|
+
assert calls["POST"] == 1
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|