Fast-Controller 0.0.0.dev2__tar.gz → 0.1.0b0__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.0.0.dev2
3
+ Version: 0.1.0b0
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>
@@ -5,11 +5,11 @@ from daomodel import DAOModel
5
5
  from daomodel.dao import NotFound
6
6
  from daomodel.db import create_engine, DAOFactory
7
7
  from daomodel.util import names_of
8
- from fastapi import FastAPI, APIRouter, Depends, Path, Body, Query
8
+ from fastapi import FastAPI, APIRouter, Depends, Path, Body, Query, Header
9
9
  from fastapi.openapi.models import Response
10
10
 
11
11
  from fast_controller.resource import Resource
12
- from fast_controller.util import docstring_format, paginated
12
+ from fast_controller.util import docstring_format
13
13
 
14
14
 
15
15
  SessionLocal = create_engine()
@@ -42,7 +42,8 @@ class Controller:
42
42
  resource: Optional[type[Resource]] = None,
43
43
  prefix: Optional[str] = None,
44
44
  skip: Optional[set[Action]] = None) -> APIRouter:
45
- api_router = APIRouter(prefix=prefix if prefix else resource.get_path())
45
+ api_router = APIRouter(prefix=prefix if prefix else resource.get_resource_path(),
46
+ tags=[resource.doc_name()] if resource else None)
46
47
  if resource:
47
48
  cls.__register_resource_endpoints(api_router, resource, skip)
48
49
  return api_router
@@ -51,21 +52,26 @@ class Controller:
51
52
  resource: type[Resource],
52
53
  skip: Optional[set[Action]] = None,
53
54
  additional_endpoints: Optional[Callable] = None) -> None:
54
- if skip is None:
55
- skip = set()
56
- api_router = APIRouter(prefix=resource.get_path(), tags=[resource.doc_name()])
55
+ api_router = APIRouter(prefix=resource.get_resource_path(), tags=[resource.doc_name()])
57
56
  self.__register_resource_endpoints(api_router, resource, skip)
58
57
  if additional_endpoints:
59
58
  additional_endpoints(api_router)
60
59
  self.app.include_router(api_router)
61
60
 
62
61
  @classmethod
63
- def __register_resource_endpoints(cls, router: APIRouter, resource: type[Resource], skip) -> None:
62
+ def __register_resource_endpoints(cls,
63
+ router: APIRouter,
64
+ resource: type[Resource],
65
+ skip: Optional[set[Action]] = None) -> None:
66
+ if skip is None:
67
+ skip = set()
64
68
  if Action.SEARCH not in skip:
65
69
  @router.get("/", response_model=list[resource.get_output_schema()])
66
70
  @docstring_format(resource=resource.doc_name())
67
71
  def search(response: Response,
68
- filters: Annotated[paginated(resource.get_search_schema()), Query()],
72
+ filters: Annotated[resource.get_search_schema(), Query()],
73
+ x_page: Optional[int] = Header(default=None, gt=0),
74
+ x_per_page: Optional[int] = Header(default=None, gt=0),
69
75
  daos: DAOFactory = Depends(get_daos)) -> list[DAOModel]:
70
76
  """Searches for {resource} by criteria"""
71
77
  results = daos[resource].find(**filters.model_dump())
@@ -1,11 +1,18 @@
1
1
  from inspect import isclass
2
- from typing import ClassVar
2
+ from typing import Any
3
3
 
4
4
  from daomodel import DAOModel
5
5
  from sqlmodel import SQLModel
6
6
 
7
7
 
8
- def either(preferred, default):
8
+ def either(preferred: Any, default: type[SQLModel]) -> type[SQLModel]:
9
+ """
10
+ Returns the preferred type if present, otherwise the default type.
11
+
12
+ :param preferred: The type to return if not None
13
+ :param default: The type to return if the preferred is not a model
14
+ :return: either the preferred type or the default type
15
+ """
9
16
  return preferred if isclass(preferred) and issubclass(preferred, SQLModel) else default
10
17
 
11
18
 
@@ -17,11 +24,16 @@ class Resource(DAOModel):
17
24
  _update_schema: type[SQLModel]
18
25
  _output_schema: type[SQLModel]
19
26
  _detailed_output_schema: type[SQLModel]
20
- path: ClassVar[str]
21
27
 
22
28
  @classmethod
23
- def get_path(cls) -> str:
24
- return getattr(cls, "path", "/api/" + cls.normalized_name())
29
+ def get_resource_path(cls) -> str:
30
+ """
31
+ Returns the URI path to this resource as defined by the 'path' class variable.
32
+ A default value of `/api/{resource_name} is returned unless overridden.
33
+
34
+ :return: The URI path to be used for this Resource
35
+ """
36
+ return "/api/" + cls.normalized_name()
25
37
 
26
38
  @classmethod
27
39
  def validate(cls, column_name, value):
@@ -37,7 +49,7 @@ class Resource(DAOModel):
37
49
 
38
50
  @classmethod
39
51
  def get_default_schema(cls) -> type[SQLModel]:
40
- return cls._default_schema
52
+ return either(cls._default_schema, cls)
41
53
 
42
54
  @classmethod
43
55
  def set_search_schema(cls, schema: type[SQLModel]) -> None:
@@ -61,7 +73,7 @@ class Resource(DAOModel):
61
73
 
62
74
  @classmethod
63
75
  def get_update_schema(cls) -> type[SQLModel]:
64
- return either(cls._update_schema, cls.get_default_schema())
76
+ return either(cls._update_schema, cls.get_input_schema())
65
77
 
66
78
  @classmethod
67
79
  def set_output_schema(cls, schema: type[SQLModel]) -> None:
@@ -0,0 +1,30 @@
1
+ from typing import Callable, get_type_hints, Optional
2
+
3
+ from sqlmodel import SQLModel
4
+
5
+
6
+ def docstring_format(**kwargs):
7
+ """
8
+ A decorator that formats the docstring of a function with specified values.
9
+
10
+ :param kwargs: The values to inject into the docstring
11
+ """
12
+ def decorator(func: Callable):
13
+ func.__doc__ = func.__doc__.format(**kwargs)
14
+ return func
15
+ return decorator
16
+
17
+
18
+ # TODO: Determine bast way to add test coverage
19
+ def all_optional(superclass: type[SQLModel]):
20
+ """Creates a new SQLModel for the specified class but having no required fields.
21
+
22
+ :param superclass: The SQLModel of which to make all fields Optional
23
+ :return: The newly wrapped Model
24
+ """
25
+ class OptionalModel(superclass):
26
+ pass
27
+ for field, field_type in get_type_hints(OptionalModel).items():
28
+ if not isinstance(field_type, type(Optional)):
29
+ OptionalModel.__annotations__[field] = Optional[field_type]
30
+ return OptionalModel
@@ -40,7 +40,7 @@ classifiers = [
40
40
  "Topic :: Software Development :: Libraries",
41
41
  "Typing :: Typed",
42
42
  ]
43
- version = "0.0.0.dev2"
43
+ version = "0.1.0b0"
44
44
 
45
45
  [project.license]
46
46
  text = "MIT"
@@ -0,0 +1,41 @@
1
+ from typing import Any
2
+
3
+ import pytest
4
+ from daomodel import DAOModel
5
+ from sqlmodel import SQLModel
6
+
7
+ from fast_controller import Resource, Controller
8
+ from fast_controller.resource import either
9
+
10
+
11
+ class Preferred(SQLModel):
12
+ pass
13
+
14
+
15
+ class Default(SQLModel):
16
+ pass
17
+
18
+
19
+ @pytest.mark.parametrize("preferred, default, expected", [
20
+ (Preferred, Default, Preferred),
21
+ (SQLModel, Default, SQLModel),
22
+ (DAOModel, Default, DAOModel),
23
+ (Resource, Default, Resource),
24
+ (None, Default, Default),
25
+ (1, Default, Default),
26
+ ("test", Default, Default),
27
+ (Controller, Default, Default)
28
+ ])
29
+ def test_either(preferred: Any, default: type[SQLModel], expected: type[SQLModel]):
30
+ assert either(preferred, default) == expected
31
+
32
+
33
+ def test_get_path():
34
+ class C(Resource):
35
+ pass
36
+ assert C.get_resource_path() == "/api/c"
37
+
38
+
39
+ # TODO - Not convinced on this design for schema definitions
40
+ def test_get_base_and_schemas():
41
+ pass
@@ -0,0 +1,27 @@
1
+ import inspect
2
+
3
+ from fast_controller import docstring_format
4
+
5
+
6
+ @docstring_format(key="value")
7
+ def test_docstring_format():
8
+ """{key}"""
9
+ assert inspect.getdoc(test_docstring_format) == "value"
10
+
11
+
12
+ @docstring_format(key="value")
13
+ def test_docstring_format__empty():
14
+ """"""
15
+ assert inspect.getdoc(test_docstring_format__empty) == ""
16
+
17
+
18
+ @docstring_format(key="value")
19
+ def test_docstring_format__multiple_values():
20
+ """{key}1, {key}2"""
21
+ assert inspect.getdoc(test_docstring_format__multiple_values) == "value1, value2"
22
+
23
+
24
+ @docstring_format(key1="value1", key2="value2")
25
+ def test_docstring_format__multiple_keys():
26
+ """{key1}, {key2}"""
27
+ assert inspect.getdoc(test_docstring_format__multiple_keys) == "value1, value2"
@@ -1,19 +0,0 @@
1
- from typing import Callable, Optional
2
-
3
- from sqlmodel import Field, SQLModel
4
-
5
-
6
- def docstring_format(**kwargs):
7
- def decorator(func: Callable):
8
- func.__doc__ = func.__doc__.format(**kwargs)
9
- return func
10
- return decorator
11
-
12
-
13
- def paginated(superclass: type[SQLModel]):
14
- class OptionalPK(superclass, table=True): # table=True is a hack to make PK optional
15
- pass
16
- class Paginated(OptionalPK):
17
- page: Optional[int] = Field(default=None, gt=0)
18
- per_page: Optional[int] = Field(default=None, gt=0)
19
- return Paginated
File without changes
File without changes