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,278 @@
|
|
|
1
|
+
"""Handler for database operations related to datasets."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from datetime import datetime, timezone
|
|
6
|
+
from uuid import UUID
|
|
7
|
+
|
|
8
|
+
from pydantic import BaseModel, Field, model_validator
|
|
9
|
+
from sqlmodel import Session, and_, col, func, or_, select
|
|
10
|
+
from sqlmodel.sql.expression import SelectOfScalar
|
|
11
|
+
|
|
12
|
+
from lightly_studio.models.annotation.annotation_base import AnnotationBaseTable
|
|
13
|
+
from lightly_studio.models.dataset import DatasetCreate, DatasetTable
|
|
14
|
+
from lightly_studio.models.sample import SampleTable
|
|
15
|
+
from lightly_studio.models.tag import TagTable
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
class ExportFilter(BaseModel):
|
|
19
|
+
"""Export Filter to be used for including or excluding."""
|
|
20
|
+
|
|
21
|
+
tag_ids: list[UUID] | None = Field(default=None, min_length=1, description="List of tag UUIDs")
|
|
22
|
+
sample_ids: list[UUID] | None = Field(
|
|
23
|
+
default=None, min_length=1, description="List of sample UUIDs"
|
|
24
|
+
)
|
|
25
|
+
annotation_ids: list[UUID] | None = Field(
|
|
26
|
+
default=None, min_length=1, description="List of annotation UUIDs"
|
|
27
|
+
)
|
|
28
|
+
|
|
29
|
+
@model_validator(mode="after")
|
|
30
|
+
def check_exactly_one(self) -> ExportFilter: # noqa: N804
|
|
31
|
+
"""Ensure that exactly one of the fields is set."""
|
|
32
|
+
count = (
|
|
33
|
+
(self.tag_ids is not None)
|
|
34
|
+
+ (self.sample_ids is not None)
|
|
35
|
+
+ (self.annotation_ids is not None)
|
|
36
|
+
)
|
|
37
|
+
if count != 1:
|
|
38
|
+
raise ValueError("Either tag_ids, sample_ids, or annotation_ids must be set.")
|
|
39
|
+
return self
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
def create(session: Session, dataset: DatasetCreate) -> DatasetTable:
|
|
43
|
+
"""Create a new dataset in the database."""
|
|
44
|
+
db_dataset = DatasetTable.model_validate(dataset)
|
|
45
|
+
session.add(db_dataset)
|
|
46
|
+
session.commit()
|
|
47
|
+
session.refresh(db_dataset)
|
|
48
|
+
return db_dataset
|
|
49
|
+
|
|
50
|
+
|
|
51
|
+
# TODO(Michal, 06/2025): Use Paginated struct instead of offset and limit
|
|
52
|
+
def get_all(session: Session, offset: int = 0, limit: int = 100) -> list[DatasetTable]:
|
|
53
|
+
"""Retrieve all datasets with pagination."""
|
|
54
|
+
datasets = session.exec(
|
|
55
|
+
select(DatasetTable)
|
|
56
|
+
.order_by(col(DatasetTable.created_at).asc())
|
|
57
|
+
.offset(offset)
|
|
58
|
+
.limit(limit)
|
|
59
|
+
).all()
|
|
60
|
+
return list(datasets) if datasets else []
|
|
61
|
+
|
|
62
|
+
|
|
63
|
+
def get_by_id(session: Session, dataset_id: UUID) -> DatasetTable | None:
|
|
64
|
+
"""Retrieve a single dataset by ID."""
|
|
65
|
+
return session.exec(
|
|
66
|
+
select(DatasetTable).where(DatasetTable.dataset_id == dataset_id)
|
|
67
|
+
).one_or_none()
|
|
68
|
+
|
|
69
|
+
|
|
70
|
+
def update(session: Session, dataset_id: UUID, dataset_data: DatasetCreate) -> DatasetTable:
|
|
71
|
+
"""Update an existing dataset."""
|
|
72
|
+
dataset = get_by_id(session=session, dataset_id=dataset_id)
|
|
73
|
+
if not dataset:
|
|
74
|
+
raise ValueError(f"Dataset ID was not found '{dataset_id}'.")
|
|
75
|
+
|
|
76
|
+
dataset.name = dataset_data.name
|
|
77
|
+
dataset.directory = dataset_data.directory
|
|
78
|
+
dataset.updated_at = datetime.now(timezone.utc)
|
|
79
|
+
|
|
80
|
+
session.commit()
|
|
81
|
+
session.refresh(dataset)
|
|
82
|
+
return dataset
|
|
83
|
+
|
|
84
|
+
|
|
85
|
+
def delete(session: Session, dataset_id: UUID) -> bool:
|
|
86
|
+
"""Delete a dataset."""
|
|
87
|
+
dataset = get_by_id(session=session, dataset_id=dataset_id)
|
|
88
|
+
if not dataset:
|
|
89
|
+
return False
|
|
90
|
+
|
|
91
|
+
session.delete(dataset)
|
|
92
|
+
session.commit()
|
|
93
|
+
return True
|
|
94
|
+
|
|
95
|
+
|
|
96
|
+
def _build_export_query( # noqa: C901
|
|
97
|
+
dataset_id: UUID,
|
|
98
|
+
include: ExportFilter | None = None,
|
|
99
|
+
exclude: ExportFilter | None = None,
|
|
100
|
+
) -> SelectOfScalar[SampleTable]:
|
|
101
|
+
"""Build the export query based on filters.
|
|
102
|
+
|
|
103
|
+
Args:
|
|
104
|
+
session: SQLAlchemy session.
|
|
105
|
+
dataset_id: UUID of the dataset.
|
|
106
|
+
include: Filter to include samples.
|
|
107
|
+
exclude: Filter to exclude samples.
|
|
108
|
+
|
|
109
|
+
Returns:
|
|
110
|
+
SQLModel select query
|
|
111
|
+
"""
|
|
112
|
+
if not include and not exclude:
|
|
113
|
+
raise ValueError("Include or exclude filter is required.")
|
|
114
|
+
if include and exclude:
|
|
115
|
+
raise ValueError("Cannot include and exclude at the same time.")
|
|
116
|
+
|
|
117
|
+
# include tags or sample_ids or annotation_ids from result
|
|
118
|
+
if include:
|
|
119
|
+
if include.tag_ids:
|
|
120
|
+
return (
|
|
121
|
+
select(SampleTable)
|
|
122
|
+
.where(SampleTable.dataset_id == dataset_id)
|
|
123
|
+
.where(
|
|
124
|
+
or_(
|
|
125
|
+
# Samples with matching sample tags
|
|
126
|
+
col(SampleTable.tags).any(
|
|
127
|
+
and_(
|
|
128
|
+
TagTable.kind == "sample",
|
|
129
|
+
col(TagTable.tag_id).in_(include.tag_ids),
|
|
130
|
+
)
|
|
131
|
+
),
|
|
132
|
+
# Samples with matching annotation tags
|
|
133
|
+
col(SampleTable.annotations).any(
|
|
134
|
+
col(AnnotationBaseTable.tags).any(
|
|
135
|
+
and_(
|
|
136
|
+
TagTable.kind == "annotation",
|
|
137
|
+
col(TagTable.tag_id).in_(include.tag_ids),
|
|
138
|
+
)
|
|
139
|
+
)
|
|
140
|
+
),
|
|
141
|
+
)
|
|
142
|
+
)
|
|
143
|
+
.order_by(col(SampleTable.created_at).asc())
|
|
144
|
+
.distinct()
|
|
145
|
+
)
|
|
146
|
+
|
|
147
|
+
# get samples by specific sample_ids
|
|
148
|
+
if include.sample_ids:
|
|
149
|
+
return (
|
|
150
|
+
select(SampleTable)
|
|
151
|
+
.where(SampleTable.dataset_id == dataset_id)
|
|
152
|
+
.where(col(SampleTable.sample_id).in_(include.sample_ids))
|
|
153
|
+
.order_by(col(SampleTable.created_at).asc())
|
|
154
|
+
.distinct()
|
|
155
|
+
)
|
|
156
|
+
|
|
157
|
+
# get samples by specific annotation_ids
|
|
158
|
+
if include.annotation_ids:
|
|
159
|
+
return (
|
|
160
|
+
select(SampleTable)
|
|
161
|
+
.join(SampleTable.annotations)
|
|
162
|
+
.where(AnnotationBaseTable.dataset_id == dataset_id)
|
|
163
|
+
.where(col(AnnotationBaseTable.annotation_id).in_(include.annotation_ids))
|
|
164
|
+
.order_by(col(SampleTable.created_at).asc())
|
|
165
|
+
.distinct()
|
|
166
|
+
)
|
|
167
|
+
|
|
168
|
+
# exclude tags or sample_ids or annotation_ids from result
|
|
169
|
+
elif exclude:
|
|
170
|
+
if exclude.tag_ids:
|
|
171
|
+
return (
|
|
172
|
+
select(SampleTable)
|
|
173
|
+
.where(SampleTable.dataset_id == dataset_id)
|
|
174
|
+
.where(
|
|
175
|
+
and_(
|
|
176
|
+
~col(SampleTable.tags).any(
|
|
177
|
+
and_(
|
|
178
|
+
TagTable.kind == "sample",
|
|
179
|
+
col(TagTable.tag_id).in_(exclude.tag_ids),
|
|
180
|
+
)
|
|
181
|
+
),
|
|
182
|
+
or_(
|
|
183
|
+
~col(SampleTable.annotations).any(),
|
|
184
|
+
~col(SampleTable.annotations).any(
|
|
185
|
+
col(AnnotationBaseTable.tags).any(
|
|
186
|
+
and_(
|
|
187
|
+
TagTable.kind == "annotation",
|
|
188
|
+
col(TagTable.tag_id).in_(exclude.tag_ids),
|
|
189
|
+
)
|
|
190
|
+
)
|
|
191
|
+
),
|
|
192
|
+
),
|
|
193
|
+
)
|
|
194
|
+
)
|
|
195
|
+
.order_by(col(SampleTable.created_at).asc())
|
|
196
|
+
.distinct()
|
|
197
|
+
)
|
|
198
|
+
if exclude.sample_ids:
|
|
199
|
+
return (
|
|
200
|
+
select(SampleTable)
|
|
201
|
+
.where(SampleTable.dataset_id == dataset_id)
|
|
202
|
+
.where(col(SampleTable.sample_id).notin_(exclude.sample_ids))
|
|
203
|
+
.order_by(col(SampleTable.created_at).asc())
|
|
204
|
+
.distinct()
|
|
205
|
+
)
|
|
206
|
+
if exclude.annotation_ids:
|
|
207
|
+
return (
|
|
208
|
+
select(SampleTable)
|
|
209
|
+
.where(SampleTable.dataset_id == dataset_id)
|
|
210
|
+
.where(
|
|
211
|
+
or_(
|
|
212
|
+
~col(SampleTable.annotations).any(),
|
|
213
|
+
~col(SampleTable.annotations).any(
|
|
214
|
+
col(AnnotationBaseTable.annotation_id).in_(exclude.annotation_ids)
|
|
215
|
+
),
|
|
216
|
+
)
|
|
217
|
+
)
|
|
218
|
+
.order_by(col(SampleTable.created_at).asc())
|
|
219
|
+
.distinct()
|
|
220
|
+
)
|
|
221
|
+
|
|
222
|
+
raise ValueError("Invalid include or export filter combination.")
|
|
223
|
+
|
|
224
|
+
|
|
225
|
+
# TODO: this fn should be moved to a "business logic" layer outside of the
|
|
226
|
+
# resolvers and abstracted in to reusable components.
|
|
227
|
+
# https://linear.app/lightly/issue/LIG-6196/figure-out-architecture-follow-up-changes-in-python-package
|
|
228
|
+
# TODO: this should be abstracted to allow different export formats.
|
|
229
|
+
def export(
|
|
230
|
+
session: Session,
|
|
231
|
+
dataset_id: UUID,
|
|
232
|
+
include: ExportFilter | None = None,
|
|
233
|
+
exclude: ExportFilter | None = None,
|
|
234
|
+
) -> list[str]:
|
|
235
|
+
"""Retrieve samples for exporting from a dataset.
|
|
236
|
+
|
|
237
|
+
Only one of include or exclude should be set and not both.
|
|
238
|
+
Furthermore, the include and exclude filter can only have
|
|
239
|
+
one type (tag_ids, sample_ids or annotations_ids) set.
|
|
240
|
+
|
|
241
|
+
Args:
|
|
242
|
+
session: SQLAlchemy session.
|
|
243
|
+
dataset_id: UUID of the dataset.
|
|
244
|
+
include: Filter to include samples.
|
|
245
|
+
exclude: Filter to exclude samples.
|
|
246
|
+
|
|
247
|
+
Returns:
|
|
248
|
+
List of file paths
|
|
249
|
+
"""
|
|
250
|
+
query = _build_export_query(dataset_id=dataset_id, include=include, exclude=exclude)
|
|
251
|
+
result = session.exec(query).all()
|
|
252
|
+
return [sample.file_path_abs for sample in result]
|
|
253
|
+
|
|
254
|
+
|
|
255
|
+
def get_filtered_samples_count(
|
|
256
|
+
session: Session,
|
|
257
|
+
dataset_id: UUID,
|
|
258
|
+
include: ExportFilter | None = None,
|
|
259
|
+
exclude: ExportFilter | None = None,
|
|
260
|
+
) -> int:
|
|
261
|
+
"""Get statistics about the export query.
|
|
262
|
+
|
|
263
|
+
Only one of include or exclude should be set and not both.
|
|
264
|
+
Furthermore, the include and exclude filter can only have
|
|
265
|
+
one type (tag_ids, sample_ids or annotations_ids) set.
|
|
266
|
+
|
|
267
|
+
Args:
|
|
268
|
+
session: SQLAlchemy session.
|
|
269
|
+
dataset_id: UUID of the dataset.
|
|
270
|
+
include: Filter to include samples.
|
|
271
|
+
exclude: Filter to exclude samples.
|
|
272
|
+
|
|
273
|
+
Returns:
|
|
274
|
+
Count of files to be exported
|
|
275
|
+
"""
|
|
276
|
+
query = _build_export_query(dataset_id=dataset_id, include=include, exclude=exclude)
|
|
277
|
+
count_query = select(func.count()).select_from(query.subquery())
|
|
278
|
+
return session.exec(count_query).one() or 0
|
|
@@ -0,0 +1,100 @@
|
|
|
1
|
+
"""Handler for database operations related to embedding models."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from uuid import UUID
|
|
6
|
+
|
|
7
|
+
from sqlmodel import Session, col, select
|
|
8
|
+
|
|
9
|
+
from lightly_studio.models.embedding_model import (
|
|
10
|
+
EmbeddingModelCreate,
|
|
11
|
+
EmbeddingModelTable,
|
|
12
|
+
)
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
def create(session: Session, embedding_model: EmbeddingModelCreate) -> EmbeddingModelTable:
|
|
16
|
+
"""Create a new EmbeddingModel in the database."""
|
|
17
|
+
db_embedding_model = EmbeddingModelTable.model_validate(embedding_model)
|
|
18
|
+
session.add(db_embedding_model)
|
|
19
|
+
session.commit()
|
|
20
|
+
session.refresh(db_embedding_model)
|
|
21
|
+
return db_embedding_model
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
def get_all_by_dataset_id(session: Session, dataset_id: UUID) -> list[EmbeddingModelTable]:
|
|
25
|
+
"""Retrieve all embedding models."""
|
|
26
|
+
embedding_models = session.exec(
|
|
27
|
+
select(EmbeddingModelTable)
|
|
28
|
+
.where(EmbeddingModelTable.dataset_id == dataset_id)
|
|
29
|
+
.order_by(col(EmbeddingModelTable.created_at).asc())
|
|
30
|
+
).all()
|
|
31
|
+
return list(embedding_models)
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
def get_by_id(session: Session, embedding_model_id: UUID) -> EmbeddingModelTable | None:
|
|
35
|
+
"""Retrieve a single embedding model by ID."""
|
|
36
|
+
return session.exec(
|
|
37
|
+
select(EmbeddingModelTable).where(
|
|
38
|
+
EmbeddingModelTable.embedding_model_id == embedding_model_id
|
|
39
|
+
)
|
|
40
|
+
).one_or_none()
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
def get_by_model_hash(session: Session, embedding_model_hash: str) -> EmbeddingModelTable | None:
|
|
44
|
+
"""Retrieve a single embedding model by hash."""
|
|
45
|
+
return session.exec(
|
|
46
|
+
select(EmbeddingModelTable).where(
|
|
47
|
+
EmbeddingModelTable.embedding_model_hash == embedding_model_hash
|
|
48
|
+
)
|
|
49
|
+
).one_or_none()
|
|
50
|
+
|
|
51
|
+
|
|
52
|
+
def get_by_name(
|
|
53
|
+
session: Session, dataset_id: UUID, embedding_model_name: str | None
|
|
54
|
+
) -> EmbeddingModelTable:
|
|
55
|
+
"""Helper function to resolve the embedding model name to its ID.
|
|
56
|
+
|
|
57
|
+
Args:
|
|
58
|
+
session: The database session.
|
|
59
|
+
dataset_id: The ID of the dataset.
|
|
60
|
+
embedding_model_name: The name of the embedding model.
|
|
61
|
+
If None, expects the dataset to have exactly one embedding model and
|
|
62
|
+
returns it. Otherwise raises a ValueError.
|
|
63
|
+
If set, expects the dataset to have an embedding model with the given name.
|
|
64
|
+
Otherwise raises a ValueError.
|
|
65
|
+
|
|
66
|
+
Returns:
|
|
67
|
+
The embedding model with the given name.
|
|
68
|
+
"""
|
|
69
|
+
embedding_models = get_all_by_dataset_id(
|
|
70
|
+
session=session,
|
|
71
|
+
dataset_id=dataset_id,
|
|
72
|
+
)
|
|
73
|
+
|
|
74
|
+
if embedding_model_name is None:
|
|
75
|
+
if len(embedding_models) != 1:
|
|
76
|
+
raise ValueError(
|
|
77
|
+
f"Expected exactly one embedding model, "
|
|
78
|
+
f"but found {len(embedding_models)} with names "
|
|
79
|
+
f"{[model.name for model in embedding_models]}."
|
|
80
|
+
)
|
|
81
|
+
return embedding_models[0]
|
|
82
|
+
|
|
83
|
+
embedding_model_with_name = next(
|
|
84
|
+
(model for model in embedding_models if model.name == embedding_model_name), None
|
|
85
|
+
)
|
|
86
|
+
if embedding_model_with_name is None:
|
|
87
|
+
raise ValueError(f"Embedding model with name `{embedding_model_name}` not found.")
|
|
88
|
+
|
|
89
|
+
return embedding_model_with_name
|
|
90
|
+
|
|
91
|
+
|
|
92
|
+
def delete(session: Session, embedding_model_id: UUID) -> bool:
|
|
93
|
+
"""Delete an embedding model."""
|
|
94
|
+
embedding_model = get_by_id(session=session, embedding_model_id=embedding_model_id)
|
|
95
|
+
if not embedding_model:
|
|
96
|
+
return False
|
|
97
|
+
|
|
98
|
+
session.delete(embedding_model)
|
|
99
|
+
session.commit()
|
|
100
|
+
return True
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
"""Metadata resolver module."""
|
|
2
|
+
|
|
3
|
+
from lightly_studio.resolvers.metadata_resolver.sample import (
|
|
4
|
+
bulk_set_metadata,
|
|
5
|
+
get_by_sample_id,
|
|
6
|
+
get_value_for_sample,
|
|
7
|
+
set_value_for_sample,
|
|
8
|
+
)
|
|
9
|
+
|
|
10
|
+
__all__ = [
|
|
11
|
+
"bulk_set_metadata",
|
|
12
|
+
"get_by_sample_id",
|
|
13
|
+
"get_value_for_sample",
|
|
14
|
+
"set_value_for_sample",
|
|
15
|
+
]
|
|
@@ -0,0 +1,163 @@
|
|
|
1
|
+
"""Generic metadata filtering utilities."""
|
|
2
|
+
|
|
3
|
+
import json
|
|
4
|
+
import re
|
|
5
|
+
from typing import Any, Dict, List, Literal, Protocol, Type, TypeVar
|
|
6
|
+
|
|
7
|
+
from pydantic import BaseModel
|
|
8
|
+
from sqlalchemy import text
|
|
9
|
+
|
|
10
|
+
from lightly_studio.type_definitions import QueryType
|
|
11
|
+
|
|
12
|
+
# Type variables for generic constraints
|
|
13
|
+
T = TypeVar("T", bound=BaseModel)
|
|
14
|
+
M = TypeVar("M", bound="HasMetadata")
|
|
15
|
+
|
|
16
|
+
# Valid operators for metadata filtering
|
|
17
|
+
MetadataOperator = Literal[">", "<", "==", ">=", "<=", "!="]
|
|
18
|
+
|
|
19
|
+
# Default metadata column name
|
|
20
|
+
METADATA_COLUMN = "metadata.data"
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
class HasMetadata(Protocol):
|
|
24
|
+
"""Protocol for models that have metadata."""
|
|
25
|
+
|
|
26
|
+
data: Dict[str, Any]
|
|
27
|
+
metadata_schema: Dict[str, str]
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
class MetadataFilter(BaseModel):
|
|
31
|
+
"""Encapsulates a single metadata filter condition."""
|
|
32
|
+
|
|
33
|
+
key: str
|
|
34
|
+
op: MetadataOperator
|
|
35
|
+
value: Any
|
|
36
|
+
|
|
37
|
+
def model_post_init(self, __context: Any) -> None:
|
|
38
|
+
"""Post-initialization hook to serialize string values."""
|
|
39
|
+
# Pre-serialize string values for JSON comparison
|
|
40
|
+
if isinstance(self.value, str):
|
|
41
|
+
# Avoid double-serialization
|
|
42
|
+
try:
|
|
43
|
+
json.loads(self.value)
|
|
44
|
+
# Already serialized, don't serialize again
|
|
45
|
+
except (json.JSONDecodeError, TypeError):
|
|
46
|
+
# Not serialized, serialize it
|
|
47
|
+
self.value = json.dumps(self.value)
|
|
48
|
+
|
|
49
|
+
|
|
50
|
+
class Metadata:
|
|
51
|
+
"""Helper class for creating metadata filters with operator syntax."""
|
|
52
|
+
|
|
53
|
+
def __init__(self, key: str) -> None:
|
|
54
|
+
"""Initialize metadata filter with key."""
|
|
55
|
+
self.key = key
|
|
56
|
+
|
|
57
|
+
def __gt__(self, value: Any) -> MetadataFilter:
|
|
58
|
+
"""Create greater than filter."""
|
|
59
|
+
return MetadataFilter(key=self.key, op=">", value=value)
|
|
60
|
+
|
|
61
|
+
def __lt__(self, value: Any) -> MetadataFilter:
|
|
62
|
+
"""Create less than filter."""
|
|
63
|
+
return MetadataFilter(key=self.key, op="<", value=value)
|
|
64
|
+
|
|
65
|
+
def __ge__(self, value: Any) -> MetadataFilter:
|
|
66
|
+
"""Create greater than or equal filter."""
|
|
67
|
+
return MetadataFilter(key=self.key, op=">=", value=value)
|
|
68
|
+
|
|
69
|
+
def __le__(self, value: Any) -> MetadataFilter:
|
|
70
|
+
"""Create less than or equal filter."""
|
|
71
|
+
return MetadataFilter(key=self.key, op="<=", value=value)
|
|
72
|
+
|
|
73
|
+
def __eq__(self, value: Any) -> MetadataFilter: # type: ignore
|
|
74
|
+
"""Create equality filter."""
|
|
75
|
+
return MetadataFilter(key=self.key, op="==", value=value)
|
|
76
|
+
|
|
77
|
+
def __ne__(self, value: Any) -> MetadataFilter: # type: ignore
|
|
78
|
+
"""Create not equal filter."""
|
|
79
|
+
return MetadataFilter(key=self.key, op="!=", value=value)
|
|
80
|
+
|
|
81
|
+
|
|
82
|
+
def _sanitize_param_name(field: str) -> str:
|
|
83
|
+
"""Sanitize field name for use as SQL parameter name.
|
|
84
|
+
|
|
85
|
+
Args:
|
|
86
|
+
field: The field name (may contain dots for nested paths).
|
|
87
|
+
|
|
88
|
+
Returns:
|
|
89
|
+
A sanitized parameter name safe for SQL binding.
|
|
90
|
+
"""
|
|
91
|
+
# Replace dots and other problematic characters with underscores
|
|
92
|
+
return re.sub(r"[^a-zA-Z0-9_]", "_", field)
|
|
93
|
+
|
|
94
|
+
|
|
95
|
+
def apply_metadata_filters(
|
|
96
|
+
query: QueryType,
|
|
97
|
+
metadata_filters: List[MetadataFilter],
|
|
98
|
+
*,
|
|
99
|
+
metadata_model: Type[M],
|
|
100
|
+
metadata_join_condition: Any,
|
|
101
|
+
) -> QueryType:
|
|
102
|
+
"""Apply metadata filters to a query.
|
|
103
|
+
|
|
104
|
+
Args:
|
|
105
|
+
query: The base query to filter.
|
|
106
|
+
metadata_filters: The list of metadata filters to apply.
|
|
107
|
+
metadata_model: The metadata table/model class.
|
|
108
|
+
metadata_join_condition: The join condition between the main table
|
|
109
|
+
and metadata table.
|
|
110
|
+
|
|
111
|
+
Returns:
|
|
112
|
+
The filtered query.
|
|
113
|
+
|
|
114
|
+
Raises:
|
|
115
|
+
ValueError: If any field name contains invalid characters.
|
|
116
|
+
|
|
117
|
+
Example:
|
|
118
|
+
```python
|
|
119
|
+
# Simple filters (AND by default)
|
|
120
|
+
query = apply_metadata_filters(
|
|
121
|
+
query,
|
|
122
|
+
metadata_filters=[
|
|
123
|
+
Metadata("temperature") > 25,
|
|
124
|
+
Metadata("location") == "city",
|
|
125
|
+
],
|
|
126
|
+
metadata_model=SampleMetadataTable,
|
|
127
|
+
metadata_join_condition=SampleMetadataTable.sample_id ==
|
|
128
|
+
SampleTable.sample_id,
|
|
129
|
+
)
|
|
130
|
+
```
|
|
131
|
+
"""
|
|
132
|
+
if not metadata_filters:
|
|
133
|
+
return query
|
|
134
|
+
|
|
135
|
+
# Apply the filters using JSON extraction
|
|
136
|
+
query = query.join(
|
|
137
|
+
metadata_model,
|
|
138
|
+
metadata_join_condition,
|
|
139
|
+
)
|
|
140
|
+
|
|
141
|
+
for i, meta_filter in enumerate(metadata_filters):
|
|
142
|
+
field = meta_filter.key
|
|
143
|
+
value = meta_filter.value
|
|
144
|
+
op = meta_filter.op
|
|
145
|
+
|
|
146
|
+
json_path = "$." + field
|
|
147
|
+
# Add unique identifier to parameter name to avoid conflicts
|
|
148
|
+
param_name = f"{_sanitize_param_name(field)}_{i}"
|
|
149
|
+
|
|
150
|
+
# Build the condition based on value type
|
|
151
|
+
if isinstance(value, (int, float)):
|
|
152
|
+
# For numeric values, use json_extract with CAST
|
|
153
|
+
condition = (
|
|
154
|
+
f"CAST(json_extract({METADATA_COLUMN}, '{json_path}') AS FLOAT) {op} :{param_name}"
|
|
155
|
+
)
|
|
156
|
+
else:
|
|
157
|
+
# For string values, use json_extract with parameter binding
|
|
158
|
+
condition = f"json_extract({METADATA_COLUMN}, '{json_path}') {op} :{param_name}"
|
|
159
|
+
|
|
160
|
+
# Apply the condition (same for both types)
|
|
161
|
+
query = query.where(text(condition).bindparams(**{param_name: value}))
|
|
162
|
+
|
|
163
|
+
return query
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
"""Resolvers for metadata operations."""
|
|
2
|
+
|
|
3
|
+
from .bulk_set_metadata import (
|
|
4
|
+
bulk_set_metadata,
|
|
5
|
+
)
|
|
6
|
+
from .get_by_sample_id import (
|
|
7
|
+
get_by_sample_id,
|
|
8
|
+
)
|
|
9
|
+
from .get_value_for_sample import (
|
|
10
|
+
get_value_for_sample,
|
|
11
|
+
)
|
|
12
|
+
from .set_value_for_sample import (
|
|
13
|
+
set_value_for_sample,
|
|
14
|
+
)
|
|
15
|
+
|
|
16
|
+
__all__ = [
|
|
17
|
+
"bulk_set_metadata",
|
|
18
|
+
"get_by_sample_id",
|
|
19
|
+
"get_value_for_sample",
|
|
20
|
+
"set_value_for_sample",
|
|
21
|
+
]
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
"""Resolver for operations for setting metadata."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from datetime import datetime, timezone
|
|
6
|
+
from typing import Any
|
|
7
|
+
from uuid import UUID
|
|
8
|
+
|
|
9
|
+
from sqlmodel import Session
|
|
10
|
+
|
|
11
|
+
from lightly_studio.metadata.complex_metadata import serialize_complex_metadata
|
|
12
|
+
from lightly_studio.models.metadata import (
|
|
13
|
+
SampleMetadataTable,
|
|
14
|
+
get_type_name,
|
|
15
|
+
)
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
def bulk_set_metadata(
|
|
19
|
+
session: Session,
|
|
20
|
+
sample_metadata: list[tuple[UUID, dict[str, Any]]],
|
|
21
|
+
) -> None:
|
|
22
|
+
"""Bulk insert metadata for multiple samples.
|
|
23
|
+
|
|
24
|
+
Args:
|
|
25
|
+
session: The database session.
|
|
26
|
+
sample_metadata: List of (sample_id, metadata_dict) tuples.
|
|
27
|
+
"""
|
|
28
|
+
now = datetime.now(timezone.utc)
|
|
29
|
+
objects = []
|
|
30
|
+
for sample_id, metadata in sample_metadata:
|
|
31
|
+
metadata_schema = {}
|
|
32
|
+
serialized_data = {}
|
|
33
|
+
for key, value in metadata.items():
|
|
34
|
+
# Serialize complex metadata objects.
|
|
35
|
+
serialized_data[key] = serialize_complex_metadata(value)
|
|
36
|
+
# Get the type name
|
|
37
|
+
metadata_schema[key] = get_type_name(value)
|
|
38
|
+
|
|
39
|
+
obj = SampleMetadataTable(
|
|
40
|
+
sample_id=sample_id,
|
|
41
|
+
data=serialized_data,
|
|
42
|
+
metadata_schema=metadata_schema,
|
|
43
|
+
created_at=now,
|
|
44
|
+
updated_at=now,
|
|
45
|
+
)
|
|
46
|
+
objects.append(obj)
|
|
47
|
+
session.bulk_save_objects(objects)
|
|
48
|
+
session.commit()
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
"""Resolver for operations for retrieving metadata."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from uuid import UUID
|
|
6
|
+
|
|
7
|
+
from sqlmodel import Session, select
|
|
8
|
+
|
|
9
|
+
from lightly_studio.models.metadata import SampleMetadataTable
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
def get_by_sample_id(session: Session, sample_id: UUID) -> SampleMetadataTable | None:
|
|
13
|
+
"""Retrieve the metadata object for a given sample.
|
|
14
|
+
|
|
15
|
+
Args:
|
|
16
|
+
session: The database session.
|
|
17
|
+
sample_id: The sample's UUID.
|
|
18
|
+
|
|
19
|
+
Returns:
|
|
20
|
+
The CustomMetadataTable instance or None if not found.
|
|
21
|
+
"""
|
|
22
|
+
return session.exec(
|
|
23
|
+
select(SampleMetadataTable).where(SampleMetadataTable.sample_id == sample_id)
|
|
24
|
+
).one_or_none()
|