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.
Files changed (24) hide show
  1. dragoneye_python-0.4.1/PKG-INFO +10 -0
  2. dragoneye_python-0.4.1/dragoneye_python.egg-info/PKG-INFO +10 -0
  3. dragoneye_python-0.4.1/dragoneye_python.egg-info/requires.txt +5 -0
  4. {dragoneye_python-0.3.0 → dragoneye_python-0.4.1}/pyproject.toml +7 -3
  5. dragoneye_python-0.4.1/requirements.txt +5 -0
  6. {dragoneye_python-0.3.0 → dragoneye_python-0.4.1}/src/dragoneye/classification.py +88 -31
  7. dragoneye_python-0.4.1/src/dragoneye/client.py +29 -0
  8. {dragoneye_python-0.3.0 → dragoneye_python-0.4.1}/src/dragoneye/models.py +2 -3
  9. {dragoneye_python-0.3.0 → dragoneye_python-0.4.1}/src/dragoneye/types/media.py +41 -14
  10. dragoneye_python-0.3.0/PKG-INFO +0 -7
  11. dragoneye_python-0.3.0/dragoneye_python.egg-info/PKG-INFO +0 -7
  12. dragoneye_python-0.3.0/dragoneye_python.egg-info/requires.txt +0 -2
  13. dragoneye_python-0.3.0/requirements.txt +0 -2
  14. dragoneye_python-0.3.0/src/dragoneye/client.py +0 -18
  15. {dragoneye_python-0.3.0 → dragoneye_python-0.4.1}/README.md +0 -0
  16. {dragoneye_python-0.3.0 → dragoneye_python-0.4.1}/dragoneye_python.egg-info/SOURCES.txt +0 -0
  17. {dragoneye_python-0.3.0 → dragoneye_python-0.4.1}/dragoneye_python.egg-info/dependency_links.txt +0 -0
  18. {dragoneye_python-0.3.0 → dragoneye_python-0.4.1}/dragoneye_python.egg-info/top_level.txt +0 -0
  19. {dragoneye_python-0.3.0 → dragoneye_python-0.4.1}/setup.cfg +0 -0
  20. {dragoneye_python-0.3.0 → dragoneye_python-0.4.1}/src/dragoneye/__init__.py +0 -0
  21. {dragoneye_python-0.3.0 → dragoneye_python-0.4.1}/src/dragoneye/constants.py +0 -0
  22. {dragoneye_python-0.3.0 → dragoneye_python-0.4.1}/src/dragoneye/types/__init__.py +0 -0
  23. {dragoneye_python-0.3.0 → dragoneye_python-0.4.1}/src/dragoneye/types/common.py +0 -0
  24. {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
@@ -0,0 +1,5 @@
1
+ requests
2
+ pydantic>=2
3
+ typing-extensions>=4.0.0
4
+ backoff>=2.0.0
5
+ 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.3.0"
7
+ version = "0.4.1"
4
8
  requires-python = ">=3.8"
5
9
  dynamic = ["dependencies"]
6
- license = {text = "MIT"}
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"
@@ -0,0 +1,5 @@
1
+ requests
2
+ pydantic>=2
3
+ typing-extensions>=4.0.0
4
+ backoff>=2.0.0
5
+ aiohttp
@@ -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: dict[str, Any]
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: list[_MediaUploadUrl]
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 | None = None,
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 | None = None,
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
- async with aiohttp.ClientSession() as session:
108
- async with session.get(url, headers=headers) as resp:
109
- resp.raise_for_status()
110
- payload = await resp.json()
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
- ) -> ClassificationPredictImageResponse | ClassificationPredictVideoResponse: ...
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 | ClassificationPredictVideoResponse:
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
- try:
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
- match prediction_type:
173
- case "image":
174
- return ClassificationPredictImageResponse.model_validate(payload)
175
- case "video":
176
- return ClassificationPredictVideoResponse.model_validate(payload)
177
- case _: # pyright: ignore [reportUnnecessaryComparison]
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 | None,
187
- timeout_seconds: int | None = None,
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 | None,
196
- timeout_seconds: int | None = None,
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 | Video,
241
+ media: Union[Image, Video],
202
242
  model_name: str,
203
- frames_per_second: int | None,
204
- timeout_seconds: int | None = None,
205
- ) -> ClassificationPredictImageResponse | ClassificationPredictVideoResponse:
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
- try:
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 | None = None,
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 | None,
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
- try:
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: dict[
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, Self, Union
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[tuple[str, ...]] = ()
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(cls, data: bytes, mime_type: str) -> Self:
35
- return cls(file_or_bytes=data, mime_type=mime_type)
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(cls, stream: BinaryIO, *, mime_type: str) -> Self:
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 | None = None,
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 | None:
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[tuple[str, ...]] = ("image/",)
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[tuple[str, ...]] = ("video/",)
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 | None:
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 | None) -> None:
204
+ def _seek_safe(stream: BinaryIO, pos: Optional[int]) -> None:
178
205
  if pos is None:
179
206
  return
180
207
  try:
@@ -1,7 +0,0 @@
1
- Metadata-Version: 2.4
2
- Name: dragoneye-python
3
- Version: 0.3.0
4
- License: MIT
5
- Requires-Python: >=3.8
6
- Requires-Dist: requests
7
- Requires-Dist: pydantic>=2
@@ -1,7 +0,0 @@
1
- Metadata-Version: 2.4
2
- Name: dragoneye-python
3
- Version: 0.3.0
4
- License: MIT
5
- Requires-Python: >=3.8
6
- Requires-Dist: requests
7
- Requires-Dist: pydantic>=2
@@ -1,2 +0,0 @@
1
- requests
2
- pydantic>=2
@@ -1,2 +0,0 @@
1
- requests
2
- pydantic>=2
@@ -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)