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,104 @@
|
|
|
1
|
+
"""Resolver for operations for retrieving metadata info."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from uuid import UUID
|
|
6
|
+
|
|
7
|
+
from sqlalchemy import Float, func
|
|
8
|
+
from sqlmodel import Session, col, select
|
|
9
|
+
|
|
10
|
+
from lightly_studio.models.metadata import (
|
|
11
|
+
MetadataInfoView,
|
|
12
|
+
SampleMetadataTable,
|
|
13
|
+
)
|
|
14
|
+
from lightly_studio.models.sample import SampleTable
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
def get_all_metadata_keys_and_schema(
|
|
18
|
+
session: Session,
|
|
19
|
+
dataset_id: UUID,
|
|
20
|
+
) -> list[MetadataInfoView]:
|
|
21
|
+
"""Get all unique metadata keys and their schema for a dataset.
|
|
22
|
+
|
|
23
|
+
Args:
|
|
24
|
+
session: The database session.
|
|
25
|
+
dataset_id: The dataset's UUID.
|
|
26
|
+
|
|
27
|
+
Returns:
|
|
28
|
+
List of dicts with 'name', 'type', and optionally 'min'/'max' for numerical types.
|
|
29
|
+
"""
|
|
30
|
+
# Query all metadata_schema dicts for samples in the dataset
|
|
31
|
+
rows = session.exec(
|
|
32
|
+
select(SampleMetadataTable.metadata_schema)
|
|
33
|
+
.select_from(SampleTable)
|
|
34
|
+
.join(
|
|
35
|
+
SampleMetadataTable,
|
|
36
|
+
col(SampleMetadataTable.sample_id) == col(SampleTable.sample_id),
|
|
37
|
+
)
|
|
38
|
+
.where(SampleTable.dataset_id == dataset_id)
|
|
39
|
+
).all()
|
|
40
|
+
# Merge all schemas
|
|
41
|
+
merged: dict[str, str] = {}
|
|
42
|
+
for schema_dict in rows:
|
|
43
|
+
merged.update(schema_dict)
|
|
44
|
+
|
|
45
|
+
# Get min and max values for numerical metadata
|
|
46
|
+
result = []
|
|
47
|
+
for key, metadata_type in merged.items():
|
|
48
|
+
metadata_info = MetadataInfoView(name=key, type=metadata_type)
|
|
49
|
+
|
|
50
|
+
# Add min and max for numerical types
|
|
51
|
+
if metadata_type in ["integer", "float"]:
|
|
52
|
+
min_max_values = _get_metadata_min_max_values(session, dataset_id, key, metadata_type)
|
|
53
|
+
if min_max_values:
|
|
54
|
+
metadata_info.min = min_max_values[0]
|
|
55
|
+
metadata_info.max = min_max_values[1]
|
|
56
|
+
|
|
57
|
+
result.append(metadata_info)
|
|
58
|
+
|
|
59
|
+
return result
|
|
60
|
+
|
|
61
|
+
|
|
62
|
+
def _get_metadata_min_max_values(
|
|
63
|
+
session: Session,
|
|
64
|
+
dataset_id: UUID,
|
|
65
|
+
metadata_key: str,
|
|
66
|
+
metadata_type: str,
|
|
67
|
+
) -> tuple[int, int] | tuple[float, float] | None:
|
|
68
|
+
"""Get min and max values for a specific numerical metadata key.
|
|
69
|
+
|
|
70
|
+
Args:
|
|
71
|
+
session: The database session.
|
|
72
|
+
dataset_id: The dataset's UUID.
|
|
73
|
+
metadata_key: The metadata key to get min/max for.
|
|
74
|
+
metadata_type: The metadata type ("integer" or "float").
|
|
75
|
+
|
|
76
|
+
Returns:
|
|
77
|
+
Tuple with 'min' and 'max' values, or None if no values found.
|
|
78
|
+
"""
|
|
79
|
+
# Build JSON path for the metadata key.
|
|
80
|
+
json_path = f"$.{metadata_key}"
|
|
81
|
+
|
|
82
|
+
query = (
|
|
83
|
+
select(
|
|
84
|
+
func.min(func.cast(func.json_extract(SampleMetadataTable.data, json_path), Float)),
|
|
85
|
+
func.max(func.cast(func.json_extract(SampleMetadataTable.data, json_path), Float)),
|
|
86
|
+
)
|
|
87
|
+
.select_from(SampleTable)
|
|
88
|
+
.join(SampleMetadataTable, col(SampleMetadataTable.sample_id) == col(SampleTable.sample_id))
|
|
89
|
+
.where(
|
|
90
|
+
SampleTable.dataset_id == dataset_id,
|
|
91
|
+
func.json_extract(SampleMetadataTable.data, json_path).is_not(None),
|
|
92
|
+
)
|
|
93
|
+
)
|
|
94
|
+
|
|
95
|
+
result = session.exec(query).first()
|
|
96
|
+
|
|
97
|
+
if result and result[0] is not None and result[1] is not None:
|
|
98
|
+
# Convert to appropriate type
|
|
99
|
+
if metadata_type == "integer":
|
|
100
|
+
return int(result[0]), int(result[1])
|
|
101
|
+
if metadata_type == "float":
|
|
102
|
+
return float(result[0]), float(result[1])
|
|
103
|
+
|
|
104
|
+
return None
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
"""Resolver for operations for retrieving metadata."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from typing import Any
|
|
6
|
+
from uuid import UUID
|
|
7
|
+
|
|
8
|
+
from sqlmodel import Session
|
|
9
|
+
|
|
10
|
+
from .get_by_sample_id import get_by_sample_id
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
def get_value_for_sample(session: Session, sample_id: UUID, key: str) -> Any | None:
|
|
14
|
+
"""Get a specific metadata value for a sample.
|
|
15
|
+
|
|
16
|
+
Args:
|
|
17
|
+
session: The database session.
|
|
18
|
+
sample_id: The sample's UUID.
|
|
19
|
+
key: The metadata key.
|
|
20
|
+
|
|
21
|
+
Returns:
|
|
22
|
+
The value for the given key, or None if not found.
|
|
23
|
+
"""
|
|
24
|
+
metadata = get_by_sample_id(session=session, sample_id=sample_id)
|
|
25
|
+
if metadata is None:
|
|
26
|
+
return None
|
|
27
|
+
return metadata.data.get(key)
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
"""Resolver for operations for setting metadata."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from typing import Any
|
|
6
|
+
from uuid import UUID
|
|
7
|
+
|
|
8
|
+
from sqlmodel import Session
|
|
9
|
+
|
|
10
|
+
from lightly_studio.models.metadata import (
|
|
11
|
+
SampleMetadataTable,
|
|
12
|
+
)
|
|
13
|
+
from lightly_studio.resolvers.metadata_resolver.sample.get_by_sample_id import (
|
|
14
|
+
get_by_sample_id,
|
|
15
|
+
)
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
def set_value_for_sample(
|
|
19
|
+
session: Session,
|
|
20
|
+
sample_id: UUID,
|
|
21
|
+
key: str,
|
|
22
|
+
value: Any,
|
|
23
|
+
) -> SampleMetadataTable:
|
|
24
|
+
"""Set a specific metadata value for a sample.
|
|
25
|
+
|
|
26
|
+
Args:
|
|
27
|
+
session: The database session.
|
|
28
|
+
sample_id: The sample's UUID.
|
|
29
|
+
key: The metadata key.
|
|
30
|
+
value: The value to set.
|
|
31
|
+
|
|
32
|
+
Returns:
|
|
33
|
+
The updated CustomMetadataTable instance.
|
|
34
|
+
|
|
35
|
+
Raises:
|
|
36
|
+
ValueError: If the value type doesn't match the schema.
|
|
37
|
+
"""
|
|
38
|
+
metadata = get_by_sample_id(session=session, sample_id=sample_id)
|
|
39
|
+
if metadata is None:
|
|
40
|
+
# Create new metadata row if it does not exist
|
|
41
|
+
metadata = SampleMetadataTable(
|
|
42
|
+
sample_id=sample_id,
|
|
43
|
+
data={},
|
|
44
|
+
metadata_schema={},
|
|
45
|
+
)
|
|
46
|
+
session.add(metadata)
|
|
47
|
+
|
|
48
|
+
metadata.set_value(key, value)
|
|
49
|
+
|
|
50
|
+
# Commit changes and refresh the object
|
|
51
|
+
session.commit()
|
|
52
|
+
session.refresh(metadata)
|
|
53
|
+
return metadata
|
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
"""Handler for database operations related to sample embeddings."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from uuid import UUID
|
|
6
|
+
|
|
7
|
+
from sqlalchemy import String, cast
|
|
8
|
+
from sqlmodel import Session, col, select
|
|
9
|
+
|
|
10
|
+
from lightly_studio.models.sample import SampleTable
|
|
11
|
+
from lightly_studio.models.sample_embedding import (
|
|
12
|
+
SampleEmbeddingCreate,
|
|
13
|
+
SampleEmbeddingTable,
|
|
14
|
+
)
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
def create(session: Session, sample_embedding: SampleEmbeddingCreate) -> SampleEmbeddingTable:
|
|
18
|
+
"""Create a new SampleEmbedding in the database."""
|
|
19
|
+
db_sample_embedding = SampleEmbeddingTable.model_validate(sample_embedding)
|
|
20
|
+
session.add(db_sample_embedding)
|
|
21
|
+
session.commit()
|
|
22
|
+
session.refresh(db_sample_embedding)
|
|
23
|
+
return db_sample_embedding
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
def create_many(session: Session, sample_embeddings: list[SampleEmbeddingCreate]) -> None:
|
|
27
|
+
"""Create many sample embeddings in a single database commit."""
|
|
28
|
+
db_sample_embeddings = [SampleEmbeddingTable.model_validate(e) for e in sample_embeddings]
|
|
29
|
+
session.bulk_save_objects(db_sample_embeddings)
|
|
30
|
+
session.commit()
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
def get_by_sample_ids(
|
|
34
|
+
session: Session,
|
|
35
|
+
sample_ids: list[UUID],
|
|
36
|
+
embedding_model_id: UUID,
|
|
37
|
+
) -> list[SampleEmbeddingTable]:
|
|
38
|
+
"""Get sample embeddings for the specified sample IDs.
|
|
39
|
+
|
|
40
|
+
Output order matches the input order.
|
|
41
|
+
|
|
42
|
+
Args:
|
|
43
|
+
session: The database session.
|
|
44
|
+
sample_ids: List of sample IDs to get embeddings for.
|
|
45
|
+
embedding_model_id: The embedding model ID to filter by.
|
|
46
|
+
|
|
47
|
+
Returns:
|
|
48
|
+
List of sample embeddings associated with the provided IDs.
|
|
49
|
+
"""
|
|
50
|
+
string_ids = [str(id_) for id_ in sample_ids]
|
|
51
|
+
results = list(
|
|
52
|
+
session.exec(
|
|
53
|
+
select(SampleEmbeddingTable)
|
|
54
|
+
.where(cast(SampleEmbeddingTable.sample_id, String).in_(string_ids))
|
|
55
|
+
.where(SampleEmbeddingTable.embedding_model_id == embedding_model_id)
|
|
56
|
+
).all()
|
|
57
|
+
)
|
|
58
|
+
# Return embeddings in the same order as the input IDs
|
|
59
|
+
embedding_map = {embedding.sample_id: embedding for embedding in results}
|
|
60
|
+
return [embedding_map[id_] for id_ in sample_ids if id_ in embedding_map]
|
|
61
|
+
|
|
62
|
+
|
|
63
|
+
def get_all_by_dataset_id(
|
|
64
|
+
session: Session,
|
|
65
|
+
dataset_id: UUID,
|
|
66
|
+
embedding_model_id: UUID,
|
|
67
|
+
) -> list[SampleEmbeddingTable]:
|
|
68
|
+
"""Get all sample embeddings for samples in a specific dataset.
|
|
69
|
+
|
|
70
|
+
Args:
|
|
71
|
+
session: The database session.
|
|
72
|
+
dataset_id: The dataset ID to filter by.
|
|
73
|
+
embedding_model_id: The embedding model ID to filter by.
|
|
74
|
+
|
|
75
|
+
Returns:
|
|
76
|
+
List of sample embeddings associated with the dataset.
|
|
77
|
+
"""
|
|
78
|
+
query = (
|
|
79
|
+
select(SampleEmbeddingTable)
|
|
80
|
+
.join(SampleTable)
|
|
81
|
+
.where(SampleEmbeddingTable.sample_id == SampleTable.sample_id)
|
|
82
|
+
.where(SampleTable.dataset_id == dataset_id)
|
|
83
|
+
.where(SampleEmbeddingTable.embedding_model_id == embedding_model_id)
|
|
84
|
+
.order_by(col(SampleTable.created_at).asc())
|
|
85
|
+
)
|
|
86
|
+
return list(session.exec(query).all())
|
|
@@ -0,0 +1,249 @@
|
|
|
1
|
+
"""Handler for database operations related to samples."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from collections.abc import Sequence
|
|
6
|
+
from datetime import datetime, timezone
|
|
7
|
+
from uuid import UUID
|
|
8
|
+
|
|
9
|
+
from pydantic import BaseModel
|
|
10
|
+
from sqlmodel import Session, col, func, select
|
|
11
|
+
from sqlmodel.sql.expression import Select
|
|
12
|
+
|
|
13
|
+
from lightly_studio.models.annotation.annotation_base import AnnotationBaseTable
|
|
14
|
+
from lightly_studio.models.annotation_label import AnnotationLabelTable
|
|
15
|
+
from lightly_studio.models.embedding_model import EmbeddingModelTable
|
|
16
|
+
from lightly_studio.models.sample import SampleCreate, SampleTable
|
|
17
|
+
from lightly_studio.models.sample_embedding import SampleEmbeddingTable
|
|
18
|
+
from lightly_studio.models.tag import TagTable
|
|
19
|
+
from lightly_studio.resolvers.samples_filter import SampleFilter
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
def create(session: Session, sample: SampleCreate) -> SampleTable:
|
|
23
|
+
"""Create a new sample in the database."""
|
|
24
|
+
db_sample = SampleTable.model_validate(sample)
|
|
25
|
+
session.add(db_sample)
|
|
26
|
+
session.commit()
|
|
27
|
+
session.refresh(db_sample)
|
|
28
|
+
return db_sample
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
def create_many(session: Session, samples: list[SampleCreate]) -> list[SampleTable]:
|
|
32
|
+
"""Create multiple samples in a single database commit."""
|
|
33
|
+
db_samples = [SampleTable.model_validate(sample) for sample in samples]
|
|
34
|
+
session.bulk_save_objects(db_samples)
|
|
35
|
+
session.commit()
|
|
36
|
+
return db_samples
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
def get_by_id(session: Session, dataset_id: UUID, sample_id: UUID) -> SampleTable | None:
|
|
40
|
+
"""Retrieve a single sample by ID."""
|
|
41
|
+
return session.exec(
|
|
42
|
+
select(SampleTable).where(
|
|
43
|
+
SampleTable.sample_id == sample_id, SampleTable.dataset_id == dataset_id
|
|
44
|
+
)
|
|
45
|
+
).one_or_none()
|
|
46
|
+
|
|
47
|
+
|
|
48
|
+
def get_many_by_id(session: Session, sample_ids: list[UUID]) -> list[SampleTable]:
|
|
49
|
+
"""Retrieve multiple samples by their IDs.
|
|
50
|
+
|
|
51
|
+
Output order matches the input order.
|
|
52
|
+
"""
|
|
53
|
+
results = session.exec(
|
|
54
|
+
select(SampleTable).where(col(SampleTable.sample_id).in_(sample_ids))
|
|
55
|
+
).all()
|
|
56
|
+
# Return samples in the same order as the input IDs
|
|
57
|
+
sample_map = {sample.sample_id: sample for sample in results}
|
|
58
|
+
return [sample_map[id_] for id_ in sample_ids if id_ in sample_map]
|
|
59
|
+
|
|
60
|
+
|
|
61
|
+
class GetAllSamplesByDatasetIdResult(BaseModel):
|
|
62
|
+
"""Result of getting all samples."""
|
|
63
|
+
|
|
64
|
+
samples: Sequence[SampleTable]
|
|
65
|
+
total_count: int
|
|
66
|
+
|
|
67
|
+
|
|
68
|
+
def get_all_by_dataset_id( # noqa: PLR0913
|
|
69
|
+
session: Session,
|
|
70
|
+
dataset_id: UUID,
|
|
71
|
+
offset: int = 0,
|
|
72
|
+
limit: int | None = None,
|
|
73
|
+
filters: SampleFilter | None = None,
|
|
74
|
+
text_embedding: list[float] | None = None,
|
|
75
|
+
sample_ids: list[UUID] | None = None,
|
|
76
|
+
) -> GetAllSamplesByDatasetIdResult:
|
|
77
|
+
"""Retrieve samples for a specific dataset with optional filtering."""
|
|
78
|
+
samples_query = select(SampleTable).where(SampleTable.dataset_id == dataset_id)
|
|
79
|
+
total_count_query = (
|
|
80
|
+
select(func.count()).select_from(SampleTable).where(SampleTable.dataset_id == dataset_id)
|
|
81
|
+
)
|
|
82
|
+
|
|
83
|
+
if filters:
|
|
84
|
+
samples_query = filters.apply(samples_query)
|
|
85
|
+
total_count_query = filters.apply(total_count_query)
|
|
86
|
+
|
|
87
|
+
# TODO(Michal, 06/2025): Consider adding sample_ids to the filters.
|
|
88
|
+
if sample_ids:
|
|
89
|
+
samples_query = samples_query.where(col(SampleTable.sample_id).in_(sample_ids))
|
|
90
|
+
total_count_query = total_count_query.where(col(SampleTable.sample_id).in_(sample_ids))
|
|
91
|
+
|
|
92
|
+
if text_embedding:
|
|
93
|
+
# Fetch the first embedding_model_id for the given dataset_id
|
|
94
|
+
embedding_model_id = session.exec(
|
|
95
|
+
select(EmbeddingModelTable.embedding_model_id)
|
|
96
|
+
.where(EmbeddingModelTable.dataset_id == dataset_id)
|
|
97
|
+
.limit(1)
|
|
98
|
+
).first()
|
|
99
|
+
if embedding_model_id:
|
|
100
|
+
# Join with SampleEmbedding table to access embeddings
|
|
101
|
+
samples_query = (
|
|
102
|
+
samples_query.join(
|
|
103
|
+
SampleEmbeddingTable,
|
|
104
|
+
col(SampleTable.sample_id) == col(SampleEmbeddingTable.sample_id),
|
|
105
|
+
)
|
|
106
|
+
.where(SampleEmbeddingTable.embedding_model_id == embedding_model_id)
|
|
107
|
+
.order_by(
|
|
108
|
+
func.list_cosine_distance(
|
|
109
|
+
SampleEmbeddingTable.embedding,
|
|
110
|
+
text_embedding,
|
|
111
|
+
)
|
|
112
|
+
)
|
|
113
|
+
)
|
|
114
|
+
total_count_query = total_count_query.join(
|
|
115
|
+
SampleEmbeddingTable,
|
|
116
|
+
col(SampleTable.sample_id) == col(SampleEmbeddingTable.sample_id),
|
|
117
|
+
).where(SampleEmbeddingTable.embedding_model_id == embedding_model_id)
|
|
118
|
+
else:
|
|
119
|
+
samples_query = samples_query.order_by(
|
|
120
|
+
col(SampleTable.created_at).asc(), col(SampleTable.sample_id).asc()
|
|
121
|
+
)
|
|
122
|
+
|
|
123
|
+
# paginate query when offset or limit are set/positive
|
|
124
|
+
if offset > 0:
|
|
125
|
+
samples_query = samples_query.offset(offset)
|
|
126
|
+
if limit is not None:
|
|
127
|
+
samples_query = samples_query.limit(limit)
|
|
128
|
+
|
|
129
|
+
return GetAllSamplesByDatasetIdResult(
|
|
130
|
+
samples=session.exec(samples_query).all(),
|
|
131
|
+
total_count=session.exec(total_count_query).one(),
|
|
132
|
+
)
|
|
133
|
+
|
|
134
|
+
|
|
135
|
+
def get_dimension_bounds(
|
|
136
|
+
session: Session,
|
|
137
|
+
dataset_id: UUID,
|
|
138
|
+
annotation_label_ids: list[UUID] | None = None,
|
|
139
|
+
tag_ids: list[UUID] | None = None,
|
|
140
|
+
) -> dict[str, int]:
|
|
141
|
+
"""Get min and max dimensions of samples in a dataset."""
|
|
142
|
+
# Prepare the base query for dimensions
|
|
143
|
+
query: Select[tuple[int | None, int | None, int | None, int | None]] = select(
|
|
144
|
+
func.min(SampleTable.width).label("min_width"),
|
|
145
|
+
func.max(SampleTable.width).label("max_width"),
|
|
146
|
+
func.min(SampleTable.height).label("min_height"),
|
|
147
|
+
func.max(SampleTable.height).label("max_height"),
|
|
148
|
+
)
|
|
149
|
+
|
|
150
|
+
if annotation_label_ids:
|
|
151
|
+
# Subquery to filter samples matching all annotation labels
|
|
152
|
+
label_filter = (
|
|
153
|
+
select(SampleTable.sample_id)
|
|
154
|
+
.join(
|
|
155
|
+
AnnotationBaseTable,
|
|
156
|
+
col(SampleTable.sample_id) == col(AnnotationBaseTable.sample_id),
|
|
157
|
+
)
|
|
158
|
+
.join(
|
|
159
|
+
AnnotationLabelTable,
|
|
160
|
+
col(AnnotationBaseTable.annotation_label_id)
|
|
161
|
+
== col(AnnotationLabelTable.annotation_label_id),
|
|
162
|
+
)
|
|
163
|
+
.where(
|
|
164
|
+
SampleTable.dataset_id == dataset_id,
|
|
165
|
+
col(AnnotationLabelTable.annotation_label_id).in_(annotation_label_ids),
|
|
166
|
+
)
|
|
167
|
+
.group_by(col(SampleTable.sample_id))
|
|
168
|
+
.having(
|
|
169
|
+
func.count(col(AnnotationLabelTable.annotation_label_id).distinct())
|
|
170
|
+
== len(annotation_label_ids)
|
|
171
|
+
)
|
|
172
|
+
)
|
|
173
|
+
# Filter the dimension query based on the subquery
|
|
174
|
+
query = query.where(col(SampleTable.sample_id).in_(label_filter))
|
|
175
|
+
else:
|
|
176
|
+
# If no labels specified, filter dimensions
|
|
177
|
+
# for all samples in the dataset
|
|
178
|
+
query = query.where(SampleTable.dataset_id == dataset_id)
|
|
179
|
+
|
|
180
|
+
if tag_ids:
|
|
181
|
+
query = (
|
|
182
|
+
query.join(SampleTable.tags)
|
|
183
|
+
.where(SampleTable.tags.any(col(TagTable.tag_id).in_(tag_ids)))
|
|
184
|
+
.distinct()
|
|
185
|
+
)
|
|
186
|
+
|
|
187
|
+
# Note: We use SQLAlchemy's session.execute instead of SQLModel's
|
|
188
|
+
# ession.exec to be able to fetch the columns with names with the
|
|
189
|
+
# `mappings()` method.
|
|
190
|
+
result = session.execute(query).mappings().one()
|
|
191
|
+
return {key: value for key, value in result.items() if value is not None}
|
|
192
|
+
|
|
193
|
+
|
|
194
|
+
def update(session: Session, sample_id: UUID, sample_data: SampleCreate) -> SampleTable | None:
|
|
195
|
+
"""Update an existing sample."""
|
|
196
|
+
sample = get_by_id(session=session, dataset_id=sample_data.dataset_id, sample_id=sample_id)
|
|
197
|
+
if not sample:
|
|
198
|
+
return None
|
|
199
|
+
|
|
200
|
+
sample.file_name = sample_data.file_name
|
|
201
|
+
sample.width = sample_data.width
|
|
202
|
+
sample.height = sample_data.height
|
|
203
|
+
sample.updated_at = datetime.now(timezone.utc)
|
|
204
|
+
|
|
205
|
+
session.commit()
|
|
206
|
+
session.refresh(sample)
|
|
207
|
+
return sample
|
|
208
|
+
|
|
209
|
+
|
|
210
|
+
def delete(session: Session, dataset_id: UUID, sample_id: UUID) -> bool:
|
|
211
|
+
"""Delete a sample."""
|
|
212
|
+
sample = get_by_id(session=session, dataset_id=dataset_id, sample_id=sample_id)
|
|
213
|
+
if not sample:
|
|
214
|
+
return False
|
|
215
|
+
|
|
216
|
+
session.delete(sample)
|
|
217
|
+
session.commit()
|
|
218
|
+
return True
|
|
219
|
+
|
|
220
|
+
|
|
221
|
+
def get_samples_excluding(
|
|
222
|
+
session: Session,
|
|
223
|
+
dataset_id: UUID,
|
|
224
|
+
excluded_sample_ids: list[UUID],
|
|
225
|
+
limit: int | None = None,
|
|
226
|
+
) -> Sequence[SampleTable]:
|
|
227
|
+
"""Get random samples excluding specified sample IDs.
|
|
228
|
+
|
|
229
|
+
Args:
|
|
230
|
+
session: The database session.
|
|
231
|
+
dataset_id: The dataset ID to filter by.
|
|
232
|
+
excluded_sample_ids: List of sample IDs to exclude from the result.
|
|
233
|
+
limit: Maximum number of samples to return.
|
|
234
|
+
If None, returns all matches.
|
|
235
|
+
|
|
236
|
+
Returns:
|
|
237
|
+
List of samples not associated with the excluded IDs.
|
|
238
|
+
"""
|
|
239
|
+
query = (
|
|
240
|
+
select(SampleTable)
|
|
241
|
+
.where(SampleTable.dataset_id == dataset_id)
|
|
242
|
+
.where(col(SampleTable.sample_id).not_in(excluded_sample_ids))
|
|
243
|
+
.order_by(func.random())
|
|
244
|
+
)
|
|
245
|
+
|
|
246
|
+
if limit is not None:
|
|
247
|
+
query = query.limit(limit)
|
|
248
|
+
|
|
249
|
+
return session.exec(query).all()
|
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
"""Utility functions for building database queries."""
|
|
2
|
+
|
|
3
|
+
from typing import List, Optional
|
|
4
|
+
from uuid import UUID
|
|
5
|
+
|
|
6
|
+
from pydantic import BaseModel
|
|
7
|
+
from sqlmodel import col, select
|
|
8
|
+
|
|
9
|
+
from lightly_studio.models.annotation.annotation_base import AnnotationBaseTable
|
|
10
|
+
from lightly_studio.models.annotation_label import AnnotationLabelTable
|
|
11
|
+
from lightly_studio.models.metadata import SampleMetadataTable
|
|
12
|
+
from lightly_studio.models.sample import SampleTable
|
|
13
|
+
from lightly_studio.models.tag import TagTable
|
|
14
|
+
from lightly_studio.resolvers.metadata_resolver.metadata_filter import (
|
|
15
|
+
MetadataFilter,
|
|
16
|
+
apply_metadata_filters,
|
|
17
|
+
)
|
|
18
|
+
from lightly_studio.type_definitions import QueryType
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
class FilterDimensions(BaseModel):
|
|
22
|
+
"""Encapsulates dimension-based filter parameters for querying samples."""
|
|
23
|
+
|
|
24
|
+
min: Optional[int] = None
|
|
25
|
+
max: Optional[int] = None
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
class SampleFilter(BaseModel):
|
|
29
|
+
"""Encapsulates filter parameters for querying samples."""
|
|
30
|
+
|
|
31
|
+
width: Optional[FilterDimensions] = None
|
|
32
|
+
height: Optional[FilterDimensions] = None
|
|
33
|
+
annotation_label_ids: Optional[List[UUID]] = None
|
|
34
|
+
tag_ids: Optional[List[UUID]] = None
|
|
35
|
+
metadata_filters: Optional[List[MetadataFilter]] = None
|
|
36
|
+
|
|
37
|
+
def apply(self, query: QueryType) -> QueryType:
|
|
38
|
+
"""Apply the filters to the given query."""
|
|
39
|
+
# Apply dimension-based filters to the query.
|
|
40
|
+
if self.width:
|
|
41
|
+
if self.width.min is not None:
|
|
42
|
+
query = query.where(SampleTable.width >= self.width.min)
|
|
43
|
+
if self.width.max is not None:
|
|
44
|
+
query = query.where(SampleTable.width <= self.width.max)
|
|
45
|
+
if self.height:
|
|
46
|
+
if self.height.min is not None:
|
|
47
|
+
query = query.where(SampleTable.height >= self.height.min)
|
|
48
|
+
if self.height.max is not None:
|
|
49
|
+
query = query.where(SampleTable.height <= self.height.max)
|
|
50
|
+
|
|
51
|
+
# Apply annotation label filters to the query.
|
|
52
|
+
if self.annotation_label_ids:
|
|
53
|
+
sample_ids_subquery = (
|
|
54
|
+
select(AnnotationBaseTable.sample_id)
|
|
55
|
+
.select_from(AnnotationBaseTable)
|
|
56
|
+
.join(AnnotationBaseTable.annotation_label)
|
|
57
|
+
.where(col(AnnotationLabelTable.annotation_label_id).in_(self.annotation_label_ids))
|
|
58
|
+
.distinct()
|
|
59
|
+
)
|
|
60
|
+
query = query.where(col(SampleTable.sample_id).in_(sample_ids_subquery))
|
|
61
|
+
|
|
62
|
+
# Apply tag filters to the query.
|
|
63
|
+
if self.tag_ids:
|
|
64
|
+
sample_ids_subquery = (
|
|
65
|
+
select(SampleTable.sample_id)
|
|
66
|
+
.select_from(SampleTable)
|
|
67
|
+
.join(SampleTable.tags)
|
|
68
|
+
.where(col(TagTable.tag_id).in_(self.tag_ids))
|
|
69
|
+
.distinct()
|
|
70
|
+
)
|
|
71
|
+
query = query.where(col(SampleTable.sample_id).in_(sample_ids_subquery))
|
|
72
|
+
|
|
73
|
+
# Apply metadata filters to the query.
|
|
74
|
+
if self.metadata_filters:
|
|
75
|
+
query = apply_metadata_filters(
|
|
76
|
+
query,
|
|
77
|
+
self.metadata_filters,
|
|
78
|
+
metadata_model=SampleMetadataTable,
|
|
79
|
+
metadata_join_condition=SampleMetadataTable.sample_id == SampleTable.sample_id,
|
|
80
|
+
)
|
|
81
|
+
return query
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
"""This module contains the resolvers for user settings."""
|
|
2
|
+
|
|
3
|
+
from sqlmodel import Session, select
|
|
4
|
+
|
|
5
|
+
from lightly_studio.models.settings import SettingTable, SettingView
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
def get_settings(session: Session) -> SettingView:
|
|
9
|
+
"""Get current settings.
|
|
10
|
+
|
|
11
|
+
Args:
|
|
12
|
+
session: Database session.
|
|
13
|
+
|
|
14
|
+
Returns:
|
|
15
|
+
The current settings.
|
|
16
|
+
"""
|
|
17
|
+
statement = select(SettingTable)
|
|
18
|
+
result = session.exec(statement).first()
|
|
19
|
+
|
|
20
|
+
# If no settings exist, create default settings
|
|
21
|
+
if result is None:
|
|
22
|
+
result = SettingTable()
|
|
23
|
+
session.add(result)
|
|
24
|
+
session.commit()
|
|
25
|
+
session.refresh(result)
|
|
26
|
+
|
|
27
|
+
return SettingView.model_validate(result)
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
def set_settings(session: Session, settings: SettingView) -> SettingView:
|
|
31
|
+
"""Update settings.
|
|
32
|
+
|
|
33
|
+
Args:
|
|
34
|
+
session: Database session.
|
|
35
|
+
settings: New settings to apply.
|
|
36
|
+
|
|
37
|
+
Returns:
|
|
38
|
+
Updated settings.
|
|
39
|
+
"""
|
|
40
|
+
current_settings = session.exec(select(SettingTable)).first()
|
|
41
|
+
if current_settings is None:
|
|
42
|
+
current_settings = SettingTable()
|
|
43
|
+
session.add(current_settings)
|
|
44
|
+
|
|
45
|
+
# Update grid view sample rendering
|
|
46
|
+
current_settings.grid_view_sample_rendering = settings.grid_view_sample_rendering
|
|
47
|
+
|
|
48
|
+
# Update keyboard shortcut mapping
|
|
49
|
+
current_settings.key_hide_annotations = settings.key_hide_annotations
|
|
50
|
+
current_settings.key_go_back = settings.key_go_back
|
|
51
|
+
|
|
52
|
+
# Update show annotation text labels
|
|
53
|
+
current_settings.show_annotation_text_labels = settings.show_annotation_text_labels
|
|
54
|
+
|
|
55
|
+
session.commit()
|
|
56
|
+
session.refresh(current_settings)
|
|
57
|
+
|
|
58
|
+
return SettingView.model_validate(current_settings)
|