orchestrator-core 4.4.0rc1__py3-none-any.whl → 5.0.0a1__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.
Files changed (65) hide show
  1. orchestrator/__init__.py +1 -1
  2. orchestrator/api/api_v1/api.py +7 -0
  3. orchestrator/api/api_v1/endpoints/agent.py +62 -0
  4. orchestrator/api/api_v1/endpoints/processes.py +6 -12
  5. orchestrator/api/api_v1/endpoints/search.py +197 -0
  6. orchestrator/app.py +4 -0
  7. orchestrator/cli/index_llm.py +73 -0
  8. orchestrator/cli/main.py +8 -1
  9. orchestrator/cli/resize_embedding.py +136 -0
  10. orchestrator/cli/scheduler.py +29 -39
  11. orchestrator/cli/search_explore.py +203 -0
  12. orchestrator/db/models.py +37 -1
  13. orchestrator/graphql/schema.py +0 -5
  14. orchestrator/graphql/schemas/process.py +2 -2
  15. orchestrator/graphql/utils/create_resolver_error_handler.py +1 -1
  16. orchestrator/migrations/versions/schema/2025-08-12_52b37b5b2714_search_index_model_for_llm_integration.py +95 -0
  17. orchestrator/schedules/__init__.py +2 -1
  18. orchestrator/schedules/resume_workflows.py +2 -2
  19. orchestrator/schedules/scheduling.py +24 -64
  20. orchestrator/schedules/task_vacuum.py +2 -2
  21. orchestrator/schedules/validate_products.py +2 -8
  22. orchestrator/schedules/validate_subscriptions.py +2 -2
  23. orchestrator/schemas/search.py +101 -0
  24. orchestrator/search/__init__.py +0 -0
  25. orchestrator/search/agent/__init__.py +1 -0
  26. orchestrator/search/agent/prompts.py +62 -0
  27. orchestrator/search/agent/state.py +8 -0
  28. orchestrator/search/agent/tools.py +122 -0
  29. orchestrator/search/core/__init__.py +0 -0
  30. orchestrator/search/core/embedding.py +64 -0
  31. orchestrator/search/core/exceptions.py +16 -0
  32. orchestrator/search/core/types.py +162 -0
  33. orchestrator/search/core/validators.py +27 -0
  34. orchestrator/search/docs/index.md +37 -0
  35. orchestrator/search/docs/running_local_text_embedding_inference.md +45 -0
  36. orchestrator/search/filters/__init__.py +27 -0
  37. orchestrator/search/filters/base.py +236 -0
  38. orchestrator/search/filters/date_filters.py +75 -0
  39. orchestrator/search/filters/definitions.py +76 -0
  40. orchestrator/search/filters/ltree_filters.py +31 -0
  41. orchestrator/search/filters/numeric_filter.py +60 -0
  42. orchestrator/search/indexing/__init__.py +3 -0
  43. orchestrator/search/indexing/indexer.py +316 -0
  44. orchestrator/search/indexing/registry.py +88 -0
  45. orchestrator/search/indexing/tasks.py +53 -0
  46. orchestrator/search/indexing/traverse.py +209 -0
  47. orchestrator/search/retrieval/__init__.py +3 -0
  48. orchestrator/search/retrieval/builder.py +64 -0
  49. orchestrator/search/retrieval/engine.py +96 -0
  50. orchestrator/search/retrieval/ranker.py +202 -0
  51. orchestrator/search/retrieval/utils.py +88 -0
  52. orchestrator/search/retrieval/validation.py +174 -0
  53. orchestrator/search/schemas/__init__.py +0 -0
  54. orchestrator/search/schemas/parameters.py +114 -0
  55. orchestrator/search/schemas/results.py +47 -0
  56. orchestrator/services/processes.py +11 -16
  57. orchestrator/settings.py +29 -1
  58. orchestrator/workflow.py +1 -8
  59. {orchestrator_core-4.4.0rc1.dist-info → orchestrator_core-5.0.0a1.dist-info}/METADATA +6 -3
  60. {orchestrator_core-4.4.0rc1.dist-info → orchestrator_core-5.0.0a1.dist-info}/RECORD +62 -26
  61. orchestrator/graphql/resolvers/scheduled_tasks.py +0 -36
  62. orchestrator/graphql/schemas/scheduled_task.py +0 -8
  63. orchestrator/schedules/scheduler.py +0 -153
  64. {orchestrator_core-4.4.0rc1.dist-info → orchestrator_core-5.0.0a1.dist-info}/WHEEL +0 -0
  65. {orchestrator_core-4.4.0rc1.dist-info → orchestrator_core-5.0.0a1.dist-info}/licenses/LICENSE +0 -0
@@ -0,0 +1,236 @@
1
+ from __future__ import annotations
2
+
3
+ from itertools import count
4
+ from typing import Any, ClassVar, Literal
5
+
6
+ from pydantic import BaseModel, ConfigDict, Field, model_validator
7
+ from sqlalchemy import and_, exists, or_, select
8
+ from sqlalchemy.sql.elements import ColumnElement
9
+ from sqlalchemy_utils.types.ltree import Ltree
10
+
11
+ from orchestrator.db.models import AiSearchIndex
12
+ from orchestrator.search.core.types import BooleanOperator, FilterOp, SQLAColumn
13
+
14
+ from .date_filters import DateFilter
15
+ from .ltree_filters import LtreeFilter
16
+ from .numeric_filter import NumericFilter
17
+
18
+
19
+ class EqualityFilter(BaseModel):
20
+ op: Literal[FilterOp.EQ, FilterOp.NEQ]
21
+ value: Any # bool, str (UUID), str (enum values)
22
+
23
+ def to_expression(self, column: SQLAColumn, path: str) -> ColumnElement[bool]:
24
+ str_value = str(self.value)
25
+ match self.op:
26
+ case FilterOp.EQ:
27
+ return column == str_value
28
+ case FilterOp.NEQ:
29
+ return column != str_value
30
+
31
+
32
+ class StringFilter(BaseModel):
33
+ op: Literal[FilterOp.LIKE]
34
+ value: str
35
+
36
+ def to_expression(self, column: SQLAColumn, path: str) -> ColumnElement[bool]:
37
+ return column.like(self.value)
38
+
39
+ @model_validator(mode="after")
40
+ def validate_like_pattern(self) -> StringFilter:
41
+ """If the operation is 'like', the value must contain a wildcard."""
42
+ if self.op == FilterOp.LIKE:
43
+ if "%" not in self.value and "_" not in self.value:
44
+ raise ValueError("The value for a 'like' operation must contain a wildcard character ('%' or '_').")
45
+ return self
46
+
47
+
48
+ FilterCondition = (
49
+ DateFilter # DATETIME
50
+ | NumericFilter # INT/FLOAT
51
+ | EqualityFilter # BOOLEAN/UUID/BLOCK/RESOURCE_TYPE
52
+ | StringFilter # STRING TODO: convert to hybrid search
53
+ | LtreeFilter # Path
54
+ )
55
+
56
+
57
+ class PathFilter(BaseModel):
58
+
59
+ path: str = Field(description="The ltree path of the field to filter on, e.g., 'subscription.customer_id'.")
60
+ condition: FilterCondition = Field(description="The filter condition to apply.")
61
+
62
+ model_config = ConfigDict(
63
+ json_schema_extra={
64
+ "examples": [
65
+ {
66
+ "path": "subscription.status",
67
+ "condition": {"op": "eq", "value": "active"},
68
+ },
69
+ {
70
+ "path": "subscription.customer_id",
71
+ "condition": {"op": "ne", "value": "acme"},
72
+ },
73
+ {
74
+ "path": "subscription.start_date",
75
+ "condition": {"op": "gt", "value": "2025-01-01"},
76
+ },
77
+ {
78
+ "path": "subscription.end_date",
79
+ "condition": {
80
+ "op": "between",
81
+ "value": {"from": "2025-06-01", "to": "2025-07-01"},
82
+ },
83
+ },
84
+ {
85
+ "path": "subscription.*.name",
86
+ "condition": {"op": "matches_lquery", "value": "*.foo_*"},
87
+ },
88
+ ]
89
+ }
90
+ )
91
+
92
+ def to_expression(self, value_column: SQLAColumn) -> ColumnElement[bool]:
93
+ """Convert the path filter into a SQLAlchemy expression.
94
+
95
+ This method delegates to the specific filter condition's ``to_expression``
96
+ implementation, passing along the column and path for context.
97
+
98
+ Parameters
99
+ ----------
100
+ value_column : ColumnElement
101
+ The SQLAlchemy column element representing the value to be filtered.
102
+
103
+ Returns:
104
+ -------
105
+ ColumnElement[bool]
106
+ A SQLAlchemy boolean expression that can be used in a ``WHERE`` clause.
107
+ """
108
+ return self.condition.to_expression(value_column, self.path)
109
+
110
+
111
+ class FilterTree(BaseModel):
112
+ model_config = ConfigDict(
113
+ json_schema_extra={
114
+ "description": (
115
+ "Boolean filter tree. Operators must be UPPERCASE: AND / OR.\n"
116
+ "Node shapes:\n"
117
+ " • Group: {'op':'AND'|'OR', 'children': [<PathFilter|FilterTree>, ...]}\n"
118
+ " • Leaf (PathFilter): {'path':'<ltree>', 'condition': {...}}\n"
119
+ "Rules:\n"
120
+ " • Do NOT put 'op' or 'children' inside a leaf 'condition'.\n"
121
+ " • Max depth = 5.\n"
122
+ " • Use from_flat_and() for a flat list of leaves."
123
+ ),
124
+ "examples": [
125
+ {
126
+ "op": "AND",
127
+ "children": [
128
+ {"path": "subscription.status", "condition": {"op": "eq", "value": "active"}},
129
+ {"path": "subscription.start_date", "condition": {"op": "gt", "value": "2021-01-01"}},
130
+ ],
131
+ },
132
+ {
133
+ "op": "AND",
134
+ "children": [
135
+ {"path": "subscription.start_date", "condition": {"op": "gte", "value": "2024-01-01"}},
136
+ {
137
+ "op": "OR",
138
+ "children": [
139
+ {"path": "subscription.product_name", "condition": {"op": "like", "value": "%fiber%"}},
140
+ {"path": "subscription.customer_id", "condition": {"op": "eq", "value": "Surf"}},
141
+ ],
142
+ },
143
+ ],
144
+ },
145
+ ],
146
+ }
147
+ )
148
+
149
+ op: BooleanOperator = Field(
150
+ description="Operator for grouping conditions in uppercase.", default=BooleanOperator.AND
151
+ )
152
+
153
+ children: list[FilterTree | PathFilter] = Field(min_length=1, description="Path filters or nested groups.")
154
+
155
+ MAX_DEPTH: ClassVar[int] = 5
156
+
157
+ @model_validator(mode="after")
158
+ def _validate_depth(self) -> FilterTree:
159
+ def depth(node: "FilterTree | PathFilter") -> int:
160
+ return 1 + max(depth(c) for c in node.children) if isinstance(node, FilterTree) else 1
161
+
162
+ if depth(self) > self.MAX_DEPTH:
163
+ raise ValueError(f"FilterTree nesting exceeds MAX_DEPTH={self.MAX_DEPTH}")
164
+ return self
165
+
166
+ @classmethod
167
+ def from_flat_and(cls, filters: list[PathFilter]) -> FilterTree | None:
168
+ """Wrap a flat list of PathFilter into an AND group (or None)."""
169
+ return None if not filters else cls(op=BooleanOperator.AND, children=list(filters))
170
+
171
+ def get_all_paths(self) -> set[str]:
172
+ """Collects all unique paths from the PathFilter leaves in the tree."""
173
+ return {leaf.path for leaf in self.get_all_leaves()}
174
+
175
+ def get_all_leaves(self) -> list[PathFilter]:
176
+ """Collect all PathFilter leaves in the tree."""
177
+ leaves: list[PathFilter] = []
178
+ for child in self.children:
179
+ if isinstance(child, PathFilter):
180
+ leaves.append(child)
181
+ else:
182
+ leaves.extend(child.get_all_leaves())
183
+ return leaves
184
+
185
+ def to_expression(
186
+ self,
187
+ entity_id_col: SQLAColumn,
188
+ *,
189
+ entity_type_value: str | None = None,
190
+ ) -> ColumnElement[bool]:
191
+ """Compile this tree into a SQLAlchemy boolean expression.
192
+
193
+ Parameters
194
+ ----------
195
+ entity_id_col : SQLAColumn
196
+ Column in the outer query representing the entity ID.
197
+ entity_type_value : str, optional
198
+ If provided, each subquery is additionally constrained to this entity type.
199
+
200
+ Returns:
201
+ -------
202
+ ColumnElement[bool]
203
+ A SQLAlchemy expression suitable for use in a WHERE clause.
204
+ """
205
+ alias_idx = count(1)
206
+
207
+ def leaf_exists(pf: PathFilter) -> ColumnElement[bool]:
208
+ from sqlalchemy.orm import aliased
209
+
210
+ alias = aliased(AiSearchIndex, name=f"flt_{next(alias_idx)}")
211
+
212
+ correlates = [alias.entity_id == entity_id_col]
213
+ if entity_type_value is not None:
214
+ correlates.append(alias.entity_type == entity_type_value)
215
+
216
+ if isinstance(pf.condition, LtreeFilter):
217
+ # Path-only condition acts on path column
218
+ pred = pf.condition.to_expression(alias.path, pf.path)
219
+ where_clause = and_(*correlates, pred)
220
+ else:
221
+ where_clause = and_(
222
+ *correlates,
223
+ alias.path == Ltree(pf.path),
224
+ pf.condition.to_expression(alias.value, pf.path),
225
+ )
226
+
227
+ subq = select(1).select_from(alias).where(where_clause)
228
+ return exists(subq)
229
+
230
+ def compile_node(node: FilterTree | PathFilter) -> ColumnElement[bool]:
231
+ if isinstance(node, FilterTree):
232
+ compiled = [compile_node(ch) for ch in node.children]
233
+ return and_(*compiled) if node.op == BooleanOperator.AND else or_(*compiled)
234
+ return leaf_exists(node)
235
+
236
+ return compile_node(self)
@@ -0,0 +1,75 @@
1
+ from datetime import date, datetime
2
+ from typing import Annotated, Any, Literal
3
+
4
+ from dateutil.parser import parse as dt_parse
5
+ from pydantic import BaseModel, BeforeValidator, Field, model_validator
6
+ from sqlalchemy import TIMESTAMP, and_
7
+ from sqlalchemy import cast as sa_cast
8
+ from sqlalchemy.sql.elements import ColumnElement
9
+
10
+ from orchestrator.search.core.types import FilterOp, SQLAColumn
11
+
12
+
13
+ def _validate_date_string(v: Any) -> Any:
14
+ if not isinstance(v, str):
15
+ return v
16
+ try:
17
+ dt_parse(v)
18
+ return v
19
+ except Exception as exc:
20
+ raise ValueError("is not a valid date or datetime string") from exc
21
+
22
+
23
+ DateValue = datetime | date | str
24
+ ValidatedDateValue = Annotated[DateValue, BeforeValidator(_validate_date_string)]
25
+
26
+
27
+ class DateRange(BaseModel):
28
+
29
+ start: ValidatedDateValue
30
+ end: ValidatedDateValue
31
+
32
+ @model_validator(mode="after")
33
+ def _order(self) -> "DateRange":
34
+ to_datetime = dt_parse(str(self.end))
35
+ from_datetime = dt_parse(str(self.start))
36
+ if to_datetime <= from_datetime:
37
+ raise ValueError("'to' must be after 'from'")
38
+ return self
39
+
40
+
41
+ class DateValueFilter(BaseModel):
42
+ """A filter that operates on a single date value."""
43
+
44
+ op: Literal[FilterOp.EQ, FilterOp.NEQ, FilterOp.LT, FilterOp.LTE, FilterOp.GT, FilterOp.GTE]
45
+ value: ValidatedDateValue
46
+
47
+ def to_expression(self, column: SQLAColumn, path: str) -> ColumnElement[bool]:
48
+ date_column = sa_cast(column, TIMESTAMP(timezone=True))
49
+ match self.op:
50
+ case FilterOp.EQ:
51
+ return date_column == self.value
52
+ case FilterOp.NEQ:
53
+ return date_column != self.value
54
+ case FilterOp.LT:
55
+ return date_column < self.value
56
+ case FilterOp.LTE:
57
+ return date_column <= self.value
58
+ case FilterOp.GT:
59
+ return date_column > self.value
60
+ case FilterOp.GTE:
61
+ return date_column >= self.value
62
+
63
+
64
+ class DateRangeFilter(BaseModel):
65
+ """A filter that operates on a range of dates."""
66
+
67
+ op: Literal[FilterOp.BETWEEN]
68
+ value: DateRange
69
+
70
+ def to_expression(self, column: SQLAColumn, path: str) -> ColumnElement[bool]:
71
+ date_column = sa_cast(column, TIMESTAMP(timezone=True))
72
+ return and_(date_column >= self.value.start, date_column < self.value.end)
73
+
74
+
75
+ DateFilter = Annotated[DateValueFilter | DateRangeFilter, Field(discriminator="op")]
@@ -0,0 +1,76 @@
1
+ from orchestrator.search.core.types import FieldType, FilterOp, UIType
2
+ from orchestrator.search.schemas.results import TypeDefinition, ValueSchema
3
+
4
+
5
+ def operators_for(ft: FieldType) -> list[FilterOp]:
6
+ """Return the list of valid operators for a given FieldType."""
7
+ return list(value_schema_for(ft).keys())
8
+
9
+
10
+ def value_schema_for(ft: FieldType) -> dict[FilterOp, ValueSchema]:
11
+ """Return the value schema map for a given FieldType."""
12
+ if ft in (FieldType.INTEGER, FieldType.FLOAT):
13
+ return {
14
+ FilterOp.EQ: ValueSchema(kind=UIType.NUMBER),
15
+ FilterOp.NEQ: ValueSchema(kind=UIType.NUMBER),
16
+ FilterOp.LT: ValueSchema(kind=UIType.NUMBER),
17
+ FilterOp.LTE: ValueSchema(kind=UIType.NUMBER),
18
+ FilterOp.GT: ValueSchema(kind=UIType.NUMBER),
19
+ FilterOp.GTE: ValueSchema(kind=UIType.NUMBER),
20
+ FilterOp.BETWEEN: ValueSchema(
21
+ kind="object",
22
+ fields={
23
+ "start": ValueSchema(kind=UIType.NUMBER),
24
+ "end": ValueSchema(kind=UIType.NUMBER),
25
+ },
26
+ ),
27
+ }
28
+
29
+ if ft == FieldType.BOOLEAN:
30
+ return {
31
+ FilterOp.EQ: ValueSchema(kind=UIType.BOOLEAN),
32
+ FilterOp.NEQ: ValueSchema(kind=UIType.BOOLEAN),
33
+ }
34
+
35
+ if ft == FieldType.DATETIME:
36
+ return {
37
+ FilterOp.EQ: ValueSchema(kind=UIType.DATETIME),
38
+ FilterOp.NEQ: ValueSchema(kind=UIType.DATETIME),
39
+ FilterOp.LT: ValueSchema(kind=UIType.DATETIME),
40
+ FilterOp.LTE: ValueSchema(kind=UIType.DATETIME),
41
+ FilterOp.GT: ValueSchema(kind=UIType.DATETIME),
42
+ FilterOp.GTE: ValueSchema(kind=UIType.DATETIME),
43
+ FilterOp.BETWEEN: ValueSchema(
44
+ kind="object",
45
+ fields={
46
+ "start": ValueSchema(kind=UIType.DATETIME),
47
+ "end": ValueSchema(kind=UIType.DATETIME),
48
+ },
49
+ ),
50
+ }
51
+
52
+ return {
53
+ FilterOp.EQ: ValueSchema(kind=UIType.STRING),
54
+ FilterOp.NEQ: ValueSchema(kind=UIType.STRING),
55
+ }
56
+
57
+
58
+ def generate_definitions() -> dict[UIType, TypeDefinition]:
59
+ """Generate the full definitions dictionary for all UI types."""
60
+ definitions = {}
61
+
62
+ for ui_type in UIType:
63
+ if ui_type == UIType.NUMBER:
64
+ rep_ft = FieldType.INTEGER
65
+ elif ui_type == UIType.DATETIME:
66
+ rep_ft = FieldType.DATETIME
67
+ elif ui_type == UIType.BOOLEAN:
68
+ rep_ft = FieldType.BOOLEAN
69
+ else:
70
+ rep_ft = FieldType.STRING
71
+
72
+ definitions[ui_type] = TypeDefinition(
73
+ operators=operators_for(rep_ft),
74
+ valueSchema=value_schema_for(rep_ft),
75
+ )
76
+ return definitions
@@ -0,0 +1,31 @@
1
+ from typing import Literal
2
+
3
+ from pydantic import BaseModel, Field
4
+ from sqlalchemy import TEXT, bindparam
5
+ from sqlalchemy.sql.elements import ColumnElement
6
+ from sqlalchemy_utils.types.ltree import Ltree
7
+
8
+ from orchestrator.search.core.types import FilterOp, SQLAColumn
9
+
10
+
11
+ class LtreeFilter(BaseModel):
12
+ """Filter for ltree path operations."""
13
+
14
+ op: Literal[FilterOp.MATCHES_LQUERY, FilterOp.IS_ANCESTOR, FilterOp.IS_DESCENDANT, FilterOp.PATH_MATCH]
15
+ value: str = Field(description="The ltree path or lquery pattern to compare against.")
16
+
17
+ def to_expression(self, column: SQLAColumn, path: str) -> ColumnElement[bool]:
18
+ """Converts the filter condition into a SQLAlchemy expression."""
19
+ match self.op:
20
+ case FilterOp.IS_DESCENDANT:
21
+ ltree_value = Ltree(self.value)
22
+ return column.op("<@")(ltree_value)
23
+ case FilterOp.IS_ANCESTOR:
24
+ ltree_value = Ltree(self.value)
25
+ return column.op("@>")(ltree_value)
26
+ case FilterOp.MATCHES_LQUERY:
27
+ param = bindparam("lquery_pattern", self.value, type_=TEXT)
28
+ return column.op("~")(param)
29
+ case FilterOp.PATH_MATCH:
30
+ ltree_value = Ltree(path)
31
+ return column == ltree_value
@@ -0,0 +1,60 @@
1
+ from typing import Annotated, Any, Literal
2
+
3
+ from pydantic import BaseModel, Field, model_validator
4
+ from sqlalchemy import DOUBLE_PRECISION, INTEGER, and_
5
+ from sqlalchemy import cast as sa_cast
6
+ from sqlalchemy.sql.elements import ColumnElement
7
+ from typing_extensions import Self
8
+
9
+ from orchestrator.search.core.types import FilterOp, SQLAColumn
10
+
11
+
12
+ class NumericRange(BaseModel):
13
+ start: int | float
14
+ end: int | float
15
+
16
+ @model_validator(mode="after")
17
+ def validate_order(self) -> Self:
18
+ if self.end <= self.start:
19
+ raise ValueError("'end' must be greater than 'start'")
20
+ return self
21
+
22
+
23
+ class NumericValueFilter(BaseModel):
24
+ """A filter for single numeric value comparisons (int or float)."""
25
+
26
+ op: Literal[FilterOp.EQ, FilterOp.NEQ, FilterOp.LT, FilterOp.LTE, FilterOp.GT, FilterOp.GTE]
27
+ value: int | float
28
+
29
+ def to_expression(self, column: SQLAColumn, path: str) -> ColumnElement[bool]:
30
+ cast_type = INTEGER if isinstance(self.value, int) else DOUBLE_PRECISION
31
+ numeric_column: ColumnElement[Any] = sa_cast(column, cast_type)
32
+ match self.op:
33
+
34
+ case FilterOp.EQ:
35
+ return numeric_column == self.value
36
+ case FilterOp.NEQ:
37
+ return numeric_column != self.value
38
+ case FilterOp.LT:
39
+ return numeric_column < self.value
40
+ case FilterOp.LTE:
41
+ return numeric_column <= self.value
42
+ case FilterOp.GT:
43
+ return numeric_column > self.value
44
+ case FilterOp.GTE:
45
+ return numeric_column >= self.value
46
+
47
+
48
+ class NumericRangeFilter(BaseModel):
49
+ """A filter for a range of numeric values (int or float)."""
50
+
51
+ op: Literal[FilterOp.BETWEEN]
52
+ value: NumericRange
53
+
54
+ def to_expression(self, column: SQLAColumn, path: str) -> ColumnElement[bool]:
55
+ cast_type = INTEGER if isinstance(self.value.start, int) else DOUBLE_PRECISION
56
+ numeric_column: ColumnElement[Any] = sa_cast(column, cast_type)
57
+ return and_(numeric_column >= self.value.start, numeric_column <= self.value.end)
58
+
59
+
60
+ NumericFilter = Annotated[NumericValueFilter | NumericRangeFilter, Field(discriminator="op")]
@@ -0,0 +1,3 @@
1
+ from .tasks import run_indexing_for_entity
2
+
3
+ __all__ = ["run_indexing_for_entity"]