unitlab 3.0.2__tar.gz → 3.0.3__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.
- {unitlab-3.0.2 → unitlab-3.0.3}/PKG-INFO +1 -1
- {unitlab-3.0.2 → unitlab-3.0.3}/pyproject.toml +1 -1
- {unitlab-3.0.2 → unitlab-3.0.3}/src/unitlab/_uploader.py +45 -6
- {unitlab-3.0.2 → unitlab-3.0.3}/src/unitlab/ontology.py +2 -0
- {unitlab-3.0.2 → unitlab-3.0.3}/src/unitlab/resources/assets.py +27 -4
- {unitlab-3.0.2 → unitlab-3.0.3}/src/unitlab/resources/projects.py +11 -1
- {unitlab-3.0.2 → unitlab-3.0.3}/tests/test_client.py +12 -0
- {unitlab-3.0.2 → unitlab-3.0.3}/tests/test_tiled_uploads.py +39 -2
- {unitlab-3.0.2 → unitlab-3.0.3}/tests/test_uploader.py +163 -0
- {unitlab-3.0.2 → unitlab-3.0.3}/.gitignore +0 -0
- {unitlab-3.0.2 → unitlab-3.0.3}/LICENSE.md +0 -0
- {unitlab-3.0.2 → unitlab-3.0.3}/README.md +0 -0
- {unitlab-3.0.2 → unitlab-3.0.3}/src/unitlab/__init__.py +0 -0
- {unitlab-3.0.2 → unitlab-3.0.3}/src/unitlab/__main__.py +0 -0
- {unitlab-3.0.2 → unitlab-3.0.3}/src/unitlab/_config.py +0 -0
- {unitlab-3.0.2 → unitlab-3.0.3}/src/unitlab/_downloader.py +0 -0
- {unitlab-3.0.2 → unitlab-3.0.3}/src/unitlab/_grouping.py +0 -0
- {unitlab-3.0.2 → unitlab-3.0.3}/src/unitlab/_http.py +0 -0
- {unitlab-3.0.2 → unitlab-3.0.3}/src/unitlab/_waiter.py +0 -0
- {unitlab-3.0.2 → unitlab-3.0.3}/src/unitlab/cli.py +0 -0
- {unitlab-3.0.2 → unitlab-3.0.3}/src/unitlab/client.py +0 -0
- {unitlab-3.0.2 → unitlab-3.0.3}/src/unitlab/exceptions.py +0 -0
- {unitlab-3.0.2 → unitlab-3.0.3}/src/unitlab/py.typed +0 -0
- {unitlab-3.0.2 → unitlab-3.0.3}/src/unitlab/resources/__init__.py +0 -0
- {unitlab-3.0.2 → unitlab-3.0.3}/src/unitlab/resources/_base.py +0 -0
- {unitlab-3.0.2 → unitlab-3.0.3}/src/unitlab/resources/cloud.py +0 -0
- {unitlab-3.0.2 → unitlab-3.0.3}/src/unitlab/resources/datasets.py +0 -0
- {unitlab-3.0.2 → unitlab-3.0.3}/src/unitlab/resources/embeddings.py +0 -0
- {unitlab-3.0.2 → unitlab-3.0.3}/src/unitlab/resources/ontologies.py +0 -0
- {unitlab-3.0.2 → unitlab-3.0.3}/src/unitlab/resources/releases.py +0 -0
- {unitlab-3.0.2 → unitlab-3.0.3}/src/unitlab/resources/workflow.py +0 -0
- {unitlab-3.0.2 → unitlab-3.0.3}/src/unitlab/types.py +0 -0
- {unitlab-3.0.2 → unitlab-3.0.3}/tests/test_cli.py +0 -0
- {unitlab-3.0.2 → unitlab-3.0.3}/tests/test_downloader.py +0 -0
- {unitlab-3.0.2 → unitlab-3.0.3}/tests/test_embeddings.py +0 -0
- {unitlab-3.0.2 → unitlab-3.0.3}/tests/test_http.py +0 -0
|
@@ -56,10 +56,12 @@ TILED_EXTENSIONS = (
|
|
|
56
56
|
EXTENSIONS_BY_GENERIC_TYPE: dict[str, set[str]] = {
|
|
57
57
|
"img": {"jpg", "png", "jpeg", "webp", "gif", "bmp", "ico", "svg"},
|
|
58
58
|
"text": {"txt"},
|
|
59
|
+
"html": {"html", "htm"},
|
|
59
60
|
"video": {"mp4", "avi", "mov", "webm", "mkv", "m4v", "wmv", "flv"},
|
|
60
61
|
"audio": {"mp3", "wav", "ogg", "aac", "flac", "m4a"},
|
|
61
62
|
"medical": {"dcm", "nii", "nii.gz", "nrrd"},
|
|
62
63
|
"document": {"pdf"},
|
|
64
|
+
"timeseries": {"csv"},
|
|
63
65
|
"geospatial": AMBIGUOUS_TIFF_EXTENSIONS | GEOSPATIAL_TILED_EXTENSIONS,
|
|
64
66
|
"pathology": PATHOLOGY_TILED_EXTENSIONS,
|
|
65
67
|
}
|
|
@@ -396,6 +398,7 @@ async def _finalize_medical_upload(client, endpoint: str, batch_queue_id: str) -
|
|
|
396
398
|
def _project_files(
|
|
397
399
|
source: str | Path,
|
|
398
400
|
upload_info: dict[str, Any],
|
|
401
|
+
generic_type: str | None = None,
|
|
399
402
|
) -> tuple[list[Path], list[UploadFailure]]:
|
|
400
403
|
accepted = {
|
|
401
404
|
str(value).lower().lstrip(".")
|
|
@@ -411,8 +414,10 @@ def _project_files(
|
|
|
411
414
|
result = []
|
|
412
415
|
failures = []
|
|
413
416
|
for path in files:
|
|
414
|
-
|
|
415
|
-
|
|
417
|
+
file_generic_type = (
|
|
418
|
+
generic_type or detect_generic_type(path.name) or project_type
|
|
419
|
+
)
|
|
420
|
+
max_size = max_sizes.get(file_generic_type)
|
|
416
421
|
if max_size is None and project_type and fallback is not None:
|
|
417
422
|
max_size = int(fallback)
|
|
418
423
|
if max_size is None or path.stat().st_size <= max_size:
|
|
@@ -435,6 +440,8 @@ async def _upload_project_async(
|
|
|
435
440
|
*,
|
|
436
441
|
session_id: str,
|
|
437
442
|
fps: float,
|
|
443
|
+
generic_type: str | None,
|
|
444
|
+
primary_column: str | None,
|
|
438
445
|
batch_size: int,
|
|
439
446
|
show_progress: bool,
|
|
440
447
|
) -> UploadResult:
|
|
@@ -455,11 +462,15 @@ async def _upload_project_async(
|
|
|
455
462
|
tasks = []
|
|
456
463
|
generic_types = []
|
|
457
464
|
for path in chunk:
|
|
458
|
-
|
|
459
|
-
generic_types.append(
|
|
465
|
+
file_generic_type = generic_type or detect_generic_type(path.name)
|
|
466
|
+
generic_types.append(file_generic_type)
|
|
460
467
|
fields = {"session_id": session_id}
|
|
461
|
-
if
|
|
468
|
+
if file_generic_type == "video":
|
|
462
469
|
fields["fps"] = str(fps)
|
|
470
|
+
if generic_type is not None:
|
|
471
|
+
fields["generic_type"] = generic_type
|
|
472
|
+
if primary_column is not None:
|
|
473
|
+
fields["primary_column"] = primary_column
|
|
463
474
|
tasks.append(
|
|
464
475
|
_upload_file(
|
|
465
476
|
client,
|
|
@@ -533,13 +544,15 @@ def upload_project(
|
|
|
533
544
|
source: str | Path,
|
|
534
545
|
*,
|
|
535
546
|
fps: float = 1.0,
|
|
547
|
+
generic_type: str | None = None,
|
|
548
|
+
primary_column: str | None = None,
|
|
536
549
|
batch_size: int = 100,
|
|
537
550
|
show_progress: bool = True,
|
|
538
551
|
) -> tuple[UploadResult, str]:
|
|
539
552
|
if batch_size < 1:
|
|
540
553
|
raise ValueError("batch_size must be at least 1")
|
|
541
554
|
upload_info = api.get(f"/api/sdk/projects/{project_id}/upload-info/")
|
|
542
|
-
files, preflight_failures = _project_files(source, upload_info)
|
|
555
|
+
files, preflight_failures = _project_files(source, upload_info, generic_type)
|
|
543
556
|
if not files and not preflight_failures:
|
|
544
557
|
accepted = upload_info.get("accepted_formats") or sorted(known_extensions())
|
|
545
558
|
raise ValueError(
|
|
@@ -560,6 +573,8 @@ def upload_project(
|
|
|
560
573
|
files,
|
|
561
574
|
session_id=session_id,
|
|
562
575
|
fps=fps,
|
|
576
|
+
generic_type=generic_type,
|
|
577
|
+
primary_column=primary_column,
|
|
563
578
|
batch_size=batch_size,
|
|
564
579
|
show_progress=show_progress,
|
|
565
580
|
)
|
|
@@ -576,6 +591,9 @@ def _asset_form_data(
|
|
|
576
591
|
path: str | None,
|
|
577
592
|
tags: list[str] | None,
|
|
578
593
|
custom_metadata: dict[str, Any] | None,
|
|
594
|
+
defer_timeseries_configuration: bool,
|
|
595
|
+
generic_type: str | None,
|
|
596
|
+
primary_column: str | None,
|
|
579
597
|
) -> dict[str, str | list[str]]:
|
|
580
598
|
fields: dict[str, str | list[str]] = {}
|
|
581
599
|
if folder:
|
|
@@ -592,6 +610,12 @@ def _asset_form_data(
|
|
|
592
610
|
ensure_ascii=False,
|
|
593
611
|
separators=(",", ":"),
|
|
594
612
|
)
|
|
613
|
+
if defer_timeseries_configuration:
|
|
614
|
+
fields["defer_timeseries_configuration"] = "true"
|
|
615
|
+
if generic_type is not None:
|
|
616
|
+
fields["generic_type"] = generic_type
|
|
617
|
+
if primary_column is not None:
|
|
618
|
+
fields["primary_column"] = primary_column
|
|
595
619
|
return fields
|
|
596
620
|
|
|
597
621
|
|
|
@@ -640,6 +664,9 @@ async def _upload_assets_async(
|
|
|
640
664
|
path: str | None,
|
|
641
665
|
tags: list[str] | None,
|
|
642
666
|
custom_metadata: dict[str, Any] | None,
|
|
667
|
+
defer_timeseries_configuration: bool,
|
|
668
|
+
generic_type: str | None,
|
|
669
|
+
primary_column: str | None,
|
|
643
670
|
show_progress: bool,
|
|
644
671
|
) -> tuple[UploadResult, str, str]:
|
|
645
672
|
failures: list[UploadFailure] = []
|
|
@@ -668,6 +695,9 @@ async def _upload_assets_async(
|
|
|
668
695
|
path=path,
|
|
669
696
|
tags=tags,
|
|
670
697
|
custom_metadata=custom_metadata,
|
|
698
|
+
defer_timeseries_configuration=defer_timeseries_configuration,
|
|
699
|
+
generic_type=generic_type,
|
|
700
|
+
primary_column=primary_column,
|
|
671
701
|
),
|
|
672
702
|
tiled_endpoint="/api/sdk/data-assets/tiled-uploads",
|
|
673
703
|
complete_fields=_asset_complete_data(
|
|
@@ -708,6 +738,9 @@ async def _upload_assets_async(
|
|
|
708
738
|
path=path,
|
|
709
739
|
tags=tags,
|
|
710
740
|
custom_metadata=custom_metadata,
|
|
741
|
+
defer_timeseries_configuration=defer_timeseries_configuration,
|
|
742
|
+
generic_type=generic_type,
|
|
743
|
+
primary_column=primary_column,
|
|
711
744
|
),
|
|
712
745
|
tiled_endpoint="/api/sdk/data-assets/tiled-uploads",
|
|
713
746
|
complete_fields=_asset_complete_data(
|
|
@@ -756,6 +789,9 @@ def upload_assets(
|
|
|
756
789
|
path: str | None = None,
|
|
757
790
|
tags: list[str] | None = None,
|
|
758
791
|
custom_metadata: dict[str, Any] | None = None,
|
|
792
|
+
defer_timeseries_configuration: bool = False,
|
|
793
|
+
generic_type: str | None = None,
|
|
794
|
+
primary_column: str | None = None,
|
|
759
795
|
show_progress: bool = True,
|
|
760
796
|
) -> tuple[UploadResult, str, str]:
|
|
761
797
|
if folder and folder_id:
|
|
@@ -774,6 +810,9 @@ def upload_assets(
|
|
|
774
810
|
path=path,
|
|
775
811
|
tags=tags,
|
|
776
812
|
custom_metadata=custom_metadata,
|
|
813
|
+
defer_timeseries_configuration=defer_timeseries_configuration,
|
|
814
|
+
generic_type=generic_type,
|
|
815
|
+
primary_column=primary_column,
|
|
777
816
|
show_progress=show_progress,
|
|
778
817
|
)
|
|
779
818
|
)
|
|
@@ -26,6 +26,9 @@ class AssetsNamespace(Namespace):
|
|
|
26
26
|
path: str | None = None,
|
|
27
27
|
tags: list[str] | None = None,
|
|
28
28
|
custom_metadata: dict[str, Any] | None = None,
|
|
29
|
+
defer_timeseries_configuration: bool = False,
|
|
30
|
+
generic_type: str | None = None,
|
|
31
|
+
primary_column: str | None = None,
|
|
29
32
|
) -> AssetUploadResult:
|
|
30
33
|
"""Upload local files into workspace Assets.
|
|
31
34
|
|
|
@@ -36,6 +39,12 @@ class AssetsNamespace(Namespace):
|
|
|
36
39
|
path: Logical path created below the destination folder.
|
|
37
40
|
tags: Tags applied to uploaded Assets.
|
|
38
41
|
custom_metadata: User-owned JSON metadata applied to every upload.
|
|
42
|
+
defer_timeseries_configuration: Keep the legacy deferred chart flow
|
|
43
|
+
when ``generic_type="timeseries"`` is explicitly selected.
|
|
44
|
+
generic_type: Explicit upload family, such as ``tabular`` for CSV
|
|
45
|
+
row mode. Omitted CSV Assets remain family-neutral until they
|
|
46
|
+
are attached to a project.
|
|
47
|
+
primary_column: Preferred row-mode CSV text column.
|
|
39
48
|
|
|
40
49
|
Returns:
|
|
41
50
|
Successful Assets and per-file failures without discarding partial
|
|
@@ -49,6 +58,9 @@ class AssetsNamespace(Namespace):
|
|
|
49
58
|
path=path,
|
|
50
59
|
tags=tags,
|
|
51
60
|
custom_metadata=custom_metadata,
|
|
61
|
+
defer_timeseries_configuration=defer_timeseries_configuration,
|
|
62
|
+
generic_type=generic_type,
|
|
63
|
+
primary_column=primary_column,
|
|
52
64
|
show_progress=sys.stderr.isatty(),
|
|
53
65
|
)
|
|
54
66
|
assets = [
|
|
@@ -269,9 +281,7 @@ class Asset:
|
|
|
269
281
|
)
|
|
270
282
|
|
|
271
283
|
def status(self) -> ProcessingStatus:
|
|
272
|
-
raw = self._client._api.get(
|
|
273
|
-
f"/api/sdk/data-assets/assets/{self.id}/tiled-status/"
|
|
274
|
-
)
|
|
284
|
+
raw = self._client._api.get(f"/api/sdk/data-assets/assets/{self.id}/status/")
|
|
275
285
|
self.upload_status = str(raw.get("upload_status", self.upload_status))
|
|
276
286
|
return ProcessingStatus._from_raw(raw)
|
|
277
287
|
|
|
@@ -284,7 +294,7 @@ class Asset:
|
|
|
284
294
|
) -> ProcessingStatus:
|
|
285
295
|
status = wait_for_status(
|
|
286
296
|
self._client._api,
|
|
287
|
-
f"/api/sdk/data-assets/assets/{self.id}/
|
|
297
|
+
f"/api/sdk/data-assets/assets/{self.id}/status/",
|
|
288
298
|
resource_name=f"Asset {self.id}",
|
|
289
299
|
timeout=timeout,
|
|
290
300
|
on_progress=on_progress,
|
|
@@ -302,6 +312,19 @@ class Asset:
|
|
|
302
312
|
self.upload_status = str(raw.get("status", "processing"))
|
|
303
313
|
return raw
|
|
304
314
|
|
|
315
|
+
def configure_timeseries(
|
|
316
|
+
self,
|
|
317
|
+
*,
|
|
318
|
+
x_column: str,
|
|
319
|
+
channels: list[str],
|
|
320
|
+
) -> dict[str, Any]:
|
|
321
|
+
raw = self._client._api.post(
|
|
322
|
+
f"/api/sdk/data-assets/assets/{self.id}/timeseries-configuration/",
|
|
323
|
+
json={"x_column": x_column, "channels": channels},
|
|
324
|
+
)
|
|
325
|
+
self.upload_status = str(raw.get("upload_status", self.upload_status))
|
|
326
|
+
return raw
|
|
327
|
+
|
|
305
328
|
def update_custom_metadata(
|
|
306
329
|
self,
|
|
307
330
|
custom_metadata: dict[str, Any] | None,
|
|
@@ -256,6 +256,8 @@ class Project:
|
|
|
256
256
|
source: str | Path,
|
|
257
257
|
*,
|
|
258
258
|
fps: float = 1.0,
|
|
259
|
+
generic_type: str | None = None,
|
|
260
|
+
primary_column: str | None = None,
|
|
259
261
|
batch_size: int = 100,
|
|
260
262
|
) -> UploadBatch:
|
|
261
263
|
"""Upload local files into this Project.
|
|
@@ -263,17 +265,25 @@ class Project:
|
|
|
263
265
|
Args:
|
|
264
266
|
source: File or directory to upload.
|
|
265
267
|
fps: Frame rate used for uploaded video.
|
|
268
|
+
generic_type: Explicit upload family, such as ``tabular`` for CSV
|
|
269
|
+
row mode. Omitted CSV uploads remain pending until configured
|
|
270
|
+
in the target project.
|
|
271
|
+
primary_column: Preferred row-mode CSV text column.
|
|
266
272
|
batch_size: Files scheduled per local upload batch.
|
|
267
273
|
|
|
268
274
|
Returns:
|
|
269
275
|
Local upload results and the server-side Batch Queue ID. Server
|
|
270
|
-
processing may still be running.
|
|
276
|
+
processing may still be running. Declared ``tabular`` uploads are
|
|
277
|
+
accepted asynchronously and their datasource IDs are returned
|
|
278
|
+
immediately.
|
|
271
279
|
"""
|
|
272
280
|
result, session_id = _uploader.upload_project(
|
|
273
281
|
self._client._api,
|
|
274
282
|
self.id,
|
|
275
283
|
source,
|
|
276
284
|
fps=fps,
|
|
285
|
+
generic_type=generic_type,
|
|
286
|
+
primary_column=primary_column,
|
|
277
287
|
batch_size=batch_size,
|
|
278
288
|
show_progress=sys.stderr.isatty(),
|
|
279
289
|
)
|
|
@@ -252,6 +252,18 @@ def test_ontology_builder_and_client_contract():
|
|
|
252
252
|
client.close()
|
|
253
253
|
|
|
254
254
|
|
|
255
|
+
@pytest.mark.parametrize("shape_value", ("interval", "instant"))
|
|
256
|
+
def test_timeseries_ontology_shapes_round_trip(shape_value):
|
|
257
|
+
shape = Shape(shape_value)
|
|
258
|
+
structure = OntologyStructure()
|
|
259
|
+
|
|
260
|
+
created = structure.add_object("Chart event", shape)
|
|
261
|
+
restored = OntologyStructure.from_dict(structure.to_dict()).objects[0]
|
|
262
|
+
|
|
263
|
+
assert created.shape is shape
|
|
264
|
+
assert restored.shape is shape
|
|
265
|
+
|
|
266
|
+
|
|
255
267
|
def test_release_detail_parses_and_downloads(monkeypatch):
|
|
256
268
|
def handler(request):
|
|
257
269
|
assert request.url.path == "/api/sdk/releases/r1/"
|
|
@@ -503,8 +503,45 @@ def test_asset_status_wait_and_retry_use_sdk_endpoints():
|
|
|
503
503
|
assert asset.retry() == {"task_id": "task-2", "status": "processing"}
|
|
504
504
|
assert asset.upload_status == "processing"
|
|
505
505
|
assert requests == [
|
|
506
|
-
("GET", "/api/sdk/data-assets/assets/asset-1/
|
|
507
|
-
("GET", "/api/sdk/data-assets/assets/asset-1/
|
|
506
|
+
("GET", "/api/sdk/data-assets/assets/asset-1/status/"),
|
|
507
|
+
("GET", "/api/sdk/data-assets/assets/asset-1/status/"),
|
|
508
508
|
("POST", "/api/sdk/data-assets/assets/asset-1/tiled-retry/"),
|
|
509
509
|
]
|
|
510
510
|
client.close()
|
|
511
|
+
|
|
512
|
+
|
|
513
|
+
def test_html_asset_status_and_wait_use_general_status_endpoint():
|
|
514
|
+
requests = []
|
|
515
|
+
|
|
516
|
+
def handler(request):
|
|
517
|
+
requests.append((request.method, request.url.path))
|
|
518
|
+
return httpx.Response(
|
|
519
|
+
200,
|
|
520
|
+
json={
|
|
521
|
+
"status": "completed",
|
|
522
|
+
"upload_status": "completed",
|
|
523
|
+
"total": 1,
|
|
524
|
+
"completed": 1,
|
|
525
|
+
"processing": 0,
|
|
526
|
+
"failed": 0,
|
|
527
|
+
},
|
|
528
|
+
)
|
|
529
|
+
|
|
530
|
+
client = configured_client(handler)
|
|
531
|
+
asset = Asset._from_raw(
|
|
532
|
+
client,
|
|
533
|
+
{
|
|
534
|
+
"pk": "asset-html",
|
|
535
|
+
"file_name": "page.html",
|
|
536
|
+
"generic_type": "html",
|
|
537
|
+
"upload_status": "processing",
|
|
538
|
+
},
|
|
539
|
+
)
|
|
540
|
+
|
|
541
|
+
assert asset.status().status == "completed"
|
|
542
|
+
assert asset.wait(show_progress=False).completed == 1
|
|
543
|
+
assert requests == [
|
|
544
|
+
("GET", "/api/sdk/data-assets/assets/asset-html/status/"),
|
|
545
|
+
("GET", "/api/sdk/data-assets/assets/asset-html/status/"),
|
|
546
|
+
]
|
|
547
|
+
client.close()
|
|
@@ -8,12 +8,25 @@ from unitlab import (
|
|
|
8
8
|
AuthenticationError,
|
|
9
9
|
ProcessingTimeoutError,
|
|
10
10
|
UnitlabClient,
|
|
11
|
+
_uploader,
|
|
11
12
|
tiles_from_template,
|
|
12
13
|
)
|
|
13
14
|
from unitlab.resources.assets import Asset
|
|
14
15
|
from unitlab.resources.projects import BatchQueue, Project
|
|
15
16
|
|
|
16
17
|
|
|
18
|
+
@pytest.mark.parametrize(
|
|
19
|
+
("filename", "generic_type"),
|
|
20
|
+
(("page.html", "html"), ("page.htm", "html"), ("series.csv", "timeseries")),
|
|
21
|
+
)
|
|
22
|
+
def test_html_and_timeseries_files_are_uploadable(tmp_path, filename, generic_type):
|
|
23
|
+
path = tmp_path / filename
|
|
24
|
+
path.write_bytes(b"content")
|
|
25
|
+
|
|
26
|
+
assert _uploader.detect_generic_type(filename) == generic_type
|
|
27
|
+
assert _uploader.collect_files(path) == [path]
|
|
28
|
+
|
|
29
|
+
|
|
17
30
|
def configured_client(handler):
|
|
18
31
|
transport = httpx.MockTransport(handler)
|
|
19
32
|
client = UnitlabClient(api_key="key", api_url="http://testserver")
|
|
@@ -79,6 +92,49 @@ def test_project_upload_uses_one_batch_queue_and_chunks(tmp_path):
|
|
|
79
92
|
client.close()
|
|
80
93
|
|
|
81
94
|
|
|
95
|
+
def test_project_upload_can_select_tabular_csv_row_mode(tmp_path):
|
|
96
|
+
upload_body = ""
|
|
97
|
+
|
|
98
|
+
def handler(request):
|
|
99
|
+
nonlocal upload_body
|
|
100
|
+
if request.method == "GET":
|
|
101
|
+
return httpx.Response(
|
|
102
|
+
200,
|
|
103
|
+
json={
|
|
104
|
+
"accepted_formats": ["csv"],
|
|
105
|
+
"max_file_sizes": {"timeseries": 1_000, "tabular": 1_000},
|
|
106
|
+
"generic_type": None,
|
|
107
|
+
},
|
|
108
|
+
)
|
|
109
|
+
upload_body = request.content.decode("latin1")
|
|
110
|
+
return httpx.Response(
|
|
111
|
+
202,
|
|
112
|
+
json={
|
|
113
|
+
"datasource_id": "row-1",
|
|
114
|
+
"upload_session_id": "queue-1",
|
|
115
|
+
"generic_type": "tabular",
|
|
116
|
+
"upload_status": "processing",
|
|
117
|
+
"task_id": "tabular-task-1",
|
|
118
|
+
},
|
|
119
|
+
)
|
|
120
|
+
|
|
121
|
+
path = tmp_path / "feedback.csv"
|
|
122
|
+
path.write_text("id,comment\n1,Great product\n")
|
|
123
|
+
client = configured_client(handler)
|
|
124
|
+
project = Project("p1", "Project", {"pk": "p1"}, client)
|
|
125
|
+
|
|
126
|
+
batch = project.upload(path, generic_type="tabular", primary_column="comment")
|
|
127
|
+
|
|
128
|
+
assert batch.uploaded == 1
|
|
129
|
+
assert batch.raw[0]["upload_status"] == "processing"
|
|
130
|
+
assert batch.raw[0]["task_id"] == "tabular-task-1"
|
|
131
|
+
assert 'name="generic_type"' in upload_body
|
|
132
|
+
assert "tabular" in upload_body
|
|
133
|
+
assert 'name="primary_column"' in upload_body
|
|
134
|
+
assert "comment" in upload_body
|
|
135
|
+
client.close()
|
|
136
|
+
|
|
137
|
+
|
|
82
138
|
def test_project_upload_reports_oversized_files(tmp_path):
|
|
83
139
|
uploads = 0
|
|
84
140
|
|
|
@@ -291,6 +347,113 @@ def test_asset_upload_resolves_folder_once_then_reuses_id(tmp_path):
|
|
|
291
347
|
client.close()
|
|
292
348
|
|
|
293
349
|
|
|
350
|
+
def test_asset_upload_can_select_tabular_csv_row_mode(tmp_path):
|
|
351
|
+
upload_body = ""
|
|
352
|
+
|
|
353
|
+
def handler(request):
|
|
354
|
+
nonlocal upload_body
|
|
355
|
+
upload_body = request.content.decode("latin1")
|
|
356
|
+
return httpx.Response(
|
|
357
|
+
201,
|
|
358
|
+
json={
|
|
359
|
+
"folder_id": "folder-1",
|
|
360
|
+
"folder_name": "Feedback",
|
|
361
|
+
"asset": {
|
|
362
|
+
"pk": "asset-1",
|
|
363
|
+
"file_name": "feedback.csv",
|
|
364
|
+
"generic_type": "tabular",
|
|
365
|
+
"folder_id": "folder-1",
|
|
366
|
+
},
|
|
367
|
+
},
|
|
368
|
+
)
|
|
369
|
+
|
|
370
|
+
path = tmp_path / "feedback.csv"
|
|
371
|
+
path.write_text("id,comment\n1,Great product\n")
|
|
372
|
+
client = configured_client(handler)
|
|
373
|
+
|
|
374
|
+
result = client.assets.upload(
|
|
375
|
+
path,
|
|
376
|
+
folder="Feedback",
|
|
377
|
+
generic_type="tabular",
|
|
378
|
+
primary_column="comment",
|
|
379
|
+
)
|
|
380
|
+
|
|
381
|
+
assert result.uploaded == 1
|
|
382
|
+
assert 'name="generic_type"' in upload_body
|
|
383
|
+
assert "tabular" in upload_body
|
|
384
|
+
assert 'name="primary_column"' in upload_body
|
|
385
|
+
assert "comment" in upload_body
|
|
386
|
+
client.close()
|
|
387
|
+
|
|
388
|
+
|
|
389
|
+
def test_deferred_timeseries_upload_can_be_completed(tmp_path):
|
|
390
|
+
upload_body = ""
|
|
391
|
+
|
|
392
|
+
def handler(request):
|
|
393
|
+
nonlocal upload_body
|
|
394
|
+
if request.url.path == "/api/sdk/data-assets/upload/":
|
|
395
|
+
upload_body = request.content.decode("latin1")
|
|
396
|
+
return httpx.Response(
|
|
397
|
+
202,
|
|
398
|
+
json={
|
|
399
|
+
"folder_id": "folder-1",
|
|
400
|
+
"folder_name": "Charts",
|
|
401
|
+
"timeseries_configuration_required": True,
|
|
402
|
+
"configuration": {
|
|
403
|
+
"columns": ["time", "value"],
|
|
404
|
+
"defaults": {"x_column": "time", "channels": ["value"]},
|
|
405
|
+
},
|
|
406
|
+
"asset": {
|
|
407
|
+
"pk": "asset-1",
|
|
408
|
+
"file_name": "chart.csv",
|
|
409
|
+
"generic_type": "timeseries",
|
|
410
|
+
"folder_id": "folder-1",
|
|
411
|
+
"upload_status": "processing",
|
|
412
|
+
},
|
|
413
|
+
},
|
|
414
|
+
)
|
|
415
|
+
assert request.url.path == (
|
|
416
|
+
"/api/sdk/data-assets/assets/asset-1/timeseries-configuration/"
|
|
417
|
+
)
|
|
418
|
+
assert json.loads(request.content) == {
|
|
419
|
+
"x_column": "time",
|
|
420
|
+
"channels": ["value"],
|
|
421
|
+
}
|
|
422
|
+
return httpx.Response(
|
|
423
|
+
200,
|
|
424
|
+
json={
|
|
425
|
+
"asset_id": "asset-1",
|
|
426
|
+
"upload_status": "completed",
|
|
427
|
+
"timeseries_configuration_required": False,
|
|
428
|
+
"configuration": None,
|
|
429
|
+
"timeseries": {"x_column": "time", "channels": ["value"]},
|
|
430
|
+
},
|
|
431
|
+
)
|
|
432
|
+
|
|
433
|
+
path = tmp_path / "chart.csv"
|
|
434
|
+
path.write_text("time,value\n0,1\n1,2\n")
|
|
435
|
+
client = configured_client(handler)
|
|
436
|
+
|
|
437
|
+
result = client.assets.upload(
|
|
438
|
+
path,
|
|
439
|
+
folder="Charts",
|
|
440
|
+
defer_timeseries_configuration=True,
|
|
441
|
+
)
|
|
442
|
+
assert 'name="defer_timeseries_configuration"' in upload_body
|
|
443
|
+
assert "true" in upload_body
|
|
444
|
+
assert result.raw[0]["configuration"]["defaults"]["x_column"] == "time"
|
|
445
|
+
asset = result.assets[0]
|
|
446
|
+
|
|
447
|
+
configuration = asset.configure_timeseries(
|
|
448
|
+
x_column="time",
|
|
449
|
+
channels=["value"],
|
|
450
|
+
)
|
|
451
|
+
|
|
452
|
+
assert configuration["timeseries_configuration_required"] is False
|
|
453
|
+
assert asset.upload_status == "completed"
|
|
454
|
+
client.close()
|
|
455
|
+
|
|
456
|
+
|
|
294
457
|
def test_asset_updates_custom_metadata():
|
|
295
458
|
def handler(request):
|
|
296
459
|
assert request.method == "PATCH"
|
|
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
|
|
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
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|