Fast-Controller 0.4.0b0__tar.gz → 0.4.2b0__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.
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.1
2
2
  Name: Fast-Controller
3
- Version: 0.4.0b0
3
+ Version: 0.4.2b0
4
4
  Summary: The fastest way to a turn your models into a full ReST API
5
5
  Keywords: controller,base,rest,api,backend
6
6
  Author-Email: Cody M Sommer <bassmastacod@gmail.com>
@@ -14,7 +14,7 @@ from sqlalchemy.orm import sessionmaker
14
14
  from sqlmodel import SQLModel
15
15
 
16
16
  from fast_controller.resource import Resource, get_field_type
17
- from fast_controller.util import docstring_format, InvalidInput, expose_path_params, extract_values, inflect
17
+ from fast_controller.util import docstring_format, InvalidInput, expose_path_params, extract_values, inflect, to_condition_operator
18
18
 
19
19
 
20
20
  class Action(Enum):
@@ -59,9 +59,13 @@ def _register_search_endpoint(controller, router: APIRouter, resource: type[Reso
59
59
  filters: resource.get_search_schema() = Query(),
60
60
  x_page: Optional[int] = Header(default=None, gt=0),
61
61
  x_per_page: Optional[int] = Header(default=None, gt=0),
62
+ x_order: Optional[str] = Header(default=None),
63
+ x_duplicate: Optional[str] = Header(default=None), # TODO: move to filters
64
+ x_unique: Optional[str] = Header(default=None), # TODO: move to filters
62
65
  daos: DAOFactory = controller.daos) -> list[DAOModel]:
63
66
  """Searches for {resource} by criteria"""
64
- results = daos[resource].find(x_page, x_per_page, **filters.model_dump(exclude_unset=True))
67
+ filters = {col: to_condition_operator(val) for col, val in filters.model_dump(exclude_unset=True)}
68
+ results = daos[resource].find(x_page, x_per_page, x_order, x_duplicate, x_unique, **filters)
65
69
  response.headers["x-total-count"] = str(results.total)
66
70
  response.headers["x-page"] = str(results.page)
67
71
  response.headers["x-per-page"] = str(results.per_page)
@@ -274,12 +278,15 @@ class Controller:
274
278
  def dependencies_for(self, resource: type[Resource], action: Action) -> list[Depends]:
275
279
  return []
276
280
 
281
+ def get_path_for(self, resource: type[Resource]) -> str:
282
+ return self.prefix + resource.get_resource_path()
283
+
277
284
  def register_resource(self,
278
285
  resource: type[Resource],
279
286
  skip: Optional[set[Action]] = frozenset(),
280
287
  additional_endpoints: Optional[Callable] = None) -> None:
281
288
  api_router = APIRouter(
282
- prefix=self.prefix + resource.get_resource_path(),
289
+ prefix=self.get_path_for(resource),
283
290
  tags=[resource.resource_name()])
284
291
  self._register_resource_endpoints(api_router, resource, skip)
285
292
  if additional_endpoints:
@@ -2,26 +2,26 @@ from inspect import isclass
2
2
  from typing import Any
3
3
 
4
4
  from daomodel import DAOModel
5
- from pydantic import create_model
5
+ from pydantic import create_model, BaseModel
6
6
  from str_case_util import Case
7
7
 
8
8
  from fast_controller.util import inflect
9
9
 
10
10
 
11
- def either(preferred: Any, default: type[SQLModel]) -> type[SQLModel]:
11
+ def either(preferred: Any, default: type[BaseModel]) -> type[BaseModel]:
12
12
  """Returns the preferred type if present, otherwise the default type.
13
13
 
14
14
  :param preferred: The type to return if not None
15
15
  :param default: The type to return if the preferred is not a model
16
16
  :return: either the preferred type or the default type
17
17
  """
18
- return preferred if isclass(preferred) and issubclass(preferred, SQLModel) else default
18
+ return preferred if isclass(preferred) and issubclass(preferred, BaseModel) else default
19
19
 
20
20
 
21
21
  def get_field_type(field) -> type:
22
22
  """Returns the equivalent type for the given field.
23
23
 
24
- :param field: The Column of an SQLModel
24
+ :param field: The Column of a model
25
25
  :return: the Python type used to represent the DB Column value
26
26
  """
27
27
  return getattr(field.type, 'impl', field.type).python_type
@@ -29,11 +29,11 @@ def get_field_type(field) -> type:
29
29
 
30
30
  class Resource(DAOModel):
31
31
  __abstract__ = True
32
- _default_schema: type[SQLModel]
33
- _input_schema: type[SQLModel]
34
- _update_schema: type[SQLModel]
35
- _output_schema: type[SQLModel]
36
- _detailed_output_schema: type[SQLModel]
32
+ _default_schema: type[BaseModel]
33
+ _input_schema: type[BaseModel]
34
+ _update_schema: type[BaseModel]
35
+ _output_schema: type[BaseModel]
36
+ _detailed_output_schema: type[BaseModel]
37
37
 
38
38
  @classmethod
39
39
  def resource_name(cls):
@@ -60,8 +60,8 @@ class Resource(DAOModel):
60
60
  return True
61
61
 
62
62
  @classmethod
63
- def get_search_schema(cls) -> type[SQLModel]:
64
- """Returns an SQLModel representing the searchable fields"""
63
+ def get_search_schema(cls) -> type[BaseModel]:
64
+ """Returns a BaseModel representing the searchable fields"""
65
65
  def get_field_name(field) -> str:
66
66
  """Constructs the field's name, optionally prepending the table name."""
67
67
  field_name = field.name
@@ -78,53 +78,57 @@ class Resource(DAOModel):
78
78
  )
79
79
 
80
80
  @classmethod
81
- def get_pk_schema(cls) -> type[SQLModel]:
82
- """Returns an SQLModel representing the primary key fields"""
81
+ def get_pk_schema(cls) -> type[BaseModel]:
82
+ """Returns a BaseModel representing the primary key fields"""
83
83
  return create_model(
84
84
  f'{cls.doc_name()}PKSchema',
85
85
  **{field.name: (get_field_type(field), ...) for field in cls.get_pk()}
86
86
  )
87
87
 
88
88
  @classmethod
89
- def get_base(cls) -> type[SQLModel]:
89
+ def get_base(cls) -> type[BaseModel]:
90
90
  return cls
91
91
 
92
92
  @classmethod
93
- def set_default_schema(cls, schema: type[SQLModel]) -> None:
93
+ def set_default_schema(cls, schema: type[BaseModel]) -> None:
94
94
  cls._default_schema = schema
95
95
 
96
96
  @classmethod
97
- def get_default_schema(cls) -> type[SQLModel]:
97
+ def get_default_schema(cls) -> type[BaseModel]:
98
98
  return either(cls._default_schema, cls)
99
99
 
100
100
  @classmethod
101
- def set_input_schema(cls, schema: type[SQLModel]) -> None:
101
+ def set_input_schema(cls, schema: type[BaseModel]) -> None:
102
102
  cls._input_schema = schema
103
103
 
104
104
  @classmethod
105
- def get_input_schema(cls) -> type[SQLModel]:
105
+ def get_input_schema(cls) -> type[BaseModel]:
106
106
  return either(cls._input_schema, cls.get_default_schema())
107
107
 
108
108
  @classmethod
109
- def set_update_schema(cls, schema: type[SQLModel]) -> None:
109
+ def set_update_schema(cls, schema: type[BaseModel]) -> None:
110
110
  cls._update_schema = schema
111
111
 
112
112
  @classmethod
113
- def get_update_schema(cls) -> type[SQLModel]:
113
+ def get_update_schema(cls) -> type[BaseModel]:
114
114
  return either(cls._update_schema, cls.get_input_schema())
115
115
 
116
116
  @classmethod
117
- def set_output_schema(cls, schema: type[SQLModel]) -> None:
117
+ def set_output_schema(cls, schema: type[BaseModel]) -> None:
118
118
  cls._output_schema = schema
119
119
 
120
120
  @classmethod
121
- def get_output_schema(cls) -> type[SQLModel]:
121
+ def get_output_schema(cls) -> type[BaseModel]:
122
122
  return either(cls._output_schema, cls.get_default_schema())
123
123
 
124
124
  @classmethod
125
- def set_detailed_output_schema(cls, schema: type[SQLModel]) -> None:
125
+ def set_detailed_output_schema(cls, schema: type[BaseModel]) -> None:
126
126
  cls._detailed_output_schema = schema
127
127
 
128
128
  @classmethod
129
- def get_detailed_output_schema(cls) -> type[SQLModel]:
129
+ def get_detailed_output_schema(cls) -> type[BaseModel]:
130
130
  return either(cls._detailed_output_schema, cls.get_output_schema())
131
+
132
+
133
+ class Schema(BaseModel):
134
+ pass
@@ -3,6 +3,7 @@ from typing import Callable, get_type_hints, Optional
3
3
  from warnings import deprecated
4
4
 
5
5
  import inflect as _inflect
6
+ from daomodel.search_util import *
6
7
  from sqlmodel import SQLModel
7
8
 
8
9
 
@@ -79,3 +80,41 @@ def extract_values(kwargs: dict, field_names: list[str]) -> list:
79
80
  :return: List of values in the same order as field_names
80
81
  """
81
82
  return [kwargs[field] for field in field_names]
83
+
84
+
85
+ def to_condition_operator(value: str) -> ConditionOperator:
86
+ """Maps a value with a potential operator prefix (e.g. 'lt:', 'contains:') to a ConditionOperator.
87
+
88
+ :param value: The query value with optional operator prefix
89
+ :return: The ConditionOperator defined by the prefix
90
+ """
91
+ op, part = None, None
92
+ if ':' in value:
93
+ op, value = value.split(':', 1)
94
+ if '_' in op:
95
+ part, op = op.split('_', 1)
96
+ match op: # TODO: support contains, starts, and ends
97
+ case 'lt':
98
+ return LessThan(value, _part=part)
99
+ case 'le':
100
+ return LessThanEqualTo(value, _part=part)
101
+ case 'gt':
102
+ return GreaterThan(value, _part=part)
103
+ case 'ge':
104
+ return GreaterThanEqualTo(value, _part=part)
105
+ case 'between':
106
+ return Between(*value.split('|', 1), _part=part)
107
+ case 'anyof':
108
+ return AnyOf(*value.split('|'), _part=part)
109
+ case 'noneof':
110
+ return NoneOf(*value.split('|'), _part=part)
111
+ case 'is':
112
+ match value:
113
+ case 'set':
114
+ return IsSet()
115
+ case 'notset':
116
+ return NotSet()
117
+ case _:
118
+ return Equals(value, _part=part)
119
+ case _:
120
+ return Equals(value, _part=part)
@@ -41,7 +41,7 @@ classifiers = [
41
41
  "Topic :: Software Development :: Libraries",
42
42
  "Typing :: Typed",
43
43
  ]
44
- version = "0.4.0b0"
44
+ version = "0.4.2b0"
45
45
 
46
46
  [project.license]
47
47
  text = "MIT"
@@ -33,9 +33,9 @@ def test_either(preferred: Any, default: type[SQLModel], expected: type[SQLModel
33
33
 
34
34
 
35
35
  def test_get_path():
36
- class C(Resource):
36
+ class Class(Resource):
37
37
  pass
38
- assert C.get_resource_path() == "/api/c"
38
+ assert Class.get_resource_path() == "/api/classes"
39
39
 
40
40
 
41
41
  class Author(Resource, table=True):
@@ -1,8 +1,10 @@
1
1
  import inspect
2
2
  from unittest.mock import Mock
3
3
 
4
+ from daomodel.search_util import *
5
+
4
6
  from fast_controller import docstring_format
5
- from fast_controller.util import expose_path_params, extract_values
7
+ from fast_controller.util import expose_path_params, extract_values, to_condition_operator
6
8
 
7
9
 
8
10
  @docstring_format(key="value")
@@ -182,3 +184,70 @@ def test_extract_values__different_types():
182
184
  expected = ["string_value", 123, True, [1, 2, 3], None]
183
185
 
184
186
  assert extract_values(kwargs, field_names) == expected
187
+
188
+
189
+ def test_to_condition_operator__equals():
190
+ op = to_condition_operator("value")
191
+ assert isinstance(op, Equals)
192
+ assert op.values == ("value",)
193
+
194
+ op = to_condition_operator("is:value")
195
+ assert isinstance(op, Equals)
196
+ assert op.values == ("value",)
197
+
198
+
199
+ def test_to_condition_operator__lt():
200
+ op = to_condition_operator("lt:5")
201
+ assert isinstance(op, LessThan)
202
+ assert op.values == ("5",)
203
+
204
+
205
+ def test_to_condition_operator__le():
206
+ op = to_condition_operator("le:5")
207
+ assert isinstance(op, LessThanEqualTo)
208
+ assert op.values == ("5",)
209
+
210
+
211
+ def test_to_condition_operator__gt():
212
+ op = to_condition_operator("gt:5")
213
+ assert isinstance(op, GreaterThan)
214
+ assert op.values == ("5",)
215
+
216
+
217
+ def test_to_condition_operator__ge():
218
+ op = to_condition_operator("ge:5")
219
+ assert isinstance(op, GreaterThanEqualTo)
220
+ assert op.values == ("5",)
221
+
222
+
223
+ def test_to_condition_operator__between():
224
+ op = to_condition_operator("between:1|5")
225
+ assert isinstance(op, Between)
226
+ assert op.values == ("1", "5")
227
+
228
+
229
+ def test_to_condition_operator__anyof():
230
+ op = to_condition_operator("anyof:1|2|3")
231
+ assert isinstance(op, AnyOf)
232
+ assert op.values == ("1", "2", "3")
233
+
234
+
235
+ def test_to_condition_operator__noneof():
236
+ op = to_condition_operator("noneof:1|2|3")
237
+ assert isinstance(op, NoneOf)
238
+ assert op.values == ("1", "2", "3")
239
+
240
+
241
+ def test_to_condition_operator__isset():
242
+ assert isinstance(to_condition_operator("is:set"), IsSet)
243
+
244
+
245
+ def test_to_condition_operator__notset():
246
+ assert isinstance(to_condition_operator("is:notset"), NotSet)
247
+
248
+
249
+ def test_to_condition_operator__part():
250
+ op = to_condition_operator("part_is:value")
251
+ assert isinstance(op, Equals)
252
+ assert op.values == ("value",)
253
+ assert op.part == "part"