-
Notifications
You must be signed in to change notification settings - Fork 4
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Merge pull request #180 from algoo/fix/159__better_example_user_manag…
…ment Fix/159 better example user managment
- Loading branch information
Showing
14 changed files
with
1,050 additions
and
52 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,59 @@ | ||
# -*- coding: utf-8 -*- | ||
import dataclasses | ||
from datetime import datetime | ||
import typing | ||
|
||
from serpyco import StringFormat | ||
from serpyco import number_field | ||
from serpyco import string_field | ||
|
||
|
||
@dataclasses.dataclass | ||
class NoContentSchema(object): | ||
"""A docstring to prevent auto generated docstring""" | ||
|
||
|
||
@dataclasses.dataclass | ||
class AboutSchema(object): | ||
""" Representation of the /about route """ | ||
|
||
version: str | ||
datetime: datetime | ||
|
||
|
||
@dataclasses.dataclass | ||
class UserIdPathSchema(object): | ||
""" | ||
representation of a user id in the uri. This allow to define rules for | ||
what is expected. For example, you may want to limit id to number between | ||
1 and 999 | ||
""" | ||
|
||
id: int = number_field(minimum=1, cast_on_load=True) | ||
|
||
|
||
@dataclasses.dataclass | ||
class UserSchema(object): | ||
"""Complete representation of a user""" | ||
|
||
first_name: str | ||
last_name: typing.Optional[str] | ||
display_name: str | ||
company: typing.Optional[str] | ||
id: int | ||
email_address: str = string_field(format_=StringFormat.EMAIL) | ||
|
||
|
||
@dataclasses.dataclass | ||
class UserDigestSchema(object): | ||
"""User representation for listing""" | ||
|
||
id: int | ||
display_name: str = "" | ||
|
||
|
||
@dataclasses.dataclass | ||
class UserAvatarSchema(object): | ||
"""Avatar (image file) of user""" | ||
|
||
avatar: typing.Any |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,149 @@ | ||
# -*- coding: utf-8 -*- | ||
|
||
from datetime import datetime | ||
import json | ||
import time | ||
|
||
from aiohttp import web | ||
from aiohttp.web_request import Request | ||
|
||
from example.usermanagement.schema_marshmallow import AboutSchema | ||
from example.usermanagement.schema_marshmallow import NoContentSchema | ||
from example.usermanagement.schema_marshmallow import UserAvatarSchema | ||
from example.usermanagement.schema_marshmallow import UserDigestSchema | ||
from example.usermanagement.schema_marshmallow import UserIdPathSchema | ||
from example.usermanagement.schema_marshmallow import UserSchema | ||
from example.usermanagement.userlib import User | ||
from example.usermanagement.userlib import UserAvatarNotFound | ||
from example.usermanagement.userlib import UserLib | ||
from example.usermanagement.userlib import UserNotFound | ||
from hapic import Hapic | ||
from hapic.data import HapicData | ||
from hapic.data import HapicFile | ||
from hapic.error.marshmallow import MarshmallowDefaultErrorBuilder | ||
from hapic.ext.aiohttp.context import AiohttpContext | ||
from hapic.processor.marshmallow import MarshmallowProcessor | ||
|
||
try: # Python 3.5+ | ||
from http import HTTPStatus | ||
except ImportError: | ||
from http import client as HTTPStatus | ||
|
||
|
||
hapic = Hapic(async_=True) | ||
hapic.set_processor_class(MarshmallowProcessor) | ||
|
||
|
||
class AiohttpController(object): | ||
@hapic.with_api_doc() | ||
@hapic.output_body(AboutSchema()) | ||
async def about(self, request: Request): | ||
""" | ||
This endpoint allow to check that the API is running. This description | ||
is generated from the docstring of the method. | ||
""" | ||
return {"version": "1.2.3", "datetime": datetime.now()} | ||
|
||
@hapic.with_api_doc() | ||
@hapic.output_body(UserDigestSchema(many=True)) | ||
async def get_users(self, request: Request): | ||
""" | ||
Obtain users list. | ||
""" | ||
return UserLib().get_users() | ||
|
||
@hapic.with_api_doc() | ||
@hapic.handle_exception(UserNotFound, HTTPStatus.NOT_FOUND) | ||
@hapic.input_path(UserIdPathSchema()) | ||
@hapic.output_body(UserSchema()) | ||
async def get_user(self, request: Request, hapic_data: HapicData): | ||
""" | ||
Return a user taken from the list or return a 404 | ||
""" | ||
return UserLib().get_user(int(hapic_data.path["id"])) | ||
|
||
@hapic.with_api_doc() | ||
# TODO - G.M - 2017-12-5 - Support input_forms ? | ||
# TODO - G.M - 2017-12-5 - Support exclude, only ? | ||
@hapic.input_body(UserSchema(exclude=("id",))) | ||
@hapic.output_body(UserSchema()) | ||
async def add_user(self, request: Request, hapic_data: HapicData): | ||
""" | ||
Add a user to the list | ||
""" | ||
new_user = User(**hapic_data.body) | ||
return UserLib().add_user(new_user) | ||
|
||
@hapic.with_api_doc() | ||
@hapic.handle_exception(UserNotFound, HTTPStatus.NOT_FOUND) | ||
@hapic.output_body(NoContentSchema(), default_http_code=204) | ||
@hapic.input_path(UserIdPathSchema()) | ||
async def del_user(self, request, hapic_data: HapicData): | ||
UserLib().del_user(int(hapic_data.path["id"])) | ||
return NoContentSchema() | ||
|
||
@hapic.with_api_doc() | ||
@hapic.handle_exception(UserNotFound, HTTPStatus.NOT_FOUND) | ||
@hapic.handle_exception(UserAvatarNotFound, HTTPStatus.NOT_FOUND) | ||
@hapic.input_path(UserIdPathSchema()) | ||
@hapic.output_file(["image/png"]) | ||
async def get_user_avatar(self, request: Request, hapic_data: HapicData): | ||
return HapicFile( | ||
file_path=UserLib().get_user_avatar_path(user_id=(int(hapic_data.path["id"]))) | ||
) | ||
|
||
@hapic.with_api_doc() | ||
@hapic.handle_exception(UserNotFound, HTTPStatus.NOT_FOUND) | ||
@hapic.handle_exception(UserAvatarNotFound, HTTPStatus.BAD_REQUEST) | ||
@hapic.input_path(UserIdPathSchema()) | ||
@hapic.input_files(UserAvatarSchema()) | ||
@hapic.output_body(NoContentSchema(), default_http_code=204) | ||
async def update_user_avatar(self, request: Request, hapic_data: HapicData): | ||
UserLib().update_user_avatar( | ||
user_id=int(hapic_data.path["id"]), avatar=hapic_data.files["avatar"] | ||
) | ||
|
||
def bind(self, app: web.Application): | ||
app.add_routes( | ||
[ | ||
web.get("/about", self.about), | ||
web.get("/users/", self.get_users), | ||
web.get(r"/users/{id}", self.get_user), | ||
web.post("/users/", self.add_user), | ||
web.delete("/users/{id}", self.del_user), | ||
web.get("/users/{id}/avatar", self.get_user_avatar), | ||
web.put("/users/{id}/avatar", self.update_user_avatar), | ||
] | ||
) | ||
|
||
|
||
if __name__ == "__main__": | ||
app = web.Application() | ||
controllers = AiohttpController() | ||
controllers.bind(app) | ||
hapic.set_context(AiohttpContext(app, default_error_builder=MarshmallowDefaultErrorBuilder())) | ||
doc_title = "Demo API documentation" | ||
doc_description = ( | ||
"This documentation has been generated from " | ||
"code. You can see it using swagger: " | ||
"http://editor2.swagger.io/" | ||
) | ||
hapic.add_documentation_view("/doc/", doc_title, doc_description) | ||
print("") | ||
print("") | ||
print("GENERATING OPENAPI DOCUMENTATION") | ||
openapi_file_name = "api-documentation.json" | ||
with open(openapi_file_name, "w") as openapi_file_handle: | ||
openapi_file_handle.write( | ||
json.dumps(hapic.generate_doc(title=doc_title, description=doc_description)) | ||
) | ||
|
||
print("Documentation generated in {}".format(openapi_file_name)) | ||
time.sleep(1) | ||
|
||
print("") | ||
print("") | ||
print("RUNNING AIOHTTP SERVER NOW") | ||
print("DOCUMENTATION AVAILABLE AT /doc/") | ||
# Run app | ||
web.run_app(app=app, host="127.0.0.1", port=8084) |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,155 @@ | ||
# -*- coding: utf-8 -*- | ||
|
||
from datetime import datetime | ||
import json | ||
import time | ||
|
||
from aiohttp import web | ||
from aiohttp.web_request import Request | ||
|
||
from example.usermanagement.schema_serpyco import AboutSchema | ||
from example.usermanagement.schema_serpyco import NoContentSchema | ||
from example.usermanagement.schema_serpyco import UserAvatarSchema | ||
from example.usermanagement.schema_serpyco import UserDigestSchema | ||
from example.usermanagement.schema_serpyco import UserIdPathSchema | ||
from example.usermanagement.schema_serpyco import UserSchema | ||
from example.usermanagement.userlib import User | ||
from example.usermanagement.userlib import UserAvatarNotFound | ||
from example.usermanagement.userlib import UserLib | ||
from example.usermanagement.userlib import UserNotFound | ||
from hapic import Hapic | ||
from hapic.data import HapicData | ||
from hapic.data import HapicFile | ||
from hapic.error.serpyco import SerpycoDefaultErrorBuilder | ||
from hapic.ext.aiohttp.context import AiohttpContext | ||
from hapic.processor.serpyco import SerpycoProcessor | ||
|
||
try: # Python 3.5+ | ||
from http import HTTPStatus | ||
except ImportError: | ||
from http import client as HTTPStatus | ||
|
||
|
||
hapic = Hapic(async_=True) | ||
hapic.set_processor_class(SerpycoProcessor) | ||
|
||
|
||
class DictLikeObject(dict): | ||
def __getattr__(self, item): | ||
return self[item] | ||
|
||
|
||
class AiohttpController(object): | ||
@hapic.with_api_doc() | ||
@hapic.output_body(AboutSchema) | ||
async def about(self, request: Request): | ||
""" | ||
This endpoint allow to check that the API is running. This description | ||
is generated from the docstring of the method. | ||
""" | ||
|
||
return DictLikeObject({"version": "1.2.3", "datetime": datetime.now()}) | ||
|
||
@hapic.with_api_doc() | ||
@hapic.output_body(UserDigestSchema, processor=SerpycoProcessor(many=True)) | ||
async def get_users(self, request: Request): | ||
""" | ||
Obtain users list. | ||
""" | ||
return UserLib().get_users() | ||
|
||
@hapic.with_api_doc() | ||
@hapic.handle_exception(UserNotFound, HTTPStatus.NOT_FOUND) | ||
@hapic.input_path(UserIdPathSchema) | ||
@hapic.output_body(UserSchema) | ||
async def get_user(self, request: Request, hapic_data: HapicData): | ||
""" | ||
Return a user taken from the list or return a 404 | ||
""" | ||
return UserLib().get_user(int(hapic_data.path.id)) | ||
|
||
@hapic.with_api_doc() | ||
# TODO - G.M - 2017-12-5 - Support input_forms ? | ||
# TODO - G.M - 2017-12-5 - Support exclude, only ? | ||
@hapic.input_body(UserSchema, processor=SerpycoProcessor(exclude=["id"])) | ||
@hapic.output_body(UserSchema) | ||
async def add_user(self, request: Request, hapic_data: HapicData): | ||
""" | ||
Add a user to the list | ||
""" | ||
new_user = User(**hapic_data.body) | ||
return UserLib().add_user(new_user) | ||
|
||
@hapic.with_api_doc() | ||
@hapic.handle_exception(UserNotFound, HTTPStatus.NOT_FOUND) | ||
@hapic.output_body(NoContentSchema, default_http_code=204) | ||
@hapic.input_path(UserIdPathSchema) | ||
async def del_user(self, request: Request, hapic_data: HapicData): | ||
UserLib().del_user(int(hapic_data.path.id)) | ||
return NoContentSchema() | ||
|
||
@hapic.with_api_doc() | ||
@hapic.handle_exception(UserNotFound, HTTPStatus.NOT_FOUND) | ||
@hapic.handle_exception(UserAvatarNotFound, HTTPStatus.NOT_FOUND) | ||
@hapic.input_path(UserIdPathSchema) | ||
@hapic.output_file(["image/png"]) | ||
async def get_user_avatar(self, request: Request, hapic_data: HapicData): | ||
return HapicFile( | ||
file_path=UserLib().get_user_avatar_path(user_id=(int(hapic_data.path.id))) | ||
) | ||
|
||
@hapic.with_api_doc() | ||
@hapic.handle_exception(UserNotFound, HTTPStatus.NOT_FOUND) | ||
@hapic.handle_exception(UserAvatarNotFound, HTTPStatus.BAD_REQUEST) | ||
@hapic.input_path(UserIdPathSchema) | ||
@hapic.input_files(UserAvatarSchema) | ||
@hapic.output_body(NoContentSchema, default_http_code=204) | ||
async def update_user_avatar(self, request, hapic_data: HapicData): | ||
UserLib().update_user_avatar( | ||
user_id=int(hapic_data.path.id), avatar=hapic_data.files.avatar | ||
) | ||
|
||
def bind(self, app: web.Application): | ||
app.add_routes( | ||
[ | ||
web.get("/about", self.about), | ||
web.get("/users/", self.get_users), | ||
web.get(r"/users/{id}", self.get_user), | ||
web.post("/users/", self.add_user), | ||
web.delete("/users/{id}", self.del_user), | ||
web.get("/users/{id}/avatar", self.get_user_avatar), | ||
web.put("/users/{id}/avatar", self.update_user_avatar), | ||
] | ||
) | ||
|
||
|
||
if __name__ == "__main__": | ||
app = web.Application() | ||
controllers = AiohttpController() | ||
controllers.bind(app) | ||
hapic.set_context(AiohttpContext(app, default_error_builder=SerpycoDefaultErrorBuilder())) | ||
doc_title = "Demo API documentation" | ||
doc_description = ( | ||
"This documentation has been generated from " | ||
"code. You can see it using swagger: " | ||
"http://editor2.swagger.io/" | ||
) | ||
hapic.add_documentation_view("/doc/", doc_title, doc_description) | ||
print("") | ||
print("") | ||
print("GENERATING OPENAPI DOCUMENTATION") | ||
openapi_file_name = "api-documentation.json" | ||
with open(openapi_file_name, "w") as openapi_file_handle: | ||
openapi_file_handle.write( | ||
json.dumps(hapic.generate_doc(title=doc_title, description=doc_description)) | ||
) | ||
|
||
print("Documentation generated in {}".format(openapi_file_name)) | ||
time.sleep(1) | ||
|
||
print("") | ||
print("") | ||
print("RUNNING AIOHTTP SERVER NOW") | ||
print("DOCUMENTATION AVAILABLE AT /doc/") | ||
# Run app | ||
web.run_app(app=app, host="127.0.0.1", port=8084) |
Oops, something went wrong.