fquery 0.3__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.
- {fquery-0.3 → fquery-0.4}/PKG-INFO +2 -1
- {fquery-0.3 → fquery-0.4}/fquery/malloy_builder.py +0 -4
- fquery-0.4/fquery/polars_builder.py +53 -0
- fquery-0.4/fquery/pydantic.py +46 -0
- {fquery-0.3 → fquery-0.4}/fquery/query.py +11 -0
- {fquery-0.3 → fquery-0.4}/fquery/sqlmodel.py +3 -0
- {fquery-0.3 → fquery-0.4}/fquery.egg-info/PKG-INFO +2 -1
- {fquery-0.3 → fquery-0.4}/fquery.egg-info/SOURCES.txt +4 -0
- {fquery-0.3 → fquery-0.4}/fquery.egg-info/requires.txt +3 -0
- {fquery-0.3 → fquery-0.4}/setup.py +2 -1
- fquery-0.4/tests/test_polars.py +36 -0
- fquery-0.4/tests/test_pydantic.py +34 -0
- {fquery-0.3 → fquery-0.4}/LICENSE +0 -0
- {fquery-0.3 → fquery-0.4}/README.md +0 -0
- {fquery-0.3 → fquery-0.4}/fquery/__init__.py +0 -0
- {fquery-0.3 → fquery-0.4}/fquery/aitertools.py +0 -0
- {fquery-0.3 → fquery-0.4}/fquery/async_utils.py +0 -0
- {fquery-0.3 → fquery-0.4}/fquery/django.py +0 -0
- {fquery-0.3 → fquery-0.4}/fquery/execute.py +0 -0
- {fquery-0.3 → fquery-0.4}/fquery/fgraphql.py +0 -0
- {fquery-0.3 → fquery-0.4}/fquery/resolve.py +0 -0
- {fquery-0.3 → fquery-0.4}/fquery/sql_builder.py +0 -0
- {fquery-0.3 → fquery-0.4}/fquery/view_model.py +0 -0
- {fquery-0.3 → fquery-0.4}/fquery/visitor.py +0 -0
- {fquery-0.3 → fquery-0.4}/fquery/walk.py +0 -0
- {fquery-0.3 → fquery-0.4}/fquery.egg-info/dependency_links.txt +0 -0
- {fquery-0.3 → fquery-0.4}/fquery.egg-info/top_level.txt +0 -0
- {fquery-0.3 → fquery-0.4}/pyproject.toml +0 -0
- {fquery-0.3 → fquery-0.4}/setup.cfg +0 -0
- {fquery-0.3 → fquery-0.4}/tests/__init__.py +0 -0
- {fquery-0.3 → fquery-0.4}/tests/async_test.py +0 -0
- {fquery-0.3 → fquery-0.4}/tests/benchmark.py +0 -0
- {fquery-0.3 → fquery-0.4}/tests/django_example_model.py +0 -0
- {fquery-0.3 → fquery-0.4}/tests/graphql_mock_user.py +0 -0
- {fquery-0.3 → fquery-0.4}/tests/mock_user.py +0 -0
- {fquery-0.3 → fquery-0.4}/tests/test_malloy.py +0 -0
- {fquery-0.3 → fquery-0.4}/tests/test_operators.py +0 -0
- {fquery-0.3 → fquery-0.4}/tests/test_sql.py +0 -0
- {fquery-0.3 → fquery-0.4}/tests/test_sqlmodel.py +0 -0
- {fquery-0.3 → fquery-0.4}/tests/test_walk.py +0 -0
- {fquery-0.3 → 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.
|
|
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
|
|
|
@@ -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
|
|
|
@@ -3,6 +3,7 @@ from dataclasses import _FIELD, dataclass, field, fields, is_dataclass
|
|
|
3
3
|
from datetime import date, datetime, time
|
|
4
4
|
from typing import (
|
|
5
5
|
ClassVar,
|
|
6
|
+
Dict,
|
|
6
7
|
ForwardRef,
|
|
7
8
|
List,
|
|
8
9
|
Optional,
|
|
@@ -14,6 +15,7 @@ from typing import (
|
|
|
14
15
|
|
|
15
16
|
import inflection
|
|
16
17
|
from sqlalchemy import (
|
|
18
|
+
JSON,
|
|
17
19
|
Boolean,
|
|
18
20
|
Date,
|
|
19
21
|
DateTime,
|
|
@@ -37,6 +39,7 @@ SA_TYPEMAP = {
|
|
|
37
39
|
date: Date,
|
|
38
40
|
time: Time,
|
|
39
41
|
bytes: LargeBinary, # or Binary for smaller data
|
|
42
|
+
Dict: JSON,
|
|
40
43
|
}
|
|
41
44
|
|
|
42
45
|
GLOBAL_ID_SEQ = Sequence("global_id_seq") # define sequence explicitly
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.1
|
|
2
2
|
Name: fquery
|
|
3
|
-
Version: 0.
|
|
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
|
|
@@ -11,7 +11,7 @@ with open("README.md", "r") as fh:
|
|
|
11
11
|
|
|
12
12
|
setuptools.setup(
|
|
13
13
|
name="fquery",
|
|
14
|
-
version="0.
|
|
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,
|
|
@@ -34,5 +34,6 @@ setuptools.setup(
|
|
|
34
34
|
"inflection >= 0.5.1",
|
|
35
35
|
],
|
|
36
36
|
"graphql": ["strawberry >= 0.37.1"],
|
|
37
|
+
"df": ["polars >= 0.12.0"],
|
|
37
38
|
},
|
|
38
39
|
)
|
|
@@ -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()
|
|
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
|
|
File without changes
|
|
File without changes
|
|
File without changes
|