From 01395aa0d70101f4672b284f972d064f681b2d45 Mon Sep 17 00:00:00 2001 From: Joao Daher Date: Sat, 6 Mar 2021 21:38:16 -0300 Subject: [PATCH] Use sanic-rest --- flamingo/exceptions.py | 45 ------ flamingo/views/app_views.py | 2 +- flamingo/views/base.py | 218 ---------------------------- flamingo/views/build_pack_views.py | 2 +- flamingo/views/environment_views.py | 4 +- flamingo/views/hook_views.py | 2 +- poetry.lock | 21 ++- pyproject.toml | 5 +- 8 files changed, 26 insertions(+), 273 deletions(-) delete mode 100644 flamingo/exceptions.py delete mode 100644 flamingo/views/base.py diff --git a/flamingo/exceptions.py b/flamingo/exceptions.py deleted file mode 100644 index 1f256a9..0000000 --- a/flamingo/exceptions.py +++ /dev/null @@ -1,45 +0,0 @@ -from dataclasses import dataclass - -from sanic.handlers import ErrorHandler -from sanic.response import json - - -@dataclass -class HttpException(Exception): - message: str - status_code: int - - @property - def response(self): - return {'error': self.message} - - -@dataclass -class ValidationError(HttpException): - status_code: int = 400 - - -@dataclass -class NotFoundError(HttpException): - message: str = "Not found" - status_code: int = 404 - - -@dataclass -class NotAllowedError(HttpException): - message: str = "Not allowed" - status_code: int = 405 - - -@dataclass -class ForbiddenError(HttpException): - message: str = "Forbidden" - status_code: int = 409 - - -def _http_error_handler(request, exc): - return json(exc.response, exc.status_code) - - -rest_error_handler = ErrorHandler() -rest_error_handler.add(exception=HttpException, handler=_http_error_handler) diff --git a/flamingo/views/app_views.py b/flamingo/views/app_views.py index f3701eb..791a4e7 100644 --- a/flamingo/views/app_views.py +++ b/flamingo/views/app_views.py @@ -9,7 +9,7 @@ from models.env_var import EnvVar from services.bootstrap import AppBootstrap from services.foundations import AppFoundation -from views.base import ActionView, DetailView, ListView, ResponseType +from sanic_rest.views import ActionView, DetailView, ListView, ResponseType apps = Blueprint('apps', url_prefix='/apps') diff --git a/flamingo/views/base.py b/flamingo/views/base.py deleted file mode 100644 index e9f3877..0000000 --- a/flamingo/views/base.py +++ /dev/null @@ -1,218 +0,0 @@ -import abc -from collections import defaultdict -from dataclasses import _MISSING_TYPE -from enum import Enum -from pathlib import Path -from typing import Tuple, Dict, Any, List - -import aiofiles -from gcp_pilot.datastore import Document, DoesNotExist, EmbeddedDocument -from sanic.request import Request, RequestParameters, File -from sanic.response import json, HTTPResponse -from sanic.views import HTTPMethodView - -import exceptions -import settings - -PayloadType = Dict[str, Any] -ResponseType = Tuple[PayloadType, int] - - -def _get_model_options(model_klass): - hints = model_klass.__dataclass_fields__ - - field_options = {} - for field_name, field_type in model_klass.Meta.fields.items(): - hint = hints[field_name] - options = { - 'type': field_type.__name__, - 'required': isinstance(hint.default, _MISSING_TYPE) - } - - if issubclass(field_type, Enum): - options['choices'] = [enum.value for enum in field_type] - elif issubclass(field_type, EmbeddedDocument): - options.update(_get_model_options(model_klass=field_type)) - - field_options[field_name] = options - return { - 'fields': field_options, - } - - -class ViewBase(HTTPMethodView): - model: Document - - @classmethod - async def write_file(cls, file: File, filepath: Path): - filepath.parent.mkdir(parents=True, exist_ok=True) - async with aiofiles.open(filepath, 'wb') as f: - await f.write(file.body) - await f.close() - - -class ListView(ViewBase): - async def get(self, request: Request) -> HTTPResponse: - query_args, page, page_size = self._parse_query_args(request=request) - items = list(await self.perform_get(query_filters=query_args)) - - items_in_page = self._paginate(items=items, page=page, page_size=page_size) - - response = { - 'results': [ - obj.serialize() - for obj in - items_in_page - ], - 'count': len(items) - } - return json(response, 200) - - def _parse_query_args(self, request: Request) -> Tuple[Dict[str, Any], int, int]: - query_args = {} - for key, value in request.query_args: - if key not in query_args: - query_args[key] = value - elif isinstance(query_args[key], list): - query_args[key].append(value) - else: - query_args[key] = [query_args[key], value] - - page = query_args.pop('page', 1) - page_size = query_args.pop('page_size', 10) - return query_args, int(page), int(page_size) - - def _paginate(self, items: List[Document], page: int, page_size: int) -> List[Document]: - start_idx = (page - 1) * page_size - start_idx = min(start_idx, len(items)) - - end_idx = start_idx + page_size - end_idx = min(end_idx, len(items)) - - items_in_page = items[start_idx:end_idx] - return items_in_page - - async def perform_get(self, query_filters) -> List[Document]: - return self.model.documents.filter(**query_filters) - - async def post(self, request: Request) -> HTTPResponse: - obj = await self.perform_create(data=request.json) - data = obj.serialize() - return json(data, 201) - - async def perform_create(self, data: PayloadType) -> Document: - obj = self.model.deserialize(**data) - return obj.save() - - async def options(self, request: Request) -> HTTPResponse: - data = await self.perform_options() - return json(data, 200) - - async def perform_options(self) -> PayloadType: - return _get_model_options(model_klass=self.model) - - -class DetailView(ViewBase): - async def get(self, request: Request, pk: str) -> HTTPResponse: - try: - obj = self.model.documents.get(id=pk) - except DoesNotExist as e: - raise exceptions.NotFoundError() from e - - data = obj.serialize() - return json(data, 200) - - async def perform_get(self, pk, query_filters) -> Document: - return self.model.documents.get(id=pk, **query_filters) - - async def put(self, request: Request, pk: str) -> HTTPResponse: - payload = request.json - obj = await self.perform_create(data=payload) - data = obj.serialize() - return json(data, 200) - - async def perform_create(self, data: PayloadType) -> Document: - obj = self.model.deserialize(**data) - return obj.save() - - async def patch(self, request: Request, pk: str) -> HTTPResponse: - payload = {} if request.files else request.json - obj = await self.perform_update(pk=pk, data=payload, files=request.files) - - data = obj.serialize() - return json(data, 200) - - async def perform_update(self, pk: str, data: PayloadType, files: RequestParameters) -> Document: - obj = self.model.documents.update(pk=pk, **data) - - file_updates: Dict[str, Any] = defaultdict(list) - for key, file in files.items(): - filepath = await self.store_file( - obj=obj, - field_name=key, - file=file[0], # TODO: check if it's possible to have more files - ) - if '__' not in key: - file_updates[key] = filepath - else: - key = key.split('__')[0] - file_updates[key].append(filepath) - obj = self.model.documents.update(pk=pk, **file_updates) - - return obj - - async def store_file(self, obj: Document, field_name: str, file: File) -> str: - local_filepath = settings.STAGE_DIR / 'media' / obj.pk / field_name - await self.write_file(file=file, filepath=local_filepath) - return str(local_filepath) - - async def delete(self, request: Request, pk: str) -> HTTPResponse: - await self.perform_delete(pk=pk) - return json({}, 204) - - async def perform_delete(self, pk: str) -> None: - self.model.documents.delete(pk=pk) - - -class ActionView(ViewBase): - def get_model(self, pk: str) -> Document: - return self.model.documents.get(id=pk) - - async def get(self, request: Request, pk: str) -> HTTPResponse: - try: - obj = self.get_model(pk=pk) - except DoesNotExist as e: - raise exceptions.NotFoundError() from e - - data, status = await self.perform_get(request=request, obj=obj) - return json(data, status) - - @abc.abstractmethod - async def perform_get(self, request: Request, obj: Document) -> ResponseType: - raise exceptions.NotAllowedError() - - async def post(self, request: Request, pk: str) -> HTTPResponse: - try: - obj = self.get_model(pk=pk) - except DoesNotExist as e: - raise exceptions.NotFoundError() from e - - data, status = await self.perform_post(request=request, obj=obj) - return json(data, status) - - @abc.abstractmethod - async def perform_post(self, request: Request, obj: Document) -> ResponseType: - raise NotImplementedError() - - async def delete(self, request: Request, pk: str) -> HTTPResponse: - try: - obj = self.get_model(pk=pk) - except DoesNotExist as e: - raise exceptions.NotFoundError() from e - - data, status = await self.perform_delete(request=request, obj=obj) - return json(data, status) - - @abc.abstractmethod - async def perform_delete(self, request: Request, obj: Document) -> ResponseType: - raise NotImplementedError() diff --git a/flamingo/views/build_pack_views.py b/flamingo/views/build_pack_views.py index 002eaee..7d70064 100644 --- a/flamingo/views/build_pack_views.py +++ b/flamingo/views/build_pack_views.py @@ -2,7 +2,7 @@ from sanic.request import File from models.buildpack import BuildPack -from views.base import DetailView, ListView, PayloadType +from sanic_rest.views import DetailView, ListView, PayloadType build_packs = Blueprint('build-packs', url_prefix='/build-packs') diff --git a/flamingo/views/environment_views.py b/flamingo/views/environment_views.py index 093a516..4c83d5d 100644 --- a/flamingo/views/environment_views.py +++ b/flamingo/views/environment_views.py @@ -1,10 +1,10 @@ from sanic import Blueprint from sanic.request import Request +from sanic_rest import exceptions -import exceptions from models.environment import Environment from services.foundations import EnvironmentFoundation -from views.base import DetailView, ListView, ActionView, ResponseType +from sanic_rest.views import DetailView, ListView, ActionView, ResponseType environments = Blueprint('environments', url_prefix='/environments') diff --git a/flamingo/views/hook_views.py b/flamingo/views/hook_views.py index 5aecd65..01d2944 100644 --- a/flamingo/views/hook_views.py +++ b/flamingo/views/hook_views.py @@ -9,8 +9,8 @@ from sanic.request import Request from sanic.response import HTTPResponse, json from sanic.views import HTTPMethodView +from sanic_rest import exceptions -import exceptions from models.app import App from models.deployment import Deployment, Event, Source diff --git a/poetry.lock b/poetry.lock index 40a278f..614ae55 100644 --- a/poetry.lock +++ b/poetry.lock @@ -699,6 +699,19 @@ dev = ["black (==19.3b0)", "flake8 (==3.7.7)", "isort (==4.3.19)", "coverage (== doc = ["recommonmark (==0.5.0)", "sphinx (==2.1.2)", "sphinx-rtd-theme (==0.4.3)"] test = ["coverage (==4.5.3)", "pytest (==4.6.2)", "pytest-cov (==2.7.1)", "pytest-html (==1.20.0)", "pytest-runner (==5.1)", "tox (==3.12.1)"] +[[package]] +name = "sanic-rest" +version = "1.0.0" +description = "Sanic Rest Framework with Google Cloud Datastore" +category = "main" +optional = false +python-versions = ">=3.8,<3.10" + +[package.dependencies] +gcp-pilot = {version = "*", extras = ["datastore"]} +sanic = "*" +sanic-openapi = "*" + [[package]] name = "six" version = "1.15.0" @@ -809,8 +822,8 @@ dev-kit = ["pylint", "coverage"] [metadata] lock-version = "1.1" -python-versions = ">=3.9,<3.10" -content-hash = "00ef415fb0a96071e9215b42908d174df4d1a42a02994970e0101152126c4e73" +python-versions = "~3.9" +content-hash = "74addc51713a8f8d8aa9b8d5c4defcf8e810b75a2ef4ead0c36a2067e7800424" [metadata.files] aiofiles = [ @@ -1312,6 +1325,10 @@ sanic = [ sanic-openapi = [ {file = "sanic-openapi-0.6.2.tar.gz", hash = "sha256:0ddd11983443d4136ef3e93146cbf89a7196533eb198d72f7489b840cf4016c4"}, ] +sanic-rest = [ + {file = "sanic-rest-1.0.0.tar.gz", hash = "sha256:4adaf5f974790e55c7bbda23d041e1fb973e5dfa1f4e172dfe21ed44a5a568ca"}, + {file = "sanic_rest-1.0.0-py3-none-any.whl", hash = "sha256:f94301b41f3860cc2a259683526ea3c601c0c048e293554baaa18db1c663b25a"}, +] six = [ {file = "six-1.15.0-py2.py3-none-any.whl", hash = "sha256:8b74bedcbbbaca38ff6d7491d76f2b06b3592611af620f8426e82dddb04a5ced"}, {file = "six-1.15.0.tar.gz", hash = "sha256:30639c035cdb23534cd4aa2dd52c3bf48f06e5f4a941509c8bafd8ce11080259"}, diff --git a/pyproject.toml b/pyproject.toml index c54b410..ab6ff76 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -8,9 +8,8 @@ packages = [ ] [tool.poetry.dependencies] -python = ">=3.9,<3.10" -sanic = "*" -sanic-openapi = "*" +python = "~3.9" +sanic-rest = "*" gcp-pilot = { version = "*", extras = ['datastore', 'build', 'storage', 'pubsub', 'dns']} python-slugify = "*" PyGithub = "*"