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

June 09, 2022
Tweet

More Decks by Moscow Python Meetup

Other Decks in Programming

Transcript

  1. 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
  2. 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
  3. 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()
  4. 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)
  5. 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()
  6. 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>')
  7. 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" } }
  8. 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" } }
  9. 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 } }
  10. 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" } }
  11. 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" } }
  12. 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"))
  13. 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", )
  14. 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", )
  15. 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" } }
  16. 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
  17. 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" } }
  18. 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" } }
  19. 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 } }
  20. ‣ ‣ ‣ ‣ ‣ ‣ ‣ ‣ ‣ ‣

    ‣ ‣ ‣ ‣ ‣ ‣ ‣ from flask_combo_jsonapi import Api ... api = Api( app, plugins=[ api_spec_plugin, permission_plugin, event_plugin, my_custom_middleleware_plugin, ], )
  21. # 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"
  22. { "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'
  23. 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", ]
  24. 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], }
  25. 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}"
  26. # 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, }
  27. 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")
  28. 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, }
  29. 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" }
  30. 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", }
  31. 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, }
  32. 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" } }