fquery 0.1__tar.gz → 0.3__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 (40) hide show
  1. fquery-0.3/PKG-INFO +61 -0
  2. {fquery-0.1 → fquery-0.3}/README.md +16 -2
  3. {fquery-0.1 → fquery-0.3}/fquery/aitertools.py +0 -2
  4. {fquery-0.1 → fquery-0.3}/fquery/async_utils.py +0 -1
  5. fquery-0.3/fquery/django.py +45 -0
  6. {fquery-0.1 → fquery-0.3}/fquery/execute.py +9 -9
  7. fquery-0.3/fquery/malloy_builder.py +86 -0
  8. {fquery-0.1 → fquery-0.3}/fquery/query.py +21 -19
  9. {fquery-0.1 → fquery-0.3}/fquery/resolve.py +1 -3
  10. {fquery-0.1 → fquery-0.3}/fquery/sql_builder.py +3 -2
  11. fquery-0.3/fquery/sqlmodel.py +280 -0
  12. {fquery-0.1 → fquery-0.3}/fquery/view_model.py +22 -1
  13. {fquery-0.1 → fquery-0.3}/fquery/visitor.py +7 -7
  14. {fquery-0.1 → fquery-0.3}/fquery/walk.py +15 -16
  15. fquery-0.3/fquery.egg-info/PKG-INFO +61 -0
  16. {fquery-0.1 → fquery-0.3}/fquery.egg-info/SOURCES.txt +7 -0
  17. fquery-0.3/fquery.egg-info/requires.txt +10 -0
  18. {fquery-0.1 → fquery-0.3}/pyproject.toml +2 -0
  19. {fquery-0.1 → fquery-0.3}/setup.py +12 -2
  20. {fquery-0.1 → fquery-0.3}/tests/async_test.py +15 -1
  21. fquery-0.3/tests/benchmark.py +68 -0
  22. fquery-0.3/tests/django_example_model.py +13 -0
  23. {fquery-0.1 → fquery-0.3}/tests/graphql_mock_user.py +4 -2
  24. {fquery-0.1 → fquery-0.3}/tests/mock_user.py +8 -25
  25. fquery-0.3/tests/test_malloy.py +40 -0
  26. {fquery-0.1 → fquery-0.3}/tests/test_operators.py +1 -2
  27. {fquery-0.1 → fquery-0.3}/tests/test_sql.py +2 -1
  28. fquery-0.3/tests/test_sqlmodel.py +102 -0
  29. {fquery-0.1 → fquery-0.3}/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.egg-info/requires.txt +0 -7
  33. {fquery-0.1 → fquery-0.3}/LICENSE +0 -0
  34. {fquery-0.1 → fquery-0.3}/fquery/__init__.py +0 -0
  35. {fquery-0.1 → fquery-0.3}/fquery/fgraphql.py +2 -2
  36. {fquery-0.1 → fquery-0.3}/fquery.egg-info/dependency_links.txt +0 -0
  37. {fquery-0.1 → fquery-0.3}/fquery.egg-info/top_level.txt +0 -0
  38. {fquery-0.1 → fquery-0.3}/setup.cfg +0 -0
  39. {fquery-0.1 → fquery-0.3}/tests/__init__.py +0 -0
  40. {fquery-0.1 → fquery-0.3}/tests/test_walk_obj.py +0 -0
fquery-0.3/PKG-INFO ADDED
@@ -0,0 +1,61 @@
1
+ Metadata-Version: 2.1
2
+ Name: fquery
3
+ Version: 0.3
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)