python-corekit 0.1.0__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 (125) hide show
  1. corekit/__init__.py +0 -0
  2. corekit/api/__init__.py +9 -0
  3. corekit/api/handler.py +76 -0
  4. corekit/api/responses.py +40 -0
  5. corekit/api/routers.py +115 -0
  6. corekit/concurrency/__init__.py +9 -0
  7. corekit/concurrency/decorators.py +72 -0
  8. corekit/concurrency/thread_local.py +99 -0
  9. corekit/concurrency/worker.py +65 -0
  10. corekit/config/__init__.py +47 -0
  11. corekit/config/loader.py +153 -0
  12. corekit/config/settings.py +161 -0
  13. corekit/config/sources.py +125 -0
  14. corekit/connections/__init__.py +31 -0
  15. corekit/connections/connectable.py +212 -0
  16. corekit/connections/decorators.py +92 -0
  17. corekit/connections/redis/__init__.py +7 -0
  18. corekit/connections/redis/connection.py +239 -0
  19. corekit/connections/registry.py +80 -0
  20. corekit/connections/sql/__init__.py +10 -0
  21. corekit/connections/sql/connection.py +342 -0
  22. corekit/connections/sql/fields/__init__.py +7 -0
  23. corekit/connections/sql/fields/jsonb.py +67 -0
  24. corekit/connections/sql/migration/__init__.py +57 -0
  25. corekit/connections/sql/migration/base.py +40 -0
  26. corekit/connections/sql/migration/operations.py +416 -0
  27. corekit/connections/sql/migration/registry.py +166 -0
  28. corekit/connections/sql/migration/table.py +27 -0
  29. corekit/connections/sql/query.py +68 -0
  30. corekit/connections/sql/table.py +96 -0
  31. corekit/constants.py +45 -0
  32. corekit/crypto/__init__.py +1 -0
  33. corekit/crypto/constants.py +7 -0
  34. corekit/crypto/enum.py +11 -0
  35. corekit/crypto/hasher.py +89 -0
  36. corekit/data/__init__.py +81 -0
  37. corekit/data/dataset.py +340 -0
  38. corekit/data/expressions/__init__.py +46 -0
  39. corekit/data/expressions/comparison.py +252 -0
  40. corekit/data/expressions/expression.py +98 -0
  41. corekit/data/record.py +147 -0
  42. corekit/data/stats.py +157 -0
  43. corekit/decorators/__init__.py +2 -0
  44. corekit/decorators/exception_handling.py +43 -0
  45. corekit/decorators/warnings.py +35 -0
  46. corekit/docker/__init__.py +7 -0
  47. corekit/docker/watchdog.py +222 -0
  48. corekit/etl/__init__.py +44 -0
  49. corekit/etl/connection.py +44 -0
  50. corekit/etl/extract/__init__.py +0 -0
  51. corekit/etl/extract/extractor.py +48 -0
  52. corekit/etl/extract/schemas.py +18 -0
  53. corekit/etl/load/__init__.py +0 -0
  54. corekit/etl/load/loader.py +53 -0
  55. corekit/etl/load/schemas.py +33 -0
  56. corekit/etl/orchestrator.py +201 -0
  57. corekit/etl/schemas.py +22 -0
  58. corekit/etl/transform/__init__.py +0 -0
  59. corekit/etl/transform/schemas.py +15 -0
  60. corekit/etl/transform/transformer.py +28 -0
  61. corekit/events/__init__.py +38 -0
  62. corekit/events/enum.py +58 -0
  63. corekit/events/frames.py +51 -0
  64. corekit/events/models.py +23 -0
  65. corekit/events/publisher.py +75 -0
  66. corekit/events/reader.py +132 -0
  67. corekit/events/sse.py +109 -0
  68. corekit/events/websocket.py +97 -0
  69. corekit/exceptions/__init__.py +0 -0
  70. corekit/exceptions/base.py +45 -0
  71. corekit/exceptions/custom/__init__.py +0 -0
  72. corekit/exceptions/http/__init__.py +0 -0
  73. corekit/exceptions/http/exceptions.py +37 -0
  74. corekit/exceptions/types.py +17 -0
  75. corekit/files/__init__.py +25 -0
  76. corekit/files/base.py +117 -0
  77. corekit/files/enum.py +30 -0
  78. corekit/files/json.py +12 -0
  79. corekit/files/pickle.py +12 -0
  80. corekit/files/toml.py +43 -0
  81. corekit/http/__init__.py +0 -0
  82. corekit/http/client.py +176 -0
  83. corekit/http/exponential_backoff.py +100 -0
  84. corekit/http/response.py +12 -0
  85. corekit/log_monitor/__init__.py +23 -0
  86. corekit/log_monitor/constants.py +8 -0
  87. corekit/log_monitor/models.py +150 -0
  88. corekit/log_monitor/service.py +418 -0
  89. corekit/notifications/__init__.py +8 -0
  90. corekit/notifications/base.py +51 -0
  91. corekit/notifications/models.py +34 -0
  92. corekit/observability/__init__.py +21 -0
  93. corekit/observability/benchmarkable.py +12 -0
  94. corekit/observability/loggable.py +29 -0
  95. corekit/observability/timing/__init__.py +0 -0
  96. corekit/observability/timing/constants.py +1 -0
  97. corekit/observability/timing/split.py +20 -0
  98. corekit/observability/timing/timer.py +30 -0
  99. corekit/py.typed +0 -0
  100. corekit/registry/__init__.py +12 -0
  101. corekit/registry/registry.py +134 -0
  102. corekit/schemas/__init__.py +0 -0
  103. corekit/schemas/dataclasses/__init__.py +0 -0
  104. corekit/schemas/enum.py +49 -0
  105. corekit/schemas/models/__init__.py +0 -0
  106. corekit/schemas/models/arbitrary.py +11 -0
  107. corekit/schemas/models/date_models.py +18 -0
  108. corekit/schemas/pydantic/__init__.py +0 -0
  109. corekit/schemas/pydantic/fields.py +35 -0
  110. corekit/schemas/types.py +40 -0
  111. corekit/serialization/__init__.py +0 -0
  112. corekit/serialization/enum.py +21 -0
  113. corekit/serialization/serializable.py +42 -0
  114. corekit/serialization/serializer.py +179 -0
  115. corekit/utils/__init__.py +5 -0
  116. corekit/utils/ids.py +5 -0
  117. corekit/utils/raise_exc.py +8 -0
  118. corekit/utils/time.py +21 -0
  119. corekit/utils/validators.py +15 -0
  120. corekit/utils/void.py +8 -0
  121. python_corekit-0.1.0.dist-info/METADATA +417 -0
  122. python_corekit-0.1.0.dist-info/RECORD +125 -0
  123. python_corekit-0.1.0.dist-info/WHEEL +5 -0
  124. python_corekit-0.1.0.dist-info/licenses/LICENSE +21 -0
  125. python_corekit-0.1.0.dist-info/top_level.txt +1 -0
@@ -0,0 +1,340 @@
1
+ import random
2
+ from typing import Any, Callable, Iterable, Iterator, Optional
3
+
4
+ from corekit.data.record import BaseRecord, resolve_default
5
+ from corekit.data.stats import DatasetStats
6
+
7
+ BASE_IDENTIFICATION_KEY = "id"
8
+
9
+
10
+ class Dataset:
11
+ """
12
+ A schema-fixed, order-preserving collection of records.
13
+
14
+ Every record in a dataset shares one schema and one generated record
15
+ class. Supports O(1) access by list position or by id, in-place field
16
+ editing, and dataset-wide schema evolution via add_field/remove_field.
17
+ """
18
+
19
+ # -- construction & protocol methods -----------------------------------
20
+
21
+ def __init__(
22
+ self,
23
+ id_key: str = BASE_IDENTIFICATION_KEY,
24
+ schema: Iterable[Any] | None = None,
25
+ missing_default: Any = None,
26
+ missing_default_factory: Callable[[], Any] | None = None,
27
+ ) -> None:
28
+ """
29
+ schema: explicit field order to lock in immediately. If omitted,
30
+ inferred from the first record passed to add().
31
+ missing_default / missing_default_factory: value (or zero-arg
32
+ factory, for mutables) used for a field a record omits.
33
+ """
34
+ self._id_key = id_key
35
+ self._cursor = 0
36
+ self._records: list[BaseRecord] = []
37
+ self._id_index: dict[Any, Any] = {}
38
+ self._missing_default_factory = resolve_default(missing_default, missing_default_factory)
39
+ self._schema: tuple | None = None
40
+ self._record_cls: type | None = None
41
+ if schema is not None:
42
+ self._set_schema(schema)
43
+
44
+ def __getitem__(self, key: int) -> Any:
45
+ """
46
+ Positional access, like a list. Use get_record() for id lookup
47
+ """
48
+ return self._records[key]
49
+
50
+ def __len__(self) -> int:
51
+ return len(self._records)
52
+
53
+ def __iter__(self) -> Iterator[Any]:
54
+ return iter(self._records)
55
+
56
+ def __contains__(self, record_id: Any) -> bool:
57
+ return record_id in self._id_index
58
+
59
+ def __eq__(self, other: Any) -> bool:
60
+ if not isinstance(other, Dataset):
61
+ return NotImplemented
62
+ return self._schema == other._schema and self.to_dict() == other.to_dict()
63
+
64
+ def __repr__(self) -> str:
65
+ return f"Dataset({len(self._records)} records, schema={self._schema})"
66
+
67
+ def __getstate__(self) -> dict:
68
+ """
69
+ _record_cls is a dynamic type() class with no module path, so it
70
+ can't pickle by reference -- dropped here and rebuilt in
71
+ __setstate__. Records are flattened to plain value tuples so every
72
+ record reconstructs through the same rebuilt class.
73
+ """
74
+ state = self.__dict__.copy()
75
+ state["_record_cls"] = None
76
+ if self._schema is not None:
77
+ state["_records"] = [tuple(rec[f] for f in self._schema) for rec in self._records]
78
+ return state
79
+
80
+ def __setstate__(self, state: dict) -> None:
81
+ self.__dict__.update(state)
82
+ if self._schema is not None:
83
+ self._record_cls = BaseRecord.create_new(self._schema)
84
+ rebuilt = []
85
+ id_index = {}
86
+ for values in self._records:
87
+ rec = self._new_record(zip(self._schema, values))
88
+ rebuilt.append(rec)
89
+ id_index[rec[self._id_key]] = rec
90
+ self._records = rebuilt
91
+ self._id_index = id_index
92
+
93
+ # -- statistics -----------------------------------------------------------
94
+
95
+ @property
96
+ def stats(self) -> DatasetStats:
97
+ """
98
+ A fresh, lightweight stats view over this dataset's current records.
99
+ """
100
+ return DatasetStats(self._records, self._schema)
101
+
102
+ # -- schema management --------------------------------------------------
103
+
104
+ @property
105
+ def schema(self) -> tuple | None:
106
+ return self._schema
107
+
108
+ def _set_schema(self, fields: Iterable[Any]) -> None:
109
+ fields = tuple(fields)
110
+ if self._id_key not in fields:
111
+ fields = (self._id_key,) + fields
112
+ self._schema = fields
113
+ self._record_cls = BaseRecord.create_new(fields)
114
+
115
+ def add_field(self, name: Any, default: Any = None, default_factory: Callable[[], Any] | None = None) -> None:
116
+ """
117
+ Add a field to the schema, seeded on every existing record. O(n)
118
+ """
119
+ if name == self._id_key:
120
+ raise ValueError(f"Cannot add the {self._id_key!r} field. It is set as the id key.")
121
+ if self._schema is not None and name in self._schema:
122
+ raise KeyError(f"field {name!r} already exists")
123
+
124
+ value_fn = resolve_default(default, default_factory)
125
+ new_fields = (self._schema or (self._id_key,)) + (name,)
126
+ self._migrate_schema(new_fields, fill={name: value_fn})
127
+
128
+ def remove_field(self, name: Any) -> None:
129
+ """
130
+ Drop a field from the schema across every record. O(n)
131
+ """
132
+ if name == self._id_key:
133
+ raise ValueError(f"Cannot remove the {self._id_key!r} field. It is set as the id key.")
134
+ if self._schema is None or name not in self._schema:
135
+ raise KeyError(name)
136
+
137
+ new_fields = tuple(f for f in self._schema if f != name)
138
+ self._migrate_schema(new_fields)
139
+
140
+ def _new_record(self, field_values: Iterable[tuple[Any, Any]]) -> "BaseRecord":
141
+ """
142
+ Build one instance of this dataset's record class from (field, value) pairs
143
+ """
144
+ instance = self._record_cls()
145
+ for field, value in field_values:
146
+ instance[field] = value
147
+ return instance
148
+
149
+ def _migrate_record(self, existing: "BaseRecord", fill: dict[str, Callable[[], Any]] | None = None) -> "BaseRecord":
150
+ """
151
+ Rebuild `existing` under the current schema.
152
+
153
+ `fill` maps a field to a zero-arg factory for values that don't
154
+ come from `existing` (e.g. a field add_field() just introduced).
155
+ Checked before reading existing[field], since a brand-new field
156
+ won't exist on the old record at all.
157
+ """
158
+ fill = fill or {}
159
+ pairs = ((field, fill[field]() if field in fill else existing[field]) for field in self.schema)
160
+ return self._new_record(pairs)
161
+
162
+ def _migrate_schema(self, new_fields: tuple, fill: dict[str, Callable[[], Any]] | None = None) -> None:
163
+ fill = fill or {}
164
+ old_records = self._records
165
+ self._set_schema(new_fields)
166
+ new_records = []
167
+ new_id_index = {}
168
+ for old in old_records:
169
+ new_record = self._migrate_record(old, fill)
170
+ new_records.append(new_record)
171
+ new_id_index[new_record[self._id_key]] = new_record
172
+
173
+ self.set_data(new_records, new_id_index)
174
+
175
+ def create_empty_clone(self) -> "Dataset":
176
+ """
177
+ A new, empty Dataset with the same schema and missing-default policy
178
+ """
179
+ return Dataset(
180
+ id_key=self._id_key,
181
+ schema=self._schema,
182
+ missing_default_factory=self._missing_default_factory,
183
+ )
184
+
185
+ def set_data(self, data: list[BaseRecord], id_to_index: dict[str, Any] | None = None) -> None:
186
+ """
187
+ Replace the dataset's records directly. Used internally by schema
188
+ migration, sample(), and filter().
189
+
190
+ If `id_to_index` is supplied, the caller is asserting every record
191
+ is already an instance of this dataset's class (true for
192
+ _migrate_schema's output) and no rebuild/check is done. Otherwise,
193
+ records may belong to a different dataset's class (sample()/filter()
194
+ cloning from a parent) -- each is checked and rebuilt through
195
+ _migrate_record if needed, with the id index accumulated in the
196
+ same pass.
197
+ """
198
+ if id_to_index is None:
199
+ records = []
200
+ id_to_index = {}
201
+ for rec in data:
202
+ if type(rec) is not self._record_cls:
203
+ rec = self._migrate_record(rec)
204
+ records.append(rec)
205
+ id_to_index[rec[self._id_key]] = rec
206
+ data = records
207
+
208
+ self._records = data
209
+ self._id_index = id_to_index
210
+ self.reset_cursor()
211
+
212
+ def add(self, data: dict[str, Any]) -> Any:
213
+ """
214
+ Add a new record. Must contain the id field. If no schema is set
215
+ yet, this record's keys define it. Fields the schema expects but
216
+ this record omits get missing_default_factory(). A field not in
217
+ the schema raises -- evolve the schema via add_field() first.
218
+ """
219
+ if self._id_key not in data:
220
+ raise ValueError(f"record must contain the identity field: {self._id_key}")
221
+
222
+ if self._schema is None:
223
+ self._set_schema(data.keys())
224
+ else:
225
+ unknown = set(data.keys()) - set(self._schema)
226
+ if unknown:
227
+ raise KeyError(
228
+ f"field(s) {unknown} not in schema {self._schema}; "
229
+ f"call add_field() first if this is an intentional schema change"
230
+ )
231
+
232
+ if data[self._id_key] in self._id_index:
233
+ raise KeyError(f"Duplicate id key: {data[self._id_key]!r}")
234
+
235
+ rec = self._record_cls()
236
+ for field in self._schema:
237
+ rec[field] = data[field] if field in data else self._missing_default_factory()
238
+
239
+ self._records.append(rec)
240
+ self._id_index[data[self._id_key]] = rec
241
+ return rec
242
+
243
+ def extend(self, items: Iterable[dict[str, Any]]) -> None:
244
+ for item in items:
245
+ self.add(item)
246
+
247
+ def clear(self) -> None:
248
+ self._records.clear()
249
+ self._id_index.clear()
250
+ self.reset_cursor()
251
+
252
+ # -- cursor & positional/id access --------------------------------------
253
+
254
+ def reset_cursor(self) -> None:
255
+ self._cursor = 0
256
+
257
+ def next(self) -> Any:
258
+ """
259
+ Manual, stateful walk over records; pairs with reset_cursor().
260
+ Independent of __iter__, which always starts fresh -- see class docs.
261
+ """
262
+ if self._cursor >= len(self._records):
263
+ raise StopIteration
264
+ rec = self._records[self._cursor]
265
+ self._cursor += 1
266
+ return rec
267
+
268
+ def at(self, idx: int) -> Any:
269
+ return self._records[idx]
270
+
271
+ def get_record(self, record_id: Any) -> Any:
272
+ return self._id_index[record_id]
273
+
274
+ # -- removing whole records ----------------------------------------------
275
+
276
+ def remove_by_id(self, record_id: Any) -> None:
277
+ rec = self._id_index.pop(record_id)
278
+ self._records.remove(rec) # O(n)
279
+
280
+ def remove_by_index(self, i: int) -> None:
281
+ rec = self._records.pop(i) # O(n) unless last element
282
+ del self._id_index[rec[self._id_key]]
283
+
284
+ # -- ordering, sampling, querying -----------------------------------------
285
+
286
+ def get_random(self) -> Any:
287
+ return random.choice(self._records)
288
+
289
+ def shuffle(self) -> None:
290
+ """
291
+ Reorders records only. _id_index maps id -> record object
292
+ regardless of list position, so it needs no rebuild here.
293
+ """
294
+ random.shuffle(self._records)
295
+ self.reset_cursor()
296
+
297
+ def sort(self, key: Callable[[Any], Any], reverse: bool = False) -> None:
298
+ """
299
+ Same reasoning as shuffle(): _id_index stays valid untouched
300
+ """
301
+ self._records.sort(key=key, reverse=reverse)
302
+ self.reset_cursor()
303
+
304
+ def sample(self, n: int) -> "Dataset":
305
+ new = self.create_empty_clone()
306
+ new.set_data(random.sample(self._records, n))
307
+ return new
308
+
309
+ def _filter_iterator(self, func: Callable[[Any], bool]) -> Iterator[BaseRecord]:
310
+ for rec in self._records:
311
+ if func(rec):
312
+ yield rec
313
+
314
+ def filter(self, func: Callable[[Any], bool]) -> "Dataset":
315
+ """
316
+ `func` may be a plain callable/lambda, or a Field(...) expression
317
+ """
318
+ new = self.create_empty_clone()
319
+ new.set_data(list(self._filter_iterator(func)))
320
+ return new
321
+
322
+ def find_first(self, func: Callable[[Any], bool]) -> Optional["BaseRecord"]:
323
+ """
324
+ `func` may be a plain callable/lambda, or a Field(...) expression
325
+ """
326
+ return next(self._filter_iterator(func), None)
327
+
328
+ def group(self, field: str) -> dict[Any, "Dataset"]:
329
+ grouped = {}
330
+ for rec in self._records:
331
+ key = rec[field]
332
+ if key not in grouped:
333
+ grouped[key] = self.create_empty_clone()
334
+ grouped[key].add(rec.to_dict())
335
+ return grouped
336
+
337
+ # -- output ------------------------------------------------------------
338
+
339
+ def to_dict(self) -> list[dict[str, Any]]:
340
+ return [rec.to_dict() for rec in self._records]
@@ -0,0 +1,46 @@
1
+ """
2
+ Composable predicates for filtering datasets.
3
+
4
+ Expressions combine with ``&``, ``|`` and ``~``, and compare fields against
5
+ values or against each other::
6
+
7
+ Field("age") >= 18
8
+ Field("first_name") == Field("nickname")
9
+ (Field("age") > 20) & ~Field("name").contains("test")
10
+
11
+ Every node can also emit a backend query via ``to_sql``, ``to_mongo`` or
12
+ ``to_elasticsearch``.
13
+ """
14
+
15
+ from corekit.data.expressions.comparison import (
16
+ Comparison,
17
+ Contains,
18
+ Equals,
19
+ FieldExpression,
20
+ GreaterThan,
21
+ GreaterThanOrEquals,
22
+ IsIn,
23
+ LessThan,
24
+ LessThanOrEquals,
25
+ NotEquals,
26
+ ValueExpression,
27
+ )
28
+ from corekit.data.expressions.expression import And, Expression, Not, Or
29
+
30
+ __all__ = [
31
+ "And",
32
+ "Comparison",
33
+ "Contains",
34
+ "Equals",
35
+ "Expression",
36
+ "FieldExpression",
37
+ "GreaterThan",
38
+ "GreaterThanOrEquals",
39
+ "IsIn",
40
+ "LessThan",
41
+ "LessThanOrEquals",
42
+ "Not",
43
+ "NotEquals",
44
+ "Or",
45
+ "ValueExpression",
46
+ ]
@@ -0,0 +1,252 @@
1
+ from typing import Any, Iterable
2
+
3
+ from corekit.data.expressions.expression import Expression
4
+
5
+
6
+ class Comparison(Expression):
7
+ """
8
+ Base for comparing two values (Equals, GreaterThan, IsIn, ...).
9
+
10
+ `left`, `right`, and `symbol` are public so external code --
11
+ e.g. a future translator to SQL/Mongo/Elasticsearch -- can walk the
12
+ tree and read out what each node represents without evaluating it.
13
+ `symbol` is a human-readable operator string; the class itself
14
+ (Equals, GreaterThan, etc.) is the canonical, type-safe identifier of
15
+ *which* operation a node represents, so a translator can dispatch on
16
+ isinstance(node, GreaterThan) rather than string-matching on symbol.
17
+ """
18
+
19
+ symbol: str
20
+
21
+ def __init__(self, left: Any, right: Any) -> None:
22
+ self.left = left
23
+ self.right = right
24
+
25
+ def __repr__(self) -> str:
26
+ return f"({self.left!r} {self.symbol} {self.right!r})"
27
+
28
+ @staticmethod
29
+ def _resolve_value(value: Any, *args: Any, **kwargs: Any) -> Any:
30
+ """
31
+ Resolves a value to a concrete value. This method utilizes a neat trick
32
+ to be able to resolve the final value of complex expressions without
33
+ having to evaluate the entire tree.
34
+ """
35
+ if isinstance(value, ValueExpression):
36
+ return value.get(*args, **kwargs)
37
+ return value
38
+
39
+ def get_left(self, *args: Any, **kwargs: Any) -> Any:
40
+ return self._resolve_value(self.left, *args, **kwargs)
41
+
42
+ def get_right(self, *args, **kwargs) -> Any:
43
+ """
44
+ Returns the right value of the comparison.
45
+ """
46
+ return self._resolve_value(self.right, *args, **kwargs)
47
+
48
+
49
+ class Equals(Comparison):
50
+ symbol = "=="
51
+
52
+ def __call__(self, *args: Any, **kwargs: Any) -> bool:
53
+ return self.get_left(*args, **kwargs) == self.get_right(*args, **kwargs)
54
+
55
+ def to_sql(self, *args: Any, **kwargs: Any) -> str:
56
+ return f"{self.get_left(*args, **kwargs)} = {self.get_right(*args, **kwargs)}"
57
+
58
+ def to_mongo(self, *args: Any, **kwargs: Any) -> dict[str, Any] | str:
59
+ return {self.get_left(*args, **kwargs): self.get_right(*args, **kwargs)}
60
+
61
+ def to_elasticsearch(self, *args: Any, **kwargs: Any) -> dict[str, Any] | str:
62
+ return {"term": {self.get_left(*args, **kwargs): self.get_right(*args, **kwargs)}}
63
+
64
+
65
+ class NotEquals(Comparison):
66
+ symbol = "!="
67
+
68
+ def __call__(self, *args: Any, **kwargs: Any) -> bool:
69
+ return self.get_left(*args, **kwargs) != self.get_right(*args, **kwargs)
70
+
71
+ def to_sql(self, *args: Any, **kwargs: Any) -> str:
72
+ return f"{self.get_left(*args, **kwargs)} != {self.get_right(*args, **kwargs)}"
73
+
74
+ def to_mongo(self, *args: Any, **kwargs: Any) -> dict[str, Any] | str:
75
+ return {self.get_left(*args, **kwargs): {"$ne": self.get_right(*args, **kwargs)}}
76
+
77
+ def to_elasticsearch(self, *args: Any, **kwargs: Any) -> dict[str, Any] | str:
78
+ return {"term": {self.get_left(*args, **kwargs): {"$ne": self.get_right(*args, **kwargs)}}}
79
+
80
+
81
+ class LessThan(Comparison):
82
+ symbol = "<"
83
+
84
+ def __call__(self, *args: Any, **kwargs: Any) -> bool:
85
+ return self.get_left(*args, **kwargs) < self.get_right(*args, **kwargs)
86
+
87
+ def to_sql(self, *args: Any, **kwargs: Any) -> str:
88
+ return f"{self.get_left(*args, **kwargs)} < {self.get_right(*args, **kwargs)}"
89
+
90
+ def to_mongo(self, *args: Any, **kwargs: Any) -> dict[str, Any] | str:
91
+ return {self.get_left(*args, **kwargs): {"$lt": self.get_right(*args, **kwargs)}}
92
+
93
+ def to_elasticsearch(self, *args: Any, **kwargs: Any) -> dict[str, Any] | str:
94
+ return {"range": {self.get_left(*args, **kwargs): {"lt": self.get_right(*args, **kwargs)}}}
95
+
96
+
97
+ class LessThanOrEquals(Comparison):
98
+ symbol = "<="
99
+
100
+ def __call__(self, *args: Any, **kwargs: Any) -> bool:
101
+ return self.get_left(*args, **kwargs) <= self.get_right(*args, **kwargs)
102
+
103
+ def to_sql(self, *args: Any, **kwargs: Any) -> str:
104
+ return f"{self.get_left(*args, **kwargs)} <= {self.get_right(*args, **kwargs)}"
105
+
106
+ def to_mongo(self, *args: Any, **kwargs: Any) -> dict[str, Any] | str:
107
+ return {self.get_left(*args, **kwargs): {"$lte": self.get_right(*args, **kwargs)}}
108
+
109
+ def to_elasticsearch(self, *args: Any, **kwargs: Any) -> dict[str, Any] | str:
110
+ return {"range": {self.get_left(*args, **kwargs): {"lte": self.get_right(*args, **kwargs)}}}
111
+
112
+
113
+ class GreaterThan(Comparison):
114
+ symbol = ">"
115
+
116
+ def __call__(self, *args: Any, **kwargs: Any) -> bool:
117
+ return self.get_left(*args, **kwargs) > self.get_right(*args, **kwargs)
118
+
119
+ def to_sql(self, *args: Any, **kwargs: Any) -> str:
120
+ return f"{self.get_left(*args, **kwargs)} > {self.get_right(*args, **kwargs)}"
121
+
122
+ def to_mongo(self, *args: Any, **kwargs: Any) -> dict[str, Any] | str:
123
+ return {self.get_left(*args, **kwargs): {"$gt": self.get_right(*args, **kwargs)}}
124
+
125
+ def to_elasticsearch(self, *args: Any, **kwargs: Any) -> dict[str, Any] | str:
126
+ return {"range": {self.get_left(*args, **kwargs): {"gt": self.get_right(*args, **kwargs)}}}
127
+
128
+
129
+ class GreaterThanOrEquals(Comparison):
130
+ symbol = ">="
131
+
132
+ def __call__(self, *args: Any, **kwargs: Any) -> bool:
133
+ return self.get_left(*args, **kwargs) >= self.get_right(*args, **kwargs)
134
+
135
+ def to_sql(self, *args: Any, **kwargs: Any) -> str:
136
+ return f"{self.get_left(*args, **kwargs)} >= {self.get_right(*args, **kwargs)}"
137
+
138
+ def to_mongo(self, *args: Any, **kwargs: Any) -> dict[str, Any] | str:
139
+ return {self.get_left(*args, **kwargs): {"$gte": self.get_right(*args, **kwargs)}}
140
+
141
+ def to_elasticsearch(self, *args: Any, **kwargs: Any) -> dict[str, Any] | str:
142
+ return {"range": {self.get_left(*args, **kwargs): {"gte": self.get_right(*args, **kwargs)}}}
143
+
144
+
145
+ class IsIn(Comparison):
146
+ symbol = "is in"
147
+
148
+ def __call__(self, *args: Any, **kwargs: Any) -> bool:
149
+ return self.get_left(*args, **kwargs) in self.get_right(*args, **kwargs)
150
+
151
+ def to_sql(self, *args: Any, **kwargs: Any) -> str:
152
+ # TODO: this might not actually be valid SQL
153
+ return f"{self.get_left(*args, **kwargs)} IN {self.get_right(*args, **kwargs)}"
154
+
155
+ def to_mongo(self, *args: Any, **kwargs: Any) -> dict[str, Any] | str:
156
+ return {self.get_left(*args, **kwargs): {"$in": self.get_right(*args, **kwargs)}}
157
+
158
+ def to_elasticsearch(self, *args: Any, **kwargs: Any) -> dict[str, Any] | str:
159
+ return {"terms": {self.get_left(*args, **kwargs): self.get_right(*args, **kwargs)}}
160
+
161
+
162
+ class Contains(Comparison):
163
+ symbol = "contains"
164
+
165
+ def __call__(self, *args: Any, **kwargs: Any) -> bool:
166
+ return self.get_right(*args, **kwargs) in self.get_left(*args, **kwargs)
167
+
168
+ def to_sql(self, *args: Any, **kwargs: Any) -> str:
169
+ return ""
170
+
171
+ def to_mongo(self, *args: Any, **kwargs: Any) -> dict[str, Any] | str:
172
+ return
173
+
174
+ def to_elasticsearch(self, *args: Any, **kwargs: Any) -> dict[str, Any] | str:
175
+ return
176
+
177
+
178
+ class ValueExpression(Expression):
179
+ """
180
+ Expression representing a single value to be compared.
181
+
182
+ The resulting tree (Equals/And/Or/... nodes) is also an inspectable structure,
183
+ not just a callable. These could be used in the future to emit structured queries
184
+ (SQL, MongoDB, Elasticsearch, etc.)
185
+ """
186
+
187
+ def __init__(self, value: Any) -> None:
188
+ self.value = value
189
+
190
+ def __repr__(self) -> str:
191
+ return f"{self.value!r}"
192
+
193
+ def __eq__(self, other: Any) -> "Comparison":
194
+ return Equals(self, other)
195
+
196
+ def __ne__(self, other: Any) -> "Comparison":
197
+ return NotEquals(self, other)
198
+
199
+ def __lt__(self, other: Any) -> "Comparison":
200
+ return LessThan(self, other)
201
+
202
+ def __le__(self, other: Any) -> "Comparison":
203
+ return LessThanOrEquals(self, other)
204
+
205
+ def __gt__(self, other: Any) -> "Comparison":
206
+ return GreaterThan(self, other)
207
+
208
+ def __ge__(self, other: Any) -> "Comparison":
209
+ return GreaterThanOrEquals(self, other)
210
+
211
+ def isin(self, values: Iterable[Any]) -> "Comparison":
212
+ return IsIn(self, frozenset(values))
213
+
214
+ def contains(self, substring: Any) -> "Comparison":
215
+ return Contains(self, substring)
216
+
217
+ def get(self, *args: Any, **kwargs: Any) -> Any:
218
+ return self.value
219
+
220
+ def to_sql(self, *args: Any, **kwargs: Any) -> str:
221
+ return str(self.value)
222
+
223
+ def to_mongo(self, *args: Any, **kwargs: Any) -> dict[str, Any] | str:
224
+ return self.value
225
+
226
+ def to_elasticsearch(self, *args: Any, **kwargs: Any) -> dict[str, Any] | str:
227
+ return self.value
228
+
229
+
230
+ class FieldExpression(ValueExpression):
231
+ """
232
+ A value expression representing a named field reference for building filter predicates.
233
+ FieldExpression instances will evaluate against anything that supports __getitem__ by field name.
234
+ """
235
+
236
+ def __call__(self, record: Any, *args, **kwargs) -> bool:
237
+ return bool(record[self.value])
238
+
239
+ def __repr__(self) -> str:
240
+ return f"Field({self.value!r})"
241
+
242
+ def get(self, record: Any, *args: Any, **kwargs: Any) -> Any:
243
+ return record[self.value]
244
+
245
+ def to_sql(self, record: Any, *args: Any, **kwargs: Any) -> str:
246
+ return record[self.value]
247
+
248
+ def to_mongo(self, record: Any, *args: Any, **kwargs: Any) -> dict[str, Any] | str:
249
+ return record[self.value]
250
+
251
+ def to_elasticsearch(self, record: Any, *args: Any, **kwargs: Any) -> dict[str, Any] | str:
252
+ return record[self.value]