API

API

This topic with the library you mean, [api-design], or something else appropriate instead.
Duyen Hoang

Duyen Hoang

1656757682

Cách Dễ Dàng Để Tạo Một API RESTful Với Flask Và APIFairy

Hướng dẫn này trình bày cách dễ dàng tạo một RESTful API với Flask và APIFairy.

Mục tiêu

Đến cuối hướng dẫn này, bạn sẽ có thể:

  1. Tạo điểm cuối API trong Flask bằng cách sử dụng bộ trang trí do APIFairy cung cấp
  2. Sử dụng Flask-Marshmallow để xác định các lược đồ cho đầu vào / đầu ra cho các điểm cuối API
  3. Tạo tài liệu API bằng APIFairy
  4. Tích hợp cơ sở dữ liệu quan hệ với các điểm cuối API
  5. Triển khai xác thực mã thông báo và cơ bản bằng Flask-HTTPAuth

APIFairy là gì?

APIFairy là một khung API được viết bởi Miguel Grinberg cho phép dễ dàng tạo một API với Flask.

APIFairy cung cấp bốn thành phần chính để dễ dàng tạo API trong Flask:

  1. Trang trí
  2. Lược đồ
  3. Xác thực
  4. Tài liệu

Hãy cùng khám phá chi tiết từng cái ...

Trang trí

APIFairy cung cấp một tập hợp các trình trang trí để xác định các đầu vào, đầu ra và xác thực cho mỗi điểm cuối API:

APIFairy Decorators

APIFairy cung cấp năm bộ trang trí cốt lõi:

  1. @arguments - chỉ định các đối số đầu vào trong chuỗi truy vấn của URL
  2. @body - chỉ định phần thân JSON đầu vào dưới dạng một lược đồ
  3. @response - chỉ định phần thân JSON đầu ra dưới dạng một lược đồ
  4. @other_responses - chỉ định phản hồi bổ sung (thường là lỗi) có thể được trả lại ( chỉ tài liệu )
  5. @authenticate - chỉ định quy trình xác thực

Lược đồ

Đầu vào (sử dụng trình @bodytrang trí) và đầu ra (sử dụng trình @responsetrang trí) của một điểm cuối API được định nghĩa là các lược đồ:

class EntrySchema(ma.Schema):
    """Schema defining the attributes in a journal entry."""
    id = ma.Integer()
    entry = ma.String()
    user_id = ma.Integer()

Các lược đồ sử dụng marshmallow để xác định các kiểu dữ liệu dưới dạng các lớp.

Xác thực

Trình @authenticatetrang trí được sử dụng để kiểm tra tiêu đề xác thực được cung cấp trong yêu cầu URL tới mỗi điểm cuối API. Lược đồ xác thực được triển khai bằng Flask-HTTPAuth , cũng được tạo bởi Miguel Grinberg.

Cách tiếp cận xác thực API điển hình sẽ là xác định xác thực cơ bản để bảo vệ tuyến đường truy xuất mã thông báo xác thực:

basic_auth = HTTPBasicAuth()

@basic_auth.verify_password
def verify_password(email, password):
    user = User.query.filter_by(email=email).first()
    if user.is_password_correct(password):
        return user

Và cũng để xác định xác thực mã thông báo để bảo vệ phần lớn các tuyến đường dựa trên mã thông báo xác thực nhạy cảm với thời gian:

token_auth = HTTPTokenAuth()

@token_auth.verify_token
def verify_token(auth_token):
    return User.verify_auth_token(auth_token)

Tài liệu

Một trong những tính năng tuyệt vời của APIFairy là tài liệu API tuyệt đẹp được tạo tự động:

Tài liệu API - Trang chính

Tài liệu được tạo dựa trên docstrings trong mã nguồn cùng với các biến cấu hình sau:

  1. APIFAIRY_TITLE- tên của dự án
  2. APIFAIRY_VERSION- chuỗi phiên bản của dự án
  3. APIFAIRY_UI- định dạng của tài liệu API

Đối với APIFAIRY_UI, bạn có thể tạo các mẫu từ một trong các trình kết xuất tài liệu OpenAPI sau:

  1. Giao diện người dùng Swagger
  2. ReDoc
  3. RapiDoc
  4. Các yếu tố

Để có danh sách đầy đủ các biến cấu hình có sẵn, hãy tham khảo Tài liệu cấu hình .

Chúng tôi đang xây dựng cái gì?

Bạn sẽ phát triển một API tạp chí trong hướng dẫn này, cho phép người dùng ghi nhật ký hàng ngày về các sự kiện. Bạn có thể tìm thấy mã nguồn đầy đủ trong kho lưu trữ flask-journal-api trên GitLab.

Các gói Python chính được sử dụng:

  1. Flask : khuôn khổ vi mô để phát triển ứng dụng web Python
  2. APIFairy : Khung API cho Flask, sử dụng-
  3. Flask-SQLAlchemy : ORM (Object Relational Mapper) cho Flask

Bạn sẽ phát triển API dần dần:

  1. Tạo các điểm cuối API để làm việc với các mục nhập nhật ký
  2. Tạo tài liệu API
  3. Thêm cơ sở dữ liệu quan hệ để lưu trữ các mục nhật ký
  4. Thêm xác thực để bảo vệ các điểm cuối API

Điểm cuối API

Hãy bắt đầu tạo API bằng Flask và APIFairy ...

Khởi tạo dự án

Bắt đầu bằng cách tạo một thư mục dự án mới và một môi trường ảo:

$ mkdir flask-journal-api
$ cd flask-journal-api
$ python3 -m venv venv
$ source venv/bin/activate
(venv)$

Hãy trao đổi virtualenv và Pip cho thơ hoặc Pipenv . Để biết thêm, hãy xem lại Môi trường Python hiện đại .

Tiếp tục và thêm các tệp và thư mục sau:

├── app.py
├── instance
│   └── .gitkeep
├── project
│   ├── __init__.py
│   └── journal_api
│       ├── __init__.py
│       └── routes.py
└── requirements.txt

Tiếp theo, để cài đặt các gói Python cần thiết, hãy thêm các phụ thuộc vào tệp tin request.txt trong thư mục gốc của dự án:

apifairy==0.9.1
Flask==2.1.2
Flask-SQLAlchemy==2.5.1
marshmallow-sqlalchemy==0.28.0

Cài đặt:

(venv)$ pip install -r requirements.txt

Dự án Flask này sẽ sử dụng hai phương pháp hay nhất cho các ứng dụng Flask:

  1. Application Factory - được sử dụng để tạo ứng dụng Flask trong một chức năng
  2. Bản thiết kế - được sử dụng để tổ chức một nhóm các chế độ xem liên quan

Nhà máy ứng dụng

Bắt đầu bằng cách xác định chức năng Nhà máy ứng dụng trong dự án / __ init__.py :

from apifairy import APIFairy
from flask import Flask, json
from flask_marshmallow import Marshmallow

# -------------
# Configuration
# -------------

# Create the instances of the Flask extensions in the global scope,
# but without any arguments passed in. These instances are not
# attached to the Flask application at this point.
apifairy = APIFairy()
ma = Marshmallow()


# ----------------------------
# Application Factory Function
# ----------------------------

def create_app():
    # Create the Flask application
    app = Flask(__name__)

    initialize_extensions(app)
    register_blueprints(app)
    return app


# ----------------
# Helper Functions
# ----------------

def initialize_extensions(app):
    # Since the application instance is now created, pass it to each Flask
    # extension instance to bind it to the Flask application instance (app)
    apifairy.init_app(app)
    ma.init_app(app)


def register_blueprints(app):
    # Import the blueprints
    from project.journal_api import journal_api_blueprint

    # Since the application instance is now created, register each Blueprint
    # with the Flask application instance (app)
    app.register_blueprint(journal_api_blueprint, url_prefix='/journal')

With the Application Factory function defined, it can be called in app.py in the top-level folder of the project:

from project import create_app


# Call the application factory function to construct a Flask application
# instance using the development configuration
app = create_app()

Blueprint

Let's define the journal_api blueprint. Start by defining the journal_api blueprint in project/journal_api/__init__.py:

"""
The 'journal_api' blueprint handles the API for managing journal entries.
Specifically, this blueprint allows for journal entries to be added, edited,
and deleted.
"""
from flask import Blueprint

journal_api_blueprint = Blueprint('journal_api', __name__, template_folder='templates')

from . import routes

Now it's time to define the API endpoints for the journal in project/journal_api/routes.py.

Start with the necessary imports:

from apifairy import body, other_responses, response
from flask import abort

from project import ma
from . import journal_api_blueprint

For this initial version of the Flask Journal API, the database will be a list of journal entries:

# --------
# Database
# --------

messages = [
    dict(id=1, entry='The sun was shining when I woke up this morning.'),
    dict(id=2, entry='I tried a new fruit mixture in my oatmeal for breakfast.'),
    dict(id=3, entry='Today I ate a great sandwich for lunch.')
]

Next, define the schemas for creating a new journal entry and for returning the journal entries:

# -------
# Schemas
# -------

class NewEntrySchema(ma.Schema):
    """Schema defining the attributes when creating a new journal entry."""
    entry = ma.String(required=True)


class EntrySchema(ma.Schema):
    """Schema defining the attributes in a journal entry."""
    id = ma.Integer()
    entry = ma.String()


new_entry_schema = NewEntrySchema()
entry_schema = EntrySchema()
entries_schema = EntrySchema(many=True)

Both of these schema classes inherit from ma.Schema, which is provided by Flask-Marshmallow. It's also a good idea to create objects of these schemas, as this allows you to define a schema that can return multiple entries (using the many=True argument).

Now we're ready to define the API endpoints!

Routes

Start with retrieving all the journal entries:

@journal_api_blueprint.route('/', methods=['GET'])
@response(entries_schema)
def journal():
    """Return all journal entries"""
    return messages

This view function uses the @response decorator to define that multiple entries are returned. The view function returns the full list of journal entries (return messages).

Next, create the API endpoint for adding a new journal entry:

@journal_api_blueprint.route('/', methods=['POST'])
@body(new_entry_schema)
@response(entry_schema, 201)
def add_journal_entry(kwargs):
    """Add a new journal entry"""
    new_message = dict(**kwargs, id=messages[-1]['id']+1)
    messages.append(new_message)
    return new_message

This view function uses the @body decorator to define the input to the API endpoint and the @response decorator to define the output from the API endpoint.

The input data that is parsed from the @body decorator is passed into the add_journal_entry() view function as the kwargs (key word arguments) argument. This data is then used to create a new journal entry and add it to the database:

new_message = dict(**kwargs, id=messages[-1]['id']+1)
messages.append(new_message)

The newly created journal entry is then returned (return new_message). Notice how the @response decorator defines the return code as 201 (Created) to indicate that the journal entry was added to the database.

Create the API endpoint for retrieving a specific journal entry:

@journal_api_blueprint.route('/<int:index>', methods=['GET'])
@response(entry_schema)
@other_responses({404: 'Entry not found'})
def get_journal_entry(index):
    """Return a journal entry"""
    if index >= len(messages):
        abort(404)

    return messages[index]

This view function uses the @other_responses decorator to specify non-standard responses.

The @other_responses decorator is only used for documentation purposes! It does not provide any functionality in terms of returning error codes.

Tạo điểm cuối API để cập nhật mục nhập tạp chí:

@journal_api_blueprint.route('/<int:index>', methods=['PUT'])
@body(new_entry_schema)
@response(entry_schema)
@other_responses({404: 'Entry not found'})
def update_journal_entry(data, index):
    """Update a journal entry"""
    if index >= len(messages):
        abort(404)

    messages[index] = dict(data, id=index+1)
    return messages[index]

Hàm dạng xem này sử dụng bộ trang trí @bodyvà bộ @responsetrang trí để xác định các đầu vào và đầu ra (tương ứng) cho điểm cuối API này. Ngoài ra, người @other_responsestrang trí xác định phản hồi không chuẩn nếu mục nhập nhật ký không được tìm thấy.

Cuối cùng, tạo điểm cuối API để xóa mục nhập tạp chí:

@journal_api_blueprint.route('/<int:index>', methods=['DELETE'])
@other_responses({404: 'Entry not found'})
def delete_journal_entry(index):
    """Delete a journal entry"""
    if index >= len(messages):
        abort(404)

    messages.pop(index)
    return '', 204

Hàm xem này không sử dụng @body@responsedecorator, vì không có đầu vào hoặc đầu ra cho điểm cuối API này. Nếu mục nhật ký được xóa thành công, thì mã trạng thái 204 (Không có Nội dung) sẽ được trả về mà không có dữ liệu.

Chạy ứng dụng bình

Để kiểm tra mọi thứ, trong một cửa sổ đầu cuối, hãy định cấu hình ứng dụng Flask và chạy máy chủ phát triển:

(venv) $ export FLASK_APP=app.py
(venv) $ export FLASK_ENV=development
(venv) $ flask run

Sau đó, trong một cửa sổ đầu cuối khác, bạn có thể tương tác với API. Hãy thoải mái sử dụng công cụ bạn chọn tại đây, như cURL, HTTPie , Yêu cầu hoặc Người đưa thư .

Ví dụ về yêu cầu:

$ python3

>>> import requests
>>>
>>> r = requests.get('http://127.0.0.1:5000/journal/')
>>> print(r.text)
>>>
>>> post_data = {'entry': "some message"}
>>> r = requests.post('http://127.0.0.1:5000/journal/', json=post_data)
>>> print(r.text)

Bạn muốn kiểm tra các điểm cuối API dễ dàng hơn? Hãy xem tập lệnh này , tập lệnh này bổ sung các lệnh CLI để tương tác với các điểm cuối API để truy xuất, tạo, cập nhật và xóa các mục nhật ký.

Tài liệu

Một tính năng đáng kinh ngạc của APIFairy là tạo tài liệu API tự động!

Có ba khía cạnh chính để định cấu hình tài liệu API:

  1. Các chuỗi tài liệu cho các điểm cuối API (tức là các hàm xem)
  2. Chuỗi tài liệu cho dự án API tổng thể
  3. Các biến cấu hình để chỉ định giao diện của tài liệu API

We already covered the first item in the previous section since we included the docstrings for each view function. For example, the journal() view function has a short description of the purpose of this API endpoint:

@journal_api_blueprint.route('/', methods=['GET'])
@response(entries_schema)
def journal():
    """Return all journal entries"""
    return messages

Next, we need to include the docstring to describe the overall project at the very top of the project/__init__.py file:

"""
Welcome to the documentation for the Flask Journal API!

## Introduction

The Flask Journal API is an API (Application Programming Interface) for creating a **daily journal** that documents events that happen each day.

## Key Functionality

The Flask Journal API has the following functionality:

1. Work with journal entries:
  * Create a new journal entry
  * Update a journal entry
  * Delete a journal entry
  * View all journal entries
2. <More to come!>

## Key Modules

This project is written using Python 3.10.1.

The project utilizes the following modules:

* **Flask**: micro-framework for web application development which includes the following dependencies:
  * **click**: package for creating command-line interfaces (CLI)
  * **itsdangerous**: cryptographically sign data
  * **Jinja2**: templating engine
  * **MarkupSafe**: escapes characters so text is safe to use in HTML and XML
  * **Werkzeug**: set of utilities for creating a Python application that can talk to a WSGI server
* **APIFairy**: API framework for Flask which includes the following dependencies:
  * **Flask-Marshmallow** - Flask extension for using Marshmallow (object serialization/deserialization library)
  * **Flask-HTTPAuth** - Flask extension for HTTP authentication
  * **apispec** - API specification generator that supports the OpenAPI specification
* **pytest**: framework for testing Python projects
"""
...

This docstring is used to describe the overall project, including the key functionality provided and the key Python packages used by the project.

Finally, some configuration variables need to be defined to specify the look of the API documentation. Update the create_app() function in project/__init__.py:

def create_app():
    # Create the Flask application
    app = Flask(__name__)

    # Configure the API documentation
    app.config['APIFAIRY_TITLE'] = 'Flask Journal API'
    app.config['APIFAIRY_VERSION'] = '0.1'
    app.config['APIFAIRY_UI'] = 'elements'

    initialize_extensions(app)
    register_blueprints(app)

    return app

Sẵn sàng để xem tài liệu cho dự án? Khởi động máy chủ phát triển Flask qua flask run, sau đó điều hướng đến http://127.0.0.1:5000/docs để xem tài liệu API được tạo bởi APIFairy:

Tài liệu API - Trang chính

Trên ngăn bên trái, có một danh sách các điểm cuối API cho journal_apibản thiết kế. Nhấp vào một trong các điểm cuối sẽ hiển thị tất cả thông tin chi tiết về điểm cuối đó:

Tài liệu API - Nhận điểm cuối API mục nhập nhật ký

Điều đáng ngạc nhiên về tài liệu API này là khả năng xem các điểm cuối API hoạt động như thế nào (giả sử máy chủ phát triển Flask đang chạy). Trên ngăn bên phải của tài liệu, nhập chỉ mục mục nhập tạp chí và nhấp vào "Gửi yêu cầu API". Phản hồi API sau đó được hiển thị:

Tài liệu API - Nhận phản hồi API mục nhập nhật ký

Tài liệu tương tác này giúp người dùng dễ dàng hiểu API!

Cơ sở dữ liệu

Đối với mục đích trình diễn, cơ sở dữ liệu SQLite sẽ được sử dụng trong hướng dẫn này.

Cấu hình

Flask-SQLAlchemy đã được cài đặt ở phần đầu của hướng dẫn này, chúng tôi cần phải định cấu hình nó trong tệp dự án / __ init__.py .

Bắt đầu bằng cách tạo một SQLAlchemy()đối tượng trong phần 'Cấu hình':

...

from apifairy import APIFairy
from flask import Flask, json
from flask_marshmallow import Marshmallow
from flask_sqlalchemy import SQLAlchemy  # <-- NEW!!


# -------------
# Configuration
# -------------

# Create the instances of the Flask extensions in the global scope,
# but without any arguments passed in. These instances are not
# attached to the Flask application at this point.
apifairy = APIFairy()
ma = Marshmallow()
database = SQLAlchemy()  # <-- NEW!!

...

Tiếp theo, cập nhật create_app()hàm để chỉ định các biến cấu hình cần thiết:

def create_app():
    # Create the Flask application
    app = Flask(__name__)

    # Configure the API documentation
    app.config['APIFAIRY_TITLE'] = 'Flask Journal API'
    app.config['APIFAIRY_VERSION'] = '0.1'
    app.config['APIFAIRY_UI'] = 'elements'

    # NEW!
    # Configure the SQLite database (intended for development only!)
    app.config['SQLALCHEMY_DATABASE_URI'] = f"sqlite:///{os.path.join(os.getcwd(), 'instance', 'app.db')}"
    app.config['SQLALCHEMY_TRACK_MODIFICATIONS'] = False

    initialize_extensions(app)
    register_blueprints(app)

    return app

Thêm nhập vào đầu:

import os

Biến SQLALCHEMY_DATABASE_URIcấu hình rất quan trọng để xác định vị trí của cơ sở dữ liệu SQLite. Đối với hướng dẫn này, cơ sở dữ liệu được lưu trữ trong instance / app.db.

Cuối cùng, cập nhật initialize_extensions()hàm để khởi tạo đối tượng Flask-SQLAlchemy:

def initialize_extensions(app):
    # Since the application instance is now created, pass it to each Flask
    # extension instance to bind it to the Flask application instance (app)
    apifairy.init_app(app)
    ma.init_app(app)
    database.init_app(app)  # <-- NEW!!

Bạn muốn tìm hiểu thêm về cách kết nối ứng dụng Flask này với nhau? Xem khóa học của tôi về cách xây dựng, kiểm tra và triển khai ứng dụng Flask:

Mô hình cơ sở dữ liệu

Tạo một tệp project / models.py mới để xác định bảng cơ sở dữ liệu đại diện cho các mục nhật ký:

from project import database


class Entry(database.Model):
    """Class that represents a journal entry."""
    __tablename__ = 'entries'

    id = database.Column(database.Integer, primary_key=True)
    entry = database.Column(database.String, nullable=False)

    def __init__(self, entry: str):
        self.entry = entry

    def update(self, entry: str):
        self.entry = entry

    def __repr__(self):
        return f'<Entry: {self.entry}>'

Lớp mới này Entry, chỉ định rằng entriesbảng cơ sở dữ liệu sẽ chứa hai phần tử (hiện tại!) Để đại diện cho một mục nhập nhật ký:

  1. id- khóa chính ( primary_key=True) cho bảng, có nghĩa là đó là mã định danh duy nhất cho mỗi phần tử (hàng) trong bảng
  2. entry- chuỗi để lưu trữ văn bản nhập nhật ký

Trong khi models.py định nghĩa bảng cơ sở dữ liệu, nó không tạo các bảng trong cơ sở dữ liệu SQLite. Để tạo bảng, hãy khởi động trình bao Flask trong cửa sổ dòng lệnh:

(venv)$ flask shell

>>> from project import database
>>> database.drop_all()
>>> database.create_all()
>>> quit()

(venv)$

Cập nhật API tạp chí

Vì chúng tôi đang tiến hành sử dụng cơ sở dữ liệu SQLite, hãy bắt đầu bằng cách xóa tạm thời database(danh sách Python) đã được xác định trong project / journal_api / route.py :

# --------
# Database
# --------

messages = [
    dict(id=1, entry='The sun was shining when I woke up this morning.'),
    dict(id=2, entry='I tried a new fruit mixture in my oatmeal for breakfast.'),
    dict(id=3, entry='Today I ate a great sandwich for lunch.')
]

Tiếp theo, chúng ta cần cập nhật từng điểm cuối API (tức là các chức năng dạng xem) để sử dụng cơ sở dữ liệu SQLite.

Bắt đầu bằng cách cập nhật journal()chức năng xem:

@journal_api_blueprint.route('/', methods=['GET'])
@response(entries_schema)
def journal():
    """Return all journal entries"""
    return Entry.query.all()

Danh sách đầy đủ các mục nhật ký hiện được truy xuất từ ​​cơ sở dữ liệu SQLite. Lưu ý rằng các lược đồ hoặc trình trang trí cho chức năng xem này không cần thay đổi như thế nào ... chỉ có quy trình cơ bản để thay đổi người dùng!

Thêm nhập khẩu:

from project.models import Entry

Tiếp theo, cập nhật add_journal_entry()chức năng xem:

@journal_api_blueprint.route('/', methods=['POST'])
@body(new_entry_schema)
@response(entry_schema, 201)
def add_journal_entry(kwargs):
    """Add a new journal entry"""
    new_message = Entry(**kwargs)
    database.session.add(new_message)
    database.session.commit()
    return new_message

Các đầu vào cho chức năng xem này được chỉ định bởi new_entry_schema:

class NewEntrySchema(ma.Schema):
    """Schema defining the attributes when creating a new journal entry."""
    entry = ma.String(required=True)

new_entry_schema = NewEntrySchema()

Chuỗi entryđược sử dụng để tạo một phiên bản mới của Entrylớp (được định nghĩa trong models.py ) và mục nhật ký này sau đó được thêm vào cơ sở dữ liệu.

Thêm nhập khẩu:

from project import database

Tiếp theo, cập nhật get_journal_entry():

@journal_api_blueprint.route('/<int:index>', methods=['GET'])
@response(entry_schema)
@other_responses({404: 'Entry not found'})
def get_journal_entry(index):
    """Return a journal entry"""
    entry = Entry.query.filter_by(id=index).first_or_404()
    return entry

Chức năng này hiện cố gắng tra cứu mục nhập nhật ký được chỉ định (dựa trên index):

entry = Entry.query.filter_by(id=index).first_or_404()

Nếu mục nhập tồn tại, nó sẽ được trả lại cho người dùng. Nếu mục nhập không tồn tại, lỗi 404 (Không tìm thấy) sẽ được trả về.

Tiếp theo, cập nhật update_journal_entry():

@journal_api_blueprint.route('/<int:index>', methods=['PUT'])
@body(new_entry_schema)
@response(entry_schema)
@other_responses({404: 'Entry not found'})
def update_journal_entry(data, index):
    """Update a journal entry"""
    entry = Entry.query.filter_by(id=index).first_or_404()
    entry.update(data['entry'])
    database.session.add(entry)
    database.session.commit()
    return entry

Chức update_journal_entry()năng xem bây giờ cố gắng truy xuất mục nhập nhật ký được chỉ định:

entry = Entry.query.filter_by(id=index).first_or_404()

Nếu mục nhật ký tồn tại, mục đó được cập nhật văn bản mới và sau đó được lưu vào cơ sở dữ liệu.

Cuối cùng, hãy cập nhật delete_journal_entry():

@journal_api_blueprint.route('/<int:index>', methods=['DELETE'])
@other_responses({404: 'Entry not found'})
def delete_journal_entry(index):
    """Delete a journal entry"""
    entry = Entry.query.filter_by(id=index).first_or_404()
    database.session.delete(entry)
    database.session.commit()
    return '', 204

Nếu mục nhật ký được chỉ định được tìm thấy, thì nó sẽ bị xóa khỏi cơ sở dữ liệu.

Chạy máy chủ phát triển. Kiểm tra từng điểm cuối để đảm bảo chúng vẫn hoạt động.

Xử lý lỗi

Vì dự án Flask này là một API, mã lỗi phải được trả về ở định dạng JSON thay vì định dạng HTML điển hình.

Trong dự án Flask, điều này có thể được thực hiện bằng cách sử dụng trình xử lý lỗi tùy chỉnh. Trong project / __ init__.py , hãy xác định một hàm mới ( register_error_handlers()) ở cuối tệp:

def register_error_handlers(app):
    @app.errorhandler(HTTPException)
    def handle_http_exception(e):
        """Return JSON instead of HTML for HTTP errors."""
        # Start with the correct headers and status code from the error
        response = e.get_response()
        # Replace the body with JSON
        response.data = json.dumps({
            'code': e.code,
            'name': e.name,
            'description': e.description,
        })
        response.content_type = 'application/json'
        return response

Hàm này đăng ký một trình xử lý lỗi mới khi một HTTPExceptionđược nâng lên để chuyển đổi đầu ra thành định dạng JSON.

Thêm nhập khẩu:

from werkzeug.exceptions import HTTPException

Ngoài ra, hãy cập nhật chức năng Nhà máy ứng dụng create_app(), để gọi chức năng mới này:

def create_app():
    # Create the Flask application
    app = Flask(__name__)

    # Configure the API documentation
    app.config['APIFAIRY_TITLE'] = 'Flask Journal API'
    app.config['APIFAIRY_VERSION'] = '0.1'
    app.config['APIFAIRY_UI'] = 'elements'

    # Configure the SQLite database (intended for development only!)
    app.config['SQLALCHEMY_DATABASE_URI'] = f"sqlite:///{os.path.join(os.getcwd(), 'instance', 'app.db')}"
    app.config['SQLALCHEMY_TRACK_MODIFICATIONS'] = False

    initialize_extensions(app)
    register_blueprints(app)
    register_error_handlers(app)  # NEW!!

    return app

Xác thực

Authentication is the process of validating the identity of a user attempting to access a system, which in this case is the API.

Authorization, on the other hand, is the process of verifying what specific resources a specific user should have access to.

APIFairy utilizes Flask-HTTPAuth for authentication support. In this tutorial, we'll be using Flask-HTTPAuth in two manners:

  1. Basic Authentication - used to generate a token based on the user's email/password
  2. Token Authentication - used to authenticate the user on all other API endpoints

Xác thực mã thông báo được sử dụng qua Flask-HTTPAuth thường được gọi là Xác thực mang, vì quá trình này yêu cầu cấp quyền truy cập cho "người mang" mã thông báo. Mã thông báo phải được bao gồm trong tiêu đề HTTP trong tiêu đề Ủy quyền, chẳng hạn như "Ủy quyền: Người mang".

Sơ đồ sau minh họa một quy trình điển hình về cách người dùng mới tương tác với ứng dụng để truy xuất mã thông báo xác thực:

Sơ đồ luồng API của tạp chí Flask

Cấu hình

Vì Flask-HTTPAuth đã được cài đặt khi APIFairy được cài đặt ở phần đầu của hướng dẫn này, chúng tôi chỉ cần định cấu hình nó trong tệp dự án / __ init__.py .

Bắt đầu bằng cách tạo các đối tượng riêng biệt để xác thực mã thông báo và cơ bản:

...

import os

from apifairy import APIFairy
from flask import Flask, json
from flask_httpauth import HTTPBasicAuth, HTTPTokenAuth  # NEW!!
from flask_marshmallow import Marshmallow
from flask_sqlalchemy import SQLAlchemy
from werkzeug.exceptions import HTTPException


# -------------
# Configuration
# -------------

# Create the instances of the Flask extensions in the global scope,
# but without any arguments passed in. These instances are not
# attached to the Flask application at this point.
apifairy = APIFairy()
ma = Marshmallow()
database = SQLAlchemy()
basic_auth = HTTPBasicAuth()  # NEW!!
token_auth = HTTPTokenAuth()  # NEW!!

...

Không cần cập nhật thêm trong dự án / __ init__.py .

Mô hình cơ sở dữ liệu

Trong project / models.py , một Usermô hình mới cần được tạo để đại diện cho người dùng:

class User(database.Model):
    __tablename__ = 'users'

    id = database.Column(database.Integer, primary_key=True)
    email = database.Column(database.String, unique=True, nullable=False)
    password_hashed = database.Column(database.String(128), nullable=False)
    entries = database.relationship('Entry', backref='user', lazy='dynamic')
    auth_token = database.Column(database.String(64), index=True)
    auth_token_expiration = database.Column(database.DateTime)

    def __init__(self, email: str, password_plaintext: str):
        """Create a new User object."""
        self.email = email
        self.password_hashed = self._generate_password_hash(password_plaintext)

    def is_password_correct(self, password_plaintext: str):
        return check_password_hash(self.password_hashed, password_plaintext)

    def set_password(self, password_plaintext: str):
        self.password_hashed = self._generate_password_hash(password_plaintext)

    @staticmethod
    def _generate_password_hash(password_plaintext):
        return generate_password_hash(password_plaintext)

    def generate_auth_token(self):
        self.auth_token = secrets.token_urlsafe()
        self.auth_token_expiration = datetime.utcnow() + timedelta(minutes=60)
        return self.auth_token

    @staticmethod
    def verify_auth_token(auth_token):
        user = User.query.filter_by(auth_token=auth_token).first()
        if user and user.auth_token_expiration > datetime.utcnow():
            return user

    def revoke_auth_token(self):
        self.auth_token_expiration = datetime.utcnow()

    def __repr__(self):
        return f'<User: {self.email}>'

Thêm nhập khẩu:

import secrets
from datetime import datetime, timedelta

from werkzeug.security import check_password_hash, generate_password_hash

Userhình sử dụng werkzeug.securityđể băm mật khẩu của người dùng trước khi lưu trữ trong cơ sở dữ liệu.

Hãy nhớ: Không bao giờ lưu trữ mật khẩu văn bản rõ trong cơ sở dữ liệu!

Userhình sử dụng secretsđể tạo mã thông báo xác thực cho một người dùng cụ thể. Mã thông báo này được tạo trong generate_auth_token()phương thức và bao gồm ngày / giờ hết hạn là 60 phút trong tương lai:

def generate_auth_token(self):
    self.auth_token = secrets.token_urlsafe()
    self.auth_token_expiration = datetime.utcnow() + timedelta(minutes=60)
    return self.auth_token

Có một phương thức tĩnh, verify_auth_token()được sử dụng để xác minh mã thông báo xác thực (trong khi xem xét thời gian hết hạn) và trả lại người dùng từ mã thông báo hợp lệ:

@staticmethod
def verify_auth_token(auth_token):
    user = User.query.filter_by(auth_token=auth_token).first()
    if user and user.auth_token_expiration > datetime.utcnow():
        return user

Một phương pháp quan tâm khác là revoke_auth_token(), được sử dụng để thu hồi mã thông báo xác thực cho một người dùng cụ thể:

def revoke_auth_token(self):
    self.auth_token_expiration = datetime.utcnow()

Mô hình nhập cảnh

Để thiết lập mối quan hệ một-nhiều giữa người dùng ("một") và các mục nhập của họ ("nhiều"), Entrymô hình cần được cập nhật để liên kết các bảng entriesusersvới nhau:

class Entry(database.Model):
    """Class that represents a journal entry."""
    __tablename__ = 'entries'

    id = database.Column(database.Integer, primary_key=True)
    entry = database.Column(database.String, nullable=False)
    user_id = database.Column(database.Integer, database.ForeignKey('users.id'))  # <-- NEW!!

    def __init__(self, entry: str):
        self.entry = entry

    def update(self, entry: str):
        self.entry = entry

    def __repr__(self):
        return f'<Entry: {self.entry}>'

Userhình đã chứa liên kết quay lại entriesbảng:

entries = database.relationship('Entry', backref='user', lazy='dynamic')

Bản thiết kế API người dùng

Chức năng quản lý người dùng của dự án Flask sẽ được định nghĩa trong một Bản thiết kế riêng được gọi là users_api_blueprint.

Bắt đầu bằng cách tạo một thư mục mới trong "dự án" được gọi là "users_api". Trong thư mục đó, hãy tạo một tệp __init__.py :

from flask import Blueprint


users_api_blueprint = Blueprint('users_api', __name__)

from . import authentication, routes

Bản kế hoạch chi tiết mới này cần được đăng ký với Flask apptrong các dự án / __ init__.py trong register_blueprints()hàm:

def register_blueprints(app):
    # Import the blueprints
    from project.journal_api import journal_api_blueprint
    from project.users_api import users_api_blueprint  # NEW!!

    # Since the application instance is now created, register each Blueprint
    # with the Flask application instance (app)
    app.register_blueprint(journal_api_blueprint, url_prefix='/journal')
    app.register_blueprint(users_api_blueprint, url_prefix='/users')  # NEW!!

Chức năng xác thực

Để sử dụng Flask-HTTPAuth, một số chức năng cần được xác định để xử lý việc kiểm tra thông tin đăng nhập của người dùng.

Tạo một tệp dự án / users_api / verify.py mới để xử lý xác thực cơ bản và xác thực mã thông báo.

Đối với xác thực cơ bản (kiểm tra email và mật khẩu của người dùng):

from werkzeug.exceptions import Forbidden, Unauthorized

from project import basic_auth, token_auth
from project.models import User


@basic_auth.verify_password
def verify_password(email, password):
    user = User.query.filter_by(email=email).first()
    if user is None:
        return None

    if user.is_password_correct(password):
        return user


@basic_auth.error_handler
def basic_auth_error(status=401):
    error = (Forbidden if status == 403 else Unauthorized)()
    return {
        'code': error.code,
        'message': error.name,
        'description': error.description,
    }, error.code, {'WWW-Authenticate': 'Form'}

Hàm verify_password()được sử dụng để kiểm tra xem người dùng có tồn tại và mật khẩu của họ có chính xác hay không. Chức năng này sẽ được sử dụng bởi Flask-HTTPAuth để xác minh mật khẩu khi cần xác thực cơ bản (nhờ trình @basic_auth.verify_passwordtrang trí).

Ngoài ra, một trình xử lý lỗi được xác định cho xác thực cơ bản trả về thông tin về lỗi ở định dạng JSON.

Đối với xác thực mã thông báo (xử lý mã thông báo để xác định xem người dùng có hợp lệ hay không):

@token_auth.verify_token
def verify_token(auth_token):
    return User.verify_auth_token(auth_token)


@token_auth.error_handler
def token_auth_error(status=401):
    error = (Forbidden if status == 403 else Unauthorized)()
    return {
        'code': error.code,
        'message': error.name,
        'description': error.description,
    }, error.code

Hàm verify_token()được sử dụng để kiểm tra xem mã thông báo xác thực có hợp lệ hay không. Chức năng này sẽ được sử dụng bởi Flask-HTTPAuth để xác minh mã thông báo khi cần xác thực mã thông báo (nhờ trình @token_auth.verify_tokentrang trí).

Ngoài ra, một trình xử lý lỗi được xác định để xác thực mã thông báo trả về thông tin về lỗi ở định dạng JSON.

Các tuyến đường của người dùng

Trong users_api_blueprint, sẽ có hai tuyến đường:

  1. Đăng ký người dùng mới
  2. Truy xuất mã thông báo xác thực

Để bắt đầu, một tập hợp các lược đồ mới (sử dụng marshmallow) cần được xác định trong các dự án / users_api / route.py :

from project import ma

from . import users_api_blueprint

# -------
# Schemas
# -------

class NewUserSchema(ma.Schema):
    """Schema defining the attributes when creating a new user."""
    email = ma.String()
    password_plaintext = ma.String()


class UserSchema(ma.Schema):
    """Schema defining the attributes of a user."""
    id = ma.Integer()
    email = ma.String()


class TokenSchema(ma.Schema):
    """Schema defining the attributes of a token."""
    token = ma.String()


new_user_schema = NewUserSchema()
user_schema = UserSchema()
token_schema = TokenSchema()

Các lược đồ này sẽ được sử dụng để xác định các đầu vào và đầu ra cho các chức năng dạng xem được xác định trong tệp này.

Đăng ký người dùng mới

Tiếp theo, xác định chức năng xem để đăng ký người dùng mới:

@users_api_blueprint.route('/', methods=['POST'])
@body(new_user_schema)
@response(user_schema, 201)
def register(kwargs):
    """Create a new user"""
    new_user = User(**kwargs)
    database.session.add(new_user)
    database.session.commit()
    return new_user

Thêm nhập khẩu:

from apifairy import authenticate, body, other_responses, response

from project import basic_auth, database, ma
from project.models import User

Điểm cuối API này sử dụng new_user_schemađể chỉ định rằng email và mật khẩu là đầu vào.

LƯU Ý: Vì email và mật khẩu được gửi đến điểm cuối API này, nên nhớ rằng việc sử dụng HTTP được chấp nhận trong quá trình thử nghiệm phát triển, nhưng HTTPS (bảo mật) phải luôn được sử dụng trong quá trình sản xuất.

Sau đó, email và mật khẩu (được định nghĩa là kwargsđối số - từ khóa) được giải nén để tạo một Userđối tượng mới, được lưu vào cơ sở dữ liệu:

new_user = User(**kwargs)
database.session.add(new_user)
database.session.commit()

Đầu ra từ điểm cuối API được xác định bởi user_schema, đó là ID và email cho người dùng mới.

Truy xuất mã thông báo xác thực

Chức năng xem khác để xác định trong các dự án / users_api / route.py là để truy xuất mã thông báo xác thực:

@users_api_blueprint.route('/get-auth-token', methods=['POST'])
@authenticate(basic_auth)
@response(token_schema)
@other_responses({401: 'Invalid username or password'})
def get_auth_token():
    """Get authentication token"""
    user = basic_auth.current_user()
    token = user.generate_auth_token()
    database.session.add(user)
    database.session.commit()
    return dict(token=token)

Trình @authenticatetrang trí được sử dụng lần đầu tiên trong hướng dẫn này và nó chỉ định rằng xác thực cơ bản nên được sử dụng để bảo vệ tuyến đường này:

@authenticate(basic_auth)

Khi người dùng muốn truy xuất mã thông báo xác thực của họ, họ cần gửi yêu cầu ĐĂNG tới điểm cuối API này với email và mật khẩu được nhúng trong tiêu đề 'Ủy quyền'. Ví dụ: lệnh Python sau sử dụng gói Yêu cầu có thể được thực hiện cho điểm cuối API này:

>>> import requests
>>> r = requests.post(
    'http://127.0.0.1:5000/users/get-auth-token',
    auth=('pkennedy@hey.com', 'FlaskIsAwesome123')
)

Nếu xác thực cơ bản thành công, chức năng xem sẽ truy xuất người dùng hiện tại bằng current_user()phương pháp được cung cấp bởi Flask-HTTPAuth:

user = basic_auth.current_user()

Mã thông báo xác thực mới được tạo cho người dùng đó:

token = user.generate_auth_token()

Và mã thông báo đó được lưu vào cơ sở dữ liệu để nó có thể được sử dụng để xác thực người dùng trong tương lai (ít nhất là trong 60 phút tới!).

Cuối cùng, mã thông báo xác thực mới được trả lại để người dùng lưu cho tất cả các lệnh gọi API tiếp theo.

Cập nhật điểm cuối API

Với quy trình xác thực đã có, đã đến lúc thêm một số bảo vệ vào các điểm cuối API hiện có để đảm bảo rằng chỉ những người dùng hợp lệ mới có thể truy cập ứng dụng.

Các cập nhật này dành cho các chức năng xem được xác định trong các dự án / journal_api / route.py .

Đầu tiên, hãy cập nhật journal()để chỉ trả lại các mục nhật ký cho người dùng hiện tại:

@journal_api_blueprint.route('/', methods=['GET'])
@authenticate(token_auth)
@response(entries_schema)
def journal():
    """Return journal entries"""
    user = token_auth.current_user()
    return Entry.query.filter_by(user_id=user.id).all()

Cập nhật các nhập ở trên cùng như sau:

from apifairy import authenticate, body, other_responses, response
from flask import abort

from project import database, ma, token_auth
from project.models import Entry

from . import journal_api_blueprint

Người @authenticatetrang trí chỉ định rằng xác thực mã thông báo cần được sử dụng khi truy cập điểm cuối API này. Ví dụ: yêu cầu GET sau có thể được thực hiện bằng Yêu cầu ( sau khi mã thông báo xác thực đã được truy xuất ):

>>> import requests
>>> headers = {'Authorization': f'Bearer {auth_token}'}
>>> r = requests.get('http://127.0.0.1:5000/journal/', headers=headers)

Sau khi người dùng được xác thực, danh sách đầy đủ các mục nhật ký sẽ được truy xuất từ ​​cơ sở dữ liệu dựa trên ID của người dùng:

user = token_auth.current_user()
return Entry.query.filter_by(user_id=user.id).all()

Đầu ra từ điểm cuối API này được xác định bởi người @responsetrang trí, là danh sách các mục nhật ký (ID, mục nhập, ID người dùng).

Tiếp theo, cập nhật add_journal_entry():

@journal_api_blueprint.route('/', methods=['POST'])
@authenticate(token_auth)
@body(new_entry_schema)
@response(entry_schema, 201)
def add_journal_entry(kwargs):
    """Add a new journal entry"""
    user = token_auth.current_user()
    new_message = Entry(user_id=user.id, **kwargs)
    database.session.add(new_message)
    database.session.commit()
    return new_message

Như với chức năng xem trước, trình @authenticatetrang trí được sử dụng để chỉ định rằng xác thực mã thông báo cần được sử dụng khi truy cập điểm cuối API này. Ngoài ra, mục nhập tạp chí hiện được thêm vào bằng cách chỉ định ID người dùng sẽ được liên kết với mục nhập tạp chí:

user = token_auth.current_user()
new_message = Entry(user_id=user.id, **kwargs)

Mục nhật ký mới được lưu vào cơ sở dữ liệu và mục nhập nhật ký được trả lại (theo định nghĩa của người @responsetrang trí).

Tiếp theo, cập nhật get_journal_entry():

@journal_api_blueprint.route('/<int:index>', methods=['GET'])
@authenticate(token_auth)
@response(entry_schema)
@other_responses({403: 'Forbidden', 404: 'Entry not found'})
def get_journal_entry(index):
    """Return a journal entry"""
    user = token_auth.current_user()
    entry = Entry.query.filter_by(id=index).first_or_404()

    if entry.user_id != user.id:
        abort(403)
    return entry

Trình @authenticatetrang trí được thêm vào để chỉ định rằng cần xác thực mã thông báo để truy cập điểm cuối API này.

Khi cố gắng truy xuất mục nhập nhật ký, một kiểm tra bổ sung được thêm vào để đảm bảo rằng người dùng cố gắng truy cập mục nhập nhật ký là "chủ sở hữu" thực sự của mục nhập. Nếu không, thì mã lỗi 403 (Bị cấm) được trả về thông qua abort()hàm từ Flask:

if entry.user_id != user.id:
        abort(403)

Hãy lưu ý rằng điểm cuối API này có hai phản hồi ngoài danh nghĩa được chỉ định bởi người @other_responsestrang trí:

@other_responses({403: 'Forbidden', 404: 'Entry not found'})

Nhắc nhở: Trình @other_responsestrang trí chỉ dành cho tài liệu; đó là trách nhiệm của chức năng xem để tạo ra những lỗi này.

Tiếp theo, cập nhật update_journal_entry():

@journal_api_blueprint.route('/<int:index>', methods=['PUT'])
@authenticate(token_auth)
@body(new_entry_schema)
@response(entry_schema)
@other_responses({403: 'Forbidden', 404: 'Entry not found'})
def update_journal_entry(data, index):
    """Update a journal entry"""
    user = token_auth.current_user()
    entry = Entry.query.filter_by(id=index).first_or_404()

    if entry.user_id != user.id:
        abort(403)

    entry.update(data['entry'])
    database.session.add(entry)
    database.session.commit()
    return entry

Các cập nhật cho chức năng xem này tương tự như các chức năng xem khác trong phần này:

  1. @authenticatedecorator chỉ định rằng xác thực mã thông báo là cần thiết để truy cập điểm cuối API này
  2. Chỉ người dùng "sở hữu" mục nhập mới được phép cập nhật mục nhập (nếu không, 403 (Bị cấm))

Cuối cùng, hãy cập nhật delete_journal_entry():

@journal_api_blueprint.route('/<int:index>', methods=['DELETE'])
@authenticate(token_auth)
@other_responses({403: 'Forbidden', 404: 'Entry not found'})
def delete_journal_entry(index):
    """Delete a journal entry"""
    user = token_auth.current_user()
    entry = Entry.query.filter_by(id=index).first_or_404()

    if entry.user_id != user.id:
        abort(403)

    database.session.delete(entry)
    database.session.commit()
    return '', 204

Sự kết luận

Hướng dẫn này đã cung cấp hướng dẫn về cách dễ dàng và nhanh chóng xây dựng một API trong Flask bằng APIFairy.

Trình trang trí là chìa khóa để xác định các điểm cuối API:

  • Đầu vào :
    • @arguments- đầu vào đối số từ chuỗi truy vấn của URL
    • @body- cấu trúc của yêu cầu JSON
  • Kết quả đầu ra :
    • @response- cấu trúc của phản hồi JSON
  • Xác thực :
    • @authenticate- phương pháp xác thực sử dụng Flask-HTTPAuth
  • Các lỗi :
    • @other_responses- phản hồi ngoài danh nghĩa, chẳng hạn như mã lỗi HTTP

Thêm vào đó, tài liệu API do APIFairy tạo ra rất tuyệt vời và cung cấp thông tin chính cho người dùng ứng dụng.

Nguồn:  https://testdriven.io

#api #flask 

Cách Dễ Dàng Để Tạo Một API RESTful Với Flask Và APIFairy

Como Desenvolver Uma API Assíncrona Com FastAPI E MongoDB

Neste tutorial, você aprenderá a desenvolver uma API assíncrona com FastAPI e MongoDB . Usaremos a biblioteca Beanie ODM para interagir com o MongoDB de forma assíncrona.

Objetivos

Ao final deste tutorial, você será capaz de:

  1. Explique o que é o Beanie ODM e por que você pode querer usá-lo
  2. Interaja com o MongoDB de forma assíncrona usando o Beanie ODM
  3. Desenvolva uma API RESTful com Python e FastAPI

Por que Beanie ODM?

Beanie é um mapeador de documento-objeto (ODM) assíncrono para MongoDB, que suporta migrações de dados e esquemas prontas para uso. Ele usa Motor , como um mecanismo de banco de dados assíncrono, e Pydantic .

Embora você possa simplesmente usar o Motor, o Beanie fornece uma camada de abstração adicional, tornando muito mais fácil interagir com coleções dentro de um banco de dados Mongo.

Quer apenas usar o Motor? Confira Construindo um aplicativo CRUD com FastAPI e MongoDB .

Configuração inicial

Comece criando uma nova pasta para armazenar seu projeto chamada "fastapi-beanie":

$ mkdir fastapi-beanie
$ cd fastapi-beanie

Em seguida, crie e ative um ambiente virtual:

$ python3.10 -m venv venv
$ source venv/bin/activate
(venv)$ export PYTHONPATH=$PWD

Sinta-se à vontade para trocar venv e Pip por Poetry ou Pipenv . Para saber mais, revise Ambientes Python Modernos .

Em seguida, crie os seguintes arquivos e pastas:

├── app
│   ├── __init__.py
│   ├── main.py
│   └── server
│       ├── app.py
│       ├── database.py
│       ├── models
│       └── routes
└── requirements.txt

Adicione as seguintes dependências ao seu arquivo requirements.txt :

beanie==1.11.0
fastapi==0.78.0
uvicorn==0.17.6

Instale as dependências do seu terminal:

(venv)$ pip install -r requirements.txt

No arquivo app/main.py , defina um ponto de entrada para executar o aplicativo:

import uvicorn

if __name__ == "__main__":
    uvicorn.run("server.app:app", host="0.0.0.0", port=8000, reload=True)

Aqui, instruímos o arquivo para executar um servidor Uvicorn na porta 8000 e recarregar a cada alteração de arquivo.

Antes de iniciar o servidor por meio do arquivo de ponto de entrada, crie uma rota base em app/server/app.py :

from fastapi import FastAPI

app = FastAPI()


@app.get("/", tags=["Root"])
async def read_root() -> dict:
    return {"message": "Welcome to your beanie powered app!"}

Execute o arquivo de ponto de entrada do seu console:

(venv)$ python app/main.py

Navegue até http://localhost:8000 em seu navegador. Você deveria ver:

{
  "message": "Welcome to your beanie powered app!"
}

O que estamos construindo?

Construiremos um aplicativo de análise de produtos que nos permitirá realizar as seguintes operações:

  • Criar avaliações
  • Ler comentários
  • Atualizar comentários
  • Excluir comentários

Antes de mergulhar na escrita das rotas, vamos usar o Beanie para configurar o modelo de banco de dados para nosso aplicativo.

Esquema de banco de dados

Beanie permite que você crie documentos que podem ser usados ​​para interagir com coleções no banco de dados. Documentos representam seu esquema de banco de dados. Eles podem ser definidos criando classes filhas que herdam a Documentclasse de Beanie. A Documentclasse é alimentada pelo Pydantic's BaseModel, o que facilita a definição de coleções e esquema de banco de dados, bem como dados de exemplo exibidos na página interativa de documentos do Swagger.

Exemplo:

from beanie import Document


class TestDrivenArticle(Document):
    title: str
    content: str
    date: datetime
    author: str

O documento definido representa como os artigos serão armazenados no banco de dados. No entanto, é uma classe de documento normal sem nenhuma coleção de banco de dados associada a ela. Para associar uma coleção, basta adicionar uma Settingsclasse como subclasse:

from beanie import Document


class TestDrivenArticle(Document):
    title: str
    content: str
    date: datetime
    author: str


    class Settings:
        name = "testdriven_collection"

Agora que temos uma ideia de como os esquemas são criados, vamos criar o esquema para nossa aplicação. Na pasta "app/server/models", crie um novo arquivo chamado product_review.py :

from datetime import datetime

from beanie import Document
from pydantic import BaseModel
from typing import Optional


class ProductReview(Document):
    name: str
    product: str
    rating: float
    review: str
    date: datetime = datetime.now()

    class Settings:
        name = "product_review"

Como a Documentclasse é desenvolvida pelo Pydantic, podemos definir dados de esquema de exemplo para tornar mais fácil para os desenvolvedores usarem a API dos documentos interativos do Swagger.

Adicione a Configsubclasse assim:

from datetime import datetime

from beanie import Document
from pydantic import BaseModel
from typing import Optional


class ProductReview(Document):
    name: str
    product: str
    rating: float
    review: str
    date: datetime = datetime.now()

    class Settings:
        name = "product_review"

    class Config:
        schema_extra = {
            "example": {
                "name": "Abdulazeez",
                "product": "TestDriven TDD Course",
                "rating": 4.9,
                "review": "Excellent course!",
                "date": datetime.now()
            }
        }

Assim, no bloco de código acima, definimos um documento chamado Beanie ProductReviewque representa como uma revisão de produto será armazenada. Também definimos a coleta, product_review, onde os dados serão armazenados.

Usaremos esse esquema na rota para impor o corpo da solicitação adequado.

Por fim, vamos definir o esquema para atualizar uma revisão de produto:

class UpdateProductReview(BaseModel):
    name: Optional[str]
    product: Optional[str]
    rating: Optional[float]
    review: Optional[str]
    date: Optional[datetime]

    class Config:
        schema_extra = {
            "example": {
                "name": "Abdulazeez Abdulazeez",
                "product": "TestDriven TDD Course",
                "rating": 5.0,
                "review": "Excellent course!",
                "date": datetime.now()
            }
        }

A UpdateProductReviewclasse acima é do tipo BaseModel , o que nos permite fazer alterações apenas nos campos presentes no corpo da requisição.

Com o esquema em vigor, vamos configurar o MongoDB e nosso banco de dados antes de continuar a escrever as rotas.

MongoDB

Nesta seção, conectaremos o MongoDB e configuraremos nosso aplicativo para se comunicar com ele.

De acordo com a Wikipedia , o MongoDB é um programa de banco de dados orientado a documentos multiplataforma. Classificado como um programa de banco de dados NoSQL, o MongoDB usa documentos semelhantes a JSON com esquemas opcionais.

Configuração do MongoDB

Se você não tiver o MongoDB instalado em sua máquina, consulte o guia de instalação da documentação. Uma vez instalado, continue com o guia para executar o processo do mongod daemon. Uma vez feito, você pode verificar se o MongoDB está funcionando, conectando-se à instância por meio do mongocomando shell:

$ mongo

Para referência, este tutorial usa o MongoDB Community Edition v5.0.7.

$ mongo --version
MongoDB shell version v5.0.7

Build Info: {
    "version": "5.0.7",
    "gitVersion": "b977129dc70eed766cbee7e412d901ee213acbda",
    "modules": [],
    "allocator": "system",
    "environment": {
        "distarch": "x86_64",
        "target_arch": "x86_64"
    }
}

Configurando o banco de dados

Em database.py , adicione o seguinte:

from beanie import init_beanie
import motor.motor_asyncio

from app.server.models.product_review import ProductReview


async def init_db():
    client = motor.motor_asyncio.AsyncIOMotorClient(
        "mongodb://localhost:27017/productreviews"
    )

    await init_beanie(database=client.db_name, document_models=[ProductReview])

No bloco de código acima, importamos o método init_beanie que é responsável por inicializar o mecanismo de banco de dados alimentado por motor.motor_asyncio . O init_beaniemétodo recebe dois argumentos:

  1. database- O nome do banco de dados a ser usado.
  2. document_models- Uma lista de modelos de documentos definidos -- o ProductReviewmodelo, no nosso caso.

A init_dbfunção será invocada no evento de inicialização do aplicativo. Atualize app.py para incluir o evento de inicialização:

from fastapi import FastAPI

from app.server.database import init_db


app = FastAPI()


@app.on_event("startup")
async def start_db():
    await init_db()


@app.get("/", tags=["Root"])
async def read_root() -> dict:
    return {"message": "Welcome to your beanie powered app!"}

Agora que temos nossas configurações de banco de dados, vamos escrever as rotas.

Rotas

Nesta seção, construiremos as rotas para realizar operações CRUD em seu banco de dados a partir do aplicativo:

  1. PUBLICAR revisão
  2. RECEBA uma única revisão e RECEBA todas as revisões
  3. PUT única revisão
  4. DELETE revisão única

Na pasta "routes", crie um arquivo chamado product_review.py :

from beanie import PydanticObjectId
from fastapi import APIRouter, HTTPException
from typing import List

from app.server.models.product_review import ProductReview, UpdateProductReview


router = APIRouter()

No bloco de código acima, importamos PydanticObjectId, que será usado para indicar o tipo do argumento ID ao recuperar uma única solicitação. Também importamos a APIRouterclasse responsável por lidar com as operações de rota. Também importamos a classe de modelo que definimos anteriormente.

Os modelos de documentos Beanie nos permitem interagir diretamente com o banco de dados com menos código. Por exemplo, para recuperar todos os registros em uma coleção de banco de dados, tudo o que precisamos fazer é:

data = await ProductReview.find_all().to_list()
return data # A list of all records in the collection.

Antes de continuarmos a escrever a função de rota para as operações CRUD, vamos registrar a rota em app.py :

from fastapi import FastAPI

from app.server.database import init_db
from app.server.routes.product_review import router as Router


app = FastAPI()
app.include_router(Router, tags=["Product Reviews"], prefix="/reviews")


@app.on_event("startup")
async def start_db():
    await init_db()


@app.get("/", tags=["Root"])
async def read_root() -> dict:
    return {"message": "Welcome to your beanie powered app!"}

Crio

Em routes/product_review.py , adicione o seguinte:

@router.post("/", response_description="Review added to the database")
async def add_product_review(review: ProductReview) -> dict:
    await review.create()
    return {"message": "Review added successfully"}

Aqui, definimos a função de rota, que recebe um argumento do tipo ProductReview. Como dito anteriormente, a classe de documento pode interagir diretamente com o banco de dados.

O novo registro é criado chamando o método create() .

A rota acima espera uma carga semelhante a esta:

{
  "name": "Abdulazeez",
  "product": "TestDriven TDD Course",
  "rating": 4.9,
  "review": "Excellent course!",
  "date": "2022-05-17T13:53:17.196135"
}

Teste a rota:

$ curl -X 'POST' \
  'http://0.0.0.0:8000/reviews/' \
  -H 'accept: application/json' \
  -H 'Content-Type: application/json' \
  -d '{
  "name": "Abdulazeez",
  "product": "TestDriven TDD Course",
  "rating": 4.9,
  "review": "Excellent course!",
  "date": "2022-05-17T13:53:17.196135"
}'

A solicitação acima deve retornar uma mensagem de sucesso:

{
  "message": "Review added successfully"
}

Ler

A seguir estão as rotas que nos permitem recuperar uma única revisão e todas as revisões presentes no banco de dados:

@router.get("/{id}", response_description="Review record retrieved")
async def get_review_record(id: PydanticObjectId) -> ProductReview:
    review = await ProductReview.get(id)
    return review


@router.get("/", response_description="Review records retrieved")
async def get_reviews() -> List[ProductReview]:
    reviews = await ProductReview.find_all().to_list()
    return reviews

No bloco de código acima, definimos duas funções:

  1. Na primeira função, a função recebe um ID do tipo ObjectiD, a codificação padrão para IDs do MongoDB. O registro é recuperado usando o método get() .
  2. Na segunda, recuperamos todas as avaliações usando o método find_all() . O to_list()método é anexado para que os resultados sejam retornados em uma lista.

Outro método que pode ser usado para recuperar uma única entrada é o método find_one() que recebe uma condição. Por exemplo:

# Return a record who has a rating of 4.0
await ProductReview.find_one(ProductReview.rating == 4.0)

Vamos testar a primeira rota para recuperar todos os registros:

$ curl -X 'GET' \
  'http://0.0.0.0:8000/reviews/' \
  -H 'accept: application/json'

Resposta:

[
  {
    "_id": "62839ad1d9a88a040663a734",
    "name": "Abdulazeez",
    "product": "TestDriven TDD Course",
    "rating": 4.9,
    "review": "Excellent course!",
    "date": "2022-05-17T13:53:17.196000"
  }
]

Em seguida, vamos testar a rota para recuperar um único registro que corresponda a um ID fornecido:

$ curl -X 'GET' \
  'http://0.0.0.0:8000/reviews/62839ad1d9a88a040663a734' \
  -H 'accept: application/json'

Resposta:

{
  "_id": "62839ad1d9a88a040663a734",
  "name": "Abdulazeez",
  "product": "TestDriven TDD Course",
  "rating": 4.9,
  "review": "Excellent course!",
  "date": "2022-05-17T13:53:17.196000"
}

Atualizar

Em seguida, vamos escrever a rota para atualizar o registro de revisão:

@router.put("/{id}", response_description="Review record updated")
async def update_student_data(id: PydanticObjectId, req: UpdateProductReview) -> ProductReview:
    req = {k: v for k, v in req.dict().items() if v is not None}
    update_query = {"$set": {
        field: value for field, value in req.items()
    }}

    review = await ProductReview.get(id)
    if not review:
        raise HTTPException(
            status_code=404,
            detail="Review record not found!"
        )

    await review.update(update_query)
    return review

Nesta função, filtramos os campos que não são atualizados para evitar a substituição de campos existentes por None.

Para atualizar um registro, é necessária uma consulta de atualização. Definimos uma consulta de atualização que substitui os campos existentes pelos dados passados ​​no corpo da solicitação. Em seguida, verificamos se o registro existe. Se existir, ele é atualizado e o registro atualizado é retornado, caso contrário, uma exceção 404 é gerada.

Vamos testar a rota:

$ curl -X 'PUT' \
  'http://0.0.0.0:8000/reviews/62839ad1d9a88a040663a734' \
  -H 'accept: application/json' \
  -H 'Content-Type: application/json' \
  -d '{
  "name": "Abdulazeez Abdulazeez",
  "product": "TestDriven TDD Course",
  "rating": 5
}'

Resposta:

{
  "_id": "62839ad1d9a88a040663a734",
  "name": "Abdulazeez Abdulazeez",
  "product": "TestDriven TDD Course",
  "rating": 5.0,
  "review": "Excellent course!",
  "date": "2022-05-17T13:53:17.196000"
}

Excluir

Por fim, vamos escrever a rota responsável por deletar um registro:

@router.delete("/{id}", response_description="Review record deleted from the database")
async def delete_student_data(id: PydanticObjectId) -> dict:
    record = await ProductReview.get(id)

    if not record:
        raise HTTPException(
            status_code=404,
            detail="Review record not found!"
        )

    await record.delete()
    return {
        "message": "Record deleted successfully"
    }

Portanto, primeiro verificamos se o registro existe antes de excluir o registro. O registro é excluído chamando o método delete() .

Vamos testar a rota:

$ curl -X 'DELETE' \
  'http://0.0.0.0:8000/reviews/62839ad1d9a88a040663a734' \
  -H 'accept: application/json'

Resposta:

{
  "message": "Record deleted successfully"
}

Construímos com sucesso um aplicativo CRUD desenvolvido com FastAPI, MongoDB e Beanie ODM.

Conclusão

Neste tutorial, você aprendeu como criar um aplicativo CRUD com FastAPI, MongoDB e Beanie ODM. Faça uma autoverificação rápida revisando os objetivos no início do tutorial, você pode encontrar o código usado neste tutorial no GitHub .

Fonte:  https://testdrive.io

#crud #fastapi #mongodb #api 

Como Desenvolver Uma API Assíncrona Com FastAPI E MongoDB

Как легко создать RESTful API с помощью Flask и APIFairy

В этом руководстве показано, как легко создать RESTful API с помощью Flask и APIFairy.

Цели

К концу этого урока вы сможете:

  1. Создавайте конечные точки API в Flask с помощью декораторов, предоставляемых APIFairy.
  2. Используйте Flask-Marshmallow для определения схем ввода/вывода для конечных точек API.
  3. Создайте документацию API с помощью APIFairy.
  4. Интеграция реляционной базы данных с конечными точками API
  5. Реализовать базовую и токеновую аутентификацию с помощью Flask-HTTPAuth.

Что такое APIFairy?

APIFairy — это платформа API, написанная Мигелем Гринбергом, которая позволяет легко создавать API с помощью Flask.

APIFairy предоставляет четыре ключевых компонента для простого создания API во Flask:

  1. Декораторы
  2. Схемы
  3. Аутентификация
  4. Документация

Давайте подробно рассмотрим каждый...

Декораторы

APIFairy предоставляет набор декораторов для определения входов, выходов и аутентификации для каждой конечной точки API:

APIFairy Decorators

APIFairy предоставляет пять основных декораторов:

  1. @arguments — указывает входные аргументы в строке запроса URL.
  2. @body — указывает входное тело JSON как схему
  3. @response — указывает тело вывода JSON в виде схемы
  4. @other_responses — указывает дополнительные ответы (часто ошибки), которые могут быть возвращены ( только документация )
  5. @authenticate — указывает процесс аутентификации

Схемы

Ввод (с помощью @bodyдекоратора) и вывод (с помощью @responseдекоратора) конечной точки API определяются как схемы:

class EntrySchema(ma.Schema):
    """Schema defining the attributes in a journal entry."""
    id = ma.Integer()
    entry = ma.String()
    user_id = ma.Integer()

Схемы используют зефир для определения типов данных как классов.

Аутентификация

Декоратор @authenticateиспользуется для проверки заголовка аутентификации, предоставленного в URL-запросе к каждой конечной точке API. Схема аутентификации реализована с помощью Flask-HTTPAuth , который также был создан Мигелем Гринбергом.

Типичным подходом к аутентификации API будет определение базовой аутентификации для защиты маршрута для получения токена аутентификации:

basic_auth = HTTPBasicAuth()

@basic_auth.verify_password
def verify_password(email, password):
    user = User.query.filter_by(email=email).first()
    if user.is_password_correct(password):
        return user

А также определить аутентификацию токена для защиты большинства маршрутов на основе чувствительного ко времени токена аутентификации:

token_auth = HTTPTokenAuth()

@token_auth.verify_token
def verify_token(auth_token):
    return User.verify_auth_token(auth_token)

Документация

Одной из замечательных особенностей APIFairy является красивая документация по API, которая генерируется автоматически:

Документация по API - главная страница

Документация создается на основе строк документации в исходном коде вместе со следующими переменными конфигурации:

  1. APIFAIRY_TITLE- название проекта
  2. APIFAIRY_VERSION- строка версии проекта
  3. APIFAIRY_UI- формат документации API

Для APIFAIRY_UI, вы можете создавать шаблоны из одного из следующих средств визуализации документации OpenAPI:

  1. Интерфейс Swagger
  2. Редок
  3. РапиДок
  4. Элементы

Полный список доступных переменных конфигурации см. в документации по конфигурации .

Что мы строим?

В этом руководстве вы будете разрабатывать API журнала, позволяющий пользователям вести ежедневный журнал событий. Вы можете найти полный исходный код в репозитории flask-journal-api на GitLab.

Основные используемые пакеты Python:

  1. Flask : микрофреймворк для разработки веб-приложений на Python.
  2. APIFairy : API-фреймворк для Flask, который использует:
    • Flask-Marshmallow : расширение Flask для использования зефира (библиотека сериализации/десериализации объектов)
    • Flask-HTTPAuth : расширение Flask для HTTP-аутентификации.
    • apispec — генератор спецификаций API, поддерживающий спецификацию OpenAPI .
  3. Flask-SQLAlchemy : ORM (реляционное сопоставление объектов) для Flask

Вы будете разрабатывать API постепенно:

  1. Создайте конечные точки API для работы с записями журнала.
  2. Создание документации по API
  3. Добавьте реляционную базу данных для хранения записей журнала.
  4. Добавьте аутентификацию для защиты конечных точек API.

Конечные точки API

Давайте перейдем к созданию API с помощью Flask и APIFairy...

Инициализация проекта

Start by creating a new project folder and a virtual environment:

$ mkdir flask-journal-api
$ cd flask-journal-api
$ python3 -m venv venv
$ source venv/bin/activate
(venv)$

Feel free to swap out virtualenv and Pip for Poetry or Pipenv. For more, review Modern Python Environments.

Go ahead and add the following files and folders:

├── app.py
├── instance
│   └── .gitkeep
├── project
│   ├── __init__.py
│   └── journal_api
│       ├── __init__.py
│       └── routes.py
└── requirements.txt

Next, to install the necessary Python packages, add the dependencies to the requirements.txt file in the project root:

apifairy==0.9.1
Flask==2.1.2
Flask-SQLAlchemy==2.5.1
marshmallow-sqlalchemy==0.28.0

Install:

(venv)$ pip install -r requirements.txt

This Flask project will utilize two best practices for Flask applications:

  1. Application Factory - used for creating the Flask application in a function
  2. Blueprints - used for organizing a group of related views

Application Factory

Start by defining the Application Factory function in project/__init__.py:

from apifairy import APIFairy
from flask import Flask, json
from flask_marshmallow import Marshmallow

# -------------
# Configuration
# -------------

# Create the instances of the Flask extensions in the global scope,
# but without any arguments passed in. These instances are not
# attached to the Flask application at this point.
apifairy = APIFairy()
ma = Marshmallow()


# ----------------------------
# Application Factory Function
# ----------------------------

def create_app():
    # Create the Flask application
    app = Flask(__name__)

    initialize_extensions(app)
    register_blueprints(app)
    return app


# ----------------
# Helper Functions
# ----------------

def initialize_extensions(app):
    # Since the application instance is now created, pass it to each Flask
    # extension instance to bind it to the Flask application instance (app)
    apifairy.init_app(app)
    ma.init_app(app)


def register_blueprints(app):
    # Import the blueprints
    from project.journal_api import journal_api_blueprint

    # Since the application instance is now created, register each Blueprint
    # with the Flask application instance (app)
    app.register_blueprint(journal_api_blueprint, url_prefix='/journal')

После определения функции Application Factory ее можно вызвать в app.py в папке верхнего уровня проекта:

from project import create_app


# Call the application factory function to construct a Flask application
# instance using the development configuration
app = create_app()

План

Давайте определим journal_apiплан. Начните с определения схемы journal_apiв project/journal_api/__init__.py :

"""
The 'journal_api' blueprint handles the API for managing journal entries.
Specifically, this blueprint allows for journal entries to be added, edited,
and deleted.
"""
from flask import Blueprint

journal_api_blueprint = Blueprint('journal_api', __name__, template_folder='templates')

from . import routes

Теперь пришло время определить конечные точки API для журнала в project/journal_api/routes.py .

Начните с необходимого импорта:

from apifairy import body, other_responses, response
from flask import abort

from project import ma
from . import journal_api_blueprint

Для этой начальной версии Flask Journal API база данных будет представлять собой список записей журнала:

# --------
# Database
# --------

messages = [
    dict(id=1, entry='The sun was shining when I woke up this morning.'),
    dict(id=2, entry='I tried a new fruit mixture in my oatmeal for breakfast.'),
    dict(id=3, entry='Today I ate a great sandwich for lunch.')
]

Затем определите схемы для создания новой записи журнала и возврата записей журнала:

# -------
# Schemas
# -------

class NewEntrySchema(ma.Schema):
    """Schema defining the attributes when creating a new journal entry."""
    entry = ma.String(required=True)


class EntrySchema(ma.Schema):
    """Schema defining the attributes in a journal entry."""
    id = ma.Integer()
    entry = ma.String()


new_entry_schema = NewEntrySchema()
entry_schema = EntrySchema()
entries_schema = EntrySchema(many=True)

Оба этих класса схемы наследуются от ma.Schema , предоставляемого Flask-Marshmallow. Также рекомендуется создавать объекты этих схем, так как это позволяет определить схему, которая может возвращать несколько записей (используя many=Trueаргумент).

Теперь мы готовы определить конечные точки API!

Маршруты

Начните с извлечения всех записей журнала:

@journal_api_blueprint.route('/', methods=['GET'])
@response(entries_schema)
def journal():
    """Return all journal entries"""
    return messages

Эта функция представления использует @responseдекоратор, чтобы указать, что возвращаются несколько записей. Функция просмотра возвращает полный список записей журнала ( return messages).

Затем создайте конечную точку API для добавления новой записи журнала:

@journal_api_blueprint.route('/', methods=['POST'])
@body(new_entry_schema)
@response(entry_schema, 201)
def add_journal_entry(kwargs):
    """Add a new journal entry"""
    new_message = dict(**kwargs, id=messages[-1]['id']+1)
    messages.append(new_message)
    return new_message

Эта функция представления использует @bodyдекоратор для определения ввода в конечную точку API и @responseдекоратор для определения выходных данных из конечной точки API.

Входные данные, полученные от @bodyдекоратора, передаются в add_journal_entry()функцию представления в качестве аргумента ( kwargsаргументы ключевого слова ) . Затем эти данные используются для создания новой записи журнала и добавления ее в базу данных:

new_message = dict(**kwargs, id=messages[-1]['id']+1)
messages.append(new_message)

Затем возвращается вновь созданная запись журнала ( return new_message). Обратите внимание, как @responseдекоратор определяет код возврата как 201 (Создано), чтобы указать, что запись журнала была добавлена ​​в базу данных.

Создайте конечную точку API для получения определенной записи журнала:

@journal_api_blueprint.route('/<int:index>', methods=['GET'])
@response(entry_schema)
@other_responses({404: 'Entry not found'})
def get_journal_entry(index):
    """Return a journal entry"""
    if index >= len(messages):
        abort(404)

    return messages[index]

Эта функция просмотра использует @other_responsesдекоратор для указания нестандартных ответов.

Декоратор @other_responsesиспользуется только для целей документации! Он не предоставляет никакой функциональности с точки зрения возврата кодов ошибок.

Create the API endpoint for updating a journal entry:

@journal_api_blueprint.route('/<int:index>', methods=['PUT'])
@body(new_entry_schema)
@response(entry_schema)
@other_responses({404: 'Entry not found'})
def update_journal_entry(data, index):
    """Update a journal entry"""
    if index >= len(messages):
        abort(404)

    messages[index] = dict(data, id=index+1)
    return messages[index]

This view function uses the @body and @response decorators to define the inputs and outputs (respectively) for this API endpoint. Additionally, the @other_responses decorator defines the non-standard response if the journal entry is not found.

Finally, create the API endpoint for deleting a journal entry:

@journal_api_blueprint.route('/<int:index>', methods=['DELETE'])
@other_responses({404: 'Entry not found'})
def delete_journal_entry(index):
    """Delete a journal entry"""
    if index >= len(messages):
        abort(404)

    messages.pop(index)
    return '', 204

This view function does not use the @body and @response decorators, as there are no inputs or outputs for this API endpoint. If the journal entry is successfully deleted, then a 204 (No Content) status code is returned with no data.

Running the Flask Application

To test things out, within one terminal window, configure the Flask application and run the development server:

(venv) $ export FLASK_APP=app.py
(venv) $ export FLASK_ENV=development
(venv) $ flask run

Then, in a different terminal window, you can interact with the API. Feel free to use your tool of choice here, like cURL, HTTPie, Requests, or Postman.

Requests example:

$ python3

>>> import requests
>>>
>>> r = requests.get('http://127.0.0.1:5000/journal/')
>>> print(r.text)
>>>
>>> post_data = {'entry': "some message"}
>>> r = requests.post('http://127.0.0.1:5000/journal/', json=post_data)
>>> print(r.text)

Want to test out the API endpoints more easily? Check out this script, which adds CLI commands for interacting with the API endpoints for retrieving, creating, updating, and deleting journal entries.

Documentation

An incredible feature of APIFairy is the automatic API documentation creation!

There are three key aspects to configuring the API documentation:

  1. Docstrings for the API endpoints (i.e., view functions)
  2. Docstring for the overall API project
  3. Configuration variables to specify the look of the API documentation

Мы уже рассмотрели первый пункт в предыдущем разделе, так как включили строки документации для каждой функции представления. Например, journal()функция просмотра имеет краткое описание назначения этой конечной точки API:

@journal_api_blueprint.route('/', methods=['GET'])
@response(entries_schema)
def journal():
    """Return all journal entries"""
    return messages

Затем нам нужно включить строку документации для описания всего проекта в самом верху файла project/__init__.py :

"""
Welcome to the documentation for the Flask Journal API!

## Introduction

The Flask Journal API is an API (Application Programming Interface) for creating a **daily journal** that documents events that happen each day.

## Key Functionality

The Flask Journal API has the following functionality:

1. Work with journal entries:
  * Create a new journal entry
  * Update a journal entry
  * Delete a journal entry
  * View all journal entries
2. <More to come!>

## Key Modules

This project is written using Python 3.10.1.

The project utilizes the following modules:

* **Flask**: micro-framework for web application development which includes the following dependencies:
  * **click**: package for creating command-line interfaces (CLI)
  * **itsdangerous**: cryptographically sign data
  * **Jinja2**: templating engine
  * **MarkupSafe**: escapes characters so text is safe to use in HTML and XML
  * **Werkzeug**: set of utilities for creating a Python application that can talk to a WSGI server
* **APIFairy**: API framework for Flask which includes the following dependencies:
  * **Flask-Marshmallow** - Flask extension for using Marshmallow (object serialization/deserialization library)
  * **Flask-HTTPAuth** - Flask extension for HTTP authentication
  * **apispec** - API specification generator that supports the OpenAPI specification
* **pytest**: framework for testing Python projects
"""
...

Эта строка документации используется для описания всего проекта, включая ключевые функции и ключевые пакеты Python, используемые в проекте.

Наконец, необходимо определить некоторые переменные конфигурации, чтобы указать внешний вид документации API. Обновите create_app()функцию в project/__init__.py :

def create_app():
    # Create the Flask application
    app = Flask(__name__)

    # Configure the API documentation
    app.config['APIFAIRY_TITLE'] = 'Flask Journal API'
    app.config['APIFAIRY_VERSION'] = '0.1'
    app.config['APIFAIRY_UI'] = 'elements'

    initialize_extensions(app)
    register_blueprints(app)

    return app

Готовы ознакомиться с документацией по проекту? Запустите сервер разработки Flask через flask run, а затем перейдите по адресу http://127.0.0.1:5000/docs , чтобы просмотреть документацию по API, созданную APIFairy:

Документация по API - главная страница

На левой панели есть список конечных точек API для journal_apiсхемы. При нажатии на одну из конечных точек отображаются все сведения об этой конечной точке:

Документация по API — получить конечную точку API для записи журнала

Что удивительно в этой документации API, так это возможность увидеть, как работают конечные точки API (при условии, что сервер разработки Flask работает). На правой панели документации введите индекс записи журнала и нажмите «Отправить запрос API». Затем отображается ответ API:

Документация по API - Получить ответ API записи журнала

Эта интерактивная документация облегчает пользователям понимание API!

База данных

For demonstration purposes, a SQLite database will be used in this tutorial.

Configuration

Since Flask-SQLAlchemy was already installed at the beginning of this tutorial, we need to configure it in the project/__init__.py file.

Start by creating a SQLAlchemy() object in the 'Configuration' section:

...

from apifairy import APIFairy
from flask import Flask, json
from flask_marshmallow import Marshmallow
from flask_sqlalchemy import SQLAlchemy  # <-- NEW!!


# -------------
# Configuration
# -------------

# Create the instances of the Flask extensions in the global scope,
# but without any arguments passed in. These instances are not
# attached to the Flask application at this point.
apifairy = APIFairy()
ma = Marshmallow()
database = SQLAlchemy()  # <-- NEW!!

...

Next, update the create_app() function to specify the necessary configuration variables:

def create_app():
    # Create the Flask application
    app = Flask(__name__)

    # Configure the API documentation
    app.config['APIFAIRY_TITLE'] = 'Flask Journal API'
    app.config['APIFAIRY_VERSION'] = '0.1'
    app.config['APIFAIRY_UI'] = 'elements'

    # NEW!
    # Configure the SQLite database (intended for development only!)
    app.config['SQLALCHEMY_DATABASE_URI'] = f"sqlite:///{os.path.join(os.getcwd(), 'instance', 'app.db')}"
    app.config['SQLALCHEMY_TRACK_MODIFICATIONS'] = False

    initialize_extensions(app)
    register_blueprints(app)

    return app

Add the import to the top:

import os

The SQLALCHEMY_DATABASE_URI configuration variable is critical to identifying the location of the SQLite database. For this tutorial, the database is stored in instance/app.db.

Finally, update the initialize_extensions() function to initialize the Flask-SQLAlchemy object:

def initialize_extensions(app):
    # Since the application instance is now created, pass it to each Flask
    # extension instance to bind it to the Flask application instance (app)
    apifairy.init_app(app)
    ma.init_app(app)
    database.init_app(app)  # <-- NEW!!

Want to learn more about how this Flask app is wired together? Check out my course on how to build, test, and deploy a Flask application:

Database Model

Create a new project/models.py file to define the database table to represent the journal entries:

from project import database


class Entry(database.Model):
    """Class that represents a journal entry."""
    __tablename__ = 'entries'

    id = database.Column(database.Integer, primary_key=True)
    entry = database.Column(database.String, nullable=False)

    def __init__(self, entry: str):
        self.entry = entry

    def update(self, entry: str):
        self.entry = entry

    def __repr__(self):
        return f'<Entry: {self.entry}>'

This new class, Entry, specifies that the entries database table will contain two elements (for now!) to represent a journal entry:

  1. id - the primary key (primary_key=True) for the table, which means that it's a unique identifier for each element (row) in the table
  2. entry - string for storing the journal entry text

While models.py defines the database table, it doesn't create the tables in the SQLite database. To create the tables, start the Flask shell in a terminal window:

(venv)$ flask shell

>>> from project import database
>>> database.drop_all()
>>> database.create_all()
>>> quit()

(venv)$

Journal API Updates

Since we're progressing on to use a SQLite database, start by deleting the temporary database (Python list) that was defined in project/journal_api/routes.py:

# --------
# Database
# --------

messages = [
    dict(id=1, entry='The sun was shining when I woke up this morning.'),
    dict(id=2, entry='I tried a new fruit mixture in my oatmeal for breakfast.'),
    dict(id=3, entry='Today I ate a great sandwich for lunch.')
]

Next, we need to update each API endpoint (i.e., the view functions) to utilize the SQLite database.

Start by updating the journal() view function:

@journal_api_blueprint.route('/', methods=['GET'])
@response(entries_schema)
def journal():
    """Return all journal entries"""
    return Entry.query.all()

The complete list of journal entries is now retrieved from the SQLite database. Notice how the schemas or decorators for this view function did not need to change... only the underlying process for getting the users changed!

Add the import:

from project.models import Entry

Next, update the add_journal_entry() view function:

@journal_api_blueprint.route('/', methods=['POST'])
@body(new_entry_schema)
@response(entry_schema, 201)
def add_journal_entry(kwargs):
    """Add a new journal entry"""
    new_message = Entry(**kwargs)
    database.session.add(new_message)
    database.session.commit()
    return new_message

The inputs to this view function are specified by new_entry_schema:

class NewEntrySchema(ma.Schema):
    """Schema defining the attributes when creating a new journal entry."""
    entry = ma.String(required=True)

new_entry_schema = NewEntrySchema()

The entry string is used to create a new instance of the Entry class (defined in models.py) and this journal entry is then added to the database.

Add the import:

from project import database

Next, update get_journal_entry():

@journal_api_blueprint.route('/<int:index>', methods=['GET'])
@response(entry_schema)
@other_responses({404: 'Entry not found'})
def get_journal_entry(index):
    """Return a journal entry"""
    entry = Entry.query.filter_by(id=index).first_or_404()
    return entry

This function now attempts to look up the specified journal entry (based on the index):

entry = Entry.query.filter_by(id=index).first_or_404()

If the entry exists, it's returned to the user. If the entry does not exist, a 404 (Not Found) error is returned.

Next, update update_journal_entry():

@journal_api_blueprint.route('/<int:index>', methods=['PUT'])
@body(new_entry_schema)
@response(entry_schema)
@other_responses({404: 'Entry not found'})
def update_journal_entry(data, index):
    """Update a journal entry"""
    entry = Entry.query.filter_by(id=index).first_or_404()
    entry.update(data['entry'])
    database.session.add(entry)
    database.session.commit()
    return entry

The update_journal_entry() view function now attempts to retrieve the specified journal entry:

entry = Entry.query.filter_by(id=index).first_or_404()

If the journal entry exists, the entry is updated with the new text and then saved to the database.

Finally, update delete_journal_entry():

@journal_api_blueprint.route('/<int:index>', methods=['DELETE'])
@other_responses({404: 'Entry not found'})
def delete_journal_entry(index):
    """Delete a journal entry"""
    entry = Entry.query.filter_by(id=index).first_or_404()
    database.session.delete(entry)
    database.session.commit()
    return '', 204

If the specified journal entry is found, then it's deleted from the database.

Run the development server. Test out each of the endpoints to ensure they still work.

Error Handling

Since this Flask project is an API, error codes should be returned in JSON format instead of the typical HTML format.

In the Flask project, this can be accomplished by using a custom error handler. In project/__init__.py, define a new function (register_error_handlers()) at the bottom of the file:

def register_error_handlers(app):
    @app.errorhandler(HTTPException)
    def handle_http_exception(e):
        """Return JSON instead of HTML for HTTP errors."""
        # Start with the correct headers and status code from the error
        response = e.get_response()
        # Replace the body with JSON
        response.data = json.dumps({
            'code': e.code,
            'name': e.name,
            'description': e.description,
        })
        response.content_type = 'application/json'
        return response

This function registers a new error handler for when an HTTPException is raised to convert the output into JSON format.

Add the import:

from werkzeug.exceptions import HTTPException

Also, update the Application Factory function, create_app(), to call this new function:

def create_app():
    # Create the Flask application
    app = Flask(__name__)

    # Configure the API documentation
    app.config['APIFAIRY_TITLE'] = 'Flask Journal API'
    app.config['APIFAIRY_VERSION'] = '0.1'
    app.config['APIFAIRY_UI'] = 'elements'

    # Configure the SQLite database (intended for development only!)
    app.config['SQLALCHEMY_DATABASE_URI'] = f"sqlite:///{os.path.join(os.getcwd(), 'instance', 'app.db')}"
    app.config['SQLALCHEMY_TRACK_MODIFICATIONS'] = False

    initialize_extensions(app)
    register_blueprints(app)
    register_error_handlers(app)  # NEW!!

    return app

Authentication

Authentication is the process of validating the identity of a user attempting to access a system, which in this case is the API.

Authorization, on the other hand, is the process of verifying what specific resources a specific user should have access to.

APIFairy utilizes Flask-HTTPAuth for authentication support. In this tutorial, we'll be using Flask-HTTPAuth in two manners:

  1. Basic Authentication - used to generate a token based on the user's email/password
  2. Token Authentication - used to authenticate the user on all other API endpoints

Аутентификация токена , используемая через Flask-HTTPAuth, часто называется аутентификацией носителя, поскольку процесс вызывает предоставление доступа к «носителю» токена. Маркер должен быть включен в заголовки HTTP в заголовке авторизации, например «Авторизация: носитель».

На следующей диаграмме показан типичный процесс взаимодействия нового пользователя с приложением для получения маркера аутентификации.

Блок-схема API журнала Flask

Конфигурация

Поскольку Flask-HTTPAuth уже был установлен при установке APIFairy в начале этого руководства, нам просто нужно настроить его в файле project/__init__.py .

Начните с создания отдельных объектов для базовой и токеновой аутентификации:

...

import os

from apifairy import APIFairy
from flask import Flask, json
from flask_httpauth import HTTPBasicAuth, HTTPTokenAuth  # NEW!!
from flask_marshmallow import Marshmallow
from flask_sqlalchemy import SQLAlchemy
from werkzeug.exceptions import HTTPException


# -------------
# Configuration
# -------------

# Create the instances of the Flask extensions in the global scope,
# but without any arguments passed in. These instances are not
# attached to the Flask application at this point.
apifairy = APIFairy()
ma = Marshmallow()
database = SQLAlchemy()
basic_auth = HTTPBasicAuth()  # NEW!!
token_auth = HTTPTokenAuth()  # NEW!!

...

Никаких дальнейших обновлений в project/__init__.py не требуется .

Модель базы данных

В project/models.py необходимо создать новую Userмодель для представления пользователя:

class User(database.Model):
    __tablename__ = 'users'

    id = database.Column(database.Integer, primary_key=True)
    email = database.Column(database.String, unique=True, nullable=False)
    password_hashed = database.Column(database.String(128), nullable=False)
    entries = database.relationship('Entry', backref='user', lazy='dynamic')
    auth_token = database.Column(database.String(64), index=True)
    auth_token_expiration = database.Column(database.DateTime)

    def __init__(self, email: str, password_plaintext: str):
        """Create a new User object."""
        self.email = email
        self.password_hashed = self._generate_password_hash(password_plaintext)

    def is_password_correct(self, password_plaintext: str):
        return check_password_hash(self.password_hashed, password_plaintext)

    def set_password(self, password_plaintext: str):
        self.password_hashed = self._generate_password_hash(password_plaintext)

    @staticmethod
    def _generate_password_hash(password_plaintext):
        return generate_password_hash(password_plaintext)

    def generate_auth_token(self):
        self.auth_token = secrets.token_urlsafe()
        self.auth_token_expiration = datetime.utcnow() + timedelta(minutes=60)
        return self.auth_token

    @staticmethod
    def verify_auth_token(auth_token):
        user = User.query.filter_by(auth_token=auth_token).first()
        if user and user.auth_token_expiration > datetime.utcnow():
            return user

    def revoke_auth_token(self):
        self.auth_token_expiration = datetime.utcnow()

    def __repr__(self):
        return f'<User: {self.email}>'

Добавьте импорт:

import secrets
from datetime import datetime, timedelta

from werkzeug.security import check_password_hash, generate_password_hash

Модель Userиспользует werkzeug.securityдля хэширования пароль пользователя перед его сохранением в базе данных.

Помните: никогда не храните открытый пароль в базе данных!

Модель Userиспользует secretsдля создания токена аутентификации для конкретного пользователя. Этот токен создается в generate_auth_token()методе и включает дату/время истечения срока действия 60 минут в будущем:

def generate_auth_token(self):
    self.auth_token = secrets.token_urlsafe()
    self.auth_token_expiration = datetime.utcnow() + timedelta(minutes=60)
    return self.auth_token

Существует статический метод, verify_auth_token()который используется для проверки токена аутентификации (с учетом времени истечения срока действия) и возврата пользователя из действительного токена:

@staticmethod
def verify_auth_token(auth_token):
    user = User.query.filter_by(auth_token=auth_token).first()
    if user and user.auth_token_expiration > datetime.utcnow():
        return user

Еще один интересный метод — revoke_auth_token(), который используется для отзыва токена аутентификации для конкретного пользователя:

def revoke_auth_token(self):
    self.auth_token_expiration = datetime.utcnow()

Начальная модель

Чтобы установить отношение «один ко многим» между пользователем («один») и его записями («многие»), Entryнеобходимо обновить модель, чтобы связать таблицы entriesи вместе:users

class Entry(database.Model):
    """Class that represents a journal entry."""
    __tablename__ = 'entries'

    id = database.Column(database.Integer, primary_key=True)
    entry = database.Column(database.String, nullable=False)
    user_id = database.Column(database.Integer, database.ForeignKey('users.id'))  # <-- NEW!!

    def __init__(self, entry: str):
        self.entry = entry

    def update(self, entry: str):
        self.entry = entry

    def __repr__(self):
        return f'<Entry: {self.entry}>'

Модель Userуже содержит обратную ссылку на entriesтаблицу:

entries = database.relationship('Entry', backref='user', lazy='dynamic')

Схема пользовательского API

Функциональность управления пользователями проекта Flask будет определена в отдельном проекте Blueprint под названием users_api_blueprint.

Начните с создания нового каталога в «проекте» с именем «users_api». В этом каталоге создайте файл __init__.py :

from flask import Blueprint


users_api_blueprint = Blueprint('users_api', __name__)

from . import authentication, routes

Этот новый Blueprint необходимо зарегистрировать в Flask appв файле Projects/__init__.py внутри register_blueprints()функции:

def register_blueprints(app):
    # Import the blueprints
    from project.journal_api import journal_api_blueprint
    from project.users_api import users_api_blueprint  # NEW!!

    # Since the application instance is now created, register each Blueprint
    # with the Flask application instance (app)
    app.register_blueprint(journal_api_blueprint, url_prefix='/journal')
    app.register_blueprint(users_api_blueprint, url_prefix='/users')  # NEW!!

Функции аутентификации

Чтобы использовать Flask-HTTPAuth, необходимо определить несколько функций для проверки учетных данных пользователя.

Создайте новый файл project/users_api/authentication.py для обработки базовой проверки подлинности и проверки подлинности токена.

Для базовой аутентификации (проверка электронной почты и пароля пользователя):

from werkzeug.exceptions import Forbidden, Unauthorized

from project import basic_auth, token_auth
from project.models import User


@basic_auth.verify_password
def verify_password(email, password):
    user = User.query.filter_by(email=email).first()
    if user is None:
        return None

    if user.is_password_correct(password):
        return user


@basic_auth.error_handler
def basic_auth_error(status=401):
    error = (Forbidden if status == 403 else Unauthorized)()
    return {
        'code': error.code,
        'message': error.name,
        'description': error.description,
    }, error.code, {'WWW-Authenticate': 'Form'}

Функция verify_password()используется для проверки существования пользователя и правильности его пароля. Эта функция будет использоваться Flask-HTTPAuth для проверки пароля, когда требуется базовая аутентификация (спасибо @basic_auth.verify_passwordдекоратору).

Кроме того, для базовой проверки подлинности определен обработчик ошибок, который возвращает информацию об ошибке в формате JSON.

Для аутентификации токена (обработка токена, чтобы определить, действителен ли пользователь):

@token_auth.verify_token
def verify_token(auth_token):
    return User.verify_auth_token(auth_token)


@token_auth.error_handler
def token_auth_error(status=401):
    error = (Forbidden if status == 403 else Unauthorized)()
    return {
        'code': error.code,
        'message': error.name,
        'description': error.description,
    }, error.code

Функция verify_token()используется для проверки правильности токена аутентификации. Эта функция будет использоваться Flask-HTTPAuth для проверки токена, когда требуется аутентификация токена (спасибо @token_auth.verify_tokenдекоратору).

Кроме того, для проверки подлинности токена определен обработчик ошибок, который возвращает информацию об ошибке в формате JSON.

Маршруты пользователей

В users_api_blueprint, будет два маршрута:

  1. Регистрация нового пользователя
  2. Получение токена аутентификации

Для начала необходимо определить новый набор схем (с использованием зефира) в файле project/users_api/routes.py :

from project import ma

from . import users_api_blueprint

# -------
# Schemas
# -------

class NewUserSchema(ma.Schema):
    """Schema defining the attributes when creating a new user."""
    email = ma.String()
    password_plaintext = ma.String()


class UserSchema(ma.Schema):
    """Schema defining the attributes of a user."""
    id = ma.Integer()
    email = ma.String()


class TokenSchema(ma.Schema):
    """Schema defining the attributes of a token."""
    token = ma.String()


new_user_schema = NewUserSchema()
user_schema = UserSchema()
token_schema = TokenSchema()

Эти схемы будут использоваться для определения входных и выходных данных для функций просмотра, определенных в этом файле.

Регистрация нового пользователя

Затем определите функцию представления для регистрации нового пользователя:

@users_api_blueprint.route('/', methods=['POST'])
@body(new_user_schema)
@response(user_schema, 201)
def register(kwargs):
    """Create a new user"""
    new_user = User(**kwargs)
    database.session.add(new_user)
    database.session.commit()
    return new_user

Добавьте импорт:

from apifairy import authenticate, body, other_responses, response

from project import basic_auth, database, ma
from project.models import User

Эта конечная точка API использует , new_user_schemaчтобы указать, что адрес электронной почты и пароль являются входными данными.

ПРИМЕЧАНИЕ. Поскольку электронная почта и пароль отправляются на эту конечную точку API, самое время вспомнить, что использование HTTP допустимо во время тестирования разработки, но HTTPS (безопасный) всегда следует использовать в рабочей среде.

Электронная почта и пароль (определенные как kwargsаргументы ключевого слова) затем распаковываются для создания нового Userобъекта, который сохраняется в базе данных:

new_user = User(**kwargs)
database.session.add(new_user)
database.session.commit()

Выходные данные конечной точки API определяются user_schemaидентификатором и адресом электронной почты нового пользователя.

Получение токена аутентификации

Другая функция представления, которую необходимо определить в файле project/users_api/routes.py , предназначена для получения токена аутентификации:

@users_api_blueprint.route('/get-auth-token', methods=['POST'])
@authenticate(basic_auth)
@response(token_schema)
@other_responses({401: 'Invalid username or password'})
def get_auth_token():
    """Get authentication token"""
    user = basic_auth.current_user()
    token = user.generate_auth_token()
    database.session.add(user)
    database.session.commit()
    return dict(token=token)

Декоратор @authenticateиспользуется в этом руководстве впервые и указывает, что для защиты этого маршрута следует использовать базовую аутентификацию:

@authenticate(basic_auth)

Когда пользователь хочет получить свой токен аутентификации, ему необходимо отправить запрос POST на эту конечную точку API с адресом электронной почты и паролем, встроенными в заголовок «Авторизация». Например, следующая команда Python с использованием пакета Requests может быть выполнена для этой конечной точки API:

>>> import requests
>>> r = requests.post(
    'http://127.0.0.1:5000/users/get-auth-token',
    auth=('pkennedy@hey.com', 'FlaskIsAwesome123')
)

Если базовая аутентификация прошла успешно, функция просмотра извлекает текущего пользователя, используя current_user()метод, предоставляемый Flask-HTTPAuth:

user = basic_auth.current_user()

Для этого пользователя создается новый токен аутентификации:

token = user.generate_auth_token()

И этот токен сохраняется в базе данных, чтобы его можно было использовать для аутентификации пользователя в будущем (по крайней мере, в течение следующих 60 минут!).

Наконец, новый токен аутентификации возвращается пользователю для сохранения для всех последующих вызовов API.

Обновления конечной точки API

При наличии процесса аутентификации пришло время добавить некоторые средства защиты к существующим конечным точкам API, чтобы убедиться, что только действительные пользователи могут получить доступ к приложению.

Эти обновления предназначены для функций представления, определенных в файле Projects/journal_api/routes.py .

Во- первых, обновите, journal()чтобы возвращались только записи журнала для текущего пользователя:

@journal_api_blueprint.route('/', methods=['GET'])
@authenticate(token_auth)
@response(entries_schema)
def journal():
    """Return journal entries"""
    user = token_auth.current_user()
    return Entry.query.filter_by(user_id=user.id).all()

Обновите импорт вверху следующим образом:

from apifairy import authenticate, body, other_responses, response
from flask import abort

from project import database, ma, token_auth
from project.models import Entry

from . import journal_api_blueprint

@authenticateДекоратор указывает, что при доступе к этой конечной точке API необходимо использовать аутентификацию с помощью токена . В качестве примера можно сделать следующий запрос GET с использованием запросов ( после получения токена аутентификации ):

>>> import requests
>>> headers = {'Authorization': f'Bearer {auth_token}'}
>>> r = requests.get('http://127.0.0.1:5000/journal/', headers=headers)

После аутентификации пользователя полный список записей журнала извлекается из базы данных на основе идентификатора пользователя:

user = token_auth.current_user()
return Entry.query.filter_by(user_id=user.id).all()

Выходные данные этой конечной точки API определяются @responseдекоратором, который представляет собой список записей журнала (идентификатор, запись, идентификатор пользователя).

Далее обновите add_journal_entry():

@journal_api_blueprint.route('/', methods=['POST'])
@authenticate(token_auth)
@body(new_entry_schema)
@response(entry_schema, 201)
def add_journal_entry(kwargs):
    """Add a new journal entry"""
    user = token_auth.current_user()
    new_message = Entry(user_id=user.id, **kwargs)
    database.session.add(new_message)
    database.session.commit()
    return new_message

Как и в предыдущей функции представления, @authenticateдекоратор используется для указания того, что при доступе к этой конечной точке API необходимо использовать аутентификацию с помощью токена. Кроме того, запись журнала теперь добавляется путем указания идентификатора пользователя, который должен быть связан с записью журнала:

user = token_auth.current_user()
new_message = Entry(user_id=user.id, **kwargs)

Новая запись журнала сохраняется в базе данных, и запись журнала возвращается (как определено @responseдекоратором).

Далее обновите get_journal_entry():

@journal_api_blueprint.route('/<int:index>', methods=['GET'])
@authenticate(token_auth)
@response(entry_schema)
@other_responses({403: 'Forbidden', 404: 'Entry not found'})
def get_journal_entry(index):
    """Return a journal entry"""
    user = token_auth.current_user()
    entry = Entry.query.filter_by(id=index).first_or_404()

    if entry.user_id != user.id:
        abort(403)
    return entry

Добавляется @authenticateдекоратор, чтобы указать, что для доступа к этой конечной точке API необходима проверка подлинности токена.

При попытке получить запись журнала добавляется дополнительная проверка, чтобы убедиться, что пользователь, пытающийся получить доступ к записи журнала, является фактическим «владельцем» записи. Если нет, то через abort()функцию из Flask возвращается код ошибки 403 (Forbidden):

if entry.user_id != user.id:
        abort(403)

Обратите внимание, что эта конечная точка API имеет два нестандартных ответа, указанных @other_responsesдекоратором:

@other_responses({403: 'Forbidden', 404: 'Entry not found'})

Напоминание: @other_responsesдекоратор предназначен только для документации; функция просмотра несет ответственность за возникновение этих ошибок.

Далее обновите update_journal_entry():

@journal_api_blueprint.route('/<int:index>', methods=['PUT'])
@authenticate(token_auth)
@body(new_entry_schema)
@response(entry_schema)
@other_responses({403: 'Forbidden', 404: 'Entry not found'})
def update_journal_entry(data, index):
    """Update a journal entry"""
    user = token_auth.current_user()
    entry = Entry.query.filter_by(id=index).first_or_404()

    if entry.user_id != user.id:
        abort(403)

    entry.update(data['entry'])
    database.session.add(entry)
    database.session.commit()
    return entry

Обновления этой функции просмотра аналогичны другим функциям просмотра в этом разделе:

  1. @authenticateдекоратор указывает, что для доступа к этой конечной точке API необходима аутентификация токена.
  2. Только пользователь, который «владеет» записью журнала, может обновлять запись (в противном случае 403 (Запрещено))

Наконец, обновите delete_journal_entry():

@journal_api_blueprint.route('/<int:index>', methods=['DELETE'])
@authenticate(token_auth)
@other_responses({403: 'Forbidden', 404: 'Entry not found'})
def delete_journal_entry(index):
    """Delete a journal entry"""
    user = token_auth.current_user()
    entry = Entry.query.filter_by(id=index).first_or_404()

    if entry.user_id != user.id:
        abort(403)

    database.session.delete(entry)
    database.session.commit()
    return '', 204

Вывод

В этом руководстве показано, как легко и быстро создать API во Flask с помощью APIFairy.

Декораторы — это ключ к определению конечных точек API:

  • Входы :
    • @arguments- входные аргументы из строки запроса URL
    • @body- структура JSON-запроса
  • Выходы :
    • @response- структура ответа JSON
  • Аутентификация :
    • @authenticate- подход к аутентификации с использованием Flask-HTTPAuth
  • Ошибки :
    • @other_responses- нестандартные ответы, такие как коды ошибок HTTP

Кроме того, документация API, созданная APIFairy, превосходна и предоставляет ключевую информацию для пользователей приложения.

Источник:  https://testdriven.io

#api #flask 

Как легко создать RESTful API с помощью Flask и APIFairy
笹田  洋介

笹田 洋介

1656746661

如何使用 Flask 和 APIFairy 輕鬆創建 RESTful API

本教程演示瞭如何使用 Flask 和 APIFairy 輕鬆創建 RESTful API。

目標

在本教程結束時,您將能夠:

  1. 使用 APIFairy 提供的裝飾器在 Flask 中創建 API 端點
  2. 利用 Flask-Marshmallow 定義 API 端點的輸入/輸出模式
  3. 使用 APIFairy 生成 API 文檔
  4. 將關係數據庫與 API 端點集成
  5. 使用 Flask-HTTPAuth 實現基本和令牌認證

什麼是 APIFairy?

APIFairy是由 Miguel Grinberg 編寫的 API 框架,它允許使用 Flask 輕鬆創建 API。

APIFairy 為在 Flask 中輕鬆創建 API 提供了四個關鍵組件:

  1. 裝飾器
  2. 模式
  3. 驗證
  4. 文檔

讓我們詳細探索每一個......

裝飾器

APIFairy 提供了一組裝飾器,用於定義每個 API 端點的輸入、輸出和身份驗證:

APIFairy 裝飾器

APIFairy 提供了五個核心裝飾器:

  1. @arguments - 在 URL 的查詢字符串中指定輸入參數
  2. @body - 將輸入 JSON 正文指定為模式
  3. @response - 將輸出 JSON 正文指定為模式
  4. @other_responses - 指定可以返回的附加響應(通常是錯誤)(僅限文檔
  5. @authenticate - 指定認證過程

模式

API 端點的輸入(使用@body裝飾器)和輸出(使用@response裝飾器)被定義為模式:

class EntrySchema(ma.Schema):
    """Schema defining the attributes in a journal entry."""
    id = ma.Integer()
    entry = ma.String()
    user_id = ma.Integer()

這些模式利用棉花糖將數據類型定義為類。

驗證

@authenticate裝飾器用於檢查 URL 請求中提供給每個 API 端點的身份驗證標頭。身份驗證方案是使用Flask-HTTPAuth實現的,它也是由 Miguel Grinberg 創建的。

典型的 API 身份驗證方法是定義基本身份驗證以保護檢索身份驗證令牌的路由:

basic_auth = HTTPBasicAuth()

@basic_auth.verify_password
def verify_password(email, password):
    user = User.query.filter_by(email=email).first()
    if user.is_password_correct(password):
        return user

並且還定義令牌身份驗證以保護基於時間敏感的身份驗證令牌的大多數路由:

token_auth = HTTPTokenAuth()

@token_auth.verify_token
def verify_token(auth_token):
    return User.verify_auth_token(auth_token)

文檔

APIFairy 的一大特色是自動生成的精美 API 文檔:

API 文檔 - 主頁

該文檔是根據源代碼中的文檔字符串以及以下配置變量生成的:

  1. APIFAIRY_TITLE- 項目名稱
  2. APIFAIRY_VERSION- 項目的版本字符串
  3. APIFAIRY_UI- API 文檔的格式

對於APIFAIRY_UI,您可以從以下 OpenAPI 文檔渲染器之一生成模板:

  1. 招搖用戶界面
  2. 重新文檔
  3. RapiDoc
  4. 元素

有關可用配置變量的完整列表,請參閱配置文檔。

我們在建造什麼?

您將在本教程中開發一個日誌 API,允許用戶每天記錄事件。您可以在 GitLab 上的flask-journal-api存儲庫中找到完整的源代碼。

使用的關鍵 Python 包:

  1. Flask:用於 Python Web 應用程序開發的微框架
  2. APIFairy:Flask 的 API 框架,它使用 -
  3. Flask-SQLAlchemy : Flask 的 ORM(對象關係映射器)

您將逐步開發 API:

  1. 創建用於處理日記條目的 API 端點
  2. 生成 API 文檔
  3. 添加用於存儲日記帳分錄的關係數據庫
  4. 添加身份驗證以保護 API 端點

API 端點

讓我們開始使用 Flask 和 APIFairy 創建 API……

項目初始化

Start by creating a new project folder and a virtual environment:

$ mkdir flask-journal-api
$ cd flask-journal-api
$ python3 -m venv venv
$ source venv/bin/activate
(venv)$

Feel free to swap out virtualenv and Pip for Poetry or Pipenv. For more, review Modern Python Environments.

Go ahead and add the following files and folders:

├── app.py
├── instance
│   └── .gitkeep
├── project
│   ├── __init__.py
│   └── journal_api
│       ├── __init__.py
│       └── routes.py
└── requirements.txt

Next, to install the necessary Python packages, add the dependencies to the requirements.txt file in the project root:

apifairy==0.9.1
Flask==2.1.2
Flask-SQLAlchemy==2.5.1
marshmallow-sqlalchemy==0.28.0

Install:

(venv)$ pip install -r requirements.txt

This Flask project will utilize two best practices for Flask applications:

  1. Application Factory - used for creating the Flask application in a function
  2. Blueprints - used for organizing a group of related views

Application Factory

Start by defining the Application Factory function in project/__init__.py:

from apifairy import APIFairy
from flask import Flask, json
from flask_marshmallow import Marshmallow

# -------------
# Configuration
# -------------

# Create the instances of the Flask extensions in the global scope,
# but without any arguments passed in. These instances are not
# attached to the Flask application at this point.
apifairy = APIFairy()
ma = Marshmallow()


# ----------------------------
# Application Factory Function
# ----------------------------

def create_app():
    # Create the Flask application
    app = Flask(__name__)

    initialize_extensions(app)
    register_blueprints(app)
    return app


# ----------------
# Helper Functions
# ----------------

def initialize_extensions(app):
    # Since the application instance is now created, pass it to each Flask
    # extension instance to bind it to the Flask application instance (app)
    apifairy.init_app(app)
    ma.init_app(app)


def register_blueprints(app):
    # Import the blueprints
    from project.journal_api import journal_api_blueprint

    # Since the application instance is now created, register each Blueprint
    # with the Flask application instance (app)
    app.register_blueprint(journal_api_blueprint, url_prefix='/journal')

With the Application Factory function defined, it can be called in app.py in the top-level folder of the project:

from project import create_app


# Call the application factory function to construct a Flask application
# instance using the development configuration
app = create_app()

Blueprint

Let's define the journal_api blueprint. Start by defining the journal_api blueprint in project/journal_api/__init__.py:

"""
The 'journal_api' blueprint handles the API for managing journal entries.
Specifically, this blueprint allows for journal entries to be added, edited,
and deleted.
"""
from flask import Blueprint

journal_api_blueprint = Blueprint('journal_api', __name__, template_folder='templates')

from . import routes

Now it's time to define the API endpoints for the journal in project/journal_api/routes.py.

Start with the necessary imports:

from apifairy import body, other_responses, response
from flask import abort

from project import ma
from . import journal_api_blueprint

For this initial version of the Flask Journal API, the database will be a list of journal entries:

# --------
# Database
# --------

messages = [
    dict(id=1, entry='The sun was shining when I woke up this morning.'),
    dict(id=2, entry='I tried a new fruit mixture in my oatmeal for breakfast.'),
    dict(id=3, entry='Today I ate a great sandwich for lunch.')
]

Next, define the schemas for creating a new journal entry and for returning the journal entries:

# -------
# Schemas
# -------

class NewEntrySchema(ma.Schema):
    """Schema defining the attributes when creating a new journal entry."""
    entry = ma.String(required=True)


class EntrySchema(ma.Schema):
    """Schema defining the attributes in a journal entry."""
    id = ma.Integer()
    entry = ma.String()


new_entry_schema = NewEntrySchema()
entry_schema = EntrySchema()
entries_schema = EntrySchema(many=True)

Both of these schema classes inherit from ma.Schema, which is provided by Flask-Marshmallow. It's also a good idea to create objects of these schemas, as this allows you to define a schema that can return multiple entries (using the many=True argument).

Now we're ready to define the API endpoints!

Routes

Start with retrieving all the journal entries:

@journal_api_blueprint.route('/', methods=['GET'])
@response(entries_schema)
def journal():
    """Return all journal entries"""
    return messages

This view function uses the @response decorator to define that multiple entries are returned. The view function returns the full list of journal entries (return messages).

Next, create the API endpoint for adding a new journal entry:

@journal_api_blueprint.route('/', methods=['POST'])
@body(new_entry_schema)
@response(entry_schema, 201)
def add_journal_entry(kwargs):
    """Add a new journal entry"""
    new_message = dict(**kwargs, id=messages[-1]['id']+1)
    messages.append(new_message)
    return new_message

This view function uses the @body decorator to define the input to the API endpoint and the @response decorator to define the output from the API endpoint.

從裝飾器解析的輸入數據作為(關鍵字參數)參數傳遞@bodyadd_journal_entry()視圖函數。然後使用此數據創建新的日記帳分錄並將其添加到數據庫中:kwargs

new_message = dict(**kwargs, id=messages[-1]['id']+1)
messages.append(new_message)

然後返回新創建的日記帳分錄 ( return new_message)。請注意@response裝飾器如何將返回碼定義為 201(已創建),以表明日記帳分錄已添加到數據庫中。

創建用於檢索特定日記條目的 API 端點:

@journal_api_blueprint.route('/<int:index>', methods=['GET'])
@response(entry_schema)
@other_responses({404: 'Entry not found'})
def get_journal_entry(index):
    """Return a journal entry"""
    if index >= len(messages):
        abort(404)

    return messages[index]

此視圖函數使用@other_responses裝飾器指定非標準響應。

裝飾器@other_responses僅用於文檔目的!它不提供任何返回錯誤代碼的功能。

創建用於更新日誌條目的 API 端點:

@journal_api_blueprint.route('/<int:index>', methods=['PUT'])
@body(new_entry_schema)
@response(entry_schema)
@other_responses({404: 'Entry not found'})
def update_journal_entry(data, index):
    """Update a journal entry"""
    if index >= len(messages):
        abort(404)

    messages[index] = dict(data, id=index+1)
    return messages[index]

這個視圖函數使用@body@response裝飾器來定義這個 API 端點的輸入和輸出(分別)。此外,@other_responses如果找不到日記帳分錄,裝飾器會定義非標準響應。

最後,創建用於刪除日誌條目的 API 端點:

@journal_api_blueprint.route('/<int:index>', methods=['DELETE'])
@other_responses({404: 'Entry not found'})
def delete_journal_entry(index):
    """Delete a journal entry"""
    if index >= len(messages):
        abort(404)

    messages.pop(index)
    return '', 204

此視圖函數不使用@bodyand@response裝飾器,因為此 API 端點沒有輸入或輸出。如果日誌條目被成功刪除,則返回 204(無內容)狀態代碼且無數據。

運行 Flask 應用程序

要進行測試,請在一個終端窗口中配置 Flask 應用程序並運行開發服務器:

(venv) $ export FLASK_APP=app.py
(venv) $ export FLASK_ENV=development
(venv) $ flask run

然後,在不同的終端窗口中,您可以與 API 進行交互。在這裡隨意使用您選擇的工具,例如 cURL、HTTPieRequestsPostman

請求示例:

$ python3

>>> import requests
>>>
>>> r = requests.get('http://127.0.0.1:5000/journal/')
>>> print(r.text)
>>>
>>> post_data = {'entry': "some message"}
>>> r = requests.post('http://127.0.0.1:5000/journal/', json=post_data)
>>> print(r.text)

想要更輕鬆地測試 API 端點?查看此腳本,它添加了用於與 API 端點交互以檢索、創建、更新和刪除日記條目的 CLI 命令。

文檔

APIFairy 的一個令人難以置信的功能是自動創建 API 文檔!

配置 API 文檔有三個關鍵方面:

  1. API 端點的文檔字符串(即視圖函數)
  2. 整個 API 項目的文檔字符串
  3. 用於指定 API 文檔外觀的配置變量

We already covered the first item in the previous section since we included the docstrings for each view function. For example, the journal() view function has a short description of the purpose of this API endpoint:

@journal_api_blueprint.route('/', methods=['GET'])
@response(entries_schema)
def journal():
    """Return all journal entries"""
    return messages

Next, we need to include the docstring to describe the overall project at the very top of the project/__init__.py file:

"""
Welcome to the documentation for the Flask Journal API!

## Introduction

The Flask Journal API is an API (Application Programming Interface) for creating a **daily journal** that documents events that happen each day.

## Key Functionality

The Flask Journal API has the following functionality:

1. Work with journal entries:
  * Create a new journal entry
  * Update a journal entry
  * Delete a journal entry
  * View all journal entries
2. <More to come!>

## Key Modules

This project is written using Python 3.10.1.

The project utilizes the following modules:

* **Flask**: micro-framework for web application development which includes the following dependencies:
  * **click**: package for creating command-line interfaces (CLI)
  * **itsdangerous**: cryptographically sign data
  * **Jinja2**: templating engine
  * **MarkupSafe**: escapes characters so text is safe to use in HTML and XML
  * **Werkzeug**: set of utilities for creating a Python application that can talk to a WSGI server
* **APIFairy**: API framework for Flask which includes the following dependencies:
  * **Flask-Marshmallow** - Flask extension for using Marshmallow (object serialization/deserialization library)
  * **Flask-HTTPAuth** - Flask extension for HTTP authentication
  * **apispec** - API specification generator that supports the OpenAPI specification
* **pytest**: framework for testing Python projects
"""
...

This docstring is used to describe the overall project, including the key functionality provided and the key Python packages used by the project.

Finally, some configuration variables need to be defined to specify the look of the API documentation. Update the create_app() function in project/__init__.py:

def create_app():
    # Create the Flask application
    app = Flask(__name__)

    # Configure the API documentation
    app.config['APIFAIRY_TITLE'] = 'Flask Journal API'
    app.config['APIFAIRY_VERSION'] = '0.1'
    app.config['APIFAIRY_UI'] = 'elements'

    initialize_extensions(app)
    register_blueprints(app)

    return app

準備好查看項目的文檔了嗎?通過 啟動 Flask 開發服務器flask run,然後導航到http://127.0.0.1:5000/docs以查看 APIFairy 創建的 API 文檔:

API 文檔 - 主頁

在左側窗格中,有一個journal_api藍圖的 API 端點列表。單擊其中一個端點會顯示有關該端點的所有詳細信息:

API 文檔 - 獲取日誌條目 API 端點

這個 API 文檔的驚人之處在於能夠查看 API 端點是如何工作的(假設 Flask 開發服務器正在運行)。在文檔的右側窗格中,輸入日記帳分錄索引,然後單擊“發送 API 請求”。然後顯示 API 響應:

API 文檔 - 獲取日誌條目 API 響應

這個交互式文檔使用戶可以輕鬆理解 API!

數據庫

出於演示目的,本教程將使用 SQLite 數據庫。

配置

由於本教程開始時已經安裝了Flask-SQLAlchemy,我們需要在project/__init__.py文件中進行配置。

首先SQLAlchemy()在“配置”部分創建一個對象:

...

from apifairy import APIFairy
from flask import Flask, json
from flask_marshmallow import Marshmallow
from flask_sqlalchemy import SQLAlchemy  # <-- NEW!!


# -------------
# Configuration
# -------------

# Create the instances of the Flask extensions in the global scope,
# but without any arguments passed in. These instances are not
# attached to the Flask application at this point.
apifairy = APIFairy()
ma = Marshmallow()
database = SQLAlchemy()  # <-- NEW!!

...

接下來,更新create_app()函數以指定必要的配置變量:

def create_app():
    # Create the Flask application
    app = Flask(__name__)

    # Configure the API documentation
    app.config['APIFAIRY_TITLE'] = 'Flask Journal API'
    app.config['APIFAIRY_VERSION'] = '0.1'
    app.config['APIFAIRY_UI'] = 'elements'

    # NEW!
    # Configure the SQLite database (intended for development only!)
    app.config['SQLALCHEMY_DATABASE_URI'] = f"sqlite:///{os.path.join(os.getcwd(), 'instance', 'app.db')}"
    app.config['SQLALCHEMY_TRACK_MODIFICATIONS'] = False

    initialize_extensions(app)
    register_blueprints(app)

    return app

將導入添加到頂部:

import os

配置變量對於SQLALCHEMY_DATABASE_URI識別 SQLite 數據庫的位置至關重要。對於本教程,數據庫存儲在instance/app.db中。

最後,更新initialize_extensions()函數以初始化 Flask-SQLAlchemy 對象:

def initialize_extensions(app):
    # Since the application instance is now created, pass it to each Flask
    # extension instance to bind it to the Flask application instance (app)
    apifairy.init_app(app)
    ma.init_app(app)
    database.init_app(app)  # <-- NEW!!

想了解更多關於這個 Flask 應用程序是如何連接在一起的嗎?查看我關於如何構建、測試和部署 Flask 應用程序的課程:

數據庫模型

創建一個新的project/models.py文件來定義數據庫表來表示日誌條目:

from project import database


class Entry(database.Model):
    """Class that represents a journal entry."""
    __tablename__ = 'entries'

    id = database.Column(database.Integer, primary_key=True)
    entry = database.Column(database.String, nullable=False)

    def __init__(self, entry: str):
        self.entry = entry

    def update(self, entry: str):
        self.entry = entry

    def __repr__(self):
        return f'<Entry: {self.entry}>'

這個新類Entry指定entries數據庫表將包含兩個元素(現在!)來表示日記帳分錄:

  1. id- 表的主鍵 ( primary_key=True),這意味著它是表中每個元素(行)的唯一標識符
  2. entry- 用於存儲日記帳分錄文本的字符串

雖然models.py定義了數據庫表,但它不會在 SQLite 數據庫中創建表。要創建表,請在終端窗口中啟動 Flask shell:

(venv)$ flask shell

>>> from project import database
>>> database.drop_all()
>>> database.create_all()
>>> quit()

(venv)$

期刊 API 更新

由於我們正在繼續使用 SQLite 數據庫,因此首先刪除在project/journal_api/routes.pydatabase中定義的臨時(Python 列表):

# --------
# Database
# --------

messages = [
    dict(id=1, entry='The sun was shining when I woke up this morning.'),
    dict(id=2, entry='I tried a new fruit mixture in my oatmeal for breakfast.'),
    dict(id=3, entry='Today I ate a great sandwich for lunch.')
]

接下來,我們需要更新每個 API 端點(即視圖函數)以利用 SQLite 數據庫。

首先更新journal()視圖函數:

@journal_api_blueprint.route('/', methods=['GET'])
@response(entries_schema)
def journal():
    """Return all journal entries"""
    return Entry.query.all()

現在從 SQLite 數據庫中檢索到完整的日記帳條目列表。注意這個視圖函數的模式或裝飾器是如何不需要改變的……只有改變用戶的底層過程!

添加導入:

from project.models import Entry

接下來,更新add_journal_entry()視圖函數:

@journal_api_blueprint.route('/', methods=['POST'])
@body(new_entry_schema)
@response(entry_schema, 201)
def add_journal_entry(kwargs):
    """Add a new journal entry"""
    new_message = Entry(**kwargs)
    database.session.add(new_message)
    database.session.commit()
    return new_message

此視圖函數的輸入由 指定new_entry_schema

class NewEntrySchema(ma.Schema):
    """Schema defining the attributes when creating a new journal entry."""
    entry = ma.String(required=True)

new_entry_schema = NewEntrySchema()

entry字符串用於創建Entry該類的新實例(在models.py中定義),然後將此日記條目添加到數據庫中。

添加導入:

from project import database

接下來,更新get_journal_entry()

@journal_api_blueprint.route('/<int:index>', methods=['GET'])
@response(entry_schema)
@other_responses({404: 'Entry not found'})
def get_journal_entry(index):
    """Return a journal entry"""
    entry = Entry.query.filter_by(id=index).first_or_404()
    return entry

此函數現在嘗試查找指定的日記帳分錄(基於index):

entry = Entry.query.filter_by(id=index).first_or_404()

如果條目存在,則將其返回給用戶。如果該條目不存在,則返回 404(未找到)錯誤。

接下來,更新update_journal_entry()

@journal_api_blueprint.route('/<int:index>', methods=['PUT'])
@body(new_entry_schema)
@response(entry_schema)
@other_responses({404: 'Entry not found'})
def update_journal_entry(data, index):
    """Update a journal entry"""
    entry = Entry.query.filter_by(id=index).first_or_404()
    entry.update(data['entry'])
    database.session.add(entry)
    database.session.commit()
    return entry

view 函數現在update_journal_entry()嘗試檢索指定的日記帳分錄:

entry = Entry.query.filter_by(id=index).first_or_404()

如果日記條目存在,則使用新文本更新條目,然後將其保存到數據庫中。

最後,更新delete_journal_entry()

@journal_api_blueprint.route('/<int:index>', methods=['DELETE'])
@other_responses({404: 'Entry not found'})
def delete_journal_entry(index):
    """Delete a journal entry"""
    entry = Entry.query.filter_by(id=index).first_or_404()
    database.session.delete(entry)
    database.session.commit()
    return '', 204

如果找到指定的日記帳分錄,則將其從數據庫中刪除。

運行開發服務器。測試每個端點以確保它們仍然有效。

錯誤處理

由於這個 Flask 項目是一個 API,錯誤代碼應該以 JSON 格式返回,而不是典型的 HTML 格式。

在 Flask 項目中,這可以通過使用自定義錯誤處理程序來完成。在project/__init__.pyregister_error_handlers()中,在文件底部定義一個新函數 ( ):

def register_error_handlers(app):
    @app.errorhandler(HTTPException)
    def handle_http_exception(e):
        """Return JSON instead of HTML for HTTP errors."""
        # Start with the correct headers and status code from the error
        response = e.get_response()
        # Replace the body with JSON
        response.data = json.dumps({
            'code': e.code,
            'name': e.name,
            'description': e.description,
        })
        response.content_type = 'application/json'
        return response

此函數註冊一個新的錯誤處理程序,用於何時HTTPException引發將輸出轉換為 JSON 格式。

添加導入:

from werkzeug.exceptions import HTTPException

此外,更新應用程序工廠函數create_app(), 以調用此新函數:

def create_app():
    # Create the Flask application
    app = Flask(__name__)

    # Configure the API documentation
    app.config['APIFAIRY_TITLE'] = 'Flask Journal API'
    app.config['APIFAIRY_VERSION'] = '0.1'
    app.config['APIFAIRY_UI'] = 'elements'

    # Configure the SQLite database (intended for development only!)
    app.config['SQLALCHEMY_DATABASE_URI'] = f"sqlite:///{os.path.join(os.getcwd(), 'instance', 'app.db')}"
    app.config['SQLALCHEMY_TRACK_MODIFICATIONS'] = False

    initialize_extensions(app)
    register_blueprints(app)
    register_error_handlers(app)  # NEW!!

    return app

驗證

身份驗證是驗證嘗試訪問系統的用戶身份的過程,在這種情況下是 API。

另一方面,授權是驗證特定用戶應該有權訪問哪些特定資源的過程。

APIFairy 使用Flask-HTTPAuth來支持身份驗證。在本教程中,我們將以兩種方式使用 Flask-HTTPAuth:

  1. 基本身份驗證- 用於根據用戶的電子郵件/密碼生成令牌
  2. 令牌身份驗證- 用於在所有其他 API 端點上對用戶進行身份驗證

通過 Flask-HTTPAuth 使用的令牌身份驗證通常稱為承載身份驗證,因為該過程調用授予對令牌“承載者”的訪問權限。令牌必須包含在 Authorization 標頭中的 HTTP 標頭中,例如“Authorization: Bearer”。

下圖說明了新用戶如何與應用程序交互以檢索身份驗證令牌的典型流程:

Flask Journal API 流程圖

配置

由於本教程開始安裝 APIFairy 時已經安裝了 Flask-HTTPAuth,我們只需要在project/__init__.py文件中進行配置即可。

首先為基本身份驗證和令牌身份驗證創建單獨的對象:

...

import os

from apifairy import APIFairy
from flask import Flask, json
from flask_httpauth import HTTPBasicAuth, HTTPTokenAuth  # NEW!!
from flask_marshmallow import Marshmallow
from flask_sqlalchemy import SQLAlchemy
from werkzeug.exceptions import HTTPException


# -------------
# Configuration
# -------------

# Create the instances of the Flask extensions in the global scope,
# but without any arguments passed in. These instances are not
# attached to the Flask application at this point.
apifairy = APIFairy()
ma = Marshmallow()
database = SQLAlchemy()
basic_auth = HTTPBasicAuth()  # NEW!!
token_auth = HTTPTokenAuth()  # NEW!!

...

project/__init__.py中不需要進一步更新。

數據庫模型

project/models.pyUser中,需要創建一個新模型來代表用戶:

class User(database.Model):
    __tablename__ = 'users'

    id = database.Column(database.Integer, primary_key=True)
    email = database.Column(database.String, unique=True, nullable=False)
    password_hashed = database.Column(database.String(128), nullable=False)
    entries = database.relationship('Entry', backref='user', lazy='dynamic')
    auth_token = database.Column(database.String(64), index=True)
    auth_token_expiration = database.Column(database.DateTime)

    def __init__(self, email: str, password_plaintext: str):
        """Create a new User object."""
        self.email = email
        self.password_hashed = self._generate_password_hash(password_plaintext)

    def is_password_correct(self, password_plaintext: str):
        return check_password_hash(self.password_hashed, password_plaintext)

    def set_password(self, password_plaintext: str):
        self.password_hashed = self._generate_password_hash(password_plaintext)

    @staticmethod
    def _generate_password_hash(password_plaintext):
        return generate_password_hash(password_plaintext)

    def generate_auth_token(self):
        self.auth_token = secrets.token_urlsafe()
        self.auth_token_expiration = datetime.utcnow() + timedelta(minutes=60)
        return self.auth_token

    @staticmethod
    def verify_auth_token(auth_token):
        user = User.query.filter_by(auth_token=auth_token).first()
        if user and user.auth_token_expiration > datetime.utcnow():
            return user

    def revoke_auth_token(self):
        self.auth_token_expiration = datetime.utcnow()

    def __repr__(self):
        return f'<User: {self.email}>'

添加導入:

import secrets
from datetime import datetime, timedelta

from werkzeug.security import check_password_hash, generate_password_hash

User模型werkzeug.security用於在將用戶密碼存儲到數據庫之前對其進行哈希處理。

記住:永遠不要將明文密碼存儲在數據庫中!

User模型secrets用於為特定用戶生成身份驗證令牌。此令牌在該generate_auth_token()方法中創建,並包括未來 60 分鐘的到期日期/時間:

def generate_auth_token(self):
    self.auth_token = secrets.token_urlsafe()
    self.auth_token_expiration = datetime.utcnow() + timedelta(minutes=60)
    return self.auth_token

有一個靜態方法,verify_auth_token()用於驗證身份驗證令牌(同時考慮到期時間)並從有效令牌返回用戶:

@staticmethod
def verify_auth_token(auth_token):
    user = User.query.filter_by(auth_token=auth_token).first()
    if user and user.auth_token_expiration > datetime.utcnow():
        return user

另一種有趣的方法是revoke_auth_token(),它用於撤銷特定用戶的身份驗證令牌:

def revoke_auth_token(self):
    self.auth_token_expiration = datetime.utcnow()

入門模型

為了在用戶(“one”)和他們的條目(“many”)之間建立一對多的關係,Entry需要更新模型以將表entriesusers錶鍊接在一起:

class Entry(database.Model):
    """Class that represents a journal entry."""
    __tablename__ = 'entries'

    id = database.Column(database.Integer, primary_key=True)
    entry = database.Column(database.String, nullable=False)
    user_id = database.Column(database.Integer, database.ForeignKey('users.id'))  # <-- NEW!!

    def __init__(self, entry: str):
        self.entry = entry

    def update(self, entry: str):
        self.entry = entry

    def __repr__(self):
        return f'<Entry: {self.entry}>'

User模型已經包含返回entries表格的鏈接:

entries = database.relationship('Entry', backref='user', lazy='dynamic')

用戶 API 藍圖

Flask 項目的用戶管理功能將在一個名為users_api_blueprint.

首先在“項目”中創建一個名為“users_api”的新目錄。在該目錄中創建一個__init__.py文件:

from flask import Blueprint


users_api_blueprint = Blueprint('users_api', __name__)

from . import authentication, routes

這個新藍圖需要在函數中的projects/__init__.pyapp中的 Flask中註冊:register_blueprints()

def register_blueprints(app):
    # Import the blueprints
    from project.journal_api import journal_api_blueprint
    from project.users_api import users_api_blueprint  # NEW!!

    # Since the application instance is now created, register each Blueprint
    # with the Flask application instance (app)
    app.register_blueprint(journal_api_blueprint, url_prefix='/journal')
    app.register_blueprint(users_api_blueprint, url_prefix='/users')  # NEW!!

認證功能

要使用 Flask-HTTPAuth,需要定義幾個函數來處理檢查用戶憑據。

創建一個新的project/users_api/authentication.py文件來處理基本認證和令牌認證。

對於基本身份驗證(檢查用戶的電子郵件和密碼):

from werkzeug.exceptions import Forbidden, Unauthorized

from project import basic_auth, token_auth
from project.models import User


@basic_auth.verify_password
def verify_password(email, password):
    user = User.query.filter_by(email=email).first()
    if user is None:
        return None

    if user.is_password_correct(password):
        return user


@basic_auth.error_handler
def basic_auth_error(status=401):
    error = (Forbidden if status == 403 else Unauthorized)()
    return {
        'code': error.code,
        'message': error.name,
        'description': error.description,
    }, error.code, {'WWW-Authenticate': 'Form'}

verify_password()功能用於檢查用戶是否存在以及他們的密碼是否正確。當需要基本身份驗證時,Flask-HTTPAuth 將使用此函數來驗證密碼(感謝@basic_auth.verify_password裝飾器。)

此外,還為基本身份驗證定義了一個錯誤處理程序,該處理程序以 JSON 格式返回有關錯誤的信息。

對於令牌認證(處理令牌以確定用戶是否有效):

@token_auth.verify_token
def verify_token(auth_token):
    return User.verify_auth_token(auth_token)


@token_auth.error_handler
def token_auth_error(status=401):
    error = (Forbidden if status == 403 else Unauthorized)()
    return {
        'code': error.code,
        'message': error.name,
        'description': error.description,
    }, error.code

verify_token()函數用於檢查身份驗證令牌是否有效。當需要令牌身份驗證時,Flask-HTTPAuth 將使用此函數來驗證令牌(感謝@token_auth.verify_token裝飾器。)

此外,還為令牌身份驗證定義了一個錯誤處理程序,該處理程序以 JSON 格式返回有關錯誤的信息。

用戶路線

在 中users_api_blueprint,將有兩條路線:

  1. 註冊新用戶
  2. 檢索身份驗證令牌

首先,需要在projects/users_api/routes.py中定義一組新模式(使用棉花糖) :

from project import ma

from . import users_api_blueprint

# -------
# Schemas
# -------

class NewUserSchema(ma.Schema):
    """Schema defining the attributes when creating a new user."""
    email = ma.String()
    password_plaintext = ma.String()


class UserSchema(ma.Schema):
    """Schema defining the attributes of a user."""
    id = ma.Integer()
    email = ma.String()


class TokenSchema(ma.Schema):
    """Schema defining the attributes of a token."""
    token = ma.String()


new_user_schema = NewUserSchema()
user_schema = UserSchema()
token_schema = TokenSchema()

這些模式將用於定義此文件中定義的視圖函數的輸入和輸出。

註冊新用戶

接下來,定義註冊新用戶的視圖函數:

@users_api_blueprint.route('/', methods=['POST'])
@body(new_user_schema)
@response(user_schema, 201)
def register(kwargs):
    """Create a new user"""
    new_user = User(**kwargs)
    database.session.add(new_user)
    database.session.commit()
    return new_user

添加導入:

from apifairy import authenticate, body, other_responses, response

from project import basic_auth, database, ma
from project.models import User

此 API 端點使用new_user_schema指定電子郵件和密碼是輸入。

注意:由於電子郵件和密碼被發送到此 API 端點,因此最好記住在開發測試期間使用 HTTP 是可以接受的,但在生產中應始終使用 HTTPS(安全)。

然後解壓縮電子郵件和密碼(定義為kwargs- 關鍵字參數)以創建一個新User對象,並將其保存到數據庫中:

new_user = User(**kwargs)
database.session.add(new_user)
database.session.commit()

API 端點的輸出由 定義user_schema,它是新用戶的 ID 和電子郵件。

檢索身份驗證令牌

在projects/users_api/routes.py中定義的另一個視圖函數用於檢索身份驗證令牌:

@users_api_blueprint.route('/get-auth-token', methods=['POST'])
@authenticate(basic_auth)
@response(token_schema)
@other_responses({401: 'Invalid username or password'})
def get_auth_token():
    """Get authentication token"""
    user = basic_auth.current_user()
    token = user.generate_auth_token()
    database.session.add(user)
    database.session.commit()
    return dict(token=token)

在本@authenticate教程中第一次使用裝飾器,它指定應該使用基本身份驗證來保護此路由:

@authenticate(basic_auth)

當用戶想要檢索他們的身份驗證令牌時,他們需要向此 API 端點發送一個 POST 請求,並將電子郵件和密碼嵌入在“授權”標頭中。例如,可以對該 API 端點執行以下使用Requests包的 Python 命令:

>>> import requests
>>> r = requests.post(
    'http://127.0.0.1:5000/users/get-auth-token',
    auth=('pkennedy@hey.com', 'FlaskIsAwesome123')
)

current_user()如果基本認證成功,則視圖函數使用Flask-HTTPAuth 提供的方法檢索當前用戶:

user = basic_auth.current_user()

為該用戶創建一個新的身份驗證令牌:

token = user.generate_auth_token()

並且該令牌被保存到數據庫中,以便將來可用於對用戶進行身份驗證(至少在接下來的 60 分鐘內!)。

Finally, the new authentication token is returned for the user to save for all subsequent API calls.

API Endpoint Updates

With an authentication process in place, it's time to add some guards to the existing API endpoints to make sure that only valid users can access the application.

These updates are for the view functions defined in projects/journal_api/routes.py.

First, update journal() to only return the journal entries for the current user:

@journal_api_blueprint.route('/', methods=['GET'])
@authenticate(token_auth)
@response(entries_schema)
def journal():
    """Return journal entries"""
    user = token_auth.current_user()
    return Entry.query.filter_by(user_id=user.id).all()

Update the imports at the top like so:

from apifairy import authenticate, body, other_responses, response
from flask import abort

from project import database, ma, token_auth
from project.models import Entry

from . import journal_api_blueprint

The @authenticate decorator specifies that token authentication needs to be used when accessing this API endpoint. As an example, the following GET request could be made using Requests (after the authentication token has been retrieved):

>>> import requests
>>> headers = {'Authorization': f'Bearer {auth_token}'}
>>> r = requests.get('http://127.0.0.1:5000/journal/', headers=headers)

用戶通過身份驗證後,將根據用戶 ID 從數據庫中檢索完整的日記帳條目列表:

user = token_auth.current_user()
return Entry.query.filter_by(user_id=user.id).all()

此 API 端點的輸出由@response裝飾器定義,它是日誌條目(ID、條目、用戶 ID)的列表。

接下來,更新add_journal_entry()

@journal_api_blueprint.route('/', methods=['POST'])
@authenticate(token_auth)
@body(new_entry_schema)
@response(entry_schema, 201)
def add_journal_entry(kwargs):
    """Add a new journal entry"""
    user = token_auth.current_user()
    new_message = Entry(user_id=user.id, **kwargs)
    database.session.add(new_message)
    database.session.commit()
    return new_message

與之前的視圖函數一樣,@authenticate裝飾器用於指定訪問此 API 端點時需要使用令牌認證。此外,現在通過指定應與日記條目關聯的用戶 ID 添加日記條目:

user = token_auth.current_user()
new_message = Entry(user_id=user.id, **kwargs)

新的日誌條目被保存到數據庫中並返回日誌條目(由@response裝飾器定義)。

接下來,更新get_journal_entry()

@journal_api_blueprint.route('/<int:index>', methods=['GET'])
@authenticate(token_auth)
@response(entry_schema)
@other_responses({403: 'Forbidden', 404: 'Entry not found'})
def get_journal_entry(index):
    """Return a journal entry"""
    user = token_auth.current_user()
    entry = Entry.query.filter_by(id=index).first_or_404()

    if entry.user_id != user.id:
        abort(403)
    return entry

添加裝飾器@authenticate以指定訪問此 API 端點需要令牌身份驗證。

嘗試檢索日記條目時,會添加額外的檢查以確保嘗試訪問日記條目的用戶是條目的實際“所有者”。如果不是,則通過abort()Flask 中的函數返回 403(禁止)錯誤代碼:

if entry.user_id != user.id:
        abort(403)

請注意,此 API 端點有兩個由@other_responses裝飾器指定的非標稱響應:

@other_responses({403: 'Forbidden', 404: 'Entry not found'})

提醒:@other_responses裝飾器僅用於文檔;引發這些錯誤是視圖函數的責任。

接下來,更新update_journal_entry()

@journal_api_blueprint.route('/<int:index>', methods=['PUT'])
@authenticate(token_auth)
@body(new_entry_schema)
@response(entry_schema)
@other_responses({403: 'Forbidden', 404: 'Entry not found'})
def update_journal_entry(data, index):
    """Update a journal entry"""
    user = token_auth.current_user()
    entry = Entry.query.filter_by(id=index).first_or_404()

    if entry.user_id != user.id:
        abort(403)

    entry.update(data['entry'])
    database.session.add(entry)
    database.session.commit()
    return entry

此視圖函數的更新類似於本節中的其他視圖函數:

  1. @authenticate裝飾器指定訪問此 API 端點需要令牌身份驗證
  2. 只有“擁有”日誌條目的用戶才被允許更新條目(否則,403(禁止))

最後,更新delete_journal_entry()

@journal_api_blueprint.route('/<int:index>', methods=['DELETE'])
@authenticate(token_auth)
@other_responses({403: 'Forbidden', 404: 'Entry not found'})
def delete_journal_entry(index):
    """Delete a journal entry"""
    user = token_auth.current_user()
    entry = Entry.query.filter_by(id=index).first_or_404()

    if entry.user_id != user.id:
        abort(403)

    database.session.delete(entry)
    database.session.commit()
    return '', 204

結論

本教程提供瞭如何使用 APIFairy 在 Flask 中輕鬆快速地構建 API 的演練。

裝飾器是定義 API 端點的關鍵:

  • 輸入
    • @arguments- 從 URL 的查詢字符串輸入參數
    • @body- JSON 請求的結構
  • 輸出
    • @response- JSON 響應的結構
  • 身份驗證
    • @authenticate- 使用 Flask-HTTPAuth 的身份驗證方法
  • 錯誤
    • @other_responses- 非標稱響應,例如 HTTP 錯誤代碼

此外,APIFairy 生成的 API 文檔非常出色,為應用程序的用戶提供了關鍵信息。

來源:  https ://testdriven.io

#api #flask 

如何使用 Flask 和 APIFairy 輕鬆創建 RESTful API
Hunter  Krajcik

Hunter Krajcik

1656746160

Bungie-api-dart: Dart Definitions for The Bungie.net API

Bungie API Dart support

This project implements Dart definitions and API helpers for the Bungie.net API. It's based on bungie-api-ts that is meant for use in Destiny Item Manager, but should be general enough to use in any project. The code is completely generated from Bungie's documentation - I considered using something like Swagger Codegen, but instead opted for a custom generator so we could make the result as nice as possible.

Install

add this to your dependencies block in pubspec.yaml

dependencies:
  bungie_api: ^12.2.4

Interfaces and Enums

There are definitions for every type defined in the Bungie.net services. See their documentation for a list - the interface names are the last part of the full name (for example, Destiny.Definitions.DestinyVendorActionDefinition becomes DestinyVendorActionDefinition). There are a few exceptions, like SingleComponentResponseOfDestinyInventoryComponent, which have been mapped into nicer forms like SingleComponentResponse<DestinyInventoryComponent>, and the server responses, which are now ServerResponse<T> instead of something like DestinyCharacterResponse.

API Helpers

In addition to the types, there are also simple helper functions for each API endpoint. They define the inputs and outputs to that endpoint, and will call a user-provided function with HTTP request info that you can then use to make an HTTP request. This pattern was used so the API helpers could provide full type information. These helpers are not a full API client - they assist in building one. An example:

import 'dart:async';
import 'package:http/http.dart' as http;
import 'package:bungie_api_dart/destiny2.dart';

class BungieApiService{
  Future<ServerResponse<DestinyManifest>> getManifest(){
    return getDestinyManifest(new Client());
  }
}
class Client implements HttpClient{
  static const API_KEY = "your_key";
  @override
    Future<Object> request(HttpClientConfig config) {
      if(config.method == 'GET'){
        return http.get(config.url, headers: {'X-API-Key': API_KEY});
      }
      return http.post(config.url, headers: {'X-API-Key': API_KEY});
    }
}

Build

./install.sh && ./build.sh

Installing

Use this package as a library

Depend on it

Run this command:

With Dart:

 $ dart pub add bungie_api

With Flutter:

 $ flutter pub add bungie_api

This will add a line like this to your package's pubspec.yaml (and run an implicit dart pub get):

dependencies:
  bungie_api: ^13.14.0

Alternatively, your editor might support dart pub get or flutter pub get. Check the docs for your editor to learn more.

Import it

Now in your Dart code, you can use:

import 'package:bungie_api/api/app.dart';
import 'package:bungie_api/api/community_content.dart';
import 'package:bungie_api/api/content.dart';
import 'package:bungie_api/api/destiny2.dart';
import 'package:bungie_api/api/fireteam.dart';
import 'package:bungie_api/api/forum.dart';
import 'package:bungie_api/api/get_available_locales.dart';
import 'package:bungie_api/api/global_alerts.dart';
import 'package:bungie_api/api/group_v2.dart';
import 'package:bungie_api/api/settings.dart';
import 'package:bungie_api/api/social.dart';
import 'package:bungie_api/api/tokens.dart';
import 'package:bungie_api/api/trending.dart';
import 'package:bungie_api/api/user.dart';
import 'package:bungie_api/api/user_system_overrides.dart';
import 'package:bungie_api/app.dart';
import 'package:bungie_api/applications.dart';
import 'package:bungie_api/common.dart';
import 'package:bungie_api/communitycontent.dart';
import 'package:bungie_api/components.dart';
import 'package:bungie_api/config.dart';
import 'package:bungie_api/content.dart';
import 'package:bungie_api/core.dart';
import 'package:bungie_api/dates.dart';
import 'package:bungie_api/destiny2.dart';
import 'package:bungie_api/entities.dart';
import 'package:bungie_api/enums/activity_graph_node_highlight_type.dart';
import 'package:bungie_api/enums/application_scopes.dart';
import 'package:bungie_api/enums/application_status.dart';
import 'package:bungie_api/enums/awa_response_reason.dart';
import 'package:bungie_api/enums/awa_type.dart';
import 'package:bungie_api/enums/awa_user_selection.dart';
import 'package:bungie_api/enums/bucket_category.dart';
import 'package:bungie_api/enums/bucket_scope.dart';
import 'package:bungie_api/enums/bungie_credential_type.dart';
import 'package:bungie_api/enums/bungie_membership_type.dart';
import 'package:bungie_api/enums/capabilities.dart';
import 'package:bungie_api/enums/chat_security_setting.dart';
import 'package:bungie_api/enums/community_content_sort_mode.dart';
import 'package:bungie_api/enums/component_privacy_setting.dart';
import 'package:bungie_api/enums/content_property_data_type_enum.dart';
import 'package:bungie_api/enums/damage_type.dart';
import 'package:bungie_api/enums/destiny_activity_difficulty_tier.dart';
import 'package:bungie_api/enums/destiny_activity_mode_category.dart';
import 'package:bungie_api/enums/destiny_activity_mode_type.dart';
import 'package:bungie_api/enums/destiny_activity_nav_point_type.dart';
import 'package:bungie_api/enums/destiny_ammunition_type.dart';
import 'package:bungie_api/enums/destiny_breaker_type.dart';
import 'package:bungie_api/enums/destiny_class.dart';
import 'package:bungie_api/enums/destiny_collectible_state.dart';
import 'package:bungie_api/enums/destiny_component_type.dart';
import 'package:bungie_api/enums/destiny_energy_type.dart';
import 'package:bungie_api/enums/destiny_game_privacy_setting.dart';
import 'package:bungie_api/enums/destiny_game_versions.dart';
import 'package:bungie_api/enums/destiny_gating_scope.dart';
import 'package:bungie_api/enums/destiny_gender.dart';
import 'package:bungie_api/enums/destiny_graph_node_state.dart';
import 'package:bungie_api/enums/destiny_item_sort_type.dart';
import 'package:bungie_api/enums/destiny_item_sub_type.dart';
import 'package:bungie_api/enums/destiny_item_type.dart';
import 'package:bungie_api/enums/destiny_join_closed_reasons.dart';
import 'package:bungie_api/enums/destiny_milestone_display_preference.dart';
import 'package:bungie_api/enums/destiny_milestone_type.dart';
import 'package:bungie_api/enums/destiny_objective_grant_style.dart';
import 'package:bungie_api/enums/destiny_objective_ui_style.dart';
import 'package:bungie_api/enums/destiny_party_member_states.dart';
import 'package:bungie_api/enums/destiny_presentation_display_style.dart';
import 'package:bungie_api/enums/destiny_presentation_node_state.dart';
import 'package:bungie_api/enums/destiny_presentation_node_type.dart';
import 'package:bungie_api/enums/destiny_presentation_screen_style.dart';
import 'package:bungie_api/enums/destiny_progression_reward_item_acquisition_behavior.dart';
import 'package:bungie_api/enums/destiny_progression_reward_item_state.dart';
import 'package:bungie_api/enums/destiny_progression_scope.dart';
import 'package:bungie_api/enums/destiny_progression_step_display_effect.dart';
import 'package:bungie_api/enums/destiny_race.dart';
import 'package:bungie_api/enums/destiny_record_state.dart';
import 'package:bungie_api/enums/destiny_record_toast_style.dart';
import 'package:bungie_api/enums/destiny_record_value_style.dart';
import 'package:bungie_api/enums/destiny_reward_source_category.dart';
import 'package:bungie_api/enums/destiny_scope.dart';
import 'package:bungie_api/enums/destiny_socket_array_type.dart';
import 'package:bungie_api/enums/destiny_socket_category_style.dart';
import 'package:bungie_api/enums/destiny_socket_visibility.dart';
import 'package:bungie_api/enums/destiny_stat_aggregation_type.dart';
import 'package:bungie_api/enums/destiny_stat_category.dart';
import 'package:bungie_api/enums/destiny_stats_category_type.dart';
import 'package:bungie_api/enums/destiny_stats_group_type.dart';
import 'package:bungie_api/enums/destiny_stats_merge_method.dart';
import 'package:bungie_api/enums/destiny_talent_node_state.dart';
import 'package:bungie_api/enums/destiny_talent_node_step_damage_types.dart';
import 'package:bungie_api/enums/destiny_talent_node_step_guardian_attributes.dart';
import 'package:bungie_api/enums/destiny_talent_node_step_impact_effects.dart';
import 'package:bungie_api/enums/destiny_talent_node_step_light_abilities.dart';
import 'package:bungie_api/enums/destiny_talent_node_step_weapon_performances.dart';
import 'package:bungie_api/enums/destiny_unlock_value_uistyle.dart';
import 'package:bungie_api/enums/destiny_vendor_filter.dart';
import 'package:bungie_api/enums/destiny_vendor_interaction_reward_selection.dart';
import 'package:bungie_api/enums/destiny_vendor_item_refund_policy.dart';
import 'package:bungie_api/enums/destiny_vendor_item_state.dart';
import 'package:bungie_api/enums/destiny_vendor_progression_type.dart';
import 'package:bungie_api/enums/destiny_vendor_reply_type.dart';
import 'package:bungie_api/enums/developer_role.dart';
import 'package:bungie_api/enums/equip_failure_reason.dart';
import 'package:bungie_api/enums/equipping_item_block_attributes.dart';
import 'package:bungie_api/enums/fireteam_date_range.dart';
import 'package:bungie_api/enums/fireteam_platform.dart';
import 'package:bungie_api/enums/fireteam_platform_invite_result.dart';
import 'package:bungie_api/enums/fireteam_public_search_option.dart';
import 'package:bungie_api/enums/fireteam_slot_search.dart';
import 'package:bungie_api/enums/forum_flags_enum.dart';
import 'package:bungie_api/enums/forum_media_type.dart';
import 'package:bungie_api/enums/forum_post_category_enums.dart';
import 'package:bungie_api/enums/forum_post_popularity.dart';
import 'package:bungie_api/enums/forum_post_sort_enum.dart';
import 'package:bungie_api/enums/forum_recruitment_intensity_label.dart';
import 'package:bungie_api/enums/forum_recruitment_tone_label.dart';
import 'package:bungie_api/enums/forum_topics_category_filters_enum.dart';
import 'package:bungie_api/enums/forum_topics_quick_date_enum.dart';
import 'package:bungie_api/enums/forum_topics_sort_enum.dart';
import 'package:bungie_api/enums/friend_relationship_state.dart';
import 'package:bungie_api/enums/global_alert_level.dart';
import 'package:bungie_api/enums/global_alert_type.dart';
import 'package:bungie_api/enums/group_alliance_status.dart';
import 'package:bungie_api/enums/group_application_resolve_state.dart';
import 'package:bungie_api/enums/group_date_range.dart';
import 'package:bungie_api/enums/group_homepage.dart';
import 'package:bungie_api/enums/group_member_count_filter.dart';
import 'package:bungie_api/enums/group_post_publicity.dart';
import 'package:bungie_api/enums/group_potential_member_status.dart';
import 'package:bungie_api/enums/group_sort_by.dart';
import 'package:bungie_api/enums/group_type.dart';
import 'package:bungie_api/enums/groups_for_member_filter.dart';
import 'package:bungie_api/enums/host_guided_games_permission_level.dart';
import 'package:bungie_api/enums/ignore_length.dart';
import 'package:bungie_api/enums/ignore_status.dart';
import 'package:bungie_api/enums/item_bind_status.dart';
import 'package:bungie_api/enums/item_location.dart';
import 'package:bungie_api/enums/item_perk_visibility.dart';
import 'package:bungie_api/enums/item_state.dart';
import 'package:bungie_api/enums/membership_option.dart';
import 'package:bungie_api/enums/opt_in_flags.dart';
import 'package:bungie_api/enums/period_type.dart';
import 'package:bungie_api/enums/platform_error_codes.dart';
import 'package:bungie_api/enums/platform_friend_type.dart';
import 'package:bungie_api/enums/plug_availability_mode.dart';
import 'package:bungie_api/enums/plug_ui_styles.dart';
import 'package:bungie_api/enums/presence_online_state_flags.dart';
import 'package:bungie_api/enums/presence_status.dart';
import 'package:bungie_api/enums/runtime_group_member_type.dart';
import 'package:bungie_api/enums/socket_plug_sources.dart';
import 'package:bungie_api/enums/socket_type_action_type.dart';
import 'package:bungie_api/enums/special_item_type.dart';
import 'package:bungie_api/enums/tier_type.dart';
import 'package:bungie_api/enums/transfer_statuses.dart';
import 'package:bungie_api/enums/trending_entry_type.dart';
import 'package:bungie_api/enums/unit_type.dart';
import 'package:bungie_api/enums/vendor_display_category_sort_order.dart';
import 'package:bungie_api/enums/vendor_interaction_type.dart';
import 'package:bungie_api/enums/vendor_item_status.dart';
import 'package:bungie_api/exceptions.dart';
import 'package:bungie_api/fireteam.dart';
import 'package:bungie_api/forum.dart';
import 'package:bungie_api/forums.dart';
import 'package:bungie_api/getavailablelocales.dart';
import 'package:bungie_api/globalalerts.dart';
import 'package:bungie_api/groupsv2.dart';
import 'package:bungie_api/groupv2.dart';
import 'package:bungie_api/helpers/base_bungie_net_response.dart';
import 'package:bungie_api/helpers/bungie_net_token.dart';
import 'package:bungie_api/helpers/http.dart';
import 'package:bungie_api/helpers/oauth.dart';
import 'package:bungie_api/ignores.dart';
import 'package:bungie_api/interpolation.dart';
import 'package:bungie_api/links.dart';
import 'package:bungie_api/models/api_usage.dart';
import 'package:bungie_api/models/application.dart';
import 'package:bungie_api/models/application_developer.dart';
import 'package:bungie_api/models/awa_authorization_result.dart';
import 'package:bungie_api/models/awa_initialize_response.dart';
import 'package:bungie_api/models/awa_permission_requested.dart';
import 'package:bungie_api/models/awa_user_response.dart';
import 'package:bungie_api/models/bungie_friend.dart';
import 'package:bungie_api/models/bungie_friend_list_response.dart';
import 'package:bungie_api/models/bungie_friend_request_list_response.dart';
import 'package:bungie_api/models/clan_banner.dart';
import 'package:bungie_api/models/clan_banner_decal.dart';
import 'package:bungie_api/models/clan_banner_source.dart';
import 'package:bungie_api/models/comment_summary.dart';
import 'package:bungie_api/models/component_response.dart';
import 'package:bungie_api/models/content_item_public_contract.dart';
import 'package:bungie_api/models/content_preview.dart';
import 'package:bungie_api/models/content_representation.dart';
import 'package:bungie_api/models/content_type_default_value.dart';
import 'package:bungie_api/models/content_type_description.dart';
import 'package:bungie_api/models/content_type_property.dart';
import 'package:bungie_api/models/content_type_property_section.dart';
import 'package:bungie_api/models/core_setting.dart';
import 'package:bungie_api/models/core_settings_configuration.dart';
import 'package:bungie_api/models/core_system.dart';
import 'package:bungie_api/models/cross_save_user_membership.dart';
import 'package:bungie_api/models/datapoint.dart';
import 'package:bungie_api/models/date_range.dart';
import 'package:bungie_api/models/destiny2_core_settings.dart';
import 'package:bungie_api/models/destiny_action_request.dart';
import 'package:bungie_api/models/destiny_activity.dart';
import 'package:bungie_api/models/destiny_activity_challenge_definition.dart';
import 'package:bungie_api/models/destiny_activity_definition.dart';
import 'package:bungie_api/models/destiny_activity_graph_art_element_definition.dart';
import 'package:bungie_api/models/destiny_activity_graph_connection_definition.dart';
import 'package:bungie_api/models/destiny_activity_graph_definition.dart';
import 'package:bungie_api/models/destiny_activity_graph_display_objective_definition.dart';
import 'package:bungie_api/models/destiny_activity_graph_display_progression_definition.dart';
import 'package:bungie_api/models/destiny_activity_graph_list_entry_definition.dart';
import 'package:bungie_api/models/destiny_activity_graph_node_activity_definition.dart';
import 'package:bungie_api/models/destiny_activity_graph_node_definition.dart';
import 'package:bungie_api/models/destiny_activity_graph_node_featuring_state_definition.dart';
import 'package:bungie_api/models/destiny_activity_graph_node_state_entry.dart';
import 'package:bungie_api/models/destiny_activity_guided_block_definition.dart';
import 'package:bungie_api/models/destiny_activity_history_results.dart';
import 'package:bungie_api/models/destiny_activity_insertion_point_definition.dart';
import 'package:bungie_api/models/destiny_activity_loadout_requirement.dart';
import 'package:bungie_api/models/destiny_activity_loadout_requirement_set.dart';
import 'package:bungie_api/models/destiny_activity_matchmaking_block_definition.dart';
import 'package:bungie_api/models/destiny_activity_mode_definition.dart';
import 'package:bungie_api/models/destiny_activity_modifier_definition.dart';
import 'package:bungie_api/models/destiny_activity_modifier_reference_definition.dart';
import 'package:bungie_api/models/destiny_activity_playlist_item_definition.dart';
import 'package:bungie_api/models/destiny_activity_reward_definition.dart';
import 'package:bungie_api/models/destiny_activity_type_definition.dart';
import 'package:bungie_api/models/destiny_activity_unlock_string_definition.dart';
import 'package:bungie_api/models/destiny_aggregate_activity_results.dart';
import 'package:bungie_api/models/destiny_aggregate_activity_stats.dart';
import 'package:bungie_api/models/destiny_animation_reference.dart';
import 'package:bungie_api/models/destiny_art_dye_reference.dart';
import 'package:bungie_api/models/destiny_artifact_character_scoped.dart';
import 'package:bungie_api/models/destiny_artifact_definition.dart';
import 'package:bungie_api/models/destiny_artifact_profile_scoped.dart';
import 'package:bungie_api/models/destiny_artifact_tier.dart';
import 'package:bungie_api/models/destiny_artifact_tier_definition.dart';
import 'package:bungie_api/models/destiny_artifact_tier_item.dart';
import 'package:bungie_api/models/destiny_artifact_tier_item_definition.dart';
import 'package:bungie_api/models/destiny_base_item_component_set_ofint32.dart';
import 'package:bungie_api/models/destiny_base_item_component_set_ofint64.dart';
import 'package:bungie_api/models/destiny_base_item_component_set_ofuint32.dart';
import 'package:bungie_api/models/destiny_breaker_type_definition.dart';
import 'package:bungie_api/models/destiny_bubble_definition.dart';
import 'package:bungie_api/models/destiny_challenge_status.dart';
import 'package:bungie_api/models/destiny_character_action_request.dart';
import 'package:bungie_api/models/destiny_character_activities_component.dart';
import 'package:bungie_api/models/destiny_character_component.dart';
import 'package:bungie_api/models/destiny_character_customization.dart';
import 'package:bungie_api/models/destiny_character_peer_view.dart';
import 'package:bungie_api/models/destiny_character_progression_component.dart';
import 'package:bungie_api/models/destiny_character_records_component.dart';
import 'package:bungie_api/models/destiny_character_render_component.dart';
import 'package:bungie_api/models/destiny_character_response.dart';
import 'package:bungie_api/models/destiny_checklist_definition.dart';
import 'package:bungie_api/models/destiny_checklist_entry_definition.dart';
import 'package:bungie_api/models/destiny_clan_aggregate_stat.dart';
import 'package:bungie_api/models/destiny_class_definition.dart';
import 'package:bungie_api/models/destiny_collectible_acquisition_block.dart';
import 'package:bungie_api/models/destiny_collectible_component.dart';
import 'package:bungie_api/models/destiny_collectible_definition.dart';
import 'package:bungie_api/models/destiny_collectible_node_detail_response.dart';
import 'package:bungie_api/models/destiny_collectible_state_block.dart';
import 'package:bungie_api/models/destiny_collectibles_component.dart';
import 'package:bungie_api/models/destiny_color.dart';
import 'package:bungie_api/models/destiny_craftable_component.dart';
import 'package:bungie_api/models/destiny_craftable_socket_component.dart';
import 'package:bungie_api/models/destiny_craftable_socket_plug_component.dart';
import 'package:bungie_api/models/destiny_craftables_component.dart';
import 'package:bungie_api/models/destiny_currencies_component.dart';
import 'package:bungie_api/models/destiny_damage_type_definition.dart';
import 'package:bungie_api/models/destiny_definition.dart';
import 'package:bungie_api/models/destiny_derived_item_category_definition.dart';
import 'package:bungie_api/models/destiny_derived_item_definition.dart';
import 'package:bungie_api/models/destiny_destination_bubble_setting_definition.dart';
import 'package:bungie_api/models/destiny_destination_definition.dart';
import 'package:bungie_api/models/destiny_display_category_definition.dart';
import 'package:bungie_api/models/destiny_display_properties_definition.dart';
import 'package:bungie_api/models/destiny_energy_capacity_entry.dart';
import 'package:bungie_api/models/destiny_energy_cost_entry.dart';
import 'package:bungie_api/models/destiny_energy_type_definition.dart';
import 'package:bungie_api/models/destiny_entity_search_result.dart';
import 'package:bungie_api/models/destiny_entity_search_result_item.dart';
import 'package:bungie_api/models/destiny_environment_location_mapping.dart';
import 'package:bungie_api/models/destiny_equip_item_result.dart';
import 'package:bungie_api/models/destiny_equip_item_results.dart';
import 'package:bungie_api/models/destiny_equipment_slot_definition.dart';
import 'package:bungie_api/models/destiny_equipping_block_definition.dart';
import 'package:bungie_api/models/destiny_error_profile.dart';
import 'package:bungie_api/models/destiny_faction_definition.dart';
import 'package:bungie_api/models/destiny_faction_progression.dart';
import 'package:bungie_api/models/destiny_faction_vendor_definition.dart';
import 'package:bungie_api/models/destiny_gear_art_arrangement_reference.dart';
import 'package:bungie_api/models/destiny_gender_definition.dart';
import 'package:bungie_api/models/destiny_historical_stats_account_result.dart';
import 'package:bungie_api/models/destiny_historical_stats_activity.dart';
import 'package:bungie_api/models/destiny_historical_stats_by_period.dart';
import 'package:bungie_api/models/destiny_historical_stats_definition.dart';
import 'package:bungie_api/models/destiny_historical_stats_per_character.dart';
import 'package:bungie_api/models/destiny_historical_stats_period_group.dart';
import 'package:bungie_api/models/destiny_historical_stats_results.dart';
import 'package:bungie_api/models/destiny_historical_stats_value.dart';
import 'package:bungie_api/models/destiny_historical_stats_value_pair.dart';
import 'package:bungie_api/models/destiny_historical_stats_with_merged.dart';
import 'package:bungie_api/models/destiny_historical_weapon_stats.dart';
import 'package:bungie_api/models/destiny_historical_weapon_stats_data.dart';
import 'package:bungie_api/models/destiny_icon_sequence_definition.dart';
import 'package:bungie_api/models/destiny_insert_plug_action_definition.dart';
import 'package:bungie_api/models/destiny_insert_plugs_action_request.dart';
import 'package:bungie_api/models/destiny_insert_plugs_free_action_request.dart';
import 'package:bungie_api/models/destiny_insert_plugs_request_entry.dart';
import 'package:bungie_api/models/destiny_inventory_bucket_definition.dart';
import 'package:bungie_api/models/destiny_inventory_component.dart';
import 'package:bungie_api/models/destiny_inventory_item_definition.dart';
import 'package:bungie_api/models/destiny_inventory_item_stat_definition.dart';
import 'package:bungie_api/models/destiny_item_action_block_definition.dart';
import 'package:bungie_api/models/destiny_item_action_request.dart';
import 'package:bungie_api/models/destiny_item_action_required_item_definition.dart';
import 'package:bungie_api/models/destiny_item_category_definition.dart';
import 'package:bungie_api/models/destiny_item_change_response.dart';
import 'package:bungie_api/models/destiny_item_component.dart';
import 'package:bungie_api/models/destiny_item_component_set_ofint32.dart';
import 'package:bungie_api/models/destiny_item_component_set_ofint64.dart';
import 'package:bungie_api/models/destiny_item_component_set_ofuint32.dart';
import 'package:bungie_api/models/destiny_item_crafting_block_bonus_plug_definition.dart';
import 'package:bungie_api/models/destiny_item_crafting_block_definition.dart';
import 'package:bungie_api/models/destiny_item_creation_entry_level_definition.dart';
import 'package:bungie_api/models/destiny_item_gearset_block_definition.dart';
import 'package:bungie_api/models/destiny_item_instance_component.dart';
import 'package:bungie_api/models/destiny_item_instance_energy.dart';
import 'package:bungie_api/models/destiny_item_intrinsic_socket_entry_definition.dart';
import 'package:bungie_api/models/destiny_item_inventory_block_definition.dart';
import 'package:bungie_api/models/destiny_item_investment_stat_definition.dart';
import 'package:bungie_api/models/destiny_item_metric_block_definition.dart';
import 'package:bungie_api/models/destiny_item_objective_block_definition.dart';
import 'package:bungie_api/models/destiny_item_objectives_component.dart';
import 'package:bungie_api/models/destiny_item_peer_view.dart';
import 'package:bungie_api/models/destiny_item_perk_entry_definition.dart';
import 'package:bungie_api/models/destiny_item_perks_component.dart';
import 'package:bungie_api/models/destiny_item_plug.dart';
import 'package:bungie_api/models/destiny_item_plug_base.dart';
import 'package:bungie_api/models/destiny_item_plug_component.dart';
import 'package:bungie_api/models/destiny_item_plug_definition.dart';
import 'package:bungie_api/models/destiny_item_plug_objectives_component.dart';
import 'package:bungie_api/models/destiny_item_preview_block_definition.dart';
import 'package:bungie_api/models/destiny_item_quality_block_definition.dart';
import 'package:bungie_api/models/destiny_item_quantity.dart';
import 'package:bungie_api/models/destiny_item_render_component.dart';
import 'package:bungie_api/models/destiny_item_response.dart';
import 'package:bungie_api/models/destiny_item_reusable_plugs_component.dart';
import 'package:bungie_api/models/destiny_item_sack_block_definition.dart';
import 'package:bungie_api/models/destiny_item_set_action_request.dart';
import 'package:bungie_api/models/destiny_item_set_block_definition.dart';
import 'package:bungie_api/models/destiny_item_set_block_entry_definition.dart';
import 'package:bungie_api/models/destiny_item_socket_block_definition.dart';
import 'package:bungie_api/models/destiny_item_socket_category_definition.dart';
import 'package:bungie_api/models/destiny_item_socket_entry_definition.dart';
import 'package:bungie_api/models/destiny_item_socket_entry_plug_item_definition.dart';
import 'package:bungie_api/models/destiny_item_socket_entry_plug_item_randomized_definition.dart';
import 'package:bungie_api/models/destiny_item_socket_state.dart';
import 'package:bungie_api/models/destiny_item_sockets_component.dart';
import 'package:bungie_api/models/destiny_item_source_block_definition.dart';
import 'package:bungie_api/models/destiny_item_source_definition.dart';
import 'package:bungie_api/models/destiny_item_stat_block_definition.dart';
import 'package:bungie_api/models/destiny_item_state_request.dart';
import 'package:bungie_api/models/destiny_item_stats_component.dart';
import 'package:bungie_api/models/destiny_item_summary_block_definition.dart';
import 'package:bungie_api/models/destiny_item_talent_grid_block_definition.dart';
import 'package:bungie_api/models/destiny_item_talent_grid_component.dart';
import 'package:bungie_api/models/destiny_item_tier_type_definition.dart';
import 'package:bungie_api/models/destiny_item_tier_type_infusion_block.dart';
import 'package:bungie_api/models/destiny_item_tooltip_notification.dart';
import 'package:bungie_api/models/destiny_item_transfer_request.dart';
import 'package:bungie_api/models/destiny_item_translation_block_definition.dart';
import 'package:bungie_api/models/destiny_item_value_block_definition.dart';
import 'package:bungie_api/models/destiny_item_vendor_source_reference.dart';
import 'package:bungie_api/models/destiny_item_version_definition.dart';
import 'package:bungie_api/models/destiny_kiosk_item.dart';
import 'package:bungie_api/models/destiny_kiosks_component.dart';
import 'package:bungie_api/models/destiny_leaderboard.dart';
import 'package:bungie_api/models/destiny_leaderboard_entry.dart';
import 'package:bungie_api/models/destiny_leaderboard_results.dart';
import 'package:bungie_api/models/destiny_linked_graph_definition.dart';
import 'package:bungie_api/models/destiny_linked_graph_entry_definition.dart';
import 'package:bungie_api/models/destiny_linked_profiles_response.dart';
import 'package:bungie_api/models/destiny_location_definition.dart';
import 'package:bungie_api/models/destiny_location_release_definition.dart';
import 'package:bungie_api/models/destiny_lore_definition.dart';
import 'package:bungie_api/models/destiny_manifest.dart';
import 'package:bungie_api/models/destiny_material_requirement.dart';
import 'package:bungie_api/models/destiny_material_requirement_set_definition.dart';
import 'package:bungie_api/models/destiny_medal_tier_definition.dart';
import 'package:bungie_api/models/destiny_metric_component.dart';
import 'package:bungie_api/models/destiny_metric_definition.dart';
import 'package:bungie_api/models/destiny_metrics_component.dart';
import 'package:bungie_api/models/destiny_milestone.dart';
import 'package:bungie_api/models/destiny_milestone_activity.dart';
import 'package:bungie_api/models/destiny_milestone_activity_completion_status.dart';
import 'package:bungie_api/models/destiny_milestone_activity_definition.dart';
import 'package:bungie_api/models/destiny_milestone_activity_phase.dart';
import 'package:bungie_api/models/destiny_milestone_activity_variant.dart';
import 'package:bungie_api/models/destiny_milestone_activity_variant_definition.dart';
import 'package:bungie_api/models/destiny_milestone_challenge_activity.dart';
import 'package:bungie_api/models/destiny_milestone_challenge_activity_definition.dart';
import 'package:bungie_api/models/destiny_milestone_challenge_activity_graph_node_entry.dart';
import 'package:bungie_api/models/destiny_milestone_challenge_activity_phase.dart';
import 'package:bungie_api/models/destiny_milestone_challenge_definition.dart';
import 'package:bungie_api/models/destiny_milestone_content.dart';
import 'package:bungie_api/models/destiny_milestone_content_item_category.dart';
import 'package:bungie_api/models/destiny_milestone_definition.dart';
import 'package:bungie_api/models/destiny_milestone_quest.dart';
import 'package:bungie_api/models/destiny_milestone_quest_definition.dart';
import 'package:bungie_api/models/destiny_milestone_quest_reward_item.dart';
import 'package:bungie_api/models/destiny_milestone_quest_rewards_definition.dart';
import 'package:bungie_api/models/destiny_milestone_reward_category.dart';
import 'package:bungie_api/models/destiny_milestone_reward_category_definition.dart';
import 'package:bungie_api/models/destiny_milestone_reward_entry.dart';
import 'package:bungie_api/models/destiny_milestone_reward_entry_definition.dart';
import 'package:bungie_api/models/destiny_milestone_value_definition.dart';
import 'package:bungie_api/models/destiny_milestone_vendor.dart';
import 'package:bungie_api/models/destiny_milestone_vendor_definition.dart';
import 'package:bungie_api/models/destiny_node_activation_requirement.dart';
import 'package:bungie_api/models/destiny_node_socket_replace_response.dart';
import 'package:bungie_api/models/destiny_node_step_definition.dart';
import 'package:bungie_api/models/destiny_objective_definition.dart';
import 'package:bungie_api/models/destiny_objective_display_properties.dart';
import 'package:bungie_api/models/destiny_objective_perk_entry_definition.dart';
import 'package:bungie_api/models/destiny_objective_progress.dart';
import 'package:bungie_api/models/destiny_objective_stat_entry_definition.dart';
import 'package:bungie_api/models/destiny_parent_item_override.dart';
import 'package:bungie_api/models/destiny_perk_reference.dart';
import 'package:bungie_api/models/destiny_place_definition.dart';
import 'package:bungie_api/models/destiny_platform_silver_component.dart';
import 'package:bungie_api/models/destiny_player.dart';
import 'package:bungie_api/models/destiny_plug_item_crafting_requirements.dart';
import 'package:bungie_api/models/destiny_plug_item_crafting_unlock_requirement.dart';
import 'package:bungie_api/models/destiny_plug_rule_definition.dart';
import 'package:bungie_api/models/destiny_plug_set_definition.dart';
import 'package:bungie_api/models/destiny_plug_sets_component.dart';
import 'package:bungie_api/models/destiny_plug_whitelist_entry_definition.dart';
import 'package:bungie_api/models/destiny_position_definition.dart';
import 'package:bungie_api/models/destiny_post_game_carnage_report_data.dart';
import 'package:bungie_api/models/destiny_post_game_carnage_report_entry.dart';
import 'package:bungie_api/models/destiny_post_game_carnage_report_extended_data.dart';
import 'package:bungie_api/models/destiny_post_game_carnage_report_team_entry.dart';
import 'package:bungie_api/models/destiny_postmaster_transfer_request.dart';
import 'package:bungie_api/models/destiny_power_cap_definition.dart';
import 'package:bungie_api/models/destiny_presentation_child_block.dart';
import 'package:bungie_api/models/destiny_presentation_node_base_definition.dart';
import 'package:bungie_api/models/destiny_presentation_node_child_entry.dart';
import 'package:bungie_api/models/destiny_presentation_node_child_entry_base.dart';
import 'package:bungie_api/models/destiny_presentation_node_children_block.dart';
import 'package:bungie_api/models/destiny_presentation_node_collectible_child_entry.dart';
import 'package:bungie_api/models/destiny_presentation_node_component.dart';
import 'package:bungie_api/models/destiny_presentation_node_craftable_child_entry.dart';
import 'package:bungie_api/models/destiny_presentation_node_definition.dart';
import 'package:bungie_api/models/destiny_presentation_node_metric_child_entry.dart';
import 'package:bungie_api/models/destiny_presentation_node_record_child_entry.dart';
import 'package:bungie_api/models/destiny_presentation_node_requirements_block.dart';
import 'package:bungie_api/models/destiny_presentation_nodes_component.dart';
import 'package:bungie_api/models/destiny_profile_collectibles_component.dart';
import 'package:bungie_api/models/destiny_profile_component.dart';
import 'package:bungie_api/models/destiny_profile_progression_component.dart';
import 'package:bungie_api/models/destiny_profile_records_component.dart';
import 'package:bungie_api/models/destiny_profile_response.dart';
import 'package:bungie_api/models/destiny_profile_transitory_component.dart';
import 'package:bungie_api/models/destiny_profile_transitory_current_activity.dart';
import 'package:bungie_api/models/destiny_profile_transitory_joinability.dart';
import 'package:bungie_api/models/destiny_profile_transitory_party_member.dart';
import 'package:bungie_api/models/destiny_profile_transitory_tracking_entry.dart';
import 'package:bungie_api/models/destiny_profile_user_info_card.dart';
import 'package:bungie_api/models/destiny_progression.dart';
import 'package:bungie_api/models/destiny_progression_definition.dart';
import 'package:bungie_api/models/destiny_progression_display_properties_definition.dart';
import 'package:bungie_api/models/destiny_progression_level_requirement_definition.dart';
import 'package:bungie_api/models/destiny_progression_mapping_definition.dart';
import 'package:bungie_api/models/destiny_progression_reset_entry.dart';
import 'package:bungie_api/models/destiny_progression_reward_definition.dart';
import 'package:bungie_api/models/destiny_progression_reward_item_quantity.dart';
import 'package:bungie_api/models/destiny_progression_step_definition.dart';
import 'package:bungie_api/models/destiny_public_activity_status.dart';
import 'package:bungie_api/models/destiny_public_milestone.dart';
import 'package:bungie_api/models/destiny_public_milestone_activity.dart';
import 'package:bungie_api/models/destiny_public_milestone_activity_variant.dart';
import 'package:bungie_api/models/destiny_public_milestone_challenge.dart';
import 'package:bungie_api/models/destiny_public_milestone_challenge_activity.dart';
import 'package:bungie_api/models/destiny_public_milestone_quest.dart';
import 'package:bungie_api/models/destiny_public_milestone_vendor.dart';
import 'package:bungie_api/models/destiny_public_vendor_component.dart';
import 'package:bungie_api/models/destiny_public_vendor_sale_item_component.dart';
import 'package:bungie_api/models/destiny_public_vendors_response.dart';
import 'package:bungie_api/models/destiny_quest_status.dart';
import 'package:bungie_api/models/destiny_race_definition.dart';
import 'package:bungie_api/models/destiny_record_completion_block.dart';
import 'package:bungie_api/models/destiny_record_component.dart';
import 'package:bungie_api/models/destiny_record_definition.dart';
import 'package:bungie_api/models/destiny_record_expiration_block.dart';
import 'package:bungie_api/models/destiny_record_interval_block.dart';
import 'package:bungie_api/models/destiny_record_interval_objective.dart';
import 'package:bungie_api/models/destiny_record_interval_rewards.dart';
import 'package:bungie_api/models/destiny_record_title_block.dart';
import 'package:bungie_api/models/destiny_records_component.dart';
import 'package:bungie_api/models/destiny_report_offense_pgcr_request.dart';
import 'package:bungie_api/models/destiny_report_reason_category_definition.dart';
import 'package:bungie_api/models/destiny_report_reason_definition.dart';
import 'package:bungie_api/models/destiny_reward_source_definition.dart';
import 'package:bungie_api/models/destiny_sandbox_perk_definition.dart';
import 'package:bungie_api/models/destiny_scored_presentation_node_base_definition.dart';
import 'package:bungie_api/models/destiny_season_definition.dart';
import 'package:bungie_api/models/destiny_season_pass_definition.dart';
import 'package:bungie_api/models/destiny_season_preview_definition.dart';
import 'package:bungie_api/models/destiny_season_preview_image_definition.dart';
import 'package:bungie_api/models/destiny_socket_category_definition.dart';
import 'package:bungie_api/models/destiny_socket_type_definition.dart';
import 'package:bungie_api/models/destiny_socket_type_scalar_material_requirement_entry.dart';
import 'package:bungie_api/models/destiny_stat.dart';
import 'package:bungie_api/models/destiny_stat_definition.dart';
import 'package:bungie_api/models/destiny_stat_display_definition.dart';
import 'package:bungie_api/models/destiny_stat_group_definition.dart';
import 'package:bungie_api/models/destiny_stat_override_definition.dart';
import 'package:bungie_api/models/destiny_string_variables_component.dart';
import 'package:bungie_api/models/destiny_talent_exclusive_group.dart';
import 'package:bungie_api/models/destiny_talent_grid_definition.dart';
import 'package:bungie_api/models/destiny_talent_node.dart';
import 'package:bungie_api/models/destiny_talent_node_category.dart';
import 'package:bungie_api/models/destiny_talent_node_definition.dart';
import 'package:bungie_api/models/destiny_talent_node_exclusive_set_definition.dart';
import 'package:bungie_api/models/destiny_talent_node_stat_block.dart';
import 'package:bungie_api/models/destiny_talent_node_step_groups.dart';
import 'package:bungie_api/models/destiny_trait_category_definition.dart';
import 'package:bungie_api/models/destiny_trait_definition.dart';
import 'package:bungie_api/models/destiny_unlock_definition.dart';
import 'package:bungie_api/models/destiny_unlock_expression_definition.dart';
import 'package:bungie_api/models/destiny_unlock_status.dart';
import 'package:bungie_api/models/destiny_unlock_value_definition.dart';
import 'package:bungie_api/models/destiny_vendor_accepted_item_definition.dart';
import 'package:bungie_api/models/destiny_vendor_action_definition.dart';
import 'package:bungie_api/models/destiny_vendor_base_component.dart';
import 'package:bungie_api/models/destiny_vendor_categories_component.dart';
import 'package:bungie_api/models/destiny_vendor_category.dart';
import 'package:bungie_api/models/destiny_vendor_category_entry_definition.dart';
import 'package:bungie_api/models/destiny_vendor_category_overlay_definition.dart';
import 'package:bungie_api/models/destiny_vendor_component.dart';
import 'package:bungie_api/models/destiny_vendor_definition.dart';
import 'package:bungie_api/models/destiny_vendor_display_properties_definition.dart';
import 'package:bungie_api/models/destiny_vendor_group.dart';
import 'package:bungie_api/models/destiny_vendor_group_component.dart';
import 'package:bungie_api/models/destiny_vendor_group_definition.dart';
import 'package:bungie_api/models/destiny_vendor_group_reference.dart';
import 'package:bungie_api/models/destiny_vendor_interaction_definition.dart';
import 'package:bungie_api/models/destiny_vendor_interaction_reply_definition.dart';
import 'package:bungie_api/models/destiny_vendor_interaction_sack_entry_definition.dart';
import 'package:bungie_api/models/destiny_vendor_inventory_flyout_bucket_definition.dart';
import 'package:bungie_api/models/destiny_vendor_inventory_flyout_definition.dart';
import 'package:bungie_api/models/destiny_vendor_item_definition.dart';
import 'package:bungie_api/models/destiny_vendor_item_quantity.dart';
import 'package:bungie_api/models/destiny_vendor_item_socket_override.dart';
import 'package:bungie_api/models/destiny_vendor_location_definition.dart';
import 'package:bungie_api/models/destiny_vendor_receipt.dart';
import 'package:bungie_api/models/destiny_vendor_receipts_component.dart';
import 'package:bungie_api/models/destiny_vendor_requirement_display_entry_definition.dart';
import 'package:bungie_api/models/destiny_vendor_response.dart';
import 'package:bungie_api/models/destiny_vendor_sale_item_action_block_definition.dart';
import 'package:bungie_api/models/destiny_vendor_sale_item_base_component.dart';
import 'package:bungie_api/models/destiny_vendor_sale_item_component.dart';
import 'package:bungie_api/models/destiny_vendor_sale_item_set_component_of_destiny_public_vendor_sale_item_component.dart';
import 'package:bungie_api/models/destiny_vendor_sale_item_set_component_of_destiny_vendor_sale_item_component.dart';
import 'package:bungie_api/models/destiny_vendor_service_definition.dart';
import 'package:bungie_api/models/destiny_vendors_response.dart';
import 'package:bungie_api/models/dictionary_component_response_ofint32_and_destiny_item_instance_component.dart';
import 'package:bungie_api/models/dictionary_component_response_ofint32_and_destiny_item_objectives_component.dart';
import 'package:bungie_api/models/dictionary_component_response_ofint32_and_destiny_item_perks_component.dart';
import 'package:bungie_api/models/dictionary_component_response_ofint32_and_destiny_item_plug_objectives_component.dart';
import 'package:bungie_api/models/dictionary_component_response_ofint32_and_destiny_item_render_component.dart';
import 'package:bungie_api/models/dictionary_component_response_ofint32_and_destiny_item_reusable_plugs_component.dart';
import 'package:bungie_api/models/dictionary_component_response_ofint32_and_destiny_item_sockets_component.dart';
import 'package:bungie_api/models/dictionary_component_response_ofint32_and_destiny_item_stats_component.dart';
import 'package:bungie_api/models/dictionary_component_response_ofint32_and_destiny_item_talent_grid_component.dart';
import 'package:bungie_api/models/dictionary_component_response_ofint32_and_destiny_vendor_sale_item_component.dart';
import 'package:bungie_api/models/dictionary_component_response_ofint64_and_destiny_character_activities_component.dart';
import 'package:bungie_api/models/dictionary_component_response_ofint64_and_destiny_character_component.dart';
import 'package:bungie_api/models/dictionary_component_response_ofint64_and_destiny_character_progression_component.dart';
import 'package:bungie_api/models/dictionary_component_response_ofint64_and_destiny_character_records_component.dart';
import 'package:bungie_api/models/dictionary_component_response_ofint64_and_destiny_character_render_component.dart';
import 'package:bungie_api/models/dictionary_component_response_ofint64_and_destiny_collectibles_component.dart';
import 'package:bungie_api/models/dictionary_component_response_ofint64_and_destiny_craftables_component.dart';
import 'package:bungie_api/models/dictionary_component_response_ofint64_and_destiny_currencies_component.dart';
import 'package:bungie_api/models/dictionary_component_response_ofint64_and_destiny_inventory_component.dart';
import 'package:bungie_api/models/dictionary_component_response_ofint64_and_destiny_item_instance_component.dart';
import 'package:bungie_api/models/dictionary_component_response_ofint64_and_destiny_item_objectives_component.dart';
import 'package:bungie_api/models/dictionary_component_response_ofint64_and_destiny_item_perks_component.dart';
import 'package:bungie_api/models/dictionary_component_response_ofint64_and_destiny_item_plug_objectives_component.dart';
import 'package:bungie_api/models/dictionary_component_response_ofint64_and_destiny_item_render_component.dart';
import 'package:bungie_api/models/dictionary_component_response_ofint64_and_destiny_item_reusable_plugs_component.dart';
import 'package:bungie_api/models/dictionary_component_response_ofint64_and_destiny_item_sockets_component.dart';
import 'package:bungie_api/models/dictionary_component_response_ofint64_and_destiny_item_stats_component.dart';
import 'package:bungie_api/models/dictionary_component_response_ofint64_and_destiny_item_talent_grid_component.dart';
import 'package:bungie_api/models/dictionary_component_response_ofint64_and_destiny_kiosks_component.dart';
import 'package:bungie_api/models/dictionary_component_response_ofint64_and_destiny_plug_sets_component.dart';
import 'package:bungie_api/models/dictionary_component_response_ofint64_and_destiny_presentation_nodes_component.dart';
import 'package:bungie_api/models/dictionary_component_response_ofint64_and_destiny_string_variables_component.dart';
import 'package:bungie_api/models/dictionary_component_response_ofuint32_and_destiny_item_instance_component.dart';
import 'package:bungie_api/models/dictionary_component_response_ofuint32_and_destiny_item_objectives_component.dart';
import 'package:bungie_api/models/dictionary_component_response_ofuint32_and_destiny_item_perks_component.dart';
import 'package:bungie_api/models/dictionary_component_response_ofuint32_and_destiny_item_plug_component.dart';
import 'package:bungie_api/models/dictionary_component_response_ofuint32_and_destiny_item_plug_objectives_component.dart';
import 'package:bungie_api/models/dictionary_component_response_ofuint32_and_destiny_item_render_component.dart';
import 'package:bungie_api/models/dictionary_component_response_ofuint32_and_destiny_item_reusable_plugs_component.dart';
import 'package:bungie_api/models/dictionary_component_response_ofuint32_and_destiny_item_sockets_component.dart';
import 'package:bungie_api/models/dictionary_component_response_ofuint32_and_destiny_item_stats_component.dart';
import 'package:bungie_api/models/dictionary_component_response_ofuint32_and_destiny_item_talent_grid_component.dart';
import 'package:bungie_api/models/dictionary_component_response_ofuint32_and_destiny_public_vendor_component.dart';
import 'package:bungie_api/models/dictionary_component_response_ofuint32_and_destiny_vendor_categories_component.dart';
import 'package:bungie_api/models/dictionary_component_response_ofuint32_and_destiny_vendor_component.dart';
import 'package:bungie_api/models/dictionary_component_response_ofuint32_and_personal_destiny_vendor_sale_item_set_component.dart';
import 'package:bungie_api/models/dictionary_component_response_ofuint32_and_public_destiny_vendor_sale_item_set_component.dart';
import 'package:bungie_api/models/dye_reference.dart';
import 'package:bungie_api/models/email_opt_in_definition.dart';
import 'package:bungie_api/models/email_setting_localization.dart';
import 'package:bungie_api/models/email_setting_subscription_localization.dart';
import 'package:bungie_api/models/email_settings.dart';
import 'package:bungie_api/models/email_subscription_definition.dart';
import 'package:bungie_api/models/email_view_definition.dart';
import 'package:bungie_api/models/email_view_definition_setting.dart';
import 'package:bungie_api/models/entity_action_result.dart';
import 'package:bungie_api/models/exact_search_request.dart';
import 'package:bungie_api/models/fireteam_member.dart';
import 'package:bungie_api/models/fireteam_response.dart';
import 'package:bungie_api/models/fireteam_summary.dart';
import 'package:bungie_api/models/fireteam_user_info_card.dart';
import 'package:bungie_api/models/forum_recruitment_detail.dart';
import 'package:bungie_api/models/gear_asset_data_base_definition.dart';
import 'package:bungie_api/models/general_user.dart';
import 'package:bungie_api/models/get_credential_types_for_account_response.dart';
import 'package:bungie_api/models/get_groups_for_member_response.dart';
import 'package:bungie_api/models/global_alert.dart';
import 'package:bungie_api/models/group_application_list_request.dart';
import 'package:bungie_api/models/group_application_request.dart';
import 'package:bungie_api/models/group_application_response.dart';
import 'package:bungie_api/models/group_ban.dart';
import 'package:bungie_api/models/group_ban_request.dart';
import 'package:bungie_api/models/group_edit_action.dart';
import 'package:bungie_api/models/group_features.dart';
import 'package:bungie_api/models/group_member.dart';
import 'package:bungie_api/models/group_member_application.dart';
import 'package:bungie_api/models/group_member_leave_result.dart';
import 'package:bungie_api/models/group_membership.dart';
import 'package:bungie_api/models/group_membership_base.dart';
import 'package:bungie_api/models/group_membership_search_response.dart';
import 'package:bungie_api/models/group_name_search_request.dart';
import 'package:bungie_api/models/group_optional_conversation.dart';
import 'package:bungie_api/models/group_optional_conversation_add_request.dart';
import 'package:bungie_api/models/group_optional_conversation_edit_request.dart';
import 'package:bungie_api/models/group_options_edit_action.dart';
import 'package:bungie_api/models/group_potential_member.dart';
import 'package:bungie_api/models/group_potential_membership.dart';
import 'package:bungie_api/models/group_potential_membership_search_response.dart';
import 'package:bungie_api/models/group_query.dart';
import 'package:bungie_api/models/group_response.dart';
import 'package:bungie_api/models/group_search_response.dart';
import 'package:bungie_api/models/group_theme.dart';
import 'package:bungie_api/models/group_user_base.dart';
import 'package:bungie_api/models/group_user_info_card.dart';
import 'package:bungie_api/models/group_v2.dart';
import 'package:bungie_api/models/group_v2_card.dart';
import 'package:bungie_api/models/group_v2_clan_info.dart';
import 'package:bungie_api/models/group_v2_clan_info_and_investment.dart';
import 'package:bungie_api/models/hard_linked_user_membership.dart';
import 'package:bungie_api/models/hyperlink_reference.dart';
import 'package:bungie_api/models/ignore_response.dart';
import 'package:bungie_api/models/image_pyramid_entry.dart';
import 'package:bungie_api/models/interpolation_point.dart';
import 'package:bungie_api/models/interpolation_point_float.dart';
import 'package:bungie_api/models/inventory_changed_response.dart';
import 'package:bungie_api/models/paged_query.dart';
import 'package:bungie_api/models/partner_offer_claim_request.dart';
import 'package:bungie_api/models/partner_offer_history_response.dart';
import 'package:bungie_api/models/partner_offer_sku_history_response.dart';
import 'package:bungie_api/models/personal_destiny_vendor_sale_item_set_component.dart';
import 'package:bungie_api/models/platform_friend.dart';
import 'package:bungie_api/models/platform_friend_response.dart';
import 'package:bungie_api/models/poll_response.dart';
import 'package:bungie_api/models/poll_result.dart';
import 'package:bungie_api/models/post_response.dart';
import 'package:bungie_api/models/post_search_response.dart';
import 'package:bungie_api/models/public_destiny_vendor_sale_item_set_component.dart';
import 'package:bungie_api/models/schema_record_state_block.dart';
import 'package:bungie_api/models/search_result.dart';
import 'package:bungie_api/models/search_result_of_content_item_public_contract.dart';
import 'package:bungie_api/models/search_result_of_destiny_entity_search_result_item.dart';
import 'package:bungie_api/models/search_result_of_fireteam_response.dart';
import 'package:bungie_api/models/search_result_of_fireteam_summary.dart';
import 'package:bungie_api/models/search_result_of_group_ban.dart';
import 'package:bungie_api/models/search_result_of_group_member.dart';
import 'package:bungie_api/models/search_result_of_group_member_application.dart';
import 'package:bungie_api/models/search_result_of_group_membership.dart';
import 'package:bungie_api/models/search_result_of_group_potential_membership.dart';
import 'package:bungie_api/models/search_result_of_group_v2_card.dart';
import 'package:bungie_api/models/search_result_of_post_response.dart';
import 'package:bungie_api/models/search_result_of_trending_entry.dart';
import 'package:bungie_api/models/series.dart';
import 'package:bungie_api/models/single_component_response_of_destiny_character_activities_component.dart';
import 'package:bungie_api/models/single_component_response_of_destiny_character_component.dart';
import 'package:bungie_api/models/single_component_response_of_destiny_character_progression_component.dart';
import 'package:bungie_api/models/single_component_response_of_destiny_character_records_component.dart';
import 'package:bungie_api/models/single_component_response_of_destiny_character_render_component.dart';
import 'package:bungie_api/models/single_component_response_of_destiny_collectibles_component.dart';
import 'package:bungie_api/models/single_component_response_of_destiny_currencies_component.dart';
import 'package:bungie_api/models/single_component_response_of_destiny_inventory_component.dart';
import 'package:bungie_api/models/single_component_response_of_destiny_item_component.dart';
import 'package:bungie_api/models/single_component_response_of_destiny_item_instance_component.dart';
import 'package:bungie_api/models/single_component_response_of_destiny_item_objectives_component.dart';
import 'package:bungie_api/models/single_component_response_of_destiny_item_perks_component.dart';
import 'package:bungie_api/models/single_component_response_of_destiny_item_plug_objectives_component.dart';
import 'package:bungie_api/models/single_component_response_of_destiny_item_render_component.dart';
import 'package:bungie_api/models/single_component_response_of_destiny_item_reusable_plugs_component.dart';
import 'package:bungie_api/models/single_component_response_of_destiny_item_sockets_component.dart';
import 'package:bungie_api/models/single_component_response_of_destiny_item_stats_component.dart';
import 'package:bungie_api/models/single_component_response_of_destiny_item_talent_grid_component.dart';
import 'package:bungie_api/models/single_component_response_of_destiny_kiosks_component.dart';
import 'package:bungie_api/models/single_component_response_of_destiny_metrics_component.dart';
import 'package:bungie_api/models/single_component_response_of_destiny_platform_silver_component.dart';
import 'package:bungie_api/models/single_component_response_of_destiny_plug_sets_component.dart';
import 'package:bungie_api/models/single_component_response_of_destiny_presentation_nodes_component.dart';
import 'package:bungie_api/models/single_component_response_of_destiny_profile_collectibles_component.dart';
import 'package:bungie_api/models/single_component_response_of_destiny_profile_component.dart';
import 'package:bungie_api/models/single_component_response_of_destiny_profile_progression_component.dart';
import 'package:bungie_api/models/single_component_response_of_destiny_profile_records_component.dart';
import 'package:bungie_api/models/single_component_response_of_destiny_profile_transitory_component.dart';
import 'package:bungie_api/models/single_component_response_of_destiny_string_variables_component.dart';
import 'package:bungie_api/models/single_component_response_of_destiny_vendor_categories_component.dart';
import 'package:bungie_api/models/single_component_response_of_destiny_vendor_component.dart';
import 'package:bungie_api/models/single_component_response_of_destiny_vendor_group_component.dart';
import 'package:bungie_api/models/single_component_response_of_destiny_vendor_receipts_component.dart';
import 'package:bungie_api/models/stream_info.dart';
import 'package:bungie_api/models/tag_metadata_definition.dart';
import 'package:bungie_api/models/tag_metadata_item.dart';
import 'package:bungie_api/models/tag_response.dart';
import 'package:bungie_api/models/trending_categories.dart';
import 'package:bungie_api/models/trending_category.dart';
import 'package:bungie_api/models/trending_detail.dart';
import 'package:bungie_api/models/trending_entry.dart';
import 'package:bungie_api/models/trending_entry_community_creation.dart';
import 'package:bungie_api/models/trending_entry_destiny_activity.dart';
import 'package:bungie_api/models/trending_entry_destiny_item.dart';
import 'package:bungie_api/models/trending_entry_destiny_ritual.dart';
import 'package:bungie_api/models/trending_entry_news.dart';
import 'package:bungie_api/models/trending_entry_support_article.dart';
import 'package:bungie_api/models/user_info_card.dart';
import 'package:bungie_api/models/user_membership.dart';
import 'package:bungie_api/models/user_membership_data.dart';
import 'package:bungie_api/models/user_search_prefix_request.dart';
import 'package:bungie_api/models/user_search_response.dart';
import 'package:bungie_api/models/user_search_response_detail.dart';
import 'package:bungie_api/models/user_theme.dart';
import 'package:bungie_api/models/user_to_user_context.dart';
import 'package:bungie_api/queries.dart';
import 'package:bungie_api/responses/api_usage_response.dart';
import 'package:bungie_api/responses/awa_authorization_result_response.dart';
import 'package:bungie_api/responses/awa_initialize_response_response.dart';
import 'package:bungie_api/responses/boolean_response.dart';
import 'package:bungie_api/responses/bungie_friend_list_response_response.dart';
import 'package:bungie_api/responses/bungie_friend_request_list_response_response.dart';
import 'package:bungie_api/responses/cedictionary_ofstring_andstring_response.dart';
import 'package:bungie_api/responses/celist_of_forum_recruitment_detail_response.dart';
import 'package:bungie_api/responses/celist_of_global_alert_response.dart';
import 'package:bungie_api/responses/celist_of_group_optional_conversation_response.dart';
import 'package:bungie_api/responses/celist_of_partner_offer_sku_history_response_response.dart';
import 'package:bungie_api/responses/clan_banner_source_response.dart';
import 'package:bungie_api/responses/content_item_public_contract_response.dart';
import 'package:bungie_api/responses/content_type_description_response.dart';
import 'package:bungie_api/responses/core_settings_configuration_response.dart';
import 'package:bungie_api/responses/destiny_activity_history_results_response.dart';
import 'package:bungie_api/responses/destiny_aggregate_activity_results_response.dart';
import 'package:bungie_api/responses/destiny_character_response_response.dart';
import 'package:bungie_api/responses/destiny_collectible_node_detail_response_response.dart';
import 'package:bungie_api/responses/destiny_definition_response.dart';
import 'package:bungie_api/responses/destiny_entity_search_result_response.dart';
import 'package:bungie_api/responses/destiny_equip_item_results_response.dart';
import 'package:bungie_api/responses/destiny_historical_stats_account_result_response.dart';
import 'package:bungie_api/responses/destiny_historical_stats_results_response.dart';
import 'package:bungie_api/responses/destiny_historical_weapon_stats_data_response.dart';
import 'package:bungie_api/responses/destiny_item_change_response_response.dart';
import 'package:bungie_api/responses/destiny_item_response_response.dart';
import 'package:bungie_api/responses/destiny_leaderboard_results_response.dart';
import 'package:bungie_api/responses/destiny_linked_profiles_response_response.dart';
import 'package:bungie_api/responses/destiny_manifest_response.dart';
import 'package:bungie_api/responses/destiny_milestone_content_response.dart';
import 'package:bungie_api/responses/destiny_milestone_response.dart';
import 'package:bungie_api/responses/destiny_post_game_carnage_report_data_response.dart';
import 'package:bungie_api/responses/destiny_profile_response_response.dart';
import 'package:bungie_api/responses/destiny_public_vendors_response_response.dart';
import 'package:bungie_api/responses/destiny_vendor_response_response.dart';
import 'package:bungie_api/responses/destiny_vendors_response_response.dart';
import 'package:bungie_api/responses/dictionary_ofint32_andstring_response.dart';
import 'package:bungie_api/responses/dictionary_ofstring_and_core_system_response.dart';
import 'package:bungie_api/responses/dictionary_ofuint32_and_destiny_public_milestone_response.dart';
import 'package:bungie_api/responses/fireteam_response_response.dart';
import 'package:bungie_api/responses/general_user_response.dart';
import 'package:bungie_api/responses/get_groups_for_member_response_response.dart';
import 'package:bungie_api/responses/group_application_response_response.dart';
import 'package:bungie_api/responses/group_member_leave_result_response.dart';
import 'package:bungie_api/responses/group_membership_search_response_response.dart';
import 'package:bungie_api/responses/group_potential_membership_search_response_response.dart';
import 'package:bungie_api/responses/group_response_response.dart';
import 'package:bungie_api/responses/group_search_response_response.dart';
import 'package:bungie_api/responses/hard_linked_user_membership_response.dart';
import 'package:bungie_api/responses/ienumerable_of_application_response.dart';
import 'package:bungie_api/responses/ienumerable_of_user_info_card_response.dart';
import 'package:bungie_api/responses/int32_response.dart';
import 'package:bungie_api/responses/int64_response.dart';
import 'package:bungie_api/responses/iread_only_collection_of_content_item_public_contract_response.dart';
import 'package:bungie_api/responses/list_of_destiny_clan_aggregate_stat_response.dart';
import 'package:bungie_api/responses/list_of_entity_action_result_response.dart';
import 'package:bungie_api/responses/list_of_get_credential_types_for_account_response_response.dart';
import 'package:bungie_api/responses/list_of_group_theme_response.dart';
import 'package:bungie_api/responses/list_of_group_v2_card_response.dart';
import 'package:bungie_api/responses/list_of_tag_response_response.dart';
import 'package:bungie_api/responses/list_of_user_theme_response.dart';
import 'package:bungie_api/responses/platform_friend_response_response.dart';
import 'package:bungie_api/responses/post_search_response_response.dart';
import 'package:bungie_api/responses/read_only_dictionary_ofstring_and_destiny_historical_stats_definition_response.dart';
import 'package:bungie_api/responses/search_result_of_content_item_public_contract_response.dart';
import 'package:bungie_api/responses/search_result_of_fireteam_response_response.dart';
import 'package:bungie_api/responses/search_result_of_fireteam_summary_response.dart';
import 'package:bungie_api/responses/search_result_of_group_ban_response.dart';
import 'package:bungie_api/responses/search_result_of_group_member_application_response.dart';
import 'package:bungie_api/responses/search_result_of_group_member_response.dart';
import 'package:bungie_api/responses/search_result_of_trending_entry_response.dart';
import 'package:bungie_api/responses/trending_categories_response.dart';
import 'package:bungie_api/responses/trending_detail_response.dart';
import 'package:bungie_api/responses/user_membership_data_response.dart';
import 'package:bungie_api/responses/user_search_response_response.dart';
import 'package:bungie_api/settings.dart';
import 'package:bungie_api/social.dart';
import 'package:bungie_api/tags.dart';
import 'package:bungie_api/tokens.dart';
import 'package:bungie_api/trending.dart';
import 'package:bungie_api/user.dart';
import 'package:bungie_api/usersystemoverrides.dart';

Author: LittleLightForDestiny
Source Code: https://github.com/LittleLightForDestiny/bungie-api-dart 
License: MIT license

#flutter #dart #api 

Bungie-api-dart: Dart Definitions for The Bungie.net API
fd MOMIN

fd MOMIN

1656744485

How to Run a Flutter Web App with Node.js and API

 

Flutter at a glance

Flutter is one of Google’s solutions to cross-platform development. While it is fairly new on the scene, its feature set makes it an instant competitor in this space.

It compiles your app down to native code that runs on iOS or Android, resulting in incredible end-user performance and frame rates. It supports stateful hot reloading during development, meaning you can make changes to your code and watch them get applied on your emulator or physical device with no need to restart your app or lose your app state.

Flutter’s primary focus has been iOS and Android. With the 1.9 release, web support has been added as a technical preview. It is still in its early days, and it may not be production-ready just yet, but it is certainly exciting and promising. Minimal changes are required to take an existing Flutter app and compile it into an HTML, CSS, and JS bundle, as you will soon see.

Why run a Flutter web app on Node.js?

Flutter web apps can run on any web server. So why would you want to host your Flutter web app on a Node.js server? Well, to be honest, for many of the same reasons that you’d choose Node.js for your other web apps and APIs: it is incredibly good at servicing large volumes of simple requests, you can code your front end and back end in JavaScript, and so on.

You might already have a Node.js API that serves data to your Flutter iOS or Android apps. Compiling your Flutter app as a web app and hosting it on your existing Node.js server might be a logical extension to your current solution, with no need to add additional hosting costs to the equation.

Demonstration

It’s time to dive into the code and see Flutter web in action. In order to follow along with the example, you will need the following tools:

  • Android Studio (Android SDK manager and emulator)
  • Visual Studio Code + Flutter extension (or Android Studio)
  • Node.js 12

Flutter has fantastic developer documentation. If this is your first time developing a Flutter app.

You will have the opportunity to choose which editor you want to develop in. The examples and instructions in this article are based on Visual Studio Code, but you should still be able to follow along if you choose to use Android Studio instead.

A Node.js 12 server is required to run the web version of the Flutter weather app as well as the back-end weather API.

Step 1: Explore the sample code

In order to demonstrate how to add web support to an existing Flutter app, we will start with a simple weather app that has been tested on Android 10 (API level 29).

Flutter Weather App On Mobile

The weather app allows the user to view the current weather for a predefined list of cities. Weather data is retrieved from a back-end server running on Node.js.

Clone the source code for the weather app and server from GitHub:

Tip: The weather-app-nodejs-server repository has a flutter-web-support branch that contains the completed version of the app copied to the server with Flutter web support enabled.

It is best to clone both repositories beside each other in the same parent folder. The contents of the weather_app_flutter repository will be built and copied to a folder within the weather-app-nodejs-server repository.

Explore the Flutter weather app

Open the weather_app_flutter repository in your editor. Let’s take a closer look at the main.dart file. It contains the scaffolding and widgets that make up the app’s user interface. The Home widget class has a fetchWeatherData function that calls the back-end weather API to retrieve data and update the widget’s state:

fetchWeatherData({String location}) async {
    var url = WEATHER_API_URL + location;
    final response = await http.get(url);
    if (response.statusCode == 200) {
      var jsonResponse = convert.jsonDecode(response.body);
      setState(() {
        this._weatherData = WeatherData(
          jsonResponse\['weather'\]['location'],
          jsonResponse\['weather'\]['temperature'],
          jsonResponse\['weather'\]['weatherDescription'],
        );
        this._apiError = null;
      });
    } else {
      setState(() {
        this._apiError =
            'Unable to retrieve weather data from API (HTTP ${response.statusCode})';
      });
    }
  }

The fetchWeatherData function uses Dart’s http package to connect to the server over HTTP. There are other Dart packages that you could use, but this is the officially recommended package if you plan on adding web support to your Flutter app.

Also make note of the WEATHER_API_URL constant. Update the value of this constant before running the app so that it can connect to the API running on your local Node.js server. The URL must contain your machine’s hostname. A localhost URL will not be accessible to the Android emulator or physical device.

Explore the Node.js server and weather API

Open up the weather-app-nodejs-server repository in your editor.

Node.js Server Repository In Our Code Editor

There are a few important files and folders to review:

  • The public/api-test.html file can be used to quickly test that your server is working as expected after startup (e.g., http://localhost:3000/api-test.html)
  • The routes/weather.js file contains a simple GET API that accepts a path parameter and returns weather data (e.g., http://localhost:3000/api/weather/londonon)
  • The public-flutter folder is where you will copy the compiled web version of the weather app. The Node.js server is set up to serve files from this directory to the root context (e.g., http://localhost:3000)

Step 2: Add web support to the Flutter app

Since web support is still a technical preview, you need the latest in-development version of Flutter, also referred to as the master channel. In the root folder of the weather_app_flutter repository, run the following commands:

flutter channel master
flutter upgrade

Tip : You may encounter an “Unknown operating system. Cannot install Dart SDK.” error on Windows when running Flutter commands in a bash shell in Visual Studio Code. Try running the commands in a normal Windows command shell.

The upgrade process may take a few minutes. Next, you will need to enable web support in your Flutter installation so that it is available to this and other apps you develop on this workstation:

flutter config --enable-web
flutter devices

Once web support is enabled, you will see a new Chrome device in the device list. Restart Visual Studio Code after running these commands to refresh the device list menu if you don’t see Chrome in that list yet.

To add web support to the weather app, you need to run this command in the top-level folder of the weather_flutter_app repository:

flutter create .

The create command will make a few modifications to the app, which you can see in this commit. The most notable change is the addition of a web subfolder that contains an index.html:

Index.html File In Our Code Editor

Start the Node.js server by running this command in the root of the weather-app-nodejs-server repository:

npm start

Select Chrome from the device list in Visual Studio Code and then start the debugging. Alternatively, you can run the following flutter command:

flutter run -d chrome

The first time you start the app in Chrome may take a little longer while Flutter downloads additional dependencies on the fly. Chrome will eventually open, and you will see the weather app running in the browser. Some of the styling will be slightly different than what you saw on the emulator or physical device.

App Preview In Chrome

At this point, you will notice that the app is not displaying any data from the weather API. If you open Chrome DevTools, you will see a cross-origin resource sharing error.

The browser is not allowing the request to be made from the Flutter web server to the Node.js server since they are running on different ports. You could solve this problem by enabling cross-origin resource sharing on the server or installing a Chrome plugin to disable CORS.

We are going to ignore the error for now since in the next step we will run the pre-compiled Flutter web code directly on the Node.js server, thus eliminating the cross-origin requests altogether.

Try making a change to some of the code in the main.dart file and let Flutter recompile your app. You will notice that your changes do not immediately appear in the browser. This is because Flutter web does not yet support hot stateful reloading. Hopefully support for this awesome capability will come soon.

Step 3: Run the Flutter web app on Node.js

Now that you can run the weather app in the browser using Flutter, the next step is to build and copy it to the Node.js server to run alongside the API.

To build a Flutter web app bundle, run this command:

flutter build web

The build command will produce the build/web folder containing all the static files that make up the weather app.

Contents Of The build/web Folder In Our Editor

Copy the contents of weather_app_flutter/build/web to weather-app-nodejs-server/public-flutter. If your Node.js server in is still running, stop it and restart it to pick up the new files.

Access your Node.js server in the browser at http://localhost:3000 to see your app running on Node.js. This time, your app will display weather data retrieved from the weather API without the cross-origin resource sharing error.

Final App Running In The Browser

Final thoughts

It is incredible how simple it was to take an existing Flutter app and compile it into a web app ready to be deployed to a web server. The user interface rendered in the browser looks nearly identical to the user interface in Android.

Tread lightly if you are considering Flutter as your cross-platform app framework solely because of its web support. The Flutter team is very clear that web support is missing features, has known performance issues, and is not quite ready for production yet.

Thank for reading!

#node.js #flutter #javascript #wevdev #api

How to Run a Flutter Web App with Node.js and API
Shayna  Lowe

Shayna Lowe

1656739380

Comment Créer Facilement Une API RESTful Avec Flask Et APIFairy

Ce tutoriel montre comment créer facilement une API RESTful avec Flask et APIFairy.

Objectifs

À la fin de ce didacticiel, vous serez en mesure de :

  1. Créez des points de terminaison d'API dans Flask à l'aide des décorateurs fournis par APIFairy
  2. Utiliser Flask-Marshmallow pour définir les schémas des entrées/sorties vers les points de terminaison de l'API
  3. Générer la documentation de l'API à l'aide d'APIFairy
  4. Intégrer une base de données relationnelle aux points de terminaison de l'API
  5. Implémenter l'authentification de base et par jeton à l'aide de Flask-HTTPAuth

Qu'est-ce qu'APIFairy ?

APIFairy est un framework API écrit par Miguel Grinberg qui permet de créer facilement une API avec Flask.

APIFairy fournit quatre composants clés pour créer facilement une API dans Flask :

  1. Décorateurs
  2. Schémas
  3. Authentification
  4. Documentation

Découvrons chacun en détail...

Décorateurs

APIFairy fournit un ensemble de décorateurs pour définir les entrées, les sorties et l'authentification pour chaque point de terminaison d'API :

APIFairy Decorators

APIFairy fournit cinq décorateurs de base :

  1. @arguments - spécifie les arguments d'entrée dans la chaîne de requête de l'URL
  2. @body - spécifie le corps JSON d'entrée en tant que schéma
  3. @response - spécifie le corps JSON de sortie en tant que schéma
  4. @other_responses - spécifie des réponses supplémentaires (souvent des erreurs) qui peuvent être renvoyées ( documentation uniquement )
  5. @authenticate - spécifie le processus d'authentification

Schémas

L'entrée (à l'aide du @bodydécorateur) et la sortie (à l'aide du @responsedécorateur) d'un point de terminaison d'API sont définies en tant que schémas :

class EntrySchema(ma.Schema):
    """Schema defining the attributes in a journal entry."""
    id = ma.Integer()
    entry = ma.String()
    user_id = ma.Integer()

Les schémas utilisent marshmallow pour définir les types de données en tant que classes.

Authentification

Le @authenticatedécorateur est utilisé pour vérifier l'en-tête d'authentification fourni dans la demande d'URL à chaque point de terminaison d'API. Le schéma d'authentification est implémenté à l'aide de Flask-HTTPAuth , qui a également été créé par Miguel Grinberg.

Une approche d'authentification d'API typique consisterait à définir une authentification de base pour protéger la route afin de récupérer un jeton d'authentification :

basic_auth = HTTPBasicAuth()

@basic_auth.verify_password
def verify_password(email, password):
    user = User.query.filter_by(email=email).first()
    if user.is_password_correct(password):
        return user

Et aussi pour définir l'authentification par jeton pour protéger la majorité des routes basées sur un jeton d'authentification sensible au temps :

token_auth = HTTPTokenAuth()

@token_auth.verify_token
def verify_token(auth_token):
    return User.verify_auth_token(auth_token)

Documentation

L'une des fonctionnalités intéressantes d'APIFairy est la magnifique documentation de l'API générée automatiquement :

Documentation API - Page principale

La documentation est générée sur la base de docstrings dans le code source avec les variables de configuration suivantes :

  1. APIFAIRY_TITLE- Nom du projet
  2. APIFAIRY_VERSION- chaîne de version du projet
  3. APIFAIRY_UI- format de la documentation de l'API

Pour APIFAIRY_UI, vous pouvez générer des modèles à partir de l'un des moteurs de rendu de documentation OpenAPI suivants :

  1. Interface utilisateur Swagger
  2. ReDoc
  3. RapiDoc
  4. Éléments

Pour une liste complète des variables de configuration disponibles, reportez-vous à la documentation de configuration .

Que construisons-nous ?

Vous développerez une API de journal dans ce didacticiel, permettant aux utilisateurs de tenir un journal quotidien des événements. Vous pouvez trouver le code source complet dans le référentiel flask-journal-api sur GitLab.

Principaux packages Python utilisés :

  1. Flask : micro-framework pour le développement d'applications web Python
  2. APIFairy : framework API pour Flask, qui utilise-
    • Flask-Marshmallow : extension Flask pour l'utilisation de marshmallow (bibliothèque de sérialisation/désérialisation d'objets)
    • Flask-HTTPAuth : extension Flask pour l'authentification HTTP
    • apispec - Générateur de spécifications d'API prenant en charge la spécification OpenAPI
  3. Flask-SQLAlchemy : ORM (Object Relational Mapper) pour Flask

Vous développerez l'API de manière incrémentielle :

  1. Créer les points de terminaison API pour travailler avec les entrées de journal
  2. Générer la documentation de l'API
  3. Ajouter une base de données relationnelle pour stocker les entrées de journal
  4. Ajouter une authentification pour protéger les points de terminaison de l'API

Points de terminaison de l'API

Passons à la création d'une API à l'aide de Flask et APIFairy...

Initialisation du projet

Commencez par créer un nouveau dossier de projet et un environnement virtuel :

$ mkdir flask-journal-api
$ cd flask-journal-api
$ python3 -m venv venv
$ source venv/bin/activate
(venv)$

N'hésitez pas à échanger virtualenv et Pip contre Poetry ou Pipenv . Pour en savoir plus, consultez Environnements Python modernes .

Allez-y et ajoutez les fichiers et dossiers suivants :

├── app.py
├── instance
│   └── .gitkeep
├── project
│   ├── __init__.py
│   └── journal_api
│       ├── __init__.py
│       └── routes.py
└── requirements.txt

Ensuite, pour installer les packages Python nécessaires, ajoutez les dépendances au fichier requirements.txt à la racine du projet :

apifairy==0.9.1
Flask==2.1.2
Flask-SQLAlchemy==2.5.1
marshmallow-sqlalchemy==0.28.0

Installer:

(venv)$ pip install -r requirements.txt

Ce projet Flask utilisera deux bonnes pratiques pour les applications Flask :

  1. Application Factory - utilisé pour créer l'application Flask dans une fonction
  2. Blueprints - utilisés pour organiser un groupe de vues connexes

Usine d'applications

Commencez par définir la fonction Application Factory dans project/__init__.py :

from apifairy import APIFairy
from flask import Flask, json
from flask_marshmallow import Marshmallow

# -------------
# Configuration
# -------------

# Create the instances of the Flask extensions in the global scope,
# but without any arguments passed in. These instances are not
# attached to the Flask application at this point.
apifairy = APIFairy()
ma = Marshmallow()


# ----------------------------
# Application Factory Function
# ----------------------------

def create_app():
    # Create the Flask application
    app = Flask(__name__)

    initialize_extensions(app)
    register_blueprints(app)
    return app


# ----------------
# Helper Functions
# ----------------

def initialize_extensions(app):
    # Since the application instance is now created, pass it to each Flask
    # extension instance to bind it to the Flask application instance (app)
    apifairy.init_app(app)
    ma.init_app(app)


def register_blueprints(app):
    # Import the blueprints
    from project.journal_api import journal_api_blueprint

    # Since the application instance is now created, register each Blueprint
    # with the Flask application instance (app)
    app.register_blueprint(journal_api_blueprint, url_prefix='/journal')

With the Application Factory function defined, it can be called in app.py in the top-level folder of the project:

from project import create_app


# Call the application factory function to construct a Flask application
# instance using the development configuration
app = create_app()

Blueprint

Let's define the journal_api blueprint. Start by defining the journal_api blueprint in project/journal_api/__init__.py:

"""
The 'journal_api' blueprint handles the API for managing journal entries.
Specifically, this blueprint allows for journal entries to be added, edited,
and deleted.
"""
from flask import Blueprint

journal_api_blueprint = Blueprint('journal_api', __name__, template_folder='templates')

from . import routes

Now it's time to define the API endpoints for the journal in project/journal_api/routes.py.

Start with the necessary imports:

from apifairy import body, other_responses, response
from flask import abort

from project import ma
from . import journal_api_blueprint

For this initial version of the Flask Journal API, the database will be a list of journal entries:

# --------
# Database
# --------

messages = [
    dict(id=1, entry='The sun was shining when I woke up this morning.'),
    dict(id=2, entry='I tried a new fruit mixture in my oatmeal for breakfast.'),
    dict(id=3, entry='Today I ate a great sandwich for lunch.')
]

Next, define the schemas for creating a new journal entry and for returning the journal entries:

# -------
# Schemas
# -------

class NewEntrySchema(ma.Schema):
    """Schema defining the attributes when creating a new journal entry."""
    entry = ma.String(required=True)


class EntrySchema(ma.Schema):
    """Schema defining the attributes in a journal entry."""
    id = ma.Integer()
    entry = ma.String()


new_entry_schema = NewEntrySchema()
entry_schema = EntrySchema()
entries_schema = EntrySchema(many=True)

Ces deux classes de schéma héritent de ma.Schema , qui est fourni par Flask-Marshmallow. C'est aussi une bonne idée de créer des objets de ces schémas, car cela vous permet de définir un schéma qui peut renvoyer plusieurs entrées (en utilisant l' many=Trueargument ).

Nous sommes maintenant prêts à définir les points de terminaison de l'API !

Itinéraires

Commencez par récupérer toutes les entrées de journal :

@journal_api_blueprint.route('/', methods=['GET'])
@response(entries_schema)
def journal():
    """Return all journal entries"""
    return messages

Cette fonction de vue utilise le @responsedécorateur pour définir que plusieurs entrées sont renvoyées. La fonction d'affichage renvoie la liste complète des entrées de journal ( return messages).

Ensuite, créez le point de terminaison de l'API pour ajouter une nouvelle entrée de journal :

@journal_api_blueprint.route('/', methods=['POST'])
@body(new_entry_schema)
@response(entry_schema, 201)
def add_journal_entry(kwargs):
    """Add a new journal entry"""
    new_message = dict(**kwargs, id=messages[-1]['id']+1)
    messages.append(new_message)
    return new_message

Cette fonction de vue utilise le @bodydécorateur pour définir l'entrée du point de terminaison de l'API et le @responsedécorateur pour définir la sortie du point de terminaison de l'API.

Les données d'entrée qui sont analysées à partir du @bodydécorateur sont transmises à la add_journal_entry()fonction d'affichage en tant qu'argument (kwargs arguments de mots clés ). Ces données sont ensuite utilisées pour créer une nouvelle écriture de journal et l'ajouter à la base de données :

new_message = dict(**kwargs, id=messages[-1]['id']+1)
messages.append(new_message)

L'écriture au journal nouvellement créée est alors retournée ( return new_message). Remarquez comment le @responsedécorateur définit le code de retour comme 201 (créé) pour indiquer que l'entrée de journal a été ajoutée à la base de données.

Créez le point de terminaison de l'API pour récupérer une entrée de journal spécifique :

@journal_api_blueprint.route('/<int:index>', methods=['GET'])
@response(entry_schema)
@other_responses({404: 'Entry not found'})
def get_journal_entry(index):
    """Return a journal entry"""
    if index >= len(messages):
        abort(404)

    return messages[index]

Cette fonction de vue utilise le @other_responsesdécorateur pour spécifier des réponses non standard.

Le @other_responsesdécorateur n'est utilisé qu'à des fins de documentation ! Il ne fournit aucune fonctionnalité en termes de retour de codes d'erreur.

Créez le point de terminaison de l'API pour mettre à jour une entrée de journal :

@journal_api_blueprint.route('/<int:index>', methods=['PUT'])
@body(new_entry_schema)
@response(entry_schema)
@other_responses({404: 'Entry not found'})
def update_journal_entry(data, index):
    """Update a journal entry"""
    if index >= len(messages):
        abort(404)

    messages[index] = dict(data, id=index+1)
    return messages[index]

Cette fonction d'affichage utilise les décorateurs @bodyet @responsepour définir les entrées et les sorties (respectivement) pour ce point de terminaison d'API. De plus, le @other_responsesdécorateur définit la réponse non standard si l'entrée de journal n'est pas trouvée.

Enfin, créez le point de terminaison API pour supprimer une entrée de journal :

@journal_api_blueprint.route('/<int:index>', methods=['DELETE'])
@other_responses({404: 'Entry not found'})
def delete_journal_entry(index):
    """Delete a journal entry"""
    if index >= len(messages):
        abort(404)

    messages.pop(index)
    return '', 204

Cette fonction d'affichage n'utilise pas les décorateurs @bodyet @response, car il n'y a pas d'entrées ou de sorties pour ce point de terminaison d'API. Si l'entrée de journal est supprimée avec succès, un code d'état 204 (Aucun contenu) est renvoyé sans données.

Exécution de l'application Flacon

Pour tester les choses, dans une fenêtre de terminal, configurez l'application Flask et exécutez le serveur de développement :

(venv) $ export FLASK_APP=app.py
(venv) $ export FLASK_ENV=development
(venv) $ flask run

Ensuite, dans une autre fenêtre de terminal, vous pouvez interagir avec l'API. N'hésitez pas à utiliser l'outil de votre choix ici, comme cURL, HTTPie , Requests ou Postman .

Exemple de demandes :

$ python3

>>> import requests
>>>
>>> r = requests.get('http://127.0.0.1:5000/journal/')
>>> print(r.text)
>>>
>>> post_data = {'entry': "some message"}
>>> r = requests.post('http://127.0.0.1:5000/journal/', json=post_data)
>>> print(r.text)

Vous souhaitez tester plus facilement les points de terminaison de l'API ? Consultez ce script , qui ajoute des commandes CLI pour interagir avec les points de terminaison de l'API pour récupérer, créer, mettre à jour et supprimer des entrées de journal.

Documentation

Une fonctionnalité incroyable d'APIFairy est la création automatique de la documentation de l'API !

Il existe trois aspects clés pour configurer la documentation de l'API :

  1. Docstrings pour les points de terminaison de l'API (c'est-à-dire les fonctions d'affichage)
  2. Docstring pour le projet API global
  3. Variables de configuration pour spécifier l'apparence de la documentation de l'API

Nous avons déjà couvert le premier élément dans la section précédente puisque nous avons inclus les docstrings pour chaque fonction de vue. Par exemple, la journal()fonction d'affichage contient une brève description de l'objectif de ce point de terminaison d'API :

@journal_api_blueprint.route('/', methods=['GET'])
@response(entries_schema)
def journal():
    """Return all journal entries"""
    return messages

Ensuite, nous devons inclure la docstring pour décrire le projet global tout en haut du fichier project/__init__.py :

"""
Welcome to the documentation for the Flask Journal API!

## Introduction

The Flask Journal API is an API (Application Programming Interface) for creating a **daily journal** that documents events that happen each day.

## Key Functionality

The Flask Journal API has the following functionality:

1. Work with journal entries:
  * Create a new journal entry
  * Update a journal entry
  * Delete a journal entry
  * View all journal entries
2. <More to come!>

## Key Modules

This project is written using Python 3.10.1.

The project utilizes the following modules:

* **Flask**: micro-framework for web application development which includes the following dependencies:
  * **click**: package for creating command-line interfaces (CLI)
  * **itsdangerous**: cryptographically sign data
  * **Jinja2**: templating engine
  * **MarkupSafe**: escapes characters so text is safe to use in HTML and XML
  * **Werkzeug**: set of utilities for creating a Python application that can talk to a WSGI server
* **APIFairy**: API framework for Flask which includes the following dependencies:
  * **Flask-Marshmallow** - Flask extension for using Marshmallow (object serialization/deserialization library)
  * **Flask-HTTPAuth** - Flask extension for HTTP authentication
  * **apispec** - API specification generator that supports the OpenAPI specification
* **pytest**: framework for testing Python projects
"""
...

Cette docstring est utilisée pour décrire l'ensemble du projet, y compris les fonctionnalités clés fournies et les principaux packages Python utilisés par le projet.

Enfin, certaines variables de configuration doivent être définies pour spécifier l'apparence de la documentation de l'API. Mettez à jour la create_app()fonction dans project/__init__.py :

def create_app():
    # Create the Flask application
    app = Flask(__name__)

    # Configure the API documentation
    app.config['APIFAIRY_TITLE'] = 'Flask Journal API'
    app.config['APIFAIRY_VERSION'] = '0.1'
    app.config['APIFAIRY_UI'] = 'elements'

    initialize_extensions(app)
    register_blueprints(app)

    return app

Prêt à voir la documentation du projet ? Démarrez le serveur de développement Flask via flask run, puis accédez à http://127.0.0.1:5000/docs pour consulter la documentation de l'API créée par APIFairy :

Documentation API - Page principale

Dans le volet de gauche, il y a une liste de points de terminaison d'API pour le journal_apiblueprint. Cliquer sur l'un des points de terminaison affiche tous les détails sur ce point de terminaison :

Documentation de l'API – Obtenir le point de terminaison de l'API d'entrée de journal

Ce qui est étonnant dans cette documentation de l'API, c'est la possibilité de voir comment fonctionnent les points de terminaison de l'API (en supposant que le serveur de développement Flask est en cours d'exécution). Dans le volet droit de la documentation, entrez un index d'écriture au journal et cliquez sur "Envoyer une requête API". La réponse de l'API s'affiche alors :

Documentation de l'API - Obtenir la réponse de l'API d'entrée de journal

Cette documentation interactive permet aux utilisateurs de comprendre facilement l'API !

Base de données

À des fins de démonstration, une base de données SQLite sera utilisée dans ce didacticiel.

Configuration

Puisque Flask-SQLAlchemy était déjà installé au début de ce tutoriel, nous devons le configurer dans le fichier project/__init__.py .

Commencez par créer un SQLAlchemy()objet dans la section 'Configuration' :

...

from apifairy import APIFairy
from flask import Flask, json
from flask_marshmallow import Marshmallow
from flask_sqlalchemy import SQLAlchemy  # <-- NEW!!


# -------------
# Configuration
# -------------

# Create the instances of the Flask extensions in the global scope,
# but without any arguments passed in. These instances are not
# attached to the Flask application at this point.
apifairy = APIFairy()
ma = Marshmallow()
database = SQLAlchemy()  # <-- NEW!!

...

Ensuite, mettez à jour la create_app()fonction pour spécifier les variables de configuration nécessaires :

def create_app():
    # Create the Flask application
    app = Flask(__name__)

    # Configure the API documentation
    app.config['APIFAIRY_TITLE'] = 'Flask Journal API'
    app.config['APIFAIRY_VERSION'] = '0.1'
    app.config['APIFAIRY_UI'] = 'elements'

    # NEW!
    # Configure the SQLite database (intended for development only!)
    app.config['SQLALCHEMY_DATABASE_URI'] = f"sqlite:///{os.path.join(os.getcwd(), 'instance', 'app.db')}"
    app.config['SQLALCHEMY_TRACK_MODIFICATIONS'] = False

    initialize_extensions(app)
    register_blueprints(app)

    return app

Ajoutez l'importation en haut :

import os

La SQLALCHEMY_DATABASE_URIvariable de configuration est essentielle pour identifier l'emplacement de la base de données SQLite. Pour ce didacticiel, la base de données est stockée dans instance/app.db .

Enfin, mettez à jour la initialize_extensions()fonction pour initialiser l'objet Flask-SQLAlchemy :

def initialize_extensions(app):
    # Since the application instance is now created, pass it to each Flask
    # extension instance to bind it to the Flask application instance (app)
    apifairy.init_app(app)
    ma.init_app(app)
    database.init_app(app)  # <-- NEW!!

Vous voulez en savoir plus sur la façon dont cette application Flask est câblée ? Consultez mon cours sur la création, le test et le déploiement d'une application Flask :

Modèle de base de données

Créez un nouveau fichier project/models.py pour définir la table de base de données pour représenter les entrées de journal :

from project import database


class Entry(database.Model):
    """Class that represents a journal entry."""
    __tablename__ = 'entries'

    id = database.Column(database.Integer, primary_key=True)
    entry = database.Column(database.String, nullable=False)

    def __init__(self, entry: str):
        self.entry = entry

    def update(self, entry: str):
        self.entry = entry

    def __repr__(self):
        return f'<Entry: {self.entry}>'

Cette nouvelle classe, Entry, spécifie que la entriestable de la base de données contiendra deux éléments (pour l'instant !) pour représenter une entrée de journal :

  1. id- la clé primaire ( primary_key=True) de la table, ce qui signifie qu'il s'agit d'un identifiant unique pour chaque élément (ligne) de la table
  2. entry- chaîne pour stocker le texte de l'écriture au journal

Alors que models.py définit la table de la base de données, il ne crée pas les tables dans la base de données SQLite. Pour créer les tables, démarrez le shell Flask dans une fenêtre de terminal :

(venv)$ flask shell

>>> from project import database
>>> database.drop_all()
>>> database.create_all()
>>> quit()

(venv)$

Mises à jour de l'API des journaux

Puisque nous progressons vers l'utilisation d'une base de données SQLite, commencez par supprimer le temporaire database(liste Python) qui a été défini dans project/journal_api/routes.py :

# --------
# Database
# --------

messages = [
    dict(id=1, entry='The sun was shining when I woke up this morning.'),
    dict(id=2, entry='I tried a new fruit mixture in my oatmeal for breakfast.'),
    dict(id=3, entry='Today I ate a great sandwich for lunch.')
]

Ensuite, nous devons mettre à jour chaque point de terminaison de l'API (c'est-à-dire les fonctions d'affichage) pour utiliser la base de données SQLite.

Commencez par mettre à jour la journal()fonction d'affichage :

@journal_api_blueprint.route('/', methods=['GET'])
@response(entries_schema)
def journal():
    """Return all journal entries"""
    return Entry.query.all()

La liste complète des entrées de journal est désormais extraite de la base de données SQLite. Remarquez comment les schémas ou les décorateurs de cette fonction de vue n'ont pas eu besoin de changer... seulement le processus sous-jacent pour faire changer les utilisateurs !

Ajoutez l'importation :

from project.models import Entry

Ensuite, mettez à jour la add_journal_entry()fonction d'affichage :

@journal_api_blueprint.route('/', methods=['POST'])
@body(new_entry_schema)
@response(entry_schema, 201)
def add_journal_entry(kwargs):
    """Add a new journal entry"""
    new_message = Entry(**kwargs)
    database.session.add(new_message)
    database.session.commit()
    return new_message

Les entrées de cette fonction de vue sont spécifiées parnew_entry_schema :

class NewEntrySchema(ma.Schema):
    """Schema defining the attributes when creating a new journal entry."""
    entry = ma.String(required=True)

new_entry_schema = NewEntrySchema()

La entrychaîne est utilisée pour créer une nouvelle instance de la Entryclasse (définie dans models.py ) et cette entrée de journal est ensuite ajoutée à la base de données.

Ajoutez l'importation :

from project import database

Ensuite, mettez à jourget_journal_entry() :

@journal_api_blueprint.route('/<int:index>', methods=['GET'])
@response(entry_schema)
@other_responses({404: 'Entry not found'})
def get_journal_entry(index):
    """Return a journal entry"""
    entry = Entry.query.filter_by(id=index).first_or_404()
    return entry

Cette fonction tente maintenant de rechercher l'écriture de journal spécifiée (basée sur le index) :

entry = Entry.query.filter_by(id=index).first_or_404()

Si l'entrée existe, elle est renvoyée à l'utilisateur. Si l'entrée n'existe pas, une erreur 404 (Introuvable) est renvoyée.

Ensuite, mettez à jourupdate_journal_entry() :

@journal_api_blueprint.route('/<int:index>', methods=['PUT'])
@body(new_entry_schema)
@response(entry_schema)
@other_responses({404: 'Entry not found'})
def update_journal_entry(data, index):
    """Update a journal entry"""
    entry = Entry.query.filter_by(id=index).first_or_404()
    entry.update(data['entry'])
    database.session.add(entry)
    database.session.commit()
    return entry

La update_journal_entry()fonction d'affichage tente maintenant de récupérer l'écriture de journal spécifiée :

entry = Entry.query.filter_by(id=index).first_or_404()

Si l'écriture au journal existe, l'écriture est mise à jour avec le nouveau texte, puis enregistrée dans la base de données.

Enfin, mettez à jour delete_journal_entry():

@journal_api_blueprint.route('/<int:index>', methods=['DELETE'])
@other_responses({404: 'Entry not found'})
def delete_journal_entry(index):
    """Delete a journal entry"""
    entry = Entry.query.filter_by(id=index).first_or_404()
    database.session.delete(entry)
    database.session.commit()
    return '', 204

If the specified journal entry is found, then it's deleted from the database.

Run the development server. Test out each of the endpoints to ensure they still work.

Error Handling

Since this Flask project is an API, error codes should be returned in JSON format instead of the typical HTML format.

In the Flask project, this can be accomplished by using a custom error handler. In project/__init__.py, define a new function (register_error_handlers()) at the bottom of the file:

def register_error_handlers(app):
    @app.errorhandler(HTTPException)
    def handle_http_exception(e):
        """Return JSON instead of HTML for HTTP errors."""
        # Start with the correct headers and status code from the error
        response = e.get_response()
        # Replace the body with JSON
        response.data = json.dumps({
            'code': e.code,
            'name': e.name,
            'description': e.description,
        })
        response.content_type = 'application/json'
        return response

This function registers a new error handler for when an HTTPException is raised to convert the output into JSON format.

Add the import:

from werkzeug.exceptions import HTTPException

Also, update the Application Factory function, create_app(), to call this new function:

def create_app():
    # Create the Flask application
    app = Flask(__name__)

    # Configure the API documentation
    app.config['APIFAIRY_TITLE'] = 'Flask Journal API'
    app.config['APIFAIRY_VERSION'] = '0.1'
    app.config['APIFAIRY_UI'] = 'elements'

    # Configure the SQLite database (intended for development only!)
    app.config['SQLALCHEMY_DATABASE_URI'] = f"sqlite:///{os.path.join(os.getcwd(), 'instance', 'app.db')}"
    app.config['SQLALCHEMY_TRACK_MODIFICATIONS'] = False

    initialize_extensions(app)
    register_blueprints(app)
    register_error_handlers(app)  # NEW!!

    return app

Authentication

Authentication is the process of validating the identity of a user attempting to access a system, which in this case is the API.

Authorization, on the other hand, is the process of verifying what specific resources a specific user should have access to.

APIFairy utilizes Flask-HTTPAuth for authentication support. In this tutorial, we'll be using Flask-HTTPAuth in two manners:

  1. Basic Authentication - used to generate a token based on the user's email/password
  2. Token Authentication - used to authenticate the user on all other API endpoints

L' authentification par jeton utilisée via Flask-HTTPAuth est souvent appelée authentification du porteur, car le processus invoque l'octroi de l'accès au "porteur" du jeton. Le jeton doit être inclus dans les en-têtes HTTP de l'en-tête Authorization, par exemple "Authorization : Bearer".

Le diagramme suivant illustre un flux typique de la façon dont un nouvel utilisateur interagit avec l'application pour récupérer un jeton d'authentification :

Diagramme de flux de l'API Flask Journal

Configuration

Comme Flask-HTTPAuth était déjà installé lors de l'installation d'APIFairy au début de ce tutoriel, il nous suffit de le configurer dans le fichier project/__init__.py .

Commencez par créer des objets distincts pour l'authentification de base et par jeton :

...

import os

from apifairy import APIFairy
from flask import Flask, json
from flask_httpauth import HTTPBasicAuth, HTTPTokenAuth  # NEW!!
from flask_marshmallow import Marshmallow
from flask_sqlalchemy import SQLAlchemy
from werkzeug.exceptions import HTTPException


# -------------
# Configuration
# -------------

# Create the instances of the Flask extensions in the global scope,
# but without any arguments passed in. These instances are not
# attached to the Flask application at this point.
apifairy = APIFairy()
ma = Marshmallow()
database = SQLAlchemy()
basic_auth = HTTPBasicAuth()  # NEW!!
token_auth = HTTPTokenAuth()  # NEW!!

...

Aucune autre mise à jour n'est nécessaire dans project/__init__.py .

Modèle de base de données

Dans project/models.py , un nouveau Usermodèle doit être créé pour représenter un utilisateur :

class User(database.Model):
    __tablename__ = 'users'

    id = database.Column(database.Integer, primary_key=True)
    email = database.Column(database.String, unique=True, nullable=False)
    password_hashed = database.Column(database.String(128), nullable=False)
    entries = database.relationship('Entry', backref='user', lazy='dynamic')
    auth_token = database.Column(database.String(64), index=True)
    auth_token_expiration = database.Column(database.DateTime)

    def __init__(self, email: str, password_plaintext: str):
        """Create a new User object."""
        self.email = email
        self.password_hashed = self._generate_password_hash(password_plaintext)

    def is_password_correct(self, password_plaintext: str):
        return check_password_hash(self.password_hashed, password_plaintext)

    def set_password(self, password_plaintext: str):
        self.password_hashed = self._generate_password_hash(password_plaintext)

    @staticmethod
    def _generate_password_hash(password_plaintext):
        return generate_password_hash(password_plaintext)

    def generate_auth_token(self):
        self.auth_token = secrets.token_urlsafe()
        self.auth_token_expiration = datetime.utcnow() + timedelta(minutes=60)
        return self.auth_token

    @staticmethod
    def verify_auth_token(auth_token):
        user = User.query.filter_by(auth_token=auth_token).first()
        if user and user.auth_token_expiration > datetime.utcnow():
            return user

    def revoke_auth_token(self):
        self.auth_token_expiration = datetime.utcnow()

    def __repr__(self):
        return f'<User: {self.email}>'

Ajoutez les importations :

import secrets
from datetime import datetime, timedelta

from werkzeug.security import check_password_hash, generate_password_hash

Le Usermodèle utilise werkzeug.securitypour hacher le mot de passe de l'utilisateur avant de le stocker dans la base de données.

N'oubliez pas : ne stockez jamais le mot de passe en clair dans une base de données !

Le Usermodèle utilise secretspour générer un jeton d'authentification pour un utilisateur spécifique. Ce jeton est créé dans la generate_auth_token()méthode et inclut une date/heure d'expiration de 60 minutes dans le futur :

def generate_auth_token(self):
    self.auth_token = secrets.token_urlsafe()
    self.auth_token_expiration = datetime.utcnow() + timedelta(minutes=60)
    return self.auth_token

Il existe une méthode statique verify_auth_token(), qui est utilisée pour vérifier le jeton d'authentification (tout en tenant compte du délai d'expiration) et renvoyer l'utilisateur à partir d'un jeton valide :

@staticmethod
def verify_auth_token(auth_token):
    user = User.query.filter_by(auth_token=auth_token).first()
    if user and user.auth_token_expiration > datetime.utcnow():
        return user

Une autre méthode intéressante est revoke_auth_token(), qui est utilisée pour révoquer le jeton d'authentification pour un utilisateur spécifique :

def revoke_auth_token(self):
    self.auth_token_expiration = datetime.utcnow()

Modèle d'entrée

Pour établir la relation un-à-plusieurs entre l'utilisateur (« un ») et ses entrées (« plusieurs »), le Entrymodèle doit être mis à jour pour lier les tables et entries:users

class Entry(database.Model):
    """Class that represents a journal entry."""
    __tablename__ = 'entries'

    id = database.Column(database.Integer, primary_key=True)
    entry = database.Column(database.String, nullable=False)
    user_id = database.Column(database.Integer, database.ForeignKey('users.id'))  # <-- NEW!!

    def __init__(self, entry: str):
        self.entry = entry

    def update(self, entry: str):
        self.entry = entry

    def __repr__(self):
        return f'<Entry: {self.entry}>'

Le Usermodèle contient déjà le lien vers la entriestable :

entries = database.relationship('Entry', backref='user', lazy='dynamic')

Plan directeur de l'API des utilisateurs

La fonctionnalité de gestion des utilisateurs du projet Flask sera définie dans un Blueprint séparé appelé users_api_blueprint.

Commencez par créer un nouveau répertoire dans "project" appelé "users_api". Dans ce répertoire, créez un fichier __init__.py :

from flask import Blueprint


users_api_blueprint = Blueprint('users_api', __name__)

from . import authentication, routes

Ce nouveau Blueprint doit être enregistré avec le Flask appdans projects/__init__.py dans la register_blueprints()fonction :

def register_blueprints(app):
    # Import the blueprints
    from project.journal_api import journal_api_blueprint
    from project.users_api import users_api_blueprint  # NEW!!

    # Since the application instance is now created, register each Blueprint
    # with the Flask application instance (app)
    app.register_blueprint(journal_api_blueprint, url_prefix='/journal')
    app.register_blueprint(users_api_blueprint, url_prefix='/users')  # NEW!!

Fonctions d'authentification

Pour utiliser Flask-HTTPAuth, plusieurs fonctions doivent être définies pour gérer la vérification des informations d'identification de l'utilisateur.

Créez un nouveau fichier project/users_api/authentication.py pour gérer l'authentification de base et l'authentification par jeton.

Pour l'authentification de base (vérification de l'e-mail et du mot de passe d'un utilisateur) :

from werkzeug.exceptions import Forbidden, Unauthorized

from project import basic_auth, token_auth
from project.models import User


@basic_auth.verify_password
def verify_password(email, password):
    user = User.query.filter_by(email=email).first()
    if user is None:
        return None

    if user.is_password_correct(password):
        return user


@basic_auth.error_handler
def basic_auth_error(status=401):
    error = (Forbidden if status == 403 else Unauthorized)()
    return {
        'code': error.code,
        'message': error.name,
        'description': error.description,
    }, error.code, {'WWW-Authenticate': 'Form'}

La verify_password()fonction permet de vérifier qu'un utilisateur existe et que son mot de passe est correct. Cette fonction sera utilisée par Flask-HTTPAuth pour vérifier le mot de passe lorsqu'une authentification de base est nécessaire (merci au @basic_auth.verify_passworddécorateur.)

De plus, un gestionnaire d'erreurs est défini pour l'authentification de base qui renvoie des informations sur l'erreur au format JSON.

Pour l'authentification par jeton (traitement d'un jeton pour déterminer si l'utilisateur est valide) :

@token_auth.verify_token
def verify_token(auth_token):
    return User.verify_auth_token(auth_token)


@token_auth.error_handler
def token_auth_error(status=401):
    error = (Forbidden if status == 403 else Unauthorized)()
    return {
        'code': error.code,
        'message': error.name,
        'description': error.description,
    }, error.code

La verify_token()fonction est utilisée pour vérifier si un jeton d'authentification est valide. Cette fonction sera utilisée par Flask-HTTPAuth pour vérifier le jeton lorsque l'authentification par jeton est nécessaire (merci au @token_auth.verify_tokendécorateur.)

De plus, un gestionnaire d'erreurs est défini pour l'authentification par jeton qui renvoie des informations sur l'erreur au format JSON.

Itinéraires des utilisateurs

Dans le users_api_blueprint, il y aura deux parcours :

  1. Enregistrement d'un nouvel utilisateur
  2. Récupérer un jeton d'authentification

Pour commencer, un nouvel ensemble de schémas (utilisant marshmallow) doit être défini dans projects/users_api/routes.py :

from project import ma

from . import users_api_blueprint

# -------
# Schemas
# -------

class NewUserSchema(ma.Schema):
    """Schema defining the attributes when creating a new user."""
    email = ma.String()
    password_plaintext = ma.String()


class UserSchema(ma.Schema):
    """Schema defining the attributes of a user."""
    id = ma.Integer()
    email = ma.String()


class TokenSchema(ma.Schema):
    """Schema defining the attributes of a token."""
    token = ma.String()


new_user_schema = NewUserSchema()
user_schema = UserSchema()
token_schema = TokenSchema()

Ces schémas seront utilisés pour définir les entrées et les sorties des fonctions de vue définies dans ce fichier.

Enregistrement d'un nouvel utilisateur

Définissez ensuite la fonction d'affichage pour enregistrer un nouvel utilisateur :

@users_api_blueprint.route('/', methods=['POST'])
@body(new_user_schema)
@response(user_schema, 201)
def register(kwargs):
    """Create a new user"""
    new_user = User(**kwargs)
    database.session.add(new_user)
    database.session.commit()
    return new_user

Ajoutez les importations :

from apifairy import authenticate, body, other_responses, response

from project import basic_auth, database, ma
from project.models import User

Ce point de terminaison d'API utilise le new_user_schemapour spécifier que l'e-mail et le mot de passe sont les entrées.

REMARQUE : étant donné que l'e-mail et le mot de passe sont envoyés à ce point de terminaison d'API, il est temps de se rappeler que l'utilisation de HTTP est acceptable lors des tests de développement, mais HTTPS (sécurisé) doit toujours être utilisé en production.

L'e-mail et le mot de passe (définis comme kwargsarguments du mot-clé -) sont ensuite décompressés pour créer un nouvel Userobjet, qui est enregistré dans la base de données :

new_user = User(**kwargs)
database.session.add(new_user)
database.session.commit()

La sortie du point de terminaison de l'API est définie par user_schema, qui correspond à l'ID et à l'adresse e-mail du nouvel utilisateur.

Récupérer un jeton d'authentification

L'autre fonction de vue à définir dans projects/users_api/routes.py permet de récupérer le jeton d'authentification :

@users_api_blueprint.route('/get-auth-token', methods=['POST'])
@authenticate(basic_auth)
@response(token_schema)
@other_responses({401: 'Invalid username or password'})
def get_auth_token():
    """Get authentication token"""
    user = basic_auth.current_user()
    token = user.generate_auth_token()
    database.session.add(user)
    database.session.commit()
    return dict(token=token)

Le @authenticatedécorateur est utilisé pour la première fois dans ce tutoriel et il précise que l'authentification de base doit être utilisée pour protéger cette route :

@authenticate(basic_auth)

Lorsque l'utilisateur souhaite récupérer son jeton d'authentification, il doit envoyer une requête POST à ​​ce point de terminaison d'API avec l'e-mail et le mot de passe intégrés dans l'en-tête "Autorisation". Par exemple, la commande Python suivante utilisant le package Requests peut être envoyée à ce point de terminaison d'API :

>>> import requests
>>> r = requests.post(
    'http://127.0.0.1:5000/users/get-auth-token',
    auth=('pkennedy@hey.com', 'FlaskIsAwesome123')
)

Si l'authentification de base réussit, la fonction de visualisation récupère l'utilisateur actuel en utilisant la current_user()méthode fournie par Flask-HTTPAuth :

user = basic_auth.current_user()

Un nouveau jeton d'authentification est créé pour cet utilisateur :

token = user.generate_auth_token()

Et ce jeton est enregistré dans la base de données afin qu'il puisse être utilisé pour authentifier l'utilisateur à l'avenir (au moins pendant les 60 prochaines minutes !).

Enfin, le nouveau jeton d'authentification est renvoyé pour que l'utilisateur l'enregistre pour tous les appels d'API ultérieurs.

Mises à jour des points de terminaison de l'API

Avec un processus d'authentification en place, il est temps d'ajouter des gardes aux points de terminaison API existants pour s'assurer que seuls les utilisateurs valides peuvent accéder à l'application.

Ces mises à jour concernent les fonctions d'affichage définies dans projects/journal_api/routes.py .

Tout d'abord, mettez à jour journal()pour ne renvoyer que les entrées de journal de l'utilisateur actuel :

@journal_api_blueprint.route('/', methods=['GET'])
@authenticate(token_auth)
@response(entries_schema)
def journal():
    """Return journal entries"""
    user = token_auth.current_user()
    return Entry.query.filter_by(user_id=user.id).all()

Mettez à jour les importations en haut comme ceci :

from apifairy import authenticate, body, other_responses, response
from flask import abort

from project import database, ma, token_auth
from project.models import Entry

from . import journal_api_blueprint

Le @authenticatedécorateur spécifie que l'authentification par jeton doit être utilisée lors de l'accès à ce point de terminaison d'API. Par exemple, la requête GET suivante peut être effectuée à l'aide de Requests ( après la récupération du jeton d'authentification ) :

>>> import requests
>>> headers = {'Authorization': f'Bearer {auth_token}'}
>>> r = requests.get('http://127.0.0.1:5000/journal/', headers=headers)

Une fois l'utilisateur authentifié, la liste complète des entrées de journal est extraite de la base de données en fonction de l'ID de l'utilisateur :

user = token_auth.current_user()
return Entry.query.filter_by(user_id=user.id).all()

La sortie de ce point de terminaison d'API est définie par le @responsedécorateur, qui est une liste d'entrées de journal (ID, entrée, ID utilisateur).

Ensuite, mettez à jouradd_journal_entry() :

@journal_api_blueprint.route('/', methods=['POST'])
@authenticate(token_auth)
@body(new_entry_schema)
@response(entry_schema, 201)
def add_journal_entry(kwargs):
    """Add a new journal entry"""
    user = token_auth.current_user()
    new_message = Entry(user_id=user.id, **kwargs)
    database.session.add(new_message)
    database.session.commit()
    return new_message

Comme avec la fonction d'affichage précédente, le @authenticatedécorateur est utilisé pour spécifier que l'authentification par jeton doit être utilisée lors de l'accès à ce point de terminaison d'API. De plus, l'écriture au journal est maintenant ajoutée en spécifiant l'ID utilisateur qui doit être associé à l'écriture au journal :

user = token_auth.current_user()
new_message = Entry(user_id=user.id, **kwargs)

La nouvelle entrée de journal est enregistrée dans la base de données et l'entrée de journal est renvoyée (telle que définie par le @responsedécorateur).

Ensuite, mettez à jourget_journal_entry() :

@journal_api_blueprint.route('/<int:index>', methods=['GET'])
@authenticate(token_auth)
@response(entry_schema)
@other_responses({403: 'Forbidden', 404: 'Entry not found'})
def get_journal_entry(index):
    """Return a journal entry"""
    user = token_auth.current_user()
    entry = Entry.query.filter_by(id=index).first_or_404()

    if entry.user_id != user.id:
        abort(403)
    return entry

Le @authenticatedécorateur est ajouté pour spécifier que l'authentification par jeton est nécessaire pour accéder à ce point de terminaison d'API.

Lors d'une tentative de récupération d'une entrée de journal, une vérification supplémentaire est ajoutée pour s'assurer que l'utilisateur tentant d'accéder à l'entrée de journal est le véritable "propriétaire" de l'entrée. Si ce n'est pas le cas, un code d'erreur 403 (interdit) est renvoyé via la abort()fonction de Flask :

if entry.user_id != user.id:
        abort(403)

Notez que ce point de terminaison d'API a deux réponses non nominales spécifiées par le @other_responsesdécorateur :

@other_responses({403: 'Forbidden', 404: 'Entry not found'})

Rappel : Le @other_responsesdécorateur sert uniquement à la documentation ; c'est la responsabilité de la fonction view de remonter ces erreurs.

Ensuite, mettez à jourupdate_journal_entry() :

@journal_api_blueprint.route('/<int:index>', methods=['PUT'])
@authenticate(token_auth)
@body(new_entry_schema)
@response(entry_schema)
@other_responses({403: 'Forbidden', 404: 'Entry not found'})
def update_journal_entry(data, index):
    """Update a journal entry"""
    user = token_auth.current_user()
    entry = Entry.query.filter_by(id=index).first_or_404()

    if entry.user_id != user.id:
        abort(403)

    entry.update(data['entry'])
    database.session.add(entry)
    database.session.commit()
    return entry

Les mises à jour de cette fonction d'affichage sont similaires aux autres fonctions d'affichage de cette section :

  1. @authenticatele décorateur spécifie que l'authentification par jeton est nécessaire pour accéder à ce point de terminaison d'API
  2. Seul l'utilisateur qui "possède" l'écriture de journal est autorisé à mettre à jour l'écriture (sinon, 403 (Interdit))

Enfin, mettez à jour delete_journal_entry():

@journal_api_blueprint.route('/<int:index>', methods=['DELETE'])
@authenticate(token_auth)
@other_responses({403: 'Forbidden', 404: 'Entry not found'})
def delete_journal_entry(index):
    """Delete a journal entry"""
    user = token_auth.current_user()
    entry = Entry.query.filter_by(id=index).first_or_404()

    if entry.user_id != user.id:
        abort(403)

    database.session.delete(entry)
    database.session.commit()
    return '', 204

Conclusion

Ce didacticiel a expliqué comment créer facilement et rapidement une API dans Flask à l'aide d'APIFairy.

Les décorateurs sont la clé pour définir les points de terminaison de l'API :

  • Entrées :
    • @arguments- arguments d'entrée de la chaîne de requête de l'URL
    • @body- structure de la requête JSON
  • Sorties :
    • @response- structure de la réponse JSON
  • Authentification :
    • @authenticate- approche d'authentification à l'aide de Flask-HTTPAuth
  • Erreurs :
    • @other_responses- réponses non nominales, telles que les codes d'erreur HTTP

De plus, la documentation de l'API générée par APIFairy est excellente et fournit des informations clés aux utilisateurs de l'application.

Source:  https://testdrive.io

#api #flask 

Comment Créer Facilement Une API RESTful Avec Flask Et APIFairy

Cómo Crear Fácilmente Una API RESTful Con Flask Y APIFairy

Este tutorial demuestra cómo crear fácilmente una API RESTful con Flask y APIFairy.

Objetivos

Al final de este tutorial, podrá:

  1. Cree puntos finales de API en Flask utilizando los decoradores proporcionados por APIFairy
  2. Utilice Flask-Marshmallow para definir los esquemas de entradas/salidas a los puntos finales de la API
  3. Genere la documentación de la API usando APIFairy
  4. Integre una base de datos relacional con los puntos finales de la API
  5. Implemente la autenticación básica y de token mediante Flask-HTTPAuth

¿Qué es APIFairy?

APIFairy es un marco de API escrito por Miguel Grinberg que permite crear fácilmente una API con Flask.

APIFairy proporciona cuatro componentes clave para crear fácilmente una API en Flask:

  1. Decoradores
  2. esquemas
  3. Autenticación
  4. Documentación

Exploremos cada uno en detalle...

Decoradores

APIFairy proporciona un conjunto de decoradores para definir las entradas, las salidas y la autenticación para cada extremo de la API:

Decoradores de hadas API

APIFairy proporciona cinco decoradores principales:

  1. @argumentos : especifica los argumentos de entrada en la cadena de consulta de la URL
  2. @body : especifica el cuerpo JSON de entrada como un esquema
  3. @response : especifica el cuerpo JSON de salida como un esquema
  4. @other_responses : especifica respuestas adicionales (a menudo errores) que se pueden devolver ( solo documentación )
  5. @authenticate - especifica el proceso de autenticación

esquemas

La entrada (usando el @bodydecorador) y la salida (usando el @responsedecorador) de un punto final de API se definen como esquemas:

class EntrySchema(ma.Schema):
    """Schema defining the attributes in a journal entry."""
    id = ma.Integer()
    entry = ma.String()
    user_id = ma.Integer()

Los esquemas utilizan malvavisco para definir los tipos de datos como clases.

Autenticación

El @authenticatedecorador se utiliza para verificar el encabezado de autenticación proporcionado en la solicitud de URL para cada punto final de la API. El esquema de autenticación se implementa mediante Flask-HTTPAuth , que también fue creado por Miguel Grinberg.

Un enfoque típico de autenticación de API sería definir la autenticación básica para proteger la ruta para recuperar un token de autenticación:

basic_auth = HTTPBasicAuth()

@basic_auth.verify_password
def verify_password(email, password):
    user = User.query.filter_by(email=email).first()
    if user.is_password_correct(password):
        return user

Y también para definir la autenticación de token para proteger la mayoría de las rutas basadas en un token de autenticación sensible al tiempo:

token_auth = HTTPTokenAuth()

@token_auth.verify_token
def verify_token(auth_token):
    return User.verify_auth_token(auth_token)

Documentación

Una de las excelentes características de APIFairy es la hermosa documentación de la API que se genera automáticamente:

Documentación API - Página principal

La documentación se genera en base a cadenas de documentación en el código fuente junto con las siguientes variables de configuración:

  1. APIFAIRY_TITLE- nombre del proyecto
  2. APIFAIRY_VERSION- cadena de versión del proyecto
  3. APIFAIRY_UI- formato de la documentación de la API

Para APIFAIRY_UI, puede generar plantillas a partir de uno de los siguientes procesadores de documentación de OpenAPI:

  1. Interfaz de usuario de Swagger
  2. ReDoc
  3. RapiDoc
  4. Elementos

Para obtener una lista completa de las variables de configuración disponibles, consulte los documentos de configuración .

¿Qué estamos construyendo?

Desarrollará una API de diario en este tutorial, lo que permitirá a los usuarios llevar un diario de eventos. Puede encontrar el código fuente completo en el repositorio de Flass -journal-api en GitLab.

Paquetes clave de Python utilizados:

  1. Flask : micro-marco para el desarrollo de aplicaciones web Python
  2. APIFairy : marco API para Flask, que utiliza-
  3. Flask-SQLAlchemy : ORM (asignador relacional de objetos) para Flask

Desarrollará la API de forma incremental:

  1. Cree los puntos finales de API para trabajar con entradas de diario
  2. Generar documentación de la API
  3. Agregar una base de datos relacional para almacenar las entradas del diario
  4. Agregue autenticación para proteger los puntos finales de la API

Puntos finales de la API

Pasemos a crear una API usando Flask y APIFairy...

Inicialización del proyecto

Comience creando una nueva carpeta de proyecto y un entorno virtual:

$ mkdir flask-journal-api
$ cd flask-journal-api
$ python3 -m venv venv
$ source venv/bin/activate
(venv)$

Siéntete libre de cambiar virtualenv y Pip por Poetry o Pipenv . Para obtener más información, revise Entornos modernos de Python .

Continúe y agregue los siguientes archivos y carpetas:

├── app.py
├── instance
│   └── .gitkeep
├── project
│   ├── __init__.py
│   └── journal_api
│       ├── __init__.py
│       └── routes.py
└── requirements.txt

A continuación, para instalar los paquetes de Python necesarios, agregue las dependencias al archivo requirements.txt en la raíz del proyecto:

apifairy==0.9.1
Flask==2.1.2
Flask-SQLAlchemy==2.5.1
marshmallow-sqlalchemy==0.28.0

Instalar:

(venv)$ pip install -r requirements.txt

Este proyecto de Flask utilizará dos prácticas recomendadas para las aplicaciones de Flask:

  1. Application Factory : se utiliza para crear la aplicación Flask en una función
  2. Blueprints : se utiliza para organizar un grupo de vistas relacionadas

Fábrica de aplicaciones

Comience definiendo la función de Application Factory en project/__init__.py :

from apifairy import APIFairy
from flask import Flask, json
from flask_marshmallow import Marshmallow

# -------------
# Configuration
# -------------

# Create the instances of the Flask extensions in the global scope,
# but without any arguments passed in. These instances are not
# attached to the Flask application at this point.
apifairy = APIFairy()
ma = Marshmallow()


# ----------------------------
# Application Factory Function
# ----------------------------

def create_app():
    # Create the Flask application
    app = Flask(__name__)

    initialize_extensions(app)
    register_blueprints(app)
    return app


# ----------------
# Helper Functions
# ----------------

def initialize_extensions(app):
    # Since the application instance is now created, pass it to each Flask
    # extension instance to bind it to the Flask application instance (app)
    apifairy.init_app(app)
    ma.init_app(app)


def register_blueprints(app):
    # Import the blueprints
    from project.journal_api import journal_api_blueprint

    # Since the application instance is now created, register each Blueprint
    # with the Flask application instance (app)
    app.register_blueprint(journal_api_blueprint, url_prefix='/journal')

Con la función Application Factory definida, se puede llamar en app.py en la carpeta de nivel superior del proyecto:

from project import create_app


# Call the application factory function to construct a Flask application
# instance using the development configuration
app = create_app()

Plano

Vamos a definir el journal_apiplan. Comience definiendo el journal_apimodelo en project/journal_api/__init__.py :

"""
The 'journal_api' blueprint handles the API for managing journal entries.
Specifically, this blueprint allows for journal entries to be added, edited,
and deleted.
"""
from flask import Blueprint

journal_api_blueprint = Blueprint('journal_api', __name__, template_folder='templates')

from . import routes

Ahora es el momento de definir los extremos de la API para el diario en project/journal_api/routes.py .

Comience con las importaciones necesarias:

from apifairy import body, other_responses, response
from flask import abort

from project import ma
from . import journal_api_blueprint

Para esta versión inicial de Flask Journal API, la base de datos será una lista de entradas de diario:

# --------
# Database
# --------

messages = [
    dict(id=1, entry='The sun was shining when I woke up this morning.'),
    dict(id=2, entry='I tried a new fruit mixture in my oatmeal for breakfast.'),
    dict(id=3, entry='Today I ate a great sandwich for lunch.')
]

A continuación, defina los esquemas para crear una nueva entrada de diario y para devolver las entradas de diario:

# -------
# Schemas
# -------

class NewEntrySchema(ma.Schema):
    """Schema defining the attributes when creating a new journal entry."""
    entry = ma.String(required=True)


class EntrySchema(ma.Schema):
    """Schema defining the attributes in a journal entry."""
    id = ma.Integer()
    entry = ma.String()


new_entry_schema = NewEntrySchema()
entry_schema = EntrySchema()
entries_schema = EntrySchema(many=True)

Ambas clases de esquema se heredan de ma.Schema , que proporciona Flask-Marshmallow. También es una buena idea crear objetos de estos esquemas, ya que esto le permite definir un esquema que puede devolver múltiples entradas (usando el many=Trueargumento).

¡Ahora estamos listos para definir los puntos finales de la API!

Rutas

Comience recuperando todas las entradas del diario:

@journal_api_blueprint.route('/', methods=['GET'])
@response(entries_schema)
def journal():
    """Return all journal entries"""
    return messages

Esta función de vista usa el @responsedecorador para definir que se devuelvan varias entradas. La función de visualización devuelve la lista completa de entradas de diario ( return messages).

A continuación, cree el extremo de la API para agregar una nueva entrada de diario:

@journal_api_blueprint.route('/', methods=['POST'])
@body(new_entry_schema)
@response(entry_schema, 201)
def add_journal_entry(kwargs):
    """Add a new journal entry"""
    new_message = dict(**kwargs, id=messages[-1]['id']+1)
    messages.append(new_message)
    return new_message

Esta función de vista utiliza el @bodydecorador para definir la entrada al punto final de la API y el @responsedecorador para definir la salida del punto final de la API.

Los datos de entrada que se analizan desde el @bodydecorador se pasan a la add_journal_entry()función de vista como el argumento kwargs( argumentos de palabras clave ) . Estos datos luego se utilizan para crear una nueva entrada de diario y agregarla a la base de datos:

new_message = dict(**kwargs, id=messages[-1]['id']+1)
messages.append(new_message)

A continuación, se devuelve el asiento de diario recién creado ( return new_message). Observe cómo el @responsedecorador define el código de retorno como 201 (Creado) para indicar que la entrada del diario se agregó a la base de datos.

Cree el punto final de API para recuperar una entrada de diario específica:

@journal_api_blueprint.route('/<int:index>', methods=['GET'])
@response(entry_schema)
@other_responses({404: 'Entry not found'})
def get_journal_entry(index):
    """Return a journal entry"""
    if index >= len(messages):
        abort(404)

    return messages[index]

Esta función de vista usa el @other_responsesdecorador para especificar respuestas no estándar.

¡ El @other_responsesdecorador solo se utiliza con fines de documentación! No proporciona ninguna funcionalidad en términos de devolución de códigos de error.

Cree el punto final de API para actualizar una entrada de diario:

@journal_api_blueprint.route('/<int:index>', methods=['PUT'])
@body(new_entry_schema)
@response(entry_schema)
@other_responses({404: 'Entry not found'})
def update_journal_entry(data, index):
    """Update a journal entry"""
    if index >= len(messages):
        abort(404)

    messages[index] = dict(data, id=index+1)
    return messages[index]

Esta función de vista usa los decoradores @bodyy @responsepara definir las entradas y salidas (respectivamente) para este punto final de API. Además, el @other_responsesdecorador define la respuesta no estándar si no se encuentra la entrada del diario.

Finalmente, cree el punto final de API para eliminar una entrada de diario:

@journal_api_blueprint.route('/<int:index>', methods=['DELETE'])
@other_responses({404: 'Entry not found'})
def delete_journal_entry(index):
    """Delete a journal entry"""
    if index >= len(messages):
        abort(404)

    messages.pop(index)
    return '', 204

Esta función de vista no utiliza los decoradores @bodyy @response, ya que no hay entradas ni salidas para este extremo de la API. Si la entrada del diario se elimina correctamente, se devuelve un código de estado 204 (Sin contenido) sin datos.

Ejecución de la aplicación Flask

Para probar las cosas, dentro de una ventana de terminal, configure la aplicación Flask y ejecute el servidor de desarrollo:

(venv) $ export FLASK_APP=app.py
(venv) $ export FLASK_ENV=development
(venv) $ flask run

Luego, en una ventana de terminal diferente, puede interactuar con la API. Siéntase libre de usar la herramienta que prefiera aquí, como cURL, HTTPie , Requests o Postman .

Ejemplo de solicitudes:

$ python3

>>> import requests
>>>
>>> r = requests.get('http://127.0.0.1:5000/journal/')
>>> print(r.text)
>>>
>>> post_data = {'entry': "some message"}
>>> r = requests.post('http://127.0.0.1:5000/journal/', json=post_data)
>>> print(r.text)

¿Quiere probar los puntos finales de la API más fácilmente? Consulte este script , que agrega comandos CLI para interactuar con los puntos finales de la API para recuperar, crear, actualizar y eliminar entradas del diario.

Documentación

¡Una característica increíble de APIFairy es la creación automática de documentación API!

Hay tres aspectos clave para configurar la documentación de la API:

  1. Cadenas de documentos para los puntos finales de la API (es decir, ver funciones)
  2. Docstring para el proyecto API general
  3. Variables de configuración para especificar el aspecto de la documentación de la API

Ya cubrimos el primer elemento en la sección anterior, ya que incluimos las cadenas de documentación para cada función de vista. Por ejemplo, la journal()función de vista tiene una breve descripción del propósito de este punto final de la API:

@journal_api_blueprint.route('/', methods=['GET'])
@response(entries_schema)
def journal():
    """Return all journal entries"""
    return messages

A continuación, debemos incluir la cadena de documentación para describir el proyecto general en la parte superior del archivo project/__init__.py :

"""
Welcome to the documentation for the Flask Journal API!

## Introduction

The Flask Journal API is an API (Application Programming Interface) for creating a **daily journal** that documents events that happen each day.

## Key Functionality

The Flask Journal API has the following functionality:

1. Work with journal entries:
  * Create a new journal entry
  * Update a journal entry
  * Delete a journal entry
  * View all journal entries
2. <More to come!>

## Key Modules

This project is written using Python 3.10.1.

The project utilizes the following modules:

* **Flask**: micro-framework for web application development which includes the following dependencies:
  * **click**: package for creating command-line interfaces (CLI)
  * **itsdangerous**: cryptographically sign data
  * **Jinja2**: templating engine
  * **MarkupSafe**: escapes characters so text is safe to use in HTML and XML
  * **Werkzeug**: set of utilities for creating a Python application that can talk to a WSGI server
* **APIFairy**: API framework for Flask which includes the following dependencies:
  * **Flask-Marshmallow** - Flask extension for using Marshmallow (object serialization/deserialization library)
  * **Flask-HTTPAuth** - Flask extension for HTTP authentication
  * **apispec** - API specification generator that supports the OpenAPI specification
* **pytest**: framework for testing Python projects
"""
...

Esta cadena de documentación se utiliza para describir el proyecto general, incluida la funcionalidad clave proporcionada y los paquetes clave de Python utilizados por el proyecto.

Finalmente, es necesario definir algunas variables de configuración para especificar el aspecto de la documentación de la API. Actualice la create_app()función en project/__init__.py :

def create_app():
    # Create the Flask application
    app = Flask(__name__)

    # Configure the API documentation
    app.config['APIFAIRY_TITLE'] = 'Flask Journal API'
    app.config['APIFAIRY_VERSION'] = '0.1'
    app.config['APIFAIRY_UI'] = 'elements'

    initialize_extensions(app)
    register_blueprints(app)

    return app

¿Listo para ver la documentación del proyecto? Inicie el servidor de desarrollo de Flask a través flask runde y luego navegue a http://127.0.0.1:5000/docs para ver la documentación de la API creada por APIFairy:

Documentación API - Página principal

En el panel de la izquierda, hay una lista de puntos finales de API para el journal_apiplan. Al hacer clic en uno de los puntos finales, se muestran todos los detalles sobre ese punto final:

Documentación de la API - Obtener el punto final de la API de entrada de diario

Lo sorprendente de esta documentación de la API es la capacidad de ver cómo funcionan los puntos finales de la API (suponiendo que se esté ejecutando el servidor de desarrollo de Flask). En el panel derecho de la documentación, ingrese un índice de entrada de diario y haga clic en "Enviar solicitud de API". A continuación, se muestra la respuesta de la API:

Documentación de API - Obtener respuesta de API de entrada de diario

¡Esta documentación interactiva facilita a los usuarios la comprensión de la API!

Base de datos

Para fines de demostración, en este tutorial se utilizará una base de datos SQLite.

Configuración

Dado que Flask-SQLAlchemy ya estaba instalado al comienzo de este tutorial, debemos configurarlo en el archivo project/__init__.py .

Comience creando un SQLAlchemy()objeto en la sección 'Configuración':

...

from apifairy import APIFairy
from flask import Flask, json
from flask_marshmallow import Marshmallow
from flask_sqlalchemy import SQLAlchemy  # <-- NEW!!


# -------------
# Configuration
# -------------

# Create the instances of the Flask extensions in the global scope,
# but without any arguments passed in. These instances are not
# attached to the Flask application at this point.
apifairy = APIFairy()
ma = Marshmallow()
database = SQLAlchemy()  # <-- NEW!!

...

A continuación, actualice la create_app()función para especificar las variables de configuración necesarias:

def create_app():
    # Create the Flask application
    app = Flask(__name__)

    # Configure the API documentation
    app.config['APIFAIRY_TITLE'] = 'Flask Journal API'
    app.config['APIFAIRY_VERSION'] = '0.1'
    app.config['APIFAIRY_UI'] = 'elements'

    # NEW!
    # Configure the SQLite database (intended for development only!)
    app.config['SQLALCHEMY_DATABASE_URI'] = f"sqlite:///{os.path.join(os.getcwd(), 'instance', 'app.db')}"
    app.config['SQLALCHEMY_TRACK_MODIFICATIONS'] = False

    initialize_extensions(app)
    register_blueprints(app)

    return app

Agregue la importación a la parte superior:

import os

La SQLALCHEMY_DATABASE_URIvariable de configuración es fundamental para identificar la ubicación de la base de datos SQLite. Para este tutorial, la base de datos se almacena en instance/app.db .

Finalmente, actualice la initialize_extensions()función para inicializar el objeto Flask-SQLAlchemy:

def initialize_extensions(app):
    # Since the application instance is now created, pass it to each Flask
    # extension instance to bind it to the Flask application instance (app)
    apifairy.init_app(app)
    ma.init_app(app)
    database.init_app(app)  # <-- NEW!!

¿Quiere obtener más información sobre cómo se conecta esta aplicación Flask? Consulte mi curso sobre cómo crear, probar e implementar una aplicación Flask:

Modelo de base de datos

Cree un nuevo archivo project/models.py para definir la tabla de la base de datos para representar las entradas del diario:

from project import database


class Entry(database.Model):
    """Class that represents a journal entry."""
    __tablename__ = 'entries'

    id = database.Column(database.Integer, primary_key=True)
    entry = database.Column(database.String, nullable=False)

    def __init__(self, entry: str):
        self.entry = entry

    def update(self, entry: str):
        self.entry = entry

    def __repr__(self):
        return f'<Entry: {self.entry}>'

Esta nueva clase, Entry, especifica que la entriestabla de la base de datos contendrá dos elementos (¡por ahora!) para representar una entrada de diario:

  1. id- la clave principal ( primary_key=True) de la tabla, lo que significa que es un identificador único para cada elemento (fila) de la tabla
  2. entry- cadena para almacenar el texto de la entrada del diario

Si bien models.py define la tabla de la base de datos, no crea las tablas en la base de datos SQLite. Para crear las tablas, inicie Flask shell en una ventana de terminal:

(venv)$ flask shell

>>> from project import database
>>> database.drop_all()
>>> database.create_all()
>>> quit()

(venv)$

Actualizaciones de API de diario

Dado que estamos progresando en el uso de una base de datos SQLite, comience eliminando la temporal database(lista de Python) que se definió en project/journal_api/routes.py :

# --------
# Database
# --------

messages = [
    dict(id=1, entry='The sun was shining when I woke up this morning.'),
    dict(id=2, entry='I tried a new fruit mixture in my oatmeal for breakfast.'),
    dict(id=3, entry='Today I ate a great sandwich for lunch.')
]

A continuación, debemos actualizar cada extremo de la API (es decir, las funciones de vista) para utilizar la base de datos SQLite.

Comience actualizando la journal()función de vista:

@journal_api_blueprint.route('/', methods=['GET'])
@response(entries_schema)
def journal():
    """Return all journal entries"""
    return Entry.query.all()

La lista completa de entradas de diario ahora se recupera de la base de datos SQLite. Observe cómo los esquemas o decoradores para esta función de vista no necesitaban cambiar... ¡solo el proceso subyacente para cambiar a los usuarios!

Añadir la importación:

from project.models import Entry

A continuación, actualice la add_journal_entry()función de vista:

@journal_api_blueprint.route('/', methods=['POST'])
@body(new_entry_schema)
@response(entry_schema, 201)
def add_journal_entry(kwargs):
    """Add a new journal entry"""
    new_message = Entry(**kwargs)
    database.session.add(new_message)
    database.session.commit()
    return new_message

Las entradas a esta función de vista se especifican mediante new_entry_schema:

class NewEntrySchema(ma.Schema):
    """Schema defining the attributes when creating a new journal entry."""
    entry = ma.String(required=True)

new_entry_schema = NewEntrySchema()

La entrycadena se usa para crear una nueva instancia de la Entryclase (definida en models.py ) y esta entrada de diario se agrega a la base de datos.

Añadir la importación:

from project import database

A continuación, actualice get_journal_entry():

@journal_api_blueprint.route('/<int:index>', methods=['GET'])
@response(entry_schema)
@other_responses({404: 'Entry not found'})
def get_journal_entry(index):
    """Return a journal entry"""
    entry = Entry.query.filter_by(id=index).first_or_404()
    return entry

Esta función ahora intenta buscar la entrada de diario especificada (basada en index):

entry = Entry.query.filter_by(id=index).first_or_404()

Si la entrada existe, se devuelve al usuario. Si la entrada no existe, se devuelve un error 404 (No encontrado).

A continuación, actualice update_journal_entry():

@journal_api_blueprint.route('/<int:index>', methods=['PUT'])
@body(new_entry_schema)
@response(entry_schema)
@other_responses({404: 'Entry not found'})
def update_journal_entry(data, index):
    """Update a journal entry"""
    entry = Entry.query.filter_by(id=index).first_or_404()
    entry.update(data['entry'])
    database.session.add(entry)
    database.session.commit()
    return entry

La update_journal_entry()función de vista ahora intenta recuperar la entrada de diario especificada:

entry = Entry.query.filter_by(id=index).first_or_404()

Si la entrada de diario existe, la entrada se actualiza con el nuevo texto y luego se guarda en la base de datos.

Por último, actualiza delete_journal_entry():

@journal_api_blueprint.route('/<int:index>', methods=['DELETE'])
@other_responses({404: 'Entry not found'})
def delete_journal_entry(index):
    """Delete a journal entry"""
    entry = Entry.query.filter_by(id=index).first_or_404()
    database.session.delete(entry)
    database.session.commit()
    return '', 204

Si se encuentra la entrada de diario especificada, se elimina de la base de datos.

Ejecute el servidor de desarrollo. Pruebe cada uno de los puntos finales para asegurarse de que aún funcionan.

Manejo de errores

Dado que este proyecto de Flask es una API, los códigos de error deben devolverse en formato JSON en lugar del formato HTML típico.

En el proyecto Flask, esto se puede lograr mediante el uso de un controlador de errores personalizado. En project/__init__.py , defina una nueva función ( register_error_handlers()) en la parte inferior del archivo:

def register_error_handlers(app):
    @app.errorhandler(HTTPException)
    def handle_http_exception(e):
        """Return JSON instead of HTML for HTTP errors."""
        # Start with the correct headers and status code from the error
        response = e.get_response()
        # Replace the body with JSON
        response.data = json.dumps({
            'code': e.code,
            'name': e.name,
            'description': e.description,
        })
        response.content_type = 'application/json'
        return response

Esta función registra un nuevo controlador de errores para cuando HTTPExceptionse genera un correo electrónico para convertir la salida en formato JSON.

Añadir la importación:

from werkzeug.exceptions import HTTPException

Además, actualice la función Application Factory create_app(), para llamar a esta nueva función:

def create_app():
    # Create the Flask application
    app = Flask(__name__)

    # Configure the API documentation
    app.config['APIFAIRY_TITLE'] = 'Flask Journal API'
    app.config['APIFAIRY_VERSION'] = '0.1'
    app.config['APIFAIRY_UI'] = 'elements'

    # Configure the SQLite database (intended for development only!)
    app.config['SQLALCHEMY_DATABASE_URI'] = f"sqlite:///{os.path.join(os.getcwd(), 'instance', 'app.db')}"
    app.config['SQLALCHEMY_TRACK_MODIFICATIONS'] = False

    initialize_extensions(app)
    register_blueprints(app)
    register_error_handlers(app)  # NEW!!

    return app

Autenticación

La autenticación es el proceso de validación de la identidad de un usuario que intenta acceder a un sistema, que en este caso es la API.

La autorización, por otro lado, es el proceso de verificar a qué recursos específicos debe tener acceso un usuario específico.

APIFairy utiliza Flask-HTTPAuth para el soporte de autenticación. En este tutorial, usaremos Flask-HTTPAuth de dos maneras:

  1. Autenticación básica : se utiliza para generar un token basado en el correo electrónico/contraseña del usuario
  2. Autenticación de token : se utiliza para autenticar al usuario en todos los demás puntos finales de la API

La autenticación de token utilizada a través de Flask-HTTPAuth a menudo se denomina autenticación de portador, ya que el proceso invoca la concesión de acceso al "portador" del token. El token debe incluirse en los encabezados HTTP en el encabezado Autorización, como "Autorización: Portador".

El siguiente diagrama ilustra un flujo típico de cómo un nuevo usuario interactúa con la aplicación para recuperar un token de autenticación:

Diagrama de flujo de API de Flask Journal

Configuración

Dado que Flask-HTTPAuth ya estaba instalado cuando se instaló APIFairy al comienzo de este tutorial, solo necesitamos configurarlo en el archivo project/__init__.py .

Comience creando objetos separados para la autenticación básica y de token:

...

import os

from apifairy import APIFairy
from flask import Flask, json
from flask_httpauth import HTTPBasicAuth, HTTPTokenAuth  # NEW!!
from flask_marshmallow import Marshmallow
from flask_sqlalchemy import SQLAlchemy
from werkzeug.exceptions import HTTPException


# -------------
# Configuration
# -------------

# Create the instances of the Flask extensions in the global scope,
# but without any arguments passed in. These instances are not
# attached to the Flask application at this point.
apifairy = APIFairy()
ma = Marshmallow()
database = SQLAlchemy()
basic_auth = HTTPBasicAuth()  # NEW!!
token_auth = HTTPTokenAuth()  # NEW!!

...

No se necesitan más actualizaciones en project/__init__.py .

Modelo de base de datos

En project/models.py , se debe crear un nuevo Usermodelo para representar a un usuario:

class User(database.Model):
    __tablename__ = 'users'

    id = database.Column(database.Integer, primary_key=True)
    email = database.Column(database.String, unique=True, nullable=False)
    password_hashed = database.Column(database.String(128), nullable=False)
    entries = database.relationship('Entry', backref='user', lazy='dynamic')
    auth_token = database.Column(database.String(64), index=True)
    auth_token_expiration = database.Column(database.DateTime)

    def __init__(self, email: str, password_plaintext: str):
        """Create a new User object."""
        self.email = email
        self.password_hashed = self._generate_password_hash(password_plaintext)

    def is_password_correct(self, password_plaintext: str):
        return check_password_hash(self.password_hashed, password_plaintext)

    def set_password(self, password_plaintext: str):
        self.password_hashed = self._generate_password_hash(password_plaintext)

    @staticmethod
    def _generate_password_hash(password_plaintext):
        return generate_password_hash(password_plaintext)

    def generate_auth_token(self):
        self.auth_token = secrets.token_urlsafe()
        self.auth_token_expiration = datetime.utcnow() + timedelta(minutes=60)
        return self.auth_token

    @staticmethod
    def verify_auth_token(auth_token):
        user = User.query.filter_by(auth_token=auth_token).first()
        if user and user.auth_token_expiration > datetime.utcnow():
            return user

    def revoke_auth_token(self):
        self.auth_token_expiration = datetime.utcnow()

    def __repr__(self):
        return f'<User: {self.email}>'

Agrega las importaciones:

import secrets
from datetime import datetime, timedelta

from werkzeug.security import check_password_hash, generate_password_hash

El Usermodelo utiliza werkzeug.securitypara cifrar la contraseña del usuario antes de almacenarla en la base de datos.

Recuerde: ¡Nunca almacene la contraseña de texto sin formato en una base de datos!

El Usermodelo se utiliza secretspara generar un token de autenticación para un usuario específico. Este token se crea en el generate_auth_token()método e incluye una fecha/hora de vencimiento de 60 minutos en el futuro:

def generate_auth_token(self):
    self.auth_token = secrets.token_urlsafe()
    self.auth_token_expiration = datetime.utcnow() + timedelta(minutes=60)
    return self.auth_token

Hay un método estático verify_auth_token(), que se usa para verificar el token de autenticación (teniendo en cuenta el tiempo de vencimiento) y devolver al usuario un token válido:

@staticmethod
def verify_auth_token(auth_token):
    user = User.query.filter_by(auth_token=auth_token).first()
    if user and user.auth_token_expiration > datetime.utcnow():
        return user

Otro método de interés es revoke_auth_token(), que se utiliza para revocar el token de autenticación de un usuario específico:

def revoke_auth_token(self):
    self.auth_token_expiration = datetime.utcnow()

Modelo de entrada

Para establecer la relación de uno a muchos entre el usuario ("uno") y sus entradas ("muchos"), el Entrymodelo debe actualizarse para vincular las tablas entriesy entre sí:users

class Entry(database.Model):
    """Class that represents a journal entry."""
    __tablename__ = 'entries'

    id = database.Column(database.Integer, primary_key=True)
    entry = database.Column(database.String, nullable=False)
    user_id = database.Column(database.Integer, database.ForeignKey('users.id'))  # <-- NEW!!

    def __init__(self, entry: str):
        self.entry = entry

    def update(self, entry: str):
        self.entry = entry

    def __repr__(self):
        return f'<Entry: {self.entry}>'

El Usermodelo ya contiene el enlace de regreso a la entriestabla:

entries = database.relationship('Entry', backref='user', lazy='dynamic')

Plan de API de usuarios

La funcionalidad de administración de usuarios del proyecto Flask se definirá en un Blueprint separado llamado users_api_blueprint.

Comience creando un nuevo directorio en "proyecto" llamado "users_api". Dentro de ese directorio, cree un archivo __init__.py :

from flask import Blueprint


users_api_blueprint = Blueprint('users_api', __name__)

from . import authentication, routes

Este nuevo Blueprint debe registrarse con Flask appen projects/__init__.py dentro de la register_blueprints()función:

def register_blueprints(app):
    # Import the blueprints
    from project.journal_api import journal_api_blueprint
    from project.users_api import users_api_blueprint  # NEW!!

    # Since the application instance is now created, register each Blueprint
    # with the Flask application instance (app)
    app.register_blueprint(journal_api_blueprint, url_prefix='/journal')
    app.register_blueprint(users_api_blueprint, url_prefix='/users')  # NEW!!

Funciones de autenticación

Para usar Flask-HTTPAuth, se deben definir varias funciones para manejar la verificación de las credenciales del usuario.

Cree un nuevo archivo project/users_api/authentication.py para manejar la autenticación básica y la autenticación de token.

Para la autenticación básica (comprobar el correo electrónico y la contraseña de un usuario):

from werkzeug.exceptions import Forbidden, Unauthorized

from project import basic_auth, token_auth
from project.models import User


@basic_auth.verify_password
def verify_password(email, password):
    user = User.query.filter_by(email=email).first()
    if user is None:
        return None

    if user.is_password_correct(password):
        return user


@basic_auth.error_handler
def basic_auth_error(status=401):
    error = (Forbidden if status == 403 else Unauthorized)()
    return {
        'code': error.code,
        'message': error.name,
        'description': error.description,
    }, error.code, {'WWW-Authenticate': 'Form'}

La verify_password()función se utiliza para comprobar que existe un usuario y que su contraseña es correcta. Flask-HTTPAuth utilizará esta función para verificar la contraseña cuando se necesite una autenticación básica (gracias al @basic_auth.verify_passworddecorador).

Además, se define un controlador de errores para la autenticación básica que devuelve información sobre el error en formato JSON.

Para la autenticación del token (procesar un token para determinar si el usuario es válido):

@token_auth.verify_token
def verify_token(auth_token):
    return User.verify_auth_token(auth_token)


@token_auth.error_handler
def token_auth_error(status=401):
    error = (Forbidden if status == 403 else Unauthorized)()
    return {
        'code': error.code,
        'message': error.name,
        'description': error.description,
    }, error.code

La verify_token()función se utiliza para comprobar si un token de autenticación es válido. Flask-HTTPAuth utilizará esta función para verificar el token cuando se necesite la autenticación del token (gracias al @token_auth.verify_tokendecorador).

Además, se define un controlador de errores para la autenticación del token que devuelve información sobre el error en formato JSON.

Rutas de usuarios

En el users_api_blueprint, habrá dos rutas:

  1. Registro de un nuevo usuario
  2. Recuperar un token de autenticación

Para comenzar, se debe definir un nuevo conjunto de esquemas (usando malvavisco) en projects/users_api/routes.py :

from project import ma

from . import users_api_blueprint

# -------
# Schemas
# -------

class NewUserSchema(ma.Schema):
    """Schema defining the attributes when creating a new user."""
    email = ma.String()
    password_plaintext = ma.String()


class UserSchema(ma.Schema):
    """Schema defining the attributes of a user."""
    id = ma.Integer()
    email = ma.String()


class TokenSchema(ma.Schema):
    """Schema defining the attributes of a token."""
    token = ma.String()


new_user_schema = NewUserSchema()
user_schema = UserSchema()
token_schema = TokenSchema()

Estos esquemas se utilizarán para definir las entradas y salidas de las funciones de vista definidas en este archivo.

Registro de un nuevo usuario

A continuación, defina la función de vista para registrar un nuevo usuario:

@users_api_blueprint.route('/', methods=['POST'])
@body(new_user_schema)
@response(user_schema, 201)
def register(kwargs):
    """Create a new user"""
    new_user = User(**kwargs)
    database.session.add(new_user)
    database.session.commit()
    return new_user

Agrega las importaciones:

from apifairy import authenticate, body, other_responses, response

from project import basic_auth, database, ma
from project.models import User

Este extremo de la API usa new_user_schemapara especificar que el correo electrónico y la contraseña son las entradas.

NOTA: Dado que el correo electrónico y la contraseña se envían a este extremo de la API, es un buen momento para recordar que el uso de HTTP es aceptable durante las pruebas de desarrollo, pero siempre se debe usar HTTPS (seguro) en la producción.

El correo electrónico y la contraseña (definidos como los kwargsargumentos de palabra clave) se desempaquetan para crear un nuevo Userobjeto, que se guarda en la base de datos:

new_user = User(**kwargs)
database.session.add(new_user)
database.session.commit()

La salida del extremo de la API se define mediante user_schema, que es el ID y el correo electrónico del nuevo usuario.

Recuperar un token de autenticación

La otra función de vista para definir en projects/users_api/routes.py es para recuperar el token de autenticación:

@users_api_blueprint.route('/get-auth-token', methods=['POST'])
@authenticate(basic_auth)
@response(token_schema)
@other_responses({401: 'Invalid username or password'})
def get_auth_token():
    """Get authentication token"""
    user = basic_auth.current_user()
    token = user.generate_auth_token()
    database.session.add(user)
    database.session.commit()
    return dict(token=token)

El @authenticatedecorador se usa por primera vez en este tutorial y especifica que se debe usar la autenticación básica para proteger esta ruta:

@authenticate(basic_auth)

Cuando el usuario desea recuperar su token de autenticación, debe enviar una solicitud POST a este extremo de la API con el correo electrónico y la contraseña incrustados en el encabezado "Autorización". Como ejemplo, el siguiente comando de Python que utiliza el paquete de solicitudes podría realizarse en este punto final de la API:

>>> import requests
>>> r = requests.post(
    'http://127.0.0.1:5000/users/get-auth-token',
    auth=('pkennedy@hey.com', 'FlaskIsAwesome123')
)

Si la autenticación básica es exitosa, la función de visualización recupera al usuario actual mediante el current_user()método proporcionado por Flask-HTTPAuth:

user = basic_auth.current_user()

Se crea un nuevo token de autenticación para ese usuario:

token = user.generate_auth_token()

Y ese token se guarda en la base de datos para que pueda usarse para autenticar al usuario en el futuro (¡al menos durante los próximos 60 minutos!).

Finalmente, el nuevo token de autenticación se devuelve para que el usuario lo guarde para todas las llamadas API posteriores.

Actualizaciones de puntos finales de API

Con un proceso de autenticación implementado, es hora de agregar algunas protecciones a los puntos finales de API existentes para asegurarse de que solo los usuarios válidos puedan acceder a la aplicación.

Estas actualizaciones son para las funciones de visualización definidas en projects/journal_api/routes.py .

Primero, actualice journal()para devolver solo las entradas de diario para el usuario actual:

@journal_api_blueprint.route('/', methods=['GET'])
@authenticate(token_auth)
@response(entries_schema)
def journal():
    """Return journal entries"""
    user = token_auth.current_user()
    return Entry.query.filter_by(user_id=user.id).all()

Actualice las importaciones en la parte superior así:

from apifairy import authenticate, body, other_responses, response
from flask import abort

from project import database, ma, token_auth
from project.models import Entry

from . import journal_api_blueprint

El @authenticatedecorador especifica que se debe usar la autenticación de token al acceder a este punto final de la API. Como ejemplo, la siguiente solicitud GET podría realizarse mediante Solicitudes ( después de que se haya recuperado el token de autenticación ):

>>> import requests
>>> headers = {'Authorization': f'Bearer {auth_token}'}
>>> r = requests.get('http://127.0.0.1:5000/journal/', headers=headers)

Una vez que el usuario está autenticado, la lista completa de entradas de diario se recupera de la base de datos según la identificación del usuario:

user = token_auth.current_user()
return Entry.query.filter_by(user_id=user.id).all()

El decorador define la salida de este extremo de la API @response, que es una lista de entradas de diario (ID, entrada, ID de usuario).

A continuación, actualice add_journal_entry():

@journal_api_blueprint.route('/', methods=['POST'])
@authenticate(token_auth)
@body(new_entry_schema)
@response(entry_schema, 201)
def add_journal_entry(kwargs):
    """Add a new journal entry"""
    user = token_auth.current_user()
    new_message = Entry(user_id=user.id, **kwargs)
    database.session.add(new_message)
    database.session.commit()
    return new_message

Al igual que con la función de vista anterior, el @authenticatedecorador se usa para especificar que se debe usar la autenticación de token al acceder a este punto final de la API. Además, la entrada de diario ahora se agrega especificando el ID de usuario que debe asociarse con la entrada de diario:

user = token_auth.current_user()
new_message = Entry(user_id=user.id, **kwargs)

La nueva entrada de diario se guarda en la base de datos y se devuelve la entrada de diario (según lo definido por el @responsedecorador).

A continuación, actualice get_journal_entry():

@journal_api_blueprint.route('/<int:index>', methods=['GET'])
@authenticate(token_auth)
@response(entry_schema)
@other_responses({403: 'Forbidden', 404: 'Entry not found'})
def get_journal_entry(index):
    """Return a journal entry"""
    user = token_auth.current_user()
    entry = Entry.query.filter_by(id=index).first_or_404()

    if entry.user_id != user.id:
        abort(403)
    return entry

El @authenticatedecorador se agrega para especificar que se necesita autenticación de token para acceder a este punto final de API.

Al intentar recuperar una entrada de diario, se agrega una verificación adicional para asegurarse de que el usuario que intenta acceder a la entrada de diario es el "propietario" real de la entrada. De lo contrario, se devuelve un código de error 403 (Prohibido) a través de la abort()función de Flask:

if entry.user_id != user.id:
        abort(403)

Tenga en cuenta que este punto final de la API tiene dos respuestas no nominales especificadas por el @other_responsesdecorador:

@other_responses({403: 'Forbidden', 404: 'Entry not found'})

Recordatorio: El @other_responsesdecorador es solo para documentación; es responsabilidad de la función de vista generar estos errores.

A continuación, actualice update_journal_entry():

@journal_api_blueprint.route('/<int:index>', methods=['PUT'])
@authenticate(token_auth)
@body(new_entry_schema)
@response(entry_schema)
@other_responses({403: 'Forbidden', 404: 'Entry not found'})
def update_journal_entry(data, index):
    """Update a journal entry"""
    user = token_auth.current_user()
    entry = Entry.query.filter_by(id=index).first_or_404()

    if entry.user_id != user.id:
        abort(403)

    entry.update(data['entry'])
    database.session.add(entry)
    database.session.commit()
    return entry

Las actualizaciones de esta función de vista son similares a las otras funciones de vista en esta sección:

  1. @authenticatedecorador especifica que se necesita autenticación de token para acceder a este punto final de API
  2. Solo el usuario que "posee" la entrada del diario puede actualizar la entrada (de lo contrario, 403 (Prohibido))

Por último, actualiza delete_journal_entry():

@journal_api_blueprint.route('/<int:index>', methods=['DELETE'])
@authenticate(token_auth)
@other_responses({403: 'Forbidden', 404: 'Entry not found'})
def delete_journal_entry(index):
    """Delete a journal entry"""
    user = token_auth.current_user()
    entry = Entry.query.filter_by(id=index).first_or_404()

    if entry.user_id != user.id:
        abort(403)

    database.session.delete(entry)
    database.session.commit()
    return '', 204

Conclusión

Este tutorial proporcionó un recorrido de cómo crear fácil y rápidamente una API en Flask usando APIFairy.

Los decoradores son la clave para definir los puntos finales de la API:

  • Entradas :
    • @arguments- argumentos de entrada de la cadena de consulta de la URL
    • @body- estructura de la solicitud JSON
  • Salidas :
    • @response- estructura de la respuesta JSON
  • Autenticación :
    • @authenticate- enfoque de autenticación usando Flask-HTTPAuth
  • Errores :
    • @other_responses- respuestas no nominales, como códigos de error HTTP

Además, la documentación de API generada por APIFairy es excelente y proporciona información clave para los usuarios de la aplicación.

Fuente:  https://testdriven.io

#api #flask 

Cómo Crear Fácilmente Una API RESTful Con Flask Y APIFairy
Audra  Haag

Audra Haag

1656732567

How to Easily Create A RESTful API with Flask and APIFairy

This tutorial demonstrates how to easily create a RESTful API with Flask and APIFairy.

Objectives

By the end of this tutorial, you'll be able to:

  1. Create API endpoints in Flask using the decorators provided by APIFairy
  2. Utilize Flask-Marshmallow to define the schemas for inputs/outputs to the API endpoints
  3. Generate the API documentation using APIFairy
  4. Integrate a relational database with the API endpoints
  5. Implement basic and token authentication using Flask-HTTPAuth

Source: https://testdriven.io

#api #flask 

How to Easily Create A RESTful API with Flask and APIFairy

Como Criar Facilmente Uma API RESTful Com Flask E APIFairy

Este tutorial demonstra como criar facilmente uma API RESTful com Flask e APIFairy.

Objetivos

Ao final deste tutorial, você será capaz de:

  1. Crie endpoints de API no Flask usando os decoradores fornecidos pela APIFairy
  2. Utilize Flask-Marshmallow para definir os esquemas de entradas/saídas para os endpoints da API
  3. Gere a documentação da API usando APIFairy
  4. Integrar um banco de dados relacional com os endpoints da API
  5. Implemente autenticação básica e de token usando Flask-HTTPAuth

O que é APIFairy?

APIFairy é um framework de API escrito por Miguel Grinberg que permite criar facilmente uma API com Flask.

APIFairy fornece quatro componentes principais para criar facilmente uma API no Flask:

  1. Decoradores
  2. Esquemas
  3. Autenticação
  4. Documentação

Vamos explorar cada um em detalhes...

Decoradores

APIFairy fornece um conjunto de decoradores para definir as entradas, saídas e autenticação para cada endpoint da API:

APIFairy Decorators

APIFairy fornece cinco decoradores principais:

  1. @arguments - especifica os argumentos de entrada na string de consulta da URL
  2. @body - especifica o corpo JSON de entrada como um esquema
  3. @response - especifica o corpo JSON de saída como um esquema
  4. @other_responses - especifica respostas adicionais (geralmente erros) que podem ser retornadas ( apenas documentação )
  5. @authenticate - especifica o processo de autenticação

Esquemas

A entrada (usando o @bodydecorador) e a saída (usando o @responsedecorador) de um endpoint de API são definidas como esquemas:

class EntrySchema(ma.Schema):
    """Schema defining the attributes in a journal entry."""
    id = ma.Integer()
    entry = ma.String()
    user_id = ma.Integer()

Os esquemas utilizam marshmallow para definir os tipos de dados como classes.

Autenticação

O @authenticatedecorador é usado para verificar o cabeçalho de autenticação fornecido na solicitação de URL para cada endpoint da API. O esquema de autenticação é implementado usando Flask-HTTPAuth , que também foi criado por Miguel Grinberg.

Uma abordagem típica de autenticação de API seria definir a autenticação básica para proteger a rota para recuperar um token de autenticação:

basic_auth = HTTPBasicAuth()

@basic_auth.verify_password
def verify_password(email, password):
    user = User.query.filter_by(email=email).first()
    if user.is_password_correct(password):
        return user

E também para definir a autenticação de token para proteger a maioria das rotas com base em um token de autenticação sensível ao tempo:

token_auth = HTTPTokenAuth()

@token_auth.verify_token
def verify_token(auth_token):
    return User.verify_auth_token(auth_token)

Documentação

Um dos grandes recursos do APIFairy é a bela documentação da API que é gerada automaticamente:

Documentação da API - Página principal

A documentação é gerada com base em docstrings no código-fonte junto com as seguintes variáveis ​​de configuração:

  1. APIFAIRY_TITLE- nome do projeto
  2. APIFAIRY_VERSION- string de versão do projeto
  3. APIFAIRY_UI- formato da documentação da API

Para APIFAIRY_UI, você pode gerar modelos de um dos seguintes renderizadores de documentação OpenAPI:

  1. IU do Swagger
  2. ReDoc
  3. RapiDoc
  4. Elementos

Para obter uma lista completa de variáveis ​​de configuração disponíveis, consulte os documentos de configuração .

O que estamos construindo?

Você desenvolverá uma API de diário neste tutorial, permitindo que os usuários mantenham um diário de eventos. Você pode encontrar o código-fonte completo no repositório flask-journal-api no GitLab.

Principais pacotes Python usados:

  1. Flask : micro-framework para desenvolvimento de aplicações web em Python
  2. APIFairy : framework de API para Flask, que usa-
  3. Flask-SQLAlchemy : ORM (Object Relational Mapper) para Flask

Você desenvolverá a API de forma incremental:

  1. Crie os endpoints da API para trabalhar com entradas de diário
  2. Gerar documentação da API
  3. Adicionar um banco de dados relacional para armazenar as entradas de diário
  4. Adicionar autenticação para proteger os endpoints da API

Pontos de extremidade da API

Vamos pular para a criação de uma API usando Flask e APIFairy...

Inicialização do projeto

Comece criando uma nova pasta de projeto e um ambiente virtual:

$ mkdir flask-journal-api
$ cd flask-journal-api
$ python3 -m venv venv
$ source venv/bin/activate
(venv)$

Sinta-se à vontade para trocar virtualenv e Pip por Poetry ou Pipenv . Para saber mais, revise Ambientes Python Modernos .

Vá em frente e adicione os seguintes arquivos e pastas:

├── app.py
├── instance
│   └── .gitkeep
├── project
│   ├── __init__.py
│   └── journal_api
│       ├── __init__.py
│       └── routes.py
└── requirements.txt

Em seguida, para instalar os pacotes Python necessários, adicione as dependências ao arquivo requirements.txt na raiz do projeto:

apifairy==0.9.1
Flask==2.1.2
Flask-SQLAlchemy==2.5.1
marshmallow-sqlalchemy==0.28.0

Instalar:

(venv)$ pip install -r requirements.txt

Este projeto Flask utilizará duas práticas recomendadas para aplicativos Flask:

  1. Application Factory - usado para criar o aplicativo Flask em uma função
  2. Blueprints - usados ​​para organizar um grupo de visualizações relacionadas

Fábrica de aplicativos

Comece definindo a função Application Factory em project/__init__.py :

from apifairy import APIFairy
from flask import Flask, json
from flask_marshmallow import Marshmallow

# -------------
# Configuration
# -------------

# Create the instances of the Flask extensions in the global scope,
# but without any arguments passed in. These instances are not
# attached to the Flask application at this point.
apifairy = APIFairy()
ma = Marshmallow()


# ----------------------------
# Application Factory Function
# ----------------------------

def create_app():
    # Create the Flask application
    app = Flask(__name__)

    initialize_extensions(app)
    register_blueprints(app)
    return app


# ----------------
# Helper Functions
# ----------------

def initialize_extensions(app):
    # Since the application instance is now created, pass it to each Flask
    # extension instance to bind it to the Flask application instance (app)
    apifairy.init_app(app)
    ma.init_app(app)


def register_blueprints(app):
    # Import the blueprints
    from project.journal_api import journal_api_blueprint

    # Since the application instance is now created, register each Blueprint
    # with the Flask application instance (app)
    app.register_blueprint(journal_api_blueprint, url_prefix='/journal')

Com a função Application Factory definida, ela pode ser chamada em app.py na pasta de nível superior do projeto:

from project import create_app


# Call the application factory function to construct a Flask application
# instance using the development configuration
app = create_app()

Planta

Vamos definir o journal_apiplano. Comece definindo o journal_apiblueprint em project/journal_api/__init__.py :

"""
The 'journal_api' blueprint handles the API for managing journal entries.
Specifically, this blueprint allows for journal entries to be added, edited,
and deleted.
"""
from flask import Blueprint

journal_api_blueprint = Blueprint('journal_api', __name__, template_folder='templates')

from . import routes

Agora é hora de definir os endpoints da API para o diário em project/journal_api/routes.py .

Comece com as importações necessárias:

from apifairy import body, other_responses, response
from flask import abort

from project import ma
from . import journal_api_blueprint

Para esta versão inicial da API do Flask Journal, o banco de dados será uma lista de entradas de diário:

# --------
# Database
# --------

messages = [
    dict(id=1, entry='The sun was shining when I woke up this morning.'),
    dict(id=2, entry='I tried a new fruit mixture in my oatmeal for breakfast.'),
    dict(id=3, entry='Today I ate a great sandwich for lunch.')
]

Em seguida, defina os esquemas para criar uma nova entrada de diário e para retornar as entradas de diário:

# -------
# Schemas
# -------

class NewEntrySchema(ma.Schema):
    """Schema defining the attributes when creating a new journal entry."""
    entry = ma.String(required=True)


class EntrySchema(ma.Schema):
    """Schema defining the attributes in a journal entry."""
    id = ma.Integer()
    entry = ma.String()


new_entry_schema = NewEntrySchema()
entry_schema = EntrySchema()
entries_schema = EntrySchema(many=True)

Ambas as classes de esquema herdam de ma.Schema , que é fornecido pelo Flask-Marshmallow. Também é uma boa ideia criar objetos desses esquemas, pois isso permite definir um esquema que pode retornar várias entradas (usando o many=Trueargumento).

Agora estamos prontos para definir os endpoints da API!

Rotas

Comece recuperando todas as entradas de diário:

@journal_api_blueprint.route('/', methods=['GET'])
@response(entries_schema)
def journal():
    """Return all journal entries"""
    return messages

Essa função de exibição usa o @responsedecorador para definir que várias entradas sejam retornadas. A função de visualização retorna a lista completa de entradas de diário ( return messages).

Em seguida, crie o endpoint da API para adicionar uma nova entrada de diário:

@journal_api_blueprint.route('/', methods=['POST'])
@body(new_entry_schema)
@response(entry_schema, 201)
def add_journal_entry(kwargs):
    """Add a new journal entry"""
    new_message = dict(**kwargs, id=messages[-1]['id']+1)
    messages.append(new_message)
    return new_message

Essa função de visualização usa o @bodydecorador para definir a entrada para o endpoint da API e o @responsedecorador para definir a saída do endpoint da API.

Os dados de entrada que são analisados ​​do @bodydecorador são passados ​​para a função add_journal_entry()de visualização como o argumento kwargs( argumentos da palavra - chave ). Esses dados são então usados ​​para criar uma nova entrada de diário e adicioná-la ao banco de dados:

new_message = dict(**kwargs, id=messages[-1]['id']+1)
messages.append(new_message)

A entrada de diário recém-criada é então retornada ( return new_message). Observe como o @responsedecorador define o código de retorno como 201 (Criado) para indicar que a entrada de diário foi adicionada ao banco de dados.

Crie o endpoint da API para recuperar uma entrada de diário específica:

@journal_api_blueprint.route('/<int:index>', methods=['GET'])
@response(entry_schema)
@other_responses({404: 'Entry not found'})
def get_journal_entry(index):
    """Return a journal entry"""
    if index >= len(messages):
        abort(404)

    return messages[index]

Esta função de visualização usa o @other_responsesdecorador para especificar respostas não padrão.

O @other_responsesdecorador é usado apenas para fins de documentação! Ele não fornece nenhuma funcionalidade em termos de retorno de códigos de erro.

Crie o endpoint da API para atualizar uma entrada de diário:

@journal_api_blueprint.route('/<int:index>', methods=['PUT'])
@body(new_entry_schema)
@response(entry_schema)
@other_responses({404: 'Entry not found'})
def update_journal_entry(data, index):
    """Update a journal entry"""
    if index >= len(messages):
        abort(404)

    messages[index] = dict(data, id=index+1)
    return messages[index]

Essa função de visualização usa os decoradores @bodye @responsepara definir as entradas e saídas (respectivamente) para este endpoint da API. Além disso, o @other_responsesdecorador define a resposta não padrão se a entrada de diário não for encontrada.

Por fim, crie o endpoint da API para excluir uma entrada de diário:

@journal_api_blueprint.route('/<int:index>', methods=['DELETE'])
@other_responses({404: 'Entry not found'})
def delete_journal_entry(index):
    """Delete a journal entry"""
    if index >= len(messages):
        abort(404)

    messages.pop(index)
    return '', 204

Esta função de visualização não usa os decoradores @bodye @response, pois não há entradas ou saídas para este endpoint da API. Se a entrada de diário for excluída com sucesso, um código de status 204 (Sem conteúdo) será retornado sem dados.

Executando o aplicativo Flask

Para testar as coisas, em uma janela de terminal, configure o aplicativo Flask e execute o servidor de desenvolvimento:

(venv) $ export FLASK_APP=app.py
(venv) $ export FLASK_ENV=development
(venv) $ flask run

Então, em uma janela de terminal diferente, você pode interagir com a API. Sinta-se à vontade para usar sua ferramenta de escolha aqui, como cURL, HTTPie , Requests ou Postman .

Exemplo de solicitações:

$ python3

>>> import requests
>>>
>>> r = requests.get('http://127.0.0.1:5000/journal/')
>>> print(r.text)
>>>
>>> post_data = {'entry': "some message"}
>>> r = requests.post('http://127.0.0.1:5000/journal/', json=post_data)
>>> print(r.text)

Quer testar os endpoints da API com mais facilidade? Confira este script , que adiciona comandos da CLI para interagir com os endpoints da API para recuperar, criar, atualizar e excluir entradas de diário.

Documentação

Um recurso incrível do APIFairy é a criação automática de documentação da API!

Há três aspectos principais para configurar a documentação da API:

  1. Docstrings para os endpoints da API (ou seja, funções de visualização)
  2. Docstring para o projeto geral da API
  3. Variáveis ​​de configuração para especificar a aparência da documentação da API

Já abordamos o primeiro item na seção anterior, pois incluímos as docstrings para cada função de visualização. Por exemplo, a journal()função de visualização tem uma breve descrição da finalidade deste endpoint da API:

@journal_api_blueprint.route('/', methods=['GET'])
@response(entries_schema)
def journal():
    """Return all journal entries"""
    return messages

Em seguida, precisamos incluir a docstring para descrever o projeto geral no topo do arquivo project/__init__.py :

"""
Welcome to the documentation for the Flask Journal API!

## Introduction

The Flask Journal API is an API (Application Programming Interface) for creating a **daily journal** that documents events that happen each day.

## Key Functionality

The Flask Journal API has the following functionality:

1. Work with journal entries:
  * Create a new journal entry
  * Update a journal entry
  * Delete a journal entry
  * View all journal entries
2. <More to come!>

## Key Modules

This project is written using Python 3.10.1.

The project utilizes the following modules:

* **Flask**: micro-framework for web application development which includes the following dependencies:
  * **click**: package for creating command-line interfaces (CLI)
  * **itsdangerous**: cryptographically sign data
  * **Jinja2**: templating engine
  * **MarkupSafe**: escapes characters so text is safe to use in HTML and XML
  * **Werkzeug**: set of utilities for creating a Python application that can talk to a WSGI server
* **APIFairy**: API framework for Flask which includes the following dependencies:
  * **Flask-Marshmallow** - Flask extension for using Marshmallow (object serialization/deserialization library)
  * **Flask-HTTPAuth** - Flask extension for HTTP authentication
  * **apispec** - API specification generator that supports the OpenAPI specification
* **pytest**: framework for testing Python projects
"""
...

Esta docstring é usada para descrever o projeto geral, incluindo a funcionalidade chave fornecida e os principais pacotes Python usados ​​pelo projeto.

Por fim, algumas variáveis ​​de configuração precisam ser definidas para especificar a aparência da documentação da API. Atualize a create_app()função em project/__init__.py :

def create_app():
    # Create the Flask application
    app = Flask(__name__)

    # Configure the API documentation
    app.config['APIFAIRY_TITLE'] = 'Flask Journal API'
    app.config['APIFAIRY_VERSION'] = '0.1'
    app.config['APIFAIRY_UI'] = 'elements'

    initialize_extensions(app)
    register_blueprints(app)

    return app

Pronto para ver a documentação do projeto? Inicie o servidor de desenvolvimento Flask via flask rune navegue até http://127.0.0.1:5000/docs para ver a documentação da API criada pela APIFairy:

Documentação da API - Página principal

No painel esquerdo, há uma lista de endpoints de API para o journal_apiblueprint. Clicar em um dos endpoints mostra todos os detalhes sobre esse endpoint:

Documentação da API - Obter ponto final da API de entrada de diário

O que é incrível nessa documentação da API é a capacidade de ver como os endpoints da API funcionam (supondo que o servidor de desenvolvimento do Flask esteja em execução). No painel direito da documentação, insira um índice de lançamento contábil manual e clique em "Enviar solicitação de API". A resposta da API é então exibida:

Documentação da API - Obter resposta da API de lançamento no diário

Esta documentação interativa torna mais fácil para os usuários entenderem a API!

Base de dados

Para fins de demonstração, um banco de dados SQLite será usado neste tutorial.

Configuração

Como o Flask-SQLAlchemy já estava instalado no início deste tutorial, precisamos configurá-lo no arquivo project/__init__.py .

Comece criando um SQLAlchemy()objeto na seção 'Configuração':

...

from apifairy import APIFairy
from flask import Flask, json
from flask_marshmallow import Marshmallow
from flask_sqlalchemy import SQLAlchemy  # <-- NEW!!


# -------------
# Configuration
# -------------

# Create the instances of the Flask extensions in the global scope,
# but without any arguments passed in. These instances are not
# attached to the Flask application at this point.
apifairy = APIFairy()
ma = Marshmallow()
database = SQLAlchemy()  # <-- NEW!!

...

Em seguida, atualize a create_app()função para especificar as variáveis ​​de configuração necessárias:

def create_app():
    # Create the Flask application
    app = Flask(__name__)

    # Configure the API documentation
    app.config['APIFAIRY_TITLE'] = 'Flask Journal API'
    app.config['APIFAIRY_VERSION'] = '0.1'
    app.config['APIFAIRY_UI'] = 'elements'

    # NEW!
    # Configure the SQLite database (intended for development only!)
    app.config['SQLALCHEMY_DATABASE_URI'] = f"sqlite:///{os.path.join(os.getcwd(), 'instance', 'app.db')}"
    app.config['SQLALCHEMY_TRACK_MODIFICATIONS'] = False

    initialize_extensions(app)
    register_blueprints(app)

    return app

Adicione a importação ao topo:

import os

A SQLALCHEMY_DATABASE_URIvariável de configuração é fundamental para identificar a localização do banco de dados SQLite. Para este tutorial, o banco de dados é armazenado em instance/app.db .

Por fim, atualize a initialize_extensions()função para inicializar o objeto Flask-SQLAlchemy:

def initialize_extensions(app):
    # Since the application instance is now created, pass it to each Flask
    # extension instance to bind it to the Flask application instance (app)
    apifairy.init_app(app)
    ma.init_app(app)
    database.init_app(app)  # <-- NEW!!

Quer saber mais sobre como este aplicativo Flask está conectado? Confira meu curso sobre como construir, testar e implantar um aplicativo Flask:

Modelo de banco de dados

Crie um novo arquivo project/models.py para definir a tabela de banco de dados para representar as entradas de diário:

from project import database


class Entry(database.Model):
    """Class that represents a journal entry."""
    __tablename__ = 'entries'

    id = database.Column(database.Integer, primary_key=True)
    entry = database.Column(database.String, nullable=False)

    def __init__(self, entry: str):
        self.entry = entry

    def update(self, entry: str):
        self.entry = entry

    def __repr__(self):
        return f'<Entry: {self.entry}>'

Essa nova classe, Entry, especifica que a entriestabela do banco de dados conterá dois elementos (por enquanto!) para representar uma entrada de diário:

  1. id- a chave primária ( primary_key=True) para a tabela, o que significa que é um identificador exclusivo para cada elemento (linha) na tabela
  2. entry- string para armazenar o texto do lançamento contábil manual

Embora o models.py defina a tabela do banco de dados, ele não cria as tabelas no banco de dados SQLite. Para criar as tabelas, inicie o shell Flask em uma janela de terminal:

(venv)$ flask shell

>>> from project import database
>>> database.drop_all()
>>> database.create_all()
>>> quit()

(venv)$

Atualizações da API de diário

Como estamos progredindo para usar um banco de dados SQLite, comece excluindo o temporário database(lista Python) que foi definido em project/journal_api/routes.py :

# --------
# Database
# --------

messages = [
    dict(id=1, entry='The sun was shining when I woke up this morning.'),
    dict(id=2, entry='I tried a new fruit mixture in my oatmeal for breakfast.'),
    dict(id=3, entry='Today I ate a great sandwich for lunch.')
]

Em seguida, precisamos atualizar cada endpoint da API (ou seja, as funções de visualização) para utilizar o banco de dados SQLite.

Comece atualizando a journal()função de visualização:

@journal_api_blueprint.route('/', methods=['GET'])
@response(entries_schema)
def journal():
    """Return all journal entries"""
    return Entry.query.all()

A lista completa de entradas de diário agora é recuperada do banco de dados SQLite. Observe como os esquemas ou decoradores para esta função de visualização não precisaram ser alterados... apenas o processo subjacente para alterar os usuários!

Adicione a importação:

from project.models import Entry

Em seguida, atualize a add_journal_entry()função de visualização:

@journal_api_blueprint.route('/', methods=['POST'])
@body(new_entry_schema)
@response(entry_schema, 201)
def add_journal_entry(kwargs):
    """Add a new journal entry"""
    new_message = Entry(**kwargs)
    database.session.add(new_message)
    database.session.commit()
    return new_message

As entradas para esta função de visualização são especificadas por new_entry_schema:

class NewEntrySchema(ma.Schema):
    """Schema defining the attributes when creating a new journal entry."""
    entry = ma.String(required=True)

new_entry_schema = NewEntrySchema()

A entrystring é usada para criar uma nova instância da Entryclasse (definida em models.py ) e esta entrada de diário é então adicionada ao banco de dados.

Adicione a importação:

from project import database

A seguir, atualize get_journal_entry():

@journal_api_blueprint.route('/<int:index>', methods=['GET'])
@response(entry_schema)
@other_responses({404: 'Entry not found'})
def get_journal_entry(index):
    """Return a journal entry"""
    entry = Entry.query.filter_by(id=index).first_or_404()
    return entry

Esta função agora tenta pesquisar a entrada de diário especificada (com base no index):

entry = Entry.query.filter_by(id=index).first_or_404()

Se a entrada existir, ela será devolvida ao usuário. Se a entrada não existir, um erro 404 (Não encontrado) será retornado.

A seguir, atualize update_journal_entry():

@journal_api_blueprint.route('/<int:index>', methods=['PUT'])
@body(new_entry_schema)
@response(entry_schema)
@other_responses({404: 'Entry not found'})
def update_journal_entry(data, index):
    """Update a journal entry"""
    entry = Entry.query.filter_by(id=index).first_or_404()
    entry.update(data['entry'])
    database.session.add(entry)
    database.session.commit()
    return entry

A update_journal_entry()função de visualização agora tenta recuperar a entrada de diário especificada:

entry = Entry.query.filter_by(id=index).first_or_404()

Se a entrada de diário existir, a entrada é atualizada com o novo texto e salva no banco de dados.

Por fim, atualize delete_journal_entry():

@journal_api_blueprint.route('/<int:index>', methods=['DELETE'])
@other_responses({404: 'Entry not found'})
def delete_journal_entry(index):
    """Delete a journal entry"""
    entry = Entry.query.filter_by(id=index).first_or_404()
    database.session.delete(entry)
    database.session.commit()
    return '', 204

Se a entrada de diário especificada for encontrada, ela será excluída do banco de dados.

Execute o servidor de desenvolvimento. Teste cada um dos endpoints para garantir que eles ainda funcionem.

Manipulação de erros

Como este projeto Flask é uma API, os códigos de erro devem ser retornados no formato JSON em vez do formato HTML típico.

No projeto Flask, isso pode ser feito usando um manipulador de erros personalizado. Em project/__init__.py , defina uma nova função ( register_error_handlers()) na parte inferior do arquivo:

def register_error_handlers(app):
    @app.errorhandler(HTTPException)
    def handle_http_exception(e):
        """Return JSON instead of HTML for HTTP errors."""
        # Start with the correct headers and status code from the error
        response = e.get_response()
        # Replace the body with JSON
        response.data = json.dumps({
            'code': e.code,
            'name': e.name,
            'description': e.description,
        })
        response.content_type = 'application/json'
        return response

Essa função registra um novo manipulador de erros para quando um HTTPExceptioné gerado para converter a saída no formato JSON.

Adicione a importação:

from werkzeug.exceptions import HTTPException

Além disso, atualize a função Application Factory, create_app(), para chamar essa nova função:

def create_app():
    # Create the Flask application
    app = Flask(__name__)

    # Configure the API documentation
    app.config['APIFAIRY_TITLE'] = 'Flask Journal API'
    app.config['APIFAIRY_VERSION'] = '0.1'
    app.config['APIFAIRY_UI'] = 'elements'

    # Configure the SQLite database (intended for development only!)
    app.config['SQLALCHEMY_DATABASE_URI'] = f"sqlite:///{os.path.join(os.getcwd(), 'instance', 'app.db')}"
    app.config['SQLALCHEMY_TRACK_MODIFICATIONS'] = False

    initialize_extensions(app)
    register_blueprints(app)
    register_error_handlers(app)  # NEW!!

    return app

Autenticação

Autenticação é o processo de validação da identidade de um usuário que tenta acessar um sistema, que neste caso é a API.

A autorização, por outro lado, é o processo de verificar a quais recursos específicos um usuário específico deve ter acesso.

APIFairy utiliza Flask-HTTPAuth para suporte de autenticação. Neste tutorial, usaremos o Flask-HTTPAuth de duas maneiras:

  1. Autenticação básica - usada para gerar um token com base no e-mail/senha do usuário
  2. Autenticação de token - usada para autenticar o usuário em todos os outros endpoints da API

A autenticação de token usada via Flask-HTTPAuth é frequentemente chamada de autenticação de portador, pois o processo invoca a concessão de acesso ao "portador" do token. O token deve ser incluído nos cabeçalhos HTTP no cabeçalho Authorization, como "Authorization: Bearer ".

O diagrama a seguir ilustra um fluxo típico de como um novo usuário interage com o aplicativo para recuperar um token de autenticação:

Diagrama de fluxo da API do Flask Journal

Configuração

Como o Flask-HTTPAuth já estava instalado quando o APIFairy foi instalado no início deste tutorial, precisamos apenas configurá-lo no arquivo project/__init__.py .

Comece criando objetos separados para a autenticação básica e de token:

...

import os

from apifairy import APIFairy
from flask import Flask, json
from flask_httpauth import HTTPBasicAuth, HTTPTokenAuth  # NEW!!
from flask_marshmallow import Marshmallow
from flask_sqlalchemy import SQLAlchemy
from werkzeug.exceptions import HTTPException


# -------------
# Configuration
# -------------

# Create the instances of the Flask extensions in the global scope,
# but without any arguments passed in. These instances are not
# attached to the Flask application at this point.
apifairy = APIFairy()
ma = Marshmallow()
database = SQLAlchemy()
basic_auth = HTTPBasicAuth()  # NEW!!
token_auth = HTTPTokenAuth()  # NEW!!

...

Nenhuma atualização adicional é necessária em project/__init__.py .

Modelo de banco de dados

Em project/models.py , um novo Usermodelo precisa ser criado para representar um usuário:

class User(database.Model):
    __tablename__ = 'users'

    id = database.Column(database.Integer, primary_key=True)
    email = database.Column(database.String, unique=True, nullable=False)
    password_hashed = database.Column(database.String(128), nullable=False)
    entries = database.relationship('Entry', backref='user', lazy='dynamic')
    auth_token = database.Column(database.String(64), index=True)
    auth_token_expiration = database.Column(database.DateTime)

    def __init__(self, email: str, password_plaintext: str):
        """Create a new User object."""
        self.email = email
        self.password_hashed = self._generate_password_hash(password_plaintext)

    def is_password_correct(self, password_plaintext: str):
        return check_password_hash(self.password_hashed, password_plaintext)

    def set_password(self, password_plaintext: str):
        self.password_hashed = self._generate_password_hash(password_plaintext)

    @staticmethod
    def _generate_password_hash(password_plaintext):
        return generate_password_hash(password_plaintext)

    def generate_auth_token(self):
        self.auth_token = secrets.token_urlsafe()
        self.auth_token_expiration = datetime.utcnow() + timedelta(minutes=60)
        return self.auth_token

    @staticmethod
    def verify_auth_token(auth_token):
        user = User.query.filter_by(auth_token=auth_token).first()
        if user and user.auth_token_expiration > datetime.utcnow():
            return user

    def revoke_auth_token(self):
        self.auth_token_expiration = datetime.utcnow()

    def __repr__(self):
        return f'<User: {self.email}>'

Adicione as importações:

import secrets
from datetime import datetime, timedelta

from werkzeug.security import check_password_hash, generate_password_hash

O Usermodelo usa werkzeug.securitypara fazer o hash da senha do usuário antes de armazená-la no banco de dados.

Lembre-se: Nunca armazene a senha de texto simples em um banco de dados!

O Usermodelo usa secretspara gerar um token de autenticação para um usuário específico. Este token é criado no generate_auth_token()método e inclui uma data/hora de expiração de 60 minutos no futuro:

def generate_auth_token(self):
    self.auth_token = secrets.token_urlsafe()
    self.auth_token_expiration = datetime.utcnow() + timedelta(minutes=60)
    return self.auth_token

Existe um método estático, verify_auth_token(), que é usado para verificar o token de autenticação (considerando o tempo de expiração) e retornar o usuário de um token válido:

@staticmethod
def verify_auth_token(auth_token):
    user = User.query.filter_by(auth_token=auth_token).first()
    if user and user.auth_token_expiration > datetime.utcnow():
        return user

Mais um método de interesse é revoke_auth_token(), que é usado para revogar o token de autenticação para um usuário específico:

def revoke_auth_token(self):
    self.auth_token_expiration = datetime.utcnow()

Modelo de entrada

Para estabelecer o relacionamento um para muitos entre o usuário ("um") e suas entradas ("muitos"), o Entrymodelo precisa ser atualizado para vincular as tabelas e entries:users

class Entry(database.Model):
    """Class that represents a journal entry."""
    __tablename__ = 'entries'

    id = database.Column(database.Integer, primary_key=True)
    entry = database.Column(database.String, nullable=False)
    user_id = database.Column(database.Integer, database.ForeignKey('users.id'))  # <-- NEW!!

    def __init__(self, entry: str):
        self.entry = entry

    def update(self, entry: str):
        self.entry = entry

    def __repr__(self):
        return f'<Entry: {self.entry}>'

O Usermodelo já contém o link de volta para a entriestabela:

entries = database.relationship('Entry', backref='user', lazy='dynamic')

Modelo de API de usuários

A funcionalidade de gerenciamento de usuários do projeto Flask será definida em um Blueprint separado chamado users_api_blueprint.

Comece criando um novo diretório em "project" chamado "users_api". Dentro desse diretório, crie um arquivo __init__.py :

from flask import Blueprint


users_api_blueprint = Blueprint('users_api', __name__)

from . import authentication, routes

Este novo Blueprint precisa ser registrado com o Flask appem projects/__init__.py dentro da register_blueprints()função:

def register_blueprints(app):
    # Import the blueprints
    from project.journal_api import journal_api_blueprint
    from project.users_api import users_api_blueprint  # NEW!!

    # Since the application instance is now created, register each Blueprint
    # with the Flask application instance (app)
    app.register_blueprint(journal_api_blueprint, url_prefix='/journal')
    app.register_blueprint(users_api_blueprint, url_prefix='/users')  # NEW!!

Funções de autenticação

Para usar o Flask-HTTPAuth, várias funções precisam ser definidas para lidar com a verificação das credenciais do usuário.

Crie um novo arquivo project/users_api/authentication.py para lidar com a autenticação básica e a autenticação de token.

Para a autenticação básica (verificar o e-mail e a senha de um usuário):

from werkzeug.exceptions import Forbidden, Unauthorized

from project import basic_auth, token_auth
from project.models import User


@basic_auth.verify_password
def verify_password(email, password):
    user = User.query.filter_by(email=email).first()
    if user is None:
        return None

    if user.is_password_correct(password):
        return user


@basic_auth.error_handler
def basic_auth_error(status=401):
    error = (Forbidden if status == 403 else Unauthorized)()
    return {
        'code': error.code,
        'message': error.name,
        'description': error.description,
    }, error.code, {'WWW-Authenticate': 'Form'}

A verify_password()função é usada para verificar se um usuário existe e se sua senha está correta. Esta função será usada pelo Flask-HTTPAuth para verificar a senha quando a autenticação básica for necessária (graças ao @basic_auth.verify_passworddecorador).

Além disso, um manipulador de erros é definido para a autenticação básica que retorna informações sobre o erro no formato JSON.

Para a autenticação do token (processando um token para determinar se o usuário é válido):

@token_auth.verify_token
def verify_token(auth_token):
    return User.verify_auth_token(auth_token)


@token_auth.error_handler
def token_auth_error(status=401):
    error = (Forbidden if status == 403 else Unauthorized)()
    return {
        'code': error.code,
        'message': error.name,
        'description': error.description,
    }, error.code

A verify_token()função é usada para verificar se um token de autenticação é válido. Esta função será usada pelo Flask-HTTPAuth para verificar o token quando a autenticação do token for necessária (graças ao @token_auth.verify_tokendecorador).

Além disso, um manipulador de erros é definido para a autenticação de token que retorna informações sobre o erro no formato JSON.

Rotas de usuários

No users_api_blueprint, haverá duas rotas:

  1. Cadastrando um novo usuário
  2. Recuperando um token de autenticação

Para começar, um novo conjunto de esquemas (usando marshmallow) precisa ser definido em projects/users_api/routes.py :

from project import ma

from . import users_api_blueprint

# -------
# Schemas
# -------

class NewUserSchema(ma.Schema):
    """Schema defining the attributes when creating a new user."""
    email = ma.String()
    password_plaintext = ma.String()


class UserSchema(ma.Schema):
    """Schema defining the attributes of a user."""
    id = ma.Integer()
    email = ma.String()


class TokenSchema(ma.Schema):
    """Schema defining the attributes of a token."""
    token = ma.String()


new_user_schema = NewUserSchema()
user_schema = UserSchema()
token_schema = TokenSchema()

Esses esquemas serão usados ​​para definir as entradas e saídas para as funções de visualização definidas neste arquivo.

Cadastrando um novo usuário

Em seguida, defina a função de visualização para registrar um novo usuário:

@users_api_blueprint.route('/', methods=['POST'])
@body(new_user_schema)
@response(user_schema, 201)
def register(kwargs):
    """Create a new user"""
    new_user = User(**kwargs)
    database.session.add(new_user)
    database.session.commit()
    return new_user

Adicione as importações:

from apifairy import authenticate, body, other_responses, response

from project import basic_auth, database, ma
from project.models import User

Esse endpoint da API usa o new_user_schemapara especificar que o email e a senha são as entradas.

OBSERVAÇÃO: como o email e a senha são enviados para esse endpoint da API, é um bom momento para lembrar que o uso de HTTP é aceitável durante o teste de desenvolvimento, mas HTTPS (seguro) sempre deve ser usado na produção.

O email e a senha (definidos como os kwargsargumentos da palavra-chave -) são então descompactados para criar um novo Userobjeto, que é salvo no banco de dados:

new_user = User(**kwargs)
database.session.add(new_user)
database.session.commit()

A saída do endpoint da API é definida por user_schema, que é o ID e o email do novo usuário.

Recuperando um token de autenticação

A outra função de visualização a ser definida em projects/users_api/routes.py é para recuperar o token de autenticação:

@users_api_blueprint.route('/get-auth-token', methods=['POST'])
@authenticate(basic_auth)
@response(token_schema)
@other_responses({401: 'Invalid username or password'})
def get_auth_token():
    """Get authentication token"""
    user = basic_auth.current_user()
    token = user.generate_auth_token()
    database.session.add(user)
    database.session.commit()
    return dict(token=token)

O @authenticatedecorador é usado pela primeira vez neste tutorial e especifica que a autenticação básica deve ser usada para proteger esta rota:

@authenticate(basic_auth)

Quando o usuário deseja recuperar seu token de autenticação, ele precisa enviar uma solicitação POST para esse endpoint da API com o email e a senha incorporados no cabeçalho 'Authorization'. Como exemplo, o seguinte comando Python usando o pacote Requests pode ser feito para este endpoint da API:

>>> import requests
>>> r = requests.post(
    'http://127.0.0.1:5000/users/get-auth-token',
    auth=('pkennedy@hey.com', 'FlaskIsAwesome123')
)

Se a autenticação básica for bem-sucedida, a função de visualização recupera o usuário atual usando o current_user()método fornecido pelo Flask-HTTPAuth:

user = basic_auth.current_user()

Um novo token de autenticação é criado para esse usuário:

token = user.generate_auth_token()

E esse token é salvo no banco de dados para que possa ser usado para autenticar o usuário no futuro (pelo menos pelos próximos 60 minutos!).

Por fim, o novo token de autenticação é retornado para o usuário salvar para todas as chamadas de API subsequentes.

Atualizações de endpoints da API

Com um processo de autenticação em vigor, é hora de adicionar alguns guardas aos terminais de API existentes para garantir que apenas usuários válidos possam acessar o aplicativo.

Essas atualizações são para as funções de visualização definidas em projects/journal_api/routes.py .

Primeiro, atualize journal()para retornar apenas as entradas de diário do usuário atual:

@journal_api_blueprint.route('/', methods=['GET'])
@authenticate(token_auth)
@response(entries_schema)
def journal():
    """Return journal entries"""
    user = token_auth.current_user()
    return Entry.query.filter_by(user_id=user.id).all()

Atualize as importações no topo da seguinte forma:

from apifairy import authenticate, body, other_responses, response
from flask import abort

from project import database, ma, token_auth
from project.models import Entry

from . import journal_api_blueprint

O @authenticatedecorador especifica que a autenticação de token precisa ser usada ao acessar esse endpoint da API. Como exemplo, a seguinte solicitação GET pode ser feita usando Requests ( após a recuperação do token de autenticação ):

>>> import requests
>>> headers = {'Authorization': f'Bearer {auth_token}'}
>>> r = requests.get('http://127.0.0.1:5000/journal/', headers=headers)

Depois que o usuário é autenticado, a lista completa de entradas de diário é recuperada do banco de dados com base no ID do usuário:

user = token_auth.current_user()
return Entry.query.filter_by(user_id=user.id).all()

A saída desse endpoint da API é definida pelo @responsedecorador, que é uma lista de entradas de diário (ID, entrada, ID do usuário).

A seguir, atualize add_journal_entry():

@journal_api_blueprint.route('/', methods=['POST'])
@authenticate(token_auth)
@body(new_entry_schema)
@response(entry_schema, 201)
def add_journal_entry(kwargs):
    """Add a new journal entry"""
    user = token_auth.current_user()
    new_message = Entry(user_id=user.id, **kwargs)
    database.session.add(new_message)
    database.session.commit()
    return new_message

Assim como na função de visualização anterior, o @authenticatedecorador é usado para especificar que a autenticação de token precisa ser usada ao acessar esse endpoint da API. Além disso, a entrada de diário agora é adicionada especificando o ID do usuário que deve ser associado à entrada de diário:

user = token_auth.current_user()
new_message = Entry(user_id=user.id, **kwargs)

A nova entrada de diário é salva no banco de dados e a entrada de diário é retornada (conforme definido pelo @responsedecorador).

A seguir, atualize get_journal_entry():

@journal_api_blueprint.route('/<int:index>', methods=['GET'])
@authenticate(token_auth)
@response(entry_schema)
@other_responses({403: 'Forbidden', 404: 'Entry not found'})
def get_journal_entry(index):
    """Return a journal entry"""
    user = token_auth.current_user()
    entry = Entry.query.filter_by(id=index).first_or_404()

    if entry.user_id != user.id:
        abort(403)
    return entry

O @authenticatedecorador é adicionado para especificar que a autenticação de token é necessária para acessar esse endpoint da API.

Ao tentar recuperar uma entrada de diário, uma verificação adicional é adicionada para garantir que o usuário que está tentando acessar a entrada de diário seja o "proprietário" real da entrada. Caso contrário, um código de erro 403 (Proibido) é retornado por meio da abort()função do Flask:

if entry.user_id != user.id:
        abort(403)

Observe que este endpoint de API tem duas respostas fora do nominal especificadas pelo @other_responsesdecorador:

@other_responses({403: 'Forbidden', 404: 'Entry not found'})

Lembrete: O @other_responsesdecorador serve apenas para documentação; é responsabilidade da função view gerar esses erros.

A seguir, atualize update_journal_entry():

@journal_api_blueprint.route('/<int:index>', methods=['PUT'])
@authenticate(token_auth)
@body(new_entry_schema)
@response(entry_schema)
@other_responses({403: 'Forbidden', 404: 'Entry not found'})
def update_journal_entry(data, index):
    """Update a journal entry"""
    user = token_auth.current_user()
    entry = Entry.query.filter_by(id=index).first_or_404()

    if entry.user_id != user.id:
        abort(403)

    entry.update(data['entry'])
    database.session.add(entry)
    database.session.commit()
    return entry

As atualizações desta função de visualização são semelhantes às outras funções de visualização nesta seção:

  1. @authenticatedecorador especifica que a autenticação de token é necessária para acessar este endpoint da API
  2. Somente o usuário que "possui" o lançamento contábil manual tem permissão para atualizar o lançamento (caso contrário, 403 (Proibido))

Por fim, atualize delete_journal_entry():

@journal_api_blueprint.route('/<int:index>', methods=['DELETE'])
@authenticate(token_auth)
@other_responses({403: 'Forbidden', 404: 'Entry not found'})
def delete_journal_entry(index):
    """Delete a journal entry"""
    user = token_auth.current_user()
    entry = Entry.query.filter_by(id=index).first_or_404()

    if entry.user_id != user.id:
        abort(403)

    database.session.delete(entry)
    database.session.commit()
    return '', 204

Conclusão

Este tutorial forneceu um passo a passo de como construir uma API de maneira fácil e rápida no Flask usando APIFairy.

Os decoradores são a chave para definir os endpoints da API:

  • Entradas :
    • @arguments- argumentos de entrada da string de consulta da URL
    • @body- estrutura da solicitação JSON
  • Saídas :
    • @response- estrutura da resposta JSON
  • Autenticação :
    • @authenticate- abordagem de autenticação usando Flask-HTTPAuth
  • Erros :
    • @other_responses- respostas fora do nominal, como códigos de erro HTTP

Além disso, a documentação da API gerada pelo APIFairy é excelente e fornece informações importantes para os usuários do aplicativo.

Fonte:  https://testdrive.io

#api #flask 

Como Criar Facilmente Uma API RESTful Com Flask E APIFairy
Best of Crypto

Best of Crypto

1656680100

Postman Collection for Binance Pay API

Binance Pay API Postman

Postman is an API Collaboration Platform.

Binance Pay now offers Postman Collection and Environment (JSON files) for a quicker and easier usage of our RESTful APIs.
You only need to import and set up with your own API and secret keys to begin.

How to import and configure

  • Download the binance-pay-postman-collection repository.
  • Click the Import button. On Postman for Mac, for example, the button is at the top left:

Screenshot of Postman for Mac, with 'Import' button pointed out at top left.

  • On the Import pop-up page, select the Folder tab. Click the Choose folder from your computer button and choose the root folder of the downloaded repository.

Screenshot of of Postman for Mac, showing the Import screen.

  • Select which collections and environments you would like to import and click the Import button.

Screenshot of of Postman for Mac, showing the Import screen after selecting the folder.

  • Select the Environments tab on the left, choose an environment, and set your Api Key and Secret Key by changing the Current Value column (see screenshot); (The Timestamp, Signature, nonce fields can be left empty as they’ll be automatically filled by Postman when sending a request.)

Screenshot of Postman for Mac, showing where the user should fill in their API and secret keys.

  • Select your newly-added environment from the environment dropdown menu. On Mac, this is at top right, to the left of the eye icon.

Screenshot of Postman for Mac, showing how imported environments can be selected from a dropdown .

Postman safety practices

The following practices are advised to secure your account's safety:

  • Don't use Collections obtained from an unknown source.
  • Review the environment JSON file before its usage.
  • Don't use any code that you don't understand.
  • Make sure that the withdrawal permission is not enabled for your API keys.
  • When you finish trying out the API, delete your API keys.

FAQ

Q: Why I can't get any response?

You haven't imported the environment file or you've imported it but haven't selected it from the dropdown menu (mentioned in [[How to import and configure]])

Q: How can I debug a request or find the used URL?

  • Open the Postman's console to find requests' parameters and URL.
  • Debugging can be done by editing the Pre-request Script tab.

Q: Error Mandatory parameter 'xxxx' was not sent, was empty/null, or malformed.

Likely causes:

  • API key is not set.
  • API key is not correct.
  • BinancePay-Certificate-SN is not selected in your Postman Headers tab.

Q: Error Signature for this request is not valid.

Likely causes:

  • Secret key is not set.
  • Request was made with at least one empty parameter.
  • signature is not the last parameter in the parameters list.

Q: Error A mandatory parameter was not sent, was empty/null, or malformed.

Please refer to the API documentation to double check all the mandatory parameters.

My question isn't here

If you don't find your answer here, please consult https://dev.binance.vision/ for similar questions from the community or open an issue here.

Download Details:
Author: binance
Source Code: https://github.com/binance/binance-pay-postman-collection
License: MIT license

#Binance #blockchain #postman #api

Postman Collection for Binance Pay API
Best of Crypto

Best of Crypto

1656665400

Binance Pay Public API Connector in Python

This is a lightweight library that works as a connector to Binance Pay public API

Installation

pip install binance-pay-connector

Documentation

https://binance-pay-connector-python.readthedocs.io

RESTful APIs

Usage examples:

from binance.pay.merchant import Merchant as Client


# Setup merchant API from https://merchant.binance.com/en/dashboard/developers

client = Client(key='<api_key>', secret='<api_secret>')
response = client.get_order(merchantTradeNo="<trade_no>")

# Get an order details
print(response)

Please find examples folder to check for more endpoints.

Optional parameters

PEP8 suggests lowercase with words separated by underscores, but for this connector, the methods' optional parameters should follow their exact naming as in the API documentation.

# Recognised parameter name
response = client.get_order(merchantTradeNo="2223")

# Unrecognised parameter name
response = client.get_order(merchant_trade_no="2223")

Display logs

Setting the log level to DEBUG will log the request URL, payload and response text.

Error

If a request has a parameter that is not provided but required from server, this library will throw an exception binance.pay.error.ParameterRequiredError, except the endpoint that is for creating order. POST /binancepay/openapi/v2/order that used to create order has complicate parameter strucuture, the library doesn't any mandatory parameter. Please see the example file for how to place an order.

Test Case

# In case packages are not installed yet
pip install -r requirements/requirements-test.txt

pytest

Contributing

Contributions are welcome.
If you've found a bug within this project, please open an issue to discuss what you would like to change.
If it's an issue with the API, please open a topic at Binance Developer Community

Download Details:
Author: binance
Source Code: https://github.com/binance/binance-pay-connector-python
License:

#Binance #blockchain #python #api

Binance Pay Public API Connector in Python
Best of Crypto

Best of Crypto

1656643320

Binance CLI: A Simple CLI That interacts with The Binance API

Binance CLI

A simple CLI that interacts with the Binance API

Installation


# download the code
git clone git@github.com:binance/binance-cli.git
cd binance-cli
npm install -g

Usage


# Display help command
binance-cli -h

Market Data

get server time

binance-cli t
binance-cli time

get exchange infomation


binance-cli i
# get BTCUSDT pair's filters. jq is required.
binance-cli i | jq '.symbols[] | select(.symbol == "BNBUSDT") |.filters'

get order book

# binance-cli book <symbol>
# binance-cli book -l <limit> <symbol>
binance-cli book bnbusdt
binance-cli book --limit 10 bnbusdt

get trades

# binance-cli t <symbol>
binance-cli t bnbusdt
binance-cli t -l 10 bnbusdt

get aggregate Trades List

# binance-cli at <symbol>
binance-cli at bnbusdt

# get aggregate Trades List with parameters
# binance-cli at <symbol>
binance-cli at --limit 10 --startTime 1595937694913 --endTime 1595937794913 bnbusdt

get klines data

# binance-cli k <symbol> <interval>
binance-cli k bnbusdt 1m
binance-cli k -l 1 bnbusdt 1m

get average price

# binance-cli ap <symbol>
binance-cli ap bnbusdt

get 24hr ticker

# binance-cli ticker -s <symbol>
binance-cli ticker -s bnbusdt

get ticker price

binance-cli price
# or with a symbol
binance-cli price -s bnbusdt

get order book ticker

# binance-cli bt
# binance-cli bt -s <symbol>
binance-cli bt -s bnbusdt

listen to streams


# binance-cli listen <stream> <stream> <listenKey>
binance-cli listen bnbusdt@depth bnbusdt@bookTicker

User data and trade

It's required to set the API key and secret

export BINANCE_API_KEY=<the_api_key>
export BINANCE_API_SECRET=<the_api_secret>

buy

# place a limit buy order on BNBUSDT with price=350 and qty=0.05
binance-cli buy -s BNBUSDT -t LIMIT -q 0.05 -p 350 -f GTC

sell

# place a limit sell order on BNBUSDT with price=500 and qty=0.03
binance-cli sell -s bnbusdt -t limit -q 0.03 -p 500 -f GTC

get order details

binance-cli get bnbusdt -i 12345
binance-cli get bnbusdt -c my_order_123

cancel an order

binance-cli cancel bnbusdt -i 12345
binance-cli cancel bnbusdt -c my_order_123

cancel all open orders

binance-cli cancel_all bnbusdt

Use --help to consult all the available commands

binance-cli --help

Download Details:
Author: binance
Source Code: https://github.com/binance/binance-cli
License:

#Binance #blockchain #api #javascript

Binance CLI: A Simple CLI That interacts with The Binance API

Gérer L'état De L'API REST Globalement Dans React js

React Js Handle Rest API data globalement avec le tutoriel Context et useState hook. Dans ce didacticiel complet, vous apprendrez des techniques simples et faciles pour envoyer les données de l'API REST du composant parent aux composants enfants ou dans l'arborescence des composants profonds de l'application React js à l'aide du crochet de contexte et du crochet useState.

De plus, nous vous montrerons également comment installer et utiliser la bibliothèque Axios pour effectuer la requête HTTP Get afin de récupérer la réponse de données du serveur.

Nous allons créer le fournisseur de contexte, définir les valeurs ou les données dans le fournisseur de contexte et transmettre les valeurs du composant parent ou d'ordre supérieur aux composants enfants.

Les états mondiaux sont censés avoir un impact sur les applications Web à grande échelle ; vous gérez rarement des états qui se répètent et qui peuvent être nécessaires dans diverses situations. Il n'est pas considéré comme la meilleure politique d'écrire un code à plusieurs reprises, et cela consomme à la fois de la mémoire et du temps.

Comment gérer l'état de l'API REST globalement dans React Js à l'aide de Context et useState Hook

  • Étape 1 : Installer l'application React
  • Étape 2 : Installer les modules Bootstrap et Axios
  • Étape 3 : Créer un fichier de contexte
  • Étape 4 : Créer un fournisseur de contexte
  • Étape 5 : Ajouter un fournisseur sur le composant parent
  • Étape 6 : Accéder à l'état global dans le composant enfant
  • Étape 7 : Démarrez l'application React

Installer l'application React

La première étape consiste à installer une toute nouvelle application React à l'aide de la commande donnée.

npx create-react-app react-global-state-rest-api

Ensuite, entrez dans le dossier du projet.

cd react-global-state-rest-api

Installer les modules Bootstrap et Axios

Utilisez la commande suivante pour installer ensemble les modules Axios et Bootstrap.

Vous pouvez exécuter la commande donnée pour installer les packages.

npm install axios bootstrap

Créer un fichier de contexte

Créez le répertoire contexts/ , à l'intérieur vous devez créer un nouveau fichier et le nommer ApiContext.js puis y insérer le code donné.

import { createContext } from 'react'
export const ApiContext = createContext([])

Créer un fournisseur de contexte

Dans le dossier contexts , créez à nouveau un nouveau fichier, nommez-le Store.js et insérez-y le code donné.

import React, { useState, useEffect } from 'react'
import { ApiContext } from '../contexts/ApiContext'
import axios from 'axios'
function Store({ children }) {
  const [users, setUsers] = useState([])
  useEffect(() => {
    axios
      .get('https://jsonplaceholder.typicode.com/users')
      .then((res) => {
        setUsers(res.data)
      })
      .catch((error) => {
        console.log(error)
      })
  }, [])
  return (
    <ApiContext.Provider value={[users, setUsers]}>
      {children}
    </ApiContext.Provider>
  )
}
export default Store

Nous avons importé l'ApiContext, nous l'utiliserons pour définir ApiContext.Provideroù nous devons transmettre les données que nous avons obtenues à l'aide de l'API REST et de useState.

Ajouter un fournisseur sur le composant parent

Pour partager les données globalement ou dans des composants enfants, importez d'abord le magasin que nous avons créé à l'aide d'ApiContext.

Faites du magasin le composant parent de cette façon ; tous les composants définis sous ce fournisseur pourront obtenir les données globales de l'API REST.

Ouvrez le fichier App.js et insérez ce qui suit dans le fichier.

import React from 'react'
import 'bootstrap/dist/css/bootstrap.min.css'
import './App.css'
import Users from './components/Users'
import Store from './contexts/Store'
function App() {
  return (
    <div className="container mt-5">
      <Store>
        <Users />
      </Store>
    </div>
  )
}
export default App

Accéder à l'état global dans le composant enfant

Créez le répertoire des composants , puis créez le fichier Users.js , assurez-vous de coller le code donné dans le fichier.

import React, { useContext, useEffect, useState } from 'react'
import { ApiContext } from '../contexts/ApiContext'
function Users() {
  const [users, setUsers] = useContext(ApiContext)
  useEffect(() => {
    console.log(users)
  }, [users])
  return ( 
    <div>
      <h2 className="mb-4">
        React Handle REST API Global State with Context Hook Example
      </h2>
      {users.map((res, idx) => {
        return (
          <div className="card mb-2" key={idx}>
            <div className="card-body">
              <h5 className="card-title">{res.name}</h5>
              <h6 className="card-subtitle mb-2 text-muted">{res.username}</h6>
              <p className="card-text">{res.company.catchPhrase}</p>
              <a href="#" className="card-link">
                {res.website}
              </a>
            </div>
          </div>
        )
      })}
    </div>
  )
}
export default Users

Importez le crochet useContext à partir de la bibliothèque React et importez l'ApiContext à partir du répertoire de contexte. Utilisez les valeurs de la prop de déstructuration useContext que nous avons définies dans le fournisseur de contexte.

Le hook useEffect est utilisé pour effectuer l'effet secondaire ; comme vous pouvez le voir, nous avons passé la propriété users dans le symbole du tableau car dès qu'un changement se produit dans cette valeur, elle sera mise à jour dans le composant Users.

Nous affichons les données de l'API REST dans le code HTML de réaction à l'aide de Bootstrap que nous avons défini globalement.

Lancer l'application React

En fin de compte, vous êtes prêt à exécuter la commande suggérée pour exécuter l'application.

npm start

Vous pouvez voir l'application sur le navigateur en utilisant l'URL donnée.

http://localhost:3000

React Gérer l'état de l'API REST globalement avec le didacticiel de l'API contextuelle

Conclusion

Dans ce guide, vous avez appris à partager l'état global de l'API REST dans les composants React ; en plus de cela, nous avons également expliqué comment accéder à l'état global dans les composants React.

Pour cela, nous récupérons l'état global dans le composant enfant et affichons la réponse ou les données de l'API dans la vue de liste HTML de React à l'aide de la boucle for et de Bootstrap 5.

 Source : https://www.positronx.io/react-manage-rest-api-state-globally-with-context-api-tutorial/

#react #api 

Gérer L'état De L'API REST Globalement Dans React js
Trung  Nguyen

Trung Nguyen

1656636240

Xử Lý Trạng Thái API REST Trên Toàn Cầu Bằng Context Và UseState

React Js Xử lý dữ liệu API phần còn lại trên toàn cầu với hướng dẫn hook Context và useState. Trong hướng dẫn toàn diện này, bạn sẽ học các kỹ thuật đơn giản và dễ dàng về cách gửi dữ liệu API REST từ thành phần mẹ đến các thành phần con hoặc trong cây thành phần sâu trong ứng dụng React js bằng cách sử dụng ngữ cảnh hook và useState hook.

Hơn nữa, chúng tôi cũng sẽ hướng dẫn bạn cách cài đặt và sử dụng thư viện Axios để thực hiện yêu cầu HTTP Get để tìm nạp phản hồi dữ liệu từ máy chủ.

Chúng tôi sẽ tạo trình cung cấp ngữ cảnh, đặt các giá trị hoặc dữ liệu trong trình cung cấp ngữ cảnh và chuyển các giá trị từ thành phần bậc cha hoặc cấp cao hơn cho các thành phần con.

Các trạng thái toàn cầu có tác động đến các ứng dụng web quy mô lớn; hiếm khi bạn quản lý các trạng thái được lặp lại và có thể cần thiết trong các tình huống khác nhau. Nó không được coi là chính sách tốt nhất để viết một đoạn mã lặp đi lặp lại, và nó tiêu tốn cả bộ nhớ và thời gian.

Cách xử lý trạng thái API REST trên toàn cầu trong React Js bằng Context và useState Hook

  • Bước 1: Cài đặt ứng dụng React
  • Bước 2: Cài đặt các mô-đun Bootstrap & Axios
  • Bước 3: Tạo tệp ngữ cảnh
  • Bước 4: Xây dựng Trình cung cấp ngữ cảnh
  • Bước 5: Thêm nhà cung cấp trên thành phần chính
  • Bước 6: Truy cập Trạng thái toàn cầu trong Thành phần con
  • Bước 7: Khởi động ứng dụng React

Cài đặt ứng dụng React

Bước đầu tiên là cài đặt một ứng dụng React hoàn toàn mới bằng cách sử dụng lệnh đã cho.

npx create-react-app react-global-state-rest-api

Tiếp theo, vào thư mục dự án.

cd react-global-state-rest-api

Cài đặt các mô-đun Bootstrap & Axios

Sử dụng lệnh sau để cài đặt hoàn toàn các mô-đun Axios và Bootstrap.

Bạn có thể chạy lệnh đã cho để cài đặt các gói.

npm install axios bootstrap

Tạo tệp ngữ cảnh

Tạo bối cảnh / thư mục , bên trong ở đây bạn phải tạo một tệp mới và đặt tên là ApiContext.js sau đó chèn đoạn mã đã cho vào đó.

import { createContext } from 'react'
export const ApiContext = createContext([])

Xây dựng nhà cung cấp ngữ cảnh

Bên trong thư mục ngữ cảnh , lại tạo một tệp mới mà bạn phải đặt tên là Store.js và chèn mã đã cho vào đó.

import React, { useState, useEffect } from 'react'
import { ApiContext } from '../contexts/ApiContext'
import axios from 'axios'
function Store({ children }) {
  const [users, setUsers] = useState([])
  useEffect(() => {
    axios
      .get('https://jsonplaceholder.typicode.com/users')
      .then((res) => {
        setUsers(res.data)
      })
      .catch((error) => {
        console.log(error)
      })
  }, [])
  return (
    <ApiContext.Provider value={[users, setUsers]}>
      {children}
    </ApiContext.Provider>
  )
}
export default Store

Chúng tôi đã nhập ApiContext mà chúng tôi sẽ sử dụng nó để xác định ApiContext.Providernơi chúng tôi phải chuyển dữ liệu mà chúng tôi có bằng cách sử dụng API REST và useState.

Thêm nhà cung cấp trên thành phần chính

Để chia sẻ dữ liệu trên toàn cầu hoặc trong các thành phần con, trước tiên hãy nhập Cửa hàng mà chúng tôi đã tạo bằng ApiContext.

Đặt Store trở thành thành phần mẹ theo cách này; tất cả các thành phần được xác định trong nhà cung cấp này sẽ có thể nhận được dữ liệu API REST toàn cầu.

Mở tệp App.js và chèn phần sau vào tệp.

import React from 'react'
import 'bootstrap/dist/css/bootstrap.min.css'
import './App.css'
import Users from './components/Users'
import Store from './contexts/Store'
function App() {
  return (
    <div className="container mt-5">
      <Store>
        <Users />
      </Store>
    </div>
  )
}
export default App

Truy cập trạng thái toàn cầu trong thành phần con

Tạo thư mục thành phần , sau đó tạo tệp Users.js , đảm bảo dán mã đã cho vào tệp.

import React, { useContext, useEffect, useState } from 'react'
import { ApiContext } from '../contexts/ApiContext'
function Users() {
  const [users, setUsers] = useContext(ApiContext)
  useEffect(() => {
    console.log(users)
  }, [users])
  return ( 
    <div>
      <h2 className="mb-4">
        React Handle REST API Global State with Context Hook Example
      </h2>
      {users.map((res, idx) => {
        return (
          <div className="card mb-2" key={idx}>
            <div className="card-body">
              <h5 className="card-title">{res.name}</h5>
              <h6 className="card-subtitle mb-2 text-muted">{res.username}</h6>
              <p className="card-text">{res.company.catchPhrase}</p>
              <a href="#" className="card-link">
                {res.website}
              </a>
            </div>
          </div>
        )
      })}
    </div>
  )
}
export default Users

Nhập hook useContext từ thư viện react và nhập ApiContext từ thư mục ngữ cảnh. Sử dụng các giá trị trong phần mềm hỗ trợ hủy cấu trúc useContext mà chúng tôi đã xác định trong trình cung cấp ngữ cảnh.

Móc useEffect đang được sử dụng để thực hiện hiệu ứng phụ; như bạn có thể thấy, chúng tôi đã chuyển các hỗ trợ người dùng trong biểu tượng mảng vì ngay sau khi bất kỳ thay đổi nào xảy ra trong giá trị này, nó sẽ được cập nhật trong thành phần Người dùng.

Chúng tôi đang hiển thị dữ liệu API REST trong HTML phản ứng bằng Bootstrap mà chúng tôi đặt trên toàn cầu.

Khởi động ứng dụng React

Cuối cùng, bạn đã sẵn sàng chạy lệnh được đề xuất để chạy ứng dụng.

npm start

Bạn có thể xem ứng dụng trên trình duyệt bằng cách sử dụng url đã cho.

http://localhost:3000

React Quản lý trạng thái API REST trên toàn cầu với Hướng dẫn về API ngữ cảnh

Sự kết luận

Trong hướng dẫn này, bạn đã học cách chia sẻ trạng thái chung của REST API trong các thành phần React; trên hết, chúng tôi cũng đã giải thích cách truy cập trạng thái toàn cục trong các thành phần React.

Đối với điều đó, chúng tôi truy xuất trạng thái toàn cục trong thành phần con và hiển thị phản hồi API hoặc dữ liệu trong chế độ xem danh sách HTML của React bằng cách sử dụng vòng lặp for và Bootstrap 5.

Nguồn: https://www.positronx.io/react-manage-rest-api-state-globally-with-context-api-tutorial/

#react  #api 

Xử Lý Trạng Thái API REST Trên Toàn Cầu Bằng Context Và UseState