lightly-studio 0.3.1__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.
Potentially problematic release.
This version of lightly-studio might be problematic. Click here for more details.
- lightly_studio/__init__.py +11 -0
- lightly_studio/api/__init__.py +0 -0
- lightly_studio/api/app.py +110 -0
- lightly_studio/api/cache.py +77 -0
- lightly_studio/api/db.py +133 -0
- lightly_studio/api/db_tables.py +32 -0
- lightly_studio/api/features.py +7 -0
- lightly_studio/api/routes/api/annotation.py +233 -0
- lightly_studio/api/routes/api/annotation_label.py +90 -0
- lightly_studio/api/routes/api/annotation_task.py +38 -0
- lightly_studio/api/routes/api/classifier.py +387 -0
- lightly_studio/api/routes/api/dataset.py +182 -0
- lightly_studio/api/routes/api/dataset_tag.py +257 -0
- lightly_studio/api/routes/api/exceptions.py +96 -0
- lightly_studio/api/routes/api/features.py +17 -0
- lightly_studio/api/routes/api/metadata.py +37 -0
- lightly_studio/api/routes/api/metrics.py +80 -0
- lightly_studio/api/routes/api/sample.py +196 -0
- lightly_studio/api/routes/api/settings.py +45 -0
- lightly_studio/api/routes/api/status.py +19 -0
- lightly_studio/api/routes/api/text_embedding.py +48 -0
- lightly_studio/api/routes/api/validators.py +17 -0
- lightly_studio/api/routes/healthz.py +13 -0
- lightly_studio/api/routes/images.py +104 -0
- lightly_studio/api/routes/webapp.py +51 -0
- lightly_studio/api/server.py +82 -0
- lightly_studio/core/__init__.py +0 -0
- lightly_studio/core/dataset.py +523 -0
- lightly_studio/core/sample.py +77 -0
- lightly_studio/core/start_gui.py +15 -0
- lightly_studio/dataset/__init__.py +0 -0
- lightly_studio/dataset/edge_embedding_generator.py +144 -0
- lightly_studio/dataset/embedding_generator.py +91 -0
- lightly_studio/dataset/embedding_manager.py +163 -0
- lightly_studio/dataset/env.py +16 -0
- lightly_studio/dataset/file_utils.py +35 -0
- lightly_studio/dataset/loader.py +622 -0
- lightly_studio/dataset/mobileclip_embedding_generator.py +144 -0
- lightly_studio/dist_lightly_studio_view_app/_app/env.js +1 -0
- lightly_studio/dist_lightly_studio_view_app/_app/immutable/assets/0.DenzbfeK.css +1 -0
- lightly_studio/dist_lightly_studio_view_app/_app/immutable/assets/LightlyLogo.BNjCIww-.png +0 -0
- lightly_studio/dist_lightly_studio_view_app/_app/immutable/assets/OpenSans- +0 -0
- lightly_studio/dist_lightly_studio_view_app/_app/immutable/assets/OpenSans-Bold.DGvYQtcs.ttf +0 -0
- lightly_studio/dist_lightly_studio_view_app/_app/immutable/assets/OpenSans-Italic-VariableFont_wdth_wght.B4AZ-wl6.ttf +0 -0
- lightly_studio/dist_lightly_studio_view_app/_app/immutable/assets/OpenSans-Regular.DxJTClRG.ttf +0 -0
- lightly_studio/dist_lightly_studio_view_app/_app/immutable/assets/OpenSans-SemiBold.D3TTYgdB.ttf +0 -0
- lightly_studio/dist_lightly_studio_view_app/_app/immutable/assets/OpenSans-VariableFont_wdth_wght.BZBpG5Iz.ttf +0 -0
- lightly_studio/dist_lightly_studio_view_app/_app/immutable/assets/SelectableSvgGroup.OwPEPQZu.css +1 -0
- lightly_studio/dist_lightly_studio_view_app/_app/immutable/assets/SelectableSvgGroup.b653GmVf.css +1 -0
- lightly_studio/dist_lightly_studio_view_app/_app/immutable/assets/_layout.T-zjSUd3.css +1 -0
- lightly_studio/dist_lightly_studio_view_app/_app/immutable/assets/useFeatureFlags.CV-KWLNP.css +1 -0
- lightly_studio/dist_lightly_studio_view_app/_app/immutable/chunks/69_IOA4Y.js +1 -0
- lightly_studio/dist_lightly_studio_view_app/_app/immutable/chunks/B2FVR0s0.js +1 -0
- lightly_studio/dist_lightly_studio_view_app/_app/immutable/chunks/B90CZVMX.js +1 -0
- lightly_studio/dist_lightly_studio_view_app/_app/immutable/chunks/B9zumHo5.js +1 -0
- lightly_studio/dist_lightly_studio_view_app/_app/immutable/chunks/BJXwVxaE.js +1 -0
- lightly_studio/dist_lightly_studio_view_app/_app/immutable/chunks/Bsi3UGy5.js +1 -0
- lightly_studio/dist_lightly_studio_view_app/_app/immutable/chunks/Bu7uvVrG.js +1 -0
- lightly_studio/dist_lightly_studio_view_app/_app/immutable/chunks/Bx1xMsFy.js +1 -0
- lightly_studio/dist_lightly_studio_view_app/_app/immutable/chunks/BylOuP6i.js +1 -0
- lightly_studio/dist_lightly_studio_view_app/_app/immutable/chunks/C8I8rFJQ.js +1 -0
- lightly_studio/dist_lightly_studio_view_app/_app/immutable/chunks/CDnpyLsT.js +1 -0
- lightly_studio/dist_lightly_studio_view_app/_app/immutable/chunks/CWj6FrbW.js +1 -0
- lightly_studio/dist_lightly_studio_view_app/_app/immutable/chunks/CYgJF_JY.js +1 -0
- lightly_studio/dist_lightly_studio_view_app/_app/immutable/chunks/CcaPhhk3.js +1 -0
- lightly_studio/dist_lightly_studio_view_app/_app/immutable/chunks/CvOmgdoc.js +93 -0
- lightly_studio/dist_lightly_studio_view_app/_app/immutable/chunks/CxtLVaYz.js +3 -0
- lightly_studio/dist_lightly_studio_view_app/_app/immutable/chunks/D5-A_Ffd.js +4 -0
- lightly_studio/dist_lightly_studio_view_app/_app/immutable/chunks/D6RI2Zrd.js +1 -0
- lightly_studio/dist_lightly_studio_view_app/_app/immutable/chunks/D6su9Aln.js +1 -0
- lightly_studio/dist_lightly_studio_view_app/_app/immutable/chunks/D98V7j6A.js +1 -0
- lightly_studio/dist_lightly_studio_view_app/_app/immutable/chunks/DIRAtgl0.js +1 -0
- lightly_studio/dist_lightly_studio_view_app/_app/immutable/chunks/DIeogL5L.js +1 -0
- lightly_studio/dist_lightly_studio_view_app/_app/immutable/chunks/DOlTMNyt.js +1 -0
- lightly_studio/dist_lightly_studio_view_app/_app/immutable/chunks/DjUWrjOv.js +1 -0
- lightly_studio/dist_lightly_studio_view_app/_app/immutable/chunks/DjfY96ND.js +1 -0
- lightly_studio/dist_lightly_studio_view_app/_app/immutable/chunks/H7C68rOM.js +1 -0
- lightly_studio/dist_lightly_studio_view_app/_app/immutable/chunks/O-EABkf9.js +1 -0
- lightly_studio/dist_lightly_studio_view_app/_app/immutable/chunks/XO7A28GO.js +1 -0
- lightly_studio/dist_lightly_studio_view_app/_app/immutable/chunks/hQVEETDE.js +1 -0
- lightly_studio/dist_lightly_studio_view_app/_app/immutable/chunks/l7KrR96u.js +1 -0
- lightly_studio/dist_lightly_studio_view_app/_app/immutable/chunks/nAHhluT7.js +1 -0
- lightly_studio/dist_lightly_studio_view_app/_app/immutable/chunks/r64xT6ao.js +1 -0
- lightly_studio/dist_lightly_studio_view_app/_app/immutable/chunks/vC4nQVEB.js +1 -0
- lightly_studio/dist_lightly_studio_view_app/_app/immutable/chunks/x9G_hzyY.js +1 -0
- lightly_studio/dist_lightly_studio_view_app/_app/immutable/entry/app.CjnvpsmS.js +2 -0
- lightly_studio/dist_lightly_studio_view_app/_app/immutable/entry/start.0o1H7wM9.js +1 -0
- lightly_studio/dist_lightly_studio_view_app/_app/immutable/nodes/0.XRq_TUwu.js +1 -0
- lightly_studio/dist_lightly_studio_view_app/_app/immutable/nodes/1.B4rNYwVp.js +1 -0
- lightly_studio/dist_lightly_studio_view_app/_app/immutable/nodes/10.DfBwOEhN.js +1 -0
- lightly_studio/dist_lightly_studio_view_app/_app/immutable/nodes/11.CWG1ehzT.js +1 -0
- lightly_studio/dist_lightly_studio_view_app/_app/immutable/nodes/12.CwF2_8mP.js +1 -0
- lightly_studio/dist_lightly_studio_view_app/_app/immutable/nodes/2.CS4muRY-.js +6 -0
- lightly_studio/dist_lightly_studio_view_app/_app/immutable/nodes/3.CWHpKonm.js +1 -0
- lightly_studio/dist_lightly_studio_view_app/_app/immutable/nodes/4.OUWOLQeV.js +1 -0
- lightly_studio/dist_lightly_studio_view_app/_app/immutable/nodes/5.Dm6t9F5W.js +1 -0
- lightly_studio/dist_lightly_studio_view_app/_app/immutable/nodes/6.Bw5ck4gK.js +1 -0
- lightly_studio/dist_lightly_studio_view_app/_app/immutable/nodes/7.CF0EDTR6.js +1 -0
- lightly_studio/dist_lightly_studio_view_app/_app/immutable/nodes/8.Cw30LEcV.js +1 -0
- lightly_studio/dist_lightly_studio_view_app/_app/immutable/nodes/9.CPu3CiBc.js +1 -0
- lightly_studio/dist_lightly_studio_view_app/_app/version.json +1 -0
- lightly_studio/dist_lightly_studio_view_app/apple-touch-icon-precomposed.png +0 -0
- lightly_studio/dist_lightly_studio_view_app/apple-touch-icon.png +0 -0
- lightly_studio/dist_lightly_studio_view_app/favicon.png +0 -0
- lightly_studio/dist_lightly_studio_view_app/index.html +44 -0
- lightly_studio/examples/example.py +23 -0
- lightly_studio/examples/example_metadata.py +338 -0
- lightly_studio/examples/example_selection.py +39 -0
- lightly_studio/examples/example_split_work.py +67 -0
- lightly_studio/examples/example_v2.py +21 -0
- lightly_studio/export_schema.py +18 -0
- lightly_studio/few_shot_classifier/__init__.py +0 -0
- lightly_studio/few_shot_classifier/classifier.py +80 -0
- lightly_studio/few_shot_classifier/classifier_manager.py +663 -0
- lightly_studio/few_shot_classifier/random_forest_classifier.py +489 -0
- lightly_studio/metadata/complex_metadata.py +47 -0
- lightly_studio/metadata/gps_coordinate.py +41 -0
- lightly_studio/metadata/metadata_protocol.py +17 -0
- lightly_studio/metrics/__init__.py +0 -0
- lightly_studio/metrics/detection/__init__.py +0 -0
- lightly_studio/metrics/detection/map.py +268 -0
- lightly_studio/models/__init__.py +1 -0
- lightly_studio/models/annotation/__init__.py +0 -0
- lightly_studio/models/annotation/annotation_base.py +171 -0
- lightly_studio/models/annotation/instance_segmentation.py +56 -0
- lightly_studio/models/annotation/links.py +17 -0
- lightly_studio/models/annotation/object_detection.py +47 -0
- lightly_studio/models/annotation/semantic_segmentation.py +44 -0
- lightly_studio/models/annotation_label.py +47 -0
- lightly_studio/models/annotation_task.py +28 -0
- lightly_studio/models/classifier.py +20 -0
- lightly_studio/models/dataset.py +84 -0
- lightly_studio/models/embedding_model.py +30 -0
- lightly_studio/models/metadata.py +208 -0
- lightly_studio/models/sample.py +180 -0
- lightly_studio/models/sample_embedding.py +37 -0
- lightly_studio/models/settings.py +60 -0
- lightly_studio/models/tag.py +96 -0
- lightly_studio/py.typed +0 -0
- lightly_studio/resolvers/__init__.py +7 -0
- lightly_studio/resolvers/annotation_label_resolver/__init__.py +21 -0
- lightly_studio/resolvers/annotation_label_resolver/create.py +27 -0
- lightly_studio/resolvers/annotation_label_resolver/delete.py +28 -0
- lightly_studio/resolvers/annotation_label_resolver/get_all.py +22 -0
- lightly_studio/resolvers/annotation_label_resolver/get_by_id.py +24 -0
- lightly_studio/resolvers/annotation_label_resolver/get_by_ids.py +25 -0
- lightly_studio/resolvers/annotation_label_resolver/get_by_label_name.py +24 -0
- lightly_studio/resolvers/annotation_label_resolver/names_by_ids.py +25 -0
- lightly_studio/resolvers/annotation_label_resolver/update.py +38 -0
- lightly_studio/resolvers/annotation_resolver/__init__.py +33 -0
- lightly_studio/resolvers/annotation_resolver/count_annotations_by_dataset.py +120 -0
- lightly_studio/resolvers/annotation_resolver/create.py +19 -0
- lightly_studio/resolvers/annotation_resolver/create_many.py +96 -0
- lightly_studio/resolvers/annotation_resolver/delete_annotation.py +45 -0
- lightly_studio/resolvers/annotation_resolver/delete_annotations.py +56 -0
- lightly_studio/resolvers/annotation_resolver/get_all.py +74 -0
- lightly_studio/resolvers/annotation_resolver/get_by_id.py +18 -0
- lightly_studio/resolvers/annotation_resolver/update_annotation_label.py +144 -0
- lightly_studio/resolvers/annotation_resolver/update_bounding_box.py +68 -0
- lightly_studio/resolvers/annotation_task_resolver.py +31 -0
- lightly_studio/resolvers/annotations/__init__.py +1 -0
- lightly_studio/resolvers/annotations/annotations_filter.py +89 -0
- lightly_studio/resolvers/dataset_resolver.py +278 -0
- lightly_studio/resolvers/embedding_model_resolver.py +100 -0
- lightly_studio/resolvers/metadata_resolver/__init__.py +15 -0
- lightly_studio/resolvers/metadata_resolver/metadata_filter.py +163 -0
- lightly_studio/resolvers/metadata_resolver/sample/__init__.py +21 -0
- lightly_studio/resolvers/metadata_resolver/sample/bulk_set_metadata.py +48 -0
- lightly_studio/resolvers/metadata_resolver/sample/get_by_sample_id.py +24 -0
- lightly_studio/resolvers/metadata_resolver/sample/get_metadata_info.py +104 -0
- lightly_studio/resolvers/metadata_resolver/sample/get_value_for_sample.py +27 -0
- lightly_studio/resolvers/metadata_resolver/sample/set_value_for_sample.py +53 -0
- lightly_studio/resolvers/sample_embedding_resolver.py +86 -0
- lightly_studio/resolvers/sample_resolver.py +249 -0
- lightly_studio/resolvers/samples_filter.py +81 -0
- lightly_studio/resolvers/settings_resolver.py +58 -0
- lightly_studio/resolvers/tag_resolver.py +276 -0
- lightly_studio/selection/README.md +6 -0
- lightly_studio/selection/mundig.py +105 -0
- lightly_studio/selection/select.py +96 -0
- lightly_studio/selection/select_via_db.py +93 -0
- lightly_studio/selection/selection_config.py +31 -0
- lightly_studio/services/annotations_service/__init__.py +21 -0
- lightly_studio/services/annotations_service/get_annotation_by_id.py +31 -0
- lightly_studio/services/annotations_service/update_annotation.py +65 -0
- lightly_studio/services/annotations_service/update_annotation_label.py +48 -0
- lightly_studio/services/annotations_service/update_annotations.py +29 -0
- lightly_studio/setup_logging.py +19 -0
- lightly_studio/type_definitions.py +19 -0
- lightly_studio/vendor/ACKNOWLEDGEMENTS +422 -0
- lightly_studio/vendor/LICENSE +31 -0
- lightly_studio/vendor/LICENSE_weights_data +50 -0
- lightly_studio/vendor/README.md +5 -0
- lightly_studio/vendor/__init__.py +1 -0
- lightly_studio/vendor/mobileclip/__init__.py +96 -0
- lightly_studio/vendor/mobileclip/clip.py +77 -0
- lightly_studio/vendor/mobileclip/configs/mobileclip_b.json +18 -0
- lightly_studio/vendor/mobileclip/configs/mobileclip_s0.json +18 -0
- lightly_studio/vendor/mobileclip/configs/mobileclip_s1.json +18 -0
- lightly_studio/vendor/mobileclip/configs/mobileclip_s2.json +18 -0
- lightly_studio/vendor/mobileclip/image_encoder.py +67 -0
- lightly_studio/vendor/mobileclip/logger.py +154 -0
- lightly_studio/vendor/mobileclip/models/__init__.py +10 -0
- lightly_studio/vendor/mobileclip/models/mci.py +933 -0
- lightly_studio/vendor/mobileclip/models/vit.py +433 -0
- lightly_studio/vendor/mobileclip/modules/__init__.py +4 -0
- lightly_studio/vendor/mobileclip/modules/common/__init__.py +4 -0
- lightly_studio/vendor/mobileclip/modules/common/mobileone.py +341 -0
- lightly_studio/vendor/mobileclip/modules/common/transformer.py +451 -0
- lightly_studio/vendor/mobileclip/modules/image/__init__.py +4 -0
- lightly_studio/vendor/mobileclip/modules/image/image_projection.py +113 -0
- lightly_studio/vendor/mobileclip/modules/image/replknet.py +188 -0
- lightly_studio/vendor/mobileclip/modules/text/__init__.py +4 -0
- lightly_studio/vendor/mobileclip/modules/text/repmixer.py +281 -0
- lightly_studio/vendor/mobileclip/modules/text/tokenizer.py +38 -0
- lightly_studio/vendor/mobileclip/text_encoder.py +245 -0
- lightly_studio-0.3.1.dist-info/METADATA +520 -0
- lightly_studio-0.3.1.dist-info/RECORD +219 -0
- lightly_studio-0.3.1.dist-info/WHEEL +4 -0
|
@@ -0,0 +1,257 @@
|
|
|
1
|
+
"""This module contains the API routes for managing tags."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from typing import List
|
|
6
|
+
from uuid import UUID
|
|
7
|
+
|
|
8
|
+
from fastapi import APIRouter, Depends, HTTPException, Path, Query
|
|
9
|
+
from pydantic import BaseModel
|
|
10
|
+
from sqlalchemy.exc import IntegrityError
|
|
11
|
+
from sqlmodel import Field, Session
|
|
12
|
+
from typing_extensions import Annotated
|
|
13
|
+
|
|
14
|
+
from lightly_studio.api.db import get_session
|
|
15
|
+
from lightly_studio.api.routes.api.dataset import get_and_validate_dataset_id
|
|
16
|
+
from lightly_studio.api.routes.api.status import (
|
|
17
|
+
HTTP_STATUS_CONFLICT,
|
|
18
|
+
HTTP_STATUS_CREATED,
|
|
19
|
+
HTTP_STATUS_NOT_FOUND,
|
|
20
|
+
)
|
|
21
|
+
from lightly_studio.api.routes.api.validators import Paginated
|
|
22
|
+
from lightly_studio.models.dataset import DatasetTable
|
|
23
|
+
from lightly_studio.models.tag import (
|
|
24
|
+
TagCreate,
|
|
25
|
+
TagCreateBody,
|
|
26
|
+
TagTable,
|
|
27
|
+
TagUpdate,
|
|
28
|
+
TagUpdateBody,
|
|
29
|
+
TagView,
|
|
30
|
+
)
|
|
31
|
+
from lightly_studio.resolvers import tag_resolver
|
|
32
|
+
|
|
33
|
+
tag_router = APIRouter()
|
|
34
|
+
SessionDep = Annotated[Session, Depends(get_session)]
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
@tag_router.post(
|
|
38
|
+
"/datasets/{dataset_id}/tags",
|
|
39
|
+
response_model=TagView,
|
|
40
|
+
status_code=HTTP_STATUS_CREATED,
|
|
41
|
+
)
|
|
42
|
+
def create_tag(
|
|
43
|
+
session: SessionDep,
|
|
44
|
+
dataset: Annotated[
|
|
45
|
+
DatasetTable,
|
|
46
|
+
Path(title="Dataset Id"),
|
|
47
|
+
Depends(get_and_validate_dataset_id),
|
|
48
|
+
],
|
|
49
|
+
body: TagCreateBody,
|
|
50
|
+
) -> TagTable:
|
|
51
|
+
"""Create a new tag in the database."""
|
|
52
|
+
dataset_id = dataset.dataset_id
|
|
53
|
+
try:
|
|
54
|
+
return tag_resolver.create(
|
|
55
|
+
session=session,
|
|
56
|
+
tag=TagCreate(**body.model_dump(exclude_unset=True), dataset_id=dataset_id),
|
|
57
|
+
)
|
|
58
|
+
except IntegrityError as e:
|
|
59
|
+
raise HTTPException(
|
|
60
|
+
status_code=HTTP_STATUS_CONFLICT,
|
|
61
|
+
detail=f"""
|
|
62
|
+
Tag with name {body.name} already exists
|
|
63
|
+
in the dataset {dataset_id}.
|
|
64
|
+
""",
|
|
65
|
+
) from e
|
|
66
|
+
|
|
67
|
+
|
|
68
|
+
@tag_router.get("/datasets/{dataset_id}/tags", response_model=List[TagView])
|
|
69
|
+
def read_tags(
|
|
70
|
+
session: SessionDep,
|
|
71
|
+
dataset: Annotated[
|
|
72
|
+
DatasetTable,
|
|
73
|
+
Path(title="Dataset Id"),
|
|
74
|
+
Depends(get_and_validate_dataset_id),
|
|
75
|
+
],
|
|
76
|
+
paginated: Annotated[Paginated, Query()],
|
|
77
|
+
) -> list[TagTable]:
|
|
78
|
+
"""Retrieve a list of tags from the database."""
|
|
79
|
+
return tag_resolver.get_all_by_dataset_id(
|
|
80
|
+
session=session,
|
|
81
|
+
dataset_id=dataset.dataset_id,
|
|
82
|
+
offset=paginated.offset,
|
|
83
|
+
limit=paginated.limit,
|
|
84
|
+
)
|
|
85
|
+
|
|
86
|
+
|
|
87
|
+
@tag_router.get("/datasets/{dataset_id}/tags/{tag_id}")
|
|
88
|
+
def read_tag(
|
|
89
|
+
session: SessionDep,
|
|
90
|
+
dataset: Annotated[
|
|
91
|
+
DatasetTable,
|
|
92
|
+
Path(title="Dataset Id"),
|
|
93
|
+
Depends(get_and_validate_dataset_id),
|
|
94
|
+
],
|
|
95
|
+
tag_id: Annotated[UUID, Path(title="Tag Id")],
|
|
96
|
+
) -> TagTable:
|
|
97
|
+
"""Retrieve a single tag from the database."""
|
|
98
|
+
tag = tag_resolver.get_by_id(session=session, tag_id=tag_id)
|
|
99
|
+
if not tag:
|
|
100
|
+
raise HTTPException(
|
|
101
|
+
status_code=HTTP_STATUS_NOT_FOUND,
|
|
102
|
+
detail=f"""
|
|
103
|
+
Tag with id {tag_id} for dataset {dataset.dataset_id} not found.
|
|
104
|
+
""",
|
|
105
|
+
)
|
|
106
|
+
return tag
|
|
107
|
+
|
|
108
|
+
|
|
109
|
+
@tag_router.put("/datasets/{dataset_id}/tags/{tag_id}")
|
|
110
|
+
def update_tag(
|
|
111
|
+
session: SessionDep,
|
|
112
|
+
dataset: Annotated[
|
|
113
|
+
DatasetTable,
|
|
114
|
+
Path(title="Dataset Id"),
|
|
115
|
+
Depends(get_and_validate_dataset_id),
|
|
116
|
+
],
|
|
117
|
+
tag_id: Annotated[UUID, Path(title="Tag Id")],
|
|
118
|
+
body: TagUpdateBody,
|
|
119
|
+
) -> TagTable:
|
|
120
|
+
"""Update an existing tag in the database."""
|
|
121
|
+
try:
|
|
122
|
+
tag = tag_resolver.update(
|
|
123
|
+
session=session,
|
|
124
|
+
tag_id=tag_id,
|
|
125
|
+
tag_data=TagUpdate(
|
|
126
|
+
**body.model_dump(exclude_unset=True),
|
|
127
|
+
),
|
|
128
|
+
)
|
|
129
|
+
if not tag:
|
|
130
|
+
raise HTTPException(
|
|
131
|
+
status_code=HTTP_STATUS_NOT_FOUND,
|
|
132
|
+
detail=f"Tag with id {tag_id} not found.",
|
|
133
|
+
)
|
|
134
|
+
except IntegrityError as e:
|
|
135
|
+
raise HTTPException(
|
|
136
|
+
status_code=HTTP_STATUS_CONFLICT,
|
|
137
|
+
detail=f"""
|
|
138
|
+
Cannot update tag. Tag with name {body.name}
|
|
139
|
+
already exists in the dataset {dataset.dataset_id}.
|
|
140
|
+
""",
|
|
141
|
+
) from e
|
|
142
|
+
return tag
|
|
143
|
+
|
|
144
|
+
|
|
145
|
+
@tag_router.delete("/datasets/{dataset_id}/tags/{tag_id}")
|
|
146
|
+
def delete_tag(
|
|
147
|
+
session: SessionDep,
|
|
148
|
+
tag_id: Annotated[UUID, Path(title="Tag Id")],
|
|
149
|
+
) -> dict[str, str]:
|
|
150
|
+
"""Delete a tag from the database."""
|
|
151
|
+
if not tag_resolver.delete(session=session, tag_id=tag_id):
|
|
152
|
+
raise HTTPException(status_code=HTTP_STATUS_NOT_FOUND, detail="tag not found")
|
|
153
|
+
return {"status": "deleted"}
|
|
154
|
+
|
|
155
|
+
|
|
156
|
+
class SampleIdsBody(BaseModel):
|
|
157
|
+
"""body parameters for adding or removing thing_ids."""
|
|
158
|
+
|
|
159
|
+
sample_ids: list[UUID] | None = Field(None, description="sample ids to add/remove")
|
|
160
|
+
|
|
161
|
+
|
|
162
|
+
@tag_router.post(
|
|
163
|
+
"/datasets/{dataset_id}/tags/{tag_id}/add/samples",
|
|
164
|
+
status_code=HTTP_STATUS_CREATED,
|
|
165
|
+
)
|
|
166
|
+
def add_sample_ids_to_tag_id(
|
|
167
|
+
session: SessionDep,
|
|
168
|
+
tag_id: UUID,
|
|
169
|
+
body: SampleIdsBody,
|
|
170
|
+
) -> bool:
|
|
171
|
+
"""Add sample_ids to a tag_id."""
|
|
172
|
+
tag = tag_resolver.get_by_id(session=session, tag_id=tag_id)
|
|
173
|
+
if not tag:
|
|
174
|
+
raise HTTPException(
|
|
175
|
+
status_code=HTTP_STATUS_NOT_FOUND,
|
|
176
|
+
detail=f"Tag {tag_id} not found, can't add sample_ids.",
|
|
177
|
+
)
|
|
178
|
+
|
|
179
|
+
sample_ids = body.sample_ids if body.sample_ids else []
|
|
180
|
+
tag_resolver.add_sample_ids_to_tag_id(session=session, tag_id=tag_id, sample_ids=sample_ids)
|
|
181
|
+
return True
|
|
182
|
+
|
|
183
|
+
|
|
184
|
+
@tag_router.delete(
|
|
185
|
+
"/datasets/{dataset_id}/tags/{tag_id}/remove/samples",
|
|
186
|
+
)
|
|
187
|
+
def remove_thing_ids_to_tag_id(
|
|
188
|
+
session: SessionDep,
|
|
189
|
+
tag_id: UUID,
|
|
190
|
+
body: SampleIdsBody,
|
|
191
|
+
) -> bool:
|
|
192
|
+
"""Add thing_ids to a tag_id."""
|
|
193
|
+
tag = tag_resolver.get_by_id(session=session, tag_id=tag_id)
|
|
194
|
+
if not tag:
|
|
195
|
+
raise HTTPException(
|
|
196
|
+
status_code=HTTP_STATUS_NOT_FOUND,
|
|
197
|
+
detail=f"Tag {tag_id} not found, can't remove samples.",
|
|
198
|
+
)
|
|
199
|
+
|
|
200
|
+
sample_ids = body.sample_ids if body.sample_ids else []
|
|
201
|
+
tag_resolver.remove_sample_ids_from_tag_id(
|
|
202
|
+
session=session, tag_id=tag_id, sample_ids=sample_ids
|
|
203
|
+
)
|
|
204
|
+
return True
|
|
205
|
+
|
|
206
|
+
|
|
207
|
+
class AnnotationIdsBody(BaseModel):
|
|
208
|
+
"""body parameters for adding or removing annotation_ids."""
|
|
209
|
+
|
|
210
|
+
annotation_ids: list[UUID] | None = Field(None, description="annotation ids to add/remove")
|
|
211
|
+
|
|
212
|
+
|
|
213
|
+
@tag_router.post(
|
|
214
|
+
"/datasets/{dataset_id}/tags/{tag_id}/add/annotations",
|
|
215
|
+
status_code=HTTP_STATUS_CREATED,
|
|
216
|
+
)
|
|
217
|
+
def add_annotation_ids_to_tag_id(
|
|
218
|
+
session: SessionDep,
|
|
219
|
+
tag_id: UUID,
|
|
220
|
+
body: AnnotationIdsBody,
|
|
221
|
+
) -> bool:
|
|
222
|
+
"""Add thing_ids to a tag_id."""
|
|
223
|
+
tag = tag_resolver.get_by_id(session=session, tag_id=tag_id)
|
|
224
|
+
if not tag:
|
|
225
|
+
raise HTTPException(
|
|
226
|
+
status_code=HTTP_STATUS_NOT_FOUND,
|
|
227
|
+
detail=f"Tag {tag_id} not found, can't add annotations.",
|
|
228
|
+
)
|
|
229
|
+
|
|
230
|
+
annotation_ids = body.annotation_ids if body.annotation_ids else []
|
|
231
|
+
tag_resolver.add_annotation_ids_to_tag_id(
|
|
232
|
+
session=session, tag_id=tag_id, annotation_ids=annotation_ids
|
|
233
|
+
)
|
|
234
|
+
return True
|
|
235
|
+
|
|
236
|
+
|
|
237
|
+
@tag_router.delete(
|
|
238
|
+
"/datasets/{dataset_id}/tags/{tag_id}/remove/annotations",
|
|
239
|
+
)
|
|
240
|
+
def remove_annotation_ids_to_tag_id(
|
|
241
|
+
session: SessionDep,
|
|
242
|
+
tag_id: UUID,
|
|
243
|
+
body: AnnotationIdsBody,
|
|
244
|
+
) -> bool:
|
|
245
|
+
"""Add thing_ids to a tag_id."""
|
|
246
|
+
tag = tag_resolver.get_by_id(session=session, tag_id=tag_id)
|
|
247
|
+
if not tag:
|
|
248
|
+
raise HTTPException(
|
|
249
|
+
status_code=HTTP_STATUS_NOT_FOUND,
|
|
250
|
+
detail=f"Tag {tag_id} not found, can't remove annotations.",
|
|
251
|
+
)
|
|
252
|
+
|
|
253
|
+
annotation_ids = body.annotation_ids if body.annotation_ids else []
|
|
254
|
+
tag_resolver.remove_annotation_ids_from_tag_id(
|
|
255
|
+
session=session, tag_id=tag_id, annotation_ids=annotation_ids
|
|
256
|
+
)
|
|
257
|
+
return True
|
|
@@ -0,0 +1,96 @@
|
|
|
1
|
+
"""This module contains the FastAPI app configuration."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import logging
|
|
6
|
+
|
|
7
|
+
from fastapi import FastAPI, Request
|
|
8
|
+
from fastapi.exceptions import ResponseValidationError
|
|
9
|
+
from fastapi.responses import JSONResponse
|
|
10
|
+
from sqlalchemy.exc import DataError, IntegrityError, OperationalError
|
|
11
|
+
|
|
12
|
+
from lightly_studio.api.routes.api.status import (
|
|
13
|
+
HTTP_STATUS_BAD_REQUEST,
|
|
14
|
+
HTTP_STATUS_CONFLICT,
|
|
15
|
+
HTTP_STATUS_INTERNAL_SERVER_ERROR,
|
|
16
|
+
)
|
|
17
|
+
|
|
18
|
+
# Set up logger for error handling
|
|
19
|
+
logger = logging.getLogger("lightly_studio.api.exceptions")
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
def _log_error_details(
|
|
23
|
+
exc: Exception,
|
|
24
|
+
status_code: int,
|
|
25
|
+
) -> None:
|
|
26
|
+
"""Log detailed error information with request context."""
|
|
27
|
+
# Log the error with different levels based on status code
|
|
28
|
+
logger.error(f"Server Error {status_code}: {exc}")
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
def register_exception_handlers(app: FastAPI) -> None:
|
|
32
|
+
"""Register exception handlers for the FastAPI app."""
|
|
33
|
+
|
|
34
|
+
@app.exception_handler(IntegrityError)
|
|
35
|
+
async def _integrity_error_handler(_request: Request, _exc: IntegrityError) -> JSONResponse:
|
|
36
|
+
"""Handle database integrity errors."""
|
|
37
|
+
_log_error_details(
|
|
38
|
+
exc=_exc,
|
|
39
|
+
status_code=HTTP_STATUS_CONFLICT,
|
|
40
|
+
)
|
|
41
|
+
|
|
42
|
+
return JSONResponse(
|
|
43
|
+
status_code=HTTP_STATUS_CONFLICT,
|
|
44
|
+
content={"error": _exc.statement or "Database constraint violated."},
|
|
45
|
+
)
|
|
46
|
+
|
|
47
|
+
@app.exception_handler(OperationalError)
|
|
48
|
+
async def _operational_error_handler(_request: Request, _exc: OperationalError) -> JSONResponse:
|
|
49
|
+
"""Handle database operational errors."""
|
|
50
|
+
_log_error_details(
|
|
51
|
+
exc=_exc,
|
|
52
|
+
status_code=HTTP_STATUS_INTERNAL_SERVER_ERROR,
|
|
53
|
+
)
|
|
54
|
+
return JSONResponse(
|
|
55
|
+
status_code=HTTP_STATUS_INTERNAL_SERVER_ERROR,
|
|
56
|
+
content={"error": _exc.statement or "Database operation failed."},
|
|
57
|
+
)
|
|
58
|
+
|
|
59
|
+
@app.exception_handler(DataError)
|
|
60
|
+
async def _data_error_handler(_request: Request, _exc: DataError) -> JSONResponse:
|
|
61
|
+
"""Handle database data errors."""
|
|
62
|
+
_log_error_details(
|
|
63
|
+
exc=_exc,
|
|
64
|
+
status_code=HTTP_STATUS_BAD_REQUEST,
|
|
65
|
+
)
|
|
66
|
+
return JSONResponse(
|
|
67
|
+
status_code=HTTP_STATUS_BAD_REQUEST,
|
|
68
|
+
content={"error": _exc.statement or "Invalid response."},
|
|
69
|
+
)
|
|
70
|
+
|
|
71
|
+
@app.exception_handler(ResponseValidationError)
|
|
72
|
+
async def _data_validation_error_handler(
|
|
73
|
+
_request: Request, _exc: ResponseValidationError
|
|
74
|
+
) -> JSONResponse:
|
|
75
|
+
"""Handle database data errors."""
|
|
76
|
+
error_details = _exc.errors()
|
|
77
|
+
if error_details:
|
|
78
|
+
detail = error_details[0].get("msg", "Invalid data provided.")
|
|
79
|
+
else:
|
|
80
|
+
detail = "Invalid data provided."
|
|
81
|
+
|
|
82
|
+
_log_error_details(
|
|
83
|
+
exc=_exc,
|
|
84
|
+
status_code=HTTP_STATUS_BAD_REQUEST,
|
|
85
|
+
)
|
|
86
|
+
|
|
87
|
+
return JSONResponse(status_code=HTTP_STATUS_BAD_REQUEST, content={"error": detail})
|
|
88
|
+
|
|
89
|
+
@app.exception_handler(ValueError)
|
|
90
|
+
async def _value_error_handler(_request: Request, _exc: ValueError) -> JSONResponse:
|
|
91
|
+
"""Handle value errors."""
|
|
92
|
+
_log_error_details(
|
|
93
|
+
exc=_exc,
|
|
94
|
+
status_code=HTTP_STATUS_INTERNAL_SERVER_ERROR,
|
|
95
|
+
)
|
|
96
|
+
return JSONResponse(status_code=HTTP_STATUS_BAD_REQUEST, content={"error": str(_exc)})
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
"""This module contains the API routes for active features."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from fastapi import APIRouter
|
|
6
|
+
|
|
7
|
+
from lightly_studio.api.features import lightly_studio_active_features
|
|
8
|
+
|
|
9
|
+
__all__ = ["features_router", "lightly_studio_active_features"]
|
|
10
|
+
|
|
11
|
+
features_router = APIRouter()
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
@features_router.get("/features")
|
|
15
|
+
def get_features() -> list[str]:
|
|
16
|
+
"""Get the list of active features in the LightlyStudio app."""
|
|
17
|
+
return lightly_studio_active_features
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
"""This module contains the API routes for managing datasets."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from typing import List
|
|
6
|
+
from uuid import UUID
|
|
7
|
+
|
|
8
|
+
from fastapi import APIRouter, Depends, Path
|
|
9
|
+
from sqlmodel import Session
|
|
10
|
+
from typing_extensions import Annotated
|
|
11
|
+
|
|
12
|
+
from lightly_studio.api.db import get_session
|
|
13
|
+
from lightly_studio.models.metadata import MetadataInfoView
|
|
14
|
+
from lightly_studio.resolvers.metadata_resolver.sample.get_metadata_info import (
|
|
15
|
+
get_all_metadata_keys_and_schema,
|
|
16
|
+
)
|
|
17
|
+
|
|
18
|
+
metadata_router = APIRouter(prefix="/datasets/{dataset_id}", tags=["metadata"])
|
|
19
|
+
SessionDep = Annotated[Session, Depends(get_session)]
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
@metadata_router.get("/metadata/info", response_model=List[MetadataInfoView])
|
|
23
|
+
def get_metadata_info(
|
|
24
|
+
session: SessionDep,
|
|
25
|
+
dataset_id: Annotated[UUID, Path(title="Dataset Id")],
|
|
26
|
+
) -> list[MetadataInfoView]:
|
|
27
|
+
"""Get all metadata keys and their schema for a dataset.
|
|
28
|
+
|
|
29
|
+
Args:
|
|
30
|
+
session: The database session.
|
|
31
|
+
dataset_id: The ID of the dataset.
|
|
32
|
+
|
|
33
|
+
Returns:
|
|
34
|
+
List of metadata info objects with name, type, and optionally min/max values
|
|
35
|
+
for numerical metadata types.
|
|
36
|
+
"""
|
|
37
|
+
return get_all_metadata_keys_and_schema(session=session, dataset_id=dataset_id)
|
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
"""This module contains the API routes for computing detection metrics."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from uuid import UUID
|
|
6
|
+
|
|
7
|
+
from fastapi import APIRouter, Depends
|
|
8
|
+
from pydantic import BaseModel
|
|
9
|
+
from sqlmodel import Session
|
|
10
|
+
from typing_extensions import Annotated
|
|
11
|
+
|
|
12
|
+
from lightly_studio.api.db import get_session
|
|
13
|
+
from lightly_studio.metrics.detection.map import (
|
|
14
|
+
DetectionMetricsMAP,
|
|
15
|
+
calculate_map_metric,
|
|
16
|
+
)
|
|
17
|
+
from lightly_studio.resolvers import (
|
|
18
|
+
annotation_label_resolver,
|
|
19
|
+
annotation_resolver,
|
|
20
|
+
)
|
|
21
|
+
from lightly_studio.resolvers.annotations.annotations_filter import (
|
|
22
|
+
AnnotationsFilter,
|
|
23
|
+
)
|
|
24
|
+
|
|
25
|
+
metrics_router = APIRouter()
|
|
26
|
+
|
|
27
|
+
SessionDep = Annotated[Session, Depends(get_session)]
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
class DetectionMetricsMAPRequest(BaseModel):
|
|
31
|
+
"""Request for computing the MAP detection metric."""
|
|
32
|
+
|
|
33
|
+
dataset_id: UUID
|
|
34
|
+
ground_truth_task_id: UUID
|
|
35
|
+
prediction_task_id: UUID
|
|
36
|
+
tag_id: UUID | None = None
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
@metrics_router.post("/metrics/compute/detection/map", response_model=DetectionMetricsMAP)
|
|
40
|
+
def compute_detection_map(
|
|
41
|
+
request_body: DetectionMetricsMAPRequest,
|
|
42
|
+
session: SessionDep,
|
|
43
|
+
) -> DetectionMetricsMAP:
|
|
44
|
+
"""Compute the MAP detection metric."""
|
|
45
|
+
ground_truth_annotations = annotation_resolver.get_all(
|
|
46
|
+
session=session,
|
|
47
|
+
filters=AnnotationsFilter(
|
|
48
|
+
dataset_ids=[request_body.dataset_id],
|
|
49
|
+
annotation_task_ids=[request_body.ground_truth_task_id],
|
|
50
|
+
sample_tag_ids=[request_body.tag_id] if request_body.tag_id else None,
|
|
51
|
+
),
|
|
52
|
+
).annotations
|
|
53
|
+
prediction_annotations = annotation_resolver.get_all(
|
|
54
|
+
session=session,
|
|
55
|
+
filters=AnnotationsFilter(
|
|
56
|
+
dataset_ids=[request_body.dataset_id],
|
|
57
|
+
annotation_task_ids=[request_body.prediction_task_id],
|
|
58
|
+
sample_tag_ids=[request_body.tag_id] if request_body.tag_id else None,
|
|
59
|
+
),
|
|
60
|
+
).annotations
|
|
61
|
+
|
|
62
|
+
metrics_result = calculate_map_metric(
|
|
63
|
+
pred_annotations=prediction_annotations,
|
|
64
|
+
gt_annotations=ground_truth_annotations,
|
|
65
|
+
)
|
|
66
|
+
|
|
67
|
+
# Rename per-class metrics to use label names
|
|
68
|
+
raw_map_pc = metrics_result.map_per_class
|
|
69
|
+
if raw_map_pc:
|
|
70
|
+
id2name = annotation_label_resolver.names_by_ids(
|
|
71
|
+
session=session, ids=[UUID(k) for k in raw_map_pc]
|
|
72
|
+
)
|
|
73
|
+
metrics_result.map_per_class = {id2name.get(k, k): v for k, v in raw_map_pc.items()}
|
|
74
|
+
raw_mar100_pc = metrics_result.mar_100_per_class
|
|
75
|
+
if raw_mar100_pc:
|
|
76
|
+
id2name = annotation_label_resolver.names_by_ids(
|
|
77
|
+
session=session, ids=[UUID(k) for k in raw_mar100_pc]
|
|
78
|
+
)
|
|
79
|
+
metrics_result.mar_100_per_class = {id2name.get(k, k): v for k, v in raw_mar100_pc.items()}
|
|
80
|
+
return metrics_result
|
|
@@ -0,0 +1,196 @@
|
|
|
1
|
+
"""This module contains the API routes for managing samples."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from uuid import UUID
|
|
6
|
+
|
|
7
|
+
from fastapi import APIRouter, Depends, HTTPException, Path, Query
|
|
8
|
+
from pydantic import BaseModel, Field
|
|
9
|
+
from sqlmodel import Session
|
|
10
|
+
from typing_extensions import Annotated
|
|
11
|
+
|
|
12
|
+
from lightly_studio.api.db import get_session
|
|
13
|
+
from lightly_studio.api.routes.api.dataset import get_and_validate_dataset_id
|
|
14
|
+
from lightly_studio.api.routes.api.status import (
|
|
15
|
+
HTTP_STATUS_CREATED,
|
|
16
|
+
HTTP_STATUS_NOT_FOUND,
|
|
17
|
+
)
|
|
18
|
+
from lightly_studio.api.routes.api.validators import Paginated
|
|
19
|
+
from lightly_studio.models.dataset import DatasetTable
|
|
20
|
+
from lightly_studio.models.sample import (
|
|
21
|
+
SampleCreate,
|
|
22
|
+
SampleTable,
|
|
23
|
+
SampleView,
|
|
24
|
+
SampleViewsWithCount,
|
|
25
|
+
)
|
|
26
|
+
from lightly_studio.resolvers import (
|
|
27
|
+
sample_resolver,
|
|
28
|
+
tag_resolver,
|
|
29
|
+
)
|
|
30
|
+
from lightly_studio.resolvers.samples_filter import (
|
|
31
|
+
SampleFilter,
|
|
32
|
+
)
|
|
33
|
+
|
|
34
|
+
samples_router = APIRouter(prefix="/datasets/{dataset_id}", tags=["samples"])
|
|
35
|
+
SessionDep = Annotated[Session, Depends(get_session)]
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
@samples_router.post("/samples", response_model=SampleView)
|
|
39
|
+
def create_sample(
|
|
40
|
+
session: SessionDep,
|
|
41
|
+
input_sample: SampleCreate,
|
|
42
|
+
) -> SampleTable:
|
|
43
|
+
"""Create a new sample in the database."""
|
|
44
|
+
return sample_resolver.create(session=session, sample=input_sample)
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
class ReadSamplesRequest(BaseModel):
|
|
48
|
+
"""Request body for reading samples with text embedding."""
|
|
49
|
+
|
|
50
|
+
filters: SampleFilter | None = Field(None, description="Filter parameters for samples")
|
|
51
|
+
text_embedding: list[float] | None = Field(None, description="Text embedding to search for")
|
|
52
|
+
sample_ids: list[UUID] | None = Field(None, description="The list of requested sample IDs")
|
|
53
|
+
pagination: Paginated | None = Field(
|
|
54
|
+
None, description="Pagination parameters for offset and limit"
|
|
55
|
+
)
|
|
56
|
+
|
|
57
|
+
|
|
58
|
+
@samples_router.post("/samples/list", response_model=SampleViewsWithCount)
|
|
59
|
+
def read_samples(
|
|
60
|
+
session: SessionDep,
|
|
61
|
+
dataset_id: Annotated[UUID, Path(title="Dataset Id")],
|
|
62
|
+
body: ReadSamplesRequest,
|
|
63
|
+
) -> SampleViewsWithCount:
|
|
64
|
+
"""Retrieve a list of samples from the database with optional filtering.
|
|
65
|
+
|
|
66
|
+
Args:
|
|
67
|
+
session: The database session.
|
|
68
|
+
dataset_id: The ID of the dataset to filter samples by.
|
|
69
|
+
body: Optional request body containing text embedding.
|
|
70
|
+
|
|
71
|
+
Returns:
|
|
72
|
+
A list of filtered samples.
|
|
73
|
+
"""
|
|
74
|
+
result = sample_resolver.get_all_by_dataset_id(
|
|
75
|
+
session=session,
|
|
76
|
+
dataset_id=dataset_id,
|
|
77
|
+
# TODO(Michal, 06/2025): Pass the Paginated object directly.
|
|
78
|
+
offset=body.pagination.offset if body.pagination else 0,
|
|
79
|
+
limit=body.pagination.limit if body.pagination else 10,
|
|
80
|
+
filters=body.filters,
|
|
81
|
+
text_embedding=body.text_embedding,
|
|
82
|
+
sample_ids=body.sample_ids,
|
|
83
|
+
)
|
|
84
|
+
|
|
85
|
+
return SampleViewsWithCount(
|
|
86
|
+
data=result.samples,
|
|
87
|
+
total_count=result.total_count,
|
|
88
|
+
)
|
|
89
|
+
|
|
90
|
+
|
|
91
|
+
@samples_router.get("/samples/dimensions")
|
|
92
|
+
def get_sample_dimensions(
|
|
93
|
+
session: SessionDep,
|
|
94
|
+
dataset: Annotated[
|
|
95
|
+
DatasetTable,
|
|
96
|
+
Path(title="Dataset Id"),
|
|
97
|
+
Depends(get_and_validate_dataset_id),
|
|
98
|
+
],
|
|
99
|
+
annotation_label_ids: Annotated[list[UUID] | None, Query()] = None,
|
|
100
|
+
) -> dict[str, int]:
|
|
101
|
+
"""Get min and max dimensions of samples in a dataset."""
|
|
102
|
+
return sample_resolver.get_dimension_bounds(
|
|
103
|
+
session=session,
|
|
104
|
+
dataset_id=dataset.dataset_id,
|
|
105
|
+
annotation_label_ids=annotation_label_ids,
|
|
106
|
+
)
|
|
107
|
+
|
|
108
|
+
|
|
109
|
+
@samples_router.get("/samples/{sample_id}", response_model=SampleView)
|
|
110
|
+
def read_sample(
|
|
111
|
+
session: SessionDep,
|
|
112
|
+
dataset_id: Annotated[UUID, Path(title="Dataset Id", description="The ID of the dataset")],
|
|
113
|
+
sample_id: Annotated[UUID, Path(title="Sample Id")],
|
|
114
|
+
) -> SampleTable:
|
|
115
|
+
"""Retrieve a single sample from the database."""
|
|
116
|
+
sample = sample_resolver.get_by_id(session=session, dataset_id=dataset_id, sample_id=sample_id)
|
|
117
|
+
if not sample:
|
|
118
|
+
raise HTTPException(status_code=HTTP_STATUS_NOT_FOUND, detail="Sample not found")
|
|
119
|
+
return sample
|
|
120
|
+
|
|
121
|
+
|
|
122
|
+
@samples_router.put("/samples/{sample_id}")
|
|
123
|
+
def update_sample(
|
|
124
|
+
session: SessionDep,
|
|
125
|
+
sample_id: Annotated[UUID, Path(title="Sample Id")],
|
|
126
|
+
sample_input: SampleCreate,
|
|
127
|
+
) -> SampleTable:
|
|
128
|
+
"""Update an existing sample in the database."""
|
|
129
|
+
sample = sample_resolver.update(session=session, sample_id=sample_id, sample_data=sample_input)
|
|
130
|
+
if not sample:
|
|
131
|
+
raise HTTPException(status_code=HTTP_STATUS_NOT_FOUND, detail="Sample not found")
|
|
132
|
+
return sample
|
|
133
|
+
|
|
134
|
+
|
|
135
|
+
@samples_router.delete("/samples/{sample_id}")
|
|
136
|
+
def delete_sample(
|
|
137
|
+
session: SessionDep,
|
|
138
|
+
dataset_id: Annotated[UUID, Path(title="Dataset Id", description="The ID of the dataset")],
|
|
139
|
+
sample_id: Annotated[UUID, Path(title="Sample Id")],
|
|
140
|
+
) -> dict[str, str]:
|
|
141
|
+
"""Delete a sample from the database."""
|
|
142
|
+
if not sample_resolver.delete(session=session, dataset_id=dataset_id, sample_id=sample_id):
|
|
143
|
+
raise HTTPException(status_code=HTTP_STATUS_NOT_FOUND, detail="Sample not found")
|
|
144
|
+
return {"status": "deleted"}
|
|
145
|
+
|
|
146
|
+
|
|
147
|
+
@samples_router.post(
|
|
148
|
+
"/samples/{sample_id}/tag/{tag_id}",
|
|
149
|
+
status_code=HTTP_STATUS_CREATED,
|
|
150
|
+
)
|
|
151
|
+
def add_tag_to_sample(
|
|
152
|
+
session: SessionDep,
|
|
153
|
+
sample_id: UUID,
|
|
154
|
+
dataset_id: Annotated[UUID, Path(title="Dataset Id", description="The ID of the dataset")],
|
|
155
|
+
tag_id: UUID,
|
|
156
|
+
) -> bool:
|
|
157
|
+
"""Add sample to a tag."""
|
|
158
|
+
sample = sample_resolver.get_by_id(session=session, dataset_id=dataset_id, sample_id=sample_id)
|
|
159
|
+
if not sample:
|
|
160
|
+
raise HTTPException(
|
|
161
|
+
status_code=HTTP_STATUS_NOT_FOUND,
|
|
162
|
+
detail=f"Sample {sample_id} not found",
|
|
163
|
+
)
|
|
164
|
+
|
|
165
|
+
if not tag_resolver.add_tag_to_sample(session=session, tag_id=tag_id, sample=sample):
|
|
166
|
+
raise HTTPException(status_code=HTTP_STATUS_NOT_FOUND, detail=f"Tag {tag_id} not found")
|
|
167
|
+
|
|
168
|
+
return True
|
|
169
|
+
|
|
170
|
+
|
|
171
|
+
@samples_router.delete("/samples/{sample_id}/tag/{tag_id}")
|
|
172
|
+
def remove_tag_from_sample(
|
|
173
|
+
session: SessionDep,
|
|
174
|
+
tag_id: UUID,
|
|
175
|
+
dataset_id: Annotated[UUID, Path(title="Dataset Id", description="The ID of the dataset")],
|
|
176
|
+
sample_id: UUID,
|
|
177
|
+
) -> bool:
|
|
178
|
+
"""Remove sample from a tag."""
|
|
179
|
+
sample = sample_resolver.get_by_id(session=session, dataset_id=dataset_id, sample_id=sample_id)
|
|
180
|
+
if not sample:
|
|
181
|
+
raise HTTPException(
|
|
182
|
+
status_code=HTTP_STATUS_NOT_FOUND,
|
|
183
|
+
detail=f"Sample {sample_id} not found",
|
|
184
|
+
)
|
|
185
|
+
|
|
186
|
+
if not tag_resolver.remove_tag_from_sample(session=session, tag_id=tag_id, sample=sample):
|
|
187
|
+
raise HTTPException(status_code=HTTP_STATUS_NOT_FOUND, detail=f"Tag {tag_id} not found")
|
|
188
|
+
|
|
189
|
+
return True
|
|
190
|
+
|
|
191
|
+
|
|
192
|
+
class SampleAdjacentsParams(BaseModel):
|
|
193
|
+
"""Parameters for getting adjacent samples."""
|
|
194
|
+
|
|
195
|
+
filters: SampleFilter | None = None
|
|
196
|
+
text_embedding: list[float] | None = None
|