mirror of
https://github.com/enpaul/kodak.git
synced 2024-11-14 02:27:24 +00:00
Add shared base resource object and update main resources to use it
This commit is contained in:
parent
be1f02c414
commit
3e08b62010
@ -1,7 +1,17 @@
|
|||||||
|
from typing import Tuple
|
||||||
|
|
||||||
|
from imagemonk.resources._shared import ImageMonkResource
|
||||||
from imagemonk.resources.image import Image
|
from imagemonk.resources.image import Image
|
||||||
from imagemonk.resources.image import ImageUpload
|
from imagemonk.resources.image import ImageUpload
|
||||||
|
from imagemonk.resources.openapi import OpenAPI
|
||||||
from imagemonk.resources.thumbnail import ThumbnailResize
|
from imagemonk.resources.thumbnail import ThumbnailResize
|
||||||
from imagemonk.resources.thumbnail import ThumbnailScale
|
from imagemonk.resources.thumbnail import ThumbnailScale
|
||||||
|
|
||||||
|
|
||||||
RESOURCES = (ImageUpload, Image, ThumbnailScale, ThumbnailResize)
|
RESOURCES: Tuple[ImageMonkResource, ...] = (
|
||||||
|
ImageUpload,
|
||||||
|
Image,
|
||||||
|
OpenAPI,
|
||||||
|
ThumbnailScale,
|
||||||
|
ThumbnailResize,
|
||||||
|
)
|
||||||
|
110
imagemonk/resources/_shared.py
Normal file
110
imagemonk/resources/_shared.py
Normal file
@ -0,0 +1,110 @@
|
|||||||
|
"""Shared resource base with common functionality"""
|
||||||
|
import logging
|
||||||
|
from typing import Any
|
||||||
|
from typing import Dict
|
||||||
|
from typing import List
|
||||||
|
from typing import NamedTuple
|
||||||
|
from typing import Optional
|
||||||
|
from typing import Tuple
|
||||||
|
from typing import Union
|
||||||
|
|
||||||
|
import flask
|
||||||
|
import flask_restful
|
||||||
|
|
||||||
|
|
||||||
|
ResponseBody = Optional[Union[Dict[str, Any], List[Dict[str, Any]], List[str]]]
|
||||||
|
|
||||||
|
|
||||||
|
ResponseHeaders = Dict[str, str]
|
||||||
|
|
||||||
|
|
||||||
|
class ResponseTuple(NamedTuple):
|
||||||
|
"""Namedtuple representing the format of a flask-restful response tuple
|
||||||
|
|
||||||
|
:param body: Response body; must be comprised only of JSON-friendly primative types
|
||||||
|
:param code: HTTP response code
|
||||||
|
:param headers: Dictionary of headers
|
||||||
|
"""
|
||||||
|
|
||||||
|
body: ResponseBody
|
||||||
|
code: int
|
||||||
|
headers: ResponseHeaders
|
||||||
|
|
||||||
|
|
||||||
|
class ImageMonkResource(flask_restful.Resource):
|
||||||
|
"""Extension of the default :class:`flask_restful.Resource` class
|
||||||
|
|
||||||
|
Add a couple of useful things to the default resource class:
|
||||||
|
|
||||||
|
* Adds the :meth:`options` method to respond to HTTP OPTION requests
|
||||||
|
* Adds the :meth:`_head` method as a stub helper for responding to HTTP HEAD requests
|
||||||
|
* Adds the :meth:`make_response` method which handles response formatting boilerplate
|
||||||
|
* Type hints the :attr:`routes` attribute for usage in subclasses
|
||||||
|
* Adds an instance logger
|
||||||
|
|
||||||
|
.. warning:: This class is a stub and should not be directly attached to an application
|
||||||
|
|
||||||
|
:attribute routes: Tuple of route paths that this resource should handle; can be unpacked into
|
||||||
|
``flask_restful.Api().add_route()``
|
||||||
|
"""
|
||||||
|
|
||||||
|
routes: Tuple[str, ...]
|
||||||
|
|
||||||
|
def __init__(self, *args, **kwargs):
|
||||||
|
super().__init__(*args, **kwargs)
|
||||||
|
self.logger = logging.getLogger()
|
||||||
|
|
||||||
|
def options(
|
||||||
|
self, *args, **kwargs
|
||||||
|
) -> ResponseTuple: # pylint: disable=unused-argument
|
||||||
|
"""Implement HTTP ``OPTIONS`` support
|
||||||
|
|
||||||
|
`Reference documentation <https://developer.mozilla.org/en-US/docs/Web/HTTP/Methods/OPTIONS>`_
|
||||||
|
"""
|
||||||
|
|
||||||
|
verbs = ",".join([verb.upper() for verb in flask.request.url_rule.methods])
|
||||||
|
|
||||||
|
return self.make_response(None, 204, {"Allowed": verbs})
|
||||||
|
|
||||||
|
def _head(self, response: ResponseTuple) -> ResponseTuple:
|
||||||
|
"""Wrapper to implement HTTP ``HEAD`` support
|
||||||
|
|
||||||
|
`Reference documentation <https://developer.mozilla.org/en-US/docs/Web/HTTP/Methods/HEAD>`_
|
||||||
|
|
||||||
|
.. note:: The ``head`` method cannot be implemented directly as an alias of ``get`` because
|
||||||
|
that would require a uniform signature for ``get`` across all resources; or some
|
||||||
|
hacky nonsense that wouldn't be worth it. This stub instead lets child resources
|
||||||
|
implement ``head`` as a oneliner.
|
||||||
|
"""
|
||||||
|
return self.make_response(None, response.code, response.headers)
|
||||||
|
|
||||||
|
def make_response(
|
||||||
|
self,
|
||||||
|
data: ResponseBody,
|
||||||
|
code: int = 200,
|
||||||
|
headers: Optional[ResponseHeaders] = None,
|
||||||
|
):
|
||||||
|
"""Create a response tuple from the current context
|
||||||
|
|
||||||
|
Helper function for generating defaults, parsing common data, and formatting the response.
|
||||||
|
|
||||||
|
:param data: Response data to return from the request
|
||||||
|
:param code: Response code to return; defaults to `200: Ok <https://httpstatuses.com/200>`_
|
||||||
|
:param headers: Additional headers to return with the request; the default headers will
|
||||||
|
be added automatically and do not need to be passed.
|
||||||
|
:returns: Response tuple ready to be returned out of a resource method
|
||||||
|
|
||||||
|
.. note:: This function will handle pagination and header assembly internally. The response
|
||||||
|
data passed to the ``data`` parameter should be unpaginated.
|
||||||
|
"""
|
||||||
|
|
||||||
|
headers = headers or {}
|
||||||
|
headers = {**headers, **flask.request.make_response_headers()}
|
||||||
|
|
||||||
|
# 204 code specifies that it must never include a response body. Most clients will ignore
|
||||||
|
# any response body when a 204 is given, but that's no reason to abandon best practices here
|
||||||
|
# on the server side
|
||||||
|
# https://developer.mozilla.org/en-US/docs/Web/HTTP/Status/204
|
||||||
|
return ResponseTuple(
|
||||||
|
body=data if code != 204 else None, code=code, headers=headers
|
||||||
|
)
|
@ -1,26 +1,20 @@
|
|||||||
import flask_restful
|
from imagemonk.resources._shared import ImageMonkResource
|
||||||
|
|
||||||
|
|
||||||
class ImageUpload(flask_restful.Resource):
|
class ImageUpload(ImageMonkResource):
|
||||||
|
|
||||||
route = "/image/"
|
route = ("/image/",)
|
||||||
|
|
||||||
def put(self):
|
def post(self):
|
||||||
raise NotImplementedError
|
|
||||||
|
|
||||||
def options(self):
|
|
||||||
raise NotImplementedError
|
raise NotImplementedError
|
||||||
|
|
||||||
|
|
||||||
class Image(flask_restful.Resource):
|
class Image(ImageMonkResource):
|
||||||
|
|
||||||
route = "/image/<string:image_id>.jpg"
|
route = ("/image/<string:image_id>.jpg",)
|
||||||
|
|
||||||
def get(self, image_id: str):
|
def get(self, image_id: str):
|
||||||
raise NotImplementedError
|
raise NotImplementedError
|
||||||
|
|
||||||
def delete(self, image_id: str):
|
def delete(self, image_id: str):
|
||||||
raise NotImplementedError
|
raise NotImplementedError
|
||||||
|
|
||||||
def options(self, image_id: str):
|
|
||||||
raise NotImplementedError
|
|
||||||
|
@ -1,12 +1,16 @@
|
|||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
|
|
||||||
import flask_restful
|
|
||||||
from ruamel.yaml import YAML
|
from ruamel.yaml import YAML
|
||||||
|
|
||||||
|
from imagemonk.resources._shared import ImageMonkResource
|
||||||
|
|
||||||
yaml = YAML(typ="safe")
|
yaml = YAML(typ="safe")
|
||||||
|
|
||||||
|
|
||||||
class OpenAPI(flask_restful.Resource):
|
class OpenAPI(ImageMonkResource):
|
||||||
|
|
||||||
|
routes = ("/openapi.json",)
|
||||||
|
|
||||||
def get(self):
|
def get(self):
|
||||||
|
|
||||||
with (Path(__file__).parent, "openapi.yaml").open() as infile:
|
with (Path(__file__).parent, "openapi.yaml").open() as infile:
|
||||||
|
@ -1,17 +1,17 @@
|
|||||||
import flask_restful
|
from imagemonk.resources._shared import ImageMonkResource
|
||||||
|
|
||||||
|
|
||||||
class ThumbnailScale(flask_restful.Resource):
|
class ThumbnailScale(ImageMonkResource):
|
||||||
|
|
||||||
route = "/thumb/<string:image_id>/scale/<integer:scale_width>.jpg"
|
routes = ("/thumb/<string:image_id>/scale/<int:scale_width>.jpg",)
|
||||||
|
|
||||||
def get(self, image_id: str, scale_width: int):
|
def get(self, image_id: str, scale_width: int):
|
||||||
raise NotImplementedError
|
raise NotImplementedError
|
||||||
|
|
||||||
|
|
||||||
class ThumbnailResize(flask_restful.Resource):
|
class ThumbnailResize(ImageMonkResource):
|
||||||
|
|
||||||
route = "/thumb/<string:image_id>/size/<integer:width>x<integer:height>.jpg"
|
routes = ("/thumb/<string:image_id>/size/<int:width>x<int:height>.jpg",)
|
||||||
|
|
||||||
def get(self, image_id: str, width: int, height: int):
|
def get(self, image_id: str, width: int, height: int):
|
||||||
raise NotImplementedError
|
raise NotImplementedError
|
||||||
|
Loading…
Reference in New Issue
Block a user