lightly-studio 0.3.1__py3-none-any.whl → 0.3.3__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 (169) hide show
  1. lightly_studio/__init__.py +4 -4
  2. lightly_studio/api/app.py +7 -5
  3. lightly_studio/api/db_tables.py +0 -3
  4. lightly_studio/api/routes/api/annotation.py +32 -16
  5. lightly_studio/api/routes/api/annotation_label.py +2 -5
  6. lightly_studio/api/routes/api/annotations/__init__.py +7 -0
  7. lightly_studio/api/routes/api/annotations/create_annotation.py +52 -0
  8. lightly_studio/api/routes/api/classifier.py +2 -5
  9. lightly_studio/api/routes/api/dataset.py +5 -8
  10. lightly_studio/api/routes/api/dataset_tag.py +2 -3
  11. lightly_studio/api/routes/api/embeddings2d.py +104 -0
  12. lightly_studio/api/routes/api/export.py +73 -0
  13. lightly_studio/api/routes/api/metadata.py +2 -4
  14. lightly_studio/api/routes/api/sample.py +5 -13
  15. lightly_studio/api/routes/api/selection.py +87 -0
  16. lightly_studio/api/routes/api/settings.py +2 -6
  17. lightly_studio/api/routes/images.py +6 -6
  18. lightly_studio/core/add_samples.py +374 -0
  19. lightly_studio/core/dataset.py +272 -400
  20. lightly_studio/core/dataset_query/boolean_expression.py +67 -0
  21. lightly_studio/core/dataset_query/dataset_query.py +216 -0
  22. lightly_studio/core/dataset_query/field.py +113 -0
  23. lightly_studio/core/dataset_query/field_expression.py +79 -0
  24. lightly_studio/core/dataset_query/match_expression.py +23 -0
  25. lightly_studio/core/dataset_query/order_by.py +79 -0
  26. lightly_studio/core/dataset_query/sample_field.py +28 -0
  27. lightly_studio/core/dataset_query/tags_expression.py +46 -0
  28. lightly_studio/core/sample.py +159 -32
  29. lightly_studio/core/start_gui.py +35 -0
  30. lightly_studio/dataset/edge_embedding_generator.py +13 -8
  31. lightly_studio/dataset/embedding_generator.py +2 -3
  32. lightly_studio/dataset/embedding_manager.py +74 -6
  33. lightly_studio/dataset/env.py +4 -0
  34. lightly_studio/dataset/file_utils.py +13 -2
  35. lightly_studio/dataset/fsspec_lister.py +275 -0
  36. lightly_studio/dataset/loader.py +49 -84
  37. lightly_studio/dataset/mobileclip_embedding_generator.py +9 -6
  38. lightly_studio/db_manager.py +145 -0
  39. lightly_studio/dist_lightly_studio_view_app/_app/immutable/assets/0.CA_CXIBb.css +1 -0
  40. lightly_studio/dist_lightly_studio_view_app/_app/immutable/assets/_layout.DS78jgNY.css +1 -0
  41. lightly_studio/dist_lightly_studio_view_app/_app/immutable/assets/index.BVs_sZj9.css +1 -0
  42. lightly_studio/dist_lightly_studio_view_app/_app/immutable/assets/transform.D487hwJk.css +1 -0
  43. lightly_studio/dist_lightly_studio_view_app/_app/immutable/chunks/6t3IJ0vQ.js +1 -0
  44. lightly_studio/dist_lightly_studio_view_app/_app/immutable/chunks/{D6su9Aln.js → 8NsknIT2.js} +1 -1
  45. lightly_studio/dist_lightly_studio_view_app/_app/immutable/chunks/{x9G_hzyY.js → BND_-4Kp.js} +1 -1
  46. lightly_studio/dist_lightly_studio_view_app/_app/immutable/chunks/{BylOuP6i.js → BdfTHw61.js} +1 -1
  47. lightly_studio/dist_lightly_studio_view_app/_app/immutable/chunks/{DOlTMNyt.js → BfHVnyNT.js} +1 -1
  48. lightly_studio/dist_lightly_studio_view_app/_app/immutable/chunks/BjkP1AHA.js +1 -0
  49. lightly_studio/dist_lightly_studio_view_app/_app/immutable/chunks/BuuNVL9G.js +1 -0
  50. lightly_studio/dist_lightly_studio_view_app/_app/immutable/chunks/{O-EABkf9.js → BzKGpnl4.js} +1 -1
  51. lightly_studio/dist_lightly_studio_view_app/_app/immutable/chunks/CCx7Ho51.js +1 -0
  52. lightly_studio/dist_lightly_studio_view_app/_app/immutable/chunks/{l7KrR96u.js → CH6P3X75.js} +1 -1
  53. lightly_studio/dist_lightly_studio_view_app/_app/immutable/chunks/{D5-A_Ffd.js → CR2upx_Q.js} +2 -2
  54. lightly_studio/dist_lightly_studio_view_app/_app/immutable/chunks/CWPZrTTJ.js +1 -0
  55. lightly_studio/dist_lightly_studio_view_app/_app/immutable/chunks/{C8I8rFJQ.js → Cs1XmhiF.js} +1 -1
  56. lightly_studio/dist_lightly_studio_view_app/_app/immutable/chunks/{CDnpyLsT.js → CwPowJfP.js} +1 -1
  57. lightly_studio/dist_lightly_studio_view_app/_app/immutable/chunks/CxFKfZ9T.js +1 -0
  58. lightly_studio/dist_lightly_studio_view_app/_app/immutable/chunks/Cxevwdid.js +1 -0
  59. lightly_studio/dist_lightly_studio_view_app/_app/immutable/chunks/{DjfY96ND.js → D4whDBUi.js} +1 -1
  60. lightly_studio/dist_lightly_studio_view_app/_app/immutable/chunks/D6r9vr07.js +1 -0
  61. lightly_studio/dist_lightly_studio_view_app/_app/immutable/chunks/DA6bFLPR.js +1 -0
  62. lightly_studio/dist_lightly_studio_view_app/_app/immutable/chunks/DEgUu98i.js +3 -0
  63. lightly_studio/dist_lightly_studio_view_app/_app/immutable/chunks/DGTPl6Gk.js +1 -0
  64. lightly_studio/dist_lightly_studio_view_app/_app/immutable/chunks/DKGxBSlK.js +1 -0
  65. lightly_studio/dist_lightly_studio_view_app/_app/immutable/chunks/DQXoLcsF.js +1 -0
  66. lightly_studio/dist_lightly_studio_view_app/_app/immutable/chunks/DQe_kdRt.js +92 -0
  67. lightly_studio/dist_lightly_studio_view_app/_app/immutable/chunks/DcY4jgG3.js +1 -0
  68. lightly_studio/dist_lightly_studio_view_app/_app/immutable/chunks/{Bu7uvVrG.js → RmD8FzRo.js} +1 -1
  69. lightly_studio/dist_lightly_studio_view_app/_app/immutable/chunks/V-MnMC1X.js +1 -0
  70. lightly_studio/dist_lightly_studio_view_app/_app/immutable/chunks/{Bsi3UGy5.js → keKYsoph.js} +1 -1
  71. lightly_studio/dist_lightly_studio_view_app/_app/immutable/entry/app.BVr6DYqP.js +2 -0
  72. lightly_studio/dist_lightly_studio_view_app/_app/immutable/entry/start.u7zsVvqp.js +1 -0
  73. lightly_studio/dist_lightly_studio_view_app/_app/immutable/nodes/0.Da2agmdd.js +1 -0
  74. lightly_studio/dist_lightly_studio_view_app/_app/immutable/nodes/{1.B4rNYwVp.js → 1.B11tVRJV.js} +1 -1
  75. lightly_studio/dist_lightly_studio_view_app/_app/immutable/nodes/10.l30Zud4h.js +1 -0
  76. lightly_studio/dist_lightly_studio_view_app/_app/immutable/nodes/12.CgKPGcAP.js +1 -0
  77. lightly_studio/dist_lightly_studio_view_app/_app/immutable/nodes/2.C8HLK8mj.js +857 -0
  78. lightly_studio/dist_lightly_studio_view_app/_app/immutable/nodes/{3.CWHpKonm.js → 3.CLvg3QcJ.js} +1 -1
  79. lightly_studio/dist_lightly_studio_view_app/_app/immutable/nodes/{4.OUWOLQeV.js → 4.BQhDtXUI.js} +1 -1
  80. lightly_studio/dist_lightly_studio_view_app/_app/immutable/nodes/5.-6XqWX5G.js +1 -0
  81. lightly_studio/dist_lightly_studio_view_app/_app/immutable/nodes/6.uBV1Lhat.js +1 -0
  82. lightly_studio/dist_lightly_studio_view_app/_app/immutable/nodes/7.BXsgoQZh.js +1 -0
  83. lightly_studio/dist_lightly_studio_view_app/_app/immutable/nodes/8.BkbcnUs8.js +1 -0
  84. lightly_studio/dist_lightly_studio_view_app/_app/immutable/nodes/{9.CPu3CiBc.js → 9.Bkrv-Vww.js} +1 -1
  85. lightly_studio/dist_lightly_studio_view_app/_app/immutable/workers/clustering.worker-DKqeLtG0.js +2 -0
  86. lightly_studio/dist_lightly_studio_view_app/_app/immutable/workers/search.worker-vNSty3B0.js +1 -0
  87. lightly_studio/dist_lightly_studio_view_app/_app/version.json +1 -1
  88. lightly_studio/dist_lightly_studio_view_app/index.html +14 -14
  89. lightly_studio/examples/example.py +13 -12
  90. lightly_studio/examples/example_coco.py +13 -0
  91. lightly_studio/examples/example_metadata.py +83 -98
  92. lightly_studio/examples/example_selection.py +7 -19
  93. lightly_studio/examples/example_split_work.py +12 -36
  94. lightly_studio/examples/{example_v2.py → example_yolo.py} +3 -4
  95. lightly_studio/export/export_dataset.py +65 -0
  96. lightly_studio/export/lightly_studio_label_input.py +120 -0
  97. lightly_studio/few_shot_classifier/classifier_manager.py +5 -26
  98. lightly_studio/metadata/compute_typicality.py +67 -0
  99. lightly_studio/models/annotation/annotation_base.py +18 -20
  100. lightly_studio/models/annotation/instance_segmentation.py +8 -8
  101. lightly_studio/models/annotation/object_detection.py +4 -4
  102. lightly_studio/models/dataset.py +6 -2
  103. lightly_studio/models/sample.py +10 -3
  104. lightly_studio/resolvers/annotation_label_resolver/__init__.py +2 -1
  105. lightly_studio/resolvers/annotation_label_resolver/get_all.py +15 -0
  106. lightly_studio/resolvers/annotation_resolver/__init__.py +2 -3
  107. lightly_studio/resolvers/annotation_resolver/create_many.py +3 -3
  108. lightly_studio/resolvers/annotation_resolver/delete_annotation.py +1 -1
  109. lightly_studio/resolvers/annotation_resolver/delete_annotations.py +7 -3
  110. lightly_studio/resolvers/annotation_resolver/get_by_id.py +19 -1
  111. lightly_studio/resolvers/annotation_resolver/update_annotation_label.py +0 -1
  112. lightly_studio/resolvers/annotations/annotations_filter.py +1 -11
  113. lightly_studio/resolvers/dataset_resolver.py +10 -0
  114. lightly_studio/resolvers/embedding_model_resolver.py +22 -0
  115. lightly_studio/resolvers/sample_resolver.py +53 -9
  116. lightly_studio/resolvers/tag_resolver.py +23 -0
  117. lightly_studio/selection/mundig.py +7 -10
  118. lightly_studio/selection/select.py +55 -46
  119. lightly_studio/selection/select_via_db.py +23 -19
  120. lightly_studio/selection/selection_config.py +10 -4
  121. lightly_studio/services/annotations_service/__init__.py +12 -0
  122. lightly_studio/services/annotations_service/create_annotation.py +63 -0
  123. lightly_studio/services/annotations_service/delete_annotation.py +22 -0
  124. lightly_studio/services/annotations_service/update_annotation.py +21 -32
  125. lightly_studio/services/annotations_service/update_annotation_bounding_box.py +36 -0
  126. lightly_studio-0.3.3.dist-info/METADATA +814 -0
  127. {lightly_studio-0.3.1.dist-info → lightly_studio-0.3.3.dist-info}/RECORD +130 -113
  128. lightly_studio/api/db.py +0 -133
  129. lightly_studio/api/routes/api/annotation_task.py +0 -38
  130. lightly_studio/api/routes/api/metrics.py +0 -80
  131. lightly_studio/dist_lightly_studio_view_app/_app/immutable/assets/0.DenzbfeK.css +0 -1
  132. lightly_studio/dist_lightly_studio_view_app/_app/immutable/assets/SelectableSvgGroup.OwPEPQZu.css +0 -1
  133. lightly_studio/dist_lightly_studio_view_app/_app/immutable/assets/SelectableSvgGroup.b653GmVf.css +0 -1
  134. lightly_studio/dist_lightly_studio_view_app/_app/immutable/assets/_layout.T-zjSUd3.css +0 -1
  135. lightly_studio/dist_lightly_studio_view_app/_app/immutable/chunks/B2FVR0s0.js +0 -1
  136. lightly_studio/dist_lightly_studio_view_app/_app/immutable/chunks/B9zumHo5.js +0 -1
  137. lightly_studio/dist_lightly_studio_view_app/_app/immutable/chunks/BJXwVxaE.js +0 -1
  138. lightly_studio/dist_lightly_studio_view_app/_app/immutable/chunks/Bx1xMsFy.js +0 -1
  139. lightly_studio/dist_lightly_studio_view_app/_app/immutable/chunks/CcaPhhk3.js +0 -1
  140. lightly_studio/dist_lightly_studio_view_app/_app/immutable/chunks/CvOmgdoc.js +0 -93
  141. lightly_studio/dist_lightly_studio_view_app/_app/immutable/chunks/CxtLVaYz.js +0 -3
  142. lightly_studio/dist_lightly_studio_view_app/_app/immutable/chunks/D6RI2Zrd.js +0 -1
  143. lightly_studio/dist_lightly_studio_view_app/_app/immutable/chunks/D98V7j6A.js +0 -1
  144. lightly_studio/dist_lightly_studio_view_app/_app/immutable/chunks/DIRAtgl0.js +0 -1
  145. lightly_studio/dist_lightly_studio_view_app/_app/immutable/chunks/DjUWrjOv.js +0 -1
  146. lightly_studio/dist_lightly_studio_view_app/_app/immutable/chunks/XO7A28GO.js +0 -1
  147. lightly_studio/dist_lightly_studio_view_app/_app/immutable/chunks/hQVEETDE.js +0 -1
  148. lightly_studio/dist_lightly_studio_view_app/_app/immutable/chunks/nAHhluT7.js +0 -1
  149. lightly_studio/dist_lightly_studio_view_app/_app/immutable/chunks/r64xT6ao.js +0 -1
  150. lightly_studio/dist_lightly_studio_view_app/_app/immutable/chunks/vC4nQVEB.js +0 -1
  151. lightly_studio/dist_lightly_studio_view_app/_app/immutable/entry/app.CjnvpsmS.js +0 -2
  152. lightly_studio/dist_lightly_studio_view_app/_app/immutable/entry/start.0o1H7wM9.js +0 -1
  153. lightly_studio/dist_lightly_studio_view_app/_app/immutable/nodes/0.XRq_TUwu.js +0 -1
  154. lightly_studio/dist_lightly_studio_view_app/_app/immutable/nodes/10.DfBwOEhN.js +0 -1
  155. lightly_studio/dist_lightly_studio_view_app/_app/immutable/nodes/12.CwF2_8mP.js +0 -1
  156. lightly_studio/dist_lightly_studio_view_app/_app/immutable/nodes/2.CS4muRY-.js +0 -6
  157. lightly_studio/dist_lightly_studio_view_app/_app/immutable/nodes/5.Dm6t9F5W.js +0 -1
  158. lightly_studio/dist_lightly_studio_view_app/_app/immutable/nodes/6.Bw5ck4gK.js +0 -1
  159. lightly_studio/dist_lightly_studio_view_app/_app/immutable/nodes/7.CF0EDTR6.js +0 -1
  160. lightly_studio/dist_lightly_studio_view_app/_app/immutable/nodes/8.Cw30LEcV.js +0 -1
  161. lightly_studio/metrics/detection/__init__.py +0 -0
  162. lightly_studio/metrics/detection/map.py +0 -268
  163. lightly_studio/models/annotation_task.py +0 -28
  164. lightly_studio/resolvers/annotation_resolver/create.py +0 -19
  165. lightly_studio/resolvers/annotation_task_resolver.py +0 -31
  166. lightly_studio-0.3.1.dist-info/METADATA +0 -520
  167. /lightly_studio/{metrics → core/dataset_query}/__init__.py +0 -0
  168. /lightly_studio/dist_lightly_studio_view_app/_app/immutable/assets/{OpenSans- → OpenSans-Medium.DVUZMR_6.ttf} +0 -0
  169. {lightly_studio-0.3.1.dist-info → lightly_studio-0.3.3.dist-info}/WHEEL +0 -0
@@ -0,0 +1,67 @@
1
+ """Classes for boolean expressions in dataset queries."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from sqlalchemy import ColumnElement, and_, false, not_, or_, true
6
+
7
+ from lightly_studio.core.dataset_query.match_expression import MatchExpression
8
+
9
+
10
+ class AND(MatchExpression):
11
+ """Logical AND operation between other MatchExpression objects."""
12
+
13
+ def __init__(self, *terms: MatchExpression) -> None:
14
+ """Initialize AND Expression with multiple MatchExpression terms.
15
+
16
+ Args:
17
+ terms: The MatchExpression instances to combine with AND. They can also be nested.
18
+ """
19
+ self.terms = terms
20
+
21
+ def get(self) -> ColumnElement[bool]:
22
+ """Combine expressions of all terms using AND.
23
+
24
+ Returns:
25
+ The combined SQLAlchemy expression.
26
+ """
27
+ return and_(true(), *(term.get() for term in self.terms))
28
+
29
+
30
+ class OR(MatchExpression):
31
+ """Logical OR operation between other MatchExpression objects."""
32
+
33
+ def __init__(self, *terms: MatchExpression) -> None:
34
+ """Initialize OR Expression with multiple MatchExpression terms.
35
+
36
+ Args:
37
+ terms: The MatchExpression instances to combine with OR. They can also be nested.
38
+ """
39
+ self.terms = terms
40
+
41
+ def get(self) -> ColumnElement[bool]:
42
+ """Combine expressions of all terms using OR.
43
+
44
+ Returns:
45
+ The combined SQLAlchemy expression.
46
+ """
47
+ return or_(false(), *(term.get() for term in self.terms))
48
+
49
+
50
+ class NOT(MatchExpression):
51
+ """Logical NOT operation for a MatchExpression object."""
52
+
53
+ def __init__(self, term: MatchExpression) -> None:
54
+ """Initialize NOT Expression with a MatchExpression term.
55
+
56
+ Args:
57
+ term: The MatchExpression to be negated. It can also be nested.
58
+ """
59
+ self.term = term
60
+
61
+ def get(self) -> ColumnElement[bool]:
62
+ """Negate the expression of the term using NOT.
63
+
64
+ Returns:
65
+ The combined SQLAlchemy expression.
66
+ """
67
+ return not_(self.term.get())
@@ -0,0 +1,216 @@
1
+ """Matching functionality for filtering database samples based on field conditions."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from typing import Iterator
6
+
7
+ from sqlmodel import Session, select
8
+
9
+ from lightly_studio.core.dataset_query.match_expression import MatchExpression
10
+ from lightly_studio.core.dataset_query.order_by import OrderByExpression, OrderByField
11
+ from lightly_studio.core.dataset_query.sample_field import SampleField
12
+ from lightly_studio.core.sample import Sample
13
+ from lightly_studio.export.export_dataset import DatasetExport
14
+ from lightly_studio.models.dataset import DatasetTable
15
+ from lightly_studio.models.sample import SampleTable
16
+ from lightly_studio.resolvers import tag_resolver
17
+ from lightly_studio.selection.select import Selection
18
+
19
+ _SliceType = slice # to avoid shadowing built-in slice in type annotations
20
+
21
+
22
+ class DatasetQuery:
23
+ """Class for executing querying on a dataset."""
24
+
25
+ def __init__(self, dataset: DatasetTable, session: Session) -> None:
26
+ """Initialize with dataset and database session.
27
+
28
+ Args:
29
+ dataset: The dataset to query.
30
+ session: Database session for executing queries.
31
+ """
32
+ self.dataset = dataset
33
+ self.session = session
34
+ self.match_expression: MatchExpression | None = None
35
+ self.order_by_expressions: list[OrderByExpression] | None = None
36
+ self._slice: _SliceType | None = None
37
+
38
+ def match(self, match_expression: MatchExpression) -> DatasetQuery:
39
+ """Store a field condition for filtering.
40
+
41
+ Args:
42
+ match_expression: Defines the filter.
43
+
44
+ Returns:
45
+ Self for method chaining.
46
+
47
+ Raises:
48
+ ValueError: If match() has already been called on this instance.
49
+ """
50
+ if self.match_expression is not None:
51
+ raise ValueError("match() can only be called once per DatasetQuery instance")
52
+
53
+ self.match_expression = match_expression
54
+ return self
55
+
56
+ def order_by(self, *order_by: OrderByExpression) -> DatasetQuery:
57
+ """Store ordering expressions.
58
+
59
+ Args:
60
+ order_by: One or more ordering expressions. They are applied in order.
61
+ E.g. first ordering by sample width and then by sample file_name will
62
+ only order the samples with the same sample width by file_name.
63
+
64
+ Returns:
65
+ Self for method chaining.
66
+
67
+ Raises:
68
+ ValueError: If order_by() has already been called on this instance.
69
+ """
70
+ if self.order_by_expressions:
71
+ raise ValueError("order_by() can only be called once per DatasetQuery instance")
72
+
73
+ self.order_by_expressions = list(order_by)
74
+ return self
75
+
76
+ def slice(self, offset: int = 0, limit: int | None = None) -> DatasetQuery:
77
+ """Apply offset and limit to results.
78
+
79
+ Args:
80
+ offset: Number of items to skip from beginning (default: 0).
81
+ limit: Maximum number of items to return (None = no limit).
82
+
83
+ Returns:
84
+ Self for method chaining.
85
+
86
+ Raises:
87
+ ValueError: If slice() has already been called on this instance.
88
+ """
89
+ if self._slice is not None:
90
+ raise ValueError("slice() can only be called once per DatasetQuery instance")
91
+
92
+ # Convert to slice object for internal consistency
93
+ stop = None if limit is None else offset + limit
94
+ self._slice = _SliceType(offset, stop)
95
+ return self
96
+
97
+ def __getitem__(self, key: _SliceType) -> DatasetQuery:
98
+ """Enable bracket notation for slicing.
99
+
100
+ Args:
101
+ key: A slice object (e.g., [10:20], [:50], [100:]).
102
+
103
+ Returns:
104
+ Self with slice applied.
105
+
106
+ Raises:
107
+ TypeError: If key is not a slice object.
108
+ ValueError: If slice contains unsupported features or conflicts with existing slice.
109
+ """
110
+ if not isinstance(key, _SliceType):
111
+ raise TypeError(
112
+ "DatasetQuery only supports slice notation, not integer indexing. "
113
+ "Use execute() to get results as a list for element access."
114
+ )
115
+
116
+ # Validate unsupported features
117
+ if key.step is not None:
118
+ raise ValueError("Strides are not supported. Use simple slices like [start:stop].")
119
+
120
+ if (key.start is not None and key.start < 0) or (key.stop is not None and key.stop < 0):
121
+ raise ValueError("Negative indices are not supported. Use positive indices only.")
122
+
123
+ # Check for conflicts with existing slice
124
+ if self._slice is not None:
125
+ raise ValueError("Cannot use bracket notation after slice() has been called.")
126
+
127
+ # Set slice and return self
128
+ self._slice = key
129
+ return self
130
+
131
+ def __iter__(self) -> Iterator[Sample]:
132
+ """Iterate over the query results.
133
+
134
+ Returns:
135
+ Iterator of Sample objects from the database.
136
+ """
137
+ # Build query
138
+ query = select(SampleTable).where(SampleTable.dataset_id == self.dataset.dataset_id)
139
+
140
+ # Apply filter if present
141
+ if self.match_expression:
142
+ query = query.where(self.match_expression.get())
143
+
144
+ # Apply ordering
145
+ if self.order_by_expressions:
146
+ for order_by in self.order_by_expressions:
147
+ query = order_by.apply(query)
148
+ else:
149
+ # Order by SampleField.created_at by default.
150
+ default_order_by = OrderByField(SampleField.created_at)
151
+ query = default_order_by.apply(query)
152
+
153
+ # Apply slicing if present
154
+ if self._slice is not None:
155
+ start = self._slice.start or 0
156
+ query = query.offset(start)
157
+ if self._slice.stop is not None:
158
+ limit = max(self._slice.stop - start, 0)
159
+ query = query.limit(limit)
160
+
161
+ # Execute query and yield results
162
+ for sample_table in self.session.exec(query):
163
+ yield Sample(inner=sample_table)
164
+
165
+ def to_list(self) -> list[Sample]:
166
+ """Execute the query and return the results as a list.
167
+
168
+ Returns:
169
+ List of Sample objects from the database.
170
+ """
171
+ return list(self)
172
+
173
+ def add_tag(self, tag_name: str) -> None:
174
+ """Add a tag to all samples returned by this query.
175
+
176
+ First, creates the tag if it doesn't exist. Then applies the tag to all samples
177
+ that match the current query filters. Samples already having that tag are unchanged,
178
+ as the database prevents duplicates.
179
+
180
+ Args:
181
+ tag_name: Name of the tag to add to matching samples.
182
+ """
183
+ # Get or create the tag
184
+ tag = tag_resolver.get_or_create_sample_tag_by_name(
185
+ session=self.session, dataset_id=self.dataset.dataset_id, tag_name=tag_name
186
+ )
187
+
188
+ # Execute query to get matching samples
189
+ samples = self.to_list()
190
+ sample_ids = [sample.sample_id for sample in samples]
191
+
192
+ # Use resolver to bulk assign tag (handles validation and edge cases)
193
+ tag_resolver.add_sample_ids_to_tag_id(
194
+ session=self.session, tag_id=tag.tag_id, sample_ids=sample_ids
195
+ )
196
+
197
+ def selection(self) -> Selection:
198
+ """Selection interface for this query.
199
+
200
+ The returned Selection snapshots the current query results immediately.
201
+ Mutating the query after calling this method will therefore not affect
202
+ the samples used by that Selection instance.
203
+
204
+ Returns:
205
+ Selection interface operating on the current query result snapshot.
206
+ """
207
+ input_sample_ids = (sample.sample_id for sample in self)
208
+ return Selection(
209
+ dataset_id=self.dataset.dataset_id,
210
+ session=self.session,
211
+ input_sample_ids=input_sample_ids,
212
+ )
213
+
214
+ def export(self) -> DatasetExport:
215
+ """Return a DatasetExport instance which can export the dataset in various formats."""
216
+ return DatasetExport(session=self.session, samples=self)
@@ -0,0 +1,113 @@
1
+ """Base field classes for building dataset queries."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from abc import ABC, abstractmethod
6
+ from datetime import datetime
7
+ from typing import Any, Generic, TypeVar, Union
8
+
9
+ from sqlalchemy.orm import Mapped
10
+
11
+ from lightly_studio.core.dataset_query.field_expression import (
12
+ OrdinalFieldExpression,
13
+ StringFieldExpression,
14
+ )
15
+
16
+ T = TypeVar("T")
17
+
18
+
19
+ class Field(ABC):
20
+ """Abstract base class for all field types in dataset queries."""
21
+
22
+ @abstractmethod
23
+ def get_sqlmodel_field(self) -> Mapped[Any]:
24
+ """Get the database column or property that this field represents.
25
+
26
+ Returns:
27
+ The database column or property for queries.
28
+ """
29
+
30
+
31
+ class OrdinalField(Field, Generic[T]):
32
+ """Generic field for ordinal values that support comparison operations.
33
+
34
+ Ordinal values have a natural ordering and support all comparison operators:
35
+ >, <, >=, <=, ==, !=
36
+ """
37
+
38
+ def __init__(self, column: Mapped[T]) -> None:
39
+ """Initialize the ordinal field with a database column.
40
+
41
+ Args:
42
+ column: The database column this field represents.
43
+ """
44
+ self._column = column
45
+
46
+ def get_sqlmodel_field(self) -> Mapped[T]:
47
+ """Get the ordinal database column or property.
48
+
49
+ Returns:
50
+ The ordinal column for database queries.
51
+ """
52
+ return self._column
53
+
54
+ def __gt__(self, other: T) -> OrdinalFieldExpression[T]:
55
+ """Create a greater-than expression."""
56
+ return OrdinalFieldExpression(field=self, operator=">", value=other)
57
+
58
+ def __lt__(self, other: T) -> OrdinalFieldExpression[T]:
59
+ """Create a less-than expression."""
60
+ return OrdinalFieldExpression(field=self, operator="<", value=other)
61
+
62
+ def __ge__(self, other: T) -> OrdinalFieldExpression[T]:
63
+ """Create a greater-than-or-equal expression."""
64
+ return OrdinalFieldExpression(field=self, operator=">=", value=other)
65
+
66
+ def __le__(self, other: T) -> OrdinalFieldExpression[T]:
67
+ """Create a less-than-or-equal expression."""
68
+ return OrdinalFieldExpression(field=self, operator="<=", value=other)
69
+
70
+ def __eq__(self, other: T) -> OrdinalFieldExpression[T]: # type: ignore[override]
71
+ """Create an equality expression."""
72
+ return OrdinalFieldExpression(field=self, operator="==", value=other)
73
+
74
+ def __ne__(self, other: T) -> OrdinalFieldExpression[T]: # type: ignore[override]
75
+ """Create a not-equal expression."""
76
+ return OrdinalFieldExpression(field=self, operator="!=", value=other)
77
+
78
+
79
+ NumericalField = OrdinalField[Union[float, int]]
80
+ DatetimeField = OrdinalField[datetime]
81
+
82
+
83
+ class StringField(Field):
84
+ """Field for string values that supports equality operations.
85
+
86
+ Optional refactor when needed: Split into
87
+ - StringField(ABC) with the comparison operators.
88
+ - StringColumnField(StringField) for the __init__ and get_sqlmodel_field implementation.
89
+ """
90
+
91
+ def __init__(self, column: Mapped[str]) -> None:
92
+ """Initialize the string field with a database column.
93
+
94
+ Args:
95
+ column: The database column this field represents.
96
+ """
97
+ self._column = column
98
+
99
+ def get_sqlmodel_field(self) -> Mapped[str]:
100
+ """Get the string database column or property.
101
+
102
+ Returns:
103
+ The string column for database queries.
104
+ """
105
+ return self._column
106
+
107
+ def __eq__(self, other: str) -> StringFieldExpression: # type: ignore[override]
108
+ """Create an equality expression."""
109
+ return StringFieldExpression(field=self, operator="==", value=other)
110
+
111
+ def __ne__(self, other: str) -> StringFieldExpression: # type: ignore[override]
112
+ """Create a not-equal expression."""
113
+ return StringFieldExpression(field=self, operator="!=", value=other)
@@ -0,0 +1,79 @@
1
+ """Field expressions for building specific query conditions."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from dataclasses import dataclass
6
+ from datetime import datetime
7
+ from typing import TYPE_CHECKING, Callable, Generic, Literal, TypeVar, Union
8
+
9
+ from sqlalchemy import ColumnElement
10
+ from sqlalchemy.orm import Mapped
11
+
12
+ from lightly_studio.core.dataset_query.match_expression import MatchExpression
13
+
14
+ if TYPE_CHECKING:
15
+ from lightly_studio.core.dataset_query.field import (
16
+ OrdinalField,
17
+ StringField,
18
+ )
19
+
20
+
21
+ T = TypeVar("T")
22
+
23
+ """Conditions themselves, in the format <field> <operator> <value>
24
+
25
+ Example:
26
+ SampleField.file_name == "img1.jpg",
27
+ becomes StringField(file_name) == "img1.jpg",
28
+ becomes StringFieldExpression(field=StringField(file_name), operator="==", value="img1.jpg")
29
+ becomes SQLQuery.where(...)
30
+ """
31
+ StringOperator = Literal["==", "!="]
32
+ OrdinalOperator = Literal[">", "<", "==", ">=", "<=", "!="]
33
+
34
+
35
+ @dataclass
36
+ class OrdinalFieldExpression(MatchExpression, Generic[T]):
37
+ """Generic expression for ordinal field comparisons."""
38
+
39
+ field: OrdinalField[T]
40
+ operator: OrdinalOperator
41
+ value: T
42
+
43
+ def get(self) -> ColumnElement[bool]:
44
+ """Return the SQLAlchemy expression for this ordinal field expression."""
45
+ table_property = self.field.get_sqlmodel_field()
46
+ operations: dict[
47
+ OrdinalOperator,
48
+ Callable[[Mapped[T], T], ColumnElement[bool]],
49
+ ] = {
50
+ "<": lambda tp, v: tp < v,
51
+ "<=": lambda tp, v: tp <= v,
52
+ ">": lambda tp, v: tp > v,
53
+ ">=": lambda tp, v: tp >= v,
54
+ "==": lambda tp, v: tp == v,
55
+ "!=": lambda tp, v: tp != v,
56
+ }
57
+ return operations[self.operator](table_property, self.value)
58
+
59
+
60
+ NumericalFieldExpression = OrdinalFieldExpression[Union[float, int]]
61
+ DatetimeFieldExpression = OrdinalFieldExpression[datetime]
62
+
63
+
64
+ @dataclass
65
+ class StringFieldExpression(MatchExpression):
66
+ """Expression for string field comparisons."""
67
+
68
+ field: StringField
69
+ operator: StringOperator
70
+ value: str
71
+
72
+ def get(self) -> ColumnElement[bool]:
73
+ """Return the SQLAlchemy expression for this string field expression."""
74
+ table_property = self.field.get_sqlmodel_field()
75
+ operations: dict[StringOperator, Callable[[Mapped[str], str], ColumnElement[bool]]] = {
76
+ "==": lambda tp, v: tp == v,
77
+ "!=": lambda tp, v: tp != v,
78
+ }
79
+ return operations[self.operator](table_property, self.value)
@@ -0,0 +1,23 @@
1
+ """Base classes for match expressions in dataset queries."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from abc import ABC, abstractmethod
6
+
7
+ from sqlalchemy import ColumnElement
8
+
9
+
10
+ class MatchExpression(ABC):
11
+ """Base class for all match expressions that can be applied to database queries.
12
+
13
+ This class provides the foundation for implementing complex query expressions
14
+ that can be combined using AND/OR operations in the future.
15
+ """
16
+
17
+ @abstractmethod
18
+ def get(self) -> ColumnElement[bool]:
19
+ """Get the SQLAlchemy expression for this match expression.
20
+
21
+ Returns:
22
+ The combined SQLAlchemy expression.
23
+ """
@@ -0,0 +1,79 @@
1
+ """Classes for order by expressions in dataset queries."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from abc import ABC, abstractmethod
6
+
7
+ from sqlmodel.sql.expression import SelectOfScalar
8
+ from typing_extensions import Self
9
+
10
+ from lightly_studio.core.dataset_query.field import Field
11
+ from lightly_studio.models.sample import SampleTable
12
+
13
+
14
+ class OrderByExpression(ABC):
15
+ """Base class for all order by expressions that can be applied to database queries."""
16
+
17
+ def __init__(self, *, ascending: bool = True) -> None:
18
+ """Initialize the order by expression.
19
+
20
+ Args:
21
+ ascending: Whether to order in ascending (True) or descending (False) order.
22
+ """
23
+ self.ascending = ascending
24
+
25
+ @abstractmethod
26
+ def apply(self, query: SelectOfScalar[SampleTable]) -> SelectOfScalar[SampleTable]:
27
+ """Apply this ordering to a SQLModel Select query.
28
+
29
+ Args:
30
+ query: The SQLModel Select query to modify.
31
+
32
+ Returns:
33
+ The modified query after ordering
34
+ """
35
+
36
+ def asc(self) -> Self:
37
+ """Set the ordering to ascending.
38
+
39
+ Returns:
40
+ Self for method chaining.
41
+ """
42
+ self.ascending = True
43
+ return self
44
+
45
+ def desc(self) -> Self:
46
+ """Set the ordering to descending.
47
+
48
+ Returns:
49
+ Self for method chaining.
50
+ """
51
+ self.ascending = False
52
+ return self
53
+
54
+
55
+ class OrderByField(OrderByExpression):
56
+ """Order by a specific field, either ascending or descending.
57
+
58
+ Args:
59
+ field: The field to order by.
60
+ ascending: Whether to order in ascending (True) or descending (False) order.
61
+ """
62
+
63
+ def __init__(self, field: Field) -> None:
64
+ """Initialize with field and order direction."""
65
+ super().__init__()
66
+ self.field = field
67
+
68
+ def apply(self, query: SelectOfScalar[SampleTable]) -> SelectOfScalar[SampleTable]:
69
+ """Apply this ordering to a SQLModel Select query.
70
+
71
+ Args:
72
+ query: The SQLModel Select query to modify.
73
+
74
+ Returns:
75
+ The modified query after ordering
76
+ """
77
+ if self.ascending:
78
+ return query.order_by(self.field.get_sqlmodel_field().asc())
79
+ return query.order_by(self.field.get_sqlmodel_field().desc())
@@ -0,0 +1,28 @@
1
+ """Fields for querying sample properties in the dataset query system."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from sqlmodel import col
6
+
7
+ from lightly_studio.core.dataset_query.field import (
8
+ DatetimeField,
9
+ NumericalField,
10
+ StringField,
11
+ )
12
+ from lightly_studio.core.dataset_query.tags_expression import TagsAccessor
13
+ from lightly_studio.models.sample import SampleTable
14
+
15
+
16
+ class SampleField:
17
+ """Providing access to predefined sample fields for queries.
18
+
19
+ This class provides static instances of different field types that can be
20
+ used to build database queries on sample properties.
21
+ """
22
+
23
+ file_name = StringField(col(SampleTable.file_name))
24
+ width = NumericalField(col(SampleTable.width))
25
+ height = NumericalField(col(SampleTable.height))
26
+ file_path_abs = StringField(col(SampleTable.file_path_abs))
27
+ created_at = DatetimeField(col(SampleTable.created_at))
28
+ tags = TagsAccessor()
@@ -0,0 +1,46 @@
1
+ """Tag field classes for building dataset queries on sample tags."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from dataclasses import dataclass
6
+
7
+ from sqlalchemy import ColumnElement
8
+ from sqlmodel import col
9
+
10
+ from lightly_studio.core.dataset_query.match_expression import MatchExpression
11
+ from lightly_studio.models.sample import SampleTable
12
+ from lightly_studio.models.tag import TagTable
13
+
14
+
15
+ class TagsAccessor:
16
+ """Provides access to tag operations for query building.
17
+
18
+ This class enables checking tag membership using the contains method:
19
+ SampleField.tags.contains("tag_name") returns a TagsContainsExpression.
20
+ """
21
+
22
+ def contains(self, tag_name: str) -> TagsContainsExpression:
23
+ """Check if a tag name is in the sample's tags.
24
+
25
+ Args:
26
+ tag_name: The name of the tag to check for.
27
+
28
+ Returns:
29
+ A TagsContainsExpression for building queries.
30
+ """
31
+ return TagsContainsExpression(tag_name=tag_name)
32
+
33
+
34
+ @dataclass
35
+ class TagsContainsExpression(MatchExpression):
36
+ """Expression for checking if a sample contains a specific tag."""
37
+
38
+ tag_name: str
39
+
40
+ def get(self) -> ColumnElement[bool]:
41
+ """Get the tag contains expression.
42
+
43
+ Returns:
44
+ The SQLAlchemy expression for this field expression.
45
+ """
46
+ return SampleTable.tags.any(col(TagTable.name) == self.tag_name)