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.
Files changed (39) hide show
  1. fquery-0.2/PKG-INFO +61 -0
  2. {fquery-0.1 → fquery-0.2}/README.md +16 -2
  3. {fquery-0.1 → fquery-0.2}/fquery/aitertools.py +0 -2
  4. {fquery-0.1 → fquery-0.2}/fquery/async_utils.py +0 -1
  5. fquery-0.2/fquery/django.py +45 -0
  6. {fquery-0.1 → fquery-0.2}/fquery/execute.py +9 -9
  7. fquery-0.2/fquery/malloy_builder.py +86 -0
  8. {fquery-0.1 → fquery-0.2}/fquery/query.py +21 -19
  9. {fquery-0.1 → fquery-0.2}/fquery/resolve.py +1 -3
  10. {fquery-0.1 → fquery-0.2}/fquery/sql_builder.py +3 -2
  11. fquery-0.2/fquery/sqlmodel.py +96 -0
  12. {fquery-0.1 → fquery-0.2}/fquery/view_model.py +22 -1
  13. {fquery-0.1 → fquery-0.2}/fquery/visitor.py +7 -7
  14. {fquery-0.1 → fquery-0.2}/fquery/walk.py +15 -16
  15. fquery-0.2/fquery.egg-info/PKG-INFO +61 -0
  16. {fquery-0.1 → fquery-0.2}/fquery.egg-info/SOURCES.txt +7 -0
  17. {fquery-0.1 → fquery-0.2}/fquery.egg-info/requires.txt +2 -0
  18. {fquery-0.1 → fquery-0.2}/pyproject.toml +2 -0
  19. {fquery-0.1 → fquery-0.2}/setup.py +7 -2
  20. {fquery-0.1 → fquery-0.2}/tests/async_test.py +15 -1
  21. fquery-0.2/tests/benchmark.py +42 -0
  22. fquery-0.2/tests/django_example_model.py +13 -0
  23. {fquery-0.1 → fquery-0.2}/tests/graphql_mock_user.py +4 -2
  24. {fquery-0.1 → fquery-0.2}/tests/mock_user.py +8 -25
  25. fquery-0.2/tests/test_malloy.py +40 -0
  26. {fquery-0.1 → fquery-0.2}/tests/test_operators.py +1 -2
  27. {fquery-0.1 → fquery-0.2}/tests/test_sql.py +2 -1
  28. fquery-0.2/tests/test_sqlmodel.py +80 -0
  29. {fquery-0.1 → fquery-0.2}/tests/test_walk.py +2 -1
  30. fquery-0.1/PKG-INFO +0 -48
  31. fquery-0.1/fquery.egg-info/PKG-INFO +0 -48
  32. {fquery-0.1 → fquery-0.2}/LICENSE +0 -0
  33. {fquery-0.1 → fquery-0.2}/fquery/__init__.py +0 -0
  34. {fquery-0.1 → fquery-0.2}/fquery/fgraphql.py +2 -2
  35. {fquery-0.1 → fquery-0.2}/fquery.egg-info/dependency_links.txt +0 -0
  36. {fquery-0.1 → fquery-0.2}/fquery.egg-info/top_level.txt +0 -0
  37. {fquery-0.1 → fquery-0.2}/setup.cfg +0 -0
  38. {fquery-0.1 → fquery-0.2}/tests/__init__.py +0 -0
  39. {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. This transparent
9
- mapping functionality is not included in this release.
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
 
@@ -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 contextlib import contextmanager
8
7
 
9
8
 
@@ -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
- p[pkey] may be [orig, a, b, c]
166
- On exit, it changes to a coroutine returning c(b(a(orig)))."""
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
- map_func takes a list of ViewModels as the argument
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
- """ Given a recursive dict in self.iter backed by generator
185
- expressions, insert `func' right above the leaves.
184
+ """Given a recursive dict in self.iter backed by generator
185
+ expressions, insert `func' right above the leaves.
186
186
 
187
- self.parent_iter and self.parent_key are used to
188
- quickly locate the parents of leaf nodes.
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
- query_name = str(self.OP)[len("QueryableOp.") :]
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 get_return_type(func):
281
- ret = get_type_hints(func)["return"]
282
- if hasattr(ret, "_name") and ret._name == "List":
283
- ret = ret.__args__[0]
284
- if isinstance(ret, ForwardRef):
285
- return ret.__forward_arg__
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) == str:
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) == str:
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
- equality purposes and hashable (so you can create sets).
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
- 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.
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 = str(node.OP).lower().split(".")[-1]
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
- needs to visit children. Compared to visit(), this can
30
- do cleanup work that needs to be scheduled after each
31
- child."""
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 interator
10
- above, materialzie all the lazy operators, print values and print types.
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
- This is particularly useful if you want to iterate over
114
- leaves, while also expanding the tree by adding more children
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
- parent"""
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
- any lazy data structures such as iterators, generators
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
- ) # noqa - inner import as query depends on walk to walk query
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
- any lazy data structures such as iterators, generators
247
- encountered.
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
- any lazy data structures such as iterators, generators
255
- encountered.
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) == types.AsyncGeneratorType:
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) == types.AsyncGeneratorType:
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
@@ -1,7 +1,9 @@
1
1
  aioitertools
2
2
 
3
3
  [SQL]
4
+ duckdb_engine>=0.14.0
4
5
  pypika>=0.36.5
6
+ sqlmodel>=0.0.22
5
7
 
6
8
  [graphql]
7
9
  strawberry>=0.37.1
@@ -4,3 +4,5 @@ requires = [
4
4
  "wheel"
5
5
  ]
6
6
  build-backend = "setuptools.build_meta"
7
+ [tool.isort]
8
+ profile = "black"
@@ -11,8 +11,9 @@ with open("README.md", "r") as fh:
11
11
 
12
12
  setuptools.setup(
13
13
  name="fquery",
14
- version="0.1",
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
- extras_require={"SQL": ["pypika >= 0.36.5"], "graphql": ["strawberry >= 0.37.1"]},
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 = asyncio.coroutine(f)
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
+ )
@@ -0,0 +1,13 @@
1
+ from dataclasses import dataclass
2
+
3
+ from fquery.django import model
4
+ from fquery.view_model import node
5
+
6
+
7
+ @model
8
+ @dataclass
9
+ @node
10
+ class Person:
11
+ first_name: str
12
+ last_name: str
13
+ age: int
@@ -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, Optional
9
+ from typing import List
11
10
 
12
- from fquery.query import query, Query, QueryableOp
13
- from fquery.view_model import edge, node, ViewModel
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 ReviewQuery:
83
- OP = QueryableOp.LEAF
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
- @staticmethod
90
- def resolve_obj(_id: int, edge: str = "") -> Optional[ViewModel]:
91
- return MockReview.get(_id)
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()
@@ -7,9 +7,8 @@ import os
7
7
  import random
8
8
  import unittest
9
9
 
10
- from .mock_user import UserQuery
11
10
  from .async_test import async_test
12
-
11
+ from .mock_user import UserQuery
13
12
 
14
13
  TEST_DATA = os.path.join(os.path.dirname(__file__), "test_data")
15
14
 
@@ -6,9 +6,10 @@ import ast
6
6
  import random
7
7
  import unittest
8
8
 
9
- from .mock_user import UserQuery
10
9
  from pypika import Query, Tables
11
10
 
11
+ from .mock_user import UserQuery
12
+
12
13
 
13
14
  class SQLTests(unittest.TestCase):
14
15
  def setUp(self):
@@ -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