lightly-studio 0.3.1__py3-none-any.whl → 0.3.2__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 +4 -4
- lightly_studio/api/app.py +1 -1
- lightly_studio/api/routes/api/annotation.py +6 -16
- lightly_studio/api/routes/api/annotation_label.py +2 -5
- lightly_studio/api/routes/api/annotation_task.py +4 -5
- lightly_studio/api/routes/api/classifier.py +2 -5
- lightly_studio/api/routes/api/dataset.py +2 -3
- lightly_studio/api/routes/api/dataset_tag.py +2 -3
- lightly_studio/api/routes/api/metadata.py +2 -4
- lightly_studio/api/routes/api/metrics.py +2 -6
- lightly_studio/api/routes/api/sample.py +5 -13
- lightly_studio/api/routes/api/settings.py +2 -6
- lightly_studio/api/routes/images.py +6 -6
- lightly_studio/core/add_samples.py +383 -0
- lightly_studio/core/dataset.py +250 -362
- lightly_studio/core/dataset_query/__init__.py +0 -0
- lightly_studio/core/dataset_query/boolean_expression.py +67 -0
- lightly_studio/core/dataset_query/dataset_query.py +211 -0
- lightly_studio/core/dataset_query/field.py +113 -0
- lightly_studio/core/dataset_query/field_expression.py +79 -0
- lightly_studio/core/dataset_query/match_expression.py +23 -0
- lightly_studio/core/dataset_query/order_by.py +79 -0
- lightly_studio/core/dataset_query/sample_field.py +28 -0
- lightly_studio/core/dataset_query/tags_expression.py +46 -0
- lightly_studio/core/sample.py +159 -32
- lightly_studio/core/start_gui.py +35 -0
- lightly_studio/dataset/edge_embedding_generator.py +13 -8
- lightly_studio/dataset/embedding_generator.py +2 -3
- lightly_studio/dataset/embedding_manager.py +74 -6
- lightly_studio/dataset/fsspec_lister.py +275 -0
- lightly_studio/dataset/loader.py +49 -30
- lightly_studio/dataset/mobileclip_embedding_generator.py +6 -4
- lightly_studio/db_manager.py +145 -0
- lightly_studio/dist_lightly_studio_view_app/_app/immutable/assets/SelectableSvgGroup.BBm0IWdq.css +1 -0
- lightly_studio/dist_lightly_studio_view_app/_app/immutable/assets/SelectableSvgGroup.BNTuXSAe.css +1 -0
- lightly_studio/dist_lightly_studio_view_app/_app/immutable/chunks/2O287xak.js +3 -0
- lightly_studio/dist_lightly_studio_view_app/_app/immutable/chunks/{O-EABkf9.js → 7YNGEs1C.js} +1 -1
- lightly_studio/dist_lightly_studio_view_app/_app/immutable/chunks/BBoGk9hq.js +1 -0
- lightly_studio/dist_lightly_studio_view_app/_app/immutable/chunks/BRnH9v23.js +92 -0
- lightly_studio/dist_lightly_studio_view_app/_app/immutable/chunks/Bg1Y5eUZ.js +1 -0
- lightly_studio/dist_lightly_studio_view_app/_app/immutable/chunks/{DOlTMNyt.js → BqBqV92V.js} +1 -1
- lightly_studio/dist_lightly_studio_view_app/_app/immutable/chunks/C0JiMuYn.js +1 -0
- lightly_studio/dist_lightly_studio_view_app/_app/immutable/chunks/{DjfY96ND.js → C98Hk3r5.js} +1 -1
- lightly_studio/dist_lightly_studio_view_app/_app/immutable/chunks/{r64xT6ao.js → CG0dMCJi.js} +1 -1
- lightly_studio/dist_lightly_studio_view_app/_app/immutable/chunks/{C8I8rFJQ.js → Ccq4ZD0B.js} +1 -1
- lightly_studio/dist_lightly_studio_view_app/_app/immutable/chunks/Cpy-nab_.js +1 -0
- lightly_studio/dist_lightly_studio_view_app/_app/immutable/chunks/{Bu7uvVrG.js → Crk-jcvV.js} +1 -1
- lightly_studio/dist_lightly_studio_view_app/_app/immutable/chunks/Cs31G8Qn.js +1 -0
- lightly_studio/dist_lightly_studio_view_app/_app/immutable/chunks/CsKrY2zA.js +1 -0
- lightly_studio/dist_lightly_studio_view_app/_app/immutable/chunks/{x9G_hzyY.js → Cur71c3O.js} +1 -1
- lightly_studio/dist_lightly_studio_view_app/_app/immutable/chunks/CzgC3GFB.js +1 -0
- lightly_studio/dist_lightly_studio_view_app/_app/immutable/chunks/D8GZDMNN.js +1 -0
- lightly_studio/dist_lightly_studio_view_app/_app/immutable/chunks/DFRh-Spp.js +1 -0
- lightly_studio/dist_lightly_studio_view_app/_app/immutable/chunks/{BylOuP6i.js → DRZO-E-T.js} +1 -1
- lightly_studio/dist_lightly_studio_view_app/_app/immutable/chunks/{l7KrR96u.js → DcGCxgpH.js} +1 -1
- lightly_studio/dist_lightly_studio_view_app/_app/immutable/chunks/{Bsi3UGy5.js → Df3aMO5B.js} +1 -1
- lightly_studio/dist_lightly_studio_view_app/_app/immutable/chunks/{hQVEETDE.js → DkR_EZ_B.js} +1 -1
- lightly_studio/dist_lightly_studio_view_app/_app/immutable/chunks/DqUGznj_.js +1 -0
- lightly_studio/dist_lightly_studio_view_app/_app/immutable/chunks/KpAtIldw.js +1 -0
- lightly_studio/dist_lightly_studio_view_app/_app/immutable/chunks/M1Q1F7bw.js +4 -0
- lightly_studio/dist_lightly_studio_view_app/_app/immutable/chunks/{CDnpyLsT.js → OH7-C_mc.js} +1 -1
- lightly_studio/dist_lightly_studio_view_app/_app/immutable/chunks/{D6su9Aln.js → gLNdjSzu.js} +1 -1
- lightly_studio/dist_lightly_studio_view_app/_app/immutable/chunks/i0ZZ4z06.js +1 -0
- lightly_studio/dist_lightly_studio_view_app/_app/immutable/entry/app.BI-EA5gL.js +2 -0
- lightly_studio/dist_lightly_studio_view_app/_app/immutable/entry/start.CcsRl3cZ.js +1 -0
- lightly_studio/dist_lightly_studio_view_app/_app/immutable/nodes/0.BbO4Zc3r.js +1 -0
- lightly_studio/dist_lightly_studio_view_app/_app/immutable/nodes/{1.B4rNYwVp.js → 1._I9GR805.js} +1 -1
- lightly_studio/dist_lightly_studio_view_app/_app/immutable/nodes/10.J2RBFrSr.js +1 -0
- lightly_studio/dist_lightly_studio_view_app/_app/immutable/nodes/12.Cmqj25a-.js +1 -0
- lightly_studio/dist_lightly_studio_view_app/_app/immutable/nodes/2.C45iKJHA.js +6 -0
- lightly_studio/dist_lightly_studio_view_app/_app/immutable/nodes/{3.CWHpKonm.js → 3.w9g4AcAx.js} +1 -1
- lightly_studio/dist_lightly_studio_view_app/_app/immutable/nodes/{4.OUWOLQeV.js → 4.BBI8KwnD.js} +1 -1
- lightly_studio/dist_lightly_studio_view_app/_app/immutable/nodes/5.huHuxdiF.js +1 -0
- lightly_studio/dist_lightly_studio_view_app/_app/immutable/nodes/6.CrbkRPam.js +1 -0
- lightly_studio/dist_lightly_studio_view_app/_app/immutable/nodes/7.FomEdhD6.js +1 -0
- lightly_studio/dist_lightly_studio_view_app/_app/immutable/nodes/8.Cb_ADSLk.js +1 -0
- lightly_studio/dist_lightly_studio_view_app/_app/immutable/nodes/{9.CPu3CiBc.js → 9.CajIG5ce.js} +1 -1
- lightly_studio/dist_lightly_studio_view_app/_app/version.json +1 -1
- lightly_studio/dist_lightly_studio_view_app/index.html +14 -14
- lightly_studio/examples/example.py +13 -12
- lightly_studio/examples/example_coco.py +13 -0
- lightly_studio/examples/example_metadata.py +83 -98
- lightly_studio/examples/example_selection.py +7 -19
- lightly_studio/examples/example_split_work.py +12 -36
- lightly_studio/examples/{example_v2.py → example_yolo.py} +3 -4
- lightly_studio/models/annotation/annotation_base.py +7 -8
- lightly_studio/models/annotation/instance_segmentation.py +8 -8
- lightly_studio/models/annotation/object_detection.py +4 -4
- lightly_studio/models/dataset.py +6 -2
- lightly_studio/models/sample.py +10 -3
- lightly_studio/resolvers/dataset_resolver.py +10 -0
- lightly_studio/resolvers/embedding_model_resolver.py +22 -0
- lightly_studio/resolvers/sample_resolver.py +53 -9
- lightly_studio/resolvers/tag_resolver.py +23 -0
- lightly_studio/selection/select.py +55 -46
- lightly_studio/selection/select_via_db.py +23 -19
- lightly_studio/selection/selection_config.py +6 -3
- lightly_studio/services/annotations_service/__init__.py +4 -0
- lightly_studio/services/annotations_service/update_annotation.py +21 -32
- lightly_studio/services/annotations_service/update_annotation_bounding_box.py +36 -0
- lightly_studio-0.3.2.dist-info/METADATA +689 -0
- {lightly_studio-0.3.1.dist-info → lightly_studio-0.3.2.dist-info}/RECORD +104 -91
- lightly_studio/api/db.py +0 -133
- lightly_studio/dist_lightly_studio_view_app/_app/immutable/assets/SelectableSvgGroup.OwPEPQZu.css +0 -1
- lightly_studio/dist_lightly_studio_view_app/_app/immutable/assets/SelectableSvgGroup.b653GmVf.css +0 -1
- lightly_studio/dist_lightly_studio_view_app/_app/immutable/chunks/B2FVR0s0.js +0 -1
- lightly_studio/dist_lightly_studio_view_app/_app/immutable/chunks/B9zumHo5.js +0 -1
- lightly_studio/dist_lightly_studio_view_app/_app/immutable/chunks/BJXwVxaE.js +0 -1
- lightly_studio/dist_lightly_studio_view_app/_app/immutable/chunks/Bx1xMsFy.js +0 -1
- lightly_studio/dist_lightly_studio_view_app/_app/immutable/chunks/CcaPhhk3.js +0 -1
- lightly_studio/dist_lightly_studio_view_app/_app/immutable/chunks/CvOmgdoc.js +0 -93
- lightly_studio/dist_lightly_studio_view_app/_app/immutable/chunks/CxtLVaYz.js +0 -3
- lightly_studio/dist_lightly_studio_view_app/_app/immutable/chunks/D5-A_Ffd.js +0 -4
- lightly_studio/dist_lightly_studio_view_app/_app/immutable/chunks/D6RI2Zrd.js +0 -1
- lightly_studio/dist_lightly_studio_view_app/_app/immutable/chunks/D98V7j6A.js +0 -1
- lightly_studio/dist_lightly_studio_view_app/_app/immutable/chunks/DIRAtgl0.js +0 -1
- lightly_studio/dist_lightly_studio_view_app/_app/immutable/chunks/DjUWrjOv.js +0 -1
- lightly_studio/dist_lightly_studio_view_app/_app/immutable/chunks/XO7A28GO.js +0 -1
- lightly_studio/dist_lightly_studio_view_app/_app/immutable/chunks/nAHhluT7.js +0 -1
- lightly_studio/dist_lightly_studio_view_app/_app/immutable/chunks/vC4nQVEB.js +0 -1
- lightly_studio/dist_lightly_studio_view_app/_app/immutable/entry/app.CjnvpsmS.js +0 -2
- lightly_studio/dist_lightly_studio_view_app/_app/immutable/entry/start.0o1H7wM9.js +0 -1
- lightly_studio/dist_lightly_studio_view_app/_app/immutable/nodes/0.XRq_TUwu.js +0 -1
- lightly_studio/dist_lightly_studio_view_app/_app/immutable/nodes/10.DfBwOEhN.js +0 -1
- lightly_studio/dist_lightly_studio_view_app/_app/immutable/nodes/12.CwF2_8mP.js +0 -1
- lightly_studio/dist_lightly_studio_view_app/_app/immutable/nodes/2.CS4muRY-.js +0 -6
- lightly_studio/dist_lightly_studio_view_app/_app/immutable/nodes/5.Dm6t9F5W.js +0 -1
- lightly_studio/dist_lightly_studio_view_app/_app/immutable/nodes/6.Bw5ck4gK.js +0 -1
- lightly_studio/dist_lightly_studio_view_app/_app/immutable/nodes/7.CF0EDTR6.js +0 -1
- lightly_studio/dist_lightly_studio_view_app/_app/immutable/nodes/8.Cw30LEcV.js +0 -1
- lightly_studio-0.3.1.dist-info/METADATA +0 -520
- /lightly_studio/dist_lightly_studio_view_app/_app/immutable/assets/{OpenSans- → OpenSans-Medium.DVUZMR_6.ttf} +0 -0
- {lightly_studio-0.3.1.dist-info → lightly_studio-0.3.2.dist-info}/WHEEL +0 -0
|
File without changes
|
|
@@ -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,211 @@
|
|
|
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.models.dataset import DatasetTable
|
|
14
|
+
from lightly_studio.models.sample import SampleTable
|
|
15
|
+
from lightly_studio.resolvers import tag_resolver
|
|
16
|
+
from lightly_studio.selection.select import Selection
|
|
17
|
+
|
|
18
|
+
_SliceType = slice # to avoid shadowing built-in slice in type annotations
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
class DatasetQuery:
|
|
22
|
+
"""Class for executing querying on a dataset."""
|
|
23
|
+
|
|
24
|
+
def __init__(self, dataset: DatasetTable, session: Session) -> None:
|
|
25
|
+
"""Initialize with dataset and database session.
|
|
26
|
+
|
|
27
|
+
Args:
|
|
28
|
+
dataset: The dataset to query.
|
|
29
|
+
session: Database session for executing queries.
|
|
30
|
+
"""
|
|
31
|
+
self.dataset = dataset
|
|
32
|
+
self.session = session
|
|
33
|
+
self.match_expression: MatchExpression | None = None
|
|
34
|
+
self.order_by_expressions: list[OrderByExpression] | None = None
|
|
35
|
+
self._slice: _SliceType | None = None
|
|
36
|
+
|
|
37
|
+
def match(self, match_expression: MatchExpression) -> DatasetQuery:
|
|
38
|
+
"""Store a field condition for filtering.
|
|
39
|
+
|
|
40
|
+
Args:
|
|
41
|
+
match_expression: Defines the filter.
|
|
42
|
+
|
|
43
|
+
Returns:
|
|
44
|
+
Self for method chaining.
|
|
45
|
+
|
|
46
|
+
Raises:
|
|
47
|
+
ValueError: If match() has already been called on this instance.
|
|
48
|
+
"""
|
|
49
|
+
if self.match_expression is not None:
|
|
50
|
+
raise ValueError("match() can only be called once per DatasetQuery instance")
|
|
51
|
+
|
|
52
|
+
self.match_expression = match_expression
|
|
53
|
+
return self
|
|
54
|
+
|
|
55
|
+
def order_by(self, *order_by: OrderByExpression) -> DatasetQuery:
|
|
56
|
+
"""Store ordering expressions.
|
|
57
|
+
|
|
58
|
+
Args:
|
|
59
|
+
order_by: One or more ordering expressions. They are applied in order.
|
|
60
|
+
E.g. first ordering by sample width and then by sample file_name will
|
|
61
|
+
only order the samples with the same sample width by file_name.
|
|
62
|
+
|
|
63
|
+
Returns:
|
|
64
|
+
Self for method chaining.
|
|
65
|
+
|
|
66
|
+
Raises:
|
|
67
|
+
ValueError: If order_by() has already been called on this instance.
|
|
68
|
+
"""
|
|
69
|
+
if self.order_by_expressions:
|
|
70
|
+
raise ValueError("order_by() can only be called once per DatasetQuery instance")
|
|
71
|
+
|
|
72
|
+
self.order_by_expressions = list(order_by)
|
|
73
|
+
return self
|
|
74
|
+
|
|
75
|
+
def slice(self, offset: int = 0, limit: int | None = None) -> DatasetQuery:
|
|
76
|
+
"""Apply offset and limit to results.
|
|
77
|
+
|
|
78
|
+
Args:
|
|
79
|
+
offset: Number of items to skip from beginning (default: 0).
|
|
80
|
+
limit: Maximum number of items to return (None = no limit).
|
|
81
|
+
|
|
82
|
+
Returns:
|
|
83
|
+
Self for method chaining.
|
|
84
|
+
|
|
85
|
+
Raises:
|
|
86
|
+
ValueError: If slice() has already been called on this instance.
|
|
87
|
+
"""
|
|
88
|
+
if self._slice is not None:
|
|
89
|
+
raise ValueError("slice() can only be called once per DatasetQuery instance")
|
|
90
|
+
|
|
91
|
+
# Convert to slice object for internal consistency
|
|
92
|
+
stop = None if limit is None else offset + limit
|
|
93
|
+
self._slice = _SliceType(offset, stop)
|
|
94
|
+
return self
|
|
95
|
+
|
|
96
|
+
def __getitem__(self, key: _SliceType) -> DatasetQuery:
|
|
97
|
+
"""Enable bracket notation for slicing.
|
|
98
|
+
|
|
99
|
+
Args:
|
|
100
|
+
key: A slice object (e.g., [10:20], [:50], [100:]).
|
|
101
|
+
|
|
102
|
+
Returns:
|
|
103
|
+
Self with slice applied.
|
|
104
|
+
|
|
105
|
+
Raises:
|
|
106
|
+
TypeError: If key is not a slice object.
|
|
107
|
+
ValueError: If slice contains unsupported features or conflicts with existing slice.
|
|
108
|
+
"""
|
|
109
|
+
if not isinstance(key, _SliceType):
|
|
110
|
+
raise TypeError(
|
|
111
|
+
"DatasetQuery only supports slice notation, not integer indexing. "
|
|
112
|
+
"Use execute() to get results as a list for element access."
|
|
113
|
+
)
|
|
114
|
+
|
|
115
|
+
# Validate unsupported features
|
|
116
|
+
if key.step is not None:
|
|
117
|
+
raise ValueError("Strides are not supported. Use simple slices like [start:stop].")
|
|
118
|
+
|
|
119
|
+
if (key.start is not None and key.start < 0) or (key.stop is not None and key.stop < 0):
|
|
120
|
+
raise ValueError("Negative indices are not supported. Use positive indices only.")
|
|
121
|
+
|
|
122
|
+
# Check for conflicts with existing slice
|
|
123
|
+
if self._slice is not None:
|
|
124
|
+
raise ValueError("Cannot use bracket notation after slice() has been called.")
|
|
125
|
+
|
|
126
|
+
# Set slice and return self
|
|
127
|
+
self._slice = key
|
|
128
|
+
return self
|
|
129
|
+
|
|
130
|
+
def __iter__(self) -> Iterator[Sample]:
|
|
131
|
+
"""Iterate over the query results.
|
|
132
|
+
|
|
133
|
+
Returns:
|
|
134
|
+
Iterator of Sample objects from the database.
|
|
135
|
+
"""
|
|
136
|
+
# Build query
|
|
137
|
+
query = select(SampleTable).where(SampleTable.dataset_id == self.dataset.dataset_id)
|
|
138
|
+
|
|
139
|
+
# Apply filter if present
|
|
140
|
+
if self.match_expression:
|
|
141
|
+
query = query.where(self.match_expression.get())
|
|
142
|
+
|
|
143
|
+
# Apply ordering
|
|
144
|
+
if self.order_by_expressions:
|
|
145
|
+
for order_by in self.order_by_expressions:
|
|
146
|
+
query = order_by.apply(query)
|
|
147
|
+
else:
|
|
148
|
+
# Order by SampleField.created_at by default.
|
|
149
|
+
default_order_by = OrderByField(SampleField.created_at)
|
|
150
|
+
query = default_order_by.apply(query)
|
|
151
|
+
|
|
152
|
+
# Apply slicing if present
|
|
153
|
+
if self._slice is not None:
|
|
154
|
+
start = self._slice.start or 0
|
|
155
|
+
query = query.offset(start)
|
|
156
|
+
if self._slice.stop is not None:
|
|
157
|
+
limit = max(self._slice.stop - start, 0)
|
|
158
|
+
query = query.limit(limit)
|
|
159
|
+
|
|
160
|
+
# Execute query and yield results
|
|
161
|
+
for sample_table in self.session.exec(query):
|
|
162
|
+
yield Sample(inner=sample_table)
|
|
163
|
+
|
|
164
|
+
def to_list(self) -> list[Sample]:
|
|
165
|
+
"""Execute the query and return the results as a list.
|
|
166
|
+
|
|
167
|
+
Returns:
|
|
168
|
+
List of Sample objects from the database.
|
|
169
|
+
"""
|
|
170
|
+
return list(self)
|
|
171
|
+
|
|
172
|
+
def add_tag(self, tag_name: str) -> None:
|
|
173
|
+
"""Add a tag to all samples returned by this query.
|
|
174
|
+
|
|
175
|
+
First, creates the tag if it doesn't exist. Then applies the tag to all samples
|
|
176
|
+
that match the current query filters. Samples already having that tag are unchanged,
|
|
177
|
+
as the database prevents duplicates.
|
|
178
|
+
|
|
179
|
+
Args:
|
|
180
|
+
tag_name: Name of the tag to add to matching samples.
|
|
181
|
+
"""
|
|
182
|
+
# Get or create the tag
|
|
183
|
+
tag = tag_resolver.get_or_create_sample_tag_by_name(
|
|
184
|
+
session=self.session, dataset_id=self.dataset.dataset_id, tag_name=tag_name
|
|
185
|
+
)
|
|
186
|
+
|
|
187
|
+
# Execute query to get matching samples
|
|
188
|
+
samples = self.to_list()
|
|
189
|
+
sample_ids = [sample.sample_id for sample in samples]
|
|
190
|
+
|
|
191
|
+
# Use resolver to bulk assign tag (handles validation and edge cases)
|
|
192
|
+
tag_resolver.add_sample_ids_to_tag_id(
|
|
193
|
+
session=self.session, tag_id=tag.tag_id, sample_ids=sample_ids
|
|
194
|
+
)
|
|
195
|
+
|
|
196
|
+
def selection(self) -> Selection:
|
|
197
|
+
"""Selection interface for this query.
|
|
198
|
+
|
|
199
|
+
The returned Selection snapshots the current query results immediately.
|
|
200
|
+
Mutating the query after calling this method will therefore not affect
|
|
201
|
+
the samples used by that Selection instance.
|
|
202
|
+
|
|
203
|
+
Returns:
|
|
204
|
+
Selection interface operating on the current query result snapshot.
|
|
205
|
+
"""
|
|
206
|
+
input_sample_ids = (sample.sample_id for sample in self)
|
|
207
|
+
return Selection(
|
|
208
|
+
dataset_id=self.dataset.dataset_id,
|
|
209
|
+
session=self.session,
|
|
210
|
+
input_sample_ids=input_sample_ids,
|
|
211
|
+
)
|
|
@@ -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)
|