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.

Files changed (219) hide show
  1. lightly_studio/__init__.py +11 -0
  2. lightly_studio/api/__init__.py +0 -0
  3. lightly_studio/api/app.py +110 -0
  4. lightly_studio/api/cache.py +77 -0
  5. lightly_studio/api/db.py +133 -0
  6. lightly_studio/api/db_tables.py +32 -0
  7. lightly_studio/api/features.py +7 -0
  8. lightly_studio/api/routes/api/annotation.py +233 -0
  9. lightly_studio/api/routes/api/annotation_label.py +90 -0
  10. lightly_studio/api/routes/api/annotation_task.py +38 -0
  11. lightly_studio/api/routes/api/classifier.py +387 -0
  12. lightly_studio/api/routes/api/dataset.py +182 -0
  13. lightly_studio/api/routes/api/dataset_tag.py +257 -0
  14. lightly_studio/api/routes/api/exceptions.py +96 -0
  15. lightly_studio/api/routes/api/features.py +17 -0
  16. lightly_studio/api/routes/api/metadata.py +37 -0
  17. lightly_studio/api/routes/api/metrics.py +80 -0
  18. lightly_studio/api/routes/api/sample.py +196 -0
  19. lightly_studio/api/routes/api/settings.py +45 -0
  20. lightly_studio/api/routes/api/status.py +19 -0
  21. lightly_studio/api/routes/api/text_embedding.py +48 -0
  22. lightly_studio/api/routes/api/validators.py +17 -0
  23. lightly_studio/api/routes/healthz.py +13 -0
  24. lightly_studio/api/routes/images.py +104 -0
  25. lightly_studio/api/routes/webapp.py +51 -0
  26. lightly_studio/api/server.py +82 -0
  27. lightly_studio/core/__init__.py +0 -0
  28. lightly_studio/core/dataset.py +523 -0
  29. lightly_studio/core/sample.py +77 -0
  30. lightly_studio/core/start_gui.py +15 -0
  31. lightly_studio/dataset/__init__.py +0 -0
  32. lightly_studio/dataset/edge_embedding_generator.py +144 -0
  33. lightly_studio/dataset/embedding_generator.py +91 -0
  34. lightly_studio/dataset/embedding_manager.py +163 -0
  35. lightly_studio/dataset/env.py +16 -0
  36. lightly_studio/dataset/file_utils.py +35 -0
  37. lightly_studio/dataset/loader.py +622 -0
  38. lightly_studio/dataset/mobileclip_embedding_generator.py +144 -0
  39. lightly_studio/dist_lightly_studio_view_app/_app/env.js +1 -0
  40. lightly_studio/dist_lightly_studio_view_app/_app/immutable/assets/0.DenzbfeK.css +1 -0
  41. lightly_studio/dist_lightly_studio_view_app/_app/immutable/assets/LightlyLogo.BNjCIww-.png +0 -0
  42. lightly_studio/dist_lightly_studio_view_app/_app/immutable/assets/OpenSans- +0 -0
  43. lightly_studio/dist_lightly_studio_view_app/_app/immutable/assets/OpenSans-Bold.DGvYQtcs.ttf +0 -0
  44. lightly_studio/dist_lightly_studio_view_app/_app/immutable/assets/OpenSans-Italic-VariableFont_wdth_wght.B4AZ-wl6.ttf +0 -0
  45. lightly_studio/dist_lightly_studio_view_app/_app/immutable/assets/OpenSans-Regular.DxJTClRG.ttf +0 -0
  46. lightly_studio/dist_lightly_studio_view_app/_app/immutable/assets/OpenSans-SemiBold.D3TTYgdB.ttf +0 -0
  47. lightly_studio/dist_lightly_studio_view_app/_app/immutable/assets/OpenSans-VariableFont_wdth_wght.BZBpG5Iz.ttf +0 -0
  48. lightly_studio/dist_lightly_studio_view_app/_app/immutable/assets/SelectableSvgGroup.OwPEPQZu.css +1 -0
  49. lightly_studio/dist_lightly_studio_view_app/_app/immutable/assets/SelectableSvgGroup.b653GmVf.css +1 -0
  50. lightly_studio/dist_lightly_studio_view_app/_app/immutable/assets/_layout.T-zjSUd3.css +1 -0
  51. lightly_studio/dist_lightly_studio_view_app/_app/immutable/assets/useFeatureFlags.CV-KWLNP.css +1 -0
  52. lightly_studio/dist_lightly_studio_view_app/_app/immutable/chunks/69_IOA4Y.js +1 -0
  53. lightly_studio/dist_lightly_studio_view_app/_app/immutable/chunks/B2FVR0s0.js +1 -0
  54. lightly_studio/dist_lightly_studio_view_app/_app/immutable/chunks/B90CZVMX.js +1 -0
  55. lightly_studio/dist_lightly_studio_view_app/_app/immutable/chunks/B9zumHo5.js +1 -0
  56. lightly_studio/dist_lightly_studio_view_app/_app/immutable/chunks/BJXwVxaE.js +1 -0
  57. lightly_studio/dist_lightly_studio_view_app/_app/immutable/chunks/Bsi3UGy5.js +1 -0
  58. lightly_studio/dist_lightly_studio_view_app/_app/immutable/chunks/Bu7uvVrG.js +1 -0
  59. lightly_studio/dist_lightly_studio_view_app/_app/immutable/chunks/Bx1xMsFy.js +1 -0
  60. lightly_studio/dist_lightly_studio_view_app/_app/immutable/chunks/BylOuP6i.js +1 -0
  61. lightly_studio/dist_lightly_studio_view_app/_app/immutable/chunks/C8I8rFJQ.js +1 -0
  62. lightly_studio/dist_lightly_studio_view_app/_app/immutable/chunks/CDnpyLsT.js +1 -0
  63. lightly_studio/dist_lightly_studio_view_app/_app/immutable/chunks/CWj6FrbW.js +1 -0
  64. lightly_studio/dist_lightly_studio_view_app/_app/immutable/chunks/CYgJF_JY.js +1 -0
  65. lightly_studio/dist_lightly_studio_view_app/_app/immutable/chunks/CcaPhhk3.js +1 -0
  66. lightly_studio/dist_lightly_studio_view_app/_app/immutable/chunks/CvOmgdoc.js +93 -0
  67. lightly_studio/dist_lightly_studio_view_app/_app/immutable/chunks/CxtLVaYz.js +3 -0
  68. lightly_studio/dist_lightly_studio_view_app/_app/immutable/chunks/D5-A_Ffd.js +4 -0
  69. lightly_studio/dist_lightly_studio_view_app/_app/immutable/chunks/D6RI2Zrd.js +1 -0
  70. lightly_studio/dist_lightly_studio_view_app/_app/immutable/chunks/D6su9Aln.js +1 -0
  71. lightly_studio/dist_lightly_studio_view_app/_app/immutable/chunks/D98V7j6A.js +1 -0
  72. lightly_studio/dist_lightly_studio_view_app/_app/immutable/chunks/DIRAtgl0.js +1 -0
  73. lightly_studio/dist_lightly_studio_view_app/_app/immutable/chunks/DIeogL5L.js +1 -0
  74. lightly_studio/dist_lightly_studio_view_app/_app/immutable/chunks/DOlTMNyt.js +1 -0
  75. lightly_studio/dist_lightly_studio_view_app/_app/immutable/chunks/DjUWrjOv.js +1 -0
  76. lightly_studio/dist_lightly_studio_view_app/_app/immutable/chunks/DjfY96ND.js +1 -0
  77. lightly_studio/dist_lightly_studio_view_app/_app/immutable/chunks/H7C68rOM.js +1 -0
  78. lightly_studio/dist_lightly_studio_view_app/_app/immutable/chunks/O-EABkf9.js +1 -0
  79. lightly_studio/dist_lightly_studio_view_app/_app/immutable/chunks/XO7A28GO.js +1 -0
  80. lightly_studio/dist_lightly_studio_view_app/_app/immutable/chunks/hQVEETDE.js +1 -0
  81. lightly_studio/dist_lightly_studio_view_app/_app/immutable/chunks/l7KrR96u.js +1 -0
  82. lightly_studio/dist_lightly_studio_view_app/_app/immutable/chunks/nAHhluT7.js +1 -0
  83. lightly_studio/dist_lightly_studio_view_app/_app/immutable/chunks/r64xT6ao.js +1 -0
  84. lightly_studio/dist_lightly_studio_view_app/_app/immutable/chunks/vC4nQVEB.js +1 -0
  85. lightly_studio/dist_lightly_studio_view_app/_app/immutable/chunks/x9G_hzyY.js +1 -0
  86. lightly_studio/dist_lightly_studio_view_app/_app/immutable/entry/app.CjnvpsmS.js +2 -0
  87. lightly_studio/dist_lightly_studio_view_app/_app/immutable/entry/start.0o1H7wM9.js +1 -0
  88. lightly_studio/dist_lightly_studio_view_app/_app/immutable/nodes/0.XRq_TUwu.js +1 -0
  89. lightly_studio/dist_lightly_studio_view_app/_app/immutable/nodes/1.B4rNYwVp.js +1 -0
  90. lightly_studio/dist_lightly_studio_view_app/_app/immutable/nodes/10.DfBwOEhN.js +1 -0
  91. lightly_studio/dist_lightly_studio_view_app/_app/immutable/nodes/11.CWG1ehzT.js +1 -0
  92. lightly_studio/dist_lightly_studio_view_app/_app/immutable/nodes/12.CwF2_8mP.js +1 -0
  93. lightly_studio/dist_lightly_studio_view_app/_app/immutable/nodes/2.CS4muRY-.js +6 -0
  94. lightly_studio/dist_lightly_studio_view_app/_app/immutable/nodes/3.CWHpKonm.js +1 -0
  95. lightly_studio/dist_lightly_studio_view_app/_app/immutable/nodes/4.OUWOLQeV.js +1 -0
  96. lightly_studio/dist_lightly_studio_view_app/_app/immutable/nodes/5.Dm6t9F5W.js +1 -0
  97. lightly_studio/dist_lightly_studio_view_app/_app/immutable/nodes/6.Bw5ck4gK.js +1 -0
  98. lightly_studio/dist_lightly_studio_view_app/_app/immutable/nodes/7.CF0EDTR6.js +1 -0
  99. lightly_studio/dist_lightly_studio_view_app/_app/immutable/nodes/8.Cw30LEcV.js +1 -0
  100. lightly_studio/dist_lightly_studio_view_app/_app/immutable/nodes/9.CPu3CiBc.js +1 -0
  101. lightly_studio/dist_lightly_studio_view_app/_app/version.json +1 -0
  102. lightly_studio/dist_lightly_studio_view_app/apple-touch-icon-precomposed.png +0 -0
  103. lightly_studio/dist_lightly_studio_view_app/apple-touch-icon.png +0 -0
  104. lightly_studio/dist_lightly_studio_view_app/favicon.png +0 -0
  105. lightly_studio/dist_lightly_studio_view_app/index.html +44 -0
  106. lightly_studio/examples/example.py +23 -0
  107. lightly_studio/examples/example_metadata.py +338 -0
  108. lightly_studio/examples/example_selection.py +39 -0
  109. lightly_studio/examples/example_split_work.py +67 -0
  110. lightly_studio/examples/example_v2.py +21 -0
  111. lightly_studio/export_schema.py +18 -0
  112. lightly_studio/few_shot_classifier/__init__.py +0 -0
  113. lightly_studio/few_shot_classifier/classifier.py +80 -0
  114. lightly_studio/few_shot_classifier/classifier_manager.py +663 -0
  115. lightly_studio/few_shot_classifier/random_forest_classifier.py +489 -0
  116. lightly_studio/metadata/complex_metadata.py +47 -0
  117. lightly_studio/metadata/gps_coordinate.py +41 -0
  118. lightly_studio/metadata/metadata_protocol.py +17 -0
  119. lightly_studio/metrics/__init__.py +0 -0
  120. lightly_studio/metrics/detection/__init__.py +0 -0
  121. lightly_studio/metrics/detection/map.py +268 -0
  122. lightly_studio/models/__init__.py +1 -0
  123. lightly_studio/models/annotation/__init__.py +0 -0
  124. lightly_studio/models/annotation/annotation_base.py +171 -0
  125. lightly_studio/models/annotation/instance_segmentation.py +56 -0
  126. lightly_studio/models/annotation/links.py +17 -0
  127. lightly_studio/models/annotation/object_detection.py +47 -0
  128. lightly_studio/models/annotation/semantic_segmentation.py +44 -0
  129. lightly_studio/models/annotation_label.py +47 -0
  130. lightly_studio/models/annotation_task.py +28 -0
  131. lightly_studio/models/classifier.py +20 -0
  132. lightly_studio/models/dataset.py +84 -0
  133. lightly_studio/models/embedding_model.py +30 -0
  134. lightly_studio/models/metadata.py +208 -0
  135. lightly_studio/models/sample.py +180 -0
  136. lightly_studio/models/sample_embedding.py +37 -0
  137. lightly_studio/models/settings.py +60 -0
  138. lightly_studio/models/tag.py +96 -0
  139. lightly_studio/py.typed +0 -0
  140. lightly_studio/resolvers/__init__.py +7 -0
  141. lightly_studio/resolvers/annotation_label_resolver/__init__.py +21 -0
  142. lightly_studio/resolvers/annotation_label_resolver/create.py +27 -0
  143. lightly_studio/resolvers/annotation_label_resolver/delete.py +28 -0
  144. lightly_studio/resolvers/annotation_label_resolver/get_all.py +22 -0
  145. lightly_studio/resolvers/annotation_label_resolver/get_by_id.py +24 -0
  146. lightly_studio/resolvers/annotation_label_resolver/get_by_ids.py +25 -0
  147. lightly_studio/resolvers/annotation_label_resolver/get_by_label_name.py +24 -0
  148. lightly_studio/resolvers/annotation_label_resolver/names_by_ids.py +25 -0
  149. lightly_studio/resolvers/annotation_label_resolver/update.py +38 -0
  150. lightly_studio/resolvers/annotation_resolver/__init__.py +33 -0
  151. lightly_studio/resolvers/annotation_resolver/count_annotations_by_dataset.py +120 -0
  152. lightly_studio/resolvers/annotation_resolver/create.py +19 -0
  153. lightly_studio/resolvers/annotation_resolver/create_many.py +96 -0
  154. lightly_studio/resolvers/annotation_resolver/delete_annotation.py +45 -0
  155. lightly_studio/resolvers/annotation_resolver/delete_annotations.py +56 -0
  156. lightly_studio/resolvers/annotation_resolver/get_all.py +74 -0
  157. lightly_studio/resolvers/annotation_resolver/get_by_id.py +18 -0
  158. lightly_studio/resolvers/annotation_resolver/update_annotation_label.py +144 -0
  159. lightly_studio/resolvers/annotation_resolver/update_bounding_box.py +68 -0
  160. lightly_studio/resolvers/annotation_task_resolver.py +31 -0
  161. lightly_studio/resolvers/annotations/__init__.py +1 -0
  162. lightly_studio/resolvers/annotations/annotations_filter.py +89 -0
  163. lightly_studio/resolvers/dataset_resolver.py +278 -0
  164. lightly_studio/resolvers/embedding_model_resolver.py +100 -0
  165. lightly_studio/resolvers/metadata_resolver/__init__.py +15 -0
  166. lightly_studio/resolvers/metadata_resolver/metadata_filter.py +163 -0
  167. lightly_studio/resolvers/metadata_resolver/sample/__init__.py +21 -0
  168. lightly_studio/resolvers/metadata_resolver/sample/bulk_set_metadata.py +48 -0
  169. lightly_studio/resolvers/metadata_resolver/sample/get_by_sample_id.py +24 -0
  170. lightly_studio/resolvers/metadata_resolver/sample/get_metadata_info.py +104 -0
  171. lightly_studio/resolvers/metadata_resolver/sample/get_value_for_sample.py +27 -0
  172. lightly_studio/resolvers/metadata_resolver/sample/set_value_for_sample.py +53 -0
  173. lightly_studio/resolvers/sample_embedding_resolver.py +86 -0
  174. lightly_studio/resolvers/sample_resolver.py +249 -0
  175. lightly_studio/resolvers/samples_filter.py +81 -0
  176. lightly_studio/resolvers/settings_resolver.py +58 -0
  177. lightly_studio/resolvers/tag_resolver.py +276 -0
  178. lightly_studio/selection/README.md +6 -0
  179. lightly_studio/selection/mundig.py +105 -0
  180. lightly_studio/selection/select.py +96 -0
  181. lightly_studio/selection/select_via_db.py +93 -0
  182. lightly_studio/selection/selection_config.py +31 -0
  183. lightly_studio/services/annotations_service/__init__.py +21 -0
  184. lightly_studio/services/annotations_service/get_annotation_by_id.py +31 -0
  185. lightly_studio/services/annotations_service/update_annotation.py +65 -0
  186. lightly_studio/services/annotations_service/update_annotation_label.py +48 -0
  187. lightly_studio/services/annotations_service/update_annotations.py +29 -0
  188. lightly_studio/setup_logging.py +19 -0
  189. lightly_studio/type_definitions.py +19 -0
  190. lightly_studio/vendor/ACKNOWLEDGEMENTS +422 -0
  191. lightly_studio/vendor/LICENSE +31 -0
  192. lightly_studio/vendor/LICENSE_weights_data +50 -0
  193. lightly_studio/vendor/README.md +5 -0
  194. lightly_studio/vendor/__init__.py +1 -0
  195. lightly_studio/vendor/mobileclip/__init__.py +96 -0
  196. lightly_studio/vendor/mobileclip/clip.py +77 -0
  197. lightly_studio/vendor/mobileclip/configs/mobileclip_b.json +18 -0
  198. lightly_studio/vendor/mobileclip/configs/mobileclip_s0.json +18 -0
  199. lightly_studio/vendor/mobileclip/configs/mobileclip_s1.json +18 -0
  200. lightly_studio/vendor/mobileclip/configs/mobileclip_s2.json +18 -0
  201. lightly_studio/vendor/mobileclip/image_encoder.py +67 -0
  202. lightly_studio/vendor/mobileclip/logger.py +154 -0
  203. lightly_studio/vendor/mobileclip/models/__init__.py +10 -0
  204. lightly_studio/vendor/mobileclip/models/mci.py +933 -0
  205. lightly_studio/vendor/mobileclip/models/vit.py +433 -0
  206. lightly_studio/vendor/mobileclip/modules/__init__.py +4 -0
  207. lightly_studio/vendor/mobileclip/modules/common/__init__.py +4 -0
  208. lightly_studio/vendor/mobileclip/modules/common/mobileone.py +341 -0
  209. lightly_studio/vendor/mobileclip/modules/common/transformer.py +451 -0
  210. lightly_studio/vendor/mobileclip/modules/image/__init__.py +4 -0
  211. lightly_studio/vendor/mobileclip/modules/image/image_projection.py +113 -0
  212. lightly_studio/vendor/mobileclip/modules/image/replknet.py +188 -0
  213. lightly_studio/vendor/mobileclip/modules/text/__init__.py +4 -0
  214. lightly_studio/vendor/mobileclip/modules/text/repmixer.py +281 -0
  215. lightly_studio/vendor/mobileclip/modules/text/tokenizer.py +38 -0
  216. lightly_studio/vendor/mobileclip/text_encoder.py +245 -0
  217. lightly_studio-0.3.1.dist-info/METADATA +520 -0
  218. lightly_studio-0.3.1.dist-info/RECORD +219 -0
  219. 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()