This is the twenty-third part of the Mega-Tutorial, in which I will tell you how to expand microblogging using the application programming interface (or API), which customers can use to work with the application in a more direct way than the traditional web browser workflow.
Under the spoiler is a list of all articles in the 2018 series.
Note 1: If you are looking for old versions of this course, this is here .
All the functionality I have built so far for this application is designed for one specific type of client: a web browser. But what about other types of customers? For example, if I wanted to create an application for Android or iOS, I have two main ways to solve it. The simplest solution would be to create an application using a web component that fills the entire screen and loads the Microblog website, but this will not be qualitatively better than opening the application in the device’s web browser. The best solution (albeit much more time consuming) would be to create your own application, but how can this application interact with a server that returns only HTML pages?
This is a problem area in which Application Programming Interfaces (or APIs) can help. The API is a collection of HTTP routes that are developed as low-level entry points to the application. Instead of defining routes and viewing the functions that return HTML to be used by web browsers, the API allows the client to work directly with application resources, leaving the decision on how to present the information to the user completely to the client. For example, a microblog API can provide a client with user information and blog entries, and also allow the user to edit an existing blog entry, but only at the data level, without mixing this logic with HTML.
If you examine all the routes currently defined in the application, you will notice that there are several that may correspond to the API definition that I used above. Did you find them? I'm talking about several routes that return JSON, such as the / translate route defined in chapter 14 . This is the route that accepts the text, source and destination languages, all data in JSON format in the POST
request. The answer to this request is the translation of this text, also in JSON format. The server returns only the requested information, leaving the client responsible to present this information to the user.
While the JSON routes in the application have an API to "feel" to them, they were designed to support a web application running in a browser.
Although the JSON routes in the application have an API, there remains a “feel” that they were designed to support a web application running in a browser. Please note that if the application for smartphones wanted to use these routes, it would not be able to, because they need a registered user, and access to the system is possible only through an HTML form. In this chapter, I will explain how to create APIs that do not rely on a web browser, and not make any assumptions about which client connects to them.
GitHub links for this chapter: Browse , Zip , Diff .
Someone may strongly disagree with my statement above that / translate and other JSON routes are API routes. Others may agree with the caveat that they consider them a poorly designed API. So, what are the characteristics of a well-developed API, and why are JSON routes outside this category?
You may have heard the term rest api. REST, which means Representational State Transfer, is the architecture proposed by Dr. Roy Fielding in his doctoral dissertation. In his work, Dr. Fielding presents the six defining characteristics of REST in a rather abstract and general form.
Other than Dr. Fielding’s thesis, there is no other authoritative REST specification, which leaves much for free interpretation by the reader. The topic of whether this API is consistent with REST or not is often a source of heated debate between REST “purists”, who believe that the REST API must comply with all six characteristics and do it in a well-defined way compared to the REST “pragmatists” who take the ideas presented by Dr. Fielding in his dissertation as guidelines or recommendations. Dr. Fielding himself took the side of the purist camp and gave some additional insight into his vision in blogs and online comments.
The vast majority of APIs currently implemented adhere to the “pragmatic” REST implementation. This includes most APIs from “big players,” such as Facebook, GitHub, Twitter, etc. There are very few public APIs that are unanimously considered to be pure REST, since most APIs skip some implementation details that purists consider mandatory. Despite the rigorous views of Dr. Fielding and other REST purists that the REST API is or is not, the software industry usually refers to REST in a pragmatic sense.
To give you an idea of ​​what's in the REST dissertation, the following sections describe the six principles listed by Dr. Fielding.
The principle of client-server is quite simple, as it simply states that in the REST API, the roles of the client and server must be clearly differentiated. In practice, this means that the client and server are in separate processes that interact through a transport, which in most cases is the HTTP protocol over the TCP network.
The Layered System principle ( multi-level system ) says that when a client needs to interact with a server, it can be associated with an intermediary, and not with an actual server. The idea is that for the client there should be absolutely no difference in how he sends requests, if not connected directly to the server, in fact he may not even know whether he is connected to the target server or not. Similarly, this principle states that a server can receive client requests from an intermediary, and not directly from a client, so it should never assume that the other side of the connection is a client.
This is an important REST feature, because the ability to add intermediate nodes allows application architects to develop large and complex networks that can satisfy a large volume of requests using a load balancer, caches, proxy servers, etc.
This principle extends the layered system, explicitly indicating that a server or an intermediary can cache responses to requests that often arrive to improve system performance. There is a cache implementation that you are probably familiar with: one in all web browsers. The web browser's cache layer is often used to avoid having to request the same files, such as images, over and over.
For API purposes, the target server must specify using the cache controls whether the response can be cached by intermediaries when it is returned to the client. Note that because, for security reasons, APIs deployed in a production environment must use encryption, caching is usually not performed at the smart host unless this node terminates the SSL connection or performs decryption and re-encryption.
This is an optional requirement indicating that the server can provide executable code in responses to the client. Since this principle requires an agreement between the server and the client about which executable code the client can execute, this is rarely used in the API. You might think that the server can return JavaScript code to launch web browsers, but REST is not specifically intended for web browser clients. For example, executing JavaScript can be difficult if the client is an iOS or Android device.
The stateless principle is one of two at the center of most debates between REST purists and pragmatists. It states that the REST API should not save any client state that will be triggered each time the client sends a request. This means that none of the mechanisms that are common in web development for “remembering” users when navigating through the pages of an application cannot be used. In a stateless API, each request must include information that the server must identify and authenticate the client and execute the request. It also means that the server cannot store data related to a client connection in a database or other form of storage.
If you are wondering why REST requires a stateless server, the main reason is that stateless servers are very easy to scale, all you need to do is run several server instances behind the load balancer. If the server stores the state of the client, the situation becomes more complicated, as you need to figure out how several servers can access and update this state, or ensure that this client is always processed by the same server, which is usually called sticky sessions.
If you again consider the / translate route discussed at the beginning of the chapter, you will realize that it cannot be considered RESTful, because the view function associated with this route relies on the @login_required
decoder from Flask-Login, which in turn stores the registered as a user in a Flask user session.
The last, most important, most discussed and most vaguely documented REST principle is a single interface. Dr. Fielding lists four distinctive aspects of a single REST interface: unique resource identifiers, resource representations, self-descriptive messages, and hypermedia.
Unique resource identifiers are obtained by assigning a unique URL to each resource. For example, the URL associated with this user could be / api / users / <user-id>, where <user-id> is the identifier assigned to the user as the primary key of the database table. This is quite acceptable implemented by most APIs.
Using resource representations means that if the server and the client exchange information about the resource, they must use the agreed Format. For most modern APIs, JSON format is used to construct resource representations. The API can support several resource presentation formats, in which case the content negotiation parameters in the HTTP protocol are the mechanism by which the client and the server can agree on a format that both like.
Self-descriptive messages mean that requests and responses exchanged between clients and the server should include all the information needed by the other party. A typical example is the HTTP request method used to specify which operation the client wants to receive from the server. A GET
indicates that the client wants to get information about the resource, a POST
request indicates that the client wants to create a new resource, PUT
or PATCH
requests determine changes to existing resources, and a DELETE
request indicates that the resource has been deleted. The target resource is specified as the URL of the request with additional information provided in the HTTP headers, part of the URL request string, or request body.
The hypermedia requirement is the most controversial of many, and one that is implemented by few APIs, and those APIs that implement it, rarely do so to satisfy REST purists. Since all the resources in an application are interconnected, it requires that links be included in resource views so that clients can discover new resources by circumventing links, much like you discover new pages in a web application by clicking links that lead you from one page to other. The idea is that the client can enter the API without any prior knowledge of the resources in it and learn about them, simply by following hypermedia links. One aspect that makes it difficult to fulfill this requirement is that, unlike HTML and XML, the json format, which is usually used to represent resources in the API, does not define a standard way to include links, so you have to use special custom structures, or one of the proposed JSON extensions that try to fill this gap, such as JSON-API , HAL , JSON-LD, or similar.
To give you an idea of ​​what is involved in API development, I'm going to add it to microblogging. It will not be a complete API, I am going to implement all the functions associated with users, leaving the implementation of other resources, such as blog posts to the reader, as an exercise.
So that everything is organized and structured in accordance with the concept described in Chapter 15 , I am going to create a new project that will contain all the API routes. So let's start by creating a directory in which this project will live:
(venv) $ mkdir app/api
Blueprint file __init __. py
__init __. py
creates a blueprint object, similar to other blueprint applications:
app/api/__init__.py:
API blueprint constructor.
from flask import Blueprint bp = Blueprint('api', __name__) from app.api import users, errors, tokens
You probably remember that sometimes it is necessary to move the import to the very bottom of the module in order to avoid cyclic dependency errors. This is the reason why the app / api / users.py , app / api / errors.py and app / api / tokens.py modules (what I have yet to write) are imported after creating the project.
The main content of the API will be stored in the app / api / users.py module . The following table lists the routes that I am going to implement:
HTTP Method | Resource URL | Notes |
---|---|---|
Get | /api/users/<id> | Returns the user. |
Get | /api/users | Returns a collection of all users. |
Get | /api/users/<id>/followers | Returns followers of this user. |
Get | /api/users/<id>/followed | Will return users subscribed by this user. |
POST | /api/users | Registers a new user account. |
PUT | /api/users/<id> | Changes user. |
The framework of the module with placeholders for all these routes will be as follows:
app/api/users.py:
User API resource placeholders.
from app.api import bp @bp.route('/users/<int:id>', methods=['GET']) def get_user(id): pass @bp.route('/users', methods=['GET']) def get_users(): pass @bp.route('/users/<int:id>/followers', methods=['GET']) def get_followers(id): pass @bp.route('/users/<int:id>/followed', methods=['GET']) def get_followed(id): pass @bp.route('/users', methods=['POST']) def create_user(): pass @bp.route('/users/<int:id>', methods=['PUT']) def update_user(id): pass
In the app / api / errors.py module, you need to define several helper functions that deal with error responses. But now, I will create a placeholder that I fill in later:
app/api/errors.py:
Error handling placeholder.
def bad_request(): pass
app / api / tokens.py module in which the authentication subsystem will be defined. This will provide an alternative login method for clients that are not web browsers. Let's write a placeholder for this module:
app/api/tokens.py:
handling.
def get_token(): pass def revoke_token(): pass
The new Blueprint API elements scheme should be registered in the application factory function:
app/__init__.py:
Register the schema of the API elements in the application.
# ... def create_app(config_class=Config): app = Flask(__name__) # ... from app.api import bp as api_bp app.register_blueprint(api_bp, url_prefix='/api') # ...
The first aspect to consider when implementing an API is to decide what the presentation of its resources will be. I'm going to implement an API that works with users, so the view for my user resources is what I need to solve. After some brainstorming, I came up with the following json view:
{ "id": 123, "username": "susan", "password": "my-password", "email": "susan@example.com", "last_seen": "2017-10-20T15:04:27Z", "about_me": "Hello, my name is Susan!", "post_count": 7, "follower_count": 35, "followed_count": 21, "_links": { "self": "/api/users/123", "followers": "/api/users/123/followers", "followed": "/api/users/123/followed", "avatar": "https://www.gravatar.com/avatar/..." } }
Many of the fields come directly from the user database model. The password
field is different in that it will only be used when registering a new user. As you remember from Chapter 5 , user passwords are not stored in the database, but only a hash, so the password never returns. The email
field is also specially processed, because I do not want to disclose the email addresses of users. The email field will be returned only when users request their own record, but not when receiving records from other users. The fields post_count
, follower_count
and follow_count
are “virtual” fields that do not exist as fields in the database, but are provided to the client as a convenience. This is a great example that demonstrates that the resource representation does not have to match the way the actual resource is defined on the server.
Check out the _links
section, which implements hypermedia requirements. Certain links include links to the current resource, a list of users following this user, a list of users followed by the user, and finally, a link to the user's avatar image. In the future, if I decide to add messages to this API, the link to the user's message list should also be included here.
One of the nice features of the JSON format is that it is always translated as a representation in the form of a dictionary or a Python list. The json
package from the standard Python library takes care of converting Python data structures to and from JSON. Therefore, to generate these views, I'm going to add a method to the User
model, called to_dict()
, which the Python dictionary returns:
app/models.py:
User model for the view.
from flask import url_for # ... class User(UserMixin, db.Model): # ... def to_dict(self, include_email=False): data = { 'id': self.id, 'username': self.username, 'last_seen': self.last_seen.isoformat() + 'Z', 'about_me': self.about_me, 'post_count': self.posts.count(), 'follower_count': self.followers.count(), 'followed_count': self.followed.count(), '_links': { 'self': url_for('api.get_user', id=self.id), 'followers': url_for('api.get_followers', id=self.id), 'followed': url_for('api.get_followed', id=self.id), 'avatar': self.avatar(128) } } if include_email: data['email'] = self.email return data
This method should not cause any special questions and be generally understandable. The dictionary with the user view that I’ve stopped at is simply generated and returned. As I mentioned above, the email
field needs special processing, because I only want to enable email when users request their own data. Therefore, I use the include_email
flag to determine whether this field is included in the view or not.
Notice how the last_seen
field is last_seen
. For date and time fields, I'm going to use the ISO 8601 Format, which can generate Python datetime
using the isoformat()
method. But since I use naive datetime
objects that are UTC but do not have a time zone recorded in their state, I need to add a Z
at the end, which is the ISO 8601 time zone code for UTC.
Explanation by amkko : In python, datetime objects can be “naive” or “naive / aware” relative to the time zone.
Finally, check out how I implemented hipermedia links. For the three links that point to other application routes, I use url_for()
to generate URLs (which currently point to the lookup function of the replacement elements defined in app / api / users.py ). The avatar link is special because it is a Gravatar URL that is external to the application. avatar()
, -.
to_dict()
Python, JSON. , , User
. from_dict()
, Python :
app/models.py:
.
class User(UserMixin, db.Model): # ... def from_dict(self, data, new_user=False): for field in ['username', 'email', 'about_me']: if field in data: setattr(self, field, data[field]) if new_user and 'password' in data: self.set_password(data['password'])
, : username
, email
about_me
. , data
, , setattr()
Python, .
password
, . new_user
, , , . password
, set_password()
, .
, API . , , , . :
{ "items": [ { ... user resource ... }, { ... user resource ... }, ... ], "_meta": { "page": 1, "per_page": 10, "total_pages": 20, "total_items": 195 }, "_links": { "self": "http://localhost:5000/api/users?page=1", "next": "http://localhost:5000/api/users?page=2", "prev": null } }
items
- , , . _meta
, . _links
, , , .
- , , , , API , , . 16 , , , . , , , SearchableMixin
, , . , mixin, PaginatedAPIMixin
:
app/models.py:
mixin.
class PaginatedAPIMixin(object): @staticmethod def to_collection_dict(query, page, per_page, endpoint, **kwargs): resources = query.paginate(page, per_page, False) data = { 'items': [item.to_dict() for item in resources.items], '_meta': { 'page': page, 'per_page': per_page, 'total_pages': resources.pages, 'total_items': resources.total }, '_links': { 'self': url_for(endpoint, page=page, per_page=per_page, **kwargs), 'next': url_for(endpoint, page=page + 1, per_page=per_page, **kwargs) if resources.has_next else None, 'prev': url_for(endpoint, page=page - 1, per_page=per_page, **kwargs) if resources.has_prev else None } } return data
to_collection_dict()
, items
, _meta
_links
. , , . - Flask-SQLAlchemy, . , . paginate()
, , , -.
, . , , , url_for ('api.get_users', id = id, page = page)
. url_for()
, , url_for()
. , kwargs
url_for()
. page
per_page
, API.
mixin User
:
app/models.py:
PaginatedAPIMixin .
class User(PaginatedAPIMixin, UserMixin, db.Model): # ...
, , .
, 7 , , , -. API , « » , , . API JSON, API. , :
{ "error": "short error description", "message": "error message (optional)" }
HTTP . , error_response()
app/api/errors.py :
app/api/errors.py:
.
from flask import jsonify from werkzeug.http import HTTP_STATUS_CODES def error_response(status_code, message=None): payload = {'error': HTTP_STATUS_CODES.get(status_code, 'Unknown error')} if message: payload['message'] = message response = jsonify(payload) response.status_code = status_code return response
HTTP_STATUS_CODES
Werkzeug
( Flask), HTTP. error
, . jsonify()
Response
Flask 200, .
, API , 400, " ". -, , , . , , . bad_request()
, :
app/api/errors.py:
.
# ... def bad_request(message): return error_response(400, message)
, JSON, , API.
, id
:
app/api/users.py:
.
from flask import jsonify from app.models import User @bp.route('/users/<int:id>', methods=['GET']) def get_user(id): return jsonify(User.query.get_or_404(id).to_dict())
view URL-. get_or_404()
get()
, , , , , None
, id
, 404 . get_or_404()
get()
, , .
to_dict()
, User
, , Flask jsonify()
JSON .
, API, , URL- :
http://localhost:5000/api/users/1
, JSON. id
, , get_or_404()
SQLAlchemy 404 ( , , JSON).
, HTTPie , HTTP- , Python, API:
(venv) $ pip install httpie
1 (, , ) :
(venv) $ http GET http://localhost:5000/api/users/1 HTTP/1.0 200 OK Content-Length: 457 Content-Type: application/json Date: Mon, 27 Nov 2017 20:19:01 GMT Server: Werkzeug/0.12.2 Python/3.6.3 { "_links": { "avatar": "https://www.gravatar.com/avatar/993c...2724?d=identicon&s=128", "followed": "/api/users/1/followed", "followers": "/api/users/1/followers", "self": "/api/users/1" }, "about_me": "Hello! I'm the author of the Flask Mega-Tutorial.", "followed_count": 0, "follower_count": 1, "id": 1, "last_seen": "2017-11-26T07:40:52.942865Z", "post_count": 10, "username": "miguel" }
, to_collection_dict()
PaginatedAPIMixin:
app/api/users.py: .
from flask import request @bp.route('/users', methods=['GET']) def get_users(): page = request.args.get('page', 1, type=int) per_page = min(request.args.get('per_page', 10, type=int), 100) data = User.to_collection_dict(User.query, page, per_page, 'api.get_users') return jsonify(data)
page
per_page
, 1 10 , . per_page
, 100. , . page
per_page
to_collection_query()
, User.query
- , . - api.get_users
, , .
HTTPie, :
(venv) $ http GET http://localhost:5000/api/users The next two endpoints are the ones that return the follower and followed users. These are fairly similar to the one above: app/api/users.py: Return followers and followed users. @bp.route('/users/<int:id>/followers', methods=['GET']) def get_followers(id): user = User.query.get_or_404(id) page = request.args.get('page', 1, type=int) per_page = min(request.args.get('per_page', 10, type=int), 100) data = User.to_collection_dict(user.followers, page, per_page, 'api.get_followers', id=id) return jsonify(data) @bp.route('/users/<int:id>/followed', methods=['GET']) def get_followed(id): user = User.query.get_or_404(id) page = request.args.get('page', 1, type=int) per_page = min(request.args.get('per_page', 10, type=int), 100) data = User.to_collection_dict(user.followed, page, per_page, 'api.get_followed', id=id) return jsonify(data)
, id
. , user.followers
user.followed
to_collection_dict()
, , , , . to_collection_dict()
— , kwargs
, url_for()
.
, HTTPie :
(venv) $ http GET http://localhost:5000/api/users/1/followers (venv) $ http GET http://localhost:5000/api/users/1/followed
, hypermedia URL-, _links
.
POST
/users . :
app/api/users.py:
.
from flask import url_for from app import db from app.api.errors import bad_request @bp.route('/users', methods=['POST']) def create_user(): data = request.get_json() or {} if 'username' not in data or 'email' not in data or 'password' not in data: return bad_request('must include username, email and password fields') if User.query.filter_by(username=data['username']).first(): return bad_request('please use a different username') if User.query.filter_by(email=data['email']).first(): return bad_request('please use a different email address') user = User() user.from_dict(data, new_user=True) db.session.add(user) db.session.commit() response = jsonify(user.to_dict()) response.status_code = 201 response.headers['Location'] = url_for('api.get_user', id=user.id) return response
JSON , . Flask request.get_json()
, JSON Python. None
, JSON , , , request.get_json()
{}
.
, , , , . username
, email
password
. - , bad_request()
app/api/errors.py . , username
email
, , - , .
, , . from_dict()
. new_user
True
, password
, .
, , , to_dict()
. POST
, , 201
, , . , HTTP , 201
Location, URL- .
, HTTPie:
(venv) $ http POST http://localhost:5000/api/users username=alice password=dog \ email=alice@example.com "about_me=Hello, my name is Alice!"
, API, — , :
app/api/users.py:
.
@bp.route('/users/<int:id>', methods=['PUT']) def update_user(id): user = User.query.get_or_404(id) data = request.get_json() or {} if 'username' in data and data['username'] != user.username and \ User.query.filter_by(username=data['username']).first(): return bad_request('please use a different username') if 'email' in data and data['email'] != user.email and \ User.query.filter_by(email=data['email']).first(): return bad_request('please use a different email address') user.from_dict(data, new_user=False) db.session.commit() return jsonify(user.to_dict())
id
URL, 404
, . , , username
email
, , , , . , , , . , , , , , , . - , 400
, .
From_dict()
, , . 200
.
, about_me
HTTPie:
(venv) $ http PUT http://localhost:5000/api/users/2 "about_me=Hi, I am Miguel"
API, , . , , , «AuthN» «AuthZ» . , , , , , , , .
API @login_required
Flask-Login, . , HTML . API HTML , , , 401. , API - HTML. API 401, , , , .
API . API, , . API, , , . . User
:
app/models.py:
.
import base64 from datetime import datetime, timedelta import os class User(UserMixin, PaginatedAPIMixin, db.Model): # ... token = db.Column(db.String(32), index=True, unique=True) token_expiration = db.Column(db.DateTime) # ... def get_token(self, expires_in=3600): now = datetime.utcnow() if self.token and self.token_expiration > now + timedelta(seconds=60): return self.token self.token = base64.b64encode(os.urandom(24)).decode('utf-8') self.token_expiration = now + timedelta(seconds=expires_in) db.session.add(self) return self.token def revoke_token(self): self.token_expiration = datetime.utcnow() - timedelta(seconds=1) @staticmethod def check_token(token): user = User.query.filter_by(token=token).first() if user is None or user.token_expiration < datetime.utcnow(): return None return user
token
, , . token_expiration
, . , , .
. get_token()
. , base64, . , , .
, . , . revoke_token()
, , , .
check_token()
, , . , None.
, , :
(venv) $ flask db migrate -m "user tokens" (venv) $ flask db upgrade
API, , -, -. API , , . API, , -.
, Flask Flask-HTTPAuth . Flask-HTTPAuth pip:
(venv) $ pip install flask-httpauth
Flask-HTTPAuth , API . HTTP Basic Authentication 11.1 , http . Flask-HTTPAuth : , , , , . Flask-HTTPAuth , . :
app/api/auth.py:
.
from flask import g from flask_httpauth import HTTPBasicAuth from app.models import User from app.api.errors import error_response basic_auth = HTTPBasicAuth() @basic_auth.verify_password def verify_password(username, password): user = User.query.filter_by(username=username).first() if user is None: return False g.current_user = user return user.check_password(password) @basic_auth.error_handler def basic_auth_error(): return error_response(401)
HTTPBasicAuth
Flask-HTTPAuth- , . verify_password
error_handler
.
, , True
, , False
, . check_password()
User
, Flask-Login -. g.current_user
, API.
401, error_response()
app/api/errors.py . 401 HTTP "Unauthorized" (" "). HTTP , .
, , , :
app/api/tokens.py:
Generate user tokens.
from flask import jsonify, g from app import db from app.api import bp from app.api.auth import basic_auth @bp.route('/tokens', methods=['POST']) @basic_auth.login_required def get_token(): token = g.current_user.get_token() db.session.commit() return jsonify({'token': token})
@basic_auth.login_required
HTTPBasicAuth, Flask-HTTPAuth ( ) , . get_token()
. , , .
POST API :
(venv) $ http POST http://localhost:5000/api/tokens HTTP/1.0 401 UNAUTHORIZED Content-Length: 30 Content-Type: application/json Date: Mon, 27 Nov 2017 20:01:00 GMT Server: Werkzeug/0.12.2 Python/3.6.3 WWW-Authenticate: Basic realm="Authentication Required" { "error": "Unauthorized" }
HTTP 401 , basic_auth_error()
. , :
(venv) $ http --auth <username>:<password> POST http://localhost:5000/api/tokens HTTP/1.0 200 OK Content-Length: 50 Content-Type: application/json Date: Mon, 27 Nov 2017 20:01:22 GMT Server: Werkzeug/0.12.2 Python/3.6.3 { "token": "pC1Nu9wwyNt8VCj1trWilFdFI276AcbS" }
200, , . , <username>:<password>
. .
API, . , Flask-HTTPAuth . HTTPTokenAuth
:
app/api/auth.py:
Token.
# ... from flask_httpauth import HTTPTokenAuth # ... token_auth = HTTPTokenAuth() # ... @token_auth.verify_token def verify_token(token): g.current_user = User.check_token(token) if token else None return g.current_user is not None @token_auth.error_handler def token_auth_error(): return error_response(401)
Flask-HTTPAuth verify_token
, , . User.check_token()
, , . , None
. True
False
, Flask-HTTPAuth .
API , @token_auth.login_required
:
app/api/users.py:
Protect user routes with token authentication.
from app.api.auth import token_auth @bp.route('/users/<int:id>', methods=['GET']) @token_auth.login_required def get_user(id): # ... @bp.route('/users', methods=['GET']) @token_auth.login_required def get_users(): # ... @bp.route('/users/<int:id>/followers', methods=['GET']) @token_auth.login_required def get_followers(id): # ... @bp.route('/users/<int:id>/followed', methods=['GET']) @token_auth.login_required def get_followed(id): # ... @bp.route('/users', methods=['POST']) def create_user(): # ... @bp.route('/users/<int:id>', methods=['PUT']) @token_auth.login_required def update_user(id): # ...
, API, create_user()
, , , , .
, , 401. , Authorization
, /api/tokens . Flask-HTTPAuth , -, HTTPie. HTTPie --auth
, . -:
(venv) $ http GET http://localhost:5000/api/users/1 \ "Authorization:Bearer pC1Nu9wwyNt8VCj1trWilFdFI276AcbS"
, , , — , :
app/api/tokens.py:
Revoke tokens.
from app.api.auth import token_auth @bp.route('/tokens', methods=['DELETE']) @token_auth.login_required def revoke_token(): g.current_user.revoke_token() db.session.commit() return '', 204
DELETE
URL- /tokens , . , , Authorization
, . User
, . , . , . return 204, , .
, HTTPie:
(venv) $ http DELETE http://localhost:5000/api/tokens \ Authorization:"Bearer pC1Nu9wwyNt8VCj1trWilFdFI276AcbS"
, , API URL- ? 404, 404 HTML. , API, JSON API, , Flask, - , , HTML.
HTTP , , . Accept
, . , , , .
, HTML JSON . Flask request.accept_mimetypes
:
app/errors/handlers.py:
.
from flask import render_template, request from app import db from app.errors import bp from app.api.errors import error_response as api_error_response def wants_json_response(): return request.accept_mimetypes['application/json'] >= \ request.accept_mimetypes['text/html'] @bp.app_errorhandler(404) def not_found_error(error): if wants_json_response(): return api_error_response(404) return render_template('errors/404.html'), 404 @bp.app_errorhandler(500) def internal_error(error): db.session.rollback() if wants_json_response(): return api_error_response(500) return render_template('errors/500.html'), 500
wants_json_response()
JSON HTML, . JSON , HTML, JSON. HTML- . JSON error_response
API, api_error_response()
, , .
Source: https://habr.com/ru/post/358152/
All Articles