modelstudio-sdk 0.0.0.dev0__py3-none-any.whl
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.
- modelstudio/__init__.py +25 -0
- modelstudio/_http.py +116 -0
- modelstudio/_pandas.py +33 -0
- modelstudio/_polling.py +59 -0
- modelstudio/_version.py +1 -0
- modelstudio/client.py +108 -0
- modelstudio/exceptions.py +80 -0
- modelstudio/models/__init__.py +81 -0
- modelstudio/models/annotations.py +80 -0
- modelstudio/models/categories.py +106 -0
- modelstudio/models/common.py +18 -0
- modelstudio/models/datasets.py +55 -0
- modelstudio/models/deletion.py +29 -0
- modelstudio/models/exports.py +32 -0
- modelstudio/models/few_shot.py +41 -0
- modelstudio/models/filters.py +81 -0
- modelstudio/models/history.py +58 -0
- modelstudio/models/images.py +91 -0
- modelstudio/models/imports.py +141 -0
- modelstudio/models/media.py +37 -0
- modelstudio/models/merge.py +51 -0
- modelstudio/models/metrics.py +180 -0
- modelstudio/models/oversample.py +41 -0
- modelstudio/models/splits.py +112 -0
- modelstudio/models/validation.py +89 -0
- modelstudio/resources/__init__.py +1 -0
- modelstudio/resources/dataset.py +732 -0
- modelstudio/resources/split.py +140 -0
- modelstudio_sdk-0.0.0.dev0.dist-info/METADATA +513 -0
- modelstudio_sdk-0.0.0.dev0.dist-info/RECORD +32 -0
- modelstudio_sdk-0.0.0.dev0.dist-info/WHEEL +4 -0
- modelstudio_sdk-0.0.0.dev0.dist-info/licenses/LICENSE +21 -0
modelstudio/__init__.py
ADDED
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
"""Model Studio Python SDK."""
|
|
2
|
+
|
|
3
|
+
from modelstudio._version import __version__
|
|
4
|
+
from modelstudio.client import ModelStudioClient
|
|
5
|
+
from modelstudio.exceptions import (
|
|
6
|
+
AuthenticationError,
|
|
7
|
+
BadRequestError,
|
|
8
|
+
ConflictError,
|
|
9
|
+
ModelStudioError,
|
|
10
|
+
NotFoundError,
|
|
11
|
+
ServerError,
|
|
12
|
+
TimeoutError,
|
|
13
|
+
)
|
|
14
|
+
|
|
15
|
+
__all__ = [
|
|
16
|
+
"__version__",
|
|
17
|
+
"ModelStudioClient",
|
|
18
|
+
"ModelStudioError",
|
|
19
|
+
"BadRequestError",
|
|
20
|
+
"AuthenticationError",
|
|
21
|
+
"NotFoundError",
|
|
22
|
+
"ConflictError",
|
|
23
|
+
"ServerError",
|
|
24
|
+
"TimeoutError",
|
|
25
|
+
]
|
modelstudio/_http.py
ADDED
|
@@ -0,0 +1,116 @@
|
|
|
1
|
+
"""HTTP transport layer wrapping httpx with auth and error mapping."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from typing import Any
|
|
6
|
+
|
|
7
|
+
import httpx
|
|
8
|
+
|
|
9
|
+
from modelstudio.exceptions import (
|
|
10
|
+
STATUS_CODE_MAP,
|
|
11
|
+
AuthenticationError,
|
|
12
|
+
ModelStudioError,
|
|
13
|
+
ServerError,
|
|
14
|
+
)
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
class HttpTransport:
|
|
18
|
+
"""Thin httpx wrapper that injects auth headers and maps errors to typed exceptions."""
|
|
19
|
+
|
|
20
|
+
def __init__(
|
|
21
|
+
self,
|
|
22
|
+
base_url: str,
|
|
23
|
+
jwt_token: str | None = None,
|
|
24
|
+
timeout: float = 30.0,
|
|
25
|
+
org_name: str | None = None,
|
|
26
|
+
) -> None:
|
|
27
|
+
headers: dict[str, str] = {
|
|
28
|
+
"X-Source": "modelstudio-sdk",
|
|
29
|
+
"Accept": "application/json",
|
|
30
|
+
}
|
|
31
|
+
if jwt_token:
|
|
32
|
+
headers["Authorization"] = f"Bearer {jwt_token}"
|
|
33
|
+
if org_name:
|
|
34
|
+
headers["X-Org-Name"] = org_name
|
|
35
|
+
|
|
36
|
+
self._client = httpx.Client(
|
|
37
|
+
base_url=base_url.rstrip("/"),
|
|
38
|
+
headers=headers,
|
|
39
|
+
timeout=timeout,
|
|
40
|
+
)
|
|
41
|
+
|
|
42
|
+
def close(self) -> None:
|
|
43
|
+
self._client.close()
|
|
44
|
+
|
|
45
|
+
# --- HTTP methods ---
|
|
46
|
+
|
|
47
|
+
def get(self, path: str, params: dict[str, Any] | None = None) -> Any:
|
|
48
|
+
resp = self._client.get(path, params=params)
|
|
49
|
+
return self._handle(resp)
|
|
50
|
+
|
|
51
|
+
def post(self, path: str, json: Any = None) -> Any:
|
|
52
|
+
resp = self._client.post(path, json=json)
|
|
53
|
+
return self._handle(resp)
|
|
54
|
+
|
|
55
|
+
def put(self, path: str, json: Any = None) -> Any:
|
|
56
|
+
resp = self._client.put(path, json=json)
|
|
57
|
+
return self._handle(resp)
|
|
58
|
+
|
|
59
|
+
def patch(self, path: str, json: Any = None) -> Any:
|
|
60
|
+
resp = self._client.patch(path, json=json)
|
|
61
|
+
return self._handle(resp)
|
|
62
|
+
|
|
63
|
+
def delete(self, path: str, json: Any = None, params: dict[str, Any] | None = None) -> Any:
|
|
64
|
+
if json is not None:
|
|
65
|
+
req = self._client.build_request("DELETE", path, json=json, params=params)
|
|
66
|
+
resp = self._client.send(req)
|
|
67
|
+
else:
|
|
68
|
+
resp = self._client.delete(path, params=params)
|
|
69
|
+
return self._handle(resp)
|
|
70
|
+
|
|
71
|
+
def delete_with_body(self, path: str, json: Any = None) -> Any:
|
|
72
|
+
"""DELETE with a JSON request body (e.g. bulk annotation delete)."""
|
|
73
|
+
req = self._client.build_request("DELETE", path, json=json)
|
|
74
|
+
resp = self._client.send(req)
|
|
75
|
+
return self._handle(resp)
|
|
76
|
+
|
|
77
|
+
def post_accepted(self, path: str, json: Any = None) -> Any:
|
|
78
|
+
"""POST that expects 202 Accepted (async operations)."""
|
|
79
|
+
resp = self._client.post(path, json=json)
|
|
80
|
+
return self._handle(resp)
|
|
81
|
+
|
|
82
|
+
# --- Response handling ---
|
|
83
|
+
|
|
84
|
+
def _handle(self, resp: httpx.Response) -> Any:
|
|
85
|
+
if resp.status_code == 204:
|
|
86
|
+
return None
|
|
87
|
+
|
|
88
|
+
if resp.is_success:
|
|
89
|
+
if not resp.content:
|
|
90
|
+
return None
|
|
91
|
+
return resp.json()
|
|
92
|
+
|
|
93
|
+
self._raise_for_status(resp)
|
|
94
|
+
|
|
95
|
+
def _raise_for_status(self, resp: httpx.Response) -> None:
|
|
96
|
+
error_code = "Unknown"
|
|
97
|
+
message = f"HTTP {resp.status_code}"
|
|
98
|
+
|
|
99
|
+
try:
|
|
100
|
+
body = resp.json()
|
|
101
|
+
error_code = body.get("error", error_code)
|
|
102
|
+
message = body.get("message", message)
|
|
103
|
+
except Exception:
|
|
104
|
+
message = resp.text or message
|
|
105
|
+
|
|
106
|
+
exc_class = STATUS_CODE_MAP.get(resp.status_code)
|
|
107
|
+
if exc_class:
|
|
108
|
+
raise exc_class(message=message, error_code=error_code)
|
|
109
|
+
|
|
110
|
+
if resp.status_code == 401:
|
|
111
|
+
raise AuthenticationError(message=message)
|
|
112
|
+
if resp.status_code >= 500:
|
|
113
|
+
raise ServerError(message=message)
|
|
114
|
+
raise ModelStudioError(
|
|
115
|
+
message=message, status_code=resp.status_code, error_code=error_code
|
|
116
|
+
)
|
modelstudio/_pandas.py
ADDED
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
"""Optional pandas DataFrame helpers (guarded import)."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from typing import TYPE_CHECKING, Any, TypeVar
|
|
6
|
+
|
|
7
|
+
from pydantic import BaseModel
|
|
8
|
+
|
|
9
|
+
if TYPE_CHECKING:
|
|
10
|
+
import pandas as pd
|
|
11
|
+
|
|
12
|
+
T = TypeVar("T", bound=BaseModel)
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
def _get_pandas() -> Any:
|
|
16
|
+
try:
|
|
17
|
+
import pandas
|
|
18
|
+
|
|
19
|
+
return pandas
|
|
20
|
+
except ImportError:
|
|
21
|
+
raise ImportError(
|
|
22
|
+
"pandas is required for DataFrame operations. "
|
|
23
|
+
"Install it with: pip install 'modelstudio-sdk[pandas]'"
|
|
24
|
+
) from None
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
def to_dataframe(items: list[T]) -> pd.DataFrame:
|
|
28
|
+
"""Convert a list of Pydantic models to a pandas DataFrame."""
|
|
29
|
+
pd = _get_pandas()
|
|
30
|
+
if not items:
|
|
31
|
+
return pd.DataFrame()
|
|
32
|
+
rows = [item.model_dump() for item in items]
|
|
33
|
+
return pd.DataFrame(rows)
|
modelstudio/_polling.py
ADDED
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
"""Poller for async operations (clone, import, finalize)."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import time
|
|
6
|
+
from collections.abc import Callable
|
|
7
|
+
from typing import Any
|
|
8
|
+
|
|
9
|
+
from modelstudio.exceptions import TimeoutError
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
class OperationPoller:
|
|
13
|
+
"""Polls an async operation endpoint until a terminal status is reached."""
|
|
14
|
+
|
|
15
|
+
def __init__(
|
|
16
|
+
self,
|
|
17
|
+
transport: Any,
|
|
18
|
+
poll_url: str,
|
|
19
|
+
terminal_statuses: set[str] | None = None,
|
|
20
|
+
interval: float = 2.0,
|
|
21
|
+
max_wait: float = 300.0,
|
|
22
|
+
) -> None:
|
|
23
|
+
self._transport = transport
|
|
24
|
+
self._poll_url = poll_url
|
|
25
|
+
self._terminal = terminal_statuses or {"COMPLETED", "FAILED", "ERROR", "DONE"}
|
|
26
|
+
self._interval = interval
|
|
27
|
+
self._max_wait = max_wait
|
|
28
|
+
|
|
29
|
+
def poll_once(self) -> dict[str, Any]:
|
|
30
|
+
"""Single poll, returns the status response dict."""
|
|
31
|
+
return self._transport.get(self._poll_url) # type: ignore[no-any-return]
|
|
32
|
+
|
|
33
|
+
def wait(self, callback: Callable[[dict[str, Any]], None] | None = None) -> dict[str, Any]:
|
|
34
|
+
"""Block until terminal status. Optional progress callback called on each poll."""
|
|
35
|
+
elapsed = 0.0
|
|
36
|
+
while elapsed < self._max_wait:
|
|
37
|
+
result = self.poll_once()
|
|
38
|
+
if callback:
|
|
39
|
+
callback(result)
|
|
40
|
+
|
|
41
|
+
status = self._extract_status(result)
|
|
42
|
+
if status and status.upper() in self._terminal:
|
|
43
|
+
return result
|
|
44
|
+
|
|
45
|
+
time.sleep(self._interval)
|
|
46
|
+
elapsed += self._interval
|
|
47
|
+
|
|
48
|
+
raise TimeoutError(
|
|
49
|
+
f"Operation at {self._poll_url} did not complete within {self._max_wait}s"
|
|
50
|
+
)
|
|
51
|
+
|
|
52
|
+
@staticmethod
|
|
53
|
+
def _extract_status(data: dict[str, Any]) -> str | None:
|
|
54
|
+
"""Extract status from common response shapes."""
|
|
55
|
+
for key in ("status", "clone_status", "finalize_status", "import_status"):
|
|
56
|
+
val = data.get(key)
|
|
57
|
+
if val is not None:
|
|
58
|
+
return str(val)
|
|
59
|
+
return None
|
modelstudio/_version.py
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
__version__ = "0.0.0.dev0"
|
modelstudio/client.py
ADDED
|
@@ -0,0 +1,108 @@
|
|
|
1
|
+
"""ModelStudioClient — main entry point for the SDK."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import os
|
|
6
|
+
from uuid import UUID
|
|
7
|
+
|
|
8
|
+
from modelstudio._http import HttpTransport
|
|
9
|
+
from modelstudio.resources.dataset import Dataset, DatasetsCollection
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
class ModelStudioClient:
|
|
13
|
+
"""Client for the Model Studio REST API.
|
|
14
|
+
|
|
15
|
+
Usage::
|
|
16
|
+
|
|
17
|
+
# Auto-configured from environment variables
|
|
18
|
+
client = ModelStudioClient.from_env()
|
|
19
|
+
|
|
20
|
+
# Or explicit
|
|
21
|
+
client = ModelStudioClient(
|
|
22
|
+
base_url="http://localhost:8081",
|
|
23
|
+
jwt_token="eyJhbG...",
|
|
24
|
+
)
|
|
25
|
+
|
|
26
|
+
# List datasets
|
|
27
|
+
datasets = client.datasets.list()
|
|
28
|
+
|
|
29
|
+
# Work with a specific dataset
|
|
30
|
+
ds = client.dataset("uuid-here")
|
|
31
|
+
overview = ds.overview()
|
|
32
|
+
"""
|
|
33
|
+
|
|
34
|
+
def __init__(
|
|
35
|
+
self,
|
|
36
|
+
base_url: str,
|
|
37
|
+
jwt_token: str | None = None,
|
|
38
|
+
timeout: float = 30.0,
|
|
39
|
+
org_name: str | None = None,
|
|
40
|
+
) -> None:
|
|
41
|
+
self._transport = HttpTransport(
|
|
42
|
+
base_url=base_url,
|
|
43
|
+
jwt_token=jwt_token,
|
|
44
|
+
timeout=timeout,
|
|
45
|
+
org_name=org_name,
|
|
46
|
+
)
|
|
47
|
+
self._base_url = base_url.rstrip("/")
|
|
48
|
+
|
|
49
|
+
@classmethod
|
|
50
|
+
def from_env(cls) -> ModelStudioClient:
|
|
51
|
+
"""Create client from environment variables.
|
|
52
|
+
|
|
53
|
+
Reads:
|
|
54
|
+
MODEL_STUDIO_API_URL: API base URL (required)
|
|
55
|
+
MODEL_STUDIO_JWT: JWT token (optional)
|
|
56
|
+
MODEL_STUDIO_ORG: Organization name (optional)
|
|
57
|
+
"""
|
|
58
|
+
base_url = os.environ.get("MODEL_STUDIO_API_URL")
|
|
59
|
+
if not base_url:
|
|
60
|
+
raise ValueError(
|
|
61
|
+
"MODEL_STUDIO_API_URL environment variable is required. "
|
|
62
|
+
"Set it to the Model Studio API base URL "
|
|
63
|
+
"(e.g. http://model-studio-api-service.model-studio.svc.cluster.local)"
|
|
64
|
+
)
|
|
65
|
+
return cls(
|
|
66
|
+
base_url=base_url,
|
|
67
|
+
jwt_token=os.environ.get("MODEL_STUDIO_JWT"),
|
|
68
|
+
org_name=os.environ.get("MODEL_STUDIO_ORG"),
|
|
69
|
+
)
|
|
70
|
+
|
|
71
|
+
@property
|
|
72
|
+
def datasets(self) -> DatasetsCollection:
|
|
73
|
+
"""Access dataset collection methods (list, create, merge)."""
|
|
74
|
+
return DatasetsCollection(self._transport)
|
|
75
|
+
|
|
76
|
+
def dataset(self, dataset_id: str | UUID) -> Dataset:
|
|
77
|
+
"""Get a Dataset resource bound to a specific dataset ID."""
|
|
78
|
+
return Dataset(self._transport, str(dataset_id))
|
|
79
|
+
|
|
80
|
+
def full_image_url(
|
|
81
|
+
self, dataset_id: str | UUID, image_id: str | UUID, ext: str = "png"
|
|
82
|
+
) -> str:
|
|
83
|
+
"""Build a full image URL for direct browser/download use."""
|
|
84
|
+
return f"{self._base_url}/api/v1/media/images/{dataset_id}/{image_id}.{ext}"
|
|
85
|
+
|
|
86
|
+
def thumbnail_url(
|
|
87
|
+
self,
|
|
88
|
+
dataset_id: str | UUID,
|
|
89
|
+
image_id: str | UUID,
|
|
90
|
+
size: int,
|
|
91
|
+
hash: str,
|
|
92
|
+
ext: str = "webp",
|
|
93
|
+
) -> str:
|
|
94
|
+
"""Build a thumbnail URL."""
|
|
95
|
+
return (
|
|
96
|
+
f"{self._base_url}/api/v1/media/thumbs"
|
|
97
|
+
f"/{dataset_id}/{image_id}_{size}.{ext}?h={hash}"
|
|
98
|
+
)
|
|
99
|
+
|
|
100
|
+
def close(self) -> None:
|
|
101
|
+
"""Close the underlying HTTP connection."""
|
|
102
|
+
self._transport.close()
|
|
103
|
+
|
|
104
|
+
def __enter__(self) -> ModelStudioClient:
|
|
105
|
+
return self
|
|
106
|
+
|
|
107
|
+
def __exit__(self, *args: object) -> None:
|
|
108
|
+
self.close()
|
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
"""Typed exception hierarchy mapped to API error envelope."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
class ModelStudioError(Exception):
|
|
7
|
+
"""Base exception for all Model Studio SDK errors."""
|
|
8
|
+
|
|
9
|
+
def __init__(
|
|
10
|
+
self,
|
|
11
|
+
message: str,
|
|
12
|
+
status_code: int | None = None,
|
|
13
|
+
error_code: str | None = None,
|
|
14
|
+
) -> None:
|
|
15
|
+
super().__init__(message)
|
|
16
|
+
self.message = message
|
|
17
|
+
self.status_code = status_code
|
|
18
|
+
self.error_code = error_code
|
|
19
|
+
|
|
20
|
+
def __repr__(self) -> str:
|
|
21
|
+
return (
|
|
22
|
+
f"{self.__class__.__name__}("
|
|
23
|
+
f"status_code={self.status_code}, "
|
|
24
|
+
f"error_code={self.error_code!r}, "
|
|
25
|
+
f"message={self.message!r})"
|
|
26
|
+
)
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
class BadRequestError(ModelStudioError):
|
|
30
|
+
"""400 Bad Request — invalid input or validation failure."""
|
|
31
|
+
|
|
32
|
+
def __init__(self, message: str, error_code: str = "BadRequest") -> None:
|
|
33
|
+
super().__init__(message, status_code=400, error_code=error_code)
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
class AuthenticationError(ModelStudioError):
|
|
37
|
+
"""401 Unauthorized — missing or invalid JWT token."""
|
|
38
|
+
|
|
39
|
+
def __init__(self, message: str = "Unauthorized", error_code: str = "Unauthorized") -> None:
|
|
40
|
+
super().__init__(message, status_code=401, error_code=error_code)
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
class NotFoundError(ModelStudioError):
|
|
44
|
+
"""404 Not Found — resource does not exist."""
|
|
45
|
+
|
|
46
|
+
def __init__(self, message: str, error_code: str = "NotFound") -> None:
|
|
47
|
+
super().__init__(message, status_code=404, error_code=error_code)
|
|
48
|
+
|
|
49
|
+
|
|
50
|
+
class ConflictError(ModelStudioError):
|
|
51
|
+
"""409 Conflict — operation conflicts with current state."""
|
|
52
|
+
|
|
53
|
+
def __init__(self, message: str, error_code: str = "Conflict") -> None:
|
|
54
|
+
super().__init__(message, status_code=409, error_code=error_code)
|
|
55
|
+
|
|
56
|
+
|
|
57
|
+
class ServerError(ModelStudioError):
|
|
58
|
+
"""500 Internal Server Error — unexpected server failure."""
|
|
59
|
+
|
|
60
|
+
def __init__(
|
|
61
|
+
self, message: str = "Internal server error", error_code: str = "InternalServerError"
|
|
62
|
+
) -> None:
|
|
63
|
+
super().__init__(message, status_code=500, error_code=error_code)
|
|
64
|
+
|
|
65
|
+
|
|
66
|
+
class TimeoutError(ModelStudioError):
|
|
67
|
+
"""Polling timeout — async operation did not complete in time."""
|
|
68
|
+
|
|
69
|
+
def __init__(self, message: str = "Operation timed out") -> None:
|
|
70
|
+
super().__init__(message, status_code=None, error_code="Timeout")
|
|
71
|
+
|
|
72
|
+
|
|
73
|
+
# Map HTTP status codes to exception classes
|
|
74
|
+
STATUS_CODE_MAP: dict[int, type[ModelStudioError]] = {
|
|
75
|
+
400: BadRequestError,
|
|
76
|
+
401: AuthenticationError,
|
|
77
|
+
404: NotFoundError,
|
|
78
|
+
409: ConflictError,
|
|
79
|
+
500: ServerError,
|
|
80
|
+
}
|
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
"""Pydantic v2 response/request models matching the Model Studio API."""
|
|
2
|
+
|
|
3
|
+
from modelstudio.models.annotations import (
|
|
4
|
+
AnnotationModel,
|
|
5
|
+
CreateAnnotationRequest,
|
|
6
|
+
DeleteAnnotationsResponse,
|
|
7
|
+
PaginatedAnnotationModel,
|
|
8
|
+
UpdateAnnotationRequest,
|
|
9
|
+
)
|
|
10
|
+
from modelstudio.models.common import PagedResponse
|
|
11
|
+
from modelstudio.models.images import (
|
|
12
|
+
DatasetImageModel,
|
|
13
|
+
ImageModel,
|
|
14
|
+
MarkSyntheticResponse,
|
|
15
|
+
PaginatedImageModel,
|
|
16
|
+
RemoveEmptyImagesResponse,
|
|
17
|
+
SyntheticStatsModel,
|
|
18
|
+
)
|
|
19
|
+
from modelstudio.models.imports import (
|
|
20
|
+
ImportJobListModel,
|
|
21
|
+
ImportJobModel,
|
|
22
|
+
ImportJobQueuedModel,
|
|
23
|
+
ImportLogEntryModel,
|
|
24
|
+
ImportQueuedModel,
|
|
25
|
+
ImportStatusModel,
|
|
26
|
+
PreImportValidationModel,
|
|
27
|
+
ValidationStartedModel,
|
|
28
|
+
)
|
|
29
|
+
from modelstudio.models.media import (
|
|
30
|
+
MediaJobActionModel,
|
|
31
|
+
MediaJobErrorModel,
|
|
32
|
+
MediaJobStatusModel,
|
|
33
|
+
)
|
|
34
|
+
from modelstudio.models.splits import (
|
|
35
|
+
AlgorithmStats,
|
|
36
|
+
ClassAwareRedistributeResponse,
|
|
37
|
+
ClassError,
|
|
38
|
+
CreateSplitsResponse,
|
|
39
|
+
DistributionError,
|
|
40
|
+
LeakageResponse,
|
|
41
|
+
RedistributeResponse,
|
|
42
|
+
SmartRedistributeResponse,
|
|
43
|
+
SplitModel,
|
|
44
|
+
SplitStats,
|
|
45
|
+
)
|
|
46
|
+
|
|
47
|
+
__all__ = [
|
|
48
|
+
"AnnotationModel",
|
|
49
|
+
"CreateAnnotationRequest",
|
|
50
|
+
"DeleteAnnotationsResponse",
|
|
51
|
+
"PaginatedAnnotationModel",
|
|
52
|
+
"UpdateAnnotationRequest",
|
|
53
|
+
"PagedResponse",
|
|
54
|
+
"DatasetImageModel",
|
|
55
|
+
"ImageModel",
|
|
56
|
+
"MarkSyntheticResponse",
|
|
57
|
+
"PaginatedImageModel",
|
|
58
|
+
"RemoveEmptyImagesResponse",
|
|
59
|
+
"SyntheticStatsModel",
|
|
60
|
+
"ImportJobListModel",
|
|
61
|
+
"ImportJobModel",
|
|
62
|
+
"ImportJobQueuedModel",
|
|
63
|
+
"ImportLogEntryModel",
|
|
64
|
+
"ImportQueuedModel",
|
|
65
|
+
"ImportStatusModel",
|
|
66
|
+
"PreImportValidationModel",
|
|
67
|
+
"ValidationStartedModel",
|
|
68
|
+
"MediaJobActionModel",
|
|
69
|
+
"MediaJobErrorModel",
|
|
70
|
+
"MediaJobStatusModel",
|
|
71
|
+
"AlgorithmStats",
|
|
72
|
+
"ClassAwareRedistributeResponse",
|
|
73
|
+
"ClassError",
|
|
74
|
+
"CreateSplitsResponse",
|
|
75
|
+
"DistributionError",
|
|
76
|
+
"LeakageResponse",
|
|
77
|
+
"RedistributeResponse",
|
|
78
|
+
"SmartRedistributeResponse",
|
|
79
|
+
"SplitModel",
|
|
80
|
+
"SplitStats",
|
|
81
|
+
]
|
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
"""Annotation-related models."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from typing import Any
|
|
6
|
+
from uuid import UUID
|
|
7
|
+
|
|
8
|
+
from pydantic import BaseModel
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
class AnnotationModel(BaseModel):
|
|
12
|
+
"""COCO annotation."""
|
|
13
|
+
|
|
14
|
+
annotation_id: int
|
|
15
|
+
image_id: UUID
|
|
16
|
+
category_id: int
|
|
17
|
+
segmentation: Any | None = None
|
|
18
|
+
bbox: list[float] = []
|
|
19
|
+
ignore: bool = False
|
|
20
|
+
iscrowd: bool = False
|
|
21
|
+
area: float = 0.0
|
|
22
|
+
keypoints: Any | None = None
|
|
23
|
+
num_keypoints: int | None = None
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
class CreateAnnotationRequest(BaseModel):
|
|
27
|
+
"""Request for creating an annotation."""
|
|
28
|
+
|
|
29
|
+
annotation_id: int | None = None
|
|
30
|
+
image_id: UUID
|
|
31
|
+
category_id: int
|
|
32
|
+
segmentation: Any | None = None
|
|
33
|
+
bbox: list[float] = []
|
|
34
|
+
ignore: bool = False
|
|
35
|
+
iscrowd: bool = False
|
|
36
|
+
area: float = 0.0
|
|
37
|
+
keypoints: Any | None = None
|
|
38
|
+
num_keypoints: int | None = None
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
class UpdateAnnotationRequest(BaseModel):
|
|
42
|
+
"""Request for updating an annotation."""
|
|
43
|
+
|
|
44
|
+
category_id: int | None = None
|
|
45
|
+
segmentation: Any | None = None
|
|
46
|
+
bbox: list[float] | None = None
|
|
47
|
+
area: float | None = None
|
|
48
|
+
iscrowd: bool | None = None
|
|
49
|
+
ignore: bool | None = None
|
|
50
|
+
|
|
51
|
+
|
|
52
|
+
class DeleteAnnotationsResponse(BaseModel):
|
|
53
|
+
"""Response from DELETE /datasets/{id}/annotations."""
|
|
54
|
+
|
|
55
|
+
success: bool = False
|
|
56
|
+
message: str | None = None
|
|
57
|
+
annotations_deleted: int = 0
|
|
58
|
+
images_deleted: int = 0
|
|
59
|
+
categories_deleted: int = 0
|
|
60
|
+
|
|
61
|
+
|
|
62
|
+
class PaginatedAnnotationModel(BaseModel):
|
|
63
|
+
"""An annotation in the paginated dataset-level annotation listing."""
|
|
64
|
+
|
|
65
|
+
annotation_id: int
|
|
66
|
+
image_id: UUID
|
|
67
|
+
category_id: int
|
|
68
|
+
segmentation: Any | None = None
|
|
69
|
+
bbox: list[float] = []
|
|
70
|
+
ignore: bool = False
|
|
71
|
+
iscrowd: bool = False
|
|
72
|
+
area: float = 0.0
|
|
73
|
+
keypoints: Any | None = None
|
|
74
|
+
num_keypoints: int | None = None
|
|
75
|
+
category_name: str | None = None
|
|
76
|
+
segmentation_type: str | None = None
|
|
77
|
+
tags: list[str] = []
|
|
78
|
+
confidence_score: float | None = None
|
|
79
|
+
annotator_id: str | None = None
|
|
80
|
+
status: str | None = None
|
|
@@ -0,0 +1,106 @@
|
|
|
1
|
+
"""Category-related models."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from typing import Any
|
|
6
|
+
|
|
7
|
+
from pydantic import BaseModel
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
class CategoryModel(BaseModel):
|
|
11
|
+
"""A dataset category."""
|
|
12
|
+
|
|
13
|
+
category_id: int
|
|
14
|
+
name: str
|
|
15
|
+
supercategory: str | None = None
|
|
16
|
+
annotation_count: int = 0
|
|
17
|
+
image_count: int = 0
|
|
18
|
+
pixel_count: int | None = None
|
|
19
|
+
pixel_percentage: float | None = None
|
|
20
|
+
area_m2: float | None = None
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
class CategoryListModel(BaseModel):
|
|
24
|
+
"""Response from GET /datasets/{id}/categories."""
|
|
25
|
+
|
|
26
|
+
categories: list[CategoryModel] = []
|
|
27
|
+
total_categories: int = 0
|
|
28
|
+
total_annotations: int = 0
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
class MergeCategoriesResponse(BaseModel):
|
|
32
|
+
"""Response from POST /datasets/{id}/categories/merge."""
|
|
33
|
+
|
|
34
|
+
merged: bool = False
|
|
35
|
+
target_id: int | None = None
|
|
36
|
+
target_name: str | None = None
|
|
37
|
+
affected_annotations: int = 0
|
|
38
|
+
deleted_categories: list[str] = []
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
class RenameCategoryResponse(BaseModel):
|
|
42
|
+
"""Response from PUT /datasets/{id}/categories/{cid}/rename."""
|
|
43
|
+
|
|
44
|
+
old_name: str | None = None
|
|
45
|
+
new_name: str | None = None
|
|
46
|
+
category_id: int | None = None
|
|
47
|
+
affected_annotations: int = 0
|
|
48
|
+
|
|
49
|
+
|
|
50
|
+
class RemoveCategoryResponse(BaseModel):
|
|
51
|
+
"""Response from DELETE /datasets/{id}/categories/{cid}."""
|
|
52
|
+
|
|
53
|
+
removed: Any = None
|
|
54
|
+
category_id: int | None = None
|
|
55
|
+
annotations_affected: int = 0
|
|
56
|
+
action_taken: str | None = None
|
|
57
|
+
|
|
58
|
+
|
|
59
|
+
class ConsolidateLabelsResponse(BaseModel):
|
|
60
|
+
"""Response from POST /datasets/{id}/categories/consolidate."""
|
|
61
|
+
|
|
62
|
+
consolidated: Any = None
|
|
63
|
+
mapping_applied: dict[str, Any] | None = None
|
|
64
|
+
total_annotations_updated: int = 0
|
|
65
|
+
|
|
66
|
+
|
|
67
|
+
class MergeBySupercategoryResponse(BaseModel):
|
|
68
|
+
"""Response from POST /datasets/{id}/categories/merge-by-supercategory."""
|
|
69
|
+
|
|
70
|
+
supercategory: str | None = None
|
|
71
|
+
target_category_name: str | None = None
|
|
72
|
+
target_category_id: int | None = None
|
|
73
|
+
categories_merged: int = 0
|
|
74
|
+
merged_categories: list[str] = []
|
|
75
|
+
annotations_affected: int = 0
|
|
76
|
+
|
|
77
|
+
|
|
78
|
+
class SplitCategoryResponse(BaseModel):
|
|
79
|
+
"""Response from POST /datasets/{id}/categories/split."""
|
|
80
|
+
|
|
81
|
+
operation_id: str | None = None
|
|
82
|
+
source_category_name: str | None = None
|
|
83
|
+
target_categories: list[str] = []
|
|
84
|
+
annotations_to_assign: int = 0
|
|
85
|
+
|
|
86
|
+
|
|
87
|
+
class CancelSplitCategoryResponse(BaseModel):
|
|
88
|
+
"""Response from DELETE /datasets/{id}/categories/split/{opId}."""
|
|
89
|
+
|
|
90
|
+
operation_id: str | None = None
|
|
91
|
+
source_category_name: str | None = None
|
|
92
|
+
target_categories_expected: int = 0
|
|
93
|
+
target_categories_deleted: int = 0
|
|
94
|
+
status: str | None = None
|
|
95
|
+
|
|
96
|
+
|
|
97
|
+
class MergeSourcesResponse(BaseModel):
|
|
98
|
+
"""Response from GET /datasets/{id}/categories/{cid}/merge-sources."""
|
|
99
|
+
|
|
100
|
+
dataset_id: str | None = None
|
|
101
|
+
category_id: int | None = None
|
|
102
|
+
category_name: str | None = None
|
|
103
|
+
has_merge_history: bool = False
|
|
104
|
+
leaf_categories: list[dict[str, Any]] = []
|
|
105
|
+
merge_chain: list[dict[str, Any]] = []
|
|
106
|
+
suggested_split_request: dict[str, Any] | None = None
|