dragoneye-python 0.3.0__tar.gz → 0.4.1__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.1/PKG-INFO +10 -0
- dragoneye_python-0.4.1/dragoneye_python.egg-info/PKG-INFO +10 -0
- dragoneye_python-0.4.1/dragoneye_python.egg-info/requires.txt +5 -0
- {dragoneye_python-0.3.0 → dragoneye_python-0.4.1}/pyproject.toml +7 -3
- dragoneye_python-0.4.1/requirements.txt +5 -0
- {dragoneye_python-0.3.0 → dragoneye_python-0.4.1}/src/dragoneye/classification.py +88 -31
- dragoneye_python-0.4.1/src/dragoneye/client.py +29 -0
- {dragoneye_python-0.3.0 → dragoneye_python-0.4.1}/src/dragoneye/models.py +2 -3
- {dragoneye_python-0.3.0 → dragoneye_python-0.4.1}/src/dragoneye/types/media.py +41 -14
- 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.1}/README.md +0 -0
- {dragoneye_python-0.3.0 → dragoneye_python-0.4.1}/dragoneye_python.egg-info/SOURCES.txt +0 -0
- {dragoneye_python-0.3.0 → dragoneye_python-0.4.1}/dragoneye_python.egg-info/dependency_links.txt +0 -0
- {dragoneye_python-0.3.0 → dragoneye_python-0.4.1}/dragoneye_python.egg-info/top_level.txt +0 -0
- {dragoneye_python-0.3.0 → dragoneye_python-0.4.1}/setup.cfg +0 -0
- {dragoneye_python-0.3.0 → dragoneye_python-0.4.1}/src/dragoneye/__init__.py +0 -0
- {dragoneye_python-0.3.0 → dragoneye_python-0.4.1}/src/dragoneye/constants.py +0 -0
- {dragoneye_python-0.3.0 → dragoneye_python-0.4.1}/src/dragoneye/types/__init__.py +0 -0
- {dragoneye_python-0.3.0 → dragoneye_python-0.4.1}/src/dragoneye/types/common.py +0 -0
- {dragoneye_python-0.3.0 → dragoneye_python-0.4.1}/src/dragoneye/types/exception.py +0 -0
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: dragoneye-python
|
|
3
|
+
Version: 0.4.1
|
|
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.1
|
|
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,12 +1,16 @@
|
|
|
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.1"
|
|
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"]}
|
|
10
14
|
|
|
11
15
|
[tool.setuptools.package-dir]
|
|
12
|
-
dragoneye = "src/dragoneye"
|
|
16
|
+
dragoneye = "src/dragoneye"
|
|
@@ -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,17 +70,41 @@ 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,
|
|
100
|
+
**kwargs: Any,
|
|
76
101
|
) -> ClassificationPredictImageResponse:
|
|
77
102
|
return await self._predict_unified(
|
|
78
103
|
media=media,
|
|
79
104
|
model_name=model_name,
|
|
80
105
|
frames_per_second=None,
|
|
81
106
|
timeout_seconds=timeout_seconds,
|
|
107
|
+
**kwargs,
|
|
82
108
|
)
|
|
83
109
|
|
|
84
110
|
async def predict_video(
|
|
@@ -86,13 +112,15 @@ class Classification:
|
|
|
86
112
|
media: Video,
|
|
87
113
|
model_name: str,
|
|
88
114
|
frames_per_second: int = 1,
|
|
89
|
-
timeout_seconds: int
|
|
115
|
+
timeout_seconds: Optional[int] = None,
|
|
116
|
+
**kwargs: Any,
|
|
90
117
|
) -> ClassificationPredictVideoResponse:
|
|
91
118
|
return await self._predict_unified(
|
|
92
119
|
media=media,
|
|
93
120
|
model_name=model_name,
|
|
94
121
|
frames_per_second=frames_per_second,
|
|
95
122
|
timeout_seconds=timeout_seconds,
|
|
123
|
+
**kwargs,
|
|
96
124
|
)
|
|
97
125
|
|
|
98
126
|
async def status(
|
|
@@ -104,11 +132,15 @@ class Classification:
|
|
|
104
132
|
url = f"{BASE_API_URL}/prediction-task/status?predictionTaskUuid={prediction_task_uuid}"
|
|
105
133
|
headers = {"Authorization": f"Bearer {self._client.api_key}"}
|
|
106
134
|
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
135
|
+
@self._backoff_on_429
|
|
136
|
+
async def _make_request():
|
|
137
|
+
async with aiohttp.ClientSession() as session:
|
|
138
|
+
async with session.get(url, headers=headers) as resp:
|
|
139
|
+
resp.raise_for_status()
|
|
140
|
+
payload = await resp.json()
|
|
141
|
+
return payload
|
|
111
142
|
|
|
143
|
+
payload = await _make_request()
|
|
112
144
|
return PredictionTaskStatusResponse.model_validate(payload)
|
|
113
145
|
|
|
114
146
|
async def get_image_results(
|
|
@@ -148,19 +180,26 @@ class Classification:
|
|
|
148
180
|
self,
|
|
149
181
|
prediction_task_uuid: PredictionTaskUUID,
|
|
150
182
|
prediction_type: PredictionType,
|
|
151
|
-
) ->
|
|
183
|
+
) -> Union[
|
|
184
|
+
ClassificationPredictImageResponse, ClassificationPredictVideoResponse
|
|
185
|
+
]: ...
|
|
152
186
|
|
|
153
187
|
async def _get_results_unified(
|
|
154
188
|
self, prediction_task_uuid: PredictionTaskUUID, prediction_type: PredictionType
|
|
155
|
-
) -> ClassificationPredictImageResponse
|
|
189
|
+
) -> Union[ClassificationPredictImageResponse, ClassificationPredictVideoResponse]:
|
|
156
190
|
url = f"{BASE_API_URL}/prediction-task/results?predictionTaskUuid={prediction_task_uuid}"
|
|
157
191
|
headers = {"Authorization": f"Bearer {self._client.api_key}"}
|
|
158
192
|
|
|
159
|
-
|
|
193
|
+
@self._backoff_on_429
|
|
194
|
+
async def _make_request():
|
|
160
195
|
async with aiohttp.ClientSession() as session:
|
|
161
196
|
async with session.get(url, headers=headers) as resp:
|
|
162
197
|
resp.raise_for_status()
|
|
163
198
|
payload = await resp.json()
|
|
199
|
+
return payload
|
|
200
|
+
|
|
201
|
+
try:
|
|
202
|
+
payload = await _make_request()
|
|
164
203
|
except ClientError as error:
|
|
165
204
|
raise PredictionTaskResultsUnavailableError(
|
|
166
205
|
f"Error getting prediction task results: {error}"
|
|
@@ -169,13 +208,12 @@ class Classification:
|
|
|
169
208
|
# Add the prediction task uuid to the response before returning
|
|
170
209
|
payload["prediction_task_uuid"] = prediction_task_uuid
|
|
171
210
|
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
raise ValueError(f"Unsupported prediction type: {prediction_type}")
|
|
211
|
+
if prediction_type == "image":
|
|
212
|
+
return ClassificationPredictImageResponse.model_validate(payload)
|
|
213
|
+
elif prediction_type == "video":
|
|
214
|
+
return ClassificationPredictVideoResponse.model_validate(payload)
|
|
215
|
+
else:
|
|
216
|
+
raise ValueError(f"Unsupported prediction type: {prediction_type}")
|
|
179
217
|
|
|
180
218
|
##### Internal API methods #####
|
|
181
219
|
@overload
|
|
@@ -183,8 +221,9 @@ class Classification:
|
|
|
183
221
|
self,
|
|
184
222
|
media: Image,
|
|
185
223
|
model_name: str,
|
|
186
|
-
frames_per_second: int
|
|
187
|
-
timeout_seconds: int
|
|
224
|
+
frames_per_second: Optional[int],
|
|
225
|
+
timeout_seconds: Optional[int] = None,
|
|
226
|
+
**kwargs: Any,
|
|
188
227
|
) -> ClassificationPredictImageResponse: ...
|
|
189
228
|
|
|
190
229
|
@overload
|
|
@@ -192,20 +231,23 @@ class Classification:
|
|
|
192
231
|
self,
|
|
193
232
|
media: Video,
|
|
194
233
|
model_name: str,
|
|
195
|
-
frames_per_second: int
|
|
196
|
-
timeout_seconds: int
|
|
234
|
+
frames_per_second: Optional[int],
|
|
235
|
+
timeout_seconds: Optional[int] = None,
|
|
236
|
+
**kwargs: Any,
|
|
197
237
|
) -> ClassificationPredictVideoResponse: ...
|
|
198
238
|
|
|
199
239
|
async def _predict_unified(
|
|
200
240
|
self,
|
|
201
|
-
media: Image
|
|
241
|
+
media: Union[Image, Video],
|
|
202
242
|
model_name: str,
|
|
203
|
-
frames_per_second: int
|
|
204
|
-
timeout_seconds: int
|
|
205
|
-
|
|
243
|
+
frames_per_second: Optional[int],
|
|
244
|
+
timeout_seconds: Optional[int] = None,
|
|
245
|
+
**kwargs: Any,
|
|
246
|
+
) -> Union[ClassificationPredictImageResponse, ClassificationPredictVideoResponse]:
|
|
206
247
|
prediction_task_begin_response = await self._begin_prediction_task(
|
|
207
248
|
mime_type=media.mime_type,
|
|
208
249
|
frames_per_second=frames_per_second,
|
|
250
|
+
file_name=media.name,
|
|
209
251
|
)
|
|
210
252
|
|
|
211
253
|
await self._upload_media_to_prediction_task(
|
|
@@ -216,16 +258,22 @@ class Classification:
|
|
|
216
258
|
predict_data = {
|
|
217
259
|
"model_name": model_name,
|
|
218
260
|
"prediction_task_uuid": prediction_task_begin_response.prediction_task_uuid,
|
|
261
|
+
**kwargs,
|
|
219
262
|
}
|
|
220
263
|
predict_headers = {
|
|
221
264
|
"Authorization": f"Bearer {self._client.api_key}",
|
|
222
265
|
}
|
|
223
|
-
|
|
266
|
+
|
|
267
|
+
@self._backoff_on_429
|
|
268
|
+
async def _make_request():
|
|
224
269
|
async with aiohttp.ClientSession() as session:
|
|
225
270
|
async with session.post(
|
|
226
271
|
predict_url, data=predict_data, headers=predict_headers
|
|
227
272
|
) as resp:
|
|
228
273
|
resp.raise_for_status()
|
|
274
|
+
|
|
275
|
+
try:
|
|
276
|
+
await _make_request()
|
|
229
277
|
except ClientError as error:
|
|
230
278
|
raise PredictionTaskError("Error initiating prediction:", error)
|
|
231
279
|
|
|
@@ -246,7 +294,7 @@ class Classification:
|
|
|
246
294
|
self,
|
|
247
295
|
prediction_task_uuid: PredictionTaskUUID,
|
|
248
296
|
polling_interval: float = 1.0,
|
|
249
|
-
timeout_seconds: int
|
|
297
|
+
timeout_seconds: Optional[int] = None,
|
|
250
298
|
) -> PredictionTaskStatusResponse:
|
|
251
299
|
start_time = time.monotonic()
|
|
252
300
|
while True:
|
|
@@ -299,12 +347,16 @@ class Classification:
|
|
|
299
347
|
async def _begin_prediction_task(
|
|
300
348
|
self,
|
|
301
349
|
mime_type: str,
|
|
302
|
-
frames_per_second: int
|
|
350
|
+
frames_per_second: Optional[int],
|
|
351
|
+
file_name: Optional[str],
|
|
303
352
|
) -> _PredictionTaskBeginResponse:
|
|
304
353
|
url = f"{BASE_API_URL}/prediction-task/begin"
|
|
305
354
|
|
|
306
355
|
form_data = aiohttp.FormData()
|
|
307
356
|
form_data.add_field("mimetype", mime_type)
|
|
357
|
+
if file_name is not None:
|
|
358
|
+
form_data.add_field("file_name", file_name)
|
|
359
|
+
|
|
308
360
|
if frames_per_second is not None:
|
|
309
361
|
form_data.add_field("frames_per_second", str(frames_per_second))
|
|
310
362
|
|
|
@@ -312,11 +364,16 @@ class Classification:
|
|
|
312
364
|
"Authorization": f"Bearer {self._client.api_key}",
|
|
313
365
|
}
|
|
314
366
|
|
|
315
|
-
|
|
367
|
+
@self._backoff_on_429
|
|
368
|
+
async def _make_request():
|
|
316
369
|
async with aiohttp.ClientSession() as session:
|
|
317
370
|
async with session.post(url, data=form_data, headers=headers) as resp:
|
|
318
371
|
resp.raise_for_status()
|
|
319
372
|
payload = await resp.json()
|
|
373
|
+
return payload
|
|
374
|
+
|
|
375
|
+
try:
|
|
376
|
+
payload = await _make_request()
|
|
320
377
|
except ClientError as error:
|
|
321
378
|
raise PredictionTaskBeginError("Error beginning prediction task:", error)
|
|
322
379
|
|
|
@@ -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)
|
|
@@ -13,10 +15,11 @@ class Media:
|
|
|
13
15
|
"""Generic binary media + mime_type with conservative, non-destructive access."""
|
|
14
16
|
|
|
15
17
|
file_or_bytes: Union[bytes, BytesIO, BinaryIO, BufferedReader]
|
|
18
|
+
name: Optional[str]
|
|
16
19
|
mime_type: str
|
|
17
20
|
|
|
18
21
|
# Subclasses set this to enforce a family of mimetypes, e.g. ("image/",)
|
|
19
|
-
ACCEPT_PREFIXES: ClassVar[
|
|
22
|
+
ACCEPT_PREFIXES: ClassVar[Tuple[str, ...]] = ()
|
|
20
23
|
|
|
21
24
|
def __post_init__(self) -> None:
|
|
22
25
|
# Enforce subtype-specific mimetype families when specified.
|
|
@@ -31,23 +34,41 @@ class Media:
|
|
|
31
34
|
# ---------- Convenience constructors ----------
|
|
32
35
|
|
|
33
36
|
@classmethod
|
|
34
|
-
def from_bytes(
|
|
35
|
-
|
|
37
|
+
def from_bytes(
|
|
38
|
+
cls, data: bytes, mime_type: str, name: Optional[str] = None
|
|
39
|
+
) -> Self:
|
|
40
|
+
"""
|
|
41
|
+
Create a Media (or subclass) from raw bytes.
|
|
42
|
+
|
|
43
|
+
- `data`: Raw bytes of the media content.
|
|
44
|
+
- `mime_type`: The MIME type of the media (e.g., 'image/jpeg').
|
|
45
|
+
- `name`: Optional non-unique descriptive identifier provided by the user
|
|
46
|
+
for identifying or tracking responses to inputs.
|
|
47
|
+
"""
|
|
48
|
+
return cls(file_or_bytes=data, mime_type=mime_type, name=name)
|
|
36
49
|
|
|
37
50
|
@classmethod
|
|
38
|
-
def from_stream(
|
|
51
|
+
def from_stream(
|
|
52
|
+
cls, stream: BinaryIO, *, mime_type: str, name: Optional[str] = None
|
|
53
|
+
) -> Self:
|
|
39
54
|
"""
|
|
40
55
|
Accepts any readable binary stream (e.g., open('file', 'rb')).
|
|
41
56
|
Keeps the stream as-is; reading is deferred to bytes_io().
|
|
57
|
+
|
|
58
|
+
- `stream`: A readable binary stream.
|
|
59
|
+
- `mime_type`: The MIME type of the media (e.g., 'image/jpeg').
|
|
60
|
+
- `name`: Optional non-unique descriptive identifier provided by the user
|
|
61
|
+
for identifying or tracking responses to inputs.
|
|
42
62
|
"""
|
|
43
|
-
return cls(file_or_bytes=stream, mime_type=mime_type)
|
|
63
|
+
return cls(file_or_bytes=stream, mime_type=mime_type, name=name)
|
|
44
64
|
|
|
45
65
|
@classmethod
|
|
46
66
|
def from_path(
|
|
47
67
|
cls,
|
|
48
68
|
path: Union[str, os.PathLike[str]],
|
|
49
69
|
*,
|
|
50
|
-
mime_type: str
|
|
70
|
+
mime_type: Optional[str] = None,
|
|
71
|
+
name: Optional[str] = None,
|
|
51
72
|
guess_from_extension: bool = True,
|
|
52
73
|
read_into_memory: bool = False,
|
|
53
74
|
) -> Self:
|
|
@@ -57,6 +78,9 @@ class Media:
|
|
|
57
78
|
- `path`: Path to the file on disk.
|
|
58
79
|
- `mime_type`: Explicit mime type. If omitted and `guess_from_extension=True`,
|
|
59
80
|
we'll try to guess from the file extension.
|
|
81
|
+
- `name`: Optional non-unique descriptive identifier provided by the user
|
|
82
|
+
for identifying or tracking responses to inputs. If not provided, will
|
|
83
|
+
default to the filename from the path.
|
|
60
84
|
- `read_into_memory=True`: load file bytes into memory (closes file immediately).
|
|
61
85
|
Otherwise, keep an open file stream.
|
|
62
86
|
"""
|
|
@@ -72,12 +96,15 @@ class Media:
|
|
|
72
96
|
f"mime_type is required for {path} (no extension-based guess available)."
|
|
73
97
|
)
|
|
74
98
|
|
|
99
|
+
# Use provided name or extract from path
|
|
100
|
+
media_name = name if name is not None else path.name
|
|
101
|
+
|
|
75
102
|
if read_into_memory:
|
|
76
103
|
data = path.read_bytes()
|
|
77
|
-
return cls(file_or_bytes=data, mime_type=mt)
|
|
104
|
+
return cls(file_or_bytes=data, mime_type=mt, name=media_name)
|
|
78
105
|
else:
|
|
79
106
|
f = path.open("rb")
|
|
80
|
-
return cls(file_or_bytes=f, mime_type=mt)
|
|
107
|
+
return cls(file_or_bytes=f, mime_type=mt, name=media_name)
|
|
81
108
|
|
|
82
109
|
# ---------- Utilities ----------
|
|
83
110
|
|
|
@@ -118,7 +145,7 @@ class Media:
|
|
|
118
145
|
"Invalid media source: expected bytes, BytesIO, or a readable binary stream."
|
|
119
146
|
)
|
|
120
147
|
|
|
121
|
-
def size_bytes(self) -> int
|
|
148
|
+
def size_bytes(self) -> Optional[int]:
|
|
122
149
|
"""
|
|
123
150
|
Best-effort size inference without consuming the stream.
|
|
124
151
|
Returns None if size can't be determined cheaply.
|
|
@@ -152,20 +179,20 @@ class Media:
|
|
|
152
179
|
class Image(Media):
|
|
153
180
|
"""Media restricted to image/* mimetypes."""
|
|
154
181
|
|
|
155
|
-
ACCEPT_PREFIXES: ClassVar[
|
|
182
|
+
ACCEPT_PREFIXES: ClassVar[Tuple[str, ...]] = ("image/",)
|
|
156
183
|
|
|
157
184
|
|
|
158
185
|
@dataclass(frozen=True)
|
|
159
186
|
class Video(Media):
|
|
160
187
|
"""Media restricted to video/* mimetypes."""
|
|
161
188
|
|
|
162
|
-
ACCEPT_PREFIXES: ClassVar[
|
|
189
|
+
ACCEPT_PREFIXES: ClassVar[Tuple[str, ...]] = ("video/",)
|
|
163
190
|
|
|
164
191
|
|
|
165
192
|
# ---------- Helpers ----------
|
|
166
193
|
|
|
167
194
|
|
|
168
|
-
def _tell_safe(stream: BinaryIO) -> int
|
|
195
|
+
def _tell_safe(stream: BinaryIO) -> Optional[int]:
|
|
169
196
|
try:
|
|
170
197
|
if hasattr(stream, "tell"):
|
|
171
198
|
return stream.tell()
|
|
@@ -174,7 +201,7 @@ def _tell_safe(stream: BinaryIO) -> int | None:
|
|
|
174
201
|
return None
|
|
175
202
|
|
|
176
203
|
|
|
177
|
-
def _seek_safe(stream: BinaryIO, pos: int
|
|
204
|
+
def _seek_safe(stream: BinaryIO, pos: Optional[int]) -> None:
|
|
178
205
|
if pos is None:
|
|
179
206
|
return
|
|
180
207
|
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.1}/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
|