Python 29 Restful规范
预备知识:
-- 遵循rest风格实现的前后端交互都叫RESTful架构
-- URI:统一资源标识符,相当于身份证号
-- URL:统一资源定位符,相当于姓名
RESTful规范:
一、核心思想
1、面向资源编程,url中尽量用名词而不是动词
2、根据HTTP请求方式的不同对资源进行不同操作。
二、在url中体现的规范
1、体现版本
2、体现是否是API
3、有过滤条件
4、尽量用https
三、在返回值中的规范
1、携带状态码
2、返回值:get返回查看的所有或单条数据;post返回新增的这条数据;put / patch 返回更新的这条数据;delete 返回值空
3、携带错误信息
4、携带超链接
RestFramework
使用:
1、pip install djangorestframework
2、在settings的app中注册rest_framework
一、序列化
from SerDemo import models from SerDemo.serializers import BookSerializer from rest_framework.response import Response from rest_framework.views import APIView # Create your views here. class BookView(APIView): def get(self, request, book_id=0): if book_id == 0: books_query = models.Books.objects.all() else: books_query = models.Books.objects.filter(id=book_id) ret = BookSerializer(books_query, many=True) return Response(ret.data) # 序列化后的数据保存在.data里 def post(self, request): book_obj = request.data ser_obj = BookSerializer(data=book_obj) if ser_obj.is_valid(): ser_obj.save() return Response(ser_obj.validated_data) return Response(ser_obj.errors) def put(self, request, book_id): book_obj = models.Books.objects.filter(id=book_id).first() ser_obj = BookSerializer(instance=book_obj, data=request.data, partial=True) # 表示部分校验 if ser_obj.is_valid(): ser_obj.save() return Response(ser_obj.validated_data) return Response(ser_obj.errors)
from SerDemo import models from rest_framework import serializers class AuthorsSerializer(serializers.Serializer): id = serializers.IntegerField() name = serializers.CharField(max_length=32) class PublisherSerializer(serializers.Serializer): id = serializers.IntegerField() name = serializers.CharField(max_length=32) class BookSerializer(serializers.Serializer): id = serializers.IntegerField(required=False) # 表示验证时不需要传 tittle = serializers.CharField(max_length=32) publisher = PublisherSerializer(read_only=True) # 只在取值时验证 publisher_id = serializers.IntegerField(write_only=True) # 只在传值时验证 authors = AuthorsSerializer(many=True, read_only=True) # many表示需要循环 authors_list = serializers.ListField(write_only=True) def create(self, validated_data): # 创建数据需要自定义create方法 book_obj = models.Books.objects.create(tittle=validated_data["tittle"], publisher_id=validated_data["publisher_id"]) book_obj.authors.add(*validated_data["authors_list"]) return book_obj def update(self, instance, validated_data): # 更新数据需要自定义update方法 instance.tittle = validated_data.get("tittle", instance.tittle) instance.publisher_id = validated_data.get("publisher_id", instance.publisher_id) if validated_data.get("authors_list"): instance.authors.set(validated_data["authors_list"]) instance.save() # 注意这一步 return instance
restframework的序列化也提供了钩子函数,用法和forms一样,不过是validate_tittle形式
def validate_tittle(self, value): # value就是tittle的值 对value处理 if "python" not in value.lower(): raise serializers.ValidationError("标题必须含有python") return value
def validate(self, attrs): # attrs 字典有你传过来的所有的字段 print(attrs) if "python" in attrs["title"].lower() or attrs["post_category"] == 1: return attrs else: raise serializers.ValidationError("分类或标题不合符要求")
class BookSerializer(serializers.ModelSerializer): publisher_info = serializers.SerializerMethodField(read_only=True) # 会在查看时新增加一组键值 authors_info = serializers.SerializerMethodField(read_only=True) def get_authors_info(self, obj): # obj就是传入的Book对象 authors_querset = obj.authors.all() return [{"id": author.id, "name": author.name} for author in authors_querset] def get_publisher_info(self, obj): publisher_obj = obj.publisher return {"id": publisher_obj.id, "title": publisher_obj.title} class Meta: model = models.Books fields = "__all__" # exclude=["id"] # depth = 1 # 会让你这些所有的外键关系变成read_only = True,并且会显示所有字段,一般不用,会自定义 extra_kwargs = {"publisher": {"write_only": True}, "authors": {"write_only": True}}
二、视图
url(r'^book$', BookModelView.as_view({"get": "list", "post": "create"})), url(r'^book/(?P<pk>\d+)', BookModelView.as_view({"get": "retrieve", "put": "update", "delete": "destroy"})),
from rest_framework import viewsets
class BookModelView(viewsets.ModelViewSet): queryset = Book.objects.all() serializer_class = BookSerializer
class ModelViewSet(mixins.CreateModelMixin, mixins.RetrieveModelMixin, mixins.UpdateModelMixin, mixins.DestroyModelMixin, mixins.ListModelMixin, GenericViewSet):
GenericViewSet 继承了ViewSetMixin,里面重写了as_view方法,让as_view可以接受参数,并且将请求方式与对应的方法对应起来,所以继承了ModelViewSet方法就能给as_view传参。
一共可以说有三次封装,第一次封装将获取queryset对象以及获取序列化器的两个方法封装到GenericAPIView
class GenericAPIView(views.APIView): queryset = None serializer_class = None def get_queryset(self): queryset = self.queryset if isinstance(queryset, QuerySet): # Ensure queryset is re-evaluated on each request. queryset = queryset.all() return queryset def get_serializer(self, *args, **kwargs): serializer_class = self.get_serializer_class() kwargs['context'] = self.get_serializer_context() return serializer_class(*args, **kwargs) def get_serializer_class(self): return self.serializer_class
第二次封装,将各个视图方法封装成各个类ListModelMixin,CreateModelMixin,RetrieveModelMixin,UpdateModelMixin,DestroyModelMin
地三次封装,重写as_view方法,让as_view可以接受参数,在self.dispatch之前,将self.get = self.list......
三、路由组件
rest_framework提供了自动生成传参路由的功能
from rest_framework.routers import DefaultRouter router = DefaultRouter() router.register(r'^books')
urlpatterns += router.urls
这样会自动生成多个路由,包括books/(\d+),且都as_view中都带有参数,对应视图必须可以接受参数,但是一般不使用,因为生成的路由多也是一种浪费,也会有风险。
四、版本控制
在APIView的dispatch方法中调用了initial方法,其中包含了版本控制的代码:
# Determine the API version, if versioning is in use. version, scheme = self.determine_version(request, *args, **kwargs) request.version, request.versioning_scheme = version, scheme # 版本和使用的类
如果没有version_class类,将把None赋值给上面两个属性,默认的version_class类是api.settings.DEFAULT_VERSIONING_CLASS,默认是None。
restframework给我们提供了一些类,用来做版本控制,可以通过url匹配、params等方法获取版本信息:
# coding: utf-8 from __future__ import unicode_literals import re from django.utils.translation import ugettext_lazy as _ from rest_framework import exceptions from rest_framework.compat import unicode_http_header from rest_framework.reverse import _reverse from rest_framework.settings import api_settings from rest_framework.templatetags.rest_framework import replace_query_param from rest_framework.utils.mediatypes import _MediaType class BaseVersioning(object): default_version = api_settings.DEFAULT_VERSION allowed_versions = api_settings.ALLOWED_VERSIONS version_param = api_settings.VERSION_PARAM def determine_version(self, request, *args, **kwargs): msg = '{cls}.determine_version() must be implemented.' raise NotImplementedError(msg.format( cls=self.__class__.__name__ )) def reverse(self, viewname, args=None, kwargs=None, request=None, format=None, **extra): return _reverse(viewname, args, kwargs, request, format, **extra) def is_allowed_version(self, version): if not self.allowed_versions: return True return ((version is not None and version == self.default_version) or (version in self.allowed_versions)) class AcceptHeaderVersioning(BaseVersioning): """ GET /something/ HTTP/1.1 Host: example.com Accept: application/json; version=1.0 """ invalid_version_message = _('Invalid version in "Accept" header.') def determine_version(self, request, *args, **kwargs): media_type = _MediaType(request.accepted_media_type) version = media_type.params.get(self.version_param, self.default_version) version = unicode_http_header(version) if not self.is_allowed_version(version): raise exceptions.NotAcceptable(self.invalid_version_message) return version # We don't need to implement `reverse`, as the versioning is based # on the `Accept` header, not on the request URL. class URLPathVersioning(BaseVersioning): """ To the client this is the same style as `NamespaceVersioning`. The difference is in the backend - this implementation uses Django's URL keyword arguments to determine the version. An example URL conf for two views that accept two different versions. urlpatterns = [ url(r'^(?P<version>[v1|v2]+)/users/$', users_list, name='users-list'), url(r'^(?P<version>[v1|v2]+)/users/(?P<pk>[0-9]+)/$', users_detail, name='users-detail') ] GET /1.0/something/ HTTP/1.1 Host: example.com Accept: application/json """ invalid_version_message = _('Invalid version in URL path.') def determine_version(self, request, *args, **kwargs): version = kwargs.get(self.version_param, self.default_version) if version is None: version = self.default_version if not self.is_allowed_version(version): raise exceptions.NotFound(self.invalid_version_message) return version def reverse(self, viewname, args=None, kwargs=None, request=None, format=None, **extra): if request.version is not None: kwargs = {} if (kwargs is None) else kwargs kwargs[self.version_param] = request.version return super(URLPathVersioning, self).reverse( viewname, args, kwargs, request, format, **extra ) class NamespaceVersioning(BaseVersioning): """ To the client this is the same style as `URLPathVersioning`. The difference is in the backend - this implementation uses Django's URL namespaces to determine the version. An example URL conf that is namespaced into two separate versions # users/urls.py urlpatterns = [ url(r'^/users/$', users_list, name='users-list'), url(r'^/users/(?P<pk>[0-9]+)/$', users_detail, name='users-detail') ] # urls.py urlpatterns = [ url(r'^v1/', include('users.urls', namespace='v1')), url(r'^v2/', include('users.urls', namespace='v2')) ] GET /1.0/something/ HTTP/1.1 Host: example.com Accept: application/json """ invalid_version_message = _('Invalid version in URL path. Does not match any version namespace.') def determine_version(self, request, *args, **kwargs): resolver_match = getattr(request, 'resolver_match', None) if resolver_match is None or not resolver_match.namespace: return self.default_version # Allow for possibly nested namespaces. possible_versions = resolver_match.namespace.split(':') for version in possible_versions: if self.is_allowed_version(version): return version raise exceptions.NotFound(self.invalid_version_message) def reverse(self, viewname, args=None, kwargs=None, request=None, format=None, **extra): if request.version is not None: viewname = self.get_versioned_viewname(viewname, request) return super(NamespaceVersioning, self).reverse( viewname, args, kwargs, request, format, **extra ) def get_versioned_viewname(self, viewname, request): return request.version + ':' + viewname class HostNameVersioning(BaseVersioning): """ GET /something/ HTTP/1.1 Host: v1.example.com Accept: application/json """ hostname_regex = re.compile(r'^([a-zA-Z0-9]+)\.[a-zA-Z0-9]+\.[a-zA-Z0-9]+$') invalid_version_message = _('Invalid version in hostname.') def determine_version(self, request, *args, **kwargs): hostname, separator, port = request.get_host().partition(':') match = self.hostname_regex.match(hostname) if not match: return self.default_version version = match.group(1) if not self.is_allowed_version(version): raise exceptions.NotFound(self.invalid_version_message) return version # We don't need to implement `reverse`, as the hostname will already be # preserved as part of the REST framework `reverse` implementation. class QueryParameterVersioning(BaseVersioning): """ GET /something/?version=0.1 HTTP/1.1 Host: example.com Accept: application/json """ invalid_version_message = _('Invalid version in query parameter.') def determine_version(self, request, *args, **kwargs): version = request.query_params.get(self.version_param, self.default_version) if not self.is_allowed_version(version): raise exceptions.NotFound(self.invalid_version_message) return version def reverse(self, viewname, args=None, kwargs=None, request=None, format=None, **extra): url = super(QueryParameterVersioning, self).reverse( viewname, args, kwargs, request, format, **extra ) if request.version is not None: return replace_query_param(url, self.version_param, request.version) return url
使用方式:
# 在settings中配置要使用的版本控制类 REST_FRAMEWORK = { # 默认使用的版本控制类 'DEFAULT_VERSIONING_CLASS': 'rest_framework.versioning.URLPathVersioning', # 允许的版本 'ALLOWED_VERSIONS': ['v1', 'v2'], # 版本使用的参数名称 'VERSION_PARAM': 'version', # 默认使用的版本 'DEFAULT_VERSION': 'v1', }
urlpatterns = [ url(r"^versions", MyView.as_view()), url(r"^(?P<version>[v1|v2]+)/test01", TestView.as_view()), ]
class TestView(APIView): def get(self, request, *args, **kwargs): print(request.versioning_scheme) ret = request.version if ret == "v1": return Response("版本v1的信息") elif ret == "v2": return Response("版本v2的信息") else: return Response("根本就匹配不到这个路由")
五、认证组件
在APIView中有一个认证组件,在initial方法里,版本控制的数据处理后进行认证:
self.perform_authentication(request) # 执行request.user
def perform_authentication(self, request): request.user
在Request类中:
@property def user(self): if not hasattr(self, '_user'): with wrap_attributeerrors(): self._authenticate() return self._user
def _authenticate(self): for authenticator in self.authenticators: try: user_auth_tuple = authenticator.authenticate(self) except exceptions.APIException: self._not_authenticated() raise if user_auth_tuple is not None: self._authenticator = authenticator self.user, self.auth = user_auth_tuple return self._not_authenticated()
self.authoenticators是在实例化Request类时传递进来的,authenticators=self.get_authenticators()
def get_authenticators(self): return [auth() for auth in self.authentication_classes]
authentication_classes默认是空的,所以需要自己定义,传递的就是每个认证类的实例化对象,对象中需要定义authenticate方法,返回一个元祖,分别赋值给request.user和request.auth。
使用:
这里认证的方式是通过token,在用户表中有一个token字段,每次登陆成功都会重新生成一个uuid,将这个uuid传递给前端,作为当前用户的token,后面每次访问都需要携带这个token。
from rest_framework.exception import AuthenticationFailed class MyAuth(BaseAuthentication): def authenticate(self, request): request_token = request.query_params.get("token", None) if not request_token: raise AuthenticationFailed({"code": 1001, "error": "缺少token"}) token_obj = UserInfo.objects.filter(token=request_token).first() if not token_obj: raise AuthenticationFailed({"code": 1001, "error": "无效的token"}) return token_obj.username, token_obj
class TestAuthView(APIView): authentication_classes = [MyAuth, ] def get(self, request, *args, **kwargs): return Response("测试认证")
如果需要全局配置:
REST_FRAMEWORK = { # 默认使用的版本控制类 'DEFAULT_VERSIONING_CLASS': 'rest_framework.versioning.URLPathVersioning', # 允许的版本 'ALLOWED_VERSIONS': ['v1', 'v2'], # 版本使用的参数名称 'VERSION_PARAM': 'version', # 默认使用的版本 'DEFAULT_VERSION': 'v1', # 配置全局认证 'DEFAULT_AUTHENTICATION_CLASSES': ["BRQP.utils.MyAuth", ] }
六、权限组件
权限组件的源码和认证组件几乎一样,需要自定义权限校验的类
class MyPermission(BasePermission): message = "VIP用户才能访问" def has_permission(self, request, view): """ 自定义权限只有vip用户能访问, 注意我们初始化时候的顺序是认证在权限前面的,所以只要认证通过~ 我们这里就可以通过request.user,拿到我们用户信息 request.auth就能拿到用户对象 """ if request.user and request.auth.type == 2: return True else: return False
class TestAuthView(APIView): authentication_classes = [MyAuth, ] permission_classes = [MyPermission, ] def get(self, request, *args, **kwargs): print(request.user) print(request.auth) username = request.user return Response(username)
REST_FRAMEWORK = { # 默认使用的版本控制类 'DEFAULT_VERSIONING_CLASS': 'rest_framework.versioning.URLPathVersioning', # 允许的版本 'ALLOWED_VERSIONS': ['v1', 'v2'], # 版本使用的参数名称 'VERSION_PARAM': 'version', # 默认使用的版本 'DEFAULT_VERSION': 'v1', # 配置全局认证 # 'DEFAULT_AUTHENTICATION_CLASSES': ["BRQP.utils.MyAuth", ] # 配置全局权限 "DEFAULT_PERMISSION_CLASSES": ["BROP.utils.MyPermission"] }
七、频率组件
# by gaoxin from rest_framework.throttling import BaseThrottle import time VISIT_RECORD = {} class MyThrottle(BaseThrottle): def __init__(self): self.history = None def allow_request(self, request, view): # 实现限流的逻辑 # 以IP限流 # 访问列表 {IP: [time1, time2, time3]} # 1, 获取请求的IP地址 ip = request.META.get("REMOTE_ADDR") # 2,判断IP地址是否在访问列表 now = time.time() if ip not in VISIT_RECORD: # --1, 不在 需要给访问列表添加key,value VISIT_RECORD[ip] = [now,] return True # --2 在 需要把这个IP的访问记录 把当前时间加入到列表 history = VISIT_RECORD[ip] history.insert(0, now) # 3, 确保列表里最新访问时间以及最老的访问时间差 是1分钟 while history and history[0] - history[-1] > 60: history.pop() self.history = history # 4,得到列表长度,判断是否是允许的次数 if len(history) > 3: return False else: return True def wait(self): # 返回需要再等多久才能访问 time = 60 - (self.history[0] - self.history[-1]) return time
REST_FRAMEWORK = { # ...... # 频率限制的配置 "DEFAULT_THROTTLE_CLASSES": ["Throttle.throttle.MyThrottle"], } }
from rest_framework.throttling import SimpleRateThrottle class MyVisitThrottle(SimpleRateThrottle): scope = "WD" def get_cache_key(self, request, view): return self.get_ident(request)
REST_FRAMEWORK = { # 频率限制的配置 # "DEFAULT_THROTTLE_CLASSES": ["Throttle.throttle.MyVisitThrottle"], "DEFAULT_THROTTLE_CLASSES": ["Throttle.throttle.MyThrottle"], "DEFAULT_THROTTLE_RATES":{ 'WD':'5/m', #速率配置每分钟不能超过5次访问,WD是scope定义的值, } }
八、分页
https://www.cnblogs.com/GGGG-XXXX/articles/9867882.html
九、解析器
content-type告诉对方是什么样的数据类型。
- multipart/form-data 对应文件类型数据
- application/x-www-form-urlencoded 对应表单数据类型
- application/json 对应json数据类型
accept告诉对方我能解析什么样的数据类型。
十、渲染器
rest_framework提供了两种测试接口的渲染器,一种是默认的、有页面样式的模板,一种是纯json数据,但是无法自定义请求头,所以还是需要postman来测试接口。