fquery 0.2__tar.gz → 0.4__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.
Files changed (43) hide show
  1. {fquery-0.2 → fquery-0.4}/PKG-INFO +2 -1
  2. {fquery-0.2 → fquery-0.4}/fquery/malloy_builder.py +0 -4
  3. fquery-0.4/fquery/polars_builder.py +53 -0
  4. fquery-0.4/fquery/pydantic.py +46 -0
  5. {fquery-0.2 → fquery-0.4}/fquery/query.py +11 -0
  6. fquery-0.4/fquery/sqlmodel.py +283 -0
  7. {fquery-0.2 → fquery-0.4}/fquery.egg-info/PKG-INFO +2 -1
  8. {fquery-0.2 → fquery-0.4}/fquery.egg-info/SOURCES.txt +4 -0
  9. fquery-0.4/fquery.egg-info/requires.txt +13 -0
  10. {fquery-0.2 → fquery-0.4}/setup.py +8 -2
  11. {fquery-0.2 → fquery-0.4}/tests/benchmark.py +28 -2
  12. fquery-0.4/tests/test_polars.py +36 -0
  13. fquery-0.4/tests/test_pydantic.py +34 -0
  14. {fquery-0.2 → fquery-0.4}/tests/test_sqlmodel.py +32 -10
  15. fquery-0.2/fquery/sqlmodel.py +0 -96
  16. fquery-0.2/fquery.egg-info/requires.txt +0 -9
  17. {fquery-0.2 → fquery-0.4}/LICENSE +0 -0
  18. {fquery-0.2 → fquery-0.4}/README.md +0 -0
  19. {fquery-0.2 → fquery-0.4}/fquery/__init__.py +0 -0
  20. {fquery-0.2 → fquery-0.4}/fquery/aitertools.py +0 -0
  21. {fquery-0.2 → fquery-0.4}/fquery/async_utils.py +0 -0
  22. {fquery-0.2 → fquery-0.4}/fquery/django.py +0 -0
  23. {fquery-0.2 → fquery-0.4}/fquery/execute.py +0 -0
  24. {fquery-0.2 → fquery-0.4}/fquery/fgraphql.py +0 -0
  25. {fquery-0.2 → fquery-0.4}/fquery/resolve.py +0 -0
  26. {fquery-0.2 → fquery-0.4}/fquery/sql_builder.py +0 -0
  27. {fquery-0.2 → fquery-0.4}/fquery/view_model.py +0 -0
  28. {fquery-0.2 → fquery-0.4}/fquery/visitor.py +0 -0
  29. {fquery-0.2 → fquery-0.4}/fquery/walk.py +0 -0
  30. {fquery-0.2 → fquery-0.4}/fquery.egg-info/dependency_links.txt +0 -0
  31. {fquery-0.2 → fquery-0.4}/fquery.egg-info/top_level.txt +0 -0
  32. {fquery-0.2 → fquery-0.4}/pyproject.toml +0 -0
  33. {fquery-0.2 → fquery-0.4}/setup.cfg +0 -0
  34. {fquery-0.2 → fquery-0.4}/tests/__init__.py +0 -0
  35. {fquery-0.2 → fquery-0.4}/tests/async_test.py +0 -0
  36. {fquery-0.2 → fquery-0.4}/tests/django_example_model.py +0 -0
  37. {fquery-0.2 → fquery-0.4}/tests/graphql_mock_user.py +0 -0
  38. {fquery-0.2 → fquery-0.4}/tests/mock_user.py +0 -0
  39. {fquery-0.2 → fquery-0.4}/tests/test_malloy.py +0 -0
  40. {fquery-0.2 → fquery-0.4}/tests/test_operators.py +0 -0
  41. {fquery-0.2 → fquery-0.4}/tests/test_sql.py +0 -0
  42. {fquery-0.2 → fquery-0.4}/tests/test_walk.py +0 -0
  43. {fquery-0.2 → fquery-0.4}/tests/test_walk_obj.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.1
2
2
  Name: fquery
3
- Version: 0.2
3
+ Version: 0.4
4
4
  Summary: A graph query engine
5
5
  Home-page: https://github.com/adsharma/fquery
6
6
  Classifier: Programming Language :: Python :: 3
@@ -9,6 +9,7 @@ Classifier: Operating System :: OS Independent
9
9
  Requires-Python: >=3.6
10
10
  Description-Content-Type: text/markdown
11
11
  Provides-Extra: SQL
12
+ Provides-Extra: df
12
13
  Provides-Extra: graphql
13
14
  License-File: LICENSE
14
15
 
@@ -1,7 +1,3 @@
1
- # Copyright (c) Facebook, Inc. and its affiliates.
2
- #
3
- # This source code is licensed under the MIT license found in the
4
- # LICENSE file in the root directory of this source tree.
5
1
  import ast
6
2
  import operator
7
3
 
@@ -0,0 +1,53 @@
1
+ import ast
2
+ import operator
3
+
4
+ import polars as pl
5
+
6
+ from .visitor import Visitor
7
+
8
+ # inspired from pandas.core.computation.ops
9
+ _cmp_ops_syms = (">", "<", ">=", "<=", "==", "!=")
10
+ _cmp_ops_funcs = (
11
+ operator.gt,
12
+ operator.lt,
13
+ operator.ge,
14
+ operator.le,
15
+ operator.eq,
16
+ operator.ne,
17
+ )
18
+ _cmp_ops_dict = dict(zip(_cmp_ops_syms, _cmp_ops_funcs))
19
+
20
+
21
+ class PolarsBuilderVisitor(Visitor):
22
+
23
+ def __init__(self, id1s):
24
+ self.polars = None
25
+ self.polars_stack = []
26
+ self.visited = set()
27
+
28
+ async def visit_leaf(self, query):
29
+ # TODO: make this columnar and real lazy instead of faking laziness
30
+ self.polars = pl.DataFrame(await query.as_list()).lazy()
31
+ while self.polars_stack:
32
+ func, params = self.polars_stack.pop()
33
+ self.polars = getattr(self.polars, func)(params)
34
+
35
+ async def visit_project(self, query):
36
+ self.polars_stack.append(("select", query.projector))
37
+ await self.visit(query.child)
38
+
39
+ async def visit_take(self, query):
40
+ self.polars_stack.append(("limit", query._count))
41
+ await self.visit(query.child)
42
+
43
+ async def visit_where(self, query):
44
+ left, op, right = query._expr.value.split()
45
+ right = ast.literal_eval(right)
46
+ table, field = left.split(".") if "." in left else (self.malloy, left)
47
+ self.polars_stack.append(("filter", (_cmp_ops_dict[op](pl.col(field), right))))
48
+ await self.visit(query.child)
49
+
50
+ async def visit_order_by(self, query):
51
+ table, field = query._expr.value.split(".")
52
+ self.polars_stack.append(("sort", field))
53
+ await self.visit(query.child)
@@ -0,0 +1,46 @@
1
+ import dataclasses
2
+ from dataclasses import dataclass, fields
3
+ from typing import Type
4
+
5
+ from pydantic import BaseModel, ConfigDict, Field
6
+
7
+
8
+ def pydantic(cls):
9
+ return model(dataclass(kw_only=True)(cls))
10
+
11
+
12
+ def validator(self) -> BaseModel:
13
+ attrs = {name: getattr(self, name) for name in self.__pydantic__.model_fields}
14
+ return self.__pydantic__(**attrs)
15
+
16
+
17
+ def get_field_def(cls, field):
18
+ # if the dataclass has a default_factory, or a default value, use it in pydantic Field
19
+ kwargs = {}
20
+ if not isinstance(field.default, dataclasses._MISSING_TYPE):
21
+ kwargs["default"] = field.default
22
+ if not isinstance(field.default_factory, dataclasses._MISSING_TYPE):
23
+ kwargs["default_factory"] = field.default_factory
24
+ return Field(**kwargs)
25
+
26
+
27
+ def model(cls: Type) -> Type:
28
+ """
29
+ Decorator to convert a dataclass to a Pydantic model.
30
+ """
31
+ # Generate the SQLModel class
32
+ pydantic_cls = type(
33
+ cls.__name__ + "Model",
34
+ (BaseModel,),
35
+ {
36
+ # Add type annotations to the generated fields
37
+ "__annotations__": {**{field.name: field.type for field in fields(cls)}},
38
+ # Actual field defs
39
+ **{field.name: get_field_def(cls, field) for field in fields(cls)},
40
+ },
41
+ )
42
+ cls.__pydantic__ = pydantic_cls
43
+ cls.model_config = ConfigDict(extra="ignore")
44
+ cls.validator = validator
45
+
46
+ return cls
@@ -12,6 +12,7 @@ from typing import Dict, List, Optional, Tuple, Type, Union
12
12
  from .async_utils import wait_for
13
13
  from .execute import AbstractSyntaxTreeVisitor
14
14
  from .malloy_builder import MalloyBuilderVisitor
15
+ from .polars_builder import PolarsBuilderVisitor
15
16
  from .sql_builder import SQLBuilderVisitor
16
17
  from .view_model import ViewModel, get_edges, get_return_type
17
18
  from .walk import (
@@ -256,6 +257,16 @@ class Query:
256
257
  wait_for(visitor.visit(self))
257
258
  return visitor.malloy
258
259
 
260
+ def to_polars(self) -> Tree:
261
+ visitor = PolarsBuilderVisitor([])
262
+ wait_for(visitor.visit(self))
263
+ return visitor.polars.collect()
264
+
265
+ async def to_async_polars(self) -> Tree:
266
+ visitor = PolarsBuilderVisitor([])
267
+ await visitor.visit(self)
268
+ return await visitor.polars.collect_async()
269
+
259
270
  def batch_resolve_objs(self) -> List[Dict[str, List[ViewModel]]]:
260
271
  return [{str(None): [o for o in (self.resolve_obj(i) for i in self.ids) if o]}]
261
272
 
@@ -0,0 +1,283 @@
1
+ import dataclasses
2
+ from dataclasses import _FIELD, dataclass, field, fields, is_dataclass
3
+ from datetime import date, datetime, time
4
+ from typing import (
5
+ ClassVar,
6
+ Dict,
7
+ ForwardRef,
8
+ List,
9
+ Optional,
10
+ Union,
11
+ get_args,
12
+ get_origin,
13
+ get_type_hints,
14
+ )
15
+
16
+ import inflection
17
+ from sqlalchemy import (
18
+ JSON,
19
+ Boolean,
20
+ Date,
21
+ DateTime,
22
+ Float,
23
+ Integer,
24
+ LargeBinary,
25
+ Sequence,
26
+ String,
27
+ Time,
28
+ )
29
+ from sqlalchemy.orm.base import Mapped
30
+ from sqlmodel import Column, Field, Relationship, SQLModel
31
+
32
+ SA_TYPEMAP = {
33
+ int: Integer,
34
+ int | None: Integer,
35
+ float: Float,
36
+ str: String,
37
+ bool: Boolean,
38
+ datetime: DateTime,
39
+ date: Date,
40
+ time: Time,
41
+ bytes: LargeBinary, # or Binary for smaller data
42
+ Dict: JSON,
43
+ }
44
+
45
+ GLOBAL_ID_SEQ = Sequence("global_id_seq") # define sequence explicitly
46
+ SQL_PK = {"metadata": {"SQL": {"primary_key": True}}}
47
+
48
+
49
+ def unique():
50
+ return field(default=None, metadata={"SQL": {"unique": True}})
51
+
52
+
53
+ def foreign_key(name):
54
+ return field(
55
+ default=None,
56
+ metadata={
57
+ "SQL": {"relationship": True, "back_populates": False, "fk_name": name}
58
+ },
59
+ )
60
+
61
+
62
+ def one_to_many():
63
+ return field(default=None, metadata={"SQL": {"relationship": True}})
64
+
65
+
66
+ def many_to_one(key_column=None, back_populates=None):
67
+ ret = field(
68
+ default=None, metadata={"SQL": {"relationship": True, "many_to_one": True}}
69
+ )
70
+ # if key_column is None, we default to {key_table_name}.id
71
+ if key_column is not None:
72
+ ret.metadata["SQL"]["key_column"] = key_column
73
+ if back_populates is not None:
74
+ ret.metadata["SQL"]["back_populates"] = back_populates
75
+ return ret
76
+
77
+
78
+ def sqlmodel(cls):
79
+ return model()(dataclass(kw_only=True)(cls))
80
+
81
+
82
+ def model(table: bool = True, table_name: str = None, global_id: bool = False):
83
+ """
84
+ A decorator that generates a SQLModel from a dataclass.
85
+
86
+ Args:
87
+ table_name (str): The name of the database table. Defaults to the name of the dataclass.
88
+
89
+ Returns:
90
+ A decorator that generates a SQLModel from a dataclass.
91
+ """
92
+
93
+ def sqlmodel(self) -> SQLModel:
94
+ attrs = {name: getattr(self, name) for name in self.__sqlmodel__.__fields__}
95
+ return self.__sqlmodel__(**attrs)
96
+
97
+ def get_field_def(cls, field) -> Union[Field, Relationship]:
98
+ sql_meta = field.metadata.get("SQL", {})
99
+ has_foreign_key = bool(sql_meta.get("foreign_key", None))
100
+ has_relationship = bool(sql_meta.get("relationship", None))
101
+ has_unique_constraint = sql_meta.get("unique", False)
102
+ if has_unique_constraint:
103
+ return Field(unique=True)
104
+
105
+ if not sql_meta or not (has_foreign_key or has_relationship):
106
+ sql_default_factory = field.default_factory
107
+ if isinstance(sql_default_factory, dataclasses._MISSING_TYPE):
108
+ sql_default_factory = None
109
+ return Field(
110
+ default_factory=sql_default_factory,
111
+ # TODO: revisit the idea of using string for unknown types
112
+ sa_column=Column(
113
+ SA_TYPEMAP.get(field.type, String),
114
+ GLOBAL_ID_SEQ if global_id else cls.id_seq,
115
+ primary_key=(
116
+ field.name == "id"
117
+ or field.metadata.get("SQL", {}).get("primary_key", False)
118
+ ),
119
+ ),
120
+ )
121
+ if has_relationship:
122
+ back_populates = sql_meta.get("back_populates", None)
123
+ if back_populates is False:
124
+ return Relationship()
125
+ if not back_populates:
126
+ back_populates = inflection.underscore(cls.__name__)
127
+ if sql_meta.get("many_to_one", False):
128
+ back_populates = inflection.pluralize(back_populates)
129
+ return Relationship(back_populates=back_populates)
130
+ if has_foreign_key:
131
+ return Field(default=None, foreign_key=sql_meta["foreign_key"])
132
+ raise "Unsupported case"
133
+
134
+ def get_field_type(field, cls):
135
+ sql_meta = field.metadata.get("SQL", {})
136
+ has_foreign_key = bool(sql_meta.get("foreign_key", None))
137
+ has_relationship = bool(sql_meta.get("relationship", None))
138
+ has_many_to_one_relationship = bool(sql_meta.get("many_to_one", None))
139
+ if has_foreign_key:
140
+ # Translate ClassName to id type.
141
+ # TODO: what if the id type is different?
142
+ return Optional[int]
143
+ if has_relationship:
144
+ type_class = field.type
145
+ other_class = type_class.__args__[0]
146
+ if has_many_to_one_relationship:
147
+ try:
148
+ type_class = get_type_hints(cls)[field.name]
149
+ except NameError:
150
+ # TODO: log exception?
151
+ pass
152
+ else:
153
+ return Optional[other_class.__sqlmodel__]
154
+ return field.type
155
+
156
+ def patch_back_populates_types(field, back_populates, cls, sqlmodel_cls):
157
+ sql_meta = field.metadata.get("SQL", {})
158
+ has_relationship = bool(sql_meta.get("relationship", None))
159
+ has_many_to_one_relationship = bool(sql_meta.get("many_to_one", None))
160
+ if has_relationship:
161
+ if has_many_to_one_relationship:
162
+ type_class = field.type
163
+ try:
164
+ type_class = get_type_hints(cls)[field.name]
165
+ except NameError:
166
+ # TODO: log exception?
167
+ pass
168
+ inner = type_class.__args__[0]
169
+ if isinstance(inner, ForwardRef):
170
+ # can't patch right now. Try at a later time via back_populates
171
+ return
172
+ other_class = inner.__sqlmodel__
173
+ old = other_class.__annotations__[back_populates]
174
+ # Should be sqlalchemy.orm.base.Mapped[typing.List[ForwardRef('T')]]
175
+ # replace it with Mapped[List[sqlmodel_cls]]
176
+ origin = get_origin(old)
177
+ inner = get_args(old)
178
+ if origin == Mapped and len(inner) and get_origin(inner[0]) is list:
179
+ other_class.__annotations__[back_populates] = Mapped[
180
+ List[sqlmodel_cls]
181
+ ]
182
+ other_class.sqlmodel_rebuild()
183
+
184
+ # Replace Optional['T'] with Optional[TSQLModel]
185
+ old = field.type
186
+ origin = get_origin(old)
187
+ inner = get_args(old)
188
+ needs_rebuild = False
189
+ if origin == Union and len(inner) and inner[0] == ForwardRef(cls.__name__):
190
+ sqlmodel_cls.__annotations__[field.name] = Optional[sqlmodel_cls]
191
+ needs_rebuild = True
192
+
193
+ # Replace Optional[T] with Optional[TSQLModel] if T is a dataclass
194
+ if origin == Union and len(inner) and is_dataclass(inner[0]):
195
+ sqlmodel_cls.__annotations__[field.name] = Optional[inner[0].__sqlmodel__]
196
+ needs_rebuild = True
197
+
198
+ if needs_rebuild:
199
+ sqlmodel_cls.sqlmodel_rebuild()
200
+
201
+ def default_table_name(clsname: str) -> str:
202
+ return inflection.underscore(inflection.pluralize(clsname))
203
+
204
+ def decorator(cls):
205
+ # Check if the class is a dataclass
206
+ if not is_dataclass(cls):
207
+ raise ValueError("The class must be a dataclass")
208
+
209
+ nonlocal table_name
210
+ table_name = table_name or default_table_name(cls.__name__)
211
+
212
+ if not global_id:
213
+ cls.id_seq = Sequence(f"{table_name}_seq")
214
+
215
+ # Insert any foreign keys as necessary
216
+ for cfield in fields(cls):
217
+ sql_meta = cfield.metadata.get("SQL", {})
218
+ has_relationship = bool(sql_meta.get("relationship", None))
219
+ if has_relationship:
220
+ many_to_one = sql_meta.get("many_to_one", False)
221
+ foreign_key_name = cfield.name + "_id"
222
+ key_table_name = table_name
223
+ key_column_name = sql_meta.get("fk_name", f"{key_table_name}.id")
224
+ if many_to_one:
225
+ type_class = cfield.type
226
+ other_class = type_class.__args__[0]
227
+ if isinstance(other_class, ForwardRef):
228
+ other_class = other_class.__forward_arg__
229
+ other_class = getattr(other_class, "__name__", other_class)
230
+ key_table_name = default_table_name(other_class)
231
+ key_column_name = sql_meta.get(
232
+ "key_column_name", f"{key_table_name}.id"
233
+ )
234
+ back_populates = sql_meta.get("back_populates", None)
235
+ if back_populates is False or many_to_one:
236
+
237
+ new_field = field(
238
+ default=None,
239
+ metadata={"SQL": {"foreign_key": key_column_name}},
240
+ )
241
+ new_field._field_type = _FIELD
242
+ new_field.name = foreign_key_name
243
+ new_field.type = Optional[int]
244
+ cls.__dataclass_fields__[foreign_key_name] = new_field
245
+ setattr(cls, new_field.name, new_field.default)
246
+
247
+ # Generate the SQLModel class
248
+ sqlmodel_cls = type(
249
+ cls.__name__ + "SQLModel",
250
+ (SQLModel,),
251
+ {
252
+ # Table name is a plural and hence the 's' at the end
253
+ "__tablename__": table_name,
254
+ # Add type annotations to the generated fields
255
+ "__annotations__": {
256
+ **{field.name: get_field_type(field, cls) for field in fields(cls)},
257
+ **{
258
+ "Config": ClassVar,
259
+ },
260
+ },
261
+ # pydantic wants this
262
+ "__module__": cls.__module__,
263
+ "Config": {"exclude": {"__sqlmodel__", "sqlmodel"}},
264
+ **{field.name: get_field_def(cls, field) for field in fields(cls)},
265
+ },
266
+ # For SQLModel's SQLModelMetaClass
267
+ table=table,
268
+ )
269
+ cls.__sqlmodel__ = sqlmodel_cls
270
+ # Update type annotations in any class with a relationship with this class to point
271
+ # to the SQLModel, not the dataclass
272
+ for cfield in fields(cls):
273
+ if not cfield.name in sqlmodel_cls.__sqlmodel_relationships__:
274
+ continue
275
+ rel = sqlmodel_cls.__sqlmodel_relationships__.get(cfield.name, None)
276
+ if rel and hasattr(rel, "back_populates"):
277
+ patch_back_populates_types(
278
+ cfield, rel.back_populates, cls, sqlmodel_cls
279
+ )
280
+ cls.sqlmodel = sqlmodel
281
+ return cls
282
+
283
+ return decorator
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.1
2
2
  Name: fquery
3
- Version: 0.2
3
+ Version: 0.4
4
4
  Summary: A graph query engine
5
5
  Home-page: https://github.com/adsharma/fquery
6
6
  Classifier: Programming Language :: Python :: 3
@@ -9,6 +9,7 @@ Classifier: Operating System :: OS Independent
9
9
  Requires-Python: >=3.6
10
10
  Description-Content-Type: text/markdown
11
11
  Provides-Extra: SQL
12
+ Provides-Extra: df
12
13
  Provides-Extra: graphql
13
14
  License-File: LICENSE
14
15
 
@@ -10,6 +10,8 @@ fquery/django.py
10
10
  fquery/execute.py
11
11
  fquery/fgraphql.py
12
12
  fquery/malloy_builder.py
13
+ fquery/polars_builder.py
14
+ fquery/pydantic.py
13
15
  fquery/query.py
14
16
  fquery/resolve.py
15
17
  fquery/sql_builder.py
@@ -30,6 +32,8 @@ tests/graphql_mock_user.py
30
32
  tests/mock_user.py
31
33
  tests/test_malloy.py
32
34
  tests/test_operators.py
35
+ tests/test_polars.py
36
+ tests/test_pydantic.py
33
37
  tests/test_sql.py
34
38
  tests/test_sqlmodel.py
35
39
  tests/test_walk.py
@@ -0,0 +1,13 @@
1
+ aioitertools
2
+
3
+ [SQL]
4
+ duckdb_engine>=0.14.0
5
+ inflection>=0.5.1
6
+ pypika>=0.36.5
7
+ sqlmodel@ git+https://github.com/adsharma/sqlmodel.git@sqlmodel_rebuild
8
+
9
+ [df]
10
+ polars>=0.12.0
11
+
12
+ [graphql]
13
+ strawberry>=0.37.1
@@ -11,7 +11,7 @@ with open("README.md", "r") as fh:
11
11
 
12
12
  setuptools.setup(
13
13
  name="fquery",
14
- version="0.2",
14
+ version="0.4",
15
15
  description="A graph query engine",
16
16
  url="https://github.com/adsharma/fquery",
17
17
  long_description=long_description,
@@ -27,7 +27,13 @@ setuptools.setup(
27
27
  install_requires=["aioitertools"],
28
28
  test_requires=["sqlalchemy >= 2.0.36"],
29
29
  extras_require={
30
- "SQL": ["pypika >= 0.36.5", "sqlmodel >= 0.0.22", "duckdb_engine >= 0.14.0"],
30
+ "SQL": [
31
+ "pypika >= 0.36.5",
32
+ "sqlmodel@git+https://github.com/adsharma/sqlmodel.git@sqlmodel_rebuild",
33
+ "duckdb_engine >= 0.14.0",
34
+ "inflection >= 0.5.1",
35
+ ],
31
36
  "graphql": ["strawberry >= 0.37.1"],
37
+ "df": ["polars >= 0.12.0"],
32
38
  },
33
39
  )
@@ -2,6 +2,8 @@ import timeit
2
2
  from dataclasses import dataclass
3
3
  from datetime import datetime
4
4
 
5
+ import pydantic
6
+
5
7
  from fquery.sqlmodel import model
6
8
 
7
9
 
@@ -11,8 +13,17 @@ class User:
11
13
  id: int
12
14
  name: str
13
15
  email: str
14
- created_at: datetime = None
15
- updated_at: datetime = None
16
+ created_at: datetime | None = None
17
+ updated_at: datetime | None = None
18
+
19
+
20
+ @pydantic.dataclasses.dataclass
21
+ class UserPydantic:
22
+ id: int
23
+ name: str
24
+ email: str
25
+ created_at: datetime | None = None
26
+ updated_at: datetime | None = None
16
27
 
17
28
 
18
29
  def create_user():
@@ -23,6 +34,10 @@ def create_user_sqlmodel():
23
34
  return User(1, "John Doe", "john@example.com").sqlmodel()
24
35
 
25
36
 
37
+ def create_user_pydantic():
38
+ return UserPydantic(1, "John Doe", "john@example.com")
39
+
40
+
26
41
  # Run the benchmark
27
42
  num_iterations = 100000
28
43
  time_taken = timeit.timeit(create_user, number=num_iterations)
@@ -40,3 +55,14 @@ print(f"Creating {num_iterations} User SQL Model objects took {time_taken:.6f} s
40
55
  print(
41
56
  f"Average time per object creation: {(time_taken / num_iterations) * 1e9:.2f} nanoseconds"
42
57
  )
58
+
59
+ # Run the benchmark
60
+ num_iterations = 100000
61
+ time_taken = timeit.timeit(create_user_pydantic, number=num_iterations)
62
+
63
+ print(
64
+ f"Creating {num_iterations} User Pydantic Model objects took {time_taken:.6f} seconds"
65
+ )
66
+ print(
67
+ f"Average time per object creation: {(time_taken / num_iterations) * 1e9:.2f} nanoseconds"
68
+ )
@@ -0,0 +1,36 @@
1
+ import ast
2
+ import random
3
+ import unittest
4
+
5
+ import polars as pl
6
+ from polars.testing import assert_frame_equal
7
+
8
+ from .mock_user import UserQuery
9
+
10
+
11
+ class PolarsTests(unittest.TestCase):
12
+ def setUp(self):
13
+ random.seed(100)
14
+ self.maxDiff = None
15
+
16
+ def test_project(self):
17
+ df = (
18
+ UserQuery(range(1, 10))
19
+ .project([":id", "name", "age"])
20
+ .where(ast.Expr("user.age >= 16"))
21
+ .order_by(ast.Expr("user.age"))
22
+ .take(3)
23
+ .to_polars()
24
+ )
25
+ expected = pl.DataFrame(
26
+ {
27
+ ":id": [1, 4, 2],
28
+ "name": ["id1", "id4", "id2"],
29
+ "age": [16, 16, 17],
30
+ }
31
+ )
32
+ assert_frame_equal(expected, df)
33
+
34
+
35
+ if __name__ == "__main__":
36
+ unittest.main()
@@ -0,0 +1,34 @@
1
+ from dataclasses import is_dataclass
2
+
3
+ import pytest
4
+ from pydantic import BaseModel, ValidationError
5
+
6
+ from fquery.pydantic import pydantic
7
+
8
+
9
+ @pydantic
10
+ class User:
11
+ name: str
12
+ age: int
13
+ is_active: bool = True
14
+
15
+
16
+ def test_pydantic():
17
+ u1 = User(name="John Doe", age=42)
18
+ u2 = User(name="John Doe", age=42, is_active=False)
19
+ assert is_dataclass(u1)
20
+ assert is_dataclass(u2)
21
+
22
+ v1 = u1.validator()
23
+ v2 = u2.validator()
24
+ assert isinstance(v1, BaseModel)
25
+ assert isinstance(v2, BaseModel)
26
+
27
+ assert v1.model_dump() == u1.__dict__
28
+ assert v2.model_dump() == u2.__dict__
29
+
30
+
31
+ def test_pydantic_fail():
32
+ u1 = User(name="John Doe", age=42.3)
33
+ with pytest.raises(ValidationError):
34
+ _ = u1.validator()
@@ -1,25 +1,41 @@
1
- from dataclasses import dataclass, field
1
+ from dataclasses import field
2
2
  from datetime import datetime
3
+ from typing import List, Optional
3
4
 
4
5
  from sqlalchemy import create_engine
5
6
  from sqlalchemy.orm import sessionmaker
6
7
  from sqlmodel import SQLModel
7
8
 
8
- from fquery.sqlmodel import SQL_PK, model
9
+ from fquery.sqlmodel import (
10
+ SQL_PK,
11
+ foreign_key,
12
+ many_to_one,
13
+ one_to_many,
14
+ sqlmodel,
15
+ unique,
16
+ )
9
17
 
10
18
 
11
- @model(global_id=True)
12
- @dataclass
19
+ @sqlmodel
13
20
  class User:
14
- name: str
15
- email: str
16
21
  id: int | None = None
22
+ name: str
23
+ email: str = unique()
17
24
  created_at: datetime = None
18
25
  updated_at: datetime = None
19
26
 
27
+ friend: Optional["User"] = foreign_key("users.id")
28
+ reviews: List["Review"] = one_to_many()
29
+
20
30
 
21
- @model(global_id=True)
22
- @dataclass
31
+ @sqlmodel
32
+ class Review:
33
+ id: int | None = None
34
+ score: int
35
+ user: Optional[User] = many_to_one("users.id")
36
+
37
+
38
+ @sqlmodel
23
39
  class Relation:
24
40
  src: int | None = field(**SQL_PK)
25
41
  type: int = field(**SQL_PK)
@@ -41,9 +57,9 @@ user1 = User(
41
57
  email="jane@example.com",
42
58
  created_at=datetime.now(),
43
59
  updated_at=datetime.now(),
60
+ friend=user.id,
44
61
  )
45
62
 
46
-
47
63
  # The following is equivalent to: user.sql_model()
48
64
  # from sqlmodel import Field
49
65
  # from typing import Optional
@@ -70,7 +86,13 @@ def test_sqlmodel():
70
86
  session.add(user1.sqlmodel())
71
87
  session.commit()
72
88
 
73
- relation = Relation(user.id, 1, user1.id, datetime.now(), datetime.now())
89
+ relation = Relation(
90
+ src=user.id,
91
+ type=1,
92
+ dst=user1.id,
93
+ created_at=datetime.now(),
94
+ updated_at=datetime.now(),
95
+ )
74
96
  session.add(relation.sqlmodel())
75
97
  session.commit()
76
98
  # Read all users from the database
@@ -1,96 +0,0 @@
1
- from dataclasses import fields, is_dataclass
2
- from datetime import date, datetime, time
3
- from typing import ClassVar
4
-
5
- from sqlalchemy import (
6
- Boolean,
7
- Date,
8
- DateTime,
9
- Float,
10
- Integer,
11
- LargeBinary,
12
- Sequence,
13
- String,
14
- Time,
15
- )
16
- from sqlmodel import Column, Field, SQLModel
17
-
18
- SA_TYPEMAP = {
19
- int: Integer,
20
- int | None: Integer,
21
- float: Float,
22
- str: String,
23
- bool: Boolean,
24
- datetime: DateTime,
25
- date: Date,
26
- time: Time,
27
- bytes: LargeBinary, # or Binary for smaller data
28
- }
29
-
30
- GLOBAL_ID_SEQ = Sequence("global_id_seq") # define sequence explicitly
31
- SQL_PK = {"metadata": {"SQL": {"primary_key": True}}}
32
-
33
-
34
- def model(table_name: str = None, global_id: bool = False):
35
- """
36
- A decorator that generates a SQLModel from a dataclass.
37
-
38
- Args:
39
- table_name (str): The name of the database table. Defaults to the name of the dataclass.
40
-
41
- Returns:
42
- A decorator that generates a SQLModel from a dataclass.
43
- """
44
-
45
- def sqlmodel(self) -> SQLModel:
46
- return self.__sqlmodel__(**self.__dict__)
47
-
48
- def decorator(cls):
49
- # Check if the class is a dataclass
50
- if not is_dataclass(cls):
51
- raise ValueError("The class must be a dataclass")
52
-
53
- # Generate the SQLModel class
54
- sqlmodel_cls = type(
55
- cls.__name__ + "SQLModel",
56
- (SQLModel,),
57
- {
58
- # Table name is a plural and hence the 's' at the end
59
- "__tablename__": table_name or cls.__name__.lower() + "s",
60
- # Add type annotations to the generated fields
61
- "__annotations__": {
62
- **{field.name: field.type for field in fields(cls)},
63
- **{
64
- "Config": ClassVar,
65
- },
66
- },
67
- # pydantic wants this
68
- "__module__": cls.__module__,
69
- "Config": {"exclude": {"__sqlmodel__", "sqlmodel"}},
70
- **{
71
- field.name: Field(
72
- default_factory=getattr(cls, field.name, None),
73
- # TODO: revisit the idea of using string for unknown types
74
- sa_column=Column(
75
- SA_TYPEMAP.get(field.type, String),
76
- GLOBAL_ID_SEQ if global_id else None,
77
- primary_key=(
78
- field.name == "id"
79
- or field.metadata.get("SQL", {}).get(
80
- "primary_key", False
81
- )
82
- ),
83
- ),
84
- )
85
- for field in fields(cls)
86
- },
87
- },
88
- # For SQLModel's SQLModelMetaClass
89
- table=True,
90
- )
91
-
92
- cls.__sqlmodel__ = sqlmodel_cls
93
- cls.sqlmodel = sqlmodel
94
- return cls
95
-
96
- return decorator
@@ -1,9 +0,0 @@
1
- aioitertools
2
-
3
- [SQL]
4
- duckdb_engine>=0.14.0
5
- pypika>=0.36.5
6
- sqlmodel>=0.0.22
7
-
8
- [graphql]
9
- strawberry>=0.37.1
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes