Tutorial 1: Serialization 序列化
安裝基本環(huán)境
pip install django
pip install djangorestframework
pip install pygments # We'll be using this for the code highlighting
開始 創(chuàng)建測(cè)試環(huán)境
創(chuàng)建一個(gè) django 工程
django-admin.py startproject tutorial
在工程中創(chuàng)建一個(gè) app
python manage.py startapp snippets
在 tutorial/settings.py 文件中修改:聲明 app (自己創(chuàng)建的 app 和 rest_framework)
INSTALLED_APPS = (
...
'rest_framework',
'snippets.apps.SnippetsConfig',
)
創(chuàng)建 Model 模塊
修改 snippets/models.py 文件:
from django.db import models
from pygments.lexers import get_all_lexers
from pygments.styles import get_all_styles
LEXERS = [item for item in get_all_lexers() if item[1]]
LANGUAGE_CHOICES = sorted([(item[1][0], item[0]) for item in LEXERS])
STYLE_CHOICES = sorted((item, item) for item in get_all_styles())
class Snippet(models.Model):
created = models.DateTimeField(auto_now_add=True)
title = models.CharField(max_length=100, blank=True, default='')
code = models.TextField()
linenos = models.BooleanField(default=False)
language = models.CharField(choices=LANGUAGE_CHOICES, default='python', max_length=100)
style = models.CharField(choices=STYLE_CHOICES, default='friendly', max_length=100)
class Meta:
ordering = ('created',)
將創(chuàng)建的 Model 建立相應(yīng)的 db 表,更新數(shù)據(jù)庫:
python manage.py makemigrations snippets
python manage.py migrate
Creating a Serializer class 創(chuàng)建序列化器類
snippets app 目錄下創(chuàng)建 serializers.py:
from rest_framework import serializers
from snippets.models import Snippet, LANGUAGE_CHOICES, STYLE_CHOICES
class SnippetSerializer(serializers.Serializer):
id = serializers.IntegerField(read_only=True)
title = serializers.CharField(required=False, allow_blank=True, max_length=100)
code = serializers.CharField(style={'base_template': 'textarea.html'})
linenos = serializers.BooleanField(required=False)
language = serializers.ChoiceField(choices=LANGUAGE_CHOICES, default='python')
style = serializers.ChoiceField(choices=STYLE_CHOICES, default='friendly')
def create(self, validated_data):
"""
Create and return a new `Snippet` instance, given the validated data.
"""
return Snippet.objects.create(**validated_data)
def update(self, instance, validated_data):
"""
Update and return an existing `Snippet` instance, given the validated data.
"""
instance.title = validated_data.get('title', instance.title)
instance.code = validated_data.get('code', instance.code)
instance.linenos = validated_data.get('linenos', instance.linenos)
instance.language = validated_data.get('language', instance.language)
instance.style = validated_data.get('style', instance.style)
instance.save()
return instance
Working with Serializers 使用 序列化器類
開啟 shell
python manage.py shell
創(chuàng)建 model 對(duì)象,保存到數(shù)據(jù)庫中:
from snippets.models import Snippet
from snippets.serializers import SnippetSerializer
from rest_framework.renderers import JSONRenderer
from rest_framework.parsers import JSONParser
snippet = Snippet(code='foo = "bar"\n')
snippet.save()
snippet = Snippet(code='print "hello, world"\n')
snippet.save()
序列化對(duì)象:將 model 對(duì)象轉(zhuǎn)化為 python 基本數(shù)據(jù)類型
serializer = SnippetSerializer(snippet)
serializer.data
# {'title': '', 'id': 3, 'code': 'create by new\n', 'style': 'friendly', 'language': 'python', 'linenos': False}
將 python 基本數(shù)據(jù),渲染成 json
content = JSONRenderer().render(serializer.data)
content
# b'{"id":3,"title":"","code":"create by new\\n","linenos":false,"language":"python","style":"friendly"}'
反序列化 json-》object
from django.utils.six import BytesIO
stream = BytesIO(content)
data = JSONParser().parse(stream)
將一個(gè)反序列化出來的 object 存到數(shù)據(jù)庫中:
serializer = SnippetSerializer(data=data) # data 是 json->object 轉(zhuǎn)化出來的 object
serializer.is_valid()
# True
serializer.validated_data
# OrderedDict([('title', ''), ('code', 'print "hello, world"\n'), ('linenos', False), ('language', 'python'), ('style', 'friendly')])
serializer.save()
# <Snippet: Snippet object>
查詢一組數(shù)據(jù):
serializer = SnippetSerializer(Snippet.objects.all(), many=True)
serializer.data
# [OrderedDict([('id', 1), ('title', u''), ('code', u'foo = "bar"\n'), ('linenos', False), ('langua......
Using ModelSerializers 使用 模型序列化
將原來的 snippets/serializers.py 文件內(nèi)容替換為:
from rest_framework import serializers
from snippets.models import Snippet, LANGUAGE_CHOICES, STYLE_CHOICES
class SnippetSerializer(serializers.ModelSerializer):
class Meta:
model = Snippet
fields = ('id', 'title', 'code', 'linenos', 'language', 'style')
使用這個(gè)的一個(gè)好處是,可以直接看到 serializer 實(shí)例的 屬性 by printing its representation:
python manage.py shell
from snippets.serializers import SnippetSerializer
serializer = SnippetSerializer()
print(repr(serializer))
# SnippetSerializer():
# id = IntegerField(label='ID', read_only=True)
# title = CharField(allow_blank=True, max_length=100, required=False)
# code = CharField(style={'base_template': 'textarea.html'})
# linenos = BooleanField(required=False)
# language = ChoiceField(choices=[('Clipper', 'FoxPro'), ('Cucumber', 'Gherkin'), ('RobotFramework', 'RobotFramework'), ('abap', 'ABAP'), ('ada', 'Ada')...
# style = ChoiceField(choices=[('autumn', 'autumn'), ('borland', 'borland'), ('bw', 'bw'), ('colorful', 'colorful')...
需要注意的是 ModelSerializer 類沒有做什么特殊的處理,他只是一個(gè) Serializer 類的簡(jiǎn)化:
- An automatically determined set of fields.
- Simple default implementations for the create() and update() methods.
Writing regular Django views using our Serializer 使用我們定義的 可序列化對(duì)象編寫正常的 django View
下面的例子不使用 rest-framework 的特性,按照 django 的方法來建立 view
創(chuàng)建一個(gè) HttpResponse 的子類,用來將 data-》 json
snippets/views.py:
from django.http import HttpResponse
from django.views.decorators.csrf import csrf_exempt
from rest_framework.renderers import JSONRenderer
from rest_framework.parsers import JSONParser
from snippets.models import Snippet
from snippets.serializers import SnippetSerializer
class JSONResponse(HttpResponse):
"""
An HttpResponse that renders its content into JSON.
"""
def __init__(self, data, **kwargs):
content = JSONRenderer().render(data)
kwargs['content_type'] = 'application/json'
super(JSONResponse, self).__init__(content, **kwargs)
api: 獲取已存在的 snippets ,或者創(chuàng)建新的 snippets
@csrf_exempt
def snippet_list(request):
"""
List all code snippets, or create a new snippet.
"""
if request.method == 'GET':
snippets = Snippet.objects.all()
serializer = SnippetSerializer(snippets, many=True)
return JSONResponse(serializer.data)
elif request.method == 'POST':
data = JSONParser().parse(request)
serializer = SnippetSerializer(data=data)
if serializer.is_valid():
serializer.save()
return JSONResponse(serializer.data, status=201)
return JSONResponse(serializer.errors, status=400)
csrf_exempt 表示 POST 不需要 CSRF token,通常我們不會(huì)這樣做,rest-framework 中對(duì)于這個(gè)有更好的解決方案,這里只是一個(gè)簡(jiǎn)單的例子。
api:獲取一個(gè)獨(dú)立的 snippets,用于 retrieve, update or delete
@csrf_exempt
def snippet_detail(request, pk):
"""
Retrieve, update or delete a code snippet.
"""
try:
snippet = Snippet.objects.get(pk=pk)
except Snippet.DoesNotExist:
return HttpResponse(status=404)
if request.method == 'GET':
serializer = SnippetSerializer(snippet)
return JSONResponse(serializer.data)
elif request.method == 'PUT':
data = JSONParser().parse(request)
serializer = SnippetSerializer(snippet, data=data)
if serializer.is_valid():
serializer.save()
return JSONResponse(serializer.data)
return JSONResponse(serializer.errors, status=400)
elif request.method == 'DELETE':
snippet.delete()
return HttpResponse(status=204)
聲明上面的 view snippets/urls.py: ( wire these views up)
from django.conf.urls import url
from snippets import views
urlpatterns = [
url(r'^snippets/$', views.snippet_list),
url(r'^snippets/(?P<pk>[0-9]+)/$', views.snippet_detail),
]
聲明 url (wire up the root urlconf) tutorial/urls.py:
from django.conf.urls import url, include
urlpatterns = [
url(r'^', include('snippets.urls')),
]
Tutorial 2: Requests and Responses 請(qǐng)求和回復(fù)
Request objects
rest-framework 采用 Request 對(duì)象,它繼承自 HttpRequest,提供了更加靈活的請(qǐng)求解析
其核心是屬性: request.data
request.POST # Only handles form data. Only works for 'POST' method.
request.data # Handles arbitrary data. Works for 'POST', 'PUT' and 'PATCH' methods.
Response objects
return Response(data) # Renders to content type as requested by the client.
Status codes
status module 提供了請(qǐng)求code, 比如:HTTP_400_BAD_REQUEST
這樣比直接使用 數(shù)字code 要更加易懂
Wrapping API views 包裝 API 視圖
兩種包裝方式:
-
@api_view修飾方法視圖 -
APIView包裝以類為視圖的api
包裝提供了一些新的特性,比如
- 確認(rèn)視圖接收了 Request 實(shí)例, 修改 Response 。
- 返回 405 Method Not Allowed
- 捕獲 ParseError
Pulling it all together 使用上面介紹的 rest-framework 的特性編寫程序
修改 snippets/views.py: 1. 去除 JSONResponse 2. 修改 view
from rest_framework import status
from rest_framework.decorators import api_view
from rest_framework.response import Response
from snippets.models import Snippet
from snippets.serializers import SnippetSerializer
@api_view(['GET', 'POST'])
def snippet_list(request):
"""
List all snippets, or create a new snippet.
"""
if request.method == 'GET':
snippets = Snippet.objects.all()
serializer = SnippetSerializer(snippets, many=True)
return Response(serializer.data)
elif request.method == 'POST':
serializer = SnippetSerializer(data=request.data)
if serializer.is_valid():
serializer.save()
return Response(serializer.data, status=status.HTTP_201_CREATED)
return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST)
@api_view(['GET', 'PUT', 'DELETE'])
def snippet_detail(request, pk):
"""
Retrieve, update or delete a snippet instance.
"""
try:
snippet = Snippet.objects.get(pk=pk)
except Snippet.DoesNotExist:
return Response(status=status.HTTP_404_NOT_FOUND)
if request.method == 'GET':
serializer = SnippetSerializer(snippet)
return Response(serializer.data)
elif request.method == 'PUT':
serializer = SnippetSerializer(snippet, data=request.data)
if serializer.is_valid():
serializer.save()
return Response(serializer.data)
return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST)
elif request.method == 'DELETE':
snippet.delete()
return Response(status=status.HTTP_204_NO_CONTENT)
優(yōu)點(diǎn):
- 代碼更加簡(jiǎn)潔
- 使用 HTTP_400_BAD_REQUEST 而不是 400
Adding optional format suffixes to our URLs 給 url 添加后綴
修改 view:添加 format 參數(shù)
def snippet_list(request, format=None):
def snippet_detail(request, pk, format=None):
修改 urls.py
urlpatterns = format_suffix_patterns(urlpatterns)
發(fā)起請(qǐng)求回去不同的數(shù)據(jù)格式:
-
設(shè)置請(qǐng)求頭
Accept:application/json # Request JSON Accept:text/html # Request HTML -
使用 url 后綴
http http://127.0.0.1:8000/snippets.json # JSON suffix http http://127.0.0.1:8000/snippets.api # Browsable API suffix
Tutorial 3: Class-based Views 類視圖
Rewriting our API using class-based views 重寫 view
views.py:
from snippets.models import Snippet
from snippets.serializers import SnippetSerializer
from django.http import Http404
from rest_framework.views import APIView
from rest_framework.response import Response
from rest_framework import status
class SnippetList(APIView):
"""
List all snippets, or create a new snippet.
"""
def get(self, request, format=None):
snippets = Snippet.objects.all()
serializer = SnippetSerializer(snippets, many=True)
return Response(serializer.data)
def post(self, request, format=None):
serializer = SnippetSerializer(data=request.data)
if serializer.is_valid():
serializer.save()
return Response(serializer.data, status=status.HTTP_201_CREATED)
return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST)
class SnippetDetail(APIView):
"""
Retrieve, update or delete a snippet instance.
"""
def get_object(self, pk):
try:
return Snippet.objects.get(pk=pk)
except Snippet.DoesNotExist:
raise Http404
def get(self, request, pk, format=None):
snippet = self.get_object(pk)
serializer = SnippetSerializer(snippet)
return Response(serializer.data)
def put(self, request, pk, format=None):
snippet = self.get_object(pk)
serializer = SnippetSerializer(snippet, data=request.data)
if serializer.is_valid():
serializer.save()
return Response(serializer.data)
return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST)
def delete(self, request, pk, format=None):
snippet = self.get_object(pk)
snippet.delete()
return Response(status=status.HTTP_204_NO_CONTENT)
修改 urls.py :
from django.conf.urls import url
from rest_framework.urlpatterns import format_suffix_patterns
from snippets import views
urlpatterns = [
url(r'^snippets/$', views.SnippetList.as_view()),
url(r'^snippets/(?P<pk>[0-9]+)/$', views.SnippetDetail.as_view()),
]
urlpatterns = format_suffix_patterns(urlpatterns)
Using mixins
使用類視圖好處:
- 可復(fù)用
- create/retrieve/update/delete 操作在 rest-framework 中都有相應(yīng)的封裝類
修改 views.py:
from snippets.models import Snippet
from snippets.serializers import SnippetSerializer
from rest_framework import mixins
from rest_framework import generics
class SnippetList(mixins.ListModelMixin,
mixins.CreateModelMixin,
generics.GenericAPIView):
queryset = Snippet.objects.all()
serializer_class = SnippetSerializer
def get(self, request, *args, **kwargs):
return self.list(request, *args, **kwargs)
def post(self, request, *args, **kwargs):
return self.create(request, *args, **kwargs)
class SnippetDetail(mixins.RetrieveModelMixin,
mixins.UpdateModelMixin,
mixins.DestroyModelMixin,
generics.GenericAPIView):
queryset = Snippet.objects.all()
serializer_class = SnippetSerializer
def get(self, request, *args, **kwargs):
return self.retrieve(request, *args, **kwargs)
def put(self, request, *args, **kwargs):
return self.update(request, *args, **kwargs)
def delete(self, request, *args, **kwargs):
return self.destroy(request, *args, **kwargs)
添加了基本類:GenericAPIView
mixins 方法擴(kuò)展類: ListModelMixin,CreateModelMixin,UpdateModelMixin,DestroyModelMixin,RetrieveModelMixin
Using generic class-based views 使用 generic 基本類
使用 mixin 類能夠簡(jiǎn)化很多代碼,但是可以做到更加簡(jiǎn)潔,使用包含了 mixin 功能的 generic 類
修改 views.py:
from snippets.models import Snippet
from snippets.serializers import SnippetSerializer
from rest_framework import generics
class SnippetList(generics.ListCreateAPIView):
queryset = Snippet.objects.all()
serializer_class = SnippetSerializer
class SnippetDetail(generics.RetrieveUpdateDestroyAPIView):
queryset = Snippet.objects.all()
serializer_class = SnippetSerializer
Tutorial 4: Authentication & Permissions 認(rèn)證和權(quán)限
目的:
- Code snippets are always associated with a creator.
- Only authenticated users may create snippets.
- Only the creator of a snippet may update or delete it.
- Unauthenticated requests should have full read-only access.
Adding information to our model 修改 model
修改 Snippet/models.py: 添加兩個(gè)屬性
owner = models.ForeignKey('auth.User', related_name='snippets', on_delete=models.CASCADE)
highlighted = models.TextField()
添加 保存方法:
from pygments.lexers import get_lexer_by_name
from pygments.formatters.html import HtmlFormatter
from pygments import highlight
def save(self, *args, **kwargs):
"""
Use the `pygments` library to create a highlighted HTML
representation of the code snippet.
"""
lexer = get_lexer_by_name(self.language)
linenos = self.linenos and 'table' or False
options = self.title and {'title': self.title} or {}
formatter = HtmlFormatter(style=self.style, linenos=linenos,
full=True, **options)
self.highlighted = highlight(self.code, lexer, formatter)
super(Snippet, self).save(*args, **kwargs)
修改完數(shù)據(jù)后更新數(shù)據(jù)庫:這里更新是直接刪除舊的數(shù)據(jù)庫,創(chuàng)建新的數(shù)據(jù)庫
rm -f tmp.db db.sqlite3
rm -r snippets/migrations
python manage.py makemigrations snippets
python manage.py migrate
創(chuàng)建用戶:
python manage.py createsuperuser
Adding endpoints for our User models 添加用戶控制入口
serializers.py: 添加 User 序列化器
from django.contrib.auth.models import User
class UserSerializer(serializers.ModelSerializer):
snippets = serializers.PrimaryKeyRelatedField(many=True, queryset=Snippet.objects.all())
class Meta:
model = User
fields = ('id', 'username', 'snippets')
添加 User 對(duì)應(yīng)的 View,修改 views.py:
from django.contrib.auth.models import User
from snippets.serializers import UserSerializer
class UserList(generics.ListAPIView):
queryset = User.objects.all()
serializer_class = UserSerializer
class UserDetail(generics.RetrieveAPIView):
queryset = User.objects.all()
serializer_class = UserSerializer
添加 url urls.py:
url(r'^users/$', views.UserList.as_view()),
url(r'^users/(?P<pk>[0-9]+)/$', views.UserDetail.as_view()),
Associating Snippets with Users 關(guān)聯(lián) Snippets 和 users
修改 views.py 中的 SnippetList 類:重寫下面的方法,讓
def perform_create(self, serializer):
serializer.save(owner=self.request.user)
Updating our serializer 更新 序列化器
上面的代碼將 Snippets 和創(chuàng)建它的 user 關(guān)聯(lián)在一起,下面修改 序列化器:添加 field
owner = serializers.ReadOnlyField(source='owner.username')
Adding required permissions to views 添加請(qǐng)求權(quán)限到 views
rest-framework 中有很多權(quán)限類用于限制哪些用戶可以請(qǐng)求views,下面我們只用 IsAuthenticatedOrReadOnly 來設(shè)置權(quán)限
在 views.py 中添加 SnippetList ,SnippetDetail 類屬性
from rest_framework import permissions
permission_classes = (permissions.IsAuthenticatedOrReadOnly,)
Adding login to the Browsable API 添加游覽器登入api
修改 urls.py 添加 登入視圖url:
urlpatterns += [
url(r'^api-auth/', include('rest_framework.urls',
namespace='rest_framework')),
]
Object level permissions 對(duì)象級(jí)別的權(quán)限
下面設(shè)置權(quán)限: 創(chuàng)建 Snippets 的用戶才能修改這個(gè) Snippets
創(chuàng)建 snippets/permissions.py: 添加自定義的權(quán)限類
from rest_framework import permissions
class IsOwnerOrReadOnly(permissions.BasePermission):
"""
Custom permission to only allow owners of an object to edit it.
"""
def has_object_permission(self, request, view, obj):
# Read permissions are allowed to any request,
# so we'll always allow GET, HEAD or OPTIONS requests.
if request.method in permissions.SAFE_METHODS:
return True
# Write permissions are only allowed to the owner of the snippet.
return obj.owner == request.user
將權(quán)限添加到 SnippetDetail 中 views.py:
from snippets.permissions import IsOwnerOrReadOnly
permission_classes = (permissions.IsAuthenticatedOrReadOnly,
IsOwnerOrReadOnly,)
Now, if you open a browser again, you find that the 'DELETE' and 'PUT' actions only appear on a snippet instance endpoint if you're logged in as the same user that created the code snippet.
Authenticating with the API 認(rèn)證api
目前位置我們還沒有設(shè)置任何 authentication classes ,和權(quán)限有關(guān)的類,所以目前這個(gè)工程的默認(rèn)認(rèn)證類是:SessionAuthentication,BasicAuthentication
可以在請(qǐng)求時(shí)設(shè)置 Basic Auth: 用戶名:密碼 訪問api
Tutorial 5: Relationships & Hyperlinked APIs 關(guān)系和超鏈接 APIs
Creating an endpoint for the root of our API 為接口創(chuàng)建一個(gè)入口
添加下面代碼:snippets/views.py
from rest_framework.decorators import api_view
from rest_framework.response import Response
from rest_framework.reverse import reverse
@api_view(['GET'])
def api_root(request, format=None):
return Response({
'users': reverse('user-list', request=request, format=format),
'snippets': reverse('snippet-list', request=request, format=format)
})
上面代碼
- 使用了 rest-framework 提供的
reverse方法來轉(zhuǎn)化url - 其對(duì)應(yīng)的 URL 模版,我們會(huì)在后面的代碼中看到
snippets/urls.py
Creating an endpoint for the highlighted snippets
添加 snippets/views.py:
from rest_framework import renderers
from rest_framework.response import Response
class SnippetHighlight(generics.GenericAPIView):
queryset = Snippet.objects.all()
renderer_classes = (renderers.StaticHTMLRenderer,)
def get(self, request, *args, **kwargs):
snippet = self.get_object()
return Response(snippet.highlighted)
添加 snippets/urls.py:
url(r'^$', views.api_root),
url(r'^snippets/(?P<pk>[0-9]+)/highlight/$', views.SnippetHighlight.as_view()),
Hyperlinking our API 為 api 創(chuàng)建超級(jí)鏈接
處理 實(shí)體類 之間的關(guān)系有很多種方式:
- 使用主鍵
- Using hyperlinking between entities. 使用超級(jí)鏈接
- Using a unique identifying slug field on the related entity.
- Using the default string representation of the related entity.
- Nesting the related entity inside the parent representation.
- Some other custom representation.
下面使用超級(jí)鏈接的方式來處理。
修改 序列化器(serializers) 繼承 HyperlinkedModelSerializer
HyperlinkedModelSerializer 和 ModelSerializer 的區(qū)別:
- It does not include the id field by default. 默認(rèn)不包含 id
- It includes a url field, using HyperlinkedIdentityField。 包含了 url 屬性
- Relationships use HyperlinkedRelatedField, instead of PrimaryKeyRelatedField。 關(guān)系維護(hù)使用 HyperlinkedRelatedField
修改 snippets/serializers.py:
class SnippetSerializer(serializers.HyperlinkedModelSerializer):
owner = serializers.ReadOnlyField(source='owner.username')
highlight = serializers.HyperlinkedIdentityField(view_name='snippet-highlight', format='html')
class Meta:
model = Snippet
fields = ('url', 'id', 'highlight', 'owner',
'title', 'code', 'linenos', 'language', 'style')
class UserSerializer(serializers.HyperlinkedModelSerializer):
snippets = serializers.HyperlinkedRelatedField(many=True, view_name='snippet-detail', read_only=True)
class Meta:
model = User
fields = ('url', 'id', 'username', 'snippets')
Making sure our URL patterns are named 確保 url 中定義了參數(shù) name
snippets/urls.py:
from django.conf.urls import url, include
from rest_framework.urlpatterns import format_suffix_patterns
from snippets import views
# API endpoints
urlpatterns = format_suffix_patterns([
url(r'^$', views.api_root),
url(r'^snippets/$',
views.SnippetList.as_view(),
name='snippet-list'),
url(r'^snippets/(?P<pk>[0-9]+)/$',
views.SnippetDetail.as_view(),
name='snippet-detail'),
url(r'^snippets/(?P<pk>[0-9]+)/highlight/$',
views.SnippetHighlight.as_view(),
name='snippet-highlight'),
url(r'^users/$',
views.UserList.as_view(),
name='user-list'),
url(r'^users/(?P<pk>[0-9]+)/$',
views.UserDetail.as_view(),
name='user-detail')
])
# Login and logout views for the browsable API
urlpatterns += [
url(r'^api-auth/', include('rest_framework.urls',
namespace='rest_framework')),
]
Adding pagination 添加分頁
tutorial/settings.py :
REST_FRAMEWORK = {
'PAGE_SIZE': 10
}