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)
views.py
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
serializers.py

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}}
ModelSerializers

二、视图

 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):
ModelViewSets
GenericViewSet 继承了ViewSetMixin,里面重写了as_view方法,让as_view可以接受参数,并且将请求方式与对应的方法对应起来,所以继承了ModelViewSet方法就能给as_view传参。
GenericViewSet

一共可以说有三次封装,第一次封装将获取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
GenericAPIView

第二次封装,将各个视图方法封装成各个类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
rest_framework.versioning

使用方式:

# 在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()
_authenticate

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
MyAuth类
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", ]
}
settings.py

六、权限组件

权限组件的源码和认证组件几乎一样,需要自定义权限校验的类

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来测试接口。

原文地址:https://www.cnblogs.com/yinwenjie/p/11301219.html