querymodelling 0.0.1__tar.gz → 0.0.2a0__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 (24) hide show
  1. {querymodelling-0.0.1/querymodelling.egg-info → querymodelling-0.0.2a0}/PKG-INFO +9 -1
  2. querymodelling-0.0.2a0/README.md +10 -0
  3. {querymodelling-0.0.1 → querymodelling-0.0.2a0}/pyproject.toml +1 -1
  4. {querymodelling-0.0.1 → querymodelling-0.0.2a0}/querymodelling/__version__.py +1 -1
  5. {querymodelling-0.0.1 → querymodelling-0.0.2a0}/querymodelling/base.py +28 -14
  6. {querymodelling-0.0.1 → querymodelling-0.0.2a0}/querymodelling/fields.py +4 -2
  7. {querymodelling-0.0.1 → querymodelling-0.0.2a0}/querymodelling/model.py +0 -1
  8. {querymodelling-0.0.1 → querymodelling-0.0.2a0}/querymodelling/pydantic.py +0 -3
  9. querymodelling-0.0.2a0/querymodelling/sql.py +143 -0
  10. {querymodelling-0.0.1 → querymodelling-0.0.2a0/querymodelling.egg-info}/PKG-INFO +9 -1
  11. querymodelling-0.0.1/README.md +0 -2
  12. querymodelling-0.0.1/querymodelling/sql.py +0 -111
  13. {querymodelling-0.0.1 → querymodelling-0.0.2a0}/AUTHORS.rst +0 -0
  14. {querymodelling-0.0.1 → querymodelling-0.0.2a0}/LICENSE +0 -0
  15. {querymodelling-0.0.1 → querymodelling-0.0.2a0}/querymodelling/__init__.py +0 -0
  16. {querymodelling-0.0.1 → querymodelling-0.0.2a0}/querymodelling.egg-info/SOURCES.txt +0 -0
  17. {querymodelling-0.0.1 → querymodelling-0.0.2a0}/querymodelling.egg-info/dependency_links.txt +0 -0
  18. {querymodelling-0.0.1 → querymodelling-0.0.2a0}/querymodelling.egg-info/requires.txt +0 -0
  19. {querymodelling-0.0.1 → querymodelling-0.0.2a0}/querymodelling.egg-info/top_level.txt +0 -0
  20. {querymodelling-0.0.1 → querymodelling-0.0.2a0}/setup.cfg +0 -0
  21. {querymodelling-0.0.1 → querymodelling-0.0.2a0}/setup.py +0 -0
  22. {querymodelling-0.0.1 → querymodelling-0.0.2a0}/tests/__init__.py +0 -0
  23. {querymodelling-0.0.1 → querymodelling-0.0.2a0}/tests/__main__.py +0 -0
  24. {querymodelling-0.0.1 → querymodelling-0.0.2a0}/tests/basic_test.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.2
2
2
  Name: querymodelling
3
- Version: 0.0.1
3
+ Version: 0.0.2a0
4
4
  Summary: build consistent api query models for fastapi
5
5
  Author: George Haddad
6
6
  Author-email: George Haddad <georgeh87@live.de>
@@ -215,3 +215,11 @@ Dynamic: author
215
215
 
216
216
  # querymodelling
217
217
  build consistent api query models for fastapi
218
+
219
+ ## Requirements
220
+ At least you will need to have the pydantic package installed. Of course it make sense to have some kind of service supporting query models like fast api. Currently sql models are implemented - but feel free to create new ones.
221
+
222
+ ## Installation
223
+
224
+ ## Example
225
+
@@ -0,0 +1,10 @@
1
+ # querymodelling
2
+ build consistent api query models for fastapi
3
+
4
+ ## Requirements
5
+ At least you will need to have the pydantic package installed. Of course it make sense to have some kind of service supporting query models like fast api. Currently sql models are implemented - but feel free to create new ones.
6
+
7
+ ## Installation
8
+
9
+ ## Example
10
+
@@ -11,7 +11,7 @@ testpaths = ["tests"]
11
11
 
12
12
  [project]
13
13
  name = "querymodelling"
14
- version = "0.0.1"
14
+ version = "0.0.2a"
15
15
  description = "build consistent api query models for fastapi"
16
16
  readme = "README.md"
17
17
  requires-python = ">=3.11"
@@ -1,7 +1,7 @@
1
1
  __title__ = "querymodelling"
2
2
  __description__ = ""
3
3
  __url__ = "https://querymodelling.readthedocs.io"
4
- __version__ = "0.0.1"
4
+ __version__ = "0.0.2a"
5
5
  __build__ = 0x023100
6
6
  __author__ = "George Haddad"
7
7
  __author_email__ = "georgeh87@live.de"
@@ -1,7 +1,8 @@
1
1
  from pydantic import BaseModel, create_model, Field
2
2
  from pydantic.fields import FieldInfo
3
3
  from typing import (
4
- TypeVar, Literal, Type, Callable, Dict, Tuple, Any, get_origin, Optional, Union, get_args, Generic, ParamSpecArgs, TypeVarTuple, Unpack, Self
4
+ TypeVar, Literal, Type, Callable, Dict, Tuple, Any, get_origin, Optional,
5
+ Union, get_args, Generator
5
6
  )
6
7
 
7
8
  from .model import PageQuery
@@ -21,7 +22,7 @@ def get_functions(model: BaseModel, function_type: str):
21
22
  queries.append(function(getattr(model, name)))
22
23
  return queries
23
24
 
24
- A = TypeVar("A", )
25
+ A = TypeVar("A")
25
26
 
26
27
  def optional_fields(
27
28
  base_model: BaseModel,
@@ -47,10 +48,8 @@ def optional_fields(
47
48
  return fields
48
49
 
49
50
  T = TypeVar("T", bound=BaseModel)
50
- Ts = TypeVarTuple("Ts")
51
51
  C = TypeVar("C", bound=Union[PageQuery, BaseModel])
52
52
 
53
-
54
53
  def parse_field(input_field: FieldInfo) -> FieldInfo:
55
54
  data = {
56
55
  key: getattr(input_field, key)
@@ -61,21 +60,36 @@ def parse_field(input_field: FieldInfo) -> FieldInfo:
61
60
  return Field(**data)
62
61
 
63
62
  def AutoQueryModel(
64
- base_models: Type[A] | list[Type[A]],
63
+ base_models: Type[T] | list[Type[T]],
65
64
  base_query_models: Type[C] | list[Type[C]],
66
- callback: Callable[[type, str, any, FieldInfo], FieldInfo],
65
+ callback: Callable[
66
+ [type, str, any, FieldInfo],
67
+ Generator[Tuple[Type, str, Any, FieldInfo, Any], None, None]
68
+ ],
67
69
  exclude_fields: list[str] = None,
68
70
  include_fields: list[str] = None
69
71
  ) -> Type[A]:
70
- """_summary_
71
-
72
- :param base_models: _description_
72
+ """Automatic generates a query model based on the fields of the base query
73
+ models. The newly generated model will have the same fields as the base
74
+ query models, but with the option to modify the fields using the callback
75
+ function or using the exclude- and include- fields parameters. The
76
+ callback function will be called for each field of the base query models
77
+ and should return a tuple with the field name, the field info and the
78
+ annotation of the field. The exclude- and include- fields parameters can
79
+ be used to exclude or include specific fields from the base query models.
80
+ On the other hand the newly generated model will inherit from the defined
81
+ base models.
82
+
83
+ :param base_models: The base models to inherit from.
73
84
  :type base_models: Type[A] | list[Type[A]]
74
- :param base_query_models: _description_
85
+ :param base_query_models: The base query models to generate the query
86
+ model from.
75
87
  :type base_query_models: Type[C] | list[Type[C]]
76
- :param callback: _description_
77
- :type callback: Callable[[type, str, any, FieldInfo], FieldInfo]
78
- :param exclude_fields: _description_, defaults to None
88
+ :param callback: The callback function to modify the fields of the base
89
+ query models.
90
+ :type callback: Callable[[type, str, any, FieldInfo],
91
+ Generator[Tuple[Type, str, Any, FieldInfo, Any], None, None]]
92
+ :param exclude_fields: , defaults to None
79
93
  :type exclude_fields: list[str], optional
80
94
  :param include_fields: _description_, defaults to None
81
95
  :type include_fields: list[str], optional
@@ -121,4 +135,4 @@ def AutoQueryModel(
121
135
  )
122
136
  return model
123
137
 
124
- DefaultSort = Literal["asc", "desc"]
138
+ DefaultSort = Literal["asc", "desc"]
@@ -5,18 +5,21 @@ from typing import TypeVar, ParamSpec, Callable, Concatenate
5
5
  T = TypeVar("T", bound=BaseModel)
6
6
  PK = ParamSpec("PK")
7
7
 
8
-
9
8
  def Field(BaseField: Callable[PK, T]):
10
9
  def field_wrapper(
11
10
  *args,
12
11
  query_function = None,
13
12
  sort_function = None,
14
13
  query_type: str = None,
14
+ json_schema_extra: dict = None,
15
15
  **kwargs
16
16
  ):
17
17
  field_info = BaseField(
18
18
  *args,
19
19
  query_type=query_type,
20
+ json_schema_extra=(json_schema_extra or {}) | {
21
+ "query.type": query_type
22
+ },
20
23
  **kwargs,
21
24
  )
22
25
  if query_function:
@@ -24,7 +27,6 @@ def Field(BaseField: Callable[PK, T]):
24
27
  if sort_function:
25
28
  field_info.metadata.append(("sort_function", sort_function))
26
29
  return field_info
27
-
28
30
  return field_wrapper
29
31
 
30
32
  def QueryField(BaseField: Callable[PK, T]) -> Callable[
@@ -11,4 +11,3 @@ class PageQuery:
11
11
  @property
12
12
  def offset_end(self):
13
13
  return self.offset + self.size
14
-
@@ -2,10 +2,7 @@ from pydantic import Field
2
2
 
3
3
  from .fields import QueryField as BaseQueryField
4
4
  from .fields import SortField as BaseSortField
5
- from .sql import ExtendedTextSearch as BaseExtendedSqlTextSearch
6
5
 
7
6
 
8
7
  QueryField = BaseQueryField(Field)
9
8
  SortField = BaseSortField(Field)
10
-
11
- ExtendedSqlTextSearch = BaseExtendedSqlTextSearch(Field)
@@ -0,0 +1,143 @@
1
+ from datetime import datetime
2
+ from sqlmodel import Session, select, func
3
+ from sqlalchemy.orm.attributes import InstrumentedAttribute
4
+ from typing import TypeVar, Sequence, Type, Callable, ParamSpec
5
+ from pydantic import AliasChoices
6
+
7
+ from .base import get_functions, DefaultSort
8
+ from .fields import QueryField, SortField
9
+ from .model import PageQuery
10
+
11
+
12
+ T = TypeVar("T")
13
+ PK = ParamSpec("PK")
14
+ Q = TypeVar("Q", bound=PageQuery)
15
+
16
+ def sortable_by(field):
17
+ def wrapper(order: DefaultSort):
18
+ if order == "desc":
19
+ return field.desc()
20
+ if order == "asc":
21
+ return field.asc()
22
+ return wrapper
23
+
24
+ def retrieve_entries(
25
+ query: Q,
26
+ session: Session,
27
+ t: Type[T],
28
+ t_index: InstrumentedAttribute
29
+ ) -> tuple[int, Sequence[T]]:
30
+ search_clause = get_functions(query, "query")
31
+ order_clause = get_functions(query, "sort")
32
+ total_elements = session.exec(select(func.count()).where(
33
+ *search_clause).select_from(t)).first()
34
+
35
+ sub_statement = select(t_index)
36
+ if search_clause:
37
+ sub_statement = sub_statement.where(*search_clause)
38
+ if order_clause:
39
+ sub_statement = sub_statement.order_by(*order_clause)
40
+ sub_statement = sub_statement.limit(query.size).offset(
41
+ query.page * query.size)
42
+ sub_query = sub_statement.subquery()
43
+ sub_query_id = sub_query.c.__getattr__(t_index.key)
44
+
45
+ statement = select(t).join(
46
+ sub_query, t_index == sub_query_id)
47
+
48
+ return total_elements, session.exec(statement).all()
49
+
50
+ def create_query_fields(
51
+ base_field: any,
52
+ field_name: str,
53
+ annotation: any,
54
+ operator_mapping: dict[str, Callable],
55
+ json_schema_extra: dict
56
+ ):
57
+ for operator, operator_function in operator_mapping.items():
58
+ validation_alias = None
59
+ alias = None
60
+ if operator is None:
61
+ name = field_name
62
+ else:
63
+ name = f"{field_name}_{operator}"
64
+ alias = f"{field_name}.{operator}"
65
+ validation_alias = AliasChoices(name, alias)
66
+ yield (
67
+ name,
68
+ QueryField(base_field)(
69
+ operator_function,
70
+ alias=alias,
71
+ validation_alias=validation_alias,
72
+ default=None,
73
+ json_schema_extra=json_schema_extra | {
74
+ "query.backend": "sql",
75
+ "query.operator": operator
76
+ }
77
+ ),
78
+ annotation
79
+ )
80
+
81
+ def create_callback(
82
+ base_field,
83
+ copy_field_properties: list[str] = None,
84
+ schema_extra: dict = None
85
+ ):
86
+ def auto_create_callback(
87
+ source_type: type,
88
+ field_name: str,
89
+ field,
90
+ field_info,
91
+ annotation
92
+ ):
93
+ if copy_field_properties is None and schema_extra is None:
94
+ json_schema_extra = field_info.json_schema_extra
95
+ json_schema_extra = schema_extra or {}
96
+ if copy_field_properties is not None:
97
+ for property_name in copy_field_properties:
98
+ json_schema_extra[property_name] = field_info[
99
+ property_name]
100
+
101
+ if annotation == str:
102
+ operator_mapping = {
103
+ None: lambda value: field == value,
104
+ "startswith": lambda value: field.like(f"{value}%"),
105
+ "endswith": lambda value: field.like(f"%{value}"),
106
+ "contains": lambda value: field.like(f"%{value}%")
107
+ }
108
+ yield from create_query_fields(
109
+ base_field,
110
+ field_name,
111
+ annotation,
112
+ operator_mapping,
113
+ json_schema_extra
114
+ )
115
+ elif annotation in (datetime, int):
116
+ operator_mapping = {
117
+ None: lambda value: field == value,
118
+ "from": lambda value: field >= value,
119
+ "to": lambda value: field <= value
120
+ }
121
+ yield from create_query_fields(
122
+ base_field,
123
+ field_name,
124
+ annotation,
125
+ operator_mapping,
126
+ json_schema_extra
127
+ )
128
+ sort_name = f"sort_{field_name}"
129
+ sort_name_dot = f"sort.{field_name}"
130
+ yield (
131
+ sort_name_dot,
132
+ SortField(base_field)(
133
+ sortable_by(source_type.__dict__[field_name]),
134
+ alias=sort_name_dot,
135
+ validation_alias=AliasChoices(sort_name, sort_name_dot),
136
+ default=None,
137
+ json_schema_extra=json_schema_extra | {
138
+ "query.backend": "sql"
139
+ }
140
+ ),
141
+ DefaultSort
142
+ )
143
+ return auto_create_callback
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.2
2
2
  Name: querymodelling
3
- Version: 0.0.1
3
+ Version: 0.0.2a0
4
4
  Summary: build consistent api query models for fastapi
5
5
  Author: George Haddad
6
6
  Author-email: George Haddad <georgeh87@live.de>
@@ -215,3 +215,11 @@ Dynamic: author
215
215
 
216
216
  # querymodelling
217
217
  build consistent api query models for fastapi
218
+
219
+ ## Requirements
220
+ At least you will need to have the pydantic package installed. Of course it make sense to have some kind of service supporting query models like fast api. Currently sql models are implemented - but feel free to create new ones.
221
+
222
+ ## Installation
223
+
224
+ ## Example
225
+
@@ -1,2 +0,0 @@
1
- # querymodelling
2
- build consistent api query models for fastapi
@@ -1,111 +0,0 @@
1
- from datetime import datetime
2
- from sqlmodel import Session, select, func
3
- from sqlalchemy.orm.attributes import InstrumentedAttribute
4
- from typing import TypeVar, Sequence, Type, Callable, Concatenate, ParamSpec
5
-
6
- from .base import get_functions, DefaultSort
7
- from .fields import QueryField, SortField
8
- from .model import PageQuery
9
-
10
-
11
- T = TypeVar("T")
12
- PK = ParamSpec("PK")
13
- Q = TypeVar("Q", bound=PageQuery)
14
-
15
- def sortable_by(field):
16
- def wrapper(order: DefaultSort):
17
- if order == "desc":
18
- return field.desc()
19
- return wrapper
20
-
21
- def ExtendedTextSearch(BaseField) -> Callable[
22
- Concatenate[Callable, PK], T]:
23
- def field_wrapper(field, *args, **kwargs):
24
- def query(value):
25
- if value.startswith("startswith:"):
26
- return field.like(f"{value[11:]}%")
27
- if value.startswith("endswith:"):
28
- return field.like(f"%{value[9:]}")
29
- if value.startswith("contains:"):
30
- return field.like(f"%{value[9:]}%")
31
- return field == value
32
- return QueryField(BaseField)(
33
- query,
34
- *args,
35
- pattern="^(?:(?:startswith:|endswith:|contains))?.+",
36
- **kwargs
37
- )
38
- return field_wrapper
39
-
40
- def retrieve_entries(
41
- query: Q,
42
- session: Session,
43
- t: Type[T],
44
- t_index: InstrumentedAttribute
45
- ) -> tuple[int, Sequence[T]]:
46
- search_clause = get_functions(query, "query")
47
- order_clause = get_functions(query, "sort")
48
- total_elements = session.exec(select(func.count()).where(
49
- *search_clause).select_from(t)).first()
50
-
51
- sub_statement = select(t_index)
52
- if search_clause:
53
- sub_statement = sub_statement.where(*search_clause)
54
- if order_clause:
55
- sub_statement = sub_statement.order_by(*order_clause)
56
- sub_statement = sub_statement.limit(query.size).offset(
57
- query.page * query.size)
58
- sub_query = sub_statement.subquery()
59
- sub_query_id = sub_query.c.__getattr__(t_index.key)
60
-
61
- statement = select(t).join(
62
- sub_query, t_index == sub_query_id)
63
-
64
- return total_elements, session.exec(statement).all()
65
-
66
- def create_callback(base_field):
67
- def auto_create_callback(
68
- source_type: type,
69
- field_name: str,
70
- field,
71
- field_info,
72
- annotation
73
- ):
74
- if annotation == str:
75
- yield (
76
- field_name,
77
- ExtendedTextSearch(base_field)(
78
- source_type.__dict__[field_name],
79
- default=None
80
- ),
81
- annotation
82
- )
83
- elif annotation in (datetime, int):
84
- yield (
85
- f"from_{field_name}",
86
- QueryField(base_field)(
87
- lambda value: source_type.__dict__[field_name] >= value,
88
- alias=f"{field_name}.from",
89
- default=None
90
- ),
91
- annotation
92
- )
93
- yield (
94
- f"to_{field_name}",
95
- QueryField(base_field)(
96
- lambda value: source_type.__dict__[field_name] <= value,
97
- alias=f"{field_name}.to",
98
- default=None
99
- ),
100
- annotation
101
- )
102
- yield (
103
- f"sort_{field_name}",
104
- SortField(base_field)(
105
- sortable_by(source_type.__dict__[field_name]),
106
- alias=f"sort_{field_name}",
107
- default=None
108
- ),
109
- DefaultSort
110
- )
111
- return auto_create_callback
File without changes