From a7e7a3c513a2ce285a82c728607ba6a03fb5e871 Mon Sep 17 00:00:00 2001 From: freemindcore Date: Thu, 13 Oct 2022 17:24:58 +0800 Subject: [PATCH 01/58] docs: Update README.md --- README.md | 57 +++++++++++++++++++++++++++++++---- easy/controller/base.py | 4 +-- easy/permissions/adminsite.py | 2 +- 3 files changed, 54 insertions(+), 9 deletions(-) diff --git a/README.md b/README.md index 447537f..f4affdf 100644 --- a/README.md +++ b/README.md @@ -49,9 +49,12 @@ Then add "easy" to your django INSTALLED_APPS: ] ``` - -Get your admin api up and running: +### Usage +#### Get all your Django app CRUD APIs up and running +In your Django project next to urls.py create new apis.py file: ``` +from easy.main import EasyAPI + api_admin_v1 = EasyAPI( urls_namespace="admin_api", version="v1.0.0", @@ -60,16 +63,58 @@ api_admin_v1 = EasyAPI( # Automatic Admin API generation api_admin_v1.auto_create_admin_controllers() ``` +Now go to urls.py and add the following: +``` +from django.urls import path +from .apis import apis + +urlpatterns = [ + path("admin/", admin.site.urls), + path("api_admin/v1/", apis.urls), # <---------- ! +] +``` +#### Interactive API docs +Now go to http://127.0.0.1:8000/api_admin/v1/docs + +You will see the automatic interactive API documentation (provided by Swagger UI). +![Auto generated APIs List](https://github.com/freemindcore/django-api-framework/blob/fae8209a8d08c55daf75ac3a4619fe62b8ef3af6/docs/images/admin_apis_list.png) -Please check tests/demo_app for more. +#### Adding CRUD APIs to a specific API Controller + +By inheriting CrudAPIController class, CRUD APIs will be added to your API controller. +Configuration is available via Meta class: +- `model_exclude`: fields to be excluded in Schema +- `model_fields`: fields to be included in Schema, default to `"__all__"` +- `model_join`: prefetch and retrieve all m2m fields, default to False +- `model_recursive`: recursively retrieve FK/OneToOne fields, default to False +- `sensitive_fields`: fields to be ignored + + +Example: +``` +@api_controller("even_api", permissions=[AdminSitePermission]) +class EventAPIController(CrudAPIController): + def __init__(self, service: EventService): + super().__init__(service) + + class Meta: + model = Event # django model + generate_crud = True # whether to create crud api, default to True + model_fields = ["field_1", "field_2",] # if not configured default to "__all__" + model_join = True + model_recursive = True + sensitive_fields = ["password", "sensitive_info"] + +``` +Please check tests/demo_app for more examples. ### Boilerplate Django project -A boilerplate Django project for quickly getting started: +A boilerplate Django project for quickly getting started, production ready easy-apis wiht 100% test coverage ready: https://github.com/freemindcore/django-easy-api -![Auto generated APIs List](https://github.com/freemindcore/django-api-framework/blob/fae8209a8d08c55daf75ac3a4619fe62b8ef3af6/docs/images/admin_apis_list.png) ![Auto generated APIs - Users](https://github.com/freemindcore/django-api-framework/blob/9aa26e92b6fd79f4d9db422ec450fe62d4cd97b9/docs/images/user_admin_api.png) ![Auto generated APIs - Schema](https://github.com/freemindcore/django-api-framework/blob/9aa26e92b6fd79f4d9db422ec450fe62d4cd97b9/docs/images/auto_api_demo_2.png) -_Note: this project is still in early stage, comments and advices are highly appreciated._ +### Thanks to your help +**_If you find this project useful, please give your stars to support this open-source project. :) Thank you !_** diff --git a/easy/controller/base.py b/easy/controller/base.py index a577396..d2a24b5 100644 --- a/easy/controller/base.py +++ b/easy/controller/base.py @@ -27,8 +27,8 @@ class CrudAPIController(metaclass=CrudApiMetaclass): generate_crud: whether to create crud api, default to True model_exclude: fields to be excluded in Schema, it will ignore model_fields model_fields: fields to be included in Schema, default to "__all__" - model_join: retrieve all m2m fields, default to True - model_recursive: recursively retrieve FK/OneToOne models, default to False + model_join: prefetch and retrieve all m2m fields, default to False + model_recursive: recursively retrieve FK/OneToOne fields, default to False sensitive_fields: fields to be ignored Example: diff --git a/easy/permissions/adminsite.py b/easy/permissions/adminsite.py index 07c0a86..9e9f820 100644 --- a/easy/permissions/adminsite.py +++ b/easy/permissions/adminsite.py @@ -10,7 +10,7 @@ class AdminSitePermission(IsAdminUser): """ - Allows delete only to super user, and change to only staff/super users. + Only staff users with the right permission can modify objects. """ def has_permission( From 40107b706c9488bdb1f971ec11bdeb364cf45249 Mon Sep 17 00:00:00 2001 From: freemindcore Date: Fri, 14 Oct 2022 00:43:07 +0800 Subject: [PATCH 02/58] Update README.md --- README.md | 13 ++++++------- 1 file changed, 6 insertions(+), 7 deletions(-) diff --git a/README.md b/README.md index f4affdf..b95181d 100644 --- a/README.md +++ b/README.md @@ -8,15 +8,15 @@ # Django Easy API Framework -### Easy and Fast Django REST framework based on Django-ninja-extra +### Easy and Fast Django REST framework based on Django-Ninja-Extra -- Auto CRUD Async API generation for all django models, configurable via Meta class -- Domain/Service/Controller base structure for better code organization -- Base Permission/Response/Exception classes and more to come -- Pure class based [Django-Ninja](https://github.com/vitalik/django-ninja) APIs, based on [Django-Ninja-extra](https://github.com/eadwinCode/django-ninja-extra) +- CRUD Async API Generation: Automatic and configurable, inspired by [NextJs-Crud](https://github.com/nestjsx/crud). +- Domain/Service/Controller Base Structure: for better code organization. +- Base Permission/Response/Exception Classes: and many handy features to help your API coding easier +- Pure class based [Django-Ninja](https://github.com/vitalik/django-ninja) APIs: thanks to [Django-Ninja-Extra](https://github.com/eadwinCode/django-ninja-extra) ``` -Django-Ninja features : +Django-Ninja features: Easy: Designed to be easy to use and intuitive. FAST execution: Very high performance thanks to Pydantic and async support. @@ -73,7 +73,6 @@ urlpatterns = [ path("api_admin/v1/", apis.urls), # <---------- ! ] ``` -#### Interactive API docs Now go to http://127.0.0.1:8000/api_admin/v1/docs You will see the automatic interactive API documentation (provided by Swagger UI). From c1bae4a591546654a20882dc9d9f48c8851d56fc Mon Sep 17 00:00:00 2001 From: freemindcore Date: Fri, 14 Oct 2022 00:55:07 +0800 Subject: [PATCH 03/58] refactor: BaseApiResponse to use code instead of errno --- easy/controller/meta.py | 14 +++++++------- easy/main.py | 2 +- easy/response.py | 8 ++++---- tests/test_api_base_response.py | 4 ++-- 4 files changed, 14 insertions(+), 14 deletions(-) diff --git a/easy/controller/meta.py b/easy/controller/meta.py index 1729b47..ca07080 100644 --- a/easy/controller/meta.py +++ b/easy/controller/meta.py @@ -66,11 +66,11 @@ async def get_obj(self, request: HttpRequest, id: int) -> Any: # type: ignore qs = await self.service.get_obj(id) except Exception as e: # pragma: no cover logger.error(f"Get Error - {e}", exc_info=True) - return BaseApiResponse(str(e), message="Get Failed", errno=500) + return BaseApiResponse(str(e), message="Get Failed", code=500) if qs: return qs else: - return BaseApiResponse(message="Not Found", errno=404) + return BaseApiResponse(message="Not Found", code=404) async def del_obj(self, request: HttpRequest, id: int) -> Any: # type: ignore """ @@ -78,9 +78,9 @@ async def del_obj(self, request: HttpRequest, id: int) -> Any: # type: ignore Delete a single Object """ if await self.service.del_obj(id): - return BaseApiResponse("Deleted.", errno=204) + return BaseApiResponse("Deleted.", code=204) else: - return BaseApiResponse("Not Found.", errno=404) + return BaseApiResponse("Not Found.", code=404) @paginate async def get_objs(self, request: HttpRequest, filters: str = None) -> Any: # type: ignore @@ -131,9 +131,9 @@ async def add_obj( # type: ignore """ obj_id = await self.service.add_obj(**data.dict()) if obj_id: - return BaseApiResponse({"id": obj_id}, errno=201) + return BaseApiResponse({"id": obj_id}, code=201) else: - return BaseApiResponse("Add failed", errno=204) # pragma: no cover + return BaseApiResponse("Add failed", code=204) # pragma: no cover async def patch_obj( # type: ignore self, request: HttpRequest, id: int, data: DataSchema @@ -145,7 +145,7 @@ async def patch_obj( # type: ignore if await self.service.patch_obj(id=id, payload=data.dict()): return BaseApiResponse("Updated.") else: - return BaseApiResponse("Update Failed", errno=400) + return BaseApiResponse("Update Failed", code=400) DataSchema.__name__ = ( f"{model_opts.model.__name__}__AutoSchema({str(uuid.uuid4())[:4]})" diff --git a/easy/main.py b/easy/main.py index 3f20921..9298fe0 100644 --- a/easy/main.py +++ b/easy/main.py @@ -126,7 +126,7 @@ def create_response( data = django_serializer.serialize_data(data) except Exception as e: # pragma: no cover logger.error(f"Creat Response Error - {e}", exc_info=True) - return BaseApiResponse(str(e), errno=500) + return BaseApiResponse(str(e), code=500) if self.easy_output: if temporal_response: diff --git a/easy/response.py b/easy/response.py index e141d53..e826ac4 100644 --- a/easy/response.py +++ b/easy/response.py @@ -18,18 +18,18 @@ class BaseApiResponse(JsonResponse): def __init__( self, data: Union[Dict, str] = None, - errno: int = None, + code: int = None, message: str = None, **kwargs: Any ): - if errno: + if code: message = message or UNKNOWN_ERROR_MSG else: message = SUCCESS_MESSAGE - errno = ERRNO_SUCCESS + code = ERRNO_SUCCESS _data: Union[Dict, str] = { - "code": errno, + "code": code, "message": message, "data": data if data is not None else {}, } diff --git a/tests/test_api_base_response.py b/tests/test_api_base_response.py index 3fcc633..0f494c7 100644 --- a/tests/test_api_base_response.py +++ b/tests/test_api_base_response.py @@ -32,7 +32,7 @@ def test_base_api_result_dict(): def test_base_api_result_message(): assert ( - BaseApiResponse(errno=-1, message="error test").json_data["message"] + BaseApiResponse(code=-1, message="error test").json_data["message"] == "error test" ) assert BaseApiResponse().json_data["message"] @@ -41,7 +41,7 @@ def test_base_api_result_message(): def test_base_api_edit(): orig_resp = BaseApiResponse( {"item_id": 2, "im": 14}, - errno=0, + code=0, ) with pytest.raises(KeyError): From 6c5715d22492a47b03137cb17be5341c96e3eda5 Mon Sep 17 00:00:00 2001 From: freemindcore Date: Fri, 14 Oct 2022 00:58:41 +0800 Subject: [PATCH 04/58] Update README.md --- README.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index b95181d..f18d5fd 100644 --- a/README.md +++ b/README.md @@ -66,11 +66,11 @@ api_admin_v1.auto_create_admin_controllers() Now go to urls.py and add the following: ``` from django.urls import path -from .apis import apis +from .apis import api_admin_v1 urlpatterns = [ path("admin/", admin.site.urls), - path("api_admin/v1/", apis.urls), # <---------- ! + path("api_admin/v1/", api_admin_v1.urls), # <---------- ! ] ``` Now go to http://127.0.0.1:8000/api_admin/v1/docs From 7f2eb0ddb7ef8be701d17446b9b34b8b426bf24c Mon Sep 17 00:00:00 2001 From: freemindcore Date: Fri, 14 Oct 2022 00:59:46 +0800 Subject: [PATCH 05/58] lint: minor update --- tests/easy_app/urls.py | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/tests/easy_app/urls.py b/tests/easy_app/urls.py index 6213292..d616879 100644 --- a/tests/easy_app/urls.py +++ b/tests/easy_app/urls.py @@ -1,6 +1,7 @@ from django.contrib import admin from django.urls import path -from ninja_extra import NinjaExtraAPI + +from easy import EasyAPI from .controllers import ( AutoGenCrudAPIController, @@ -8,7 +9,7 @@ PermissionAPIController, ) -api = NinjaExtraAPI() +api = EasyAPI() api.register_controllers(EasyCrudAPIController) api.register_controllers(PermissionAPIController) api.register_controllers(AutoGenCrudAPIController) From e03f86782833c1bcd951f7a851f610b791af8bbf Mon Sep 17 00:00:00 2001 From: freemindcore Date: Fri, 14 Oct 2022 01:18:12 +0800 Subject: [PATCH 06/58] =?UTF-8?q?Bump=20version:=200.1.31=20=E2=86=92=200.?= =?UTF-8?q?1.32?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .bumpversion.cfg | 2 +- easy/__init__.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/.bumpversion.cfg b/.bumpversion.cfg index 6837820..8b3ac7a 100644 --- a/.bumpversion.cfg +++ b/.bumpversion.cfg @@ -1,5 +1,5 @@ [bumpversion] -current_version = 0.1.31 +current_version = 0.1.32 commit = True tag = True parse = (?P\d+)\.(?P\d+)\.(?P\d+) diff --git a/easy/__init__.py b/easy/__init__.py index 83173ed..da633eb 100644 --- a/easy/__init__.py +++ b/easy/__init__.py @@ -1,6 +1,6 @@ """Django Easy API - Easy and Fast Django REST framework based on Django-ninja-extra""" -__version__ = "0.1.31" +__version__ = "0.1.32" from easy.main import EasyAPI From 43d0cdd061273ad162cfa6d4cec0c7e61d45bb94 Mon Sep 17 00:00:00 2001 From: freemindcore Date: Fri, 14 Oct 2022 02:40:01 +0800 Subject: [PATCH 07/58] lint: minor update --- .editorconfig | 27 +++++++++++++++++++++++++++ 1 file changed, 27 insertions(+) create mode 100644 .editorconfig diff --git a/.editorconfig b/.editorconfig new file mode 100644 index 0000000..6a9a5c4 --- /dev/null +++ b/.editorconfig @@ -0,0 +1,27 @@ +# http://editorconfig.org + +root = true + +[*] +charset = utf-8 +end_of_line = lf +insert_final_newline = true +trim_trailing_whitespace = true + +[*.{py,rst,ini}] +indent_style = space +indent_size = 4 + +[*.{html,css,scss,json,yml,xml}] +indent_style = space +indent_size = 2 + +[*.md] +trim_trailing_whitespace = false + +[Makefile] +indent_style = tab + +[nginx.conf] +indent_style = space +indent_size = 2 From 4749f13f722cf58ee30b19d5771d1753951aefed Mon Sep 17 00:00:00 2001 From: freemindcore Date: Fri, 14 Oct 2022 03:26:03 +0800 Subject: [PATCH 08/58] fix: inherited class Meta settings bug --- easy/controller/meta.py | 5 +++++ tests/easy_app/controllers.py | 14 ++++++++++++++ tests/test_async_auto_crud_apis.py | 15 +++++++++++++++ 3 files changed, 34 insertions(+) diff --git a/easy/controller/meta.py b/easy/controller/meta.py index ca07080..a5f0673 100644 --- a/easy/controller/meta.py +++ b/easy/controller/meta.py @@ -39,6 +39,11 @@ def __init__(self, service=None): # type: ignore class CrudApiMetaclass(ABCMeta): def __new__(mcs, name: str, bases: Tuple[Type[Any], ...], attrs: dict) -> Any: + # Get configs from Meta + _temp_cls: Type = super().__new__(mcs, name, (object,), attrs) + model_opts: ModelOptions = ModelOptions.get_model_options(_temp_cls) + + # Get all attrs from parents excluding private ones def is_private_attrs(attr_name: str) -> Optional[Match[str]]: return re.match(r"^__[^\d\W]\w*\Z__$", attr_name, re.UNICODE) diff --git a/tests/easy_app/controllers.py b/tests/easy_app/controllers.py index cc1a2bf..2879ead 100644 --- a/tests/easy_app/controllers.py +++ b/tests/easy_app/controllers.py @@ -198,3 +198,17 @@ def __init__(self, service: EventService): class Meta: model = Event generate_crud = False + + +@api_controller("unittest", permissions=[AdminSitePermission]) +class NoCrudInheritedAPIController(AdminSitePermissionAPIController): + def __init__(self, service: EventService): + super().__init__(service) + self.service = service + + class Meta: + model = Event + generate_crud = False + model_exclude = [ + "start_date", + ] diff --git a/tests/test_async_auto_crud_apis.py b/tests/test_async_auto_crud_apis.py index d401165..0bbb3c8 100644 --- a/tests/test_async_auto_crud_apis.py +++ b/tests/test_async_auto_crud_apis.py @@ -12,6 +12,7 @@ EventSchema, InheritedRecursiveAPIController, NoCrudAPIController, + NoCrudInheritedAPIController, RecursiveAPIController, ) from .easy_app.models import Category, Client, Event, Type @@ -39,6 +40,20 @@ async def test_crud_generate_or_not(self, transactional_db, easy_api_client): f"/{event.id}", ) + client = easy_api_client(NoCrudInheritedAPIController, is_superuser=True) + + object_data = dummy_data.copy() + object_data.update(title=f"{object_data['title']}_get") + + event = await sync_to_async(Event.objects.create)(**object_data) + + response = await client.get( + f"/{event.id}", + ) + assert response.status_code == 200 + with pytest.raises(Exception): + print(response.json()["data"]["start_date"]) + async def test_crud_default_get_all(self, transactional_db, easy_api_client): client = easy_api_client(AutoGenCrudAPIController) From bcb739a25e4bc7aa219a0fad4f9579d8039b5b27 Mon Sep 17 00:00:00 2001 From: freemindcore Date: Fri, 14 Oct 2022 03:31:26 +0800 Subject: [PATCH 09/58] fix: inherited class Meta settings bug --- easy/controller/meta.py | 4 ---- 1 file changed, 4 deletions(-) diff --git a/easy/controller/meta.py b/easy/controller/meta.py index a5f0673..1d5155f 100644 --- a/easy/controller/meta.py +++ b/easy/controller/meta.py @@ -57,10 +57,6 @@ def is_private_attrs(attr_name: str) -> Optional[Match[str]]: base_cls_attrs: dict = {} base_cls_attrs.update(parent_attrs) - # Get configs from Meta - _temp_cls: Type = super().__new__(mcs, name, (object,), base_cls_attrs) - model_opts: ModelOptions = ModelOptions.get_model_options(_temp_cls) - # Define Controller APIs for auto generation async def get_obj(self, request: HttpRequest, id: int) -> Any: # type: ignore """ From 05142c8e53c0477610a4276ae06280c917bac384 Mon Sep 17 00:00:00 2001 From: freemindcore Date: Fri, 14 Oct 2022 11:41:14 +0800 Subject: [PATCH 10/58] Update README.md --- README.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index f18d5fd..e042afd 100644 --- a/README.md +++ b/README.md @@ -91,7 +91,7 @@ Configuration is available via Meta class: Example: ``` -@api_controller("even_api", permissions=[AdminSitePermission]) +@api_controller("event_api", permissions=[AdminSitePermission]) class EventAPIController(CrudAPIController): def __init__(self, service: EventService): super().__init__(service) @@ -109,7 +109,7 @@ Please check tests/demo_app for more examples. ### Boilerplate Django project -A boilerplate Django project for quickly getting started, production ready easy-apis wiht 100% test coverage ready: +A boilerplate Django project for quickly getting started, and get production ready easy-apis with 100% test coverage UP and running: https://github.com/freemindcore/django-easy-api ![Auto generated APIs - Users](https://github.com/freemindcore/django-api-framework/blob/9aa26e92b6fd79f4d9db422ec450fe62d4cd97b9/docs/images/user_admin_api.png) From 3b01ad849baeeecac671aa086e2dfd15bc924e48 Mon Sep 17 00:00:00 2001 From: freemindcore Date: Fri, 14 Oct 2022 03:43:56 +0800 Subject: [PATCH 11/58] lint: .gitignore update --- .gitignore | 1 + 1 file changed, 1 insertion(+) diff --git a/.gitignore b/.gitignore index c3c0e3b..f326ab0 100644 --- a/.gitignore +++ b/.gitignore @@ -389,3 +389,4 @@ easy/media/ /.idea/modules.xml /.idea/vcs.xml /.idea/django-api-framework.iml +/.idea/shelf/ From 8cf17fdb9cfac38ad5f2c1ee7330db48dd72e250 Mon Sep 17 00:00:00 2001 From: freemindcore Date: Sat, 15 Oct 2022 03:56:23 +0800 Subject: [PATCH 12/58] lint: unit test --- easy/decorators.py | 2 +- easy/response.py | 5 +++-- tests/conftest.py | 10 ++++++---- tests/easy_app/controllers.py | 18 +++++++++++++++--- tests/easy_app/urls.py | 16 ++-------------- tests/test_api_base_response.py | 2 +- 6 files changed, 28 insertions(+), 25 deletions(-) diff --git a/easy/decorators.py b/easy/decorators.py index 9510adb..23b0183 100644 --- a/easy/decorators.py +++ b/easy/decorators.py @@ -54,7 +54,7 @@ def docs_permission_required( member, redirecting to the login page if necessary. """ actual_decorator = request_passes_test( - lambda r: ((r.user.is_active and r.user.is_staff)), + lambda r: (r.user.is_active and r.user.is_staff), login_url=login_url, redirect_field_name=redirect_field_name, ) diff --git a/easy/response.py b/easy/response.py index e826ac4..c6b38e8 100644 --- a/easy/response.py +++ b/easy/response.py @@ -1,6 +1,7 @@ import json -from typing import Any, Dict, Union +from typing import Any, Dict, List, Union +from django.db.models import QuerySet from django.http.response import JsonResponse from easy.renderer.json import EasyJSONEncoder @@ -17,7 +18,7 @@ class BaseApiResponse(JsonResponse): def __init__( self, - data: Union[Dict, str] = None, + data: Union[Dict, str, bool, List[Any], QuerySet] = None, code: int = None, message: str = None, **kwargs: Any diff --git a/tests/conftest.py b/tests/conftest.py index dd5a702..2946262 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -1,9 +1,11 @@ import copy +from typing import Callable, Type, Union import pytest from django.contrib.auth import get_user_model +from ninja_extra import ControllerBase, Router -from easy.controller.base import CrudAPIController +from easy import EasyAPI from easy.testing import EasyTestClient from .easy_app.auth import JWTAuthAsync, jwt_auth_async @@ -23,7 +25,7 @@ def user(db) -> User: @pytest.fixture -def easy_api_client(user) -> EasyTestClient: +def easy_api_client(user) -> Callable: orig_func = copy.deepcopy(JWTAuthAsync.__call__) orig_has_perm_fuc = copy.deepcopy(user.has_perm) @@ -41,11 +43,11 @@ async def mock_func(self, request): setattr(JWTAuthAsync, "__call__", mock_func) def create_client( - api: CrudAPIController, + api: Union[EasyAPI, Router, Type[ControllerBase]], is_staff: bool = False, is_superuser: bool = False, has_perm: bool = False, - ): + ) -> "EasyTestClient": setattr(user, "is_staff", is_staff) setattr(user, "is_superuser", is_superuser) if is_superuser: diff --git a/tests/easy_app/controllers.py b/tests/easy_app/controllers.py index 2879ead..6dc20e6 100644 --- a/tests/easy_app/controllers.py +++ b/tests/easy_app/controllers.py @@ -22,7 +22,7 @@ class AutoGenCrudAPIController(CrudAPIController): """ For unit testings of the following auto generated APIs: - get/create/patch/delete/filter/filter_exclude + get/create/patch/delete """ def __init__(self, service: EventService): @@ -36,7 +36,7 @@ class Meta: @api_controller("unittest", permissions=[BaseApiPermission]) class RecursiveAPIController(CrudAPIController): """ - For unit testings of no recursive configuration + For unit testings of recursive configuration """ def __init__(self, service: EventService): @@ -52,7 +52,7 @@ class Meta: @api_controller("unittest", permissions=[BaseApiPermission]) class InheritedRecursiveAPIController(AutoGenCrudAPIController): """ - For unit testings of no recursive configuration + For unit testings of inherited recursive configuration """ def __init__(self, service: EventService): @@ -181,6 +181,10 @@ async def test_perm_admin_site(self, request, word: str): @api_controller("unittest", permissions=[AdminSitePermission]) class AdminSitePermissionAPIController(CrudAPIController): + """ + For unit testings of AdminSite permissions class + """ + def __init__(self, service: EventService): super().__init__(service) self.service = service @@ -191,6 +195,10 @@ class Meta: @api_controller("unittest", permissions=[AdminSitePermission]) class NoCrudAPIController(CrudAPIController): + """ + For unit testings of no crud configuration + """ + def __init__(self, service: EventService): super().__init__(service) self.service = service @@ -202,6 +210,10 @@ class Meta: @api_controller("unittest", permissions=[AdminSitePermission]) class NoCrudInheritedAPIController(AdminSitePermissionAPIController): + """ + For unit testings of no crud configuration (Inherited Class) + """ + def __init__(self, service: EventService): super().__init__(service) self.service = service diff --git a/tests/easy_app/urls.py b/tests/easy_app/urls.py index d616879..3c753df 100644 --- a/tests/easy_app/urls.py +++ b/tests/easy_app/urls.py @@ -1,21 +1,9 @@ from django.contrib import admin from django.urls import path -from easy import EasyAPI - -from .controllers import ( - AutoGenCrudAPIController, - EasyCrudAPIController, - PermissionAPIController, -) - -api = EasyAPI() -api.register_controllers(EasyCrudAPIController) -api.register_controllers(PermissionAPIController) -api.register_controllers(AutoGenCrudAPIController) - +from .apis import api_unittest urlpatterns = [ path("admin/", admin.site.urls), - path("api/", api.urls), + path("api/", api_unittest.urls), ] diff --git a/tests/test_api_base_response.py b/tests/test_api_base_response.py index 0f494c7..e80ef06 100644 --- a/tests/test_api_base_response.py +++ b/tests/test_api_base_response.py @@ -45,7 +45,7 @@ def test_base_api_edit(): ) with pytest.raises(KeyError): - orig_resp.json_data["detail"] + print(orig_resp.json_data["detail"]) data = orig_resp.json_data From f9550c822dde8f784bf2234063b61a8510537374 Mon Sep 17 00:00:00 2001 From: freemindcore Date: Sat, 15 Oct 2022 05:00:48 +0800 Subject: [PATCH 13/58] lint: django orm model --- .../{admin_auto_api.py => auto_api.py} | 0 easy/controller/meta.py | 4 +- easy/domain/base.py | 4 +- easy/domain/orm.py | 58 +++++++++++++++---- easy/main.py | 2 +- easy/services/crud.py | 18 +++--- tests/easy_app/controllers.py | 2 +- 7 files changed, 61 insertions(+), 27 deletions(-) rename easy/controller/{admin_auto_api.py => auto_api.py} (100%) diff --git a/easy/controller/admin_auto_api.py b/easy/controller/auto_api.py similarity index 100% rename from easy/controller/admin_auto_api.py rename to easy/controller/auto_api.py diff --git a/easy/controller/meta.py b/easy/controller/meta.py index 1d5155f..0ce9fd2 100644 --- a/easy/controller/meta.py +++ b/easy/controller/meta.py @@ -2,7 +2,7 @@ import logging import re import uuid -from abc import ABCMeta +from abc import ABC, ABCMeta from collections import ChainMap from typing import Any, Match, Optional, Tuple, Type @@ -20,7 +20,7 @@ logger = logging.getLogger(__name__) -class CrudAPI(CrudModel): +class CrudAPI(CrudModel, ABC): # Never add type note to service, it will cause injection error def __init__(self, service=None): # type: ignore # Critical to set __Meta diff --git a/easy/domain/base.py b/easy/domain/base.py index e4f9069..ec76dc6 100644 --- a/easy/domain/base.py +++ b/easy/domain/base.py @@ -1,5 +1,5 @@ -from easy.domain.orm import BaseOrm +from easy.domain.orm import DjangoOrmModel -class BaseDomain(BaseOrm): +class BaseDomain(DjangoOrmModel): pass diff --git a/easy/domain/orm.py b/easy/domain/orm.py index 23c76d8..15d604c 100644 --- a/easy/domain/orm.py +++ b/easy/domain/orm.py @@ -1,5 +1,6 @@ import logging -from typing import Any, Dict, List, Tuple, Type +from abc import abstractmethod +from typing import Any, Dict, List, Optional, Tuple, Type from django.db import models, transaction from django.db.models.query import QuerySet @@ -11,6 +12,39 @@ class CrudModel(object): + def __init__(self, model: Any): + self.model = model + + @abstractmethod + def crud_add_obj(self, **payload: Dict) -> Any: + raise NotImplementedError + + @abstractmethod + def crud_del_obj(self, pk: int) -> bool: + raise NotImplementedError + + @abstractmethod + def crud_update_obj(self, pk: int, payload: Dict) -> bool: + raise NotImplementedError + + @abstractmethod + def crud_get_obj(self, pk: int) -> Any: + raise NotImplementedError + + @abstractmethod + def crud_get_objs_all(self, maximum: Optional[int] = None, **filters: Any) -> Any: + raise NotImplementedError + + @abstractmethod + def crud_filter(self, **kwargs: Any) -> QuerySet: + raise NotImplementedError + + @abstractmethod + def crud_filter_exclude(self, **kwargs: Any) -> QuerySet: + raise NotImplementedError + + +class DjangoOrmModel(CrudModel): def __init__(self, model: Type[models.Model]): self.model = model self.m2m_fields_list: List = list( @@ -18,6 +52,7 @@ def __init__(self, model: Type[models.Model]): for _field in self.model._meta.get_fields(include_hidden=True) if isinstance(_field, models.ManyToManyField) ) + super().__init__(model) def _separate_payload(self, payload: Dict) -> Tuple[Dict, Dict]: m2m_fields = {} @@ -45,7 +80,7 @@ def _crud_set_m2m_obj(obj: models.Model, m2m_fields: Dict) -> None: # Define BASE CRUD @transaction.atomic() - def _crud_add_obj(self, **payload: Dict) -> Any: + def crud_add_obj(self, **payload: Dict) -> Any: local_f_payload, m2m_f_payload = self._separate_payload(payload) try: @@ -58,7 +93,7 @@ def _crud_add_obj(self, **payload: Dict) -> Any: if obj: return obj.id - def _crud_del_obj(self, pk: int) -> bool: + def crud_del_obj(self, pk: int) -> bool: obj = get_object_or_none(self.model, pk=pk) if obj: self.model.objects.filter(pk=pk).delete() @@ -67,7 +102,7 @@ def _crud_del_obj(self, pk: int) -> bool: return False @transaction.atomic() - def _crud_update_obj(self, pk: int, payload: Dict) -> bool: + def crud_update_obj(self, pk: int, payload: Dict) -> bool: local_fields, m2m_fields = self._separate_payload(payload) if not self.model.objects.filter(pk=pk).exists(): return False @@ -78,7 +113,7 @@ def _crud_update_obj(self, pk: int, payload: Dict) -> bool: raise BaseAPIException(f"Update Error - {e}") return bool(obj) - def _crud_get_obj(self, pk: int) -> Any: + def crud_get_obj(self, pk: int) -> Any: if self.m2m_fields_list: qs = self.model.objects.filter(pk=pk).prefetch_related( self.m2m_fields_list[0].name @@ -90,10 +125,11 @@ def _crud_get_obj(self, pk: int) -> Any: if qs: return qs.first() - def _crud_get_objs_all(self, **filters: Any) -> Any: + def crud_get_objs_all(self, maximum: Optional[int] = None, **filters: Any) -> Any: """ CRUD: get multiple objects, with django orm filters support Args: + maximum: {int} filters: {"field_name__lte", 1} Returns: qs @@ -104,6 +140,8 @@ def _crud_get_objs_all(self, **filters: Any) -> Any: qs = self.model.objects.filter(**filters) except Exception as e: # pragma: no cover logger.error(e) + elif maximum: + qs = self.model.objects.all()[:maximum] else: qs = self.model.objects.all() # If there are 2m2_fields @@ -113,12 +151,8 @@ def _crud_get_objs_all(self, **filters: Any) -> Any: qs = qs.prefetch_related(f.name) return qs - def _crud_filter(self, **kwargs: Any) -> QuerySet: + def crud_filter(self, **kwargs: Any) -> Any: return self.model.objects.filter(**kwargs) # pragma: no cover - def _crud_filter_exclude(self, **kwargs: Any) -> QuerySet: + def crud_filter_exclude(self, **kwargs: Any) -> Any: return self.model.objects.all().exclude(**kwargs) - - -class BaseOrm(CrudModel): - ... diff --git a/easy/main.py b/easy/main.py index 9298fe0..ab0aa87 100644 --- a/easy/main.py +++ b/easy/main.py @@ -11,7 +11,7 @@ from ninja.types import TCallable from ninja_extra import NinjaExtraAPI -from easy.controller.admin_auto_api import create_admin_controller +from easy.controller.auto_api import create_admin_controller from easy.domain.serializers import django_serializer from easy.renderer.json import EasyJSONRenderer from easy.response import BaseApiResponse diff --git a/easy/services/crud.py b/easy/services/crud.py index 469df5f..6063f8b 100644 --- a/easy/services/crud.py +++ b/easy/services/crud.py @@ -4,36 +4,36 @@ from asgiref.sync import sync_to_async from django.db import models -from easy.domain.orm import CrudModel +from easy.domain.orm import DjangoOrmModel logger = logging.getLogger(__name__) -class CrudService(CrudModel): +class CrudService(DjangoOrmModel): def __init__(self, model: Type[models.Model]): super().__init__(model) self.model = model async def get_obj(self, id: int) -> Any: - return await sync_to_async(self._crud_get_obj)(id) + return await sync_to_async(self.crud_get_obj)(id) async def get_objs(self, **filters: Any) -> Any: - return await sync_to_async(self._crud_get_objs_all)(**filters) + return await sync_to_async(self.crud_get_objs_all)(**filters) async def patch_obj(self, id: int, payload: Any) -> Any: - return await sync_to_async(self._crud_update_obj)(id, payload) + return await sync_to_async(self.crud_update_obj)(id, payload) async def del_obj(self, id: int) -> Any: - return await sync_to_async(self._crud_del_obj)(id) + return await sync_to_async(self.crud_del_obj)(id) async def add_obj(self, **payload: Any) -> Any: - return await sync_to_async(self._crud_add_obj)(**payload) + return await sync_to_async(self.crud_add_obj)(**payload) async def filter_objs(self, **payload: Any) -> Any: - return await sync_to_async(self._crud_filter)(**payload) # pragma: no cover + return await sync_to_async(self.crud_filter)(**payload) # pragma: no cover async def filter_exclude_objs(self, **payload: Any) -> Any: - return await sync_to_async(self._crud_filter_exclude)(**payload) + return await sync_to_async(self.crud_filter_exclude)(**payload) # async def bulk_create_objs(self): # ... diff --git a/tests/easy_app/controllers.py b/tests/easy_app/controllers.py index 6dc20e6..612a464 100644 --- a/tests/easy_app/controllers.py +++ b/tests/easy_app/controllers.py @@ -132,7 +132,7 @@ async def get_objs_list_with_filter_exclude(self, request): "/qs/", ) async def list_events(self): - qs = await sync_to_async(self.model.objects.all)() + qs = await sync_to_async(self.service.crud_get_objs_all)(maximum=10) await sync_to_async(list)(qs) if qs: return qs From 352db821d95ad933550b0aabd1522ea51f7980d9 Mon Sep 17 00:00:00 2001 From: freemindcore Date: Sat, 15 Oct 2022 05:15:02 +0800 Subject: [PATCH 14/58] =?UTF-8?q?Bump=20version:=200.1.33=20=E2=86=92=200.?= =?UTF-8?q?1.34?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .bumpversion.cfg | 4 ++-- easy/__init__.py | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/.bumpversion.cfg b/.bumpversion.cfg index 8b3ac7a..21e1162 100644 --- a/.bumpversion.cfg +++ b/.bumpversion.cfg @@ -1,9 +1,9 @@ [bumpversion] -current_version = 0.1.32 +current_version = 0.1.34 commit = True tag = True parse = (?P\d+)\.(?P\d+)\.(?P\d+) -serialize = +serialize = {major}.{feat}.{patch} [bumpversion:file:easy/__init__.py] diff --git a/easy/__init__.py b/easy/__init__.py index da633eb..ab8d117 100644 --- a/easy/__init__.py +++ b/easy/__init__.py @@ -1,6 +1,6 @@ """Django Easy API - Easy and Fast Django REST framework based on Django-ninja-extra""" -__version__ = "0.1.32" +__version__ = "0.1.34" from easy.main import EasyAPI From e938b8ff9a0b2cb7ca527cbb3bede7522db0a6cf Mon Sep 17 00:00:00 2001 From: freemindcore Date: Sat, 15 Oct 2022 23:54:51 +0800 Subject: [PATCH 15/58] feat: do not prefetch excluded fields --- easy/domain/orm.py | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/easy/domain/orm.py b/easy/domain/orm.py index 15d604c..a5b667f 100644 --- a/easy/domain/orm.py +++ b/easy/domain/orm.py @@ -6,6 +6,7 @@ from django.db.models.query import QuerySet from ninja_extra.shortcuts import get_object_or_none +from easy.controller.meta_conf import ModelMetaConfig from easy.exception import BaseAPIException logger = logging.getLogger(__name__) @@ -47,10 +48,15 @@ def crud_filter_exclude(self, **kwargs: Any) -> QuerySet: class DjangoOrmModel(CrudModel): def __init__(self, model: Type[models.Model]): self.model = model + config = ModelMetaConfig() + exclude_list = config.get_final_excluded_list(self.model()) self.m2m_fields_list: List = list( _field for _field in self.model._meta.get_fields(include_hidden=True) - if isinstance(_field, models.ManyToManyField) + if ( + isinstance(_field, models.ManyToManyField) + and ((_field not in exclude_list) if exclude_list else True) + ) ) super().__init__(model) From 0b0309acc9a9c3b9ec58d453327cfed3c97dc3ed Mon Sep 17 00:00:00 2001 From: freemindcore Date: Sun, 16 Oct 2022 09:20:00 +0800 Subject: [PATCH 16/58] fix: AdminSite permission bug --- easy/permissions/adminsite.py | 14 ++++++++++---- 1 file changed, 10 insertions(+), 4 deletions(-) diff --git a/easy/permissions/adminsite.py b/easy/permissions/adminsite.py index 9e9f820..3b95b96 100644 --- a/easy/permissions/adminsite.py +++ b/easy/permissions/adminsite.py @@ -24,16 +24,22 @@ def has_permission( model: models.Model = cast(models.Model, getattr(controller, "model", None)) if model: app: str = model._meta.app_label + model_name = model.__name__.lower() if request.method in ("GET", "OPTIONS"): - has_perm = user.has_perm(f"{app}.view{model.__name__}") # type: ignore + has_perm = user.has_perm(f"{app}.view_{model_name}") # type: ignore + elif request.method in ("PUT", "POST"): - has_perm = user.has_perm(f"{app}.add_{model.__name__}") # type: ignore + has_perm = user.has_perm(f"{app}.add_{model_name}") # type: ignore + elif request.method in ("PUT", "PATCH", "POST"): - has_perm = user.has_perm(f"{app}.change_{model.__name__}") # type: ignore + has_perm = user.has_perm(f"{app}.change_{model_name}") # type: ignore + elif request.method in ("DELETE",): - has_perm = user.has_perm(f"{app}.delete_{model.__name__}") # type: ignore + has_perm = user.has_perm(f"{app}.delete_{model_name}") # type: ignore + if user.is_superuser: # type: ignore has_perm = True + return bool( user and user.is_authenticated From 461d5801e845a5543908ea15d5a38cb5bce28146 Mon Sep 17 00:00:00 2001 From: freemindcore Date: Sun, 16 Oct 2022 12:50:14 +0800 Subject: [PATCH 17/58] fix: auto_apis unit bugs --- tests/conftest.py | 41 +++++++++++++++++---------------- tests/test_auto_api_creation.py | 28 +++++++++++++++------- 2 files changed, 41 insertions(+), 28 deletions(-) diff --git a/tests/conftest.py b/tests/conftest.py index 2946262..6e5f7e4 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -28,37 +28,38 @@ def user(db) -> User: def easy_api_client(user) -> Callable: orig_func = copy.deepcopy(JWTAuthAsync.__call__) - orig_has_perm_fuc = copy.deepcopy(user.has_perm) - - def mock_has_perm_true(*args, **kwargs): - return True - - def mock_has_perm_false(*args, **kwargs): - return False - - async def mock_func(self, request): - setattr(request, "user", user) - return True - - setattr(JWTAuthAsync, "__call__", mock_func) - def create_client( api: Union[EasyAPI, Router, Type[ControllerBase]], + api_user=None, # type: ignore is_staff: bool = False, is_superuser: bool = False, has_perm: bool = False, ) -> "EasyTestClient": - setattr(user, "is_staff", is_staff) - setattr(user, "is_superuser", is_superuser) + if api_user is None: + api_user = user + setattr(api_user, "is_staff", is_staff) + setattr(api_user, "is_superuser", is_superuser) + + def mock_has_perm_true(*args, **kwargs): + return True + + def mock_has_perm_false(*args, **kwargs): + return False + + async def mock_func(self, request): + setattr(request, "user", api_user) + return True + + setattr(JWTAuthAsync, "__call__", mock_func) + if is_superuser: - setattr(user, "is_staff", True) + setattr(api_user, "is_staff", True) if has_perm: - setattr(user, "has_perm", mock_has_perm_true) + setattr(api_user, "has_perm", mock_has_perm_true) else: - setattr(user, "has_perm", mock_has_perm_false) + setattr(api_user, "has_perm", mock_has_perm_false) client = EasyTestClient(api, auth=jwt_auth_async) return client yield create_client setattr(JWTAuthAsync, "__call__", orig_func) - setattr(user, "has_perm", orig_has_perm_fuc) diff --git a/tests/test_auto_api_creation.py b/tests/test_auto_api_creation.py index 5dcc525..a544451 100644 --- a/tests/test_auto_api_creation.py +++ b/tests/test_auto_api_creation.py @@ -32,21 +32,33 @@ def test_auto_generate_admin_api(): assert "TypeAdminAPIController" in controller_names -async def test_auto_apis(transactional_db, easy_api_client): +async def test_auto_apis(transactional_db, user, easy_api_client): for controller_class in controllers: if not str(controller_class).endswith("ClientAdminAPIController"): continue - - client = easy_api_client(controller_class) - response = await client.get("/") - # TODO: figure out why user.is_authenticated is False in auto created API - + client = easy_api_client(controller_class, api_user=user, has_perm=True) + response = await client.get("/", data={}, json={}, user=user) assert response.status_code == 403 - # assert response.json()["data"] == [] + client = easy_api_client( + controller_class, api_user=user, has_perm=True, is_staff=True + ) + response = await client.get("/", data={}, json={}, user=user) + assert response.status_code == 200 + assert response.json()["data"] == [] + + client = easy_api_client( + controller_class, api_user=user, has_perm=True, is_staff=True + ) response = await client.delete("/20000") assert response.status_code == 403 - # assert response.json()["code"] == 404 + + client = easy_api_client( + controller_class, api_user=user, has_perm=True, is_staff=True + ) + response = await client.delete("/20000", data={}, json={}, user=user) + assert response.status_code == 200 + assert response.json()["code"] == 404 async def test_auto_generation_settings(settings): From b1154722820bc140cd20a9f995f35e9e7392982e Mon Sep 17 00:00:00 2001 From: freemindcore Date: Sun, 16 Oct 2022 20:56:18 +0800 Subject: [PATCH 18/58] lint: options --- easy/controller/meta.py | 8 +++----- easy/controller/meta_conf.py | 10 +++++----- easy/domain/orm.py | 4 +++- easy/permissions/adminsite.py | 2 +- 4 files changed, 12 insertions(+), 12 deletions(-) diff --git a/easy/controller/meta.py b/easy/controller/meta.py index 0ce9fd2..2fc0949 100644 --- a/easy/controller/meta.py +++ b/easy/controller/meta.py @@ -25,10 +25,8 @@ class CrudAPI(CrudModel, ABC): def __init__(self, service=None): # type: ignore # Critical to set __Meta self.service = service - if self.service: - self.model = self.service.model - _model_opts: ModelOptions = ModelOptions.get_model_options(self.__class__) + _model_opts: ModelOptions = ModelOptions.get_model_options(self.Meta) if self.model and _model_opts: ModelOptions.set_model_meta(self.model, _model_opts) @@ -40,8 +38,8 @@ def __init__(self, service=None): # type: ignore class CrudApiMetaclass(ABCMeta): def __new__(mcs, name: str, bases: Tuple[Type[Any], ...], attrs: dict) -> Any: # Get configs from Meta - _temp_cls: Type = super().__new__(mcs, name, (object,), attrs) - model_opts: ModelOptions = ModelOptions.get_model_options(_temp_cls) + attrs_meta = attrs.get("Meta", None) + model_opts: ModelOptions = ModelOptions.get_model_options(attrs_meta) # Get all attrs from parents excluding private ones def is_private_attrs(attr_name: str) -> Optional[Match[str]]: diff --git a/easy/controller/meta_conf.py b/easy/controller/meta_conf.py index e259eaa..f2c53fb 100644 --- a/easy/controller/meta_conf.py +++ b/easy/controller/meta_conf.py @@ -1,8 +1,8 @@ -from typing import Any, List, Optional, Type, Union +from typing import Any, Dict, List, Optional, Type, Union from django.db import models -META_ATTRIBUTE_NAME: str = "__EASY_API_META__" +META_ATTRIBUTE_NAME: str = "_easy_api_meta_" GENERATE_CRUD_ATTR: str = "generate_crud" GENERATE_CRUD_ATTR_DEFAULT = True @@ -24,7 +24,7 @@ class ModelOptions: - def __init__(self, options: object = None): + def __init__(self, options: Dict = None): """ Configuration reader """ @@ -49,8 +49,8 @@ def __init__(self, options: object = None): ) @classmethod - def get_model_options(cls, klass: Type) -> Any: - return ModelOptions(getattr(klass, "Meta", None)) + def get_model_options(cls, meta: Dict) -> Any: + return ModelOptions(meta) @classmethod def set_model_meta( diff --git a/easy/domain/orm.py b/easy/domain/orm.py index a5b667f..8107187 100644 --- a/easy/domain/orm.py +++ b/easy/domain/orm.py @@ -13,6 +13,8 @@ class CrudModel(object): + Meta: Dict = {} + def __init__(self, model: Any): self.model = model @@ -58,7 +60,7 @@ def __init__(self, model: Type[models.Model]): and ((_field not in exclude_list) if exclude_list else True) ) ) - super().__init__(model) + super().__init__(self.model) def _separate_payload(self, payload: Dict) -> Tuple[Dict, Dict]: m2m_fields = {} diff --git a/easy/permissions/adminsite.py b/easy/permissions/adminsite.py index 3b95b96..0f91608 100644 --- a/easy/permissions/adminsite.py +++ b/easy/permissions/adminsite.py @@ -24,7 +24,7 @@ def has_permission( model: models.Model = cast(models.Model, getattr(controller, "model", None)) if model: app: str = model._meta.app_label - model_name = model.__name__.lower() + model_name = model._meta.model_name if request.method in ("GET", "OPTIONS"): has_perm = user.has_perm(f"{app}.view_{model_name}") # type: ignore From fa0aeae12b962e949417c715ecdf37abfcb6311d Mon Sep 17 00:00:00 2001 From: freemindcore Date: Sun, 16 Oct 2022 21:05:26 +0800 Subject: [PATCH 19/58] docs: update --- easy/controller/meta.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/easy/controller/meta.py b/easy/controller/meta.py index 2fc0949..f5dcf4b 100644 --- a/easy/controller/meta.py +++ b/easy/controller/meta.py @@ -84,8 +84,8 @@ async def del_obj(self, request: HttpRequest, id: int) -> Any: # type: ignore @paginate async def get_objs(self, request: HttpRequest, filters: str = None) -> Any: # type: ignore """ - GET /?maximum={int}&filters={filters_dict} - Retrieve multiple Object (optional: maximum # and filters) + GET /?filters={filters_dict} + Retrieve multiple Object (optional: django filters) """ if filters: return await self.service.get_objs(**json.loads(filters)) From 1bac8d8539d112b42fc5e32b0535527d195c1213 Mon Sep 17 00:00:00 2001 From: freemindcore Date: Sun, 16 Oct 2022 21:33:22 +0800 Subject: [PATCH 20/58] feat: support class ApiMeta configuration for each django model --- easy/controller/auto_api.py | 21 ++++++++------- easy/controller/meta_conf.py | 2 +- tests/easy_app/models.py | 3 +++ tests/test_auto_api_creation.py | 48 ++++++++++++++++++--------------- 4 files changed, 41 insertions(+), 33 deletions(-) diff --git a/easy/controller/auto_api.py b/easy/controller/auto_api.py index 1c050d2..5fd342b 100644 --- a/easy/controller/auto_api.py +++ b/easy/controller/auto_api.py @@ -8,15 +8,11 @@ from easy.controller.base import CrudAPIController from easy.controller.meta_conf import ( GENERATE_CRUD_ATTR, - GENERATE_CRUD_ATTR_DEFAULT, MODEL_FIELDS_ATTR, - MODEL_FIELDS_ATTR_DEFAULT, MODEL_JOIN_ATTR, - MODEL_JOIN_ATTR_DEFAULT, MODEL_RECURSIVE_ATTR, - MODEL_RECURSIVE_ATTR_DEFAULT, SENSITIVE_FIELDS_ATTR, - SENSITIVE_FIELDS_ATTR_DEFAULT, + ModelOptions, ) from easy.permissions import AdminSitePermission, BaseApiPermission @@ -31,16 +27,21 @@ def create_api_controller( ) -> Union[Type[ControllerBase], Type]: """Create APIController class dynamically, with specified permission class""" model_name = model.__name__ # type:ignore + + model_opts: ModelOptions = ModelOptions.get_model_options( + getattr(model, "ApiMeta", None) + ) + Meta = type( "Meta", (object,), { "model": model, - GENERATE_CRUD_ATTR: GENERATE_CRUD_ATTR_DEFAULT, - MODEL_FIELDS_ATTR: MODEL_FIELDS_ATTR_DEFAULT, - MODEL_RECURSIVE_ATTR: MODEL_RECURSIVE_ATTR_DEFAULT, - MODEL_JOIN_ATTR: MODEL_JOIN_ATTR_DEFAULT, - SENSITIVE_FIELDS_ATTR: SENSITIVE_FIELDS_ATTR_DEFAULT, + GENERATE_CRUD_ATTR: model_opts.generate_crud, + MODEL_FIELDS_ATTR: model_opts.model_fields, + MODEL_RECURSIVE_ATTR: model_opts.model_recursive, + MODEL_JOIN_ATTR: model_opts.model_join, + SENSITIVE_FIELDS_ATTR: model_opts.model_fields, }, ) diff --git a/easy/controller/meta_conf.py b/easy/controller/meta_conf.py index f2c53fb..d07d043 100644 --- a/easy/controller/meta_conf.py +++ b/easy/controller/meta_conf.py @@ -49,7 +49,7 @@ def __init__(self, options: Dict = None): ) @classmethod - def get_model_options(cls, meta: Dict) -> Any: + def get_model_options(cls, meta: Optional[Any]) -> "ModelOptions": return ModelOptions(meta) @classmethod diff --git a/tests/easy_app/models.py b/tests/easy_app/models.py index b1fc578..8a66df6 100644 --- a/tests/easy_app/models.py +++ b/tests/easy_app/models.py @@ -10,6 +10,9 @@ class Category(TestBaseModel): title = models.CharField(max_length=100) status = models.PositiveSmallIntegerField(default=1, null=True) + class ApiMeta: + generate_crud = False + class Client(TestBaseModel): key = models.CharField(max_length=20, unique=True) diff --git a/tests/test_auto_api_creation.py b/tests/test_auto_api_creation.py index a544451..a7861e2 100644 --- a/tests/test_auto_api_creation.py +++ b/tests/test_auto_api_creation.py @@ -1,3 +1,4 @@ +import pytest from ninja_extra.operation import AsyncOperation from easy import EasyAPI @@ -34,31 +35,34 @@ def test_auto_generate_admin_api(): async def test_auto_apis(transactional_db, user, easy_api_client): for controller_class in controllers: - if not str(controller_class).endswith("ClientAdminAPIController"): - continue - client = easy_api_client(controller_class, api_user=user, has_perm=True) - response = await client.get("/", data={}, json={}, user=user) - assert response.status_code == 403 + if str(controller_class).endswith("ClientAdminAPIController"): + client = easy_api_client(controller_class, api_user=user, has_perm=True) + response = await client.get("/", data={}, json={}, user=user) + assert response.status_code == 403 - client = easy_api_client( - controller_class, api_user=user, has_perm=True, is_staff=True - ) - response = await client.get("/", data={}, json={}, user=user) - assert response.status_code == 200 - assert response.json()["data"] == [] + client = easy_api_client( + controller_class, api_user=user, has_perm=True, is_staff=True + ) + response = await client.get("/", data={}, json={}, user=user) + assert response.status_code == 200 + assert response.json()["data"] == [] - client = easy_api_client( - controller_class, api_user=user, has_perm=True, is_staff=True - ) - response = await client.delete("/20000") - assert response.status_code == 403 + client = easy_api_client( + controller_class, api_user=user, has_perm=True, is_staff=True + ) + response = await client.delete("/20000") + assert response.status_code == 403 - client = easy_api_client( - controller_class, api_user=user, has_perm=True, is_staff=True - ) - response = await client.delete("/20000", data={}, json={}, user=user) - assert response.status_code == 200 - assert response.json()["code"] == 404 + client = easy_api_client( + controller_class, api_user=user, has_perm=True, is_staff=True + ) + response = await client.delete("/20000", data={}, json={}, user=user) + assert response.status_code == 200 + assert response.json()["code"] == 404 + elif str(controller_class).endswith("CategoryAdminAPIController"): + client = easy_api_client(controller_class, api_user=user, has_perm=True) + with pytest.raises(Exception): + await client.get("/", data={}, json={}, user=user) async def test_auto_generation_settings(settings): From 196cccccb95ab63897e6a7f958b0317530073d6f Mon Sep 17 00:00:00 2001 From: freemindcore Date: Sun, 16 Oct 2022 22:19:24 +0800 Subject: [PATCH 21/58] =?UTF-8?q?Bump=20version:=200.1.34=20=E2=86=92=200.?= =?UTF-8?q?1.35?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .bumpversion.cfg | 4 ++-- easy/__init__.py | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/.bumpversion.cfg b/.bumpversion.cfg index 21e1162..8766caa 100644 --- a/.bumpversion.cfg +++ b/.bumpversion.cfg @@ -1,9 +1,9 @@ [bumpversion] -current_version = 0.1.34 +current_version = 0.1.35 commit = True tag = True parse = (?P\d+)\.(?P\d+)\.(?P\d+) -serialize = +serialize = {major}.{feat}.{patch} [bumpversion:file:easy/__init__.py] diff --git a/easy/__init__.py b/easy/__init__.py index ab8d117..23555c5 100644 --- a/easy/__init__.py +++ b/easy/__init__.py @@ -1,6 +1,6 @@ """Django Easy API - Easy and Fast Django REST framework based on Django-ninja-extra""" -__version__ = "0.1.34" +__version__ = "0.1.35" from easy.main import EasyAPI From 8eaf61155143283a094ec61aae59722014c8baa0 Mon Sep 17 00:00:00 2001 From: freemindcore Date: Sun, 16 Oct 2022 22:50:53 +0800 Subject: [PATCH 22/58] lint: unit test --- tests/easy_app/controllers.py | 5 ++--- tests/test_async_auto_crud_apis.py | 1 + 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/tests/easy_app/controllers.py b/tests/easy_app/controllers.py index 612a464..311b71d 100644 --- a/tests/easy_app/controllers.py +++ b/tests/easy_app/controllers.py @@ -90,9 +90,8 @@ class AutoGenCrudSomeFieldsAPIController(CrudAPIController): class Meta: model = Client - model_fields = [ - "key", - "name", + model_exclude = [ + "category", ] diff --git a/tests/test_async_auto_crud_apis.py b/tests/test_async_auto_crud_apis.py index 0bbb3c8..7f60e71 100644 --- a/tests/test_async_auto_crud_apis.py +++ b/tests/test_async_auto_crud_apis.py @@ -228,6 +228,7 @@ async def test_crud_default_create_some_fields( assert response.json()["data"]["key"] == "Type" with pytest.raises(KeyError): print(response.json()["data"]["password"]) + with pytest.raises(KeyError): print(response.json()["data"]["category"]) async def test_crud_default_patch(self, transactional_db, easy_api_client): From 678d2bea98e13f817df8e11dd4d7896532eb524c Mon Sep 17 00:00:00 2001 From: freemindcore Date: Sun, 16 Oct 2022 22:58:11 +0800 Subject: [PATCH 23/58] fix: add missing MODEL_EXCLUDE_ATTR config --- easy/controller/auto_api.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/easy/controller/auto_api.py b/easy/controller/auto_api.py index 5fd342b..d04ad80 100644 --- a/easy/controller/auto_api.py +++ b/easy/controller/auto_api.py @@ -8,6 +8,7 @@ from easy.controller.base import CrudAPIController from easy.controller.meta_conf import ( GENERATE_CRUD_ATTR, + MODEL_EXCLUDE_ATTR, MODEL_FIELDS_ATTR, MODEL_JOIN_ATTR, MODEL_RECURSIVE_ATTR, @@ -38,6 +39,7 @@ def create_api_controller( { "model": model, GENERATE_CRUD_ATTR: model_opts.generate_crud, + MODEL_EXCLUDE_ATTR: model_opts.model_exclude, MODEL_FIELDS_ATTR: model_opts.model_fields, MODEL_RECURSIVE_ATTR: model_opts.model_recursive, MODEL_JOIN_ATTR: model_opts.model_join, From 1eed45b18415439503dbe251c5fa2fcbae01584b Mon Sep 17 00:00:00 2001 From: freemindcore Date: Sun, 16 Oct 2022 23:04:55 +0800 Subject: [PATCH 24/58] fix: coverage back to 100% --- tests/easy_app/controllers.py | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/tests/easy_app/controllers.py b/tests/easy_app/controllers.py index 311b71d..612a464 100644 --- a/tests/easy_app/controllers.py +++ b/tests/easy_app/controllers.py @@ -90,8 +90,9 @@ class AutoGenCrudSomeFieldsAPIController(CrudAPIController): class Meta: model = Client - model_exclude = [ - "category", + model_fields = [ + "key", + "name", ] From f7ecd82b688f3f5961e7c04e2fbe4aa7d94ee31b Mon Sep 17 00:00:00 2001 From: freemindcore Date: Sun, 16 Oct 2022 23:05:09 +0800 Subject: [PATCH 25/58] =?UTF-8?q?Bump=20version:=200.1.35=20=E2=86=92=200.?= =?UTF-8?q?1.36?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .bumpversion.cfg | 2 +- easy/__init__.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/.bumpversion.cfg b/.bumpversion.cfg index 8766caa..f83fe2b 100644 --- a/.bumpversion.cfg +++ b/.bumpversion.cfg @@ -1,5 +1,5 @@ [bumpversion] -current_version = 0.1.35 +current_version = 0.1.36 commit = True tag = True parse = (?P\d+)\.(?P\d+)\.(?P\d+) diff --git a/easy/__init__.py b/easy/__init__.py index 23555c5..13dded8 100644 --- a/easy/__init__.py +++ b/easy/__init__.py @@ -1,6 +1,6 @@ """Django Easy API - Easy and Fast Django REST framework based on Django-ninja-extra""" -__version__ = "0.1.35" +__version__ = "0.1.36" from easy.main import EasyAPI From ad21b4e3d921806a65bb3b466d5033bd028a2a9c Mon Sep 17 00:00:00 2001 From: freemindcore Date: Sun, 16 Oct 2022 23:27:19 +0800 Subject: [PATCH 26/58] Update README.md --- README.md | 28 ++++++++++++++++++++++++---- 1 file changed, 24 insertions(+), 4 deletions(-) diff --git a/README.md b/README.md index e042afd..f6812de 100644 --- a/README.md +++ b/README.md @@ -63,7 +63,7 @@ api_admin_v1 = EasyAPI( # Automatic Admin API generation api_admin_v1.auto_create_admin_controllers() ``` -Now go to urls.py and add the following: +Go to urls.py and add the following: ``` from django.urls import path from .apis import api_admin_v1 @@ -78,16 +78,36 @@ Now go to http://127.0.0.1:8000/api_admin/v1/docs You will see the automatic interactive API documentation (provided by Swagger UI). ![Auto generated APIs List](https://github.com/freemindcore/django-api-framework/blob/fae8209a8d08c55daf75ac3a4619fe62b8ef3af6/docs/images/admin_apis_list.png) -#### Adding CRUD APIs to a specific API Controller +#### Auto generation configuration +If AUTO_ADMIN_ENABLED_ALL_APPS is set to True (default), all app models CRUD apis will be generated. +Apps in the AUTO_ADMIN_EXCLUDE_APPS list, will be always excluded. -By inheriting CrudAPIController class, CRUD APIs will be added to your API controller. -Configuration is available via Meta class: +If AUTO_ADMIN_ENABLED_ALL_APPS is set to False, only apps in the AUTO_ADMIN_INCLUDE_APPS list will have CRUD apis generated. + +Also, configuration is possible for each model, via ApiMeta class: +- `generate_crud`: whether to create crud api, default to True - `model_exclude`: fields to be excluded in Schema - `model_fields`: fields to be included in Schema, default to `"__all__"` - `model_join`: prefetch and retrieve all m2m fields, default to False - `model_recursive`: recursively retrieve FK/OneToOne fields, default to False - `sensitive_fields`: fields to be ignored +``` +class Category(TestBaseModel): + title = models.CharField(max_length=100) + status = models.PositiveSmallIntegerField(default=1, null=True) + + class ApiMeta: + generate_crud = True + model_fields = ["field_1", "field_2",] # if not configured default to "__all__" + model_join = True + model_recursive = True + sensitive_fields = ["password", "sensitive_info"] +``` + +### Adding CRUD APIs to a specific API Controller +By inheriting CrudAPIController class, CRUD APIs can be added to any API controller. +Configuration is available via Meta inner class in your Controller, same as the above ApiMeta inner class defined in your Django models. Example: ``` From 780e7ee20446a88b591a6caa47959a000f295251 Mon Sep 17 00:00:00 2001 From: freemindcore Date: Mon, 17 Oct 2022 09:49:22 +0800 Subject: [PATCH 27/58] Update README.md --- README.md | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/README.md b/README.md index f6812de..a42e95f 100644 --- a/README.md +++ b/README.md @@ -50,7 +50,7 @@ Then add "easy" to your django INSTALLED_APPS: ``` ### Usage -#### Get all your Django app CRUD APIs up and running +#### Get all your Django app CRUD APIs up and running in < 1 min In your Django project next to urls.py create new apis.py file: ``` from easy.main import EasyAPI @@ -93,6 +93,8 @@ Also, configuration is possible for each model, via ApiMeta class: - `sensitive_fields`: fields to be ignored ``` + +Example: class Category(TestBaseModel): title = models.CharField(max_length=100) status = models.PositiveSmallIntegerField(default=1, null=True) From a04da32544e745f1046a708c7769e3e7bc90af73 Mon Sep 17 00:00:00 2001 From: freemindcore Date: Mon, 17 Oct 2022 15:09:04 +0800 Subject: [PATCH 28/58] Update README.md --- README.md | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/README.md b/README.md index a42e95f..9ae23ec 100644 --- a/README.md +++ b/README.md @@ -79,10 +79,10 @@ You will see the automatic interactive API documentation (provided by Swagger UI ![Auto generated APIs List](https://github.com/freemindcore/django-api-framework/blob/fae8209a8d08c55daf75ac3a4619fe62b8ef3af6/docs/images/admin_apis_list.png) #### Auto generation configuration -If AUTO_ADMIN_ENABLED_ALL_APPS is set to True (default), all app models CRUD apis will be generated. -Apps in the AUTO_ADMIN_EXCLUDE_APPS list, will be always excluded. +If `AUTO_ADMIN_ENABLED_ALL_APPS` is set to True (default), all app models CRUD apis will be generated. +Apps in the `AUTO_ADMIN_EXCLUDE_APPS` list, will be always excluded. -If AUTO_ADMIN_ENABLED_ALL_APPS is set to False, only apps in the AUTO_ADMIN_INCLUDE_APPS list will have CRUD apis generated. +If `AUTO_ADMIN_ENABLED_ALL_APPS` is set to False, only apps in the `AUTO_ADMIN_INCLUDE_APPS` list will have CRUD apis generated. Also, configuration is possible for each model, via ApiMeta class: - `generate_crud`: whether to create crud api, default to True @@ -92,9 +92,8 @@ Also, configuration is possible for each model, via ApiMeta class: - `model_recursive`: recursively retrieve FK/OneToOne fields, default to False - `sensitive_fields`: fields to be ignored -``` - Example: +``` class Category(TestBaseModel): title = models.CharField(max_length=100) status = models.PositiveSmallIntegerField(default=1, null=True) @@ -108,10 +107,11 @@ class Category(TestBaseModel): ``` ### Adding CRUD APIs to a specific API Controller -By inheriting CrudAPIController class, CRUD APIs can be added to any API controller. -Configuration is available via Meta inner class in your Controller, same as the above ApiMeta inner class defined in your Django models. +By inheriting `CrudAPIController` class, CRUD APIs can be added to any API controller. +Configuration is available via `Meta` inner class in your Controller, same as the above `ApiMeta` inner class defined in your Django models. Example: + ``` @api_controller("event_api", permissions=[AdminSitePermission]) class EventAPIController(CrudAPIController): From b5a18ae02b7c78737db919d575243be53a9c41a4 Mon Sep 17 00:00:00 2001 From: freemindcore Date: Tue, 18 Oct 2022 15:59:56 +0800 Subject: [PATCH 29/58] feat: remove pk from Create/Update API schema --- easy/controller/meta.py | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/easy/controller/meta.py b/easy/controller/meta.py index f5dcf4b..b7527e1 100644 --- a/easy/controller/meta.py +++ b/easy/controller/meta.py @@ -109,11 +109,15 @@ async def get_objs(self, request: HttpRequest, filters: str = None) -> Any: # t class DataSchema(ModelSchema): class Config: model = model_opts.model + model_exclude = [] if model_opts.model_exclude: - model_exclude = model_opts.model_exclude + model_exclude.extend(model_opts.model_exclude) + # Remove pk(id) from Create/Update Schema + model_exclude.extend([model._meta.pk.name]) else: if model_opts.model_fields == MODEL_FIELDS_ATTR_DEFAULT: - model_fields = MODEL_FIELDS_ATTR_DEFAULT + # Remove pk(id) from Create/Update Schema + model_exclude.extend([model._meta.pk.name]) else: model_fields = ( model_opts.model_fields # type: ignore From 9e9b6d6de5a9a725a6c31c7099c2b4de3fb6cbd5 Mon Sep 17 00:00:00 2001 From: freemindcore Date: Tue, 18 Oct 2022 16:25:23 +0800 Subject: [PATCH 30/58] feat: improve response of create/update APIs --- easy/controller/meta.py | 20 +++++++++++--------- easy/response.py | 9 ++++----- tests/test_async_api_permissions.py | 2 +- tests/test_async_auto_crud_apis.py | 5 +++-- 4 files changed, 19 insertions(+), 17 deletions(-) diff --git a/easy/controller/meta.py b/easy/controller/meta.py index b7527e1..927e855 100644 --- a/easy/controller/meta.py +++ b/easy/controller/meta.py @@ -4,7 +4,7 @@ import uuid from abc import ABC, ABCMeta from collections import ChainMap -from typing import Any, Match, Optional, Tuple, Type +from typing import Any, List, Match, Optional, Tuple, Type from django.http import HttpRequest from ninja import ModelSchema @@ -109,18 +109,18 @@ async def get_objs(self, request: HttpRequest, filters: str = None) -> Any: # t class DataSchema(ModelSchema): class Config: model = model_opts.model - model_exclude = [] + model_exclude: List = [] if model_opts.model_exclude: model_exclude.extend(model_opts.model_exclude) # Remove pk(id) from Create/Update Schema - model_exclude.extend([model._meta.pk.name]) + model_exclude.extend([model._meta.pk.name]) # type: ignore else: if model_opts.model_fields == MODEL_FIELDS_ATTR_DEFAULT: # Remove pk(id) from Create/Update Schema - model_exclude.extend([model._meta.pk.name]) + model_exclude.extend([model._meta.pk.name]) # type: ignore else: model_fields = ( - model_opts.model_fields # type: ignore + model_opts.model_fields if model_opts.model_fields else MODEL_FIELDS_ATTR_DEFAULT ) @@ -134,9 +134,11 @@ async def add_obj( # type: ignore """ obj_id = await self.service.add_obj(**data.dict()) if obj_id: - return BaseApiResponse({"id": obj_id}, code=201) + return BaseApiResponse({"id": obj_id}, code=201, message="Created.") else: - return BaseApiResponse("Add failed", code=204) # pragma: no cover + return BaseApiResponse( + code=204, message="Add failed." + ) # pragma: no cover async def patch_obj( # type: ignore self, request: HttpRequest, id: int, data: DataSchema @@ -146,9 +148,9 @@ async def patch_obj( # type: ignore Update a single object """ if await self.service.patch_obj(id=id, payload=data.dict()): - return BaseApiResponse("Updated.") + return BaseApiResponse(message="Updated.") else: - return BaseApiResponse("Update Failed", code=400) + return BaseApiResponse(code=400, message="Updated Failed") DataSchema.__name__ = ( f"{model_opts.model.__name__}__AutoSchema({str(uuid.uuid4())[:4]})" diff --git a/easy/response.py b/easy/response.py index c6b38e8..c1635ba 100644 --- a/easy/response.py +++ b/easy/response.py @@ -6,9 +6,8 @@ from easy.renderer.json import EasyJSONEncoder -ERRNO_SUCCESS = 0 +CODE_SUCCESS = 0 SUCCESS_MESSAGE = "success" -UNKNOWN_ERROR_MSG = "system error" class BaseApiResponse(JsonResponse): @@ -24,10 +23,10 @@ def __init__( **kwargs: Any ): if code: - message = message or UNKNOWN_ERROR_MSG + message = message or str(code) else: - message = SUCCESS_MESSAGE - code = ERRNO_SUCCESS + message = message or SUCCESS_MESSAGE + code = CODE_SUCCESS _data: Union[Dict, str] = { "code": code, diff --git a/tests/test_async_api_permissions.py b/tests/test_async_api_permissions.py index c4d2156..afd1893 100644 --- a/tests/test_async_api_permissions.py +++ b/tests/test_async_api_permissions.py @@ -192,7 +192,7 @@ async def test_perm_auto_apis_patch(self, transactional_db, easy_api_client): response = await client.patch( f"/{event.id}", json=new_data, content_type="application/json" ) - assert response.json().get("data") + assert response.json().get("message") == "Updated." response = await client.get( f"/{event.id}", diff --git a/tests/test_async_auto_crud_apis.py b/tests/test_async_auto_crud_apis.py index 7f60e71..e10ad07 100644 --- a/tests/test_async_auto_crud_apis.py +++ b/tests/test_async_auto_crud_apis.py @@ -193,8 +193,9 @@ async def test_crud_default_create(self, transactional_db, easy_api_client): "/", json=object_data, content_type="application/json" ) assert response.status_code == 200 - assert response.json().get("code") == 201 + assert response.json().get("message") == "Created." + event_id = response.json().get("data")["id"] response = await client.get( @@ -276,7 +277,7 @@ async def test_crud_default_patch(self, transactional_db, easy_api_client): ) assert response.status_code == 200 - assert response.json().get("data") + assert response.json().get("message") == "Updated." response = await client.get( f"/{event.pk}", From 93305aa4994d7930cd659db91daa0276b0eb13cd Mon Sep 17 00:00:00 2001 From: freemindcore Date: Tue, 18 Oct 2022 17:34:14 +0800 Subject: [PATCH 31/58] =?UTF-8?q?Bump=20version:=200.1.36=20=E2=86=92=200.?= =?UTF-8?q?1.37?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .bumpversion.cfg | 2 +- easy/__init__.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/.bumpversion.cfg b/.bumpversion.cfg index f83fe2b..fefba98 100644 --- a/.bumpversion.cfg +++ b/.bumpversion.cfg @@ -1,5 +1,5 @@ [bumpversion] -current_version = 0.1.36 +current_version = 0.1.37 commit = True tag = True parse = (?P\d+)\.(?P\d+)\.(?P\d+) diff --git a/easy/__init__.py b/easy/__init__.py index 13dded8..81c0afe 100644 --- a/easy/__init__.py +++ b/easy/__init__.py @@ -1,6 +1,6 @@ """Django Easy API - Easy and Fast Django REST framework based on Django-ninja-extra""" -__version__ = "0.1.36" +__version__ = "0.1.37" from easy.main import EasyAPI From 99e702c4e0973e95af97148428a8e501fc9dd7cb Mon Sep 17 00:00:00 2001 From: freemindcore Date: Tue, 18 Oct 2022 21:04:59 +0800 Subject: [PATCH 32/58] fix: improve get_objs exception handling --- easy/controller/meta.py | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/easy/controller/meta.py b/easy/controller/meta.py index 927e855..b9e8dc6 100644 --- a/easy/controller/meta.py +++ b/easy/controller/meta.py @@ -82,13 +82,18 @@ async def del_obj(self, request: HttpRequest, id: int) -> Any: # type: ignore return BaseApiResponse("Not Found.", code=404) @paginate - async def get_objs(self, request: HttpRequest, filters: str = None) -> Any: # type: ignore + async def get_objs(self, request: HttpRequest, filters: Optional[str] = None) -> Any: # type: ignore """ GET /?filters={filters_dict} Retrieve multiple Object (optional: django filters) """ if filters: - return await self.service.get_objs(**json.loads(filters)) + try: + _filters = json.loads(filters) + except Exception as exc: # pragma: no cover + logger.warning(str(exc), exc_info=True) + return [] + return await self.service.get_objs(**_filters) return await self.service.get_objs() if model_opts.generate_crud and model_opts.model: From 9ff9c1ab727795867405f44f34cec385ea96e9b6 Mon Sep 17 00:00:00 2001 From: freemindcore Date: Tue, 18 Oct 2022 23:18:09 +0800 Subject: [PATCH 33/58] fix: improve get_objs exception handling --- easy/controller/meta.py | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/easy/controller/meta.py b/easy/controller/meta.py index b9e8dc6..eb93112 100644 --- a/easy/controller/meta.py +++ b/easy/controller/meta.py @@ -9,6 +9,7 @@ from django.http import HttpRequest from ninja import ModelSchema from ninja_extra import ControllerBase, http_delete, http_get, http_patch, http_put +from ninja_extra.exceptions import ValidationError from ninja_extra.pagination import paginate from easy.controller.meta_conf import MODEL_FIELDS_ATTR_DEFAULT, ModelOptions @@ -91,8 +92,10 @@ async def get_objs(self, request: HttpRequest, filters: Optional[str] = None) -> try: _filters = json.loads(filters) except Exception as exc: # pragma: no cover - logger.warning(str(exc), exc_info=True) - return [] + raise ValidationError( + detail=f"Bad filter, please check carefully. {exc}", + code=402, + ) return await self.service.get_objs(**_filters) return await self.service.get_objs() From 0ca13921ac9a6063e2fe4f6bdfe3a79cce74065e Mon Sep 17 00:00:00 2001 From: freemindcore Date: Tue, 18 Oct 2022 23:19:25 +0800 Subject: [PATCH 34/58] =?UTF-8?q?Bump=20version:=200.1.37=20=E2=86=92=200.?= =?UTF-8?q?1.38?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .bumpversion.cfg | 2 +- easy/__init__.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/.bumpversion.cfg b/.bumpversion.cfg index fefba98..7e3e2fd 100644 --- a/.bumpversion.cfg +++ b/.bumpversion.cfg @@ -1,5 +1,5 @@ [bumpversion] -current_version = 0.1.37 +current_version = 0.1.38 commit = True tag = True parse = (?P\d+)\.(?P\d+)\.(?P\d+) diff --git a/easy/__init__.py b/easy/__init__.py index 81c0afe..a55ad28 100644 --- a/easy/__init__.py +++ b/easy/__init__.py @@ -1,6 +1,6 @@ """Django Easy API - Easy and Fast Django REST framework based on Django-ninja-extra""" -__version__ = "0.1.37" +__version__ = "0.1.38" from easy.main import EasyAPI From 8fe4fa512aa19e9c5b5bb7ae168ca2cb65bfdf4c Mon Sep 17 00:00:00 2001 From: freemindcore Date: Wed, 19 Oct 2022 00:03:56 +0800 Subject: [PATCH 35/58] doc: Update README.md --- README.md | 14 +++++++++----- 1 file changed, 9 insertions(+), 5 deletions(-) diff --git a/README.md b/README.md index 9ae23ec..d4e338e 100644 --- a/README.md +++ b/README.md @@ -10,9 +10,13 @@ ### Easy and Fast Django REST framework based on Django-Ninja-Extra -- CRUD Async API Generation: Automatic and configurable, inspired by [NextJs-Crud](https://github.com/nestjsx/crud). +- CRUD API Generation: Automatic and configurable, inspired by [NextJs-Crud](https://github.com/nestjsx/crud). + - Zero coding needed to get all your django app's async CRUD API up and running, with Django RBAC security protection + - Prefetch and retrieve all m2m fields if needed + - Recursively retrieve all FK/OneToOne fields if needed + - Excluding fields you do not want - Domain/Service/Controller Base Structure: for better code organization. -- Base Permission/Response/Exception Classes: and many handy features to help your API coding easier +- Base Permission/Response/Exception Classes: and some handy features to help your API coding easier. - Pure class based [Django-Ninja](https://github.com/vitalik/django-ninja) APIs: thanks to [Django-Ninja-Extra](https://github.com/eadwinCode/django-ninja-extra) ``` @@ -34,7 +38,7 @@ Plus Extra: - Python >= 3.6 - Django >= 3.1 - pydantic >= 1.6 -- Django-Ninja-extra >= 0.15.0 +- Django-Ninja-Extra >= 0.15.0 ### Install `pip install django-api-framework` @@ -78,7 +82,7 @@ Now go to http://127.0.0.1:8000/api_admin/v1/docs You will see the automatic interactive API documentation (provided by Swagger UI). ![Auto generated APIs List](https://github.com/freemindcore/django-api-framework/blob/fae8209a8d08c55daf75ac3a4619fe62b8ef3af6/docs/images/admin_apis_list.png) -#### Auto generation configuration +#### Configuration If `AUTO_ADMIN_ENABLED_ALL_APPS` is set to True (default), all app models CRUD apis will be generated. Apps in the `AUTO_ADMIN_EXCLUDE_APPS` list, will be always excluded. @@ -135,7 +139,7 @@ A boilerplate Django project for quickly getting started, and get production rea https://github.com/freemindcore/django-easy-api ![Auto generated APIs - Users](https://github.com/freemindcore/django-api-framework/blob/9aa26e92b6fd79f4d9db422ec450fe62d4cd97b9/docs/images/user_admin_api.png) -![Auto generated APIs - Schema](https://github.com/freemindcore/django-api-framework/blob/9aa26e92b6fd79f4d9db422ec450fe62d4cd97b9/docs/images/auto_api_demo_2.png) + ### Thanks to your help **_If you find this project useful, please give your stars to support this open-source project. :) Thank you !_** From 96135dcd7697885bd88509e483873dd656a5e441 Mon Sep 17 00:00:00 2001 From: freemindcore Date: Thu, 20 Oct 2022 21:00:18 +0800 Subject: [PATCH 36/58] lint --- easy/controller/meta.py | 2 +- easy/domain/__init__.py | 6 +- easy/domain/meta.py | 37 +++++++++ easy/domain/orm.py | 163 ++++++++++++++++++++++++++++--------- easy/domain/serializers.py | 133 ------------------------------ easy/main.py | 2 +- 6 files changed, 170 insertions(+), 173 deletions(-) create mode 100644 easy/domain/meta.py delete mode 100644 easy/domain/serializers.py diff --git a/easy/controller/meta.py b/easy/controller/meta.py index eb93112..01b47f7 100644 --- a/easy/controller/meta.py +++ b/easy/controller/meta.py @@ -13,7 +13,7 @@ from ninja_extra.pagination import paginate from easy.controller.meta_conf import MODEL_FIELDS_ATTR_DEFAULT, ModelOptions -from easy.domain.orm import CrudModel +from easy.domain.meta import CrudModel from easy.response import BaseApiResponse from easy.services import BaseService from easy.utils import copy_func diff --git a/easy/domain/__init__.py b/easy/domain/__init__.py index 93c2868..9d4b1ad 100644 --- a/easy/domain/__init__.py +++ b/easy/domain/__init__.py @@ -1,3 +1,7 @@ from .base import BaseDomain +from .meta import CrudModel -__all__ = ["BaseDomain"] +__all__ = [ + "BaseDomain", + "CrudModel", +] diff --git a/easy/domain/meta.py b/easy/domain/meta.py new file mode 100644 index 0000000..43eb5ac --- /dev/null +++ b/easy/domain/meta.py @@ -0,0 +1,37 @@ +from abc import abstractmethod +from typing import Any, Dict, Optional + + +class CrudModel(object): + Meta: Dict = {} + + def __init__(self, model: Any): + self.model = model + + @abstractmethod + def crud_add_obj(self, **payload: Dict) -> Any: + raise NotImplementedError + + @abstractmethod + def crud_del_obj(self, pk: int) -> bool: + raise NotImplementedError + + @abstractmethod + def crud_update_obj(self, pk: int, payload: Dict) -> bool: + raise NotImplementedError + + @abstractmethod + def crud_get_obj(self, pk: int) -> Any: + raise NotImplementedError + + @abstractmethod + def crud_get_objs_all(self, maximum: Optional[int] = None, **filters: Any) -> Any: + raise NotImplementedError + + @abstractmethod + def crud_filter(self, **kwargs: Any) -> Any: + raise NotImplementedError + + @abstractmethod + def crud_filter_exclude(self, **kwargs: Any) -> Any: + raise NotImplementedError diff --git a/easy/domain/orm.py b/easy/domain/orm.py index 8107187..aa2393d 100644 --- a/easy/domain/orm.py +++ b/easy/domain/orm.py @@ -1,52 +1,16 @@ import logging -from abc import abstractmethod from typing import Any, Dict, List, Optional, Tuple, Type from django.db import models, transaction -from django.db.models.query import QuerySet from ninja_extra.shortcuts import get_object_or_none from easy.controller.meta_conf import ModelMetaConfig +from easy.domain.meta import CrudModel from easy.exception import BaseAPIException logger = logging.getLogger(__name__) -class CrudModel(object): - Meta: Dict = {} - - def __init__(self, model: Any): - self.model = model - - @abstractmethod - def crud_add_obj(self, **payload: Dict) -> Any: - raise NotImplementedError - - @abstractmethod - def crud_del_obj(self, pk: int) -> bool: - raise NotImplementedError - - @abstractmethod - def crud_update_obj(self, pk: int, payload: Dict) -> bool: - raise NotImplementedError - - @abstractmethod - def crud_get_obj(self, pk: int) -> Any: - raise NotImplementedError - - @abstractmethod - def crud_get_objs_all(self, maximum: Optional[int] = None, **filters: Any) -> Any: - raise NotImplementedError - - @abstractmethod - def crud_filter(self, **kwargs: Any) -> QuerySet: - raise NotImplementedError - - @abstractmethod - def crud_filter_exclude(self, **kwargs: Any) -> QuerySet: - raise NotImplementedError - - class DjangoOrmModel(CrudModel): def __init__(self, model: Type[models.Model]): self.model = model @@ -164,3 +128,128 @@ def crud_filter(self, **kwargs: Any) -> Any: def crud_filter_exclude(self, **kwargs: Any) -> Any: return self.model.objects.all().exclude(**kwargs) + + +class DjangoSerializer(ModelMetaConfig): + @staticmethod + def is_model_instance(data: Any) -> bool: + return isinstance(data, models.Model) + + @staticmethod + def is_queryset(data: Any) -> bool: + return isinstance(data, models.query.QuerySet) + + @staticmethod + def is_one_relationship(data: Any) -> bool: + return isinstance(data, models.ForeignKey) or isinstance( + data, models.OneToOneRel + ) + + @staticmethod + def is_many_relationship(data: Any) -> bool: + return ( + isinstance(data, models.ManyToManyRel) + or isinstance(data, models.ManyToManyField) + or isinstance(data, models.ManyToOneRel) + ) + + @staticmethod + def is_paginated(data: Any) -> bool: + return isinstance(data, dict) and isinstance( + data.get("items", None), models.query.QuerySet + ) + + def serialize_model_instance( + self, obj: models.Model, referrers: Any = tuple() + ) -> Dict[Any, Any]: + """Serializes Django model instance to dictionary""" + out = {} + for field in obj._meta.get_fields(): + if self.show_field(obj, field.name): + if self.is_one_relationship(field): + out.update( + self.serialize_foreign_key(obj, field, referrers + (obj,)) + ) + + elif self.is_many_relationship(field): + out.update( + self.serialize_many_relationship(obj, referrers + (obj,)) + ) + + else: + out.update(self.serialize_value_field(obj, field)) + return out + + def serialize_queryset( + self, data: models.query.QuerySet, referrers: Tuple[Any, ...] = tuple() + ) -> List[Dict[Any, Any]]: + """Serializes Django Queryset to dictionary""" + return [self.serialize_model_instance(obj, referrers) for obj in data] + + def serialize_foreign_key( + self, obj: models.Model, field: Any, referrers: Any = tuple() + ) -> Dict[Any, Any]: + """Serializes foreign key field of Django model instance""" + try: + if not hasattr(obj, field.name): + return {field.name: None} # pragma: no cover + related_instance = getattr(obj, field.name) + if related_instance is None: + return {field.name: None} + if related_instance in referrers: + return {} # pragma: no cover + field_value = getattr(related_instance, "pk") + except Exception as exc: # pragma: no cover + logger.error(f"serialize_foreign_key error - {obj}", exc_info=exc) + return {field.name: None} + + if self.get_model_recursive(obj): + return { + field.name: self.serialize_model_instance(related_instance, referrers) + } + return {field.name: field_value} + + def serialize_many_relationship( + self, obj: models.Model, referrers: Any = tuple() + ) -> Dict[Any, Any]: + """ + Serializes many relationship (ManyToMany, ManyToOne) of Django model instance + """ + if not hasattr(obj, "_prefetched_objects_cache"): + return {} + out = {} + try: + for k, v in obj._prefetched_objects_cache.items(): # type: ignore + field_name = k if hasattr(obj, k) else k + "_set" + if v: + if self.get_model_join(obj): + out[field_name] = self.serialize_queryset(v, referrers + (obj,)) + else: + out[field_name] = [o.pk for o in v] + else: + out[field_name] = [] + except Exception as exc: # pragma: no cover + logger.error(f"serialize_many_relationship error - {obj}", exc_info=exc) + return out + + def serialize_value_field(self, obj: models.Model, field: Any) -> Dict[Any, Any]: + """ + Serializes regular 'jsonable' field (Char, Int, etc.) of Django model instance + """ + return {field.name: getattr(obj, field.name)} + + def serialize_data(self, data: Any) -> Any: + out = data + # Queryset + if self.is_queryset(data): + out = self.serialize_queryset(data) + # Model + elif self.is_model_instance(data): + out = self.serialize_model_instance(data) + # Add limit_off pagination support + elif self.is_paginated(data): + out = self.serialize_queryset(data.get("items")) + return out + + +django_serializer = DjangoSerializer() diff --git a/easy/domain/serializers.py b/easy/domain/serializers.py deleted file mode 100644 index 86a8748..0000000 --- a/easy/domain/serializers.py +++ /dev/null @@ -1,133 +0,0 @@ -import logging -from typing import Any, Dict, List, Tuple - -from django.db import models - -from easy.controller.meta_conf import ModelMetaConfig - -logger = logging.getLogger(__name__) - - -class DjangoSerializer(ModelMetaConfig): - @staticmethod - def is_model_instance(data: Any) -> bool: - return isinstance(data, models.Model) - - @staticmethod - def is_queryset(data: Any) -> bool: - return isinstance(data, models.query.QuerySet) - - @staticmethod - def is_one_relationship(data: Any) -> bool: - return isinstance(data, models.ForeignKey) or isinstance( - data, models.OneToOneRel - ) - - @staticmethod - def is_many_relationship(data: Any) -> bool: - return ( - isinstance(data, models.ManyToManyRel) - or isinstance(data, models.ManyToManyField) - or isinstance(data, models.ManyToOneRel) - ) - - @staticmethod - def is_paginated(data: Any) -> bool: - return isinstance(data, dict) and isinstance( - data.get("items", None), models.query.QuerySet - ) - - def serialize_model_instance( - self, obj: models.Model, referrers: Any = tuple() - ) -> Dict[Any, Any]: - """Serializes Django model instance to dictionary""" - out = {} - for field in obj._meta.get_fields(): - if self.show_field(obj, field.name): - if self.is_one_relationship(field): - out.update( - self.serialize_foreign_key(obj, field, referrers + (obj,)) - ) - - elif self.is_many_relationship(field): - out.update( - self.serialize_many_relationship(obj, referrers + (obj,)) - ) - - else: - out.update(self.serialize_value_field(obj, field)) - return out - - def serialize_queryset( - self, data: models.query.QuerySet, referrers: Tuple[Any, ...] = tuple() - ) -> List[Dict[Any, Any]]: - """Serializes Django Queryset to dictionary""" - return [self.serialize_model_instance(obj, referrers) for obj in data] - - def serialize_foreign_key( - self, obj: models.Model, field: Any, referrers: Any = tuple() - ) -> Dict[Any, Any]: - """Serializes foreign key field of Django model instance""" - try: - if not hasattr(obj, field.name): - return {field.name: None} # pragma: no cover - related_instance = getattr(obj, field.name) - if related_instance is None: - return {field.name: None} - if related_instance in referrers: - return {} # pragma: no cover - field_value = getattr(related_instance, "pk") - except Exception as exc: # pragma: no cover - logger.error(f"serialize_foreign_key error - {obj}", exc_info=exc) - return {field.name: None} - - if self.get_model_recursive(obj): - return { - field.name: self.serialize_model_instance(related_instance, referrers) - } - return {field.name: field_value} - - def serialize_many_relationship( - self, obj: models.Model, referrers: Any = tuple() - ) -> Dict[Any, Any]: - """ - Serializes many relationship (ManyToMany, ManyToOne) of Django model instance - """ - if not hasattr(obj, "_prefetched_objects_cache"): - return {} - out = {} - try: - for k, v in obj._prefetched_objects_cache.items(): # type: ignore - field_name = k if hasattr(obj, k) else k + "_set" - if v: - if self.get_model_join(obj): - out[field_name] = self.serialize_queryset(v, referrers + (obj,)) - else: - out[field_name] = [o.pk for o in v] - else: - out[field_name] = [] - except Exception as exc: # pragma: no cover - logger.error(f"serialize_many_relationship error - {obj}", exc_info=exc) - return out - - def serialize_value_field(self, obj: models.Model, field: Any) -> Dict[Any, Any]: - """ - Serializes regular 'jsonable' field (Char, Int, etc.) of Django model instance - """ - return {field.name: getattr(obj, field.name)} - - def serialize_data(self, data: Any) -> Any: - out = data - # Queryset - if self.is_queryset(data): - out = self.serialize_queryset(data) - # Model - elif self.is_model_instance(data): - out = self.serialize_model_instance(data) - # Add limit_off pagination support - elif self.is_paginated(data): - out = self.serialize_queryset(data.get("items")) - return out - - -django_serializer = DjangoSerializer() diff --git a/easy/main.py b/easy/main.py index ab0aa87..3c4e261 100644 --- a/easy/main.py +++ b/easy/main.py @@ -5,6 +5,7 @@ from django.conf import settings from django.http import HttpRequest, HttpResponse from django.utils.module_loading import module_has_submodule +from domain.orm import django_serializer from ninja.constants import NOT_SET, NOT_SET_TYPE from ninja.parser import Parser from ninja.renderers import BaseRenderer @@ -12,7 +13,6 @@ from ninja_extra import NinjaExtraAPI from easy.controller.auto_api import create_admin_controller -from easy.domain.serializers import django_serializer from easy.renderer.json import EasyJSONRenderer from easy.response import BaseApiResponse From 0eaf9b194878aad783d188401179493f2e34c486 Mon Sep 17 00:00:00 2001 From: freemindcore Date: Thu, 20 Oct 2022 21:05:22 +0800 Subject: [PATCH 37/58] lint: setting name changes --- README.md | 6 +++--- easy/conf/settings.py | 8 +++----- easy/main.py | 16 ++++++++-------- tests/config/settings.py | 6 +++--- tests/test_auto_api_creation.py | 8 ++++---- tests/test_settings.py | 12 ++++++------ 6 files changed, 27 insertions(+), 29 deletions(-) diff --git a/README.md b/README.md index d4e338e..9c7078b 100644 --- a/README.md +++ b/README.md @@ -83,10 +83,10 @@ You will see the automatic interactive API documentation (provided by Swagger UI ![Auto generated APIs List](https://github.com/freemindcore/django-api-framework/blob/fae8209a8d08c55daf75ac3a4619fe62b8ef3af6/docs/images/admin_apis_list.png) #### Configuration -If `AUTO_ADMIN_ENABLED_ALL_APPS` is set to True (default), all app models CRUD apis will be generated. -Apps in the `AUTO_ADMIN_EXCLUDE_APPS` list, will be always excluded. +If `CRUD_API_ENABLED_ALL_APPS` is set to True (default), all app models CRUD apis will be generated. +Apps in the `CRUD_API_EXCLUDE_APPS` list, will be always excluded. -If `AUTO_ADMIN_ENABLED_ALL_APPS` is set to False, only apps in the `AUTO_ADMIN_INCLUDE_APPS` list will have CRUD apis generated. +If `CRUD_API_ENABLED_ALL_APPS` is set to False, only apps in the `CRUD_API_INCLUDE_APPS` list will have CRUD apis generated. Also, configuration is possible for each model, via ApiMeta class: - `generate_crud`: whether to create crud api, default to True diff --git a/easy/conf/settings.py b/easy/conf/settings.py index 4024caf..6ac6c22 100644 --- a/easy/conf/settings.py +++ b/easy/conf/settings.py @@ -5,13 +5,11 @@ # AUTO ADMIN API settings # If not all -AUTO_ADMIN_ENABLED_ALL_APPS = getattr( - django_settings, "AUTO_ADMIN_ENABLED_ALL_APPS", True -) +CRUD_API_ENABLED_ALL_APPS = getattr(django_settings, "CRUD_API_ENABLED_ALL_APPS", True) # Only generate for included apps -AUTO_ADMIN_INCLUDE_APPS = getattr(django_settings, "AUTO_ADMIN_INCLUDE_APPS", []) +CRUD_API_INCLUDE_APPS = getattr(django_settings, "CRUD_API_INCLUDE_APPS", []) # Exclude apps always got excluded -AUTO_ADMIN_EXCLUDE_APPS = getattr(django_settings, "AUTO_ADMIN_EXCLUDE_APPS", []) +CRUD_API_EXCLUDE_APPS = getattr(django_settings, "CRUD_API_EXCLUDE_APPS", []) def reload_settings(*args: Any, **kwargs: Any) -> None: # pragma: no cover diff --git a/easy/main.py b/easy/main.py index 3c4e261..dd340f9 100644 --- a/easy/main.py +++ b/easy/main.py @@ -5,7 +5,6 @@ from django.conf import settings from django.http import HttpRequest, HttpResponse from django.utils.module_loading import module_has_submodule -from domain.orm import django_serializer from ninja.constants import NOT_SET, NOT_SET_TYPE from ninja.parser import Parser from ninja.renderers import BaseRenderer @@ -13,6 +12,7 @@ from ninja_extra import NinjaExtraAPI from easy.controller.auto_api import create_admin_controller +from easy.domain.orm import django_serializer from easy.renderer.json import EasyJSONRenderer from easy.response import BaseApiResponse @@ -29,9 +29,9 @@ class EasyAPI(NinjaExtraAPI): If True, will be encapsulated in BaseAPIResponse -renderer, default to EasyJSONRenderer -Auto generate AdminAPIs, it will read the following settings: - AUTO_ADMIN_ENABLED_ALL_APPS - AUTO_ADMIN_EXCLUDE_APPS - AUTO_ADMIN_INCLUDE_APPS + CRUD_API_ENABLED_ALL_APPS + CRUD_API_EXCLUDE_APPS + CRUD_API_INCLUDE_APPS """ def __init__( @@ -75,17 +75,17 @@ def __init__( def auto_create_admin_controllers(self, version: str = None) -> None: for app_module in self.get_installed_apps(): # If not all - if not settings.AUTO_ADMIN_ENABLED_ALL_APPS: # type:ignore + if not settings.CRUD_API_ENABLED_ALL_APPS: # type:ignore # Only generate for this included apps - if settings.AUTO_ADMIN_INCLUDE_APPS is not None: # type:ignore + if settings.CRUD_API_INCLUDE_APPS is not None: # type:ignore if ( app_module.name - not in settings.AUTO_ADMIN_INCLUDE_APPS # type:ignore + not in settings.CRUD_API_INCLUDE_APPS # type:ignore ): continue # Exclude list - if app_module.name in settings.AUTO_ADMIN_EXCLUDE_APPS: # type:ignore + if app_module.name in settings.CRUD_API_EXCLUDE_APPS: # type:ignore continue try: diff --git a/tests/config/settings.py b/tests/config/settings.py index 88d9932..324b33f 100644 --- a/tests/config/settings.py +++ b/tests/config/settings.py @@ -3,9 +3,9 @@ """ # Admin API auto-generation settings -AUTO_ADMIN_ENABLED_ALL_APPS = True -AUTO_ADMIN_INCLUDE_APPS = [] -AUTO_ADMIN_EXCLUDE_APPS = [] +CRUD_API_ENABLED_ALL_APPS = True +CRUD_API_INCLUDE_APPS = [] +CRUD_API_EXCLUDE_APPS = [] # Django Settings INSTALLED_APPS = ( diff --git a/tests/test_auto_api_creation.py b/tests/test_auto_api_creation.py index a7861e2..1f17471 100644 --- a/tests/test_auto_api_creation.py +++ b/tests/test_auto_api_creation.py @@ -66,14 +66,14 @@ async def test_auto_apis(transactional_db, user, easy_api_client): async def test_auto_generation_settings(settings): - settings.AUTO_ADMIN_EXCLUDE_APPS = ["tests.easy_app"] + settings.CRUD_API_EXCLUDE_APPS = ["tests.easy_app"] api_admin_v2 = EasyAPI() api_admin_v2.auto_create_admin_controllers() assert len(api_admin_v2._routers) == 1 - settings.AUTO_ADMIN_ENABLED_ALL_APPS = False - settings.AUTO_ADMIN_EXCLUDE_APPS = [] - settings.AUTO_ADMIN_INCLUDE_APPS = ["tests.none_existing_app"] + settings.CRUD_API_ENABLED_ALL_APPS = False + settings.CRUD_API_EXCLUDE_APPS = [] + settings.CRUD_API_INCLUDE_APPS = ["tests.none_existing_app"] api_admin_v3 = EasyAPI() api_admin_v3.auto_create_admin_controllers() assert len(api_admin_v3._routers) == 1 diff --git a/tests/test_settings.py b/tests/test_settings.py index 0c50e2e..f75aeff 100644 --- a/tests/test_settings.py +++ b/tests/test_settings.py @@ -1,11 +1,11 @@ from easy.conf.settings import ( - AUTO_ADMIN_ENABLED_ALL_APPS, - AUTO_ADMIN_EXCLUDE_APPS, - AUTO_ADMIN_INCLUDE_APPS, + CRUD_API_ENABLED_ALL_APPS, + CRUD_API_EXCLUDE_APPS, + CRUD_API_INCLUDE_APPS, ) def test_change_django_settings(settings): - assert settings.AUTO_ADMIN_ENABLED_ALL_APPS == AUTO_ADMIN_ENABLED_ALL_APPS - assert settings.AUTO_ADMIN_INCLUDE_APPS == AUTO_ADMIN_INCLUDE_APPS - assert settings.AUTO_ADMIN_EXCLUDE_APPS == AUTO_ADMIN_EXCLUDE_APPS + assert settings.CRUD_API_ENABLED_ALL_APPS == CRUD_API_ENABLED_ALL_APPS + assert settings.CRUD_API_INCLUDE_APPS == CRUD_API_INCLUDE_APPS + assert settings.CRUD_API_EXCLUDE_APPS == CRUD_API_EXCLUDE_APPS From b481463b62f23b280e8da0530e427b583c7c28be Mon Sep 17 00:00:00 2001 From: freemindcore Date: Thu, 20 Oct 2022 21:23:12 +0800 Subject: [PATCH 38/58] Update README.md --- README.md | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/README.md b/README.md index 9c7078b..419c652 100644 --- a/README.md +++ b/README.md @@ -11,10 +11,10 @@ ### Easy and Fast Django REST framework based on Django-Ninja-Extra - CRUD API Generation: Automatic and configurable, inspired by [NextJs-Crud](https://github.com/nestjsx/crud). - - Zero coding needed to get all your django app's async CRUD API up and running, with Django RBAC security protection - - Prefetch and retrieve all m2m fields if needed - - Recursively retrieve all FK/OneToOne fields if needed - - Excluding fields you do not want + - Async CRUD API with Django RBAC security protection + - Prefetch and retrieve all m2m fields if configured + - Recursively retrieve all FK/OneToOne fields if configured + - Excluding fields you do not want, or define a list sensitive fields of your choice - Domain/Service/Controller Base Structure: for better code organization. - Base Permission/Response/Exception Classes: and some handy features to help your API coding easier. - Pure class based [Django-Ninja](https://github.com/vitalik/django-ninja) APIs: thanks to [Django-Ninja-Extra](https://github.com/eadwinCode/django-ninja-extra) From 10fb507e9a5b34379b8ac4c96c151186e57a2f55 Mon Sep 17 00:00:00 2001 From: freemindcore Date: Thu, 20 Oct 2022 21:42:11 +0800 Subject: [PATCH 39/58] refactor: Meta -> APIMeta --- README.md | 8 ++++---- easy/controller/auto_api.py | 8 ++++---- easy/controller/base.py | 8 ++++---- easy/controller/meta.py | 8 ++++---- easy/domain/meta.py | 2 +- tests/easy_app/controllers.py | 20 ++++++++++---------- tests/easy_app/models.py | 2 +- 7 files changed, 28 insertions(+), 28 deletions(-) diff --git a/README.md b/README.md index 419c652..d26e6db 100644 --- a/README.md +++ b/README.md @@ -88,7 +88,7 @@ Apps in the `CRUD_API_EXCLUDE_APPS` list, will be always excluded. If `CRUD_API_ENABLED_ALL_APPS` is set to False, only apps in the `CRUD_API_INCLUDE_APPS` list will have CRUD apis generated. -Also, configuration is possible for each model, via ApiMeta class: +Also, configuration is possible for each model, via APIMeta class: - `generate_crud`: whether to create crud api, default to True - `model_exclude`: fields to be excluded in Schema - `model_fields`: fields to be included in Schema, default to `"__all__"` @@ -102,7 +102,7 @@ class Category(TestBaseModel): title = models.CharField(max_length=100) status = models.PositiveSmallIntegerField(default=1, null=True) - class ApiMeta: + class APIMeta: generate_crud = True model_fields = ["field_1", "field_2",] # if not configured default to "__all__" model_join = True @@ -112,7 +112,7 @@ class Category(TestBaseModel): ### Adding CRUD APIs to a specific API Controller By inheriting `CrudAPIController` class, CRUD APIs can be added to any API controller. -Configuration is available via `Meta` inner class in your Controller, same as the above `ApiMeta` inner class defined in your Django models. +Configuration is available via `APIMeta` inner class in your Controller, same as the above `APIMeta` inner class defined in your Django models. Example: @@ -122,7 +122,7 @@ class EventAPIController(CrudAPIController): def __init__(self, service: EventService): super().__init__(service) - class Meta: + class APIMeta: model = Event # django model generate_crud = True # whether to create crud api, default to True model_fields = ["field_1", "field_2",] # if not configured default to "__all__" diff --git a/easy/controller/auto_api.py b/easy/controller/auto_api.py index d04ad80..dd1d67b 100644 --- a/easy/controller/auto_api.py +++ b/easy/controller/auto_api.py @@ -30,11 +30,11 @@ def create_api_controller( model_name = model.__name__ # type:ignore model_opts: ModelOptions = ModelOptions.get_model_options( - getattr(model, "ApiMeta", None) + getattr(model, "APIMeta", None) ) - Meta = type( - "Meta", + APIMeta = type( + "APIMeta", (object,), { "model": model, @@ -54,7 +54,7 @@ def create_api_controller( class_name, (CrudAPIController,), { - "Meta": Meta, + "APIMeta": APIMeta, }, ) diff --git a/easy/controller/base.py b/easy/controller/base.py index d2a24b5..d5809f4 100644 --- a/easy/controller/base.py +++ b/easy/controller/base.py @@ -1,13 +1,13 @@ import logging -from easy.controller.meta import CrudApiMetaclass +from easy.controller.meta import CrudAPIMetaclass logger = logging.getLogger(__name__) -class CrudAPIController(metaclass=CrudApiMetaclass): +class CrudAPIController(metaclass=CrudAPIMetaclass): """ - Base APIController for auto creating CRUD APIs, configurable via Meta class + Base APIController for auto creating CRUD APIs, configurable via APIMeta class APIs auto generated: Creat PUT /{id} - Create a single Object @@ -32,7 +32,7 @@ class CrudAPIController(metaclass=CrudApiMetaclass): sensitive_fields: fields to be ignored Example: - class Meta + class APIMeta model = Event generate_crud = False model_exclude = ["field1", "field2"] diff --git a/easy/controller/meta.py b/easy/controller/meta.py index 01b47f7..a7f848c 100644 --- a/easy/controller/meta.py +++ b/easy/controller/meta.py @@ -27,7 +27,7 @@ def __init__(self, service=None): # type: ignore # Critical to set __Meta self.service = service - _model_opts: ModelOptions = ModelOptions.get_model_options(self.Meta) + _model_opts: ModelOptions = ModelOptions.get_model_options(self.APIMeta) if self.model and _model_opts: ModelOptions.set_model_meta(self.model, _model_opts) @@ -36,10 +36,10 @@ def __init__(self, service=None): # type: ignore super().__init__(model=self.model) -class CrudApiMetaclass(ABCMeta): +class CrudAPIMetaclass(ABCMeta): def __new__(mcs, name: str, bases: Tuple[Type[Any], ...], attrs: dict) -> Any: - # Get configs from Meta - attrs_meta = attrs.get("Meta", None) + # Get configs from APIMeta + attrs_meta = attrs.get("APIMeta", None) model_opts: ModelOptions = ModelOptions.get_model_options(attrs_meta) # Get all attrs from parents excluding private ones diff --git a/easy/domain/meta.py b/easy/domain/meta.py index 43eb5ac..246bf8d 100644 --- a/easy/domain/meta.py +++ b/easy/domain/meta.py @@ -3,7 +3,7 @@ class CrudModel(object): - Meta: Dict = {} + APIMeta: Dict = {} def __init__(self, model: Any): self.model = model diff --git a/tests/easy_app/controllers.py b/tests/easy_app/controllers.py index 612a464..76e77bc 100644 --- a/tests/easy_app/controllers.py +++ b/tests/easy_app/controllers.py @@ -28,7 +28,7 @@ class AutoGenCrudAPIController(CrudAPIController): def __init__(self, service: EventService): super().__init__(service) - class Meta: + class APIMeta: model = Event model_join = True @@ -42,7 +42,7 @@ class RecursiveAPIController(CrudAPIController): def __init__(self, service: EventService): super().__init__(service) - class Meta: + class APIMeta: model = Event model_fields = "__all__" model_join = True @@ -58,7 +58,7 @@ class InheritedRecursiveAPIController(AutoGenCrudAPIController): def __init__(self, service: EventService): super().__init__(service) - class Meta: + class APIMeta: model = Event model_fields = "__all__" model_join = True @@ -74,7 +74,7 @@ class AutoGenCrudNoJoinAPIController(CrudAPIController): def __init__(self, service: EventService): super().__init__(service) - class Meta: + class APIMeta: model = Event model_fields = "__all__" model_join = False @@ -88,7 +88,7 @@ class AutoGenCrudSomeFieldsAPIController(CrudAPIController): For unit testings of the no-m2m-fields model """ - class Meta: + class APIMeta: model = Client model_fields = [ "key", @@ -105,7 +105,7 @@ class EasyCrudAPIController(CrudAPIController): def __init__(self, service: EventService): super().__init__(service) - class Meta: + class APIMeta: model = Event model_exclude = [ "category", @@ -149,7 +149,7 @@ def __init__(self, service: EventService): super().__init__(service) self.service = service - class Meta: + class APIMeta: model = Event @http_get("/must_be_authenticated/", permissions=[IsAuthenticated]) @@ -189,7 +189,7 @@ def __init__(self, service: EventService): super().__init__(service) self.service = service - class Meta: + class APIMeta: model = Event @@ -203,7 +203,7 @@ def __init__(self, service: EventService): super().__init__(service) self.service = service - class Meta: + class APIMeta: model = Event generate_crud = False @@ -218,7 +218,7 @@ def __init__(self, service: EventService): super().__init__(service) self.service = service - class Meta: + class APIMeta: model = Event generate_crud = False model_exclude = [ diff --git a/tests/easy_app/models.py b/tests/easy_app/models.py index 8a66df6..2c2c96c 100644 --- a/tests/easy_app/models.py +++ b/tests/easy_app/models.py @@ -10,7 +10,7 @@ class Category(TestBaseModel): title = models.CharField(max_length=100) status = models.PositiveSmallIntegerField(default=1, null=True) - class ApiMeta: + class APIMeta: generate_crud = False From ddc53dd19eab79400c4102c2eb4417aff8388f5f Mon Sep 17 00:00:00 2001 From: freemindcore Date: Thu, 20 Oct 2022 21:53:28 +0800 Subject: [PATCH 40/58] rename: BaseApiResponse -> BaseAPIResponse --- easy/controller/meta.py | 18 +++++++++--------- easy/main.py | 8 ++++---- easy/response.py | 2 +- tests/easy_app/controllers.py | 6 +++--- tests/test_api_base_response.py | 30 +++++++++++++++--------------- tests/test_async_other_apis.py | 2 +- 6 files changed, 33 insertions(+), 33 deletions(-) diff --git a/easy/controller/meta.py b/easy/controller/meta.py index a7f848c..e793eeb 100644 --- a/easy/controller/meta.py +++ b/easy/controller/meta.py @@ -14,7 +14,7 @@ from easy.controller.meta_conf import MODEL_FIELDS_ATTR_DEFAULT, ModelOptions from easy.domain.meta import CrudModel -from easy.response import BaseApiResponse +from easy.response import BaseAPIResponse from easy.services import BaseService from easy.utils import copy_func @@ -66,11 +66,11 @@ async def get_obj(self, request: HttpRequest, id: int) -> Any: # type: ignore qs = await self.service.get_obj(id) except Exception as e: # pragma: no cover logger.error(f"Get Error - {e}", exc_info=True) - return BaseApiResponse(str(e), message="Get Failed", code=500) + return BaseAPIResponse(str(e), message="Get Failed", code=500) if qs: return qs else: - return BaseApiResponse(message="Not Found", code=404) + return BaseAPIResponse(message="Not Found", code=404) async def del_obj(self, request: HttpRequest, id: int) -> Any: # type: ignore """ @@ -78,9 +78,9 @@ async def del_obj(self, request: HttpRequest, id: int) -> Any: # type: ignore Delete a single Object """ if await self.service.del_obj(id): - return BaseApiResponse("Deleted.", code=204) + return BaseAPIResponse("Deleted.", code=204) else: - return BaseApiResponse("Not Found.", code=404) + return BaseAPIResponse("Not Found.", code=404) @paginate async def get_objs(self, request: HttpRequest, filters: Optional[str] = None) -> Any: # type: ignore @@ -142,9 +142,9 @@ async def add_obj( # type: ignore """ obj_id = await self.service.add_obj(**data.dict()) if obj_id: - return BaseApiResponse({"id": obj_id}, code=201, message="Created.") + return BaseAPIResponse({"id": obj_id}, code=201, message="Created.") else: - return BaseApiResponse( + return BaseAPIResponse( code=204, message="Add failed." ) # pragma: no cover @@ -156,9 +156,9 @@ async def patch_obj( # type: ignore Update a single object """ if await self.service.patch_obj(id=id, payload=data.dict()): - return BaseApiResponse(message="Updated.") + return BaseAPIResponse(message="Updated.") else: - return BaseApiResponse(code=400, message="Updated Failed") + return BaseAPIResponse(code=400, message="Updated Failed") DataSchema.__name__ = ( f"{model_opts.model.__name__}__AutoSchema({str(uuid.uuid4())[:4]})" diff --git a/easy/main.py b/easy/main.py index dd340f9..8bf45ca 100644 --- a/easy/main.py +++ b/easy/main.py @@ -14,7 +14,7 @@ from easy.controller.auto_api import create_admin_controller from easy.domain.orm import django_serializer from easy.renderer.json import EasyJSONRenderer -from easy.response import BaseApiResponse +from easy.response import BaseAPIResponse logger = logging.getLogger(__name__) @@ -126,14 +126,14 @@ def create_response( data = django_serializer.serialize_data(data) except Exception as e: # pragma: no cover logger.error(f"Creat Response Error - {e}", exc_info=True) - return BaseApiResponse(str(e), code=500) + return BaseAPIResponse(str(e), code=500) if self.easy_output: if temporal_response: status = temporal_response.status_code assert status - _temp = BaseApiResponse( + _temp = BaseAPIResponse( data, status=status, content_type=self.get_content_type() ) @@ -154,6 +154,6 @@ def create_response( def create_temporal_response(self, request: HttpRequest) -> HttpResponse: if self.easy_output: - return BaseApiResponse("", content_type=self.get_content_type()) + return BaseAPIResponse("", content_type=self.get_content_type()) else: return super().create_temporal_response(request) diff --git a/easy/response.py b/easy/response.py index c1635ba..e6ff2d9 100644 --- a/easy/response.py +++ b/easy/response.py @@ -10,7 +10,7 @@ SUCCESS_MESSAGE = "success" -class BaseApiResponse(JsonResponse): +class BaseAPIResponse(JsonResponse): """ Base for all API responses """ diff --git a/tests/easy_app/controllers.py b/tests/easy_app/controllers.py index 76e77bc..29e6dd2 100644 --- a/tests/easy_app/controllers.py +++ b/tests/easy_app/controllers.py @@ -11,7 +11,7 @@ IsAuthenticated, IsSuperUser, ) -from easy.response import BaseApiResponse +from easy.response import BaseAPIResponse from .models import Client, Event from .schema import EventSchema @@ -113,7 +113,7 @@ class APIMeta: @http_get("/base_response/") async def generate_base_response(self, request): - return BaseApiResponse({"data": "This is a BaseApiResponse."}) + return BaseAPIResponse({"data": "This is a BaseAPIResponse."}) @http_get("/qs_paginated/", auth=None) @paginate @@ -136,7 +136,7 @@ async def list_events(self): await sync_to_async(list)(qs) if qs: return qs - return BaseApiResponse() + return BaseAPIResponse() @api_controller("unittest") diff --git a/tests/test_api_base_response.py b/tests/test_api_base_response.py index e80ef06..f2cb3fa 100644 --- a/tests/test_api_base_response.py +++ b/tests/test_api_base_response.py @@ -2,44 +2,44 @@ import pytest -from easy.response import BaseApiResponse +from easy.response import BaseAPIResponse def test_base_api_result_base(): - assert BaseApiResponse("").json_data["data"] == "" + assert BaseAPIResponse("").json_data["data"] == "" - assert BaseApiResponse("1").json_data["data"] == "1" + assert BaseAPIResponse("1").json_data["data"] == "1" - assert BaseApiResponse("0").json_data["data"] == "0" - assert BaseApiResponse().json_data["data"] == {} - assert BaseApiResponse([]).json_data["data"] == [] - assert BaseApiResponse(True).json_data["data"] is True - assert BaseApiResponse(False).json_data["data"] is False - assert BaseApiResponse([1, 2, 3]).json_data["data"] == [1, 2, 3] + assert BaseAPIResponse("0").json_data["data"] == "0" + assert BaseAPIResponse().json_data["data"] == {} + assert BaseAPIResponse([]).json_data["data"] == [] + assert BaseAPIResponse(True).json_data["data"] is True + assert BaseAPIResponse(False).json_data["data"] is False + assert BaseAPIResponse([1, 2, 3]).json_data["data"] == [1, 2, 3] def test_base_api_result_dict(): - assert BaseApiResponse({"a": 1, "b": 2}).json_data["data"] == { + assert BaseAPIResponse({"a": 1, "b": 2}).json_data["data"] == { "a": 1, "b": 2, } - assert (BaseApiResponse({"code": 2, "im": 14})).json_data["data"]["im"] == 14 - assert (BaseApiResponse({"code": 2, "im": 14})).json_data["data"]["code"] == 2 + assert (BaseAPIResponse({"code": 2, "im": 14})).json_data["data"]["im"] == 14 + assert (BaseAPIResponse({"code": 2, "im": 14})).json_data["data"]["code"] == 2 def test_base_api_result_message(): assert ( - BaseApiResponse(code=-1, message="error test").json_data["message"] + BaseAPIResponse(code=-1, message="error test").json_data["message"] == "error test" ) - assert BaseApiResponse().json_data["message"] + assert BaseAPIResponse().json_data["message"] def test_base_api_edit(): - orig_resp = BaseApiResponse( + orig_resp = BaseAPIResponse( {"item_id": 2, "im": 14}, code=0, ) diff --git a/tests/test_async_other_apis.py b/tests/test_async_other_apis.py index 6ad8eca..5047fdd 100644 --- a/tests/test_async_other_apis.py +++ b/tests/test_async_other_apis.py @@ -50,7 +50,7 @@ async def test_base_response(self, transactional_db, easy_api_client): "/base_response/", ) assert response.status_code == 200 - assert response.json().get("data")["data"] == "This is a BaseApiResponse." + assert response.json().get("data")["data"] == "This is a BaseAPIResponse." async def test_qs_paginated(self, transactional_db, easy_api_client): client = easy_api_client(EasyCrudAPIController) From f6ea3714514b5d7b516298624d5239e0bbffc3b9 Mon Sep 17 00:00:00 2001 From: freemindcore Date: Thu, 20 Oct 2022 22:04:33 +0800 Subject: [PATCH 41/58] =?UTF-8?q?Bump=20version:=200.1.38=20=E2=86=92=200.?= =?UTF-8?q?1.39?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .bumpversion.cfg | 2 +- easy/__init__.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/.bumpversion.cfg b/.bumpversion.cfg index 7e3e2fd..6e93bce 100644 --- a/.bumpversion.cfg +++ b/.bumpversion.cfg @@ -1,5 +1,5 @@ [bumpversion] -current_version = 0.1.38 +current_version = 0.1.39 commit = True tag = True parse = (?P\d+)\.(?P\d+)\.(?P\d+) diff --git a/easy/__init__.py b/easy/__init__.py index a55ad28..480aa34 100644 --- a/easy/__init__.py +++ b/easy/__init__.py @@ -1,6 +1,6 @@ """Django Easy API - Easy and Fast Django REST framework based on Django-ninja-extra""" -__version__ = "0.1.38" +__version__ = "0.1.39" from easy.main import EasyAPI From c457b1679432a266b83112dbe581111497607069 Mon Sep 17 00:00:00 2001 From: freemindcore Date: Fri, 9 Dec 2022 00:18:40 +0800 Subject: [PATCH 42/58] Update README.md --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index d26e6db..fd7dc65 100644 --- a/README.md +++ b/README.md @@ -14,7 +14,7 @@ - Async CRUD API with Django RBAC security protection - Prefetch and retrieve all m2m fields if configured - Recursively retrieve all FK/OneToOne fields if configured - - Excluding fields you do not want, or define a list sensitive fields of your choice + - Excluding fields you do not want, or define a list of sensitive fields of your choice - Domain/Service/Controller Base Structure: for better code organization. - Base Permission/Response/Exception Classes: and some handy features to help your API coding easier. - Pure class based [Django-Ninja](https://github.com/vitalik/django-ninja) APIs: thanks to [Django-Ninja-Extra](https://github.com/eadwinCode/django-ninja-extra) From 6574fb2359bcb4300ee12348f954e37ebeb6e401 Mon Sep 17 00:00:00 2001 From: freemindcore Date: Fri, 9 Dec 2022 00:57:29 +0800 Subject: [PATCH 43/58] Update test_full.yml --- .github/workflows/test_full.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/test_full.yml b/.github/workflows/test_full.yml index 46c8336..211a3d7 100644 --- a/.github/workflows/test_full.yml +++ b/.github/workflows/test_full.yml @@ -11,7 +11,7 @@ jobs: runs-on: ubuntu-latest strategy: matrix: - python-version: ['3.6', '3.7', '3.8', '3.9', '3.10'] + python-version: ['3.7', '3.8', '3.9', '3.10', '3.11'] django-version: ['>3.1', '<3.2', '<3.3', '<4.1'] steps: From 609f6194b54c8309eabf02e50fe1fa7ef0087576 Mon Sep 17 00:00:00 2001 From: freemindcore Date: Sat, 28 Jan 2023 22:33:41 +0800 Subject: [PATCH 44/58] Update README.md --- README.md | 17 +++++++++-------- 1 file changed, 9 insertions(+), 8 deletions(-) diff --git a/README.md b/README.md index fd7dc65..9cdcda4 100644 --- a/README.md +++ b/README.md @@ -82,7 +82,15 @@ Now go to http://127.0.0.1:8000/api_admin/v1/docs You will see the automatic interactive API documentation (provided by Swagger UI). ![Auto generated APIs List](https://github.com/freemindcore/django-api-framework/blob/fae8209a8d08c55daf75ac3a4619fe62b8ef3af6/docs/images/admin_apis_list.png) -#### Configuration + +### Boilerplate Django project +A boilerplate Django project for quickly getting started, and get production ready easy-apis with 100% test coverage UP and running: +https://github.com/freemindcore/django-easy-api + +![Auto generated APIs - Users](https://github.com/freemindcore/django-api-framework/blob/9aa26e92b6fd79f4d9db422ec450fe62d4cd97b9/docs/images/user_admin_api.png) + + +### More Configuration If `CRUD_API_ENABLED_ALL_APPS` is set to True (default), all app models CRUD apis will be generated. Apps in the `CRUD_API_EXCLUDE_APPS` list, will be always excluded. @@ -134,12 +142,5 @@ class EventAPIController(CrudAPIController): Please check tests/demo_app for more examples. -### Boilerplate Django project -A boilerplate Django project for quickly getting started, and get production ready easy-apis with 100% test coverage UP and running: -https://github.com/freemindcore/django-easy-api - -![Auto generated APIs - Users](https://github.com/freemindcore/django-api-framework/blob/9aa26e92b6fd79f4d9db422ec450fe62d4cd97b9/docs/images/user_admin_api.png) - - ### Thanks to your help **_If you find this project useful, please give your stars to support this open-source project. :) Thank you !_** From 8c352679b4f422c7f4e43f554f3eacf33256fc28 Mon Sep 17 00:00:00 2001 From: Hongbin Huang Date: Sun, 29 Jan 2023 00:20:53 +0800 Subject: [PATCH 45/58] Add support for python 3.11 --- README.md | 4 ++-- pyproject.toml | 1 + setup.cfg | 2 +- 3 files changed, 4 insertions(+), 3 deletions(-) diff --git a/README.md b/README.md index 9cdcda4..97f9eb7 100644 --- a/README.md +++ b/README.md @@ -10,7 +10,7 @@ ### Easy and Fast Django REST framework based on Django-Ninja-Extra -- CRUD API Generation: Automatic and configurable, inspired by [NextJs-Crud](https://github.com/nestjsx/crud). +- Zero code for a full CRUD API: Automatic and configurable, inspired by [NextJs-Crud](https://github.com/nestjsx/crud). - Async CRUD API with Django RBAC security protection - Prefetch and retrieve all m2m fields if configured - Recursively retrieve all FK/OneToOne fields if configured @@ -92,7 +92,7 @@ https://github.com/freemindcore/django-easy-api ### More Configuration If `CRUD_API_ENABLED_ALL_APPS` is set to True (default), all app models CRUD apis will be generated. -Apps in the `CRUD_API_EXCLUDE_APPS` list, will be always excluded. +Apps in the `CRUD_API_EXCLUDE_APPS` list, will always be excluded. If `CRUD_API_ENABLED_ALL_APPS` is set to False, only apps in the `CRUD_API_INCLUDE_APPS` list will have CRUD apis generated. diff --git a/pyproject.toml b/pyproject.toml index 797b401..f62ce58 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -27,6 +27,7 @@ classifiers = [ "Programming Language :: Python :: 3.8", "Programming Language :: Python :: 3.9", "Programming Language :: Python :: 3.10", + "Programming Language :: Python :: 3.11", "Programming Language :: Python :: 3 :: Only", "Framework :: Django", "Framework :: Django :: 3.1", diff --git a/setup.cfg b/setup.cfg index f05c35a..d12e57c 100644 --- a/setup.cfg +++ b/setup.cfg @@ -62,7 +62,7 @@ max-line-length = 88 exclude = .tox,.git,*/migrations/*,*/static/CACHE/*,docs,node_modules,venv [mypy] -python_version = 3.8 +python_version = 3.11 ignore_missing_imports = True warn_unused_configs = True plugins = mypy_django_plugin.main From 19aad273182db7d355cbe372ef39aa439da1d889 Mon Sep 17 00:00:00 2001 From: Hongbin Huang Date: Thu, 16 Feb 2023 12:08:56 +0800 Subject: [PATCH 46/58] lint: fmt using black 23.1.0 --- tests/easy_app/factories.py | 1 - tests/test_api_base_response.py | 2 -- 2 files changed, 3 deletions(-) diff --git a/tests/easy_app/factories.py b/tests/easy_app/factories.py index 82ff897..e64708a 100644 --- a/tests/easy_app/factories.py +++ b/tests/easy_app/factories.py @@ -4,7 +4,6 @@ class UserFactory(DjangoModelFactory): - username = Faker("user_name") email = Faker("email") diff --git a/tests/test_api_base_response.py b/tests/test_api_base_response.py index f2cb3fa..894ac7d 100644 --- a/tests/test_api_base_response.py +++ b/tests/test_api_base_response.py @@ -6,7 +6,6 @@ def test_base_api_result_base(): - assert BaseAPIResponse("").json_data["data"] == "" assert BaseAPIResponse("1").json_data["data"] == "1" @@ -20,7 +19,6 @@ def test_base_api_result_base(): def test_base_api_result_dict(): - assert BaseAPIResponse({"a": 1, "b": 2}).json_data["data"] == { "a": 1, "b": 2, From 014a626faab429a8ecb83a8f4bac9cce62057951 Mon Sep 17 00:00:00 2001 From: Hongbin Huang Date: Mon, 27 Feb 2023 11:38:36 +0800 Subject: [PATCH 47/58] update django-ninja-extra requirement --- pyproject.toml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/pyproject.toml b/pyproject.toml index f62ce58..64aca23 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -41,7 +41,7 @@ classifiers = [ requires = [ "Django >= 3.1", - "django-ninja-extra-easy >= 0.15.0", + "django-ninja-extra >= 0.16.0", ] description-file = "README.md" requires-python = ">=3.6" @@ -64,7 +64,7 @@ test = [ "django-stubs", "factory-boy==3.2.1", "django_coverage_plugin", - "django-ninja-extra-easy >= 0.15.0", + "django-ninja-extra >= 0.16.0", "django-ninja-jwt", "Django >= 3.1", ] From ced7e9cbcfebf16fe9aa105e1cdb7f43b50efad2 Mon Sep 17 00:00:00 2001 From: Hongbin Huang Date: Mon, 27 Feb 2023 14:42:49 +0800 Subject: [PATCH 48/58] fix: make install error (pyc being a directory) --- Makefile | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Makefile b/Makefile index 5750709..b542c83 100644 --- a/Makefile +++ b/Makefile @@ -5,7 +5,7 @@ help: @fgrep -h "##" $(MAKEFILE_LIST) | fgrep -v fgrep | sort | awk 'BEGIN {FS = ":.*?## "}; {printf "\033[36m%-30s\033[0m %s\n", $$1, $$2}' clean: ## Removing cached python compiled files - find . -name \*pyc | xargs rm -fv + find . -name \*pyc | xargs rm -rfv find . -name \*pyo | xargs rm -fv find . -name \*~ | xargs rm -fv find . -name __pycache__ | xargs rm -rfv From 798ac9746f80f8fbaca6a99ca073fe399cbae2ee Mon Sep 17 00:00:00 2001 From: Hongbin Huang Date: Tue, 28 Feb 2023 10:59:02 +0800 Subject: [PATCH 49/58] feat: Model is now optional for initializing a BaseService --- easy/domain/orm.py | 23 ++++++++++++----------- easy/services/base.py | 7 ++----- easy/services/crud.py | 4 ++-- 3 files changed, 16 insertions(+), 18 deletions(-) diff --git a/easy/domain/orm.py b/easy/domain/orm.py index aa2393d..2617f36 100644 --- a/easy/domain/orm.py +++ b/easy/domain/orm.py @@ -12,19 +12,20 @@ class DjangoOrmModel(CrudModel): - def __init__(self, model: Type[models.Model]): + def __init__(self, model: Optional[Type[models.Model]] = None) -> None: self.model = model - config = ModelMetaConfig() - exclude_list = config.get_final_excluded_list(self.model()) - self.m2m_fields_list: List = list( - _field - for _field in self.model._meta.get_fields(include_hidden=True) - if ( - isinstance(_field, models.ManyToManyField) - and ((_field not in exclude_list) if exclude_list else True) + if self.model: + config = ModelMetaConfig() + exclude_list = config.get_final_excluded_list(self.model()) + self.m2m_fields_list: List = list( + _field + for _field in self.model._meta.get_fields(include_hidden=True) + if ( + isinstance(_field, models.ManyToManyField) + and ((_field not in exclude_list) if exclude_list else True) + ) ) - ) - super().__init__(self.model) + super().__init__(self.model) def _separate_payload(self, payload: Dict) -> Tuple[Dict, Dict]: m2m_fields = {} diff --git a/easy/services/base.py b/easy/services/base.py index d677986..6ccc573 100644 --- a/easy/services/base.py +++ b/easy/services/base.py @@ -1,5 +1,5 @@ import logging -from typing import Type +from typing import Optional, Type from django.db import models @@ -10,9 +10,6 @@ class BaseService(CrudService, PermissionService): - def __init__( - self, - model: Type[models.Model], - ): + def __init__(self, model: Optional[Type[models.Model]] = None): self.model = model super().__init__(model=self.model) diff --git a/easy/services/crud.py b/easy/services/crud.py index 6063f8b..d28ac50 100644 --- a/easy/services/crud.py +++ b/easy/services/crud.py @@ -1,5 +1,5 @@ import logging -from typing import Any, Type +from typing import Any, Optional, Type from asgiref.sync import sync_to_async from django.db import models @@ -10,7 +10,7 @@ class CrudService(DjangoOrmModel): - def __init__(self, model: Type[models.Model]): + def __init__(self, model: Optional[Type[models.Model]] = None): super().__init__(model) self.model = model From ed8466ce82c6c85e8336e03bc6ce1691042378c6 Mon Sep 17 00:00:00 2001 From: Hongbin Huang Date: Tue, 28 Feb 2023 11:00:21 +0800 Subject: [PATCH 50/58] release 0.1.40-rc2 --- easy/__init__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/easy/__init__.py b/easy/__init__.py index 480aa34..6c2309e 100644 --- a/easy/__init__.py +++ b/easy/__init__.py @@ -1,6 +1,6 @@ """Django Easy API - Easy and Fast Django REST framework based on Django-ninja-extra""" -__version__ = "0.1.39" +__version__ = "0.1.40-rc2" from easy.main import EasyAPI From 35614752ccc12a0daec1aa38e77e8b671a15ff1c Mon Sep 17 00:00:00 2001 From: Hongbin Huang Date: Wed, 7 Jun 2023 16:15:52 +0800 Subject: [PATCH 51/58] chore: add django 4.2 support --- .github/workflows/test_full.yml | 2 +- pyproject.toml | 1 + 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/.github/workflows/test_full.yml b/.github/workflows/test_full.yml index 211a3d7..12707b9 100644 --- a/.github/workflows/test_full.yml +++ b/.github/workflows/test_full.yml @@ -12,7 +12,7 @@ jobs: strategy: matrix: python-version: ['3.7', '3.8', '3.9', '3.10', '3.11'] - django-version: ['>3.1', '<3.2', '<3.3', '<4.1'] + django-version: ['>3.1', '<3.2', '<3.3', '<4.3'] steps: - uses: actions/checkout@v3 diff --git a/pyproject.toml b/pyproject.toml index 64aca23..a6a8d09 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -34,6 +34,7 @@ classifiers = [ "Framework :: Django :: 3.2", "Framework :: Django :: 4.0", "Framework :: Django :: 4.1", + "Framework :: Django :: 4.2", "Framework :: AsyncIO", "Topic :: Internet :: WWW/HTTP :: HTTP Servers", "Topic :: Internet :: WWW/HTTP", From 535932941c760f209957b489aed2edd467547ef0 Mon Sep 17 00:00:00 2001 From: Hongbin Huang Date: Wed, 7 Jun 2023 16:16:14 +0800 Subject: [PATCH 52/58] fix: install flit upon fresh make install --- Makefile | 1 + 1 file changed, 1 insertion(+) diff --git a/Makefile b/Makefile index b542c83..8b2f2a3 100644 --- a/Makefile +++ b/Makefile @@ -11,6 +11,7 @@ clean: ## Removing cached python compiled files find . -name __pycache__ | xargs rm -rfv install: ## Install dependencies + pip install flit make clean flit install --deps develop --symlink pre-commit install From e6f00a5dc4440fb9efbcfdf601648b520efbc2aa Mon Sep 17 00:00:00 2001 From: Hongbin Huang Date: Wed, 7 Jun 2023 16:19:32 +0800 Subject: [PATCH 53/58] release: 0.1.40 --- easy/__init__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/easy/__init__.py b/easy/__init__.py index 6c2309e..9ec75cc 100644 --- a/easy/__init__.py +++ b/easy/__init__.py @@ -1,6 +1,6 @@ """Django Easy API - Easy and Fast Django REST framework based on Django-ninja-extra""" -__version__ = "0.1.40-rc2" +__version__ = "0.1.40" from easy.main import EasyAPI From a2846c084d56c8ddd33013131194fb217ee77c90 Mon Sep 17 00:00:00 2001 From: Hongbin Huang Date: Wed, 7 Jun 2023 17:02:41 +0800 Subject: [PATCH 54/58] fix: make install issues --- pyproject.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pyproject.toml b/pyproject.toml index a6a8d09..242661a 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -66,7 +66,7 @@ test = [ "factory-boy==3.2.1", "django_coverage_plugin", "django-ninja-extra >= 0.16.0", - "django-ninja-jwt", + "django-ninja-jwt>=5.1.0", "Django >= 3.1", ] dev = [ From 6dadf3e2ba09973cc492d02911edc2c81a4f4ab0 Mon Sep 17 00:00:00 2001 From: Hongbin Huang Date: Wed, 7 Jun 2023 17:09:45 +0800 Subject: [PATCH 55/58] fix: pass mypy --- easy/controller/auto_api.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/easy/controller/auto_api.py b/easy/controller/auto_api.py index dd1d67b..fd7d1d0 100644 --- a/easy/controller/auto_api.py +++ b/easy/controller/auto_api.py @@ -58,7 +58,7 @@ def create_api_controller( }, ) - return api_controller( + return api_controller( # type: ignore f"/{app_name}/{model_name.lower()}", tags=[f"{model_name} {controller_name_prefix}API"], permissions=[permission_class], From b708ad28e9217f8ab2fe045a199e503ae480501c Mon Sep 17 00:00:00 2001 From: Hongbin Huang Date: Wed, 7 Jun 2023 17:54:18 +0800 Subject: [PATCH 56/58] fix: publish failure --- .github/workflows/publish.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/publish.yml b/.github/workflows/publish.yml index ff8862c..bbd0cad 100644 --- a/.github/workflows/publish.yml +++ b/.github/workflows/publish.yml @@ -13,7 +13,7 @@ jobs: - name: Set up Python uses: actions/setup-python@v4 with: - python-version: 3.6 + python-version: 3.9 - name: Install Flit run: pip install flit - name: Install Dependencies From 39b96b64ebf7b88a71e53cb01c00522d4c917581 Mon Sep 17 00:00:00 2001 From: freemindcore Date: Wed, 7 Jun 2023 23:05:24 +0800 Subject: [PATCH 57/58] Update README.md --- README.md | 38 ++++++++++++++++++-------------------- 1 file changed, 18 insertions(+), 20 deletions(-) diff --git a/README.md b/README.md index 97f9eb7..3cde0b0 100644 --- a/README.md +++ b/README.md @@ -1,23 +1,24 @@ -![Test](https://github.com/freemindcore/django-api-framework/actions/workflows/test_full.yml/badge.svg) [![PyPI version](https://badge.fury.io/py/django-api-framework.svg)](https://badge.fury.io/py/django-api-framework) [![PyPI version](https://img.shields.io/pypi/v/django-api-framework.svg)](https://pypi.python.org/pypi/django-api-framework) -[![PyPI version](https://img.shields.io/pypi/pyversions/django-api-framework.svg)](https://pypi.python.org/pypi/django-api-framework) -[![PyPI version](https://img.shields.io/pypi/djversions/django-api-framework.svg)](https://pypi.python.org/pypi/django-api-framework) + +![Test](https://github.com/freemindcore/django-api-framework/actions/workflows/test_full.yml/badge.svg) [![Codecov](https://img.shields.io/codecov/c/gh/freemindcore/django-api-framework)](https://codecov.io/gh/freemindcore/django-api-framework) [![Downloads](https://pepy.tech/badge/django-api-framework/month)](https://pepy.tech/project/django-api-framework) -# Django Easy API Framework +[![PyPI version](https://img.shields.io/pypi/pyversions/django-api-framework.svg)](https://pypi.python.org/pypi/django-api-framework) +[![PyPI version](https://img.shields.io/pypi/djversions/django-api-framework.svg)](https://pypi.python.org/pypi/django-api-framework) + -### Easy and Fast Django REST framework based on Django-Ninja-Extra +# Easy CRUD API Framework - Zero code for a full CRUD API: Automatic and configurable, inspired by [NextJs-Crud](https://github.com/nestjsx/crud). - Async CRUD API with Django RBAC security protection - Prefetch and retrieve all m2m fields if configured - Recursively retrieve all FK/OneToOne fields if configured - Excluding fields you do not want, or define a list of sensitive fields of your choice +- Pure class based [Django-Ninja](https://github.com/vitalik/django-ninja) APIs: thanks to [Django-Ninja-Extra](https://github.com/eadwinCode/django-ninja-extra) - Domain/Service/Controller Base Structure: for better code organization. - Base Permission/Response/Exception Classes: and some handy features to help your API coding easier. -- Pure class based [Django-Ninja](https://github.com/vitalik/django-ninja) APIs: thanks to [Django-Ninja-Extra](https://github.com/eadwinCode/django-ninja-extra) ``` Django-Ninja features: @@ -34,13 +35,7 @@ Plus Extra: Dependency Injection: Controller classes supports dependency injection with python Injector or django_injector. Giving you the ability to inject API dependable services to APIController class and utilizing them where needed ``` -### Requirements -- Python >= 3.6 -- Django >= 3.1 -- pydantic >= 1.6 -- Django-Ninja-Extra >= 0.15.0 - -### Install +## Install `pip install django-api-framework` Then add "easy" to your django INSTALLED_APPS: @@ -53,8 +48,8 @@ Then add "easy" to your django INSTALLED_APPS: ] ``` -### Usage -#### Get all your Django app CRUD APIs up and running in < 1 min +## Usage +### Get all your Django app CRUD APIs up and running in < 1 min In your Django project next to urls.py create new apis.py file: ``` from easy.main import EasyAPI @@ -90,7 +85,14 @@ https://github.com/freemindcore/django-easy-api ![Auto generated APIs - Users](https://github.com/freemindcore/django-api-framework/blob/9aa26e92b6fd79f4d9db422ec450fe62d4cd97b9/docs/images/user_admin_api.png) -### More Configuration +## Thanks to your help +**_If you find this project useful, please give your stars to support this open-source project. :) Thank you !_** + + + + + +## Advanced Usage If `CRUD_API_ENABLED_ALL_APPS` is set to True (default), all app models CRUD apis will be generated. Apps in the `CRUD_API_EXCLUDE_APPS` list, will always be excluded. @@ -140,7 +142,3 @@ class EventAPIController(CrudAPIController): ``` Please check tests/demo_app for more examples. - - -### Thanks to your help -**_If you find this project useful, please give your stars to support this open-source project. :) Thank you !_** From f4fd0ff27775a87a912544280c9ae8fc0c6ea26f Mon Sep 17 00:00:00 2001 From: freemindcore Date: Wed, 22 Nov 2023 16:50:43 +0800 Subject: [PATCH 58/58] Releases/0.2.0 (#14) * feat: Latest Ninja V1 Support (by django-ninja-extra 0.20.0) Updates: "django-ninja-extra >= 0.20.0" "django-ninja-jwt>=5.2.9" * release: v0.2.0 (Ninja V1 Support, mainly Pydantic V2 Upgrade) * fix: update related qa tools - mypy/black etc - to pass codestyle check * fix: mypy check --------- Co-authored-by: Hongbin Huang --- .pre-commit-config.yaml | 2 +- easy/__init__.py | 2 +- easy/controller/auto_api.py | 2 +- easy/controller/meta_conf.py | 4 ++-- easy/decorators.py | 6 +++--- easy/domain/orm.py | 2 +- easy/exception.py | 5 +++-- easy/permissions/base.py | 4 ++-- easy/testing/client.py | 8 ++++---- pyproject.toml | 10 +++++----- setup.cfg | 1 + 11 files changed, 24 insertions(+), 22 deletions(-) diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 8fea904..db33b6e 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -23,7 +23,7 @@ repos: args: [--allow-missing-credentials] - repo: https://github.com/psf/black - rev: 22.8.0 + rev: 23.10.1 hooks: - id: black diff --git a/easy/__init__.py b/easy/__init__.py index 9ec75cc..55c1888 100644 --- a/easy/__init__.py +++ b/easy/__init__.py @@ -1,6 +1,6 @@ """Django Easy API - Easy and Fast Django REST framework based on Django-ninja-extra""" -__version__ = "0.1.40" +__version__ = "0.2.0" from easy.main import EasyAPI diff --git a/easy/controller/auto_api.py b/easy/controller/auto_api.py index fd7d1d0..dd1d67b 100644 --- a/easy/controller/auto_api.py +++ b/easy/controller/auto_api.py @@ -58,7 +58,7 @@ def create_api_controller( }, ) - return api_controller( # type: ignore + return api_controller( f"/{app_name}/{model_name.lower()}", tags=[f"{model_name} {controller_name_prefix}API"], permissions=[permission_class], diff --git a/easy/controller/meta_conf.py b/easy/controller/meta_conf.py index d07d043..3902ea7 100644 --- a/easy/controller/meta_conf.py +++ b/easy/controller/meta_conf.py @@ -1,4 +1,4 @@ -from typing import Any, Dict, List, Optional, Type, Union +from typing import Any, List, Optional, Type, Union from django.db import models @@ -24,7 +24,7 @@ class ModelOptions: - def __init__(self, options: Dict = None): + def __init__(self, options: Optional[object] = None): """ Configuration reader """ diff --git a/easy/decorators.py b/easy/decorators.py index 23b0183..a56b06a 100644 --- a/easy/decorators.py +++ b/easy/decorators.py @@ -1,6 +1,6 @@ from functools import wraps from types import FunctionType -from typing import Any, Callable +from typing import Any, Callable, Optional from urllib.parse import urlparse from django.conf import settings @@ -11,7 +11,7 @@ def request_passes_test( test_func: Callable[[Any], Any], - login_url: str = None, + login_url: Optional[str] = None, redirect_field_name: str = REDIRECT_FIELD_NAME, ) -> Callable[[FunctionType], Callable[[HttpRequest, Any], Any]]: """ @@ -45,7 +45,7 @@ def _wrapped_view(request: HttpRequest, *args: Any, **kwargs: Any) -> Any: def docs_permission_required( - view_func: FunctionType = None, + view_func: Optional[FunctionType] = None, redirect_field_name: str = REDIRECT_FIELD_NAME, login_url: str = "admin:login", ) -> Any: diff --git a/easy/domain/orm.py b/easy/domain/orm.py index 2617f36..740e427 100644 --- a/easy/domain/orm.py +++ b/easy/domain/orm.py @@ -220,7 +220,7 @@ def serialize_many_relationship( return {} out = {} try: - for k, v in obj._prefetched_objects_cache.items(): # type: ignore + for k, v in obj._prefetched_objects_cache.items(): field_name = k if hasattr(obj, k) else k + "_set" if v: if self.get_model_join(obj): diff --git a/easy/exception.py b/easy/exception.py index f140f12..a842024 100644 --- a/easy/exception.py +++ b/easy/exception.py @@ -1,3 +1,4 @@ +from django.utils.translation import gettext_lazy as _ from ninja_extra import status from ninja_extra.exceptions import APIException @@ -8,7 +9,7 @@ class BaseAPIException(APIException): """ status_code = status.HTTP_500_INTERNAL_SERVER_ERROR - default_detail = ( + default_detail = _( "There is an unexpected error, please try again later, if the problem " "persists, please contact customer support team for further support." ) @@ -20,4 +21,4 @@ class APIAuthException(BaseAPIException): """ status_code = status.HTTP_401_UNAUTHORIZED - default_detail = "Unauthorized" + default_detail = _("Unauthorized") diff --git a/easy/permissions/base.py b/easy/permissions/base.py index 0c59a7f..76cb28d 100644 --- a/easy/permissions/base.py +++ b/easy/permissions/base.py @@ -21,7 +21,7 @@ def has_permission( """ has_perm: bool = True if hasattr(controller, "service"): - has_perm = controller.service.check_permission(request, controller) # type: ignore + has_perm = controller.service.check_permission(request, controller) return has_perm def has_object_permission( @@ -32,7 +32,7 @@ def has_object_permission( """ has_perm: bool = True if hasattr(controller, "service"): - has_perm = controller.service.check_object_permission( # type: ignore + has_perm = controller.service.check_object_permission( request, controller, obj ) return has_perm diff --git a/easy/testing/client.py b/easy/testing/client.py index 4c93613..3ad0d15 100644 --- a/easy/testing/client.py +++ b/easy/testing/client.py @@ -1,5 +1,5 @@ from json import dumps as json_dumps -from typing import Any, Callable, Dict, List, Sequence, Type, Union, cast +from typing import Any, Callable, Dict, List, Optional, Sequence, Type, Union, cast from unittest.mock import Mock from urllib.parse import urlencode @@ -22,7 +22,7 @@ def __init__( ) -> None: if hasattr(router_or_app, "get_api_controller"): api = api_cls(auth=auth) - controller_ninja_api_controller = router_or_app.get_api_controller() # type: ignore + controller_ninja_api_controller = router_or_app.get_api_controller() assert controller_ninja_api_controller controller_ninja_api_controller.set_api_instance(api) self._urls_cache = list(controller_ninja_api_controller.urls_paths("")) @@ -33,7 +33,7 @@ def request( self, method: str, path: str, - data: Dict = {}, + data: Optional[Dict] = None, json: Any = None, **request_params: Any, ) -> "NinjaResponse": @@ -43,7 +43,7 @@ def request( query = request_params.pop("query") url_encode = urlencode(query) path = f"{path}?{url_encode}" - func, request, kwargs = self._resolve(method, path, data, request_params) + func, request, kwargs = self._resolve(method, path, data, request_params) # type: ignore return self._call(func, request, kwargs) # type: ignore @property diff --git a/pyproject.toml b/pyproject.toml index 242661a..4cc0513 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -57,16 +57,16 @@ test = [ "pytest-cov", "pytest-django", "pytest-asyncio", - "black", + "black==23.10.1", + "mypy==1.6.1", "isort", - "injector == 0.19.0", + "injector>= 0.19.0", "flake8", - "mypy==0.931", "django-stubs", "factory-boy==3.2.1", "django_coverage_plugin", - "django-ninja-extra >= 0.16.0", - "django-ninja-jwt>=5.1.0", + "django-ninja-extra >= 0.20.0", + "django-ninja-jwt>=5.2.9", "Django >= 3.1", ] dev = [ diff --git a/setup.cfg b/setup.cfg index d12e57c..ff77453 100644 --- a/setup.cfg +++ b/setup.cfg @@ -82,6 +82,7 @@ warn_unused_ignores = True disallow_untyped_defs = True check_untyped_defs = True no_implicit_reexport = True +no_implicit_optional = False [mypy.plugins.django-stubs] django_settings_module = "tests.easy_app"