dragoneye-python 0.3.0__tar.gz → 0.4.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.
- dragoneye_python-0.4.0/PKG-INFO +10 -0
- dragoneye_python-0.4.0/dragoneye_python.egg-info/PKG-INFO +10 -0
- dragoneye_python-0.4.0/dragoneye_python.egg-info/requires.txt +5 -0
- {dragoneye_python-0.3.0 → dragoneye_python-0.4.0}/pyproject.toml +6 -2
- dragoneye_python-0.4.0/requirements.txt +5 -0
- {dragoneye_python-0.3.0 → dragoneye_python-0.4.0}/src/dragoneye/classification.py +75 -31
- dragoneye_python-0.4.0/src/dragoneye/client.py +29 -0
- {dragoneye_python-0.3.0 → dragoneye_python-0.4.0}/src/dragoneye/models.py +2 -3
- {dragoneye_python-0.3.0 → dragoneye_python-0.4.0}/src/dragoneye/types/media.py +10 -8
- dragoneye_python-0.3.0/PKG-INFO +0 -7
- dragoneye_python-0.3.0/dragoneye_python.egg-info/PKG-INFO +0 -7
- dragoneye_python-0.3.0/dragoneye_python.egg-info/requires.txt +0 -2
- dragoneye_python-0.3.0/requirements.txt +0 -2
- dragoneye_python-0.3.0/src/dragoneye/client.py +0 -18
- {dragoneye_python-0.3.0 → dragoneye_python-0.4.0}/README.md +0 -0
- {dragoneye_python-0.3.0 → dragoneye_python-0.4.0}/dragoneye_python.egg-info/SOURCES.txt +0 -0
- {dragoneye_python-0.3.0 → dragoneye_python-0.4.0}/dragoneye_python.egg-info/dependency_links.txt +0 -0
- {dragoneye_python-0.3.0 → dragoneye_python-0.4.0}/dragoneye_python.egg-info/top_level.txt +0 -0
- {dragoneye_python-0.3.0 → dragoneye_python-0.4.0}/setup.cfg +0 -0
- {dragoneye_python-0.3.0 → dragoneye_python-0.4.0}/src/dragoneye/__init__.py +0 -0
- {dragoneye_python-0.3.0 → dragoneye_python-0.4.0}/src/dragoneye/constants.py +0 -0
- {dragoneye_python-0.3.0 → dragoneye_python-0.4.0}/src/dragoneye/types/__init__.py +0 -0
- {dragoneye_python-0.3.0 → dragoneye_python-0.4.0}/src/dragoneye/types/common.py +0 -0
- {dragoneye_python-0.3.0 → dragoneye_python-0.4.0}/src/dragoneye/types/exception.py +0 -0
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: dragoneye-python
|
|
3
|
+
Version: 0.4.0
|
|
4
|
+
License-Expression: MIT
|
|
5
|
+
Requires-Python: >=3.8
|
|
6
|
+
Requires-Dist: requests
|
|
7
|
+
Requires-Dist: pydantic>=2
|
|
8
|
+
Requires-Dist: typing-extensions>=4.0.0
|
|
9
|
+
Requires-Dist: backoff>=2.0.0
|
|
10
|
+
Requires-Dist: aiohttp
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: dragoneye-python
|
|
3
|
+
Version: 0.4.0
|
|
4
|
+
License-Expression: MIT
|
|
5
|
+
Requires-Python: >=3.8
|
|
6
|
+
Requires-Dist: requests
|
|
7
|
+
Requires-Dist: pydantic>=2
|
|
8
|
+
Requires-Dist: typing-extensions>=4.0.0
|
|
9
|
+
Requires-Dist: backoff>=2.0.0
|
|
10
|
+
Requires-Dist: aiohttp
|
|
@@ -1,9 +1,13 @@
|
|
|
1
|
+
[build-system]
|
|
2
|
+
requires = ["setuptools>=77.0.3", "wheel"]
|
|
3
|
+
build-backend = "setuptools.build_meta"
|
|
4
|
+
|
|
1
5
|
[project]
|
|
2
6
|
name = "dragoneye-python"
|
|
3
|
-
version = "0.
|
|
7
|
+
version = "0.4.0"
|
|
4
8
|
requires-python = ">=3.8"
|
|
5
9
|
dynamic = ["dependencies"]
|
|
6
|
-
license =
|
|
10
|
+
license = "MIT"
|
|
7
11
|
|
|
8
12
|
[tool.setuptools.dynamic]
|
|
9
13
|
dependencies = {file = ["requirements.txt"]}
|
|
@@ -1,8 +1,10 @@
|
|
|
1
1
|
import asyncio
|
|
2
|
+
import logging
|
|
2
3
|
import time
|
|
3
|
-
from typing import TYPE_CHECKING, Any, Literal, overload
|
|
4
|
+
from typing import TYPE_CHECKING, Any, Dict, List, Literal, Optional, Union, overload
|
|
4
5
|
|
|
5
6
|
import aiohttp
|
|
7
|
+
import backoff
|
|
6
8
|
from aiohttp import ClientError
|
|
7
9
|
from pydantic import BaseModel
|
|
8
10
|
|
|
@@ -33,7 +35,7 @@ if TYPE_CHECKING:
|
|
|
33
35
|
|
|
34
36
|
class _PresignedPostRequest(BaseModel):
|
|
35
37
|
url: str
|
|
36
|
-
fields:
|
|
38
|
+
fields: Dict[str, Any]
|
|
37
39
|
|
|
38
40
|
|
|
39
41
|
class _MediaUploadUrl(BaseModel):
|
|
@@ -44,7 +46,7 @@ class _MediaUploadUrl(BaseModel):
|
|
|
44
46
|
class _PredictionTaskBeginResponse(BaseModel):
|
|
45
47
|
prediction_task_uuid: PredictionTaskUUID
|
|
46
48
|
prediction_type: PredictionType
|
|
47
|
-
signed_urls:
|
|
49
|
+
signed_urls: List[_MediaUploadUrl]
|
|
48
50
|
|
|
49
51
|
|
|
50
52
|
def _is_task_successful(status: PredictionTaskState) -> bool:
|
|
@@ -68,11 +70,33 @@ class Classification:
|
|
|
68
70
|
def __init__(self, client: "Dragoneye"):
|
|
69
71
|
self._client = client
|
|
70
72
|
|
|
73
|
+
# Create a reusable backoff decorator for 429 rate limit errors
|
|
74
|
+
def _should_retry_429(exception: Exception) -> bool:
|
|
75
|
+
"""Check if exception is a 429 rate limit error"""
|
|
76
|
+
return (
|
|
77
|
+
isinstance(exception, aiohttp.ClientResponseError)
|
|
78
|
+
and exception.status == 429
|
|
79
|
+
)
|
|
80
|
+
|
|
81
|
+
# Store the backoff decorator as an instance method
|
|
82
|
+
self._backoff_on_429 = backoff.on_exception(
|
|
83
|
+
wait_gen=backoff.expo,
|
|
84
|
+
exception=aiohttp.ClientResponseError,
|
|
85
|
+
max_tries=client.max_retries,
|
|
86
|
+
max_time=client.max_backoff_time,
|
|
87
|
+
on_backoff=lambda e: logging.info(
|
|
88
|
+
f"Rate limit exceeded - backing off: {e}"
|
|
89
|
+
),
|
|
90
|
+
on_giveup=lambda e: logging.info(f"Rate limit exceeded - giving up: {e}"),
|
|
91
|
+
giveup=lambda e: not _should_retry_429(e),
|
|
92
|
+
jitter=client.backoff_jitter,
|
|
93
|
+
)
|
|
94
|
+
|
|
71
95
|
async def predict_image(
|
|
72
96
|
self,
|
|
73
97
|
media: Image,
|
|
74
98
|
model_name: str,
|
|
75
|
-
timeout_seconds: int
|
|
99
|
+
timeout_seconds: Optional[int] = None,
|
|
76
100
|
) -> ClassificationPredictImageResponse:
|
|
77
101
|
return await self._predict_unified(
|
|
78
102
|
media=media,
|
|
@@ -86,7 +110,7 @@ class Classification:
|
|
|
86
110
|
media: Video,
|
|
87
111
|
model_name: str,
|
|
88
112
|
frames_per_second: int = 1,
|
|
89
|
-
timeout_seconds: int
|
|
113
|
+
timeout_seconds: Optional[int] = None,
|
|
90
114
|
) -> ClassificationPredictVideoResponse:
|
|
91
115
|
return await self._predict_unified(
|
|
92
116
|
media=media,
|
|
@@ -104,11 +128,15 @@ class Classification:
|
|
|
104
128
|
url = f"{BASE_API_URL}/prediction-task/status?predictionTaskUuid={prediction_task_uuid}"
|
|
105
129
|
headers = {"Authorization": f"Bearer {self._client.api_key}"}
|
|
106
130
|
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
131
|
+
@self._backoff_on_429
|
|
132
|
+
async def _make_request():
|
|
133
|
+
async with aiohttp.ClientSession() as session:
|
|
134
|
+
async with session.get(url, headers=headers) as resp:
|
|
135
|
+
resp.raise_for_status()
|
|
136
|
+
payload = await resp.json()
|
|
137
|
+
return payload
|
|
111
138
|
|
|
139
|
+
payload = await _make_request()
|
|
112
140
|
return PredictionTaskStatusResponse.model_validate(payload)
|
|
113
141
|
|
|
114
142
|
async def get_image_results(
|
|
@@ -148,19 +176,26 @@ class Classification:
|
|
|
148
176
|
self,
|
|
149
177
|
prediction_task_uuid: PredictionTaskUUID,
|
|
150
178
|
prediction_type: PredictionType,
|
|
151
|
-
) ->
|
|
179
|
+
) -> Union[
|
|
180
|
+
ClassificationPredictImageResponse, ClassificationPredictVideoResponse
|
|
181
|
+
]: ...
|
|
152
182
|
|
|
153
183
|
async def _get_results_unified(
|
|
154
184
|
self, prediction_task_uuid: PredictionTaskUUID, prediction_type: PredictionType
|
|
155
|
-
) -> ClassificationPredictImageResponse
|
|
185
|
+
) -> Union[ClassificationPredictImageResponse, ClassificationPredictVideoResponse]:
|
|
156
186
|
url = f"{BASE_API_URL}/prediction-task/results?predictionTaskUuid={prediction_task_uuid}"
|
|
157
187
|
headers = {"Authorization": f"Bearer {self._client.api_key}"}
|
|
158
188
|
|
|
159
|
-
|
|
189
|
+
@self._backoff_on_429
|
|
190
|
+
async def _make_request():
|
|
160
191
|
async with aiohttp.ClientSession() as session:
|
|
161
192
|
async with session.get(url, headers=headers) as resp:
|
|
162
193
|
resp.raise_for_status()
|
|
163
194
|
payload = await resp.json()
|
|
195
|
+
return payload
|
|
196
|
+
|
|
197
|
+
try:
|
|
198
|
+
payload = await _make_request()
|
|
164
199
|
except ClientError as error:
|
|
165
200
|
raise PredictionTaskResultsUnavailableError(
|
|
166
201
|
f"Error getting prediction task results: {error}"
|
|
@@ -169,13 +204,12 @@ class Classification:
|
|
|
169
204
|
# Add the prediction task uuid to the response before returning
|
|
170
205
|
payload["prediction_task_uuid"] = prediction_task_uuid
|
|
171
206
|
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
raise ValueError(f"Unsupported prediction type: {prediction_type}")
|
|
207
|
+
if prediction_type == "image":
|
|
208
|
+
return ClassificationPredictImageResponse.model_validate(payload)
|
|
209
|
+
elif prediction_type == "video":
|
|
210
|
+
return ClassificationPredictVideoResponse.model_validate(payload)
|
|
211
|
+
else:
|
|
212
|
+
raise ValueError(f"Unsupported prediction type: {prediction_type}")
|
|
179
213
|
|
|
180
214
|
##### Internal API methods #####
|
|
181
215
|
@overload
|
|
@@ -183,8 +217,8 @@ class Classification:
|
|
|
183
217
|
self,
|
|
184
218
|
media: Image,
|
|
185
219
|
model_name: str,
|
|
186
|
-
frames_per_second: int
|
|
187
|
-
timeout_seconds: int
|
|
220
|
+
frames_per_second: Optional[int],
|
|
221
|
+
timeout_seconds: Optional[int] = None,
|
|
188
222
|
) -> ClassificationPredictImageResponse: ...
|
|
189
223
|
|
|
190
224
|
@overload
|
|
@@ -192,17 +226,17 @@ class Classification:
|
|
|
192
226
|
self,
|
|
193
227
|
media: Video,
|
|
194
228
|
model_name: str,
|
|
195
|
-
frames_per_second: int
|
|
196
|
-
timeout_seconds: int
|
|
229
|
+
frames_per_second: Optional[int],
|
|
230
|
+
timeout_seconds: Optional[int] = None,
|
|
197
231
|
) -> ClassificationPredictVideoResponse: ...
|
|
198
232
|
|
|
199
233
|
async def _predict_unified(
|
|
200
234
|
self,
|
|
201
|
-
media: Image
|
|
235
|
+
media: Union[Image, Video],
|
|
202
236
|
model_name: str,
|
|
203
|
-
frames_per_second: int
|
|
204
|
-
timeout_seconds: int
|
|
205
|
-
) -> ClassificationPredictImageResponse
|
|
237
|
+
frames_per_second: Optional[int],
|
|
238
|
+
timeout_seconds: Optional[int] = None,
|
|
239
|
+
) -> Union[ClassificationPredictImageResponse, ClassificationPredictVideoResponse]:
|
|
206
240
|
prediction_task_begin_response = await self._begin_prediction_task(
|
|
207
241
|
mime_type=media.mime_type,
|
|
208
242
|
frames_per_second=frames_per_second,
|
|
@@ -220,12 +254,17 @@ class Classification:
|
|
|
220
254
|
predict_headers = {
|
|
221
255
|
"Authorization": f"Bearer {self._client.api_key}",
|
|
222
256
|
}
|
|
223
|
-
|
|
257
|
+
|
|
258
|
+
@self._backoff_on_429
|
|
259
|
+
async def _make_request():
|
|
224
260
|
async with aiohttp.ClientSession() as session:
|
|
225
261
|
async with session.post(
|
|
226
262
|
predict_url, data=predict_data, headers=predict_headers
|
|
227
263
|
) as resp:
|
|
228
264
|
resp.raise_for_status()
|
|
265
|
+
|
|
266
|
+
try:
|
|
267
|
+
await _make_request()
|
|
229
268
|
except ClientError as error:
|
|
230
269
|
raise PredictionTaskError("Error initiating prediction:", error)
|
|
231
270
|
|
|
@@ -246,7 +285,7 @@ class Classification:
|
|
|
246
285
|
self,
|
|
247
286
|
prediction_task_uuid: PredictionTaskUUID,
|
|
248
287
|
polling_interval: float = 1.0,
|
|
249
|
-
timeout_seconds: int
|
|
288
|
+
timeout_seconds: Optional[int] = None,
|
|
250
289
|
) -> PredictionTaskStatusResponse:
|
|
251
290
|
start_time = time.monotonic()
|
|
252
291
|
while True:
|
|
@@ -299,7 +338,7 @@ class Classification:
|
|
|
299
338
|
async def _begin_prediction_task(
|
|
300
339
|
self,
|
|
301
340
|
mime_type: str,
|
|
302
|
-
frames_per_second: int
|
|
341
|
+
frames_per_second: Optional[int],
|
|
303
342
|
) -> _PredictionTaskBeginResponse:
|
|
304
343
|
url = f"{BASE_API_URL}/prediction-task/begin"
|
|
305
344
|
|
|
@@ -312,11 +351,16 @@ class Classification:
|
|
|
312
351
|
"Authorization": f"Bearer {self._client.api_key}",
|
|
313
352
|
}
|
|
314
353
|
|
|
315
|
-
|
|
354
|
+
@self._backoff_on_429
|
|
355
|
+
async def _make_request():
|
|
316
356
|
async with aiohttp.ClientSession() as session:
|
|
317
357
|
async with session.post(url, data=form_data, headers=headers) as resp:
|
|
318
358
|
resp.raise_for_status()
|
|
319
359
|
payload = await resp.json()
|
|
360
|
+
return payload
|
|
361
|
+
|
|
362
|
+
try:
|
|
363
|
+
payload = await _make_request()
|
|
320
364
|
except ClientError as error:
|
|
321
365
|
raise PredictionTaskBeginError("Error beginning prediction task:", error)
|
|
322
366
|
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
import os
|
|
2
|
+
from typing import Callable, Optional
|
|
3
|
+
|
|
4
|
+
import backoff
|
|
5
|
+
|
|
6
|
+
from dragoneye.classification import Classification
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
class Dragoneye:
|
|
10
|
+
def __init__(
|
|
11
|
+
self,
|
|
12
|
+
api_key: Optional[str] = None,
|
|
13
|
+
max_retries: int = 10,
|
|
14
|
+
max_backoff_time: int = 120,
|
|
15
|
+
backoff_jitter: Callable[[float], float] = backoff.full_jitter,
|
|
16
|
+
):
|
|
17
|
+
if api_key is None:
|
|
18
|
+
api_key = os.getenv("DRAGONEYE_API_KEY")
|
|
19
|
+
|
|
20
|
+
assert api_key is not None, (
|
|
21
|
+
"API key is required - set the DRAGONEYE_API_KEY environment variable or pass it to the [Dragoneye] constructor"
|
|
22
|
+
)
|
|
23
|
+
|
|
24
|
+
self.api_key = api_key
|
|
25
|
+
self.max_retries = max_retries
|
|
26
|
+
self.max_backoff_time = max_backoff_time
|
|
27
|
+
self.backoff_jitter = backoff_jitter
|
|
28
|
+
|
|
29
|
+
self.classification = Classification(self)
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
from typing import Sequence
|
|
1
|
+
from typing import Dict, Sequence
|
|
2
2
|
|
|
3
3
|
from pydantic import BaseModel
|
|
4
4
|
|
|
@@ -38,12 +38,11 @@ class ClassificationPredictImageResponse(BaseModel):
|
|
|
38
38
|
|
|
39
39
|
class ClassificationVideoObjectPrediction(ClassificationObjectPrediction):
|
|
40
40
|
frame_id: str
|
|
41
|
-
frame_index: int
|
|
42
41
|
timestamp_microseconds: int
|
|
43
42
|
|
|
44
43
|
|
|
45
44
|
class ClassificationPredictVideoResponse(BaseModel):
|
|
46
|
-
timestamp_us_to_predictions:
|
|
45
|
+
timestamp_us_to_predictions: Dict[
|
|
47
46
|
int, Sequence[ClassificationVideoObjectPrediction]
|
|
48
47
|
]
|
|
49
48
|
frames_per_second: int
|
|
@@ -5,7 +5,9 @@ import os
|
|
|
5
5
|
from dataclasses import dataclass
|
|
6
6
|
from io import BufferedReader, BytesIO
|
|
7
7
|
from pathlib import Path
|
|
8
|
-
from typing import BinaryIO, ClassVar,
|
|
8
|
+
from typing import BinaryIO, ClassVar, Optional, Tuple, Union
|
|
9
|
+
|
|
10
|
+
from typing_extensions import Self
|
|
9
11
|
|
|
10
12
|
|
|
11
13
|
@dataclass(frozen=True)
|
|
@@ -16,7 +18,7 @@ class Media:
|
|
|
16
18
|
mime_type: str
|
|
17
19
|
|
|
18
20
|
# Subclasses set this to enforce a family of mimetypes, e.g. ("image/",)
|
|
19
|
-
ACCEPT_PREFIXES: ClassVar[
|
|
21
|
+
ACCEPT_PREFIXES: ClassVar[Tuple[str, ...]] = ()
|
|
20
22
|
|
|
21
23
|
def __post_init__(self) -> None:
|
|
22
24
|
# Enforce subtype-specific mimetype families when specified.
|
|
@@ -47,7 +49,7 @@ class Media:
|
|
|
47
49
|
cls,
|
|
48
50
|
path: Union[str, os.PathLike[str]],
|
|
49
51
|
*,
|
|
50
|
-
mime_type: str
|
|
52
|
+
mime_type: Optional[str] = None,
|
|
51
53
|
guess_from_extension: bool = True,
|
|
52
54
|
read_into_memory: bool = False,
|
|
53
55
|
) -> Self:
|
|
@@ -118,7 +120,7 @@ class Media:
|
|
|
118
120
|
"Invalid media source: expected bytes, BytesIO, or a readable binary stream."
|
|
119
121
|
)
|
|
120
122
|
|
|
121
|
-
def size_bytes(self) -> int
|
|
123
|
+
def size_bytes(self) -> Optional[int]:
|
|
122
124
|
"""
|
|
123
125
|
Best-effort size inference without consuming the stream.
|
|
124
126
|
Returns None if size can't be determined cheaply.
|
|
@@ -152,20 +154,20 @@ class Media:
|
|
|
152
154
|
class Image(Media):
|
|
153
155
|
"""Media restricted to image/* mimetypes."""
|
|
154
156
|
|
|
155
|
-
ACCEPT_PREFIXES: ClassVar[
|
|
157
|
+
ACCEPT_PREFIXES: ClassVar[Tuple[str, ...]] = ("image/",)
|
|
156
158
|
|
|
157
159
|
|
|
158
160
|
@dataclass(frozen=True)
|
|
159
161
|
class Video(Media):
|
|
160
162
|
"""Media restricted to video/* mimetypes."""
|
|
161
163
|
|
|
162
|
-
ACCEPT_PREFIXES: ClassVar[
|
|
164
|
+
ACCEPT_PREFIXES: ClassVar[Tuple[str, ...]] = ("video/",)
|
|
163
165
|
|
|
164
166
|
|
|
165
167
|
# ---------- Helpers ----------
|
|
166
168
|
|
|
167
169
|
|
|
168
|
-
def _tell_safe(stream: BinaryIO) -> int
|
|
170
|
+
def _tell_safe(stream: BinaryIO) -> Optional[int]:
|
|
169
171
|
try:
|
|
170
172
|
if hasattr(stream, "tell"):
|
|
171
173
|
return stream.tell()
|
|
@@ -174,7 +176,7 @@ def _tell_safe(stream: BinaryIO) -> int | None:
|
|
|
174
176
|
return None
|
|
175
177
|
|
|
176
178
|
|
|
177
|
-
def _seek_safe(stream: BinaryIO, pos: int
|
|
179
|
+
def _seek_safe(stream: BinaryIO, pos: Optional[int]) -> None:
|
|
178
180
|
if pos is None:
|
|
179
181
|
return
|
|
180
182
|
try:
|
dragoneye_python-0.3.0/PKG-INFO
DELETED
|
@@ -1,18 +0,0 @@
|
|
|
1
|
-
import os
|
|
2
|
-
from typing import Optional
|
|
3
|
-
|
|
4
|
-
from dragoneye.classification import Classification
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
class Dragoneye:
|
|
8
|
-
def __init__(self, api_key: Optional[str] = None):
|
|
9
|
-
if api_key is None:
|
|
10
|
-
api_key = os.getenv("DRAGONEYE_API_KEY")
|
|
11
|
-
|
|
12
|
-
assert (
|
|
13
|
-
api_key is not None
|
|
14
|
-
), "API key is required - set the DRAGONEYE_API_KEY environment variable or pass it to the [Dragoneye] constructor"
|
|
15
|
-
|
|
16
|
-
self.api_key = api_key
|
|
17
|
-
|
|
18
|
-
self.classification = Classification(self)
|
|
File without changes
|
|
File without changes
|
{dragoneye_python-0.3.0 → dragoneye_python-0.4.0}/dragoneye_python.egg-info/dependency_links.txt
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|