Upgrade to Pro — share decks privately, control downloads, hide ads and more …

Построение API с помощью спецификации JSON:API на Python

Построение API с помощью спецификации JSON:API на Python

Сурен Хоренян (Руководитель группы разработки VSaaS платформы, МТС AI) @ Moscow Python 77

"Спецификация JSON:API позволяет нам строго определить, как ресурсы должны вести себя, что должны и могут делать, а чего им делать нельзя. Спецификация необходима для унификации интерфейса. Благодаря строгим рамкам мы получаем универсальный интерфейс, который можем применять в различных проектах. Различные серверные реализации JSON:API позволяют нам абстрагироваться от слоёв работы с данными, а также сериализации / десериализации".

Видео: https://moscowpython.ru/meetup/77/api-json-python/

Moscow Python Meetup
PRO

June 09, 2022
Tweet

More Decks by Moscow Python Meetup

Other Decks in Programming

Transcript

  1. None
  2. None
  3. None
  4. None
  5. Run requests curl http://localhost:5000/todo1 -d "data=Remember the milk" –X PUT

    {"todo1": "Remember the milk"} curl http://localhost:5000/todo1 {"todo1": "Remember the milk"} curl http://localhost:5000/todo2 -d "data=Change my brakepads" –X PUT {"todo2": "Change my brakepads"} curl http://localhost:5000/todo2 {"todo2": "Change my brakepads"} 1 from flask import Flask, request 2 from flask_restful import Resource, Api 3 4 app = Flask(__name__) 5 api = Api(app) 6 7 todos = {} 8 9 class TodoSimple(Resource): 10 def get(self, todo_id): 11 return {todo_id: todos[todo_id]} 12 13 def put(self, todo_id): 14 todos[todo_id] = request.form["data"] 15 return {todo_id: todos[todo_id]} 16 17 api.add_resource(TodoSimple, "/<string:todo_id>") 18 19 if __name__ == "__main__": 20 app.run(debug=True) 21
  6. None
  7. 1 import flask 2 import flask_sqlalchemy 3 import flask_restless 4

    5 # Create the Flask application and the Flask-SQLAlchemy object. 6 app = flask.Flask(__name__) 7 app.config['DEBUG'] = True 8 app.config['SQLALCHEMY_DATABASE_URI'] = 'sqlite:////tmp/test.db' 9 db = flask_sqlalchemy.SQLAlchemy(app) 10 11 12 class Person(db.Model): 13 id = db.Column(db.Integer, primary_key=True) 14 name = db.Column(db.Unicode) 15 birth_date = db.Column(db.Date) 16 17 18 class Article(db.Model): 19 id = db.Column(db.Integer, primary_key=True) 20 title = db.Column(db.Unicode) 21 published_at = db.Column(db.DateTime) 22 author_id = db.Column(db.Integer, db.ForeignKey('person.id')) 23 author = db.relationship(Person, backref=db.backref('articles', lazy='dynamic')) 24
  8. 26 # Create the database tables. 27 db.create_all() 28 29

    # Create the Flask-Restless API manager. 30 manager = flask_restless.APIManager(app, flask_sqlalchemy_db=db) 31 32 # Create API endpoints, which will be available at /api/<tablename> by 33 # default. Allowed HTTP methods can be specified as well. 34 manager.create_api(Person, methods=['GET', 'POST', 'DELETE']) 35 manager.create_api(Article, methods=['GET']) 36 37 # start the flask loop 38 app.run()
  9. None
  10. None
  11. None
  12. • • • •

  13. None
  14. 1 from flask import Flask 2 from flask_combo_jsonapi import Api,

    ResourceDetail, ResourceList 3 from flask_sqlalchemy import SQLAlchemy 4 from marshmallow_jsonapi.flask import Schema 5 from marshmallow_jsonapi import fields 6 7 # Create the Flask application and the Flask-SQLAlchemy object. 8 app = Flask(__name__) 9 app.config['DEBUG'] = True 10 app.config['SQLALCHEMY_DATABASE_URI'] = 'sqlite:////tmp/api_minimal.db' 11 db = SQLAlchemy(app) 12 13 # Create the API object 14 api = Api(app)
  15. 16 # Create model 17 class Person(db.Model): 18 id =

    db.Column(db.Integer, primary_key=True) 19 name = db.Column(db.String) 20 21 22 # Create schema 23 class PersonSchema(Schema): 24 class Meta: 25 type_ = 'person' 26 self_view = 'person_detail' 27 self_view_kwargs = {'id': '<id>'} 28 self_view_many = 'person_list' 29 30 id = fields.Integer(as_string=True, dump_only=True) 31 name = fields.String()
  16. 34 # Create resource managers 35 class PersonList(ResourceList): 36 schema

    = PersonSchema 37 data_layer = { 38 'session': db.session, 39 'model': Person, 40 } 41 42 43 class PersonDetail(ResourceDetail): 44 schema = PersonSchema 45 data_layer = { 46 'session': db.session, 47 'model': Person, 48 } 49 50 51 # Register resources 52 api.route(PersonList, 'person_list', '/persons') 53 api.route(PersonDetail, 'person_detail', '/persons/<int:id>')
  17. 53 api.route(PersonList, 'person_list', '/persons') 54 api.route(PersonDetail, 'person_detail', '/persons/<int:id>')

  18. POST /persons HTTP/1.1 Content-Type: application/vnd.api+json { "data": { "type": "person",

    "attributes": { "name": "John" } } } HTTP/1.1 201 Created Content-Type: application/vnd.api+json { "data": { "attributes": { "name": "John" }, "id": "1", "links": { "self": "/persons/1" }, "type": "person" }, "jsonapi": { "version": "1.0" }, "links": { "self": "/persons/1" } }
  19. GET /persons/1 HTTP/1.1 Content-Type: application/vnd.api+json HTTP/1.1 200 OK Content-Type: application/vnd.api+json

    { "data": { "attributes": { "name": "John" }, "id": "1", "links": { "self": "/persons/1" }, "type": "person" }, "jsonapi": { "version": "1.0" }, "links": { "self": "/persons/1" } }
  20. GET /persons HTTP/1.1 Content-Type: application/vnd.api+json HTTP/1.1 200 OK Content-Type: application/vnd.api+json

    { "data": [ { "attributes": { "name": "John" }, "id": "1", "links": { "self": "/persons/1" }, "type": "person" } ], "jsonapi": { "version": "1.0" }, "links": { "self": "http://localhost:5000/persons" }, "meta": { "count": 1 } }
  21. PATCH /persons/1 HTTP/1.1 Content-Type: application/vnd.api+json { "data": { "id": 1,

    "type": "person", "attributes": { "name": "Sam" } } } HTTP/1.1 200 OK Content-Type: application/vnd.api+json { "data": { "attributes": { "name": "Sam" }, "id": "1", "links": { "self": "/persons/1" }, "type": "person" }, "jsonapi": { "version": "1.0" }, "links": { "self": "/persons/1" } }
  22. DELETE /persons/1 HTTP/1.1 Content-Type: application/vnd.api+json HTTP/1.1 200 OK Content-Type: application/vnd.api+json

    { "jsonapi": { "version": "1.0" }, "meta": { "message": "Object successfully deleted" } }
  23. class Person(db.Model): id = db.Column(db.Integer, primary_key=True) first_name = db.Column(db.String) last_name

    = db.Column(db.String) email = db.Column(db.String) password = db.Column(db.String) @property def full_name(self): return f"{self.first_name} {self.last_name}" class Computer(db.Model): id = db.Column(db.Integer, primary_key=True) serial = db.Column(db.String) person_id = db.Column(db.Integer, db.ForeignKey("person.id")) person = db.relationship("Person", backref=db.backref("computers"))
  24. class PersonSchema(Schema): class Meta: type_ = "person" self_view = "person_detail"

    self_view_kwargs = {"id": "<id>"} self_view_many = "person_list" id = fields.Integer(as_string=True, dump_only=True) first_name = fields.String(required=True) last_name = fields.String(required=True) full_name = fields.String(required=True, dump_only=True) email = fields.Email() computers = Relationship( nested="ComputerSchema", schema="ComputerSchema", self_view="person_detail", self_view_kwargs={"id": "<id>"}, many=True, type_="computer", )
  25. class ComputerSchema(Schema): class Meta: type_ = "computer" self_view = "computer_detail"

    self_view_kwargs = {"id": "<id>"} self_view_many = "computer_list" id = fields.Integer(as_string=True, dump_only=True) serial = fields.String(required=True) owner = Relationship( nested="PersonSchema", schema="PersonSchema", attribute="person", self_view="computer_detail", self_view_kwargs={"id": "<id>"}, type_="person", )
  26. POST /persons HTTP/1.1 Content-Type: application/vnd.api+json { "data": { "type": "person",

    "attributes": { "first_name": "John", "last_name": "Smith", "email": "[email protected]" } } } HTTP/1.1 201 Created Content-Type: application/vnd.api+json { "data": { "attributes": { "email": "[email protected]", "first_name": "John", "last_name": "Smith" }, "id": "1", "links": { "self": "/persons/1" }, "type": "person" }, "jsonapi": { "version": "1.0" }, "links": { "self": "/persons/1" } }
  27. HTTP/1.1 200 OK Content-Type: application/vnd.api+json { "data": [ { "attributes":

    { "email": "[email protected]", "full_name": "John Smith" }, "id": "1", "links": { "self": "/persons/1" }, "type": "person" } ], "jsonapi": { "version": "1.0" }, "links": { "self": "http://localhost:5000/persons?fields%5Bperson%5D=full_name%2Cemail" }, "meta": { "count": 1 } } GET /persons?fields[person]=full_name,email HTTP/1.1 Content-Type: application/vnd.api+json
  28. POST /computers HTTP/1.1 Content-Type: application/vnd.api+json { "data": { "type": "computer",

    "attributes": { "serial": "Amstrad" } } } HTTP/1.1 201 Created Content-Type: application/vnd.api+json { "data": { "attributes": { "serial": "Amstrad" }, "id": "1", "links": { "self": "/computers/1" }, "type": "computer" }, "jsonapi": { "version": "1.0" }, "links": { "self": "/computers/1" } }
  29. PATCH /persons/1?include=computers HTTP/1.1 Content-Type: application/vnd.api+json { "data": { "type": "person",

    "id": "1", "attributes": { "email": "[email protected]" }, "relationships": { "computers": { "data": [ { "type": "computer", "id": "1" } ] } } } } HTTP/1.1 200 OK Content-Type: application/vnd.api+json { "data": { "attributes": { "email": "[email protected]", "first_name": "John", "full_name": "John Smith", "last_name": "Smith" }, "id": "1", "links": { "self": "/persons/1" }, "relationships": { "computers": { "data": [ { "id": "1", "type": "computer" } ] } }, "type": "person" }, "included": [ { "attributes": { "serial": "Amstrad" }, "id": "1", "links": { "self": "/computers/1" }, "type": "computer" } ], "jsonapi": { "version": "1.0" }, "links": { "self": "/persons/1" } }
  30. GET /computers?include=owner&fields[person]=full_name,email HTTP/1.1 Content-Type: application/vnd.api+json HTTP/1.1 200 OK Content-Type: application/vnd.api+json

    { "data": [ { "attributes": { "serial": "Amstrad" }, "id": "1", "links": { "self": "/computers/1" }, "relationships": { "owner": { "data": { "id": "1", "type": "person" } } }, "type": "computer" } ], "included": [ { "attributes": { "email": "[email protected]", "full_name": "John Smith" }, "id": "1", "links": { "self": "/persons/1" }, "type": "person" } ], "jsonapi": { "version": "1.0" }, "links": { "self": "http://localhost:5000/computers?inc lude=owner&fields%5Bperson%5D=email% 2Cfull_name" }, "meta": { "count": 1 } }
  31. None
  32. ‣ ‣ ‣ ‣ ‣ ‣ ‣ ‣ ‣ ‣

    ‣ ‣ ‣ ‣ ‣ ‣ ‣ from flask_combo_jsonapi import Api ... api = Api( app, plugins=[ api_spec_plugin, permission_plugin, event_plugin, my_custom_middleleware_plugin, ], )
  33. from combojsonapi.spec import ApiSpecPlugin ... api_spec_plugin = ApiSpecPlugin( app=app, tags={

    "Person" : "Person API", "Computer" : "Computer API", }, )
  34. # swagger / openapi config app.config["OPENAPI_URL_PREFIX"] = "/api/swagger" app.config["OPENAPI_VERSION"] =

    "3.0.0" app.config["OPENAPI_SWAGGER_UI_PATH"] = "/" app.config["OPENAPI_SWAGGER_UI_VERSION"] = "3.45.0"
  35. None
  36. None
  37. None
  38. { "data": [ { "type": "person", "relationships": { "computers": {

    "data": [ { "type": "computer", "id": "1", } ] } }, "id": "1", "attributes": { "full_name": "John Smith" }, "links": { "self": "/persons/1" }, } ], "links": { "self": "http://localhost:5000/computers?include=owner&fields%5Bperson%5D=email%2Cfull_name" }, "included": [ { "type": "computer" "id": "1", "attributes": { "serial": "Amstrad" }, "links": { "self": "/computers/1" } } ], "meta": { "count": 1 }, "jsonapi": { "version": "1.0" } } curl -X 'GET' \ 'http://127.0.0.1:5000/ persons? include=computers &fields[person ]=id,full_name,computers &fields[computer ]=id,serial &page[number]=1 &page[size]=10'
  39. class BaseAllowAllFieldsPermission(PermissionMixin): ALL_FIELDS = [] def get( self, *args, many=True,

    user_permission: PermissionUser = None, **kwargs ) -> PermissionForGet: """Setting all available columns""" self.permission_for_get.allow_columns = (self.ALL_FIELDS, 10) return self.permission_for_get class PersonsPermission(BaseAllowAllFieldsPermission): ALL_FIELDS = [ "id", "first_name", "last_name", "full_name", "email", "computers", ] class ComputersPermission(BaseAllowAllFieldsPermission): ALL_FIELDS = [ "id", "serial", "person", "owner", ]
  40. class PersonList(ResourceList): schema = PersonSchema data_layer = { "session": db.session,

    "model": Person, "permission_get": [PersonsPermission], } class PersonDetail(ResourceDetail): schema = PersonSchema data_layer = { "session": db.session, "model": Person, "permission_get": [PersonsPermission], } class ComputerList(ResourceList): schema = ComputerSchema data_layer = { "session": db.session, "model": Computer, "permission_get": [ComputersPermission], } class ComputerDetail(ResourceDetail): schema = ComputerSchema data_layer = { "session": db.session, "model": Computer, "permission_get": [ComputersPermission], }
  41. api = Api( app, plugins=[ api_spec_plugin, PermissionPlugin(strict=False), ], )

  42. class Person(db.Model): class Meta: required_fields = { "full_name": ["first_name", "last_name"],

    } id = db.Column(db.Integer, primary_key=True) first_name = db.Column(db.String) last_name = db.Column(db.String) email = db.Column(db.String) password = db.Column(db.String) @property def full_name(self): return f"{self.first_name} {self.last_name}"
  43. # Create models class User(db.Model): id = db.Column(db.Integer, primary_key=True) name

    = db.Column(db.String) username = db.Column(db.String) email = db.Column(db.String) avatar_path = db.Column(db.String) class UserSchema(Schema): class Meta: type_ = "user" self_view = "user_detail" self_view_kwargs = {"id": "<id>"} self_view_many = "user_list" id = fields.Integer(as_string=True, dump_only=True) name = fields.String(required=False) user_name = fields.String(required=True) email = fields.String(required=True) avatar_path = fields.String(required=False, dump_only=True) class UserList(ResourceList): schema = UserSchema data_layer = { "session": db.session, "model": User, } class UserDetail(ResourceDetail): schema = UserSchema data_layer = { "session": db.session, "model": User, }
  44. api_spec_plugin = ApiSpecPlugin( app=app, tags={ "User": "User API", }, )

    # Create endpoints api = Api( app, plugins=[ api_spec_plugin, EventPlugin(trailing_slash=False), ], ) api.route(UserList, "user_list", "/users", tag="User") api.route(UserDetail, "user_detail", "/users/<int:id>", tag="User")
  45. class UserResourceListEvents(EventsResource): def event_get_info(self, *args, **kwargs): return {"message": "some info"}

    def event_post_info(self, *args, **kwargs): data = request.json data.update(message="POST request info") return data class UserList(ResourceList): schema = UserSchema events = UserResourceListEvents data_layer = { "session": db.session, "model": User, } class UserDetail(ResourceDetail): schema = UserSchema events = UserResourceDetailEvents data_layer = { "session": db.session, "model": User, }
  46. None
  47. GET /users/event_get_info HTTP/1.1 Content-Type: application/vnd.api+json HTTP/1.1 200 OK Content-Type: application/vnd.api+json

    { "jsonapi": { "versions": "1.0" }, "message": "some info" }
  48. POST /users/event_post_info HTTP/1.1 Content-Type: application/vnd.api+json { "spam": "eggs", "foo": "bar"

    } HTTP/1.1 200 OK Content-Type: application/vnd.api+json { "foo": "bar", "jsonapi": { "version": "1.0" }, "message": "POST request info" "spam": "eggs" }
  49. None
  50. None
  51. def event_update_avatar(self, *args, id = None, **view_kwargs): avatar = request.files.get("new_avatar")

    if not avatar: raise BadRequest( "avatar file is required! " "please fill `new_avatar` form field") user = User.query.filter_by(id=id).one_or_none() if user is None: raise ObjectNotFound( "User #{} not found".format(id), source={"parameter": "id"}, ) filename = avatar.filename avatar.save(str(UPLOADS_DIR / filename)) user.avatar_path = str(UPLOADS_DIR_NAME / filename) db.session.commit() return {"avatar_url": user.avatar_path}, 201 event_update_avatar.extra = { "url_suffix": "update_avatar", }
  52. class UserList(ResourceList): schema = UserSchema events = UserResourceListEvents data_layer =

    { "session": db.session, "model": User, } class UserDetail(ResourceDetail): schema = UserSchema events = UserResourceDetailEvents data_layer = { "session": db.session, "model": User, }
  53. POST /users/1/update_avatar HTTP/1.1 Content-Type: multipart/form-data; boundary=WebKitFormBoundaryQwe ------WebKitFormBoundaryQwe Content-Disposition: form-data; name="new_avatar";

    filename="my-avatar.gif" Content-Type: image/gif GIF89a ! , L; ------WebKitFormBoundaryQwe-- HTTP/1.1 201 Content-Type: application/json, application/vnd.api+json { "avatar_url": "uploads/my-avatar.gif", "jsonapi": { "version": "1.0" } }
  54. None
  55. https://ru.wikipedia.org/wiki/REST https://ru.wikipedia.org/wiki/SOAP https://ru.wikipedia.org/wiki/HTTP https://medium.com/@andr.ivas12/rest-простым-языком-90a0bca0bc78 https://ru.wikipedia.org/wiki/XMLHttpRequest https://flask-combo-jsonapi.readthedocs.io/ https://combojsonapi.readthedocs.io/ Филдинг, Рой https://ru.wikipedia.org/wiki/Филдинг,_Рой