fquery 0.1__tar.gz → 0.2__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.2/PKG-INFO +61 -0
- {fquery-0.1 → fquery-0.2}/README.md +16 -2
- {fquery-0.1 → fquery-0.2}/fquery/aitertools.py +0 -2
- {fquery-0.1 → fquery-0.2}/fquery/async_utils.py +0 -1
- fquery-0.2/fquery/django.py +45 -0
- {fquery-0.1 → fquery-0.2}/fquery/execute.py +9 -9
- fquery-0.2/fquery/malloy_builder.py +86 -0
- {fquery-0.1 → fquery-0.2}/fquery/query.py +21 -19
- {fquery-0.1 → fquery-0.2}/fquery/resolve.py +1 -3
- {fquery-0.1 → fquery-0.2}/fquery/sql_builder.py +3 -2
- fquery-0.2/fquery/sqlmodel.py +96 -0
- {fquery-0.1 → fquery-0.2}/fquery/view_model.py +22 -1
- {fquery-0.1 → fquery-0.2}/fquery/visitor.py +7 -7
- {fquery-0.1 → fquery-0.2}/fquery/walk.py +15 -16
- fquery-0.2/fquery.egg-info/PKG-INFO +61 -0
- {fquery-0.1 → fquery-0.2}/fquery.egg-info/SOURCES.txt +7 -0
- {fquery-0.1 → fquery-0.2}/fquery.egg-info/requires.txt +2 -0
- {fquery-0.1 → fquery-0.2}/pyproject.toml +2 -0
- {fquery-0.1 → fquery-0.2}/setup.py +7 -2
- {fquery-0.1 → fquery-0.2}/tests/async_test.py +15 -1
- fquery-0.2/tests/benchmark.py +42 -0
- fquery-0.2/tests/django_example_model.py +13 -0
- {fquery-0.1 → fquery-0.2}/tests/graphql_mock_user.py +4 -2
- {fquery-0.1 → fquery-0.2}/tests/mock_user.py +8 -25
- fquery-0.2/tests/test_malloy.py +40 -0
- {fquery-0.1 → fquery-0.2}/tests/test_operators.py +1 -2
- {fquery-0.1 → fquery-0.2}/tests/test_sql.py +2 -1
- fquery-0.2/tests/test_sqlmodel.py +80 -0
- {fquery-0.1 → fquery-0.2}/tests/test_walk.py +2 -1
- fquery-0.1/PKG-INFO +0 -48
- fquery-0.1/fquery.egg-info/PKG-INFO +0 -48
- {fquery-0.1 → fquery-0.2}/LICENSE +0 -0
- {fquery-0.1 → fquery-0.2}/fquery/__init__.py +0 -0
- {fquery-0.1 → fquery-0.2}/fquery/fgraphql.py +2 -2
- {fquery-0.1 → fquery-0.2}/fquery.egg-info/dependency_links.txt +0 -0
- {fquery-0.1 → fquery-0.2}/fquery.egg-info/top_level.txt +0 -0
- {fquery-0.1 → fquery-0.2}/setup.cfg +0 -0
- {fquery-0.1 → fquery-0.2}/tests/__init__.py +0 -0
- {fquery-0.1 → fquery-0.2}/tests/test_walk_obj.py +0 -0
fquery-0.2/PKG-INFO
ADDED
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
Metadata-Version: 2.1
|
|
2
|
+
Name: fquery
|
|
3
|
+
Version: 0.2
|
|
4
|
+
Summary: A graph query engine
|
|
5
|
+
Home-page: https://github.com/adsharma/fquery
|
|
6
|
+
Classifier: Programming Language :: Python :: 3
|
|
7
|
+
Classifier: License :: OSI Approved :: MIT License
|
|
8
|
+
Classifier: Operating System :: OS Independent
|
|
9
|
+
Requires-Python: >=3.6
|
|
10
|
+
Description-Content-Type: text/markdown
|
|
11
|
+
Provides-Extra: SQL
|
|
12
|
+
Provides-Extra: graphql
|
|
13
|
+
License-File: LICENSE
|
|
14
|
+
|
|
15
|
+
# Overview
|
|
16
|
+
|
|
17
|
+
Projects such as Django and Flask ship with what is known as ORM (object
|
|
18
|
+
relational mappers). These abstractions expose much of the underlying
|
|
19
|
+
relational behavior (both in schema and queries).This project on the
|
|
20
|
+
other hand allows a programmer to stay entirely in the object domain
|
|
21
|
+
(hiding any relational functionality contained within), while still
|
|
22
|
+
allowing transparent mapping to a relational database.
|
|
23
|
+
|
|
24
|
+
Only basic transparent mapping of fqueries to SQL is supported:
|
|
25
|
+
[Demo](https://github.com/adsharma/fquery/blob/main/tests/test_sql.py).
|
|
26
|
+
|
|
27
|
+
Only basic transparent mapping of fqueries to [malloy](https://www.malloydata.dev/) is supported:
|
|
28
|
+
[Demo](https://github.com/adsharma/fquery/blob/main/tests/test_malloy.py).
|
|
29
|
+
|
|
30
|
+
# Installation
|
|
31
|
+
|
|
32
|
+
Requires python3.x
|
|
33
|
+
|
|
34
|
+
```
|
|
35
|
+
pip3 install fquery
|
|
36
|
+
```
|
|
37
|
+
|
|
38
|
+
Running tests:
|
|
39
|
+
|
|
40
|
+
```
|
|
41
|
+
alias t=pytest-3
|
|
42
|
+
t
|
|
43
|
+
```
|
|
44
|
+
|
|
45
|
+
You can also run it via tox.
|
|
46
|
+
|
|
47
|
+
# Tutorial
|
|
48
|
+
|
|
49
|
+
[Intro](https://adsharma.github.io/fquery/): What is fquery, sample queries
|
|
50
|
+
and some information on internals.
|
|
51
|
+
|
|
52
|
+
[Blog post](https://adsharma.github.io/django-fquery/) on how to use
|
|
53
|
+
fquery with Django and get easy access to graphql functionality
|
|
54
|
+
|
|
55
|
+
|
|
56
|
+
# License
|
|
57
|
+
|
|
58
|
+
This project is made available under the Apache License, version 2.0.
|
|
59
|
+
|
|
60
|
+
See [LICENSE.txt](license.txt) for details.
|
|
61
|
+
|
|
@@ -5,8 +5,13 @@ relational mappers). These abstractions expose much of the underlying
|
|
|
5
5
|
relational behavior (both in schema and queries).This project on the
|
|
6
6
|
other hand allows a programmer to stay entirely in the object domain
|
|
7
7
|
(hiding any relational functionality contained within), while still
|
|
8
|
-
allowing transparent mapping to a relational database.
|
|
9
|
-
|
|
8
|
+
allowing transparent mapping to a relational database.
|
|
9
|
+
|
|
10
|
+
Only basic transparent mapping of fqueries to SQL is supported:
|
|
11
|
+
[Demo](https://github.com/adsharma/fquery/blob/main/tests/test_sql.py).
|
|
12
|
+
|
|
13
|
+
Only basic transparent mapping of fqueries to [malloy](https://www.malloydata.dev/) is supported:
|
|
14
|
+
[Demo](https://github.com/adsharma/fquery/blob/main/tests/test_malloy.py).
|
|
10
15
|
|
|
11
16
|
# Installation
|
|
12
17
|
|
|
@@ -25,6 +30,15 @@ t
|
|
|
25
30
|
|
|
26
31
|
You can also run it via tox.
|
|
27
32
|
|
|
33
|
+
# Tutorial
|
|
34
|
+
|
|
35
|
+
[Intro](https://adsharma.github.io/fquery/): What is fquery, sample queries
|
|
36
|
+
and some information on internals.
|
|
37
|
+
|
|
38
|
+
[Blog post](https://adsharma.github.io/django-fquery/) on how to use
|
|
39
|
+
fquery with Django and get easy access to graphql functionality
|
|
40
|
+
|
|
41
|
+
|
|
28
42
|
# License
|
|
29
43
|
|
|
30
44
|
This project is made available under the Apache License, version 2.0.
|
|
@@ -3,7 +3,6 @@
|
|
|
3
3
|
# This source code is licensed under the MIT license found in the
|
|
4
4
|
# LICENSE file in the root directory of this source tree.
|
|
5
5
|
import asyncio
|
|
6
|
-
|
|
7
6
|
from typing import (
|
|
8
7
|
Any,
|
|
9
8
|
AsyncGenerator,
|
|
@@ -17,7 +16,6 @@ from typing import (
|
|
|
17
16
|
Union,
|
|
18
17
|
)
|
|
19
18
|
|
|
20
|
-
|
|
21
19
|
T = TypeVar("T")
|
|
22
20
|
AnyIterable = Union[Iterable[T], AsyncIterable[T]]
|
|
23
21
|
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
import dataclasses
|
|
2
|
+
from datetime import date, datetime
|
|
3
|
+
from uuid import UUID
|
|
4
|
+
|
|
5
|
+
from django.db import models
|
|
6
|
+
from django.db.models.fields import (
|
|
7
|
+
BooleanField,
|
|
8
|
+
DateField,
|
|
9
|
+
DateTimeField,
|
|
10
|
+
FloatField,
|
|
11
|
+
IntegerField,
|
|
12
|
+
TextField,
|
|
13
|
+
UUIDField,
|
|
14
|
+
)
|
|
15
|
+
from django.db.models.fields.related import ForeignKey
|
|
16
|
+
|
|
17
|
+
from .view_model import get_edges, get_return_type
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
def model(cls):
|
|
21
|
+
def make_django_model(name, bases, **kwattrs):
|
|
22
|
+
return type(name, bases, dict(**kwattrs))
|
|
23
|
+
|
|
24
|
+
def map_type(dataclass_field):
|
|
25
|
+
DATACLASS_TO_DJANGO_FIELD = {
|
|
26
|
+
int: IntegerField,
|
|
27
|
+
float: FloatField,
|
|
28
|
+
bool: BooleanField,
|
|
29
|
+
# TODO: Figure out how to expose CharField
|
|
30
|
+
str: TextField,
|
|
31
|
+
datetime: DateTimeField,
|
|
32
|
+
date: DateField,
|
|
33
|
+
UUID: UUIDField,
|
|
34
|
+
}
|
|
35
|
+
return DATACLASS_TO_DJANGO_FIELD.get(dataclass_field, TextField)()
|
|
36
|
+
|
|
37
|
+
fields = dataclasses.fields(cls)
|
|
38
|
+
django_fields = {djf.name: map_type(djf.type) for djf in fields if djf.name != "id"}
|
|
39
|
+
django_fields["__module__"] = cls.__module__
|
|
40
|
+
django_foreign_key_funcs = get_edges(cls)
|
|
41
|
+
for name, f in django_foreign_key_funcs.items():
|
|
42
|
+
ret_type = get_return_type(f._old)
|
|
43
|
+
name = "_" + name # so it doesn't conflict with the async method name
|
|
44
|
+
django_fields[name] = ForeignKey(ret_type, on_delete=models.CASCADE)
|
|
45
|
+
return make_django_model(cls.__name__, (models.Model,), **django_fields)
|
|
@@ -3,15 +3,15 @@
|
|
|
3
3
|
# This source code is licensed under the MIT license found in the
|
|
4
4
|
# LICENSE file in the root directory of this source tree.
|
|
5
5
|
# Copyright (c) 2016-present, Facebook, Inc. All rights reserved.
|
|
6
|
-
import aioitertools
|
|
7
6
|
import asyncio
|
|
8
7
|
import heapq
|
|
9
8
|
import itertools
|
|
10
9
|
import operator
|
|
11
|
-
|
|
12
10
|
from functools import partial
|
|
13
11
|
from typing import Any, AsyncGenerator, Callable, Iterable, List
|
|
14
12
|
|
|
13
|
+
import aioitertools
|
|
14
|
+
|
|
15
15
|
from .aitertools import tee as aitertools_tee
|
|
16
16
|
from .resolve import VISITED_EDGES_KEY
|
|
17
17
|
from .view_model import ViewModel
|
|
@@ -162,8 +162,8 @@ class AbstractSyntaxTreeVisitor(Visitor):
|
|
|
162
162
|
|
|
163
163
|
async def finish(self):
|
|
164
164
|
"""Setup funcs in reverse order. On entry,
|
|
165
|
-
|
|
166
|
-
|
|
165
|
+
p[pkey] may be [orig, a, b, c]
|
|
166
|
+
On exit, it changes to a coroutine returning c(b(a(orig)))."""
|
|
167
167
|
if not self.parent_iter:
|
|
168
168
|
return
|
|
169
169
|
pkey = str(self.parent_key)
|
|
@@ -177,15 +177,15 @@ class AbstractSyntaxTreeVisitor(Visitor):
|
|
|
177
177
|
|
|
178
178
|
def nested(func):
|
|
179
179
|
"""Apply self.map_func to all the values in the map.
|
|
180
|
-
|
|
180
|
+
map_func takes a list of ViewModels as the argument
|
|
181
181
|
"""
|
|
182
182
|
|
|
183
183
|
async def _insert_parent_func(self, func):
|
|
184
|
-
"""
|
|
185
|
-
|
|
184
|
+
"""Given a recursive dict in self.iter backed by generator
|
|
185
|
+
expressions, insert `func' right above the leaves.
|
|
186
186
|
|
|
187
|
-
|
|
188
|
-
|
|
187
|
+
self.parent_iter and self.parent_key are used to
|
|
188
|
+
quickly locate the parents of leaf nodes.
|
|
189
189
|
"""
|
|
190
190
|
if not self.parent_iter or not self.parent_key:
|
|
191
191
|
self.iter = aioitertools.map(
|
|
@@ -0,0 +1,86 @@
|
|
|
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
|
+
import ast
|
|
6
|
+
import operator
|
|
7
|
+
|
|
8
|
+
from .visitor import Visitor
|
|
9
|
+
|
|
10
|
+
# inspired from pandas.core.computation.ops
|
|
11
|
+
_cmp_ops_syms = (">", "<", ">=", "<=", "==", "!=")
|
|
12
|
+
_cmp_ops_funcs = (
|
|
13
|
+
operator.gt,
|
|
14
|
+
operator.lt,
|
|
15
|
+
operator.ge,
|
|
16
|
+
operator.le,
|
|
17
|
+
operator.eq,
|
|
18
|
+
operator.ne,
|
|
19
|
+
)
|
|
20
|
+
_cmp_ops_dict = dict(zip(_cmp_ops_syms, _cmp_ops_funcs))
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
class MalloyBuilderVisitor(Visitor):
|
|
24
|
+
INDENT = 2
|
|
25
|
+
|
|
26
|
+
def __init__(self, id1s):
|
|
27
|
+
self.malloy = None
|
|
28
|
+
self.malloy_stack = []
|
|
29
|
+
self.visited = set()
|
|
30
|
+
|
|
31
|
+
@staticmethod
|
|
32
|
+
def table_from_query(query):
|
|
33
|
+
query_name = query.__class__.__name__.lower()
|
|
34
|
+
# UserQuery -> user
|
|
35
|
+
query_name = query_name.split("query")[0]
|
|
36
|
+
return query_name
|
|
37
|
+
|
|
38
|
+
@staticmethod
|
|
39
|
+
def _indent():
|
|
40
|
+
return " " * MalloyBuilderVisitor.INDENT
|
|
41
|
+
|
|
42
|
+
async def visit_leaf(self, query):
|
|
43
|
+
table = self.table_from_query(query)
|
|
44
|
+
qstr = f"run: duckdb.table('{table}') -> {{\n"
|
|
45
|
+
while self.malloy_stack:
|
|
46
|
+
func = self.malloy_stack.pop()
|
|
47
|
+
qstr = func(qstr)
|
|
48
|
+
|
|
49
|
+
if query in self.visited:
|
|
50
|
+
# Prevent infinite recursion
|
|
51
|
+
return
|
|
52
|
+
else:
|
|
53
|
+
self.visited.add(query)
|
|
54
|
+
for q in query.edges:
|
|
55
|
+
await self.visit(q)
|
|
56
|
+
qstr += "}"
|
|
57
|
+
self.malloy = qstr
|
|
58
|
+
|
|
59
|
+
async def visit_project(self, query):
|
|
60
|
+
proj = ", ".join([x if x != ":id" else "id" for x in query.projector])
|
|
61
|
+
self.malloy_stack.append(lambda x: x + self._indent() + f"select: {proj}\n")
|
|
62
|
+
await self.visit(query.child)
|
|
63
|
+
|
|
64
|
+
async def visit_take(self, query):
|
|
65
|
+
self.malloy_stack.append(
|
|
66
|
+
lambda x: x + self._indent() + f"limit: {query._count}\n"
|
|
67
|
+
)
|
|
68
|
+
await self.visit(query.child)
|
|
69
|
+
|
|
70
|
+
async def visit_where(self, query):
|
|
71
|
+
# TODO: more general lazy expression evaluator
|
|
72
|
+
left, op, right = query._expr.value.split()
|
|
73
|
+
right = ast.literal_eval(right)
|
|
74
|
+
table, field = left.split(".") if "." in left else (self.malloy, left)
|
|
75
|
+
self.malloy_stack.append(
|
|
76
|
+
lambda x: x + self._indent() + f"where: {table}.{field} {op} {right}\n"
|
|
77
|
+
)
|
|
78
|
+
await self.visit(query.child)
|
|
79
|
+
|
|
80
|
+
async def visit_order_by(self, query):
|
|
81
|
+
key = query._expr.value
|
|
82
|
+
table, field = key.split(".") if "." in key else (self.malloy, key)
|
|
83
|
+
self.malloy_stack.append(
|
|
84
|
+
lambda x: x + self._indent() + f"order_by: {table}.{field}\n"
|
|
85
|
+
)
|
|
86
|
+
await self.visit(query.child)
|
|
@@ -5,17 +5,17 @@
|
|
|
5
5
|
import ast
|
|
6
6
|
import itertools
|
|
7
7
|
import traceback
|
|
8
|
-
|
|
9
8
|
from enum import IntEnum
|
|
10
|
-
from typing import get_type_hints, Dict, ForwardRef, List, Optional, Tuple, Type, Union
|
|
11
9
|
from types import FunctionType
|
|
10
|
+
from typing import Dict, List, Optional, Tuple, Type, Union
|
|
12
11
|
|
|
13
12
|
from .async_utils import wait_for
|
|
14
13
|
from .execute import AbstractSyntaxTreeVisitor
|
|
14
|
+
from .malloy_builder import MalloyBuilderVisitor
|
|
15
15
|
from .sql_builder import SQLBuilderVisitor
|
|
16
|
+
from .view_model import ViewModel, get_edges, get_return_type
|
|
16
17
|
from .walk import (
|
|
17
18
|
EdgeContext,
|
|
18
|
-
ViewModel,
|
|
19
19
|
PrintASTVisitor,
|
|
20
20
|
Tree,
|
|
21
21
|
materialize_walk,
|
|
@@ -86,7 +86,8 @@ class Query:
|
|
|
86
86
|
self._to_json = False
|
|
87
87
|
|
|
88
88
|
def __str__(self) -> str:
|
|
89
|
-
|
|
89
|
+
# black and flake8 don't agree on formatting the next line
|
|
90
|
+
query_name = str(self.OP)[len("QueryableOp.") :] # noqa: E203
|
|
90
91
|
if self.OP == QueryableOp.LEAF:
|
|
91
92
|
return f"{query_name} ({self.__class__.__name__})"
|
|
92
93
|
else:
|
|
@@ -250,6 +251,11 @@ class Query:
|
|
|
250
251
|
wait_for(visitor.visit(self))
|
|
251
252
|
return visitor.sql
|
|
252
253
|
|
|
254
|
+
def to_malloy(self) -> str:
|
|
255
|
+
visitor = MalloyBuilderVisitor([])
|
|
256
|
+
wait_for(visitor.visit(self))
|
|
257
|
+
return visitor.malloy
|
|
258
|
+
|
|
253
259
|
def batch_resolve_objs(self) -> List[Dict[str, List[ViewModel]]]:
|
|
254
260
|
return [{str(None): [o for o in (self.resolve_obj(i) for i in self.ids) if o]}]
|
|
255
261
|
|
|
@@ -277,27 +283,23 @@ class Query:
|
|
|
277
283
|
|
|
278
284
|
|
|
279
285
|
def query(cls):
|
|
280
|
-
def
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
if isinstance(ret, type):
|
|
287
|
-
return ret.__name__
|
|
288
|
-
return ret
|
|
286
|
+
def constructor(self, ids=None, items=None):
|
|
287
|
+
Query.__init__(self, None, ids, items)
|
|
288
|
+
|
|
289
|
+
@staticmethod
|
|
290
|
+
def resolve_obj(_id: int, edge: str = "") -> Optional[ViewModel]:
|
|
291
|
+
return cls.TYPE.get(_id)
|
|
289
292
|
|
|
290
293
|
cls = type(cls.__name__, (Query,), dict(cls.__dict__))
|
|
291
294
|
node_cls = cls.TYPE
|
|
292
|
-
edges =
|
|
293
|
-
(name, func)
|
|
294
|
-
for name, func in node_cls.__dict__.items()
|
|
295
|
-
if hasattr(func, "_edge")
|
|
296
|
-
]
|
|
295
|
+
edges = get_edges(node_cls)
|
|
297
296
|
cls.EDGE_NAME_TO_RETURN_TYPE = {
|
|
298
|
-
name: get_return_type(func._old) for name, func in edges
|
|
297
|
+
name: get_return_type(func._old) for name, func in edges.items()
|
|
299
298
|
}
|
|
300
299
|
Query.ALL_QUERIES.append(cls)
|
|
300
|
+
cls.OP = QueryableOp.LEAF
|
|
301
|
+
cls.__init__ = constructor
|
|
302
|
+
cls.resolve_obj = resolve_obj
|
|
301
303
|
return cls
|
|
302
304
|
|
|
303
305
|
|
|
@@ -3,13 +3,11 @@
|
|
|
3
3
|
# This source code is licensed under the MIT license found in the
|
|
4
4
|
# LICENSE file in the root directory of this source tree.
|
|
5
5
|
import logging
|
|
6
|
-
|
|
6
|
+
from asyncio import iscoroutinefunction
|
|
7
7
|
from collections.abc import AsyncGenerator
|
|
8
8
|
|
|
9
|
-
from asyncio import iscoroutinefunction
|
|
10
9
|
from .async_utils import wait_for
|
|
11
10
|
|
|
12
|
-
|
|
13
11
|
VISITED_EDGES_KEY = "__visited_edges__"
|
|
14
12
|
|
|
15
13
|
logger = logging.getLogger("fquery")
|
|
@@ -6,6 +6,7 @@ import ast
|
|
|
6
6
|
import operator
|
|
7
7
|
|
|
8
8
|
from pypika import Query, Tables
|
|
9
|
+
|
|
9
10
|
from .visitor import Visitor
|
|
10
11
|
|
|
11
12
|
# inspired from pandas.core.computation.ops
|
|
@@ -62,7 +63,7 @@ class SQLBuilderVisitor(Visitor):
|
|
|
62
63
|
left, op, right = query._expr.value.split()
|
|
63
64
|
right = ast.literal_eval(right)
|
|
64
65
|
table, field = left.split(".") if "." in left else (self.sql, left)
|
|
65
|
-
if type(table)
|
|
66
|
+
if type(table) is str:
|
|
66
67
|
table = Tables(table)[0]
|
|
67
68
|
binary_op = _cmp_ops_dict[op]
|
|
68
69
|
self.sql_stack.append(
|
|
@@ -73,7 +74,7 @@ class SQLBuilderVisitor(Visitor):
|
|
|
73
74
|
async def visit_order_by(self, query):
|
|
74
75
|
key = query._expr.value
|
|
75
76
|
table, field = key.split(".") if "." in key else (self.sql, key)
|
|
76
|
-
if type(table)
|
|
77
|
+
if type(table) is str:
|
|
77
78
|
table = Tables(table)[0]
|
|
78
79
|
self.sql_stack.append(lambda x: x.orderby(table.__getattr__(field)))
|
|
79
80
|
await self.visit(query.child)
|
|
@@ -0,0 +1,96 @@
|
|
|
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
|
|
@@ -3,8 +3,10 @@
|
|
|
3
3
|
# This source code is licensed under the MIT license found in the
|
|
4
4
|
# LICENSE file in the root directory of this source tree.
|
|
5
5
|
# Copyright (c) 2016-present, Facebook, Inc. All rights reserved.
|
|
6
|
+
import inspect
|
|
6
7
|
from collections import OrderedDict
|
|
7
8
|
from dataclasses import dataclass
|
|
9
|
+
from typing import ForwardRef, get_type_hints
|
|
8
10
|
|
|
9
11
|
from .resolve import VISITED_EDGES_KEY
|
|
10
12
|
|
|
@@ -12,7 +14,7 @@ from .resolve import VISITED_EDGES_KEY
|
|
|
12
14
|
@dataclass
|
|
13
15
|
class ViewModel(OrderedDict):
|
|
14
16
|
"""Like an OrderedDict, but treats :id as special for
|
|
15
|
-
|
|
17
|
+
equality purposes and hashable (so you can create sets).
|
|
16
18
|
"""
|
|
17
19
|
|
|
18
20
|
id: int
|
|
@@ -82,3 +84,22 @@ def edge(fn):
|
|
|
82
84
|
decorated._edge = True
|
|
83
85
|
decorated._old = fn
|
|
84
86
|
return decorated
|
|
87
|
+
|
|
88
|
+
|
|
89
|
+
def get_edges(cls):
|
|
90
|
+
return {
|
|
91
|
+
name: f
|
|
92
|
+
for name, f in inspect.getmembers(cls, predicate=inspect.isfunction)
|
|
93
|
+
if hasattr(f, "_edge")
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
|
|
97
|
+
def get_return_type(func):
|
|
98
|
+
ret = get_type_hints(func)["return"]
|
|
99
|
+
if hasattr(ret, "_name") and ret._name == "List":
|
|
100
|
+
ret = ret.__args__[0]
|
|
101
|
+
if isinstance(ret, ForwardRef):
|
|
102
|
+
return ret.__forward_arg__
|
|
103
|
+
if isinstance(ret, type):
|
|
104
|
+
return ret.__name__
|
|
105
|
+
return ret
|
|
@@ -12,23 +12,23 @@ class Visitor:
|
|
|
12
12
|
async def visit(self, node):
|
|
13
13
|
"""Visit a node.
|
|
14
14
|
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
15
|
+
Input is assumed to be validated json against a schema.
|
|
16
|
+
Dispatch to a visit_foo if the first element in the json
|
|
17
|
+
is foo.
|
|
18
18
|
"""
|
|
19
19
|
if not node:
|
|
20
20
|
return
|
|
21
21
|
if isinstance(node, Callable):
|
|
22
22
|
return self.visit_callable(node)
|
|
23
|
-
name =
|
|
23
|
+
name = node.OP.name.lower()
|
|
24
24
|
meth = getattr(self, "visit_" + name, None)
|
|
25
25
|
await meth(node)
|
|
26
26
|
|
|
27
27
|
async def visit_child(self, child):
|
|
28
28
|
"""To be called whenever a node with multiple children
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
29
|
+
needs to visit children. Compared to visit(), this can
|
|
30
|
+
do cleanup work that needs to be scheduled after each
|
|
31
|
+
child."""
|
|
32
32
|
await self.visit(child)
|
|
33
33
|
await self.finish()
|
|
34
34
|
|
|
@@ -6,15 +6,14 @@
|
|
|
6
6
|
|
|
7
7
|
"""This module contains various walkers to traverse the lazy map.
|
|
8
8
|
|
|
9
|
-
Utilities to walk only the leaves, modify them, insert an
|
|
10
|
-
above,
|
|
9
|
+
Utilities to walk only the leaves, modify them, insert an iterator
|
|
10
|
+
above, materialze all the lazy operators, print values and print types.
|
|
11
11
|
"""
|
|
12
12
|
import asyncio
|
|
13
|
-
import itertools
|
|
14
13
|
import inspect
|
|
14
|
+
import itertools
|
|
15
15
|
import re
|
|
16
16
|
import types
|
|
17
|
-
|
|
18
17
|
from collections.abc import Iterable
|
|
19
18
|
from datetime import datetime
|
|
20
19
|
from inspect import isasyncgen, iscoroutine, isfunction, isgenerator
|
|
@@ -110,8 +109,8 @@ async def _walk(parent, key, d): # noqa - ignore function is too complex
|
|
|
110
109
|
def walk(d):
|
|
111
110
|
"""Walk the dictionary, yielding tuples of (root, parent, key, leaf).
|
|
112
111
|
|
|
113
|
-
|
|
114
|
-
|
|
112
|
+
This is particularly useful if you want to iterate over
|
|
113
|
+
leaves, while also expanding the tree by adding more children
|
|
115
114
|
"""
|
|
116
115
|
for parent, key, leaf in _walk({}, None, d):
|
|
117
116
|
yield (d, parent, key, leaf)
|
|
@@ -119,7 +118,7 @@ def walk(d):
|
|
|
119
118
|
|
|
120
119
|
async def leaf_it(d):
|
|
121
120
|
"""Similar to walk above, but doesn't provide reference to
|
|
122
|
-
|
|
121
|
+
parent"""
|
|
123
122
|
async for _parent, _key, leaf in _walk({}, None, d):
|
|
124
123
|
yield leaf
|
|
125
124
|
|
|
@@ -172,16 +171,16 @@ WALK_LIMIT = 5 # max number of items in dict or values in iterator to print
|
|
|
172
171
|
|
|
173
172
|
def print_walk(d, indent=0): # noqa - ignore print walk is too complex
|
|
174
173
|
"""Dumps a lazy dictionary without expanding
|
|
175
|
-
|
|
174
|
+
any lazy data structures such as iterators, generators
|
|
176
175
|
"""
|
|
177
176
|
# workaround to suppress 'print used in library context' lint
|
|
178
177
|
# instead of suppressing on every line
|
|
179
178
|
log = print
|
|
180
179
|
|
|
181
180
|
log(INDENT * indent, end="")
|
|
182
|
-
from query import (
|
|
181
|
+
from query import ( # noqa - inner import as query depends on walk to walk query
|
|
183
182
|
Query,
|
|
184
|
-
)
|
|
183
|
+
)
|
|
185
184
|
|
|
186
185
|
if isinstance(d, primitive) or d is None or isinstance(d, Query):
|
|
187
186
|
log(d)
|
|
@@ -243,16 +242,16 @@ def print_walk(d, indent=0): # noqa - ignore print walk is too complex
|
|
|
243
242
|
|
|
244
243
|
async def materialize_walk(d) -> Tree:
|
|
245
244
|
"""Returns a materialized dictionary expanding
|
|
246
|
-
|
|
247
|
-
|
|
245
|
+
any lazy data structures such as iterators, generators
|
|
246
|
+
encountered.
|
|
248
247
|
"""
|
|
249
248
|
return await _materialize_walk(d)
|
|
250
249
|
|
|
251
250
|
|
|
252
251
|
async def materialize_walk_obj(d) -> Tree:
|
|
253
252
|
"""Returns an object graph expanding
|
|
254
|
-
|
|
255
|
-
|
|
253
|
+
any lazy data structures such as iterators, generators
|
|
254
|
+
encountered.
|
|
256
255
|
"""
|
|
257
256
|
return await _materialize_walk_obj(d)
|
|
258
257
|
|
|
@@ -344,7 +343,7 @@ async def _materialize_walk_obj(d) -> Tree:
|
|
|
344
343
|
return await asyncio.gather(
|
|
345
344
|
*(val for val in (_materialize_walk_obj(v) for v in resolved) if val)
|
|
346
345
|
)
|
|
347
|
-
elif type(d)
|
|
346
|
+
elif type(d) is types.AsyncGeneratorType:
|
|
348
347
|
d_list = [i async for i in d] # TODO: Optimize
|
|
349
348
|
resolved = await resolve_parallel_iterable(d_list)
|
|
350
349
|
return await asyncio.gather(
|
|
@@ -393,7 +392,7 @@ async def _materialize_walk(d) -> Tree:
|
|
|
393
392
|
return await asyncio.gather(
|
|
394
393
|
*(val for val in (_materialize_walk(v) for v in resolved) if val)
|
|
395
394
|
)
|
|
396
|
-
elif type(d)
|
|
395
|
+
elif type(d) is types.AsyncGeneratorType:
|
|
397
396
|
d_list = [i async for i in d] # TODO: Optimize
|
|
398
397
|
resolved = await resolve_parallel_iterable(d_list)
|
|
399
398
|
return await asyncio.gather(
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
Metadata-Version: 2.1
|
|
2
|
+
Name: fquery
|
|
3
|
+
Version: 0.2
|
|
4
|
+
Summary: A graph query engine
|
|
5
|
+
Home-page: https://github.com/adsharma/fquery
|
|
6
|
+
Classifier: Programming Language :: Python :: 3
|
|
7
|
+
Classifier: License :: OSI Approved :: MIT License
|
|
8
|
+
Classifier: Operating System :: OS Independent
|
|
9
|
+
Requires-Python: >=3.6
|
|
10
|
+
Description-Content-Type: text/markdown
|
|
11
|
+
Provides-Extra: SQL
|
|
12
|
+
Provides-Extra: graphql
|
|
13
|
+
License-File: LICENSE
|
|
14
|
+
|
|
15
|
+
# Overview
|
|
16
|
+
|
|
17
|
+
Projects such as Django and Flask ship with what is known as ORM (object
|
|
18
|
+
relational mappers). These abstractions expose much of the underlying
|
|
19
|
+
relational behavior (both in schema and queries).This project on the
|
|
20
|
+
other hand allows a programmer to stay entirely in the object domain
|
|
21
|
+
(hiding any relational functionality contained within), while still
|
|
22
|
+
allowing transparent mapping to a relational database.
|
|
23
|
+
|
|
24
|
+
Only basic transparent mapping of fqueries to SQL is supported:
|
|
25
|
+
[Demo](https://github.com/adsharma/fquery/blob/main/tests/test_sql.py).
|
|
26
|
+
|
|
27
|
+
Only basic transparent mapping of fqueries to [malloy](https://www.malloydata.dev/) is supported:
|
|
28
|
+
[Demo](https://github.com/adsharma/fquery/blob/main/tests/test_malloy.py).
|
|
29
|
+
|
|
30
|
+
# Installation
|
|
31
|
+
|
|
32
|
+
Requires python3.x
|
|
33
|
+
|
|
34
|
+
```
|
|
35
|
+
pip3 install fquery
|
|
36
|
+
```
|
|
37
|
+
|
|
38
|
+
Running tests:
|
|
39
|
+
|
|
40
|
+
```
|
|
41
|
+
alias t=pytest-3
|
|
42
|
+
t
|
|
43
|
+
```
|
|
44
|
+
|
|
45
|
+
You can also run it via tox.
|
|
46
|
+
|
|
47
|
+
# Tutorial
|
|
48
|
+
|
|
49
|
+
[Intro](https://adsharma.github.io/fquery/): What is fquery, sample queries
|
|
50
|
+
and some information on internals.
|
|
51
|
+
|
|
52
|
+
[Blog post](https://adsharma.github.io/django-fquery/) on how to use
|
|
53
|
+
fquery with Django and get easy access to graphql functionality
|
|
54
|
+
|
|
55
|
+
|
|
56
|
+
# License
|
|
57
|
+
|
|
58
|
+
This project is made available under the Apache License, version 2.0.
|
|
59
|
+
|
|
60
|
+
See [LICENSE.txt](license.txt) for details.
|
|
61
|
+
|
|
@@ -6,11 +6,14 @@ setup.py
|
|
|
6
6
|
fquery/__init__.py
|
|
7
7
|
fquery/aitertools.py
|
|
8
8
|
fquery/async_utils.py
|
|
9
|
+
fquery/django.py
|
|
9
10
|
fquery/execute.py
|
|
10
11
|
fquery/fgraphql.py
|
|
12
|
+
fquery/malloy_builder.py
|
|
11
13
|
fquery/query.py
|
|
12
14
|
fquery/resolve.py
|
|
13
15
|
fquery/sql_builder.py
|
|
16
|
+
fquery/sqlmodel.py
|
|
14
17
|
fquery/view_model.py
|
|
15
18
|
fquery/visitor.py
|
|
16
19
|
fquery/walk.py
|
|
@@ -21,9 +24,13 @@ fquery.egg-info/requires.txt
|
|
|
21
24
|
fquery.egg-info/top_level.txt
|
|
22
25
|
tests/__init__.py
|
|
23
26
|
tests/async_test.py
|
|
27
|
+
tests/benchmark.py
|
|
28
|
+
tests/django_example_model.py
|
|
24
29
|
tests/graphql_mock_user.py
|
|
25
30
|
tests/mock_user.py
|
|
31
|
+
tests/test_malloy.py
|
|
26
32
|
tests/test_operators.py
|
|
27
33
|
tests/test_sql.py
|
|
34
|
+
tests/test_sqlmodel.py
|
|
28
35
|
tests/test_walk.py
|
|
29
36
|
tests/test_walk_obj.py
|
|
@@ -11,8 +11,9 @@ with open("README.md", "r") as fh:
|
|
|
11
11
|
|
|
12
12
|
setuptools.setup(
|
|
13
13
|
name="fquery",
|
|
14
|
-
version="0.
|
|
14
|
+
version="0.2",
|
|
15
15
|
description="A graph query engine",
|
|
16
|
+
url="https://github.com/adsharma/fquery",
|
|
16
17
|
long_description=long_description,
|
|
17
18
|
long_description_content_type="text/markdown",
|
|
18
19
|
packages=setuptools.find_packages(),
|
|
@@ -24,5 +25,9 @@ setuptools.setup(
|
|
|
24
25
|
python_requires=">=3.6",
|
|
25
26
|
test_suite="tests",
|
|
26
27
|
install_requires=["aioitertools"],
|
|
27
|
-
|
|
28
|
+
test_requires=["sqlalchemy >= 2.0.36"],
|
|
29
|
+
extras_require={
|
|
30
|
+
"SQL": ["pypika >= 0.36.5", "sqlmodel >= 0.0.22", "duckdb_engine >= 0.14.0"],
|
|
31
|
+
"graphql": ["strawberry >= 0.37.1"],
|
|
32
|
+
},
|
|
28
33
|
)
|
|
@@ -3,11 +3,25 @@
|
|
|
3
3
|
# This source code is licensed under the MIT license found in the
|
|
4
4
|
# LICENSE file in the root directory of this source tree.
|
|
5
5
|
import asyncio
|
|
6
|
+
import functools
|
|
7
|
+
import inspect
|
|
8
|
+
from typing import Callable
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
def coroutine(fn: Callable) -> Callable:
|
|
12
|
+
if inspect.iscoroutinefunction(fn):
|
|
13
|
+
return fn
|
|
14
|
+
|
|
15
|
+
@functools.wraps(fn)
|
|
16
|
+
async def _wrapper(*args, **kwargs):
|
|
17
|
+
return fn(*args, **kwargs)
|
|
18
|
+
|
|
19
|
+
return _wrapper
|
|
6
20
|
|
|
7
21
|
|
|
8
22
|
def async_test(f):
|
|
9
23
|
def wrapper(*args, **kwargs):
|
|
10
|
-
coro =
|
|
24
|
+
coro = coroutine(f)
|
|
11
25
|
future = coro(*args, **kwargs)
|
|
12
26
|
loop = asyncio.get_event_loop()
|
|
13
27
|
loop.run_until_complete(future)
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
import timeit
|
|
2
|
+
from dataclasses import dataclass
|
|
3
|
+
from datetime import datetime
|
|
4
|
+
|
|
5
|
+
from fquery.sqlmodel import model
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
@model()
|
|
9
|
+
@dataclass
|
|
10
|
+
class User:
|
|
11
|
+
id: int
|
|
12
|
+
name: str
|
|
13
|
+
email: str
|
|
14
|
+
created_at: datetime = None
|
|
15
|
+
updated_at: datetime = None
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
def create_user():
|
|
19
|
+
return User(1, "John Doe", "john@example.com")
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
def create_user_sqlmodel():
|
|
23
|
+
return User(1, "John Doe", "john@example.com").sqlmodel()
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
# Run the benchmark
|
|
27
|
+
num_iterations = 100000
|
|
28
|
+
time_taken = timeit.timeit(create_user, number=num_iterations)
|
|
29
|
+
|
|
30
|
+
print(f"Creating {num_iterations} User objects took {time_taken:.6f} seconds")
|
|
31
|
+
print(
|
|
32
|
+
f"Average time per object creation: {(time_taken / num_iterations) * 1e9:.2f} nanoseconds"
|
|
33
|
+
)
|
|
34
|
+
|
|
35
|
+
# Run the benchmark
|
|
36
|
+
num_iterations = 100000
|
|
37
|
+
time_taken = timeit.timeit(create_user_sqlmodel, number=num_iterations)
|
|
38
|
+
|
|
39
|
+
print(f"Creating {num_iterations} User SQL Model objects took {time_taken:.6f} seconds")
|
|
40
|
+
print(
|
|
41
|
+
f"Average time per object creation: {(time_taken / num_iterations) * 1e9:.2f} nanoseconds"
|
|
42
|
+
)
|
|
@@ -3,10 +3,12 @@
|
|
|
3
3
|
# This source code is licensed under the MIT license found in the
|
|
4
4
|
# LICENSE file in the root directory of this source tree.
|
|
5
5
|
# Copyright (c) 2016-present, Facebook, Inc. All rights reserved.
|
|
6
|
-
from fquery.fgraphql import obj, field, graphql, root
|
|
7
|
-
from .mock_user import MockUser, MockReview, UserQuery, ReviewQuery
|
|
8
6
|
from typing import List, Optional
|
|
9
7
|
|
|
8
|
+
from fquery.fgraphql import field, graphql, obj, root
|
|
9
|
+
|
|
10
|
+
from .mock_user import MockReview, MockUser, ReviewQuery, UserQuery
|
|
11
|
+
|
|
10
12
|
|
|
11
13
|
@obj
|
|
12
14
|
class GraphQLMockUser(MockUser):
|
|
@@ -5,12 +5,11 @@
|
|
|
5
5
|
from __future__ import annotations
|
|
6
6
|
|
|
7
7
|
import random
|
|
8
|
-
|
|
9
8
|
from dataclasses import dataclass
|
|
10
|
-
from typing import List
|
|
9
|
+
from typing import List
|
|
11
10
|
|
|
12
|
-
from fquery.query import query
|
|
13
|
-
from fquery.view_model import edge, node
|
|
11
|
+
from fquery.query import query
|
|
12
|
+
from fquery.view_model import edge, node
|
|
14
13
|
|
|
15
14
|
|
|
16
15
|
@dataclass
|
|
@@ -42,19 +41,6 @@ class MockUser:
|
|
|
42
41
|
return u
|
|
43
42
|
|
|
44
43
|
|
|
45
|
-
@query
|
|
46
|
-
class UserQuery:
|
|
47
|
-
OP = QueryableOp.LEAF
|
|
48
|
-
TYPE = MockUser
|
|
49
|
-
|
|
50
|
-
def __init__(self, ids=None, items=None):
|
|
51
|
-
Query.__init__(self, None, ids, items)
|
|
52
|
-
|
|
53
|
-
@staticmethod
|
|
54
|
-
def resolve_obj(_id: int, edge: str = "") -> Optional[ViewModel]:
|
|
55
|
-
return MockUser.get(_id)
|
|
56
|
-
|
|
57
|
-
|
|
58
44
|
@dataclass
|
|
59
45
|
@node
|
|
60
46
|
class MockReview:
|
|
@@ -79,13 +65,10 @@ class MockReview:
|
|
|
79
65
|
|
|
80
66
|
|
|
81
67
|
@query
|
|
82
|
-
class
|
|
83
|
-
|
|
84
|
-
TYPE = MockReview
|
|
68
|
+
class UserQuery:
|
|
69
|
+
TYPE = MockUser
|
|
85
70
|
|
|
86
|
-
def __init__(self, ids=None, items=None):
|
|
87
|
-
Query.__init__(self, None, ids, items)
|
|
88
71
|
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
72
|
+
@query
|
|
73
|
+
class ReviewQuery:
|
|
74
|
+
TYPE = MockReview
|
|
@@ -0,0 +1,40 @@
|
|
|
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
|
+
import ast
|
|
6
|
+
import random
|
|
7
|
+
import textwrap
|
|
8
|
+
import unittest
|
|
9
|
+
|
|
10
|
+
from .mock_user import UserQuery
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
class MalloyTests(unittest.TestCase):
|
|
14
|
+
def setUp(self):
|
|
15
|
+
random.seed(100)
|
|
16
|
+
self.maxDiff = None
|
|
17
|
+
|
|
18
|
+
def test_project(self):
|
|
19
|
+
malloy_q = (
|
|
20
|
+
UserQuery(range(1, 10))
|
|
21
|
+
.project([":id", "name"])
|
|
22
|
+
.where(ast.Expr("user.age >= 16"))
|
|
23
|
+
.order_by(ast.Expr("user.age"))
|
|
24
|
+
.take(3)
|
|
25
|
+
.to_malloy()
|
|
26
|
+
)
|
|
27
|
+
expected = textwrap.dedent(
|
|
28
|
+
"""\
|
|
29
|
+
run: duckdb.table('user') -> {
|
|
30
|
+
select: id, name
|
|
31
|
+
where: user.age >= 16
|
|
32
|
+
order_by: user.age
|
|
33
|
+
limit: 3
|
|
34
|
+
}"""
|
|
35
|
+
)
|
|
36
|
+
self.assertEqual(expected, malloy_q)
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
if __name__ == "__main__":
|
|
40
|
+
unittest.main()
|
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
from dataclasses import dataclass, field
|
|
2
|
+
from datetime import datetime
|
|
3
|
+
|
|
4
|
+
from sqlalchemy import create_engine
|
|
5
|
+
from sqlalchemy.orm import sessionmaker
|
|
6
|
+
from sqlmodel import SQLModel
|
|
7
|
+
|
|
8
|
+
from fquery.sqlmodel import SQL_PK, model
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
@model(global_id=True)
|
|
12
|
+
@dataclass
|
|
13
|
+
class User:
|
|
14
|
+
name: str
|
|
15
|
+
email: str
|
|
16
|
+
id: int | None = None
|
|
17
|
+
created_at: datetime = None
|
|
18
|
+
updated_at: datetime = None
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
@model(global_id=True)
|
|
22
|
+
@dataclass
|
|
23
|
+
class Relation:
|
|
24
|
+
src: int | None = field(**SQL_PK)
|
|
25
|
+
type: int = field(**SQL_PK)
|
|
26
|
+
dst: int = field(**SQL_PK)
|
|
27
|
+
created_at: datetime = None
|
|
28
|
+
updated_at: datetime = None
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
# Create a new user. This should be cheap
|
|
32
|
+
user = User(
|
|
33
|
+
name="John Doe",
|
|
34
|
+
email="john@example.com",
|
|
35
|
+
created_at=datetime.now(),
|
|
36
|
+
updated_at=datetime.now(),
|
|
37
|
+
)
|
|
38
|
+
|
|
39
|
+
user1 = User(
|
|
40
|
+
name="Jane Doe",
|
|
41
|
+
email="jane@example.com",
|
|
42
|
+
created_at=datetime.now(),
|
|
43
|
+
updated_at=datetime.now(),
|
|
44
|
+
)
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
# The following is equivalent to: user.sql_model()
|
|
48
|
+
# from sqlmodel import Field
|
|
49
|
+
# from typing import Optional
|
|
50
|
+
# class UserSQLModel(SQLModel, table=True):
|
|
51
|
+
# __tablename__ = "users"
|
|
52
|
+
#
|
|
53
|
+
# id: int = Field(primary_key=True)
|
|
54
|
+
# name: str
|
|
55
|
+
# email: str
|
|
56
|
+
# created_at: Optional[datetime] = Field(default_factory=datetime.now)
|
|
57
|
+
# updated_at: Optional[datetime] = Field(default=None)
|
|
58
|
+
|
|
59
|
+
|
|
60
|
+
def test_sqlmodel():
|
|
61
|
+
user_sql = user.sqlmodel()
|
|
62
|
+
assert user_sql.__tablename__ == "users"
|
|
63
|
+
engine = create_engine("duckdb:///:memory:", echo=True)
|
|
64
|
+
SQLModel.metadata.create_all(engine)
|
|
65
|
+
|
|
66
|
+
Session = sessionmaker(bind=engine)
|
|
67
|
+
|
|
68
|
+
with Session() as session:
|
|
69
|
+
session.add(user.sqlmodel())
|
|
70
|
+
session.add(user1.sqlmodel())
|
|
71
|
+
session.commit()
|
|
72
|
+
|
|
73
|
+
relation = Relation(user.id, 1, user1.id, datetime.now(), datetime.now())
|
|
74
|
+
session.add(relation.sqlmodel())
|
|
75
|
+
session.commit()
|
|
76
|
+
# Read all users from the database
|
|
77
|
+
users = session.query(User.__sqlmodel__).all()
|
|
78
|
+
assert len(users) == 2
|
|
79
|
+
relations = session.query(Relation.__sqlmodel__).all()
|
|
80
|
+
assert len(relations) == 1
|
|
@@ -4,9 +4,10 @@
|
|
|
4
4
|
# LICENSE file in the root directory of this source tree.
|
|
5
5
|
import unittest
|
|
6
6
|
|
|
7
|
-
from .async_test import async_test
|
|
8
7
|
from fquery.walk import _materialize_walk_sync, materialize_walk
|
|
9
8
|
|
|
9
|
+
from .async_test import async_test
|
|
10
|
+
|
|
10
11
|
|
|
11
12
|
class MaterializeWalkTests(unittest.TestCase):
|
|
12
13
|
def setUp(self):
|
fquery-0.1/PKG-INFO
DELETED
|
@@ -1,48 +0,0 @@
|
|
|
1
|
-
Metadata-Version: 2.1
|
|
2
|
-
Name: fquery
|
|
3
|
-
Version: 0.1
|
|
4
|
-
Summary: A graph query engine
|
|
5
|
-
Home-page: UNKNOWN
|
|
6
|
-
License: UNKNOWN
|
|
7
|
-
Description: # Overview
|
|
8
|
-
|
|
9
|
-
Projects such as Django and Flask ship with what is known as ORM (object
|
|
10
|
-
relational mappers). These abstractions expose much of the underlying
|
|
11
|
-
relational behavior (both in schema and queries).This project on the
|
|
12
|
-
other hand allows a programmer to stay entirely in the object domain
|
|
13
|
-
(hiding any relational functionality contained within), while still
|
|
14
|
-
allowing transparent mapping to a relational database. This transparent
|
|
15
|
-
mapping functionality is not included in this release.
|
|
16
|
-
|
|
17
|
-
# Installation
|
|
18
|
-
|
|
19
|
-
Requires python3.x
|
|
20
|
-
|
|
21
|
-
```
|
|
22
|
-
pip3 install fquery
|
|
23
|
-
```
|
|
24
|
-
|
|
25
|
-
Running tests:
|
|
26
|
-
|
|
27
|
-
```
|
|
28
|
-
alias t=pytest-3
|
|
29
|
-
t
|
|
30
|
-
```
|
|
31
|
-
|
|
32
|
-
You can also run it via tox.
|
|
33
|
-
|
|
34
|
-
# License
|
|
35
|
-
|
|
36
|
-
This project is made available under the Apache License, version 2.0.
|
|
37
|
-
|
|
38
|
-
See [LICENSE.txt](license.txt) for details.
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
Platform: UNKNOWN
|
|
42
|
-
Classifier: Programming Language :: Python :: 3
|
|
43
|
-
Classifier: License :: OSI Approved :: MIT License
|
|
44
|
-
Classifier: Operating System :: OS Independent
|
|
45
|
-
Requires-Python: >=3.6
|
|
46
|
-
Description-Content-Type: text/markdown
|
|
47
|
-
Provides-Extra: SQL
|
|
48
|
-
Provides-Extra: graphql
|
|
@@ -1,48 +0,0 @@
|
|
|
1
|
-
Metadata-Version: 2.1
|
|
2
|
-
Name: fquery
|
|
3
|
-
Version: 0.1
|
|
4
|
-
Summary: A graph query engine
|
|
5
|
-
Home-page: UNKNOWN
|
|
6
|
-
License: UNKNOWN
|
|
7
|
-
Description: # Overview
|
|
8
|
-
|
|
9
|
-
Projects such as Django and Flask ship with what is known as ORM (object
|
|
10
|
-
relational mappers). These abstractions expose much of the underlying
|
|
11
|
-
relational behavior (both in schema and queries).This project on the
|
|
12
|
-
other hand allows a programmer to stay entirely in the object domain
|
|
13
|
-
(hiding any relational functionality contained within), while still
|
|
14
|
-
allowing transparent mapping to a relational database. This transparent
|
|
15
|
-
mapping functionality is not included in this release.
|
|
16
|
-
|
|
17
|
-
# Installation
|
|
18
|
-
|
|
19
|
-
Requires python3.x
|
|
20
|
-
|
|
21
|
-
```
|
|
22
|
-
pip3 install fquery
|
|
23
|
-
```
|
|
24
|
-
|
|
25
|
-
Running tests:
|
|
26
|
-
|
|
27
|
-
```
|
|
28
|
-
alias t=pytest-3
|
|
29
|
-
t
|
|
30
|
-
```
|
|
31
|
-
|
|
32
|
-
You can also run it via tox.
|
|
33
|
-
|
|
34
|
-
# License
|
|
35
|
-
|
|
36
|
-
This project is made available under the Apache License, version 2.0.
|
|
37
|
-
|
|
38
|
-
See [LICENSE.txt](license.txt) for details.
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
Platform: UNKNOWN
|
|
42
|
-
Classifier: Programming Language :: Python :: 3
|
|
43
|
-
Classifier: License :: OSI Approved :: MIT License
|
|
44
|
-
Classifier: Operating System :: OS Independent
|
|
45
|
-
Requires-Python: >=3.6
|
|
46
|
-
Description-Content-Type: text/markdown
|
|
47
|
-
Provides-Extra: SQL
|
|
48
|
-
Provides-Extra: graphql
|
|
File without changes
|
|
File without changes
|
|
@@ -3,11 +3,11 @@
|
|
|
3
3
|
# This source code is licensed under the MIT license found in the
|
|
4
4
|
# LICENSE file in the root directory of this source tree.
|
|
5
5
|
# Copyright (c) 2016-present, Facebook, Inc. All rights reserved.
|
|
6
|
-
import strawberry
|
|
7
6
|
import sys
|
|
8
|
-
|
|
9
7
|
from enum import Enum
|
|
10
8
|
|
|
9
|
+
import strawberry
|
|
10
|
+
|
|
11
11
|
|
|
12
12
|
def graphql(cls):
|
|
13
13
|
if Enum in cls.__mro__:
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|