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
|
@@ -0,0 +1,140 @@
|
|
|
1
|
+
"""Split sub-resource with per-split operations."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from typing import TYPE_CHECKING, Any
|
|
6
|
+
from uuid import UUID
|
|
7
|
+
|
|
8
|
+
from modelstudio.models.annotations import AnnotationModel
|
|
9
|
+
from modelstudio.models.exports import ExportCocoModel, SegmentationExportModel
|
|
10
|
+
from modelstudio.models.images import DatasetImageModel
|
|
11
|
+
from modelstudio.models.imports import (
|
|
12
|
+
ImportQueuedModel,
|
|
13
|
+
PreImportValidationModel,
|
|
14
|
+
ValidationStartedModel,
|
|
15
|
+
)
|
|
16
|
+
|
|
17
|
+
if TYPE_CHECKING:
|
|
18
|
+
from modelstudio._http import HttpTransport
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
class Split:
|
|
22
|
+
"""Operations on a specific split within a dataset.
|
|
23
|
+
|
|
24
|
+
Access via ``client.dataset("ds-id").split("split-id")``.
|
|
25
|
+
"""
|
|
26
|
+
|
|
27
|
+
def __init__(self, transport: HttpTransport, dataset_id: str, split_id: str) -> None:
|
|
28
|
+
self._t = transport
|
|
29
|
+
self._ds = dataset_id
|
|
30
|
+
self._sid = split_id
|
|
31
|
+
self._base = f"/api/v1/datasets/{dataset_id}/splits/{split_id}"
|
|
32
|
+
|
|
33
|
+
# ── CRUD ──────────────────────────────────────────────────────────
|
|
34
|
+
|
|
35
|
+
def patch_tao_id(self, tao_id: str | UUID) -> None:
|
|
36
|
+
"""Update the TAO ID for this split."""
|
|
37
|
+
self._t.patch(self._base, json={"tao_id": str(tao_id)})
|
|
38
|
+
|
|
39
|
+
# ── Images ────────────────────────────────────────────────────────
|
|
40
|
+
|
|
41
|
+
def add_images(self, image_ids: list[str | UUID]) -> list[DatasetImageModel]:
|
|
42
|
+
"""Add images to this split."""
|
|
43
|
+
data = self._t.post(
|
|
44
|
+
f"{self._base}/images",
|
|
45
|
+
json={"images": [{"image_id": str(i)} for i in image_ids]},
|
|
46
|
+
)
|
|
47
|
+
return [DatasetImageModel.model_validate(item) for item in data]
|
|
48
|
+
|
|
49
|
+
def remove_image(self, dataset_image_id: str | UUID) -> None:
|
|
50
|
+
"""Remove an image from this split."""
|
|
51
|
+
self._t.delete(f"{self._base}/images/{dataset_image_id}")
|
|
52
|
+
|
|
53
|
+
def move_image(self, image_id: str | UUID, new_split_id: str | UUID) -> None:
|
|
54
|
+
"""Move an image to another split."""
|
|
55
|
+
self._t.patch(
|
|
56
|
+
f"{self._base}/images/{image_id}/move",
|
|
57
|
+
json={"new_split_id": str(new_split_id)},
|
|
58
|
+
)
|
|
59
|
+
|
|
60
|
+
def list_images(self) -> list[DatasetImageModel]:
|
|
61
|
+
"""List all images in this split."""
|
|
62
|
+
data = self._t.get(f"{self._base}/images")
|
|
63
|
+
return [DatasetImageModel.model_validate(item) for item in data]
|
|
64
|
+
|
|
65
|
+
# ── Annotations ─────────────────────────────────────────────────────
|
|
66
|
+
|
|
67
|
+
def list_annotations(self) -> list[AnnotationModel]:
|
|
68
|
+
"""List all annotations in this split."""
|
|
69
|
+
data = self._t.get(f"{self._base}/annotations")
|
|
70
|
+
return [AnnotationModel.model_validate(item) for item in data]
|
|
71
|
+
|
|
72
|
+
# ── DataFrame helpers ───────────────────────────────────────────────
|
|
73
|
+
|
|
74
|
+
def images_df(self) -> Any:
|
|
75
|
+
"""Return images as a pandas DataFrame."""
|
|
76
|
+
from modelstudio._pandas import to_dataframe
|
|
77
|
+
|
|
78
|
+
return to_dataframe(self.list_images())
|
|
79
|
+
|
|
80
|
+
def annotations_df(self) -> Any:
|
|
81
|
+
"""Return annotations as a pandas DataFrame."""
|
|
82
|
+
from modelstudio._pandas import to_dataframe
|
|
83
|
+
|
|
84
|
+
return to_dataframe(self.list_annotations())
|
|
85
|
+
|
|
86
|
+
# ── Import ────────────────────────────────────────────────────────
|
|
87
|
+
|
|
88
|
+
def import_from_source(
|
|
89
|
+
self, source: str, type: str, config: dict[str, Any]
|
|
90
|
+
) -> ImportQueuedModel:
|
|
91
|
+
"""Start an import from a data source adapter (s3, labelbox, etc).
|
|
92
|
+
|
|
93
|
+
Args:
|
|
94
|
+
source: The data source (e.g. "s3", "labelbox").
|
|
95
|
+
type: The import type (e.g. "coco", "segmentation").
|
|
96
|
+
config: Source-specific configuration.
|
|
97
|
+
"""
|
|
98
|
+
data = self._t.post_accepted(f"{self._base}/import/{source}/{type}", json=config)
|
|
99
|
+
return ImportQueuedModel.model_validate(data)
|
|
100
|
+
|
|
101
|
+
def import_segmentation(self, config: dict[str, Any]) -> SegmentationExportModel:
|
|
102
|
+
"""Import segmentation masks from S3."""
|
|
103
|
+
data = self._t.post(f"{self._base}/import/s3/segmentation", json=config)
|
|
104
|
+
return SegmentationExportModel.model_validate(data)
|
|
105
|
+
|
|
106
|
+
def validate_import(self, config: dict[str, Any]) -> ValidationStartedModel:
|
|
107
|
+
"""Start pre-import validation."""
|
|
108
|
+
data = self._t.post_accepted(f"{self._base}/import/s3/validate", json=config)
|
|
109
|
+
return ValidationStartedModel.model_validate(data)
|
|
110
|
+
|
|
111
|
+
def validation_status(self, validation_id: str) -> PreImportValidationModel:
|
|
112
|
+
"""Check validation status."""
|
|
113
|
+
data = self._t.get(f"{self._base}/import/validate/{validation_id}")
|
|
114
|
+
return PreImportValidationModel.model_validate(data)
|
|
115
|
+
|
|
116
|
+
# ── Export ─────────────────────────────────────────────────────────
|
|
117
|
+
|
|
118
|
+
def export_coco(
|
|
119
|
+
self,
|
|
120
|
+
reindex_images: bool = True,
|
|
121
|
+
reindex_annotations: bool = True,
|
|
122
|
+
reindex_categories: bool = True,
|
|
123
|
+
starting_id: int = 1,
|
|
124
|
+
) -> ExportCocoModel:
|
|
125
|
+
"""Export this split as COCO JSON."""
|
|
126
|
+
data = self._t.post(
|
|
127
|
+
f"{self._base}/export/coco",
|
|
128
|
+
json={
|
|
129
|
+
"reindex_images": reindex_images,
|
|
130
|
+
"reindex_annotations": reindex_annotations,
|
|
131
|
+
"reindex_categories": reindex_categories,
|
|
132
|
+
"starting_id": starting_id,
|
|
133
|
+
},
|
|
134
|
+
)
|
|
135
|
+
return ExportCocoModel.model_validate(data)
|
|
136
|
+
|
|
137
|
+
def export_segmentation(self) -> SegmentationExportModel:
|
|
138
|
+
"""Export segmentation masks for this split."""
|
|
139
|
+
data = self._t.post(f"{self._base}/export/segmentation")
|
|
140
|
+
return SegmentationExportModel.model_validate(data)
|
|
@@ -0,0 +1,513 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: modelstudio-sdk
|
|
3
|
+
Version: 0.0.0.dev0
|
|
4
|
+
Summary: Python SDK for the Model Studio REST API
|
|
5
|
+
License-Expression: MIT
|
|
6
|
+
License-File: LICENSE
|
|
7
|
+
Requires-Python: >=3.10
|
|
8
|
+
Requires-Dist: httpx<1.0,>=0.25.0
|
|
9
|
+
Requires-Dist: pydantic<3.0,>=2.0
|
|
10
|
+
Provides-Extra: dev
|
|
11
|
+
Requires-Dist: mypy>=1.8; extra == 'dev'
|
|
12
|
+
Requires-Dist: pandas>=1.5.0; extra == 'dev'
|
|
13
|
+
Requires-Dist: pytest-cov>=4.0; extra == 'dev'
|
|
14
|
+
Requires-Dist: pytest-httpx>=0.30.0; extra == 'dev'
|
|
15
|
+
Requires-Dist: pytest>=7.0; extra == 'dev'
|
|
16
|
+
Requires-Dist: ruff>=0.4.0; extra == 'dev'
|
|
17
|
+
Provides-Extra: pandas
|
|
18
|
+
Requires-Dist: pandas>=1.5.0; extra == 'pandas'
|
|
19
|
+
Description-Content-Type: text/markdown
|
|
20
|
+
|
|
21
|
+
# Model Studio Python SDK
|
|
22
|
+
|
|
23
|
+
Python SDK for the Model Studio REST API. Provides typed access to dataset management, annotation tooling, metrics, and ML workflow operations.
|
|
24
|
+
|
|
25
|
+
## Installation
|
|
26
|
+
|
|
27
|
+
```bash
|
|
28
|
+
# From wheel (in notebook containers, pre-installed)
|
|
29
|
+
pip install modelstudio-sdk
|
|
30
|
+
|
|
31
|
+
# Development install
|
|
32
|
+
git clone https://gitlab.com/orbitalinsight/elements/model-studio/modelstudio-sdk.git
|
|
33
|
+
cd modelstudio-sdk
|
|
34
|
+
pip install -e ".[dev]"
|
|
35
|
+
|
|
36
|
+
# With pandas support
|
|
37
|
+
pip install "modelstudio-sdk[pandas]"
|
|
38
|
+
```
|
|
39
|
+
|
|
40
|
+
## Quick Start
|
|
41
|
+
|
|
42
|
+
```python
|
|
43
|
+
from modelstudio import ModelStudioClient
|
|
44
|
+
|
|
45
|
+
# Auto-configured inside notebooks (reads env vars)
|
|
46
|
+
client = ModelStudioClient.from_env()
|
|
47
|
+
|
|
48
|
+
# Or explicit
|
|
49
|
+
client = ModelStudioClient(
|
|
50
|
+
base_url="http://localhost:8081",
|
|
51
|
+
jwt_token="eyJhbG...",
|
|
52
|
+
)
|
|
53
|
+
|
|
54
|
+
# List datasets
|
|
55
|
+
for ds in client.datasets.list():
|
|
56
|
+
print(f"{ds.name} ({ds.dataset_type})")
|
|
57
|
+
```
|
|
58
|
+
|
|
59
|
+
## Environment Variables
|
|
60
|
+
|
|
61
|
+
| Variable | Required | Description |
|
|
62
|
+
|----------|----------|-------------|
|
|
63
|
+
| `MODEL_STUDIO_API_URL` | Yes | API base URL |
|
|
64
|
+
| `MODEL_STUDIO_JWT` | No | JWT authentication token |
|
|
65
|
+
| `MODEL_STUDIO_ORG` | No | Organization name |
|
|
66
|
+
|
|
67
|
+
## Usage Examples
|
|
68
|
+
|
|
69
|
+
### Dataset Operations
|
|
70
|
+
|
|
71
|
+
```python
|
|
72
|
+
ds = client.dataset("dataset-uuid")
|
|
73
|
+
|
|
74
|
+
# Get overview statistics
|
|
75
|
+
overview = ds.overview()
|
|
76
|
+
print(f"Images: {overview.summary.total_images}")
|
|
77
|
+
print(f"Annotations: {overview.summary.total_annotations}")
|
|
78
|
+
|
|
79
|
+
# List splits
|
|
80
|
+
for split in ds.splits():
|
|
81
|
+
print(f"{split.name} ({split.split_type})")
|
|
82
|
+
|
|
83
|
+
# List categories
|
|
84
|
+
cats = ds.categories()
|
|
85
|
+
for cat in cats.categories:
|
|
86
|
+
print(f"{cat.name}: {cat.annotation_count} annotations")
|
|
87
|
+
```
|
|
88
|
+
|
|
89
|
+
### Split Operations
|
|
90
|
+
|
|
91
|
+
```python
|
|
92
|
+
# Create algorithmic splits
|
|
93
|
+
ds.create_algorithmic_splits(
|
|
94
|
+
splits={"train": 0.7, "val": 0.15, "test": 0.15},
|
|
95
|
+
seed=42,
|
|
96
|
+
)
|
|
97
|
+
|
|
98
|
+
# Smart redistribution
|
|
99
|
+
result = ds.smart_redistribute(
|
|
100
|
+
ratios={"train": 0.8, "val": 0.2},
|
|
101
|
+
prevent_tile_leakage=True,
|
|
102
|
+
)
|
|
103
|
+
|
|
104
|
+
# Check for data leakage
|
|
105
|
+
leakage = ds.check_leakage()
|
|
106
|
+
if leakage.has_leakage:
|
|
107
|
+
print(f"Found {leakage.leakage_count} leaked images")
|
|
108
|
+
```
|
|
109
|
+
|
|
110
|
+
### Category Management
|
|
111
|
+
|
|
112
|
+
```python
|
|
113
|
+
# Merge categories
|
|
114
|
+
ds.merge_categories(
|
|
115
|
+
source_categories=[1, 2, 3],
|
|
116
|
+
target_category="vehicle",
|
|
117
|
+
)
|
|
118
|
+
|
|
119
|
+
# Rename a category
|
|
120
|
+
ds.rename_category(category_id=5, new_name="truck")
|
|
121
|
+
|
|
122
|
+
# Remove a category
|
|
123
|
+
ds.remove_category(category_id=10)
|
|
124
|
+
|
|
125
|
+
# Consolidate labels
|
|
126
|
+
ds.consolidate_labels({"car": "vehicle", "van": "vehicle"})
|
|
127
|
+
```
|
|
128
|
+
|
|
129
|
+
### Working with Split Data
|
|
130
|
+
|
|
131
|
+
```python
|
|
132
|
+
split = ds.split("split-uuid")
|
|
133
|
+
|
|
134
|
+
# List images
|
|
135
|
+
images = split.list_images()
|
|
136
|
+
|
|
137
|
+
# List annotations
|
|
138
|
+
annotations = split.list_annotations()
|
|
139
|
+
|
|
140
|
+
# Import from S3
|
|
141
|
+
queued = split.import_from_source("s3", {
|
|
142
|
+
"connection_id": "conn-uuid",
|
|
143
|
+
"bucket": "my-bucket",
|
|
144
|
+
"prefix": "datasets/coco/",
|
|
145
|
+
})
|
|
146
|
+
|
|
147
|
+
# Wait for import to complete
|
|
148
|
+
poller = split.import_poller(interval=5.0)
|
|
149
|
+
result = poller.wait(callback=lambda r: print(f"Progress: {r.get('progress')}%"))
|
|
150
|
+
```
|
|
151
|
+
|
|
152
|
+
### Cloning & Async Operations
|
|
153
|
+
|
|
154
|
+
```python
|
|
155
|
+
# Clone a dataset
|
|
156
|
+
cloned = ds.clone(name="My Clone")
|
|
157
|
+
|
|
158
|
+
# Poll until complete
|
|
159
|
+
poller = ds.clone_poller(interval=2.0, max_wait=300.0)
|
|
160
|
+
result = poller.wait()
|
|
161
|
+
print(f"Clone status: {result['clone_status']}")
|
|
162
|
+
```
|
|
163
|
+
|
|
164
|
+
### Validation & Quality
|
|
165
|
+
|
|
166
|
+
```python
|
|
167
|
+
# Validate dataset
|
|
168
|
+
result = ds.validate()
|
|
169
|
+
print(f"Valid: {result.valid}")
|
|
170
|
+
|
|
171
|
+
# Check for duplicates
|
|
172
|
+
dupes = ds.check_duplicates()
|
|
173
|
+
if dupes.has_duplicates:
|
|
174
|
+
print(f"{dupes.total_duplicate_images} duplicate images found")
|
|
175
|
+
|
|
176
|
+
# Detect temporal conflicts
|
|
177
|
+
conflicts = ds.detect_temporal_conflicts()
|
|
178
|
+
```
|
|
179
|
+
|
|
180
|
+
### Export
|
|
181
|
+
|
|
182
|
+
```python
|
|
183
|
+
# Export as COCO JSON
|
|
184
|
+
coco = ds.export_coco()
|
|
185
|
+
print(f"Exported {coco.image_count} images, {coco.annotation_count} annotations")
|
|
186
|
+
|
|
187
|
+
# Per-split export
|
|
188
|
+
split_coco = split.export_coco()
|
|
189
|
+
```
|
|
190
|
+
|
|
191
|
+
### Filtering
|
|
192
|
+
|
|
193
|
+
```python
|
|
194
|
+
from modelstudio.models.filters import DatasetFilterRequest, CategoryFilter
|
|
195
|
+
|
|
196
|
+
# Filter to specific categories
|
|
197
|
+
result = ds.filter(DatasetFilterRequest(
|
|
198
|
+
category_filter=CategoryFilter(keep_categories=["car", "truck"]),
|
|
199
|
+
new_dataset_name="filtered-cars",
|
|
200
|
+
))
|
|
201
|
+
print(f"New dataset: {result.new_dataset_id}")
|
|
202
|
+
```
|
|
203
|
+
|
|
204
|
+
### Few-Shot & Oversampling
|
|
205
|
+
|
|
206
|
+
```python
|
|
207
|
+
from modelstudio.models.few_shot import FewShotRequest
|
|
208
|
+
from modelstudio.models.oversample import OversampleRequest
|
|
209
|
+
|
|
210
|
+
# Create few-shot dataset
|
|
211
|
+
result = ds.few_shot_create(FewShotRequest(
|
|
212
|
+
num_images=100,
|
|
213
|
+
method="MOST_CLASSES",
|
|
214
|
+
new_dataset_name="few-shot-100",
|
|
215
|
+
))
|
|
216
|
+
|
|
217
|
+
# Oversample minority classes
|
|
218
|
+
result = ds.oversample_execute(OversampleRequest(
|
|
219
|
+
target_ratio=0.5,
|
|
220
|
+
strategy="PREFER_ANNOTATED",
|
|
221
|
+
))
|
|
222
|
+
```
|
|
223
|
+
|
|
224
|
+
### Dataset Merge
|
|
225
|
+
|
|
226
|
+
```python
|
|
227
|
+
from modelstudio.models.merge import MergeDatasetRequest
|
|
228
|
+
|
|
229
|
+
# Analyze conflicts before merging
|
|
230
|
+
analysis = client.datasets.merge_analyze(["ds-1", "ds-2"])
|
|
231
|
+
|
|
232
|
+
# Merge datasets
|
|
233
|
+
result = client.datasets.merge(MergeDatasetRequest(
|
|
234
|
+
source_dataset_ids=["ds-1", "ds-2"],
|
|
235
|
+
target_name="merged-dataset",
|
|
236
|
+
))
|
|
237
|
+
```
|
|
238
|
+
|
|
239
|
+
### Undo/Redo
|
|
240
|
+
|
|
241
|
+
```python
|
|
242
|
+
# View history
|
|
243
|
+
history = ds.history()
|
|
244
|
+
for entry in history.changes:
|
|
245
|
+
print(f"{entry.operation_type}: {entry.short_description}")
|
|
246
|
+
|
|
247
|
+
# Undo
|
|
248
|
+
result = ds.undo()
|
|
249
|
+
print(f"Undid: {result.description}")
|
|
250
|
+
```
|
|
251
|
+
|
|
252
|
+
### DataFrame Integration
|
|
253
|
+
|
|
254
|
+
```python
|
|
255
|
+
# Requires: pip install "modelstudio-sdk[pandas]"
|
|
256
|
+
|
|
257
|
+
# Images as DataFrame
|
|
258
|
+
df = split.images_df()
|
|
259
|
+
|
|
260
|
+
# Annotations as DataFrame
|
|
261
|
+
df = split.annotations_df()
|
|
262
|
+
|
|
263
|
+
# Categories as DataFrame
|
|
264
|
+
df = ds.categories_df()
|
|
265
|
+
|
|
266
|
+
# Class distribution as DataFrame
|
|
267
|
+
df = ds.class_distribution_df()
|
|
268
|
+
```
|
|
269
|
+
|
|
270
|
+
## Error Handling
|
|
271
|
+
|
|
272
|
+
```python
|
|
273
|
+
from modelstudio.exceptions import NotFoundError, ConflictError, BadRequestError
|
|
274
|
+
|
|
275
|
+
try:
|
|
276
|
+
ds = client.dataset("nonexistent")
|
|
277
|
+
ds.overview()
|
|
278
|
+
except NotFoundError as e:
|
|
279
|
+
print(f"Dataset not found: {e.message}")
|
|
280
|
+
except ConflictError as e:
|
|
281
|
+
print(f"Operation conflict: {e.message}")
|
|
282
|
+
except BadRequestError as e:
|
|
283
|
+
print(f"Invalid request: {e.message}")
|
|
284
|
+
```
|
|
285
|
+
|
|
286
|
+
## Architecture
|
|
287
|
+
|
|
288
|
+
### Related Repositories
|
|
289
|
+
|
|
290
|
+
| Repo | Purpose |
|
|
291
|
+
|------|---------|
|
|
292
|
+
| [`model-studio-sdk`](https://gitlab.com/orbitalinsight/elements/model-studio/modelstudio-sdk) | This repo — Python SDK + Jupyter Server Docker |
|
|
293
|
+
| [`frontend`](https://gitlab.com/orbitalinsight/frontend-2.0) | Model Studio React frontend (custom notebook UI lives here) |
|
|
294
|
+
| [`model-studio-api`](https://gitlab.com/orbitalinsight/elements/model-studio/model-studio-api) | Backend REST API the SDK wraps |
|
|
295
|
+
| [`model-studio-notebooks`](https://gitlab.com/orbitalinsight/elements/model-studio/model-studio-notebooks) | JupyterHub + KubeSpawner Helm chart (production multi-user) |
|
|
296
|
+
| [`model-studio-agent`](https://gitlab.com/orbitalinsight/elements/model-studio/model-studio-agent) | Agent chat backend |
|
|
297
|
+
| [`keycloak-config`](https://gitlab.com/orbitalinsight/elements/keycloak-config) | Keycloak realm/client configuration |
|
|
298
|
+
|
|
299
|
+
### Local Development Architecture
|
|
300
|
+
|
|
301
|
+
```
|
|
302
|
+
┌─────────────────────────────────────────────────────────────┐
|
|
303
|
+
│ Browser (http://localhost:5173) │
|
|
304
|
+
│ │
|
|
305
|
+
│ ┌───────────────────────────────────────────────────────┐ │
|
|
306
|
+
│ │ Model Studio Frontend (Vite) │ │
|
|
307
|
+
│ │ ┌──────────────┐ ┌──────────────┐ ┌────────────┐ │ │
|
|
308
|
+
│ │ │ Dataset Pages │ │ Notebook │ │ Agent Chat │ │ │
|
|
309
|
+
│ │ │ │ │ Panel │ │ Panel │ │ │
|
|
310
|
+
│ │ └──────────────┘ └──────┬───────┘ └─────┬──────┘ │ │
|
|
311
|
+
│ └───────────────────────────┼────────────────┼──────────┘ │
|
|
312
|
+
│ │ │ │
|
|
313
|
+
│ Vite Dev Server Proxies: │ │ │
|
|
314
|
+
│ /jupyter/* ─────────────────┘ │ │
|
|
315
|
+
│ /agent/* ────────────────────────────────────┘ │
|
|
316
|
+
└──────────────────────────────┼────────────────┼─────────────┘
|
|
317
|
+
│ │
|
|
318
|
+
┌────────────────┘ │
|
|
319
|
+
▼ ▼
|
|
320
|
+
┌──────────────────────────┐ ┌──────────────────────────┐
|
|
321
|
+
│ Jupyter Server (Docker) │ │ Agent API │
|
|
322
|
+
│ localhost:8889 │ │ localhost:8080 │
|
|
323
|
+
│ │ │ (model-studio-agent) │
|
|
324
|
+
│ ┌────────────────────┐ │ └──────────────────────────┘
|
|
325
|
+
│ │ Python 3.10 Kernel │ │
|
|
326
|
+
│ │ + Model Studio SDK │ │ ▲
|
|
327
|
+
│ └────────┬───────────┘ │ │
|
|
328
|
+
└───────────┼──────────────┘ │
|
|
329
|
+
│ │
|
|
330
|
+
▼ │
|
|
331
|
+
┌──────────────────────────────────────────────────────────┐
|
|
332
|
+
│ Model Studio API │
|
|
333
|
+
│ https://model-studio-api.model-studio.privateer-dev.com │
|
|
334
|
+
│ (model-studio-api repo) │
|
|
335
|
+
└──────────────────────────────────────────────────────────┘
|
|
336
|
+
```
|
|
337
|
+
|
|
338
|
+
**Data flow**: User writes Python in the Notebook Panel → CodeMirror editor sends code via WebSocket to Jupyter kernel → kernel executes using the SDK → SDK calls Model Studio API → results render in the panel.
|
|
339
|
+
|
|
340
|
+
### Production Architecture
|
|
341
|
+
|
|
342
|
+
```
|
|
343
|
+
┌──────────────────────────────────────────────────────────┐
|
|
344
|
+
│ Browser │
|
|
345
|
+
│ Model Studio Frontend (static build on CDN/Nginx) │
|
|
346
|
+
│ ┌──────────────┐ ┌──────────────┐ ┌────────────────┐ │
|
|
347
|
+
│ │ Dataset Pages │ │ Notebook │ │ Agent Chat │ │
|
|
348
|
+
│ │ │ │ Panel │ │ Panel │ │
|
|
349
|
+
│ └──────────────┘ └──────┬───────┘ └────────────────┘ │
|
|
350
|
+
└────────────────────────────┼─────────────────────────────┘
|
|
351
|
+
│
|
|
352
|
+
▼
|
|
353
|
+
┌──────────────────────────────────────────────────────────┐
|
|
354
|
+
│ JupyterHub (model-studio-notebooks repo) │
|
|
355
|
+
│ - Keycloak OIDC auth │
|
|
356
|
+
│ - KubeSpawner → per-user Jupyter Server pods │
|
|
357
|
+
│ - Helm chart for K8s deployment │
|
|
358
|
+
│ │
|
|
359
|
+
│ ┌──────────────────────────────────────────────────┐ │
|
|
360
|
+
│ │ Per-User Jupyter Server (K8s Pod) │ │
|
|
361
|
+
│ │ ┌────────────────────┐ │ │
|
|
362
|
+
│ │ │ Python 3.10 Kernel │ │ │
|
|
363
|
+
│ │ │ + Model Studio SDK │ │ │
|
|
364
|
+
│ │ └────────┬───────────┘ │ │
|
|
365
|
+
│ └───────────┼──────────────────────────────────────┘ │
|
|
366
|
+
└──────────────┼───────────────────────────────────────────┘
|
|
367
|
+
│
|
|
368
|
+
▼
|
|
369
|
+
┌──────────────────────────────────────────────────────────┐
|
|
370
|
+
│ Model Studio API (K8s service) │
|
|
371
|
+
└──────────────────────────────────────────────────────────┘
|
|
372
|
+
```
|
|
373
|
+
|
|
374
|
+
**Key difference**: In production, JupyterHub (from `model-studio-notebooks` repo) manages multi-user server lifecycle, auth, and resource limits. The custom notebook UI replaces JupyterLab's frontend but JupyterHub still manages server spawning.
|
|
375
|
+
|
|
376
|
+
---
|
|
377
|
+
|
|
378
|
+
## Development
|
|
379
|
+
|
|
380
|
+
### Prerequisites
|
|
381
|
+
|
|
382
|
+
- [Miniconda](https://docs.conda.io/en/latest/miniconda.html) or Anaconda
|
|
383
|
+
- Docker + Docker Compose (for notebook server)
|
|
384
|
+
- `jq` and `curl` (for Keycloak token fetch)
|
|
385
|
+
|
|
386
|
+
### Quick Start — SDK Development
|
|
387
|
+
|
|
388
|
+
```bash
|
|
389
|
+
./develop.sh --mode setup # Create conda env, install deps
|
|
390
|
+
./develop.sh --mode test # Run unit tests
|
|
391
|
+
./develop.sh --mode test-integration # Fetch Keycloak token + run integration tests
|
|
392
|
+
./develop.sh --mode lint # Run ruff + mypy
|
|
393
|
+
```
|
|
394
|
+
|
|
395
|
+
This creates a `model-studio-sdk` conda environment with Python 3.10 and installs the SDK in editable mode with all dev dependencies.
|
|
396
|
+
|
|
397
|
+
### Quick Start — Custom Notebook UI
|
|
398
|
+
|
|
399
|
+
The notebook UI spans two repos: the Jupyter Server backend (this repo) and the React frontend (`frontend` repo).
|
|
400
|
+
|
|
401
|
+
**Terminal 1 — Start Jupyter Server:**
|
|
402
|
+
|
|
403
|
+
```bash
|
|
404
|
+
# From this repo (model-studio-sdk)
|
|
405
|
+
./develop.sh --mode notebook-server
|
|
406
|
+
```
|
|
407
|
+
|
|
408
|
+
This prompts for Keycloak credentials, starts a headless Jupyter Server on port 8889 with the SDK pre-installed. The `src/` and `notebooks/` directories are volume-mounted for live reloading.
|
|
409
|
+
|
|
410
|
+
**Terminal 2 — Start frontend:**
|
|
411
|
+
|
|
412
|
+
```bash
|
|
413
|
+
# From the frontend repo
|
|
414
|
+
cd ../frontend
|
|
415
|
+
source .go-privateer-dev.env
|
|
416
|
+
yarn build-consts
|
|
417
|
+
|
|
418
|
+
# First time only — install CodeMirror dependencies:
|
|
419
|
+
yarn add @codemirror/view @codemirror/state @codemirror/commands @codemirror/lang-python @codemirror/theme-one-dark
|
|
420
|
+
|
|
421
|
+
yarn start
|
|
422
|
+
```
|
|
423
|
+
|
|
424
|
+
**Verify the setup:**
|
|
425
|
+
|
|
426
|
+
1. Open http://localhost:5173
|
|
427
|
+
2. Click the **Notebook** button in the AppBar (next to Agent)
|
|
428
|
+
3. Type `print('hello')` in the cell and press **Shift+Enter**
|
|
429
|
+
4. Output should appear below the cell
|
|
430
|
+
|
|
431
|
+
**How the proxy works**: The frontend's `vite.config.ts` proxies `/jupyter/*` requests to `localhost:8889` (the Docker Jupyter Server). This includes both REST API calls and WebSocket connections for kernel communication. No environment variables are needed — the proxy is configured in code.
|
|
432
|
+
|
|
433
|
+
### JupyterLab Mode (Full Lab UI)
|
|
434
|
+
|
|
435
|
+
If you need the traditional JupyterLab interface (e.g., for notebook authoring):
|
|
436
|
+
|
|
437
|
+
```bash
|
|
438
|
+
./develop.sh --mode docker # Starts full JupyterLab on port 8888
|
|
439
|
+
# or
|
|
440
|
+
make docker
|
|
441
|
+
```
|
|
442
|
+
|
|
443
|
+
Open http://localhost:8888 for the JupyterLab UI. Notebooks are in the `notebooks/` directory.
|
|
444
|
+
|
|
445
|
+
### Integration Tests
|
|
446
|
+
|
|
447
|
+
Integration tests run against the live dev API and require a Keycloak JWT:
|
|
448
|
+
|
|
449
|
+
```bash
|
|
450
|
+
./develop.sh --mode test-integration
|
|
451
|
+
```
|
|
452
|
+
|
|
453
|
+
This will prompt for your Keycloak credentials (same as your Model Studio login), fetch a JWT, and run the integration test suite.
|
|
454
|
+
|
|
455
|
+
To skip the auth prompt (e.g. if you already have a token):
|
|
456
|
+
|
|
457
|
+
```bash
|
|
458
|
+
export MODEL_STUDIO_JWT="eyJhbG..."
|
|
459
|
+
./develop.sh --mode test-integration --skip-auth
|
|
460
|
+
```
|
|
461
|
+
|
|
462
|
+
Or set credentials as env vars to skip the interactive prompts:
|
|
463
|
+
|
|
464
|
+
```bash
|
|
465
|
+
export KEYCLOAK_USERNAME="you"
|
|
466
|
+
export KEYCLOAK_PASSWORD="secret"
|
|
467
|
+
./develop.sh --mode test-integration
|
|
468
|
+
```
|
|
469
|
+
|
|
470
|
+
### Make Targets
|
|
471
|
+
|
|
472
|
+
If you prefer to manage your own environment, the Makefile targets still work:
|
|
473
|
+
|
|
474
|
+
```bash
|
|
475
|
+
make install # pip install -e ".[dev]"
|
|
476
|
+
make test # Unit tests with coverage
|
|
477
|
+
make test-unit # Unit tests only (no integration)
|
|
478
|
+
make lint # ruff + mypy
|
|
479
|
+
make build # Build wheel
|
|
480
|
+
make notebook-server # Start headless Jupyter Server (port 8889)
|
|
481
|
+
make notebook-server-down # Stop Jupyter Server
|
|
482
|
+
make docker # Start full JupyterLab (port 8888)
|
|
483
|
+
make docker-down # Stop JupyterLab
|
|
484
|
+
```
|
|
485
|
+
|
|
486
|
+
For integration tests without `develop.sh`:
|
|
487
|
+
|
|
488
|
+
```bash
|
|
489
|
+
eval "$(scripts/get-token.sh)" && make test-integration
|
|
490
|
+
```
|
|
491
|
+
|
|
492
|
+
### Docker Services
|
|
493
|
+
|
|
494
|
+
The `docker/docker-compose.yml` defines two services:
|
|
495
|
+
|
|
496
|
+
| Service | Port | Purpose |
|
|
497
|
+
|---------|------|---------|
|
|
498
|
+
| `notebook` | 8888 | Full JupyterLab with Lab UI (for notebook authoring) |
|
|
499
|
+
| `jupyter-server` | 8889 | Headless Jupyter Server (for custom notebook UI backend) |
|
|
500
|
+
|
|
501
|
+
Both use the same `Dockerfile.dev` base image (`jupyter/scipy-notebook:python-3.10`) with the SDK installed in editable mode. The `src/` directory is volume-mounted so SDK changes are picked up without rebuilding.
|
|
502
|
+
|
|
503
|
+
The `jupyter-server` service additionally configures:
|
|
504
|
+
- CORS headers for `http://localhost:5173` (Vite dev server)
|
|
505
|
+
- Disabled XSRF checks (local dev only — production uses JupyterHub auth)
|
|
506
|
+
- No authentication token (local dev only)
|
|
507
|
+
|
|
508
|
+
## Requirements
|
|
509
|
+
|
|
510
|
+
- Python >= 3.10
|
|
511
|
+
- httpx >= 0.25.0
|
|
512
|
+
- pydantic >= 2.0
|
|
513
|
+
- pandas >= 1.5.0 (optional)
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
modelstudio/__init__.py,sha256=fUUWQWqRNr_Cr0m9ZcewVMnLAiQknH4N5dd09z_u5VE,520
|
|
2
|
+
modelstudio/_http.py,sha256=hh60cpb4P8bOV-Uth3PebvLzgA8r3ys2poWSjjUhg2M,3640
|
|
3
|
+
modelstudio/_pandas.py,sha256=hR1SrguXj24o0hCALscVS1TphT5u5dwIzXcxFj3ZcCg,803
|
|
4
|
+
modelstudio/_polling.py,sha256=v736GSpkqW_JURx5cYqexy_fTJpVXFV1_XtVGS0wgUM,1974
|
|
5
|
+
modelstudio/_version.py,sha256=VVFmX0b3faB1pUjdw28IIgO-vzaO6x4Y9leJ6S4ePT4,27
|
|
6
|
+
modelstudio/client.py,sha256=jKkt6YYSodIuDFPSbh2V7lIx4PElZQs6isWJYaOEpeQ,3245
|
|
7
|
+
modelstudio/exceptions.py,sha256=Fl35fjajCFIxq7CpBYcjrqoURz3YRU-93Ds65j1GnME,2563
|
|
8
|
+
modelstudio/models/__init__.py,sha256=2jmDWsD0IW54hSuXa3iUGwg-7wE8MiJWk0p-Tno9bpE,2023
|
|
9
|
+
modelstudio/models/annotations.py,sha256=3QU0fUIQhs3epj1zJmONfPV2yOP7EZYISOM051efWDQ,2002
|
|
10
|
+
modelstudio/models/categories.py,sha256=mnYa5zhScskfYklKve6kry1AxHycWeivEH70mZQVg-8,2938
|
|
11
|
+
modelstudio/models/common.py,sha256=qEyYSK9oTISvrWLZJpv5cs2rEL-q5u2E8raSSFrXF4w,409
|
|
12
|
+
modelstudio/models/datasets.py,sha256=07JWd-OwfgtYjgAENVZqbYDsFlN3Pn5moT95xcp45Eg,1504
|
|
13
|
+
modelstudio/models/deletion.py,sha256=T9QbSvEaK6FzvDjo69LJcZapmaaiPCClwUXL3XHZveg,659
|
|
14
|
+
modelstudio/models/exports.py,sha256=C_6qmts07B31gdZVApEmhHyD4XC916-XhCbms1oPLvk,897
|
|
15
|
+
modelstudio/models/few_shot.py,sha256=BSpvRNODt26LsMe2mZcQHj-46n9NqaPDqDAaEP5EkIY,1084
|
|
16
|
+
modelstudio/models/filters.py,sha256=ERE197Sla_lsHMEA5J9T0N_kp_6Ha_WStVQrfHLhHhM,2353
|
|
17
|
+
modelstudio/models/history.py,sha256=OpcYyAJMRdzlPvsiTNjiXNNRs07SMcjugdtvslWcWfc,1476
|
|
18
|
+
modelstudio/models/images.py,sha256=7UW0QY19j6Eio7TwNNCs2aFDnn5dchIcc5v4T6k35kk,2462
|
|
19
|
+
modelstudio/models/imports.py,sha256=VinLPsOCsmvmDFCkrKRw1aV6Wb42_PDRWlcnS-QIfQI,3934
|
|
20
|
+
modelstudio/models/media.py,sha256=htzbaxMZ3-h7OiuEykg8LIkwzBQNKdty_-OvwrORRts,843
|
|
21
|
+
modelstudio/models/merge.py,sha256=OTYqQRCnAg6-80i8R3mnOXuJSDGwwamIr3XtkzeY6SE,1372
|
|
22
|
+
modelstudio/models/metrics.py,sha256=PzyOfuChqf7uK6qfvMr9BQ1nos522AgrMSXzkd29fE4,5018
|
|
23
|
+
modelstudio/models/oversample.py,sha256=JwdfTXYmIs6XqzPt_ltRiY3nyPWCWwSD1tNy1dUFY2M,1044
|
|
24
|
+
modelstudio/models/splits.py,sha256=3LtY9JZq7YozJxBTH1sDNfCGaBIH3lrNWK0GpNNEOqg,3107
|
|
25
|
+
modelstudio/models/validation.py,sha256=ptfQj_dd4VJXASTs0cGj-DvVY0Vq_TWusaCVZce3yPA,2254
|
|
26
|
+
modelstudio/resources/__init__.py,sha256=iAw4TV6x37px7HavYD8_l4R-j4giAnO3MBRYWjEUwGI,50
|
|
27
|
+
modelstudio/resources/dataset.py,sha256=CPW5keep_2l_lT77XRqLr0XGmABT3xqz-TbiKuC_DLk,30748
|
|
28
|
+
modelstudio/resources/split.py,sha256=Rh8WVUoKG-kVkxcTB0Mi0FRjypWBrbhpmBBs_gW9mkU,6087
|
|
29
|
+
modelstudio_sdk-0.0.0.dev0.dist-info/METADATA,sha256=2rVFmj7mthxGcN6NIpyVBjNVefJ5d-5TlBhJ2BledMg,18781
|
|
30
|
+
modelstudio_sdk-0.0.0.dev0.dist-info/WHEEL,sha256=QccIxa26bgl1E6uMy58deGWi-0aeIkkangHcxk2kWfw,87
|
|
31
|
+
modelstudio_sdk-0.0.0.dev0.dist-info/licenses/LICENSE,sha256=WjcHhhxFV5wPppVsbAdR0m2aq5qxESD4J3k2EZiK0cw,1072
|
|
32
|
+
modelstudio_sdk-0.0.0.dev0.dist-info/RECORD,,
|