dragoneye-python 0.5.1__tar.gz → 1.0.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.5.1 → dragoneye_python-1.0.0}/PKG-INFO +2 -1
- {dragoneye_python-0.5.1 → dragoneye_python-1.0.0}/dragoneye_python.egg-info/PKG-INFO +2 -1
- {dragoneye_python-0.5.1 → dragoneye_python-1.0.0}/dragoneye_python.egg-info/SOURCES.txt +1 -0
- dragoneye_python-0.5.1/requirements.txt → dragoneye_python-1.0.0/dragoneye_python.egg-info/requires.txt +2 -1
- {dragoneye_python-0.5.1 → dragoneye_python-1.0.0}/pyproject.toml +1 -1
- dragoneye_python-0.5.1/dragoneye_python.egg-info/requires.txt → dragoneye_python-1.0.0/requirements.txt +1 -0
- {dragoneye_python-0.5.1 → dragoneye_python-1.0.0}/src/dragoneye/__init__.py +12 -0
- {dragoneye_python-0.5.1 → dragoneye_python-1.0.0}/src/dragoneye/classification.py +56 -15
- dragoneye_python-1.0.0/src/dragoneye/parquet_deserializer.py +136 -0
- {dragoneye_python-0.5.1 → dragoneye_python-1.0.0}/README.md +0 -0
- {dragoneye_python-0.5.1 → dragoneye_python-1.0.0}/dragoneye_python.egg-info/dependency_links.txt +0 -0
- {dragoneye_python-0.5.1 → dragoneye_python-1.0.0}/dragoneye_python.egg-info/top_level.txt +0 -0
- {dragoneye_python-0.5.1 → dragoneye_python-1.0.0}/setup.cfg +0 -0
- {dragoneye_python-0.5.1 → dragoneye_python-1.0.0}/src/dragoneye/client.py +0 -0
- {dragoneye_python-0.5.1 → dragoneye_python-1.0.0}/src/dragoneye/constants.py +0 -0
- {dragoneye_python-0.5.1 → dragoneye_python-1.0.0}/src/dragoneye/models.py +0 -0
- {dragoneye_python-0.5.1 → dragoneye_python-1.0.0}/src/dragoneye/types/__init__.py +0 -0
- {dragoneye_python-0.5.1 → dragoneye_python-1.0.0}/src/dragoneye/types/common.py +0 -0
- {dragoneye_python-0.5.1 → dragoneye_python-1.0.0}/src/dragoneye/types/exception.py +0 -0
- {dragoneye_python-0.5.1 → dragoneye_python-1.0.0}/src/dragoneye/types/media.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: dragoneye-python
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 1.0.0
|
|
4
4
|
License-Expression: MIT
|
|
5
5
|
Requires-Python: >=3.8
|
|
6
6
|
Requires-Dist: requests
|
|
@@ -8,3 +8,4 @@ Requires-Dist: pydantic>=2
|
|
|
8
8
|
Requires-Dist: typing-extensions>=4.0.0
|
|
9
9
|
Requires-Dist: backoff>=2.0.0
|
|
10
10
|
Requires-Dist: aiohttp
|
|
11
|
+
Requires-Dist: polars>=1.0.0
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: dragoneye-python
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 1.0.0
|
|
4
4
|
License-Expression: MIT
|
|
5
5
|
Requires-Python: >=3.8
|
|
6
6
|
Requires-Dist: requests
|
|
@@ -8,3 +8,4 @@ Requires-Dist: pydantic>=2
|
|
|
8
8
|
Requires-Dist: typing-extensions>=4.0.0
|
|
9
9
|
Requires-Dist: backoff>=2.0.0
|
|
10
10
|
Requires-Dist: aiohttp
|
|
11
|
+
Requires-Dist: polars>=1.0.0
|
|
@@ -13,6 +13,13 @@ from .models import (
|
|
|
13
13
|
ClassificationVideoObjectPrediction,
|
|
14
14
|
)
|
|
15
15
|
from .types.common import NormalizedBbox
|
|
16
|
+
from .types.exception import (
|
|
17
|
+
PredictionTaskBeginError,
|
|
18
|
+
PredictionTaskError,
|
|
19
|
+
PredictionTaskResultsUnavailableError,
|
|
20
|
+
PredictionTimeoutException,
|
|
21
|
+
PredictionUploadError,
|
|
22
|
+
)
|
|
16
23
|
from .types.media import Image, Video
|
|
17
24
|
|
|
18
25
|
__all__ = [
|
|
@@ -28,5 +35,10 @@ __all__ = [
|
|
|
28
35
|
"Dragoneye",
|
|
29
36
|
"Image",
|
|
30
37
|
"NormalizedBbox",
|
|
38
|
+
"PredictionTaskBeginError",
|
|
39
|
+
"PredictionTaskError",
|
|
40
|
+
"PredictionTaskResultsUnavailableError",
|
|
41
|
+
"PredictionTimeoutException",
|
|
42
|
+
"PredictionUploadError",
|
|
31
43
|
"Video",
|
|
32
44
|
]
|
|
@@ -2,10 +2,12 @@ import asyncio
|
|
|
2
2
|
import logging
|
|
3
3
|
import time
|
|
4
4
|
from typing import TYPE_CHECKING, Any, Dict, List, Literal, Optional, Union, overload
|
|
5
|
+
from urllib.parse import urlencode
|
|
5
6
|
|
|
6
7
|
import aiohttp
|
|
7
8
|
import backoff
|
|
8
9
|
from aiohttp import ClientError
|
|
10
|
+
from multidict import CIMultiDict, CIMultiDictProxy
|
|
9
11
|
from pydantic import BaseModel
|
|
10
12
|
|
|
11
13
|
from .constants import FAILED_STATUS_PREFIX, PREDICTED_STATUS
|
|
@@ -14,6 +16,10 @@ from .models import (
|
|
|
14
16
|
ClassificationPredictVideoResponse,
|
|
15
17
|
PredictionTaskStatusResponse,
|
|
16
18
|
)
|
|
19
|
+
from .parquet_deserializer import (
|
|
20
|
+
deserialize_image_predictions,
|
|
21
|
+
deserialize_video_predictions,
|
|
22
|
+
)
|
|
17
23
|
from .types.common import (
|
|
18
24
|
BASE_API_URL,
|
|
19
25
|
PredictionTaskState,
|
|
@@ -129,8 +135,9 @@ class Classification:
|
|
|
129
135
|
"""
|
|
130
136
|
Given a prediction task UUID, return
|
|
131
137
|
"""
|
|
132
|
-
|
|
133
|
-
|
|
138
|
+
query = urlencode({"predictionTaskUuid": prediction_task_uuid})
|
|
139
|
+
url = f"{BASE_API_URL}/prediction-task/status?{query}"
|
|
140
|
+
headers = {"X-API-Key": self._client.api_key}
|
|
134
141
|
|
|
135
142
|
@self._backoff_on_429
|
|
136
143
|
async def _make_request():
|
|
@@ -187,31 +194,60 @@ class Classification:
|
|
|
187
194
|
async def _get_results_unified(
|
|
188
195
|
self, prediction_task_uuid: PredictionTaskUUID, prediction_type: PredictionType
|
|
189
196
|
) -> Union[ClassificationPredictImageResponse, ClassificationPredictVideoResponse]:
|
|
190
|
-
|
|
191
|
-
|
|
197
|
+
query = urlencode(
|
|
198
|
+
{
|
|
199
|
+
"predictionTaskUuid": prediction_task_uuid,
|
|
200
|
+
"response_version": "parquet",
|
|
201
|
+
}
|
|
202
|
+
)
|
|
203
|
+
url = f"{BASE_API_URL}/prediction-task/results?{query}"
|
|
204
|
+
headers = {"X-API-Key": self._client.api_key}
|
|
192
205
|
|
|
193
206
|
@self._backoff_on_429
|
|
194
|
-
async def _make_request():
|
|
207
|
+
async def _make_request() -> tuple[bytes, CIMultiDictProxy[str]]:
|
|
195
208
|
async with aiohttp.ClientSession() as session:
|
|
196
209
|
async with session.get(url, headers=headers) as resp:
|
|
210
|
+
if resp.status == 400:
|
|
211
|
+
payload = await resp.json()
|
|
212
|
+
raise PredictionTaskResultsUnavailableError(
|
|
213
|
+
payload.get("detail", "")
|
|
214
|
+
)
|
|
197
215
|
resp.raise_for_status()
|
|
198
|
-
|
|
199
|
-
|
|
216
|
+
body_bytes = await resp.read()
|
|
217
|
+
response_headers = CIMultiDictProxy(CIMultiDict(resp.headers))
|
|
218
|
+
return body_bytes, response_headers
|
|
200
219
|
|
|
201
220
|
try:
|
|
202
|
-
|
|
221
|
+
parquet_bytes, response_headers = await _make_request()
|
|
222
|
+
except PredictionTaskResultsUnavailableError:
|
|
223
|
+
raise
|
|
203
224
|
except ClientError as error:
|
|
204
225
|
raise PredictionTaskResultsUnavailableError(
|
|
205
226
|
f"Error getting prediction task results: {error}"
|
|
206
227
|
)
|
|
207
228
|
|
|
208
|
-
|
|
209
|
-
payload["prediction_task_uuid"] = prediction_task_uuid
|
|
229
|
+
original_file_name = response_headers.get("X-Original-File-Name")
|
|
210
230
|
|
|
211
231
|
if prediction_type == "image":
|
|
212
|
-
return ClassificationPredictImageResponse
|
|
232
|
+
return ClassificationPredictImageResponse(
|
|
233
|
+
object_predictions=deserialize_image_predictions(parquet_bytes),
|
|
234
|
+
prediction_task_uuid=prediction_task_uuid,
|
|
235
|
+
original_file_name=original_file_name,
|
|
236
|
+
)
|
|
213
237
|
elif prediction_type == "video":
|
|
214
|
-
|
|
238
|
+
frames_per_second_header = response_headers.get("X-Frames-Per-Second")
|
|
239
|
+
if frames_per_second_header is None:
|
|
240
|
+
raise PredictionTaskResultsUnavailableError(
|
|
241
|
+
"Missing X-Frames-Per-Second header on video prediction response"
|
|
242
|
+
)
|
|
243
|
+
return ClassificationPredictVideoResponse(
|
|
244
|
+
timestamp_us_to_predictions=deserialize_video_predictions(
|
|
245
|
+
parquet_bytes
|
|
246
|
+
),
|
|
247
|
+
frames_per_second=int(frames_per_second_header),
|
|
248
|
+
prediction_task_uuid=prediction_task_uuid,
|
|
249
|
+
original_file_name=original_file_name,
|
|
250
|
+
)
|
|
215
251
|
else:
|
|
216
252
|
raise ValueError(f"Unsupported prediction type: {prediction_type}")
|
|
217
253
|
|
|
@@ -261,7 +297,7 @@ class Classification:
|
|
|
261
297
|
**kwargs,
|
|
262
298
|
}
|
|
263
299
|
predict_headers = {
|
|
264
|
-
"
|
|
300
|
+
"X-API-Key": self._client.api_key,
|
|
265
301
|
}
|
|
266
302
|
|
|
267
303
|
@self._backoff_on_429
|
|
@@ -354,7 +390,7 @@ class Classification:
|
|
|
354
390
|
|
|
355
391
|
form_data = aiohttp.FormData()
|
|
356
392
|
form_data.add_field("mimetype", mime_type)
|
|
357
|
-
form_data.add_field("response_version", "
|
|
393
|
+
form_data.add_field("response_version", "parquet")
|
|
358
394
|
if file_name is not None:
|
|
359
395
|
form_data.add_field("file_name", file_name)
|
|
360
396
|
|
|
@@ -362,19 +398,24 @@ class Classification:
|
|
|
362
398
|
form_data.add_field("frames_per_second", str(frames_per_second))
|
|
363
399
|
|
|
364
400
|
headers = {
|
|
365
|
-
"
|
|
401
|
+
"X-API-Key": self._client.api_key,
|
|
366
402
|
}
|
|
367
403
|
|
|
368
404
|
@self._backoff_on_429
|
|
369
405
|
async def _make_request():
|
|
370
406
|
async with aiohttp.ClientSession() as session:
|
|
371
407
|
async with session.post(url, data=form_data, headers=headers) as resp:
|
|
408
|
+
if resp.status == 400:
|
|
409
|
+
error_payload = await resp.json()
|
|
410
|
+
raise PredictionTaskBeginError(error_payload.get("detail", ""))
|
|
372
411
|
resp.raise_for_status()
|
|
373
412
|
payload = await resp.json()
|
|
374
413
|
return payload
|
|
375
414
|
|
|
376
415
|
try:
|
|
377
416
|
payload = await _make_request()
|
|
417
|
+
except PredictionTaskBeginError:
|
|
418
|
+
raise
|
|
378
419
|
except ClientError as error:
|
|
379
420
|
raise PredictionTaskBeginError("Error beginning prediction task:", error)
|
|
380
421
|
|
|
@@ -0,0 +1,136 @@
|
|
|
1
|
+
"""Convert a zstd-compressed parquet prediction blob into typed SDK models.
|
|
2
|
+
|
|
3
|
+
Matches the server-side schema produced by the dragoneye pipeline:
|
|
4
|
+
|
|
5
|
+
image_id: String
|
|
6
|
+
normalized_bbox: Array(Float64, 4)
|
|
7
|
+
bbox_score: Float64
|
|
8
|
+
predictions: List(Struct{
|
|
9
|
+
category_id: Int64,
|
|
10
|
+
name: String,
|
|
11
|
+
score: Float64,
|
|
12
|
+
attributes: List(Struct{
|
|
13
|
+
attribute_id: Int64,
|
|
14
|
+
name: String,
|
|
15
|
+
options: List(Struct{
|
|
16
|
+
option_id: Int64,
|
|
17
|
+
name: String,
|
|
18
|
+
score: Float64,
|
|
19
|
+
}),
|
|
20
|
+
}),
|
|
21
|
+
})
|
|
22
|
+
|
|
23
|
+
For video parquet blobs the extra column ``timestamp_microseconds: Int64`` is
|
|
24
|
+
present.
|
|
25
|
+
"""
|
|
26
|
+
|
|
27
|
+
import io
|
|
28
|
+
from typing import Any, Dict, List
|
|
29
|
+
|
|
30
|
+
import polars as pl
|
|
31
|
+
|
|
32
|
+
from .models import (
|
|
33
|
+
ClassificationAttributeOption,
|
|
34
|
+
ClassificationAttributeResponse,
|
|
35
|
+
ClassificationCategory,
|
|
36
|
+
ClassificationCategoryPrediction,
|
|
37
|
+
ClassificationObjectPrediction,
|
|
38
|
+
ClassificationVideoObjectPrediction,
|
|
39
|
+
)
|
|
40
|
+
from .types.common import NormalizedBbox
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
def _predictions_to_models(
|
|
44
|
+
raw_predictions: List[Dict[str, Any]],
|
|
45
|
+
) -> List[ClassificationCategoryPrediction]:
|
|
46
|
+
return [
|
|
47
|
+
ClassificationCategoryPrediction(
|
|
48
|
+
category=ClassificationCategory(
|
|
49
|
+
id=pred["category_id"],
|
|
50
|
+
name=pred["name"],
|
|
51
|
+
score=pred["score"],
|
|
52
|
+
),
|
|
53
|
+
attributes=[
|
|
54
|
+
ClassificationAttributeResponse(
|
|
55
|
+
attribute_id=attr["attribute_id"],
|
|
56
|
+
name=attr["name"],
|
|
57
|
+
options=[
|
|
58
|
+
ClassificationAttributeOption(
|
|
59
|
+
option_id=opt["option_id"],
|
|
60
|
+
name=opt["name"],
|
|
61
|
+
score=opt["score"],
|
|
62
|
+
)
|
|
63
|
+
for opt in attr["options"]
|
|
64
|
+
],
|
|
65
|
+
)
|
|
66
|
+
for attr in pred["attributes"]
|
|
67
|
+
],
|
|
68
|
+
)
|
|
69
|
+
for pred in raw_predictions
|
|
70
|
+
]
|
|
71
|
+
|
|
72
|
+
|
|
73
|
+
def _read_dataframe(parquet_bytes: bytes, columns: List[str]) -> pl.DataFrame:
|
|
74
|
+
return pl.read_parquet(io.BytesIO(parquet_bytes), columns=columns)
|
|
75
|
+
|
|
76
|
+
|
|
77
|
+
def deserialize_image_predictions(
|
|
78
|
+
parquet_bytes: bytes,
|
|
79
|
+
) -> List[ClassificationObjectPrediction]:
|
|
80
|
+
df = _read_dataframe(
|
|
81
|
+
parquet_bytes, columns=["normalized_bbox", "predictions"]
|
|
82
|
+
).filter(pl.col("normalized_bbox").is_not_null())
|
|
83
|
+
return [
|
|
84
|
+
ClassificationObjectPrediction(
|
|
85
|
+
normalizedBbox=NormalizedBbox(tuple(row["normalized_bbox"])),
|
|
86
|
+
predictions=_predictions_to_models(row["predictions"] or []),
|
|
87
|
+
)
|
|
88
|
+
for row in df.select("normalized_bbox", "predictions").iter_rows(named=True)
|
|
89
|
+
]
|
|
90
|
+
|
|
91
|
+
|
|
92
|
+
def deserialize_video_predictions(
|
|
93
|
+
parquet_bytes: bytes,
|
|
94
|
+
) -> Dict[int, List[ClassificationVideoObjectPrediction]]:
|
|
95
|
+
df = _read_dataframe(
|
|
96
|
+
parquet_bytes,
|
|
97
|
+
columns=[
|
|
98
|
+
"image_id",
|
|
99
|
+
"normalized_bbox",
|
|
100
|
+
"predictions",
|
|
101
|
+
"timestamp_microseconds",
|
|
102
|
+
],
|
|
103
|
+
)
|
|
104
|
+
|
|
105
|
+
# Frames with detections contribute rows; frames without still appear with a
|
|
106
|
+
# null bbox so the caller can distinguish "no detections" from "frame not
|
|
107
|
+
# processed".
|
|
108
|
+
real_df = df.filter(pl.col("normalized_bbox").is_not_null())
|
|
109
|
+
|
|
110
|
+
result: Dict[int, List[ClassificationVideoObjectPrediction]] = {
|
|
111
|
+
int(ts): [
|
|
112
|
+
ClassificationVideoObjectPrediction(
|
|
113
|
+
normalizedBbox=NormalizedBbox(tuple(row["normalized_bbox"])),
|
|
114
|
+
predictions=_predictions_to_models(row["predictions"] or []),
|
|
115
|
+
frame_id=row["image_id"],
|
|
116
|
+
timestamp_microseconds=int(ts),
|
|
117
|
+
)
|
|
118
|
+
for row in group.select(
|
|
119
|
+
"normalized_bbox", "predictions", "image_id"
|
|
120
|
+
).iter_rows(named=True)
|
|
121
|
+
]
|
|
122
|
+
for (ts,), group in real_df.group_by(
|
|
123
|
+
"timestamp_microseconds",
|
|
124
|
+
)
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
# Include no-detection frames with empty prediction lists.
|
|
128
|
+
if len(df):
|
|
129
|
+
for row in df.select("timestamp_microseconds").unique().iter_rows(named=True):
|
|
130
|
+
ts_raw = row["timestamp_microseconds"]
|
|
131
|
+
if ts_raw is None:
|
|
132
|
+
continue
|
|
133
|
+
ts = int(ts_raw)
|
|
134
|
+
result.setdefault(ts, [])
|
|
135
|
+
|
|
136
|
+
return result
|
|
File without changes
|
{dragoneye_python-0.5.1 → dragoneye_python-1.0.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
|
|
File without changes
|
|
File without changes
|