moose-lib 0.6.81__tar.gz → 0.6.82__tar.gz

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 moose-lib might be problematic. Click here for more details.

Files changed (48) hide show
  1. {moose_lib-0.6.81 → moose_lib-0.6.82}/PKG-INFO +1 -1
  2. {moose_lib-0.6.81 → moose_lib-0.6.82}/moose_lib/__init__.py +2 -0
  3. {moose_lib-0.6.81 → moose_lib-0.6.82}/moose_lib/data_models.py +5 -0
  4. {moose_lib-0.6.81 → moose_lib-0.6.82}/moose_lib/main.py +28 -20
  5. moose_lib-0.6.82/moose_lib/query_builder.py +198 -0
  6. moose_lib-0.6.82/moose_lib/utilities/sql.py +34 -0
  7. {moose_lib-0.6.81 → moose_lib-0.6.82}/moose_lib.egg-info/PKG-INFO +1 -1
  8. {moose_lib-0.6.81 → moose_lib-0.6.82}/moose_lib.egg-info/SOURCES.txt +2 -0
  9. moose_lib-0.6.82/tests/test_query_builder.py +38 -0
  10. moose_lib-0.6.81/moose_lib/utilities/sql.py +0 -10
  11. {moose_lib-0.6.81 → moose_lib-0.6.82}/README.md +0 -0
  12. {moose_lib-0.6.81 → moose_lib-0.6.82}/moose_lib/blocks.py +0 -0
  13. {moose_lib-0.6.81 → moose_lib-0.6.82}/moose_lib/clients/__init__.py +0 -0
  14. {moose_lib-0.6.81 → moose_lib-0.6.82}/moose_lib/clients/redis_client.py +0 -0
  15. {moose_lib-0.6.81 → moose_lib-0.6.82}/moose_lib/commons.py +0 -0
  16. {moose_lib-0.6.81 → moose_lib-0.6.82}/moose_lib/config/__init__.py +0 -0
  17. {moose_lib-0.6.81 → moose_lib-0.6.82}/moose_lib/config/config_file.py +0 -0
  18. {moose_lib-0.6.81 → moose_lib-0.6.82}/moose_lib/config/runtime.py +0 -0
  19. {moose_lib-0.6.81 → moose_lib-0.6.82}/moose_lib/dmv2/__init__.py +0 -0
  20. {moose_lib-0.6.81 → moose_lib-0.6.82}/moose_lib/dmv2/_registry.py +0 -0
  21. {moose_lib-0.6.81 → moose_lib-0.6.82}/moose_lib/dmv2/consumption.py +0 -0
  22. {moose_lib-0.6.81 → moose_lib-0.6.82}/moose_lib/dmv2/ingest_api.py +0 -0
  23. {moose_lib-0.6.81 → moose_lib-0.6.82}/moose_lib/dmv2/ingest_pipeline.py +0 -0
  24. {moose_lib-0.6.81 → moose_lib-0.6.82}/moose_lib/dmv2/life_cycle.py +0 -0
  25. {moose_lib-0.6.81 → moose_lib-0.6.82}/moose_lib/dmv2/materialized_view.py +0 -0
  26. {moose_lib-0.6.81 → moose_lib-0.6.82}/moose_lib/dmv2/olap_table.py +0 -0
  27. {moose_lib-0.6.81 → moose_lib-0.6.82}/moose_lib/dmv2/registry.py +0 -0
  28. {moose_lib-0.6.81 → moose_lib-0.6.82}/moose_lib/dmv2/sql_resource.py +0 -0
  29. {moose_lib-0.6.81 → moose_lib-0.6.82}/moose_lib/dmv2/stream.py +0 -0
  30. {moose_lib-0.6.81 → moose_lib-0.6.82}/moose_lib/dmv2/types.py +0 -0
  31. {moose_lib-0.6.81 → moose_lib-0.6.82}/moose_lib/dmv2/view.py +0 -0
  32. {moose_lib-0.6.81 → moose_lib-0.6.82}/moose_lib/dmv2/workflow.py +0 -0
  33. {moose_lib-0.6.81 → moose_lib-0.6.82}/moose_lib/dmv2_serializer.py +0 -0
  34. {moose_lib-0.6.81 → moose_lib-0.6.82}/moose_lib/internal.py +0 -0
  35. {moose_lib-0.6.81 → moose_lib-0.6.82}/moose_lib/query_param.py +0 -0
  36. {moose_lib-0.6.81 → moose_lib-0.6.82}/moose_lib/streaming/__init__.py +0 -0
  37. {moose_lib-0.6.81 → moose_lib-0.6.82}/moose_lib/streaming/streaming_function_runner.py +0 -0
  38. {moose_lib-0.6.81 → moose_lib-0.6.82}/moose_lib/utilities/__init__.py +0 -0
  39. {moose_lib-0.6.81 → moose_lib-0.6.82}/moose_lib.egg-info/dependency_links.txt +0 -0
  40. {moose_lib-0.6.81 → moose_lib-0.6.82}/moose_lib.egg-info/requires.txt +0 -0
  41. {moose_lib-0.6.81 → moose_lib-0.6.82}/moose_lib.egg-info/top_level.txt +0 -0
  42. {moose_lib-0.6.81 → moose_lib-0.6.82}/setup.cfg +0 -0
  43. {moose_lib-0.6.81 → moose_lib-0.6.82}/setup.py +0 -0
  44. {moose_lib-0.6.81 → moose_lib-0.6.82}/tests/__init__.py +0 -0
  45. {moose_lib-0.6.81 → moose_lib-0.6.82}/tests/conftest.py +0 -0
  46. {moose_lib-0.6.81 → moose_lib-0.6.82}/tests/test_moose.py +0 -0
  47. {moose_lib-0.6.81 → moose_lib-0.6.82}/tests/test_redis_client.py +0 -0
  48. {moose_lib-0.6.81 → moose_lib-0.6.82}/tests/test_s3queue_config.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: moose_lib
3
- Version: 0.6.81
3
+ Version: 0.6.82
4
4
  Home-page: https://www.fiveonefour.com/moose
5
5
  Author: Fiveonefour Labs Inc.
6
6
  Author-email: support@fiveonefour.com
@@ -26,3 +26,5 @@ from .blocks import (
26
26
  )
27
27
  from .data_models import Key, AggregateFunction, StringToEnumMixin
28
28
  from .commons import Logger
29
+
30
+ from .query_builder import *
@@ -147,6 +147,11 @@ class Column(BaseModel):
147
147
  default: str | None = None
148
148
  annotations: list[Tuple[str, Any]] = []
149
149
 
150
+ def to_expr(self):
151
+ # Lazy import to avoid circular dependency at import time
152
+ from .query_builder import ColumnRef
153
+ return ColumnRef(self)
154
+
150
155
 
151
156
  def py_type_to_column_type(t: type, mds: list[Any]) -> Tuple[bool, list[Any], DataType]:
152
157
  # handle Annotated[Optional[Annotated[...], ...]
@@ -27,6 +27,7 @@ from .data_models import Column
27
27
  from .config.runtime import RuntimeClickHouseConfig
28
28
 
29
29
  from moose_lib.commons import EnhancedJSONEncoder
30
+ from .query_builder import Query
30
31
 
31
32
 
32
33
  @dataclass
@@ -184,7 +185,29 @@ class QueryClient:
184
185
  def __call__(self, input, variables):
185
186
  return self.execute(input, variables)
186
187
 
187
- def execute(self, input, variables, row_type: Type[BaseModel] = None):
188
+ def execute(self, input: Union[str, Query], variables = None, row_type: Type[BaseModel] = None):
189
+ """
190
+ Execute a query.
191
+
192
+ - If `input` is a `Query`, do not supply `variables`.
193
+ - If `input` is a `str` intended for `string.Formatter` interpolation, `variables` must be a dict
194
+ mapping placeholder names to values.
195
+
196
+ Args:
197
+ input: Either a `Query` object or a `Formatter`-style SQL template string.
198
+ variables: Dict used to fill a `Formatter` string; must be omitted when `input` is a `Query`.
199
+ row_type: Optional Pydantic model class to map result rows into.
200
+
201
+ Returns:
202
+ A list of row dicts, or a list of `row_type` instances if provided.
203
+ """
204
+ if isinstance(input, Query):
205
+ if variables is not None:
206
+ raise ValueError("Do not supply variables when you provide Query")
207
+ sql, params = input.to_sql_and_params()
208
+ print(f"[QueryClient] | Query: {sql}")
209
+ return self.execute_raw(sql, params, row_type)
210
+
188
211
  params = {}
189
212
  values: dict[str, Any] = {}
190
213
  preview_params = {}
@@ -200,25 +223,10 @@ class QueryClient:
200
223
  params[variable_name] = f'{{p{i}: Identifier}}'
201
224
  values[f'p{i}'] = value.name
202
225
  else:
203
- if isinstance(value, bool):
204
- params[variable_name] = f'{{p{i}: Bool}}'
205
- values[f'p{i}'] = value
206
- elif isinstance(value, datetime):
207
- params[variable_name] = f'{{p{i}: DateTime}}'
208
- values[f'p{i}'] = value
209
- elif isinstance(value, int):
210
- params[variable_name] = f'{{p{i}: Int64}}'
211
- values[f'p{i}'] = value
212
- elif isinstance(value, float):
213
- params[variable_name] = f'{{p{i}: Float64}}'
214
- values[f'p{i}'] = value
215
- elif isinstance(value, str):
216
- params[variable_name] = f'{{p{i}: String}}'
217
- values[f'p{i}'] = value
218
- else:
219
- print(f"unhandled type in QueryClient {type(value)}", file=sys.stderr)
220
- params[variable_name] = f'{{p{i}: String}}'
221
- values[f'p{i}'] = str(value)
226
+ from moose_lib.utilities.sql import clickhouse_param_type_for_value
227
+ ch_type = clickhouse_param_type_for_value(value)
228
+ params[variable_name] = f'{{p{i}: {ch_type}}}'
229
+ values[f'p{i}'] = value
222
230
  preview_params[variable_name] = self._format_value_for_preview(value)
223
231
 
224
232
  clickhouse_query = input.format_map(params)
@@ -0,0 +1,198 @@
1
+ from datetime import datetime
2
+ from typing import Callable, Literal
3
+
4
+ import sqlglot as sg
5
+ import sqlglot.expressions as sge
6
+ from pydantic import BaseModel
7
+
8
+ from .data_models import Column, Key
9
+ from .dmv2 import OlapTable, IngestPipeline, IngestPipelineConfig
10
+ from moose_lib.utilities.sql import clickhouse_param_type_for_value
11
+
12
+
13
+ class Params:
14
+ def __init__(self):
15
+ self._counter = 0
16
+ self.bindings: dict[str, object] = {}
17
+
18
+ def bind(self, value: object, name: str | None = None, ch_type: str | None = None) -> sge.Expression:
19
+ if name is None:
20
+ name = f"p{self._counter}"
21
+ self._counter += 1
22
+
23
+ if ch_type is None:
24
+ ch_type = clickhouse_param_type_for_value(value)
25
+
26
+ expr = sg.parse_one(f"{{{name}: {ch_type}}}", dialect="clickhouse")
27
+ self.bindings[name] = value
28
+ return expr
29
+
30
+
31
+ def to_column(col: Column, table_name: str | None = None) -> sge.Column:
32
+ col_name = getattr(col, "name")
33
+ table_ident = None
34
+ if table_name is not None:
35
+ table_ident = sge.Identifier(this=table_name, quoted=True)
36
+ elif hasattr(col, "table_name") and getattr(col, "table_name"):
37
+ table_ident = sge.Identifier(this=getattr(col, "table_name"), quoted=True)
38
+
39
+ return sge.Column(
40
+ this=sge.Identifier(this=col_name, quoted=True),
41
+ table=table_ident,
42
+ )
43
+
44
+
45
+ type Predicate = Callable[["Query"], sge.Expression]
46
+
47
+ # Order-by type annotations
48
+ type OrderDirection = Literal["asc", "desc"]
49
+ type OrderExpr = sge.Expression | Column | "ColumnRef"
50
+ type OrderItem = OrderExpr | tuple[OrderExpr, OrderDirection]
51
+
52
+
53
+ class ColumnRef:
54
+ def __init__(self, column: Column):
55
+ self._column = column
56
+
57
+ def _binary_op(self, op_name: str, value: object) -> Predicate:
58
+ def resolve(query: "Query") -> sge.Expression:
59
+ table_name = query._from_table.name if query._from_table is not None else None
60
+ left = to_column(self._column, table_name)
61
+ right = query.params.bind(value)
62
+ op = getattr(left, op_name)
63
+ return op(right)
64
+
65
+ return resolve
66
+
67
+ def eq(self, value: object) -> Predicate:
68
+ return self._binary_op("eq", value)
69
+
70
+ def ne(self, value: object) -> Predicate:
71
+ return self._binary_op("neq", value)
72
+
73
+ def lt(self, value: object) -> Predicate:
74
+ return self._binary_op("__lt__", value)
75
+
76
+ def le(self, value: object) -> Predicate:
77
+ return self._binary_op("__le__", value)
78
+
79
+ def gt(self, value: object) -> Predicate:
80
+ return self._binary_op("__gt__", value)
81
+
82
+ def ge(self, value: object) -> Predicate:
83
+ return self._binary_op("__ge__", value)
84
+
85
+ def in_(self, values: list[object]) -> Predicate:
86
+ def resolve(query: "Query") -> sge.Expression:
87
+ table_name = query._from_table.name if query._from_table is not None else None
88
+ left = to_column(self._column, table_name)
89
+ rights = [query.params.bind(v) for v in values]
90
+ return left.isin(*rights)
91
+
92
+ return resolve
93
+
94
+ def is_null(self) -> Predicate:
95
+ def resolve(query: "Query") -> sge.Expression:
96
+ table_name = query._from_table.name if query._from_table is not None else None
97
+ left = to_column(self._column, table_name)
98
+ return left.is_(sge.Null())
99
+
100
+ return resolve
101
+
102
+
103
+ def col(column: Column) -> ColumnRef:
104
+ return ColumnRef(column)
105
+
106
+
107
+ class Query:
108
+ def __init__(self):
109
+ self.params = Params()
110
+ self.inner: sge.Select = sge.Select()
111
+ self._from_table: OlapTable | None = None
112
+
113
+ def from_(self, table: OlapTable) -> "Query":
114
+ self._from_table = table
115
+ self.inner = self.inner.from_(table.name)
116
+ return self
117
+
118
+ def select(self, *cols: Column) -> "Query":
119
+ sge_cols = [to_column(c, self._from_table.name if self._from_table is not None else None) for c in cols]
120
+ self.inner = self.inner.select(*sge_cols)
121
+ return self
122
+
123
+ def where(self, predicate_or_expr) -> "Query":
124
+ if callable(predicate_or_expr):
125
+ expr = predicate_or_expr(self)
126
+ else:
127
+ expr = predicate_or_expr
128
+ self.inner = self.inner.where(expr)
129
+ return self
130
+
131
+ def order_by(self, *items: OrderItem) -> "Query":
132
+ orders: list[sge.Expression] = []
133
+ table_name = self._from_table.name if self._from_table is not None else None
134
+
135
+ for item in items:
136
+ desc = False
137
+ expr: sge.Expression
138
+
139
+ if isinstance(item, tuple) and len(item) == 2:
140
+ col_like, direction = item
141
+ if isinstance(direction, str):
142
+ desc = direction.lower() == "desc"
143
+ else:
144
+ raise ValueError("order_by direction must be 'asc' or 'desc'")
145
+ if isinstance(col_like, Column):
146
+ expr = to_column(col_like, table_name)
147
+ elif isinstance(col_like, ColumnRef):
148
+ expr = to_column(col_like._column, table_name)
149
+ else:
150
+ expr = col_like
151
+ else:
152
+ if isinstance(item, Column):
153
+ expr = to_column(item, table_name)
154
+ elif isinstance(item, ColumnRef):
155
+ expr = to_column(item._column, table_name)
156
+ else:
157
+ expr = item
158
+
159
+ orders.append(sge.Ordered(this=expr, desc=desc))
160
+
161
+ self.inner = self.inner.order_by(*orders)
162
+ return self
163
+
164
+ def to_sql(self) -> str:
165
+ return self.inner.sql(dialect="clickhouse")
166
+
167
+ def to_sql_and_params(self) -> tuple[str, dict[str, object]]:
168
+ return self.to_sql(), dict(self.params.bindings)
169
+
170
+ def limit(self, n: int) -> "Query":
171
+ self.inner = self.inner.limit(n)
172
+ return self
173
+
174
+
175
+ def and_(*predicates_or_exprs) -> Predicate:
176
+ def resolve(query: "Query") -> sge.Expression:
177
+ exprs = [p(query) if callable(p) else p for p in predicates_or_exprs]
178
+ if not exprs:
179
+ raise ValueError("and_ requires at least one predicate")
180
+ combined = exprs[0]
181
+ for e in exprs[1:]:
182
+ combined = combined.and_(e)
183
+ return combined
184
+
185
+ return resolve
186
+
187
+
188
+ def or_(*predicates_or_exprs) -> Predicate:
189
+ def resolve(query: "Query") -> sge.Expression:
190
+ exprs = [p(query) if callable(p) else p for p in predicates_or_exprs]
191
+ if not exprs:
192
+ raise ValueError("or_ requires at least one predicate")
193
+ combined = exprs[0]
194
+ for e in exprs[1:]:
195
+ combined = combined.or_(e)
196
+ return combined
197
+
198
+ return resolve
@@ -0,0 +1,34 @@
1
+ import sys
2
+
3
+
4
+ def quote_identifier(name: str) -> str:
5
+ """Quote a ClickHouse identifier with backticks if not already quoted.
6
+
7
+ Backticks allow special characters (e.g., hyphens) in identifiers.
8
+ """
9
+ if name.startswith("`") and name.endswith("`"):
10
+ return name
11
+ return f"`{name}`"
12
+
13
+
14
+ from datetime import datetime
15
+ from typing import Any
16
+
17
+
18
+ def clickhouse_param_type_for_value(value: Any) -> str:
19
+ """Infer ClickHouse typed parameter annotation for a Python value.
20
+
21
+ Normalized to common scalar types used in placeholders.
22
+ """
23
+ if isinstance(value, bool):
24
+ return "Bool"
25
+ if isinstance(value, int):
26
+ return "Int64"
27
+ if isinstance(value, float):
28
+ return "Float64"
29
+ if isinstance(value, datetime):
30
+ return "DateTime"
31
+ if not isinstance(value, str):
32
+ print(f"unhandled type {type(value)}", file=sys.stderr)
33
+ return "String"
34
+
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: moose_lib
3
- Version: 0.6.81
3
+ Version: 0.6.82
4
4
  Home-page: https://www.fiveonefour.com/moose
5
5
  Author: Fiveonefour Labs Inc.
6
6
  Author-email: support@fiveonefour.com
@@ -7,6 +7,7 @@ moose_lib/data_models.py
7
7
  moose_lib/dmv2_serializer.py
8
8
  moose_lib/internal.py
9
9
  moose_lib/main.py
10
+ moose_lib/query_builder.py
10
11
  moose_lib/query_param.py
11
12
  moose_lib.egg-info/PKG-INFO
12
13
  moose_lib.egg-info/SOURCES.txt
@@ -39,5 +40,6 @@ moose_lib/utilities/sql.py
39
40
  tests/__init__.py
40
41
  tests/conftest.py
41
42
  tests/test_moose.py
43
+ tests/test_query_builder.py
42
44
  tests/test_redis_client.py
43
45
  tests/test_s3queue_config.py
@@ -0,0 +1,38 @@
1
+ from datetime import datetime
2
+
3
+ from moose_lib.query_builder import Query, col
4
+ from moose_lib.dmv2 import IngestPipeline, IngestPipelineConfig
5
+ from pydantic import BaseModel
6
+ from moose_lib.data_models import Key
7
+
8
+
9
+ class Bar(BaseModel):
10
+ primary_key: Key[str]
11
+ utc_timestamp: datetime
12
+ has_text: bool
13
+ text_length: int
14
+
15
+
16
+ def test_simple_select_and_where():
17
+ bar_model = IngestPipeline[Bar]("Bar", IngestPipelineConfig(
18
+ ingest=False,
19
+ stream=True,
20
+ table=True,
21
+ dead_letter_queue=True
22
+ ))
23
+ bar_cols = bar_model.get_table().cols
24
+
25
+ q1 = Query().from_(bar_model.get_table()).select(bar_cols.has_text, bar_cols.text_length)
26
+ assert q1.to_sql() == 'SELECT "Bar"."has_text", "Bar"."text_length" FROM Bar'
27
+
28
+ q2 = (
29
+ Query()
30
+ .from_(bar_model.get_table())
31
+ .select(bar_cols.has_text, bar_cols.text_length)
32
+ .where(col(bar_cols.has_text).eq(True))
33
+ )
34
+ sql, params = q2.to_sql_and_params()
35
+ assert sql == 'SELECT "Bar"."has_text", "Bar"."text_length" FROM Bar WHERE "Bar"."has_text" = {p0: Bool}'
36
+ assert params == {"p0": True}
37
+
38
+
@@ -1,10 +0,0 @@
1
- def quote_identifier(name: str) -> str:
2
- """Quote a ClickHouse identifier with backticks if not already quoted.
3
-
4
- Backticks allow special characters (e.g., hyphens) in identifiers.
5
- """
6
- if name.startswith("`") and name.endswith("`"):
7
- return name
8
- return f"`{name}`"
9
-
10
-
File without changes
File without changes
File without changes
File without changes
File without changes