From f8ac495c2dcdd16faab9b75a8a6129f95195d5d2 Mon Sep 17 00:00:00 2001 From: devfzn Date: Sun, 8 Oct 2023 20:04:34 -0300 Subject: [PATCH] =?UTF-8?q?dise=C3=B1o,=20creaci=C3=B3n=20e=20implementaci?= =?UTF-8?q?=C3=B3n=20de=20user=20API?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit serializadores, administración, autenticación y tests --- README.md | 545 ++++++++++++++++++++++++++++++++ app/app/settings.py | 8 + app/app/urls.py | 9 +- app/core/tests/test_models.py | 2 +- app/user/__init__.py | 0 app/user/apps.py | 6 + app/user/serializers.py | 56 ++++ app/user/tests/__init__.py | 0 app/user/tests/test_user_api.py | 151 +++++++++ app/user/urls.py | 15 + app/user/views.py | 29 ++ imgs_readme/api_swagger_00.png | Bin 0 -> 41251 bytes requirements.txt | 1 + 13 files changed, 820 insertions(+), 2 deletions(-) create mode 100644 app/user/__init__.py create mode 100644 app/user/apps.py create mode 100644 app/user/serializers.py create mode 100644 app/user/tests/__init__.py create mode 100644 app/user/tests/test_user_api.py create mode 100644 app/user/urls.py create mode 100644 app/user/views.py create mode 100644 imgs_readme/api_swagger_00.png diff --git a/README.md b/README.md index a0bb7d6..ef38980 100644 --- a/README.md +++ b/README.md @@ -1309,3 +1309,548 @@ class UserAdmin(BaseUserAdmin): - Página para crear usuarios ![img](./imgs_readme/django_admin_12.png) - Panel de usuarios del administrador ![img](./imgs_readme/django_admin_13.png) + +## Documentación de la API + +Es necesario tener acceso a una buena documentación para que los desarrolladores +puedan saber como usarla. Se documenta todo lo que sea necesario para usar la API + +- Endopoints disponibles (paths) +- Métodos soportados `GET`, `POST`, `PUT`, `PATCH`, `DELETE`... +- Formateo de payloads (inputs). Parametros, Post en formato **JSON** +- Formateo de respuestas (outputs). Respuesta en formato **JSON** +- Proceso Autenticación + +### Opiones de documentación + +- Manual + - Documento de texto + - Markdown +- Automatizada + - Usa la metadata del código (comments) + - Genera páginas de documentación + +## Autodocs de DRF + +- Documentación Autogenerada (3rd party library) + - `drf-spectacular` +- Genera el "schema" +- Interfaz web navegable + - Test requests + - Maneja la autenticación + +### Como funciona + +1. Creación del archivo `schema` +2. Pasa el schema al GUI + +### Open API Schema + +- Estandar para describir APIs +- Popular en la industria +- Soportada por la mayoría de herramientas de documentación de API + +### Ejemplo Schema + +fragmento + +```yml +/api/recipe/ingredients/: + get: + oprationId: recipe_ingredients_list + description: Manage ingredients in the database. + parameteres: + - in: query + name: assigned_only + schema: + type: integer + enum: + - 0 + - 1 + description: Filter by item assigned to recipies + tags: + - recipe + security: + - tokenAuth: [] + responses: + '200': + content: + application/json + schema: + type: array + items: + $ref: '#/components/schemas/Ingredient' + description: '' + ... +``` + +### Implementación DRF + +Se agrega dependencia en `drf-spectacular>=0.16` en `requirements.txt` + +Instalar app, en `settings.py` + +```py +INSTALLED_APPS = [ + ... + 'core', + 'rest_framework', + 'drf_spectacular', +] + +... +REST_FRAMEWORK = { + 'DEFAULT_SCHEMA_CLASS': 'drf_spectacular.openapi.AutoSchema', +} +``` + +### Activar las URLS + +```py +from drf_spectacular.views import SpectacularAPIView, SpectacularSwaggerView +from django.contrib import admin +from django.urls import path + +urlpatterns = [ + path('admin/', admin.site.urls), + path('api/schema/', SpectacularAPIView.as_view(), name='api-schema'), + path( + 'api/docs', + SpectacularSwaggerView.as_view(url_name='api-schema'), + name='api-docs'), +] +``` + +- `docker compose run` +- `127.0.0.1:8000/api/docs` + +## User API + +### Diseño + +- Registro de usario +- Creación de token de autenticación +- Consultar y actualizar perfil + +### Endpoins + +| Endpoint | Method | Descripción | +| - | - | - | +| `user/create` | `POST` | Registrar nuevo usuario | +| `user/token` | `POST` | Crea un nuevo token | +| `user/me/` | `PUT/PATCH` | Actualizar perfíl | + +### Creación user app + +`docker compose run --rm app sh -c "python manage.py startapp user"` + +```sh +[+] Creating 1/0 + ✔ Container recipes_api_django-db-1 Created 0.0s +[+] Running 1/1 + ✔ Container recipes_api_django-db-1 Started 0.2s +``` + +Activar `user` app en `settings.py` + +### Test User API + +[`app/user/tests/test_user_api.py`](./app/user/tests/test_user_api.py) + +```py +CREATE_USER_URL = reverse('user:create') + +def create_user(**params): + """Create and return a new user.""" + return get_user_model().objects.crate_user(**params) + + +class PublicUserApiTest(TestCase): + """Test the public features of the user API.""" + + def setUp(self): + self.client = APIClient() + + def test_create_user_success(self): + """Tests creating a user is successful.""" + payload = { + 'email': 'test@example.com', + 'password':'testpass123', + 'name': 'TestName', + } + res = self.client.post(CREATE_USER_URL, payload) + + self.assertEqual(res.status_code, status.HTTP_201_CREATED) + user = get_user_model().objects.get(email=payload['email']) + self.assertTrue(user.check_password(payload['password'])) + self.assertNotIn('password', res.data) + + def test_user_with_email_exists_error(self): + """Test error returned if user with email exists.""" + payload = { + 'email': 'test@example.com', + 'password':'testpass123', + 'name': 'Test Name', + } + create_user(**payload) + res = self.client.post(CREATE_USER_URL, payload) + + self.assertEqual(res.status_code, status.HTTP_400_BAD_REQUEST) + + def test_password_too_short_error(self): + """Test an error is returned if password less than 5 chars.""" + payload = { + 'email': 'test@example.com', + 'password':'pw', + 'name': 'Test Name', + } + res = self.client.post(CREATE_USER_URL, payload) + + self.assertEqual(res.status_code, status.HTTP_400_BAD_REQUEST) + user_exists = get_user_model().objects.filter( + email=payload['email'] + ).exists() + self.assertFalse(user_exists) +``` + +### Creando funcionalidad de User API + +- [serializers.py](./app/user/serializers.py) + + ```py + from django.contrib.auth import get_user_model + from rest_framework import serializers + + class UserSerializer(serializers.ModelSerializer): + """Seralizer for the model object.""" + + class Meta: + model = get_user_model() + fields = ['email', 'password', 'name'] + extra_kwargs = {'password': {'write_only': True, 'min_length': 5}} + + def create(self, validated_data): + """Create and return a user with encrypted password""" + return get_user_model().objects.create_user(**validated_data) + ``` + +- [views.py](./app/user/views.py) + + ```py + from rest_framework import generics + from user.serializers import UserSerializer + + class CreateUserView(generics.CreateAPIView): + """Create a new user in the system.""" + serializer_class = UserSerializer + ``` + +- [urls.py](./app/user/urls.py) + + ```py + from django.urls import path + from user import views + + app_name='user' + urlpatterns = [ + path('create/', views.CreateUserView.as_view(), name='create') + ] + ``` + +- [app/urls.py](./app/app/urls.py) + + ```py + ... + from django.urls import include, path + + urlpatterns = [ + ... + path('api/user/', include('user.urls')), + ] + ``` + +### Autenticación + +| Tipo de autenticación | Descripción | +| - | - | +| **Básica** | Envía usuario y password en cada request | +| **Token** | Usa un token en el encabezado HTTP | +| **JSON Web Token (JWT)** | Usa un token de acceso | +| **Sesión** | Usa cookies | + +En esta app se utilza **Token** por: + +- Balance entre simplicidad y seguridad +- Soporte por defecto por **DRF** +- Bién soportada por la mayoria de clientes + + +```mermaid +%%{init: {'theme': 'dark','themeVariables': {'clusterBkg': '#2b2f38'}, 'flowchart': {'curve': 'basis'}}}%% +flowchart +subgraph " " + +CT["Create token +(Post username/password)"] +STOC["Store token on client"] +ITIH["Include token in HTTP headers"] +CT .-> STOC .-> ITIH +end +``` + +#### Pros del uso de Token + +- Soporte por defecto +- Simple de usar +- Soportada por todos los clientes +- Evita enviar datos de usuario/password en cada request + +#### Cons del uso de Token + +- El token debe ser seguro +- Requiere hacer peticiones a la base de datos + +### Login out + +- Sucede en el lado del cliente +- Borra el token + +### Test token API + +Agregar tests en +[`app/user/tests/test_user_api.py`](./app/user/tests/test_user_api.py) + +```py +... +TOKEN_URL = reverse('user:token') + +... + +class PublicUserApiTest(TestCase): + """Test the public features of the user API.""" + + ... + + def test_create_token_for_user(self): + """Test generate token for valid credentials.""" + user_details = { + 'name': 'Test Name', + 'email': 'test@example.com', + 'password':'test-user-password123', + } + create_user(**user_details) + + payload = { + 'email': user_details['email'], + 'password': user_details['password'], + } + res = self.client.post(TOKEN_URL, payload) + + self.assertIn('token', res.data) + self.assertEqual(res.status_code, status.HTTP_200_OK) + + def test_create_token_bad_credentials(self): + """Test returns error if credentials invalid.""" + create_user(email='test@example.com', password='goodpass') + + payload = {'email': 'test@example.com' ,'password': 'badpass'} + res = self.client.post(TOKEN_URL, payload) + + self.assertNotIn('token', res.data) + self.assertNotIn(res.status_code, status.HTTP_400_BAD_REQUEST) + + def test_create_token_blank_password(self): + """Test posting a blank password returns an error.""" + payload = {'email': 'test@example.com' , 'password': ''} + res = self.client.post(TOKEN_URL, payload) + + self.assertNotIn('token', res.data) + self.assertNotIn(res.status_code, status.HTTP_400_BAD_REQUEST) +``` + +### Implementar Token API + +- Añadir app `rest_framework.authtoken` en [settings.py](./app/app/settings.py) + + ```py + INSTALLED_APPS = [ + ... + 'rest_framework', + 'rest_framework.authtoken', # <--- + 'drf_spectacular', + 'user', + ] + ``` + +### Creación del serlizador para token api + +- [user/serializer.py](./app/user/serializers.py) + + ```py + ... + + class AuthTokenSerializer(serializers.Serializer): + """Serializer for the user auth token.""" + email = serializer.EmailField() + password = serializer.CharField( + style={'input_type': 'password'}, + trim_whitespace=False, + ) + + def validate(self, attrs): + """Validate and authenticate the user.""" + email = attrs.get('email') + password = attrs.get('password') + user = authenticate( + request=self.context.get('request'), + username=email, + password=password, + ) + if not user: + msg = _('Unable to authenticate with provided credentials.') + raise serializers.ValidationError(msg, code='authorization') + + attrs['user'] = user + return attrs + ``` + +- vista [user/views.py](./app/user/views.py) + + ```py + ... + class CreateTokenView(ObtainAuthToken): + """Create a new auth token for user.""" + serializer_class = AuthTokenSerializer + renderer_classes = api_settings.DEFAULT_RENDERER_CLASSES + ``` + +- urls [user/urls.py](./app/user/urls.py) + + ```py + urlpatterns = [ + ... + path('token/', views.CreateTokenView.as_view(), name='token'), + ] + ``` + +### Test administrar usuario + +- [test_user_api.py](./app/user/tests/test_user_api.py) + + ```py + ... + ME_URL = reverse('user:me') + + ... + + class PrivateUserApiTests(TestCase): + """Test API requests that require authentication.""" + + def setUp(self): + self.user = create_user( + email='test@example.com', + password='testpass123', + name='Test Name', + ) + self.client = APIClient() + self.client.force_authenticate(user=self.user) + + def test_retrive_profile_success(self): + """Test retrieving profile for logged in user.""" + res = self.client.get(ME_URL) + + self.assertEqual(res.status_code, status.HTTP_200_OK) + self.assertEqual( + res.data, { + 'name': self.user.name, + 'email': self.user.email, + }) + + def test_post_me_not_allowed(self): + """Test POST is not allowed for the 'me' endpoint.""" + res = self.client.post(ME_URL, {}) + + self.assertAlmostEqual( + res.status_code, + status.HTTP_405_METHOD_NOT_ALLOWED + ) + + def test_update_user_profile(self): + """Test updating the user profile for the autenticated user.""" + payload = { 'name': 'Updated Name', 'password': 'newpassword123' } + + res = self.client.patch(ME_URL, payload) + + self.user.refresh_from_db() + self.assertEqual(self.user.name, payload['name']) + self.assertTrue(self.user.check_password(payload['password'])) + self.assertEqual(res.status_code, status.HTTP_200_OK) + ``` + +### Implementación API actualizar usuario + +`me` endpoint + +- creación (sobrescritura) del método update +[serializer.py](./app/user/serializers.py) + + ```py + ... + + class UserSerializer(serializers.ModelSerializer): + + ... + + def create(self, validated_data): + """Create and return a user with encrypted password""" + return get_user_model().objects.create_user(**validated_data) + + def update(self, instance, validated_data): + """Update and return user.""" + password = validated_data.pop('password', None) + user = super().update(instance, validated_data) + + if password: + user.set_password(password) + user.save() + + return user + + ... + ``` + +- vistas [views.py](./app/user/views.py) + + ```py + from rest_framework import generics, authentication, permissions + ... + + class ManageUserView(generics.RetrieveUpdateAPIView): + """Manage the autenticated user.""" + serializer_class = UserSerializer + authentication_classes = [authentication.TokenAuthentication] + permission_classes = [permissions.IsAuthenticated] + + def get_object(self): + """Retrieve and return the authenticated user.""" + return self.request.user + ``` + +- urls [urls.py](./app/user/urls.py) + + ```py + urlpatterns = [ + ... + path('me/', views.ManageUserView.as_view(), name='me'), + ] + ``` + +### Pruebas en navegador + +Ruta `localhost:8000/api/docs` + +![img](./imgs_readme/api_swagger_00.png) + +---- + +Segunda parte -> [Recetas](./README2.md) diff --git a/app/app/settings.py b/app/app/settings.py index 11fd324..dd06dfd 100644 --- a/app/app/settings.py +++ b/app/app/settings.py @@ -38,6 +38,10 @@ INSTALLED_APPS = [ 'django.contrib.messages', 'django.contrib.staticfiles', 'core', + 'rest_framework', + 'rest_framework.authtoken', + 'drf_spectacular', + 'user', ] MIDDLEWARE = [ @@ -127,3 +131,7 @@ STATIC_URL = 'static/' DEFAULT_AUTO_FIELD = 'django.db.models.BigAutoField' AUTH_USER_MODEL = 'core.User' + +REST_FRAMEWORK = { + 'DEFAULT_SCHEMA_CLASS': 'drf_spectacular.openapi.AutoSchema', +} diff --git a/app/app/urls.py b/app/app/urls.py index 84b3189..5a8bedf 100644 --- a/app/app/urls.py +++ b/app/app/urls.py @@ -14,9 +14,16 @@ Including another URLconf 1. Import the include() function: from django.urls import include, path 2. Add a URL to urlpatterns: path('blog/', include('blog.urls')) """ +from drf_spectacular.views import SpectacularAPIView, SpectacularSwaggerView from django.contrib import admin -from django.urls import path +from django.urls import include, path urlpatterns = [ path('admin/', admin.site.urls), + path('api/schema/', SpectacularAPIView.as_view(), name='api-schema'), + path( + 'api/docs', + SpectacularSwaggerView.as_view(url_name='api-schema'), + name='api-docs'), + path('api/user/', include('user.urls')), ] diff --git a/app/core/tests/test_models.py b/app/core/tests/test_models.py index 656638a..acd5051 100644 --- a/app/core/tests/test_models.py +++ b/app/core/tests/test_models.py @@ -2,7 +2,7 @@ Test for models. """ from django.test import TestCase -from django.contrib.auth import get_user, get_user_model +from django.contrib.auth import get_user_model class ModelTests(TestCase): diff --git a/app/user/__init__.py b/app/user/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/app/user/apps.py b/app/user/apps.py new file mode 100644 index 0000000..36cce4c --- /dev/null +++ b/app/user/apps.py @@ -0,0 +1,6 @@ +from django.apps import AppConfig + + +class UserConfig(AppConfig): + default_auto_field = 'django.db.models.BigAutoField' + name = 'user' diff --git a/app/user/serializers.py b/app/user/serializers.py new file mode 100644 index 0000000..4dc6652 --- /dev/null +++ b/app/user/serializers.py @@ -0,0 +1,56 @@ +""" +Seralizers for the user API View +""" +from django.contrib.auth import get_user_model, authenticate +from django.utils.translation import gettext as _, trim_whitespace + +from rest_framework import serializers + + +class UserSerializer(serializers.ModelSerializer): + """Seralizer for the model object.""" + + class Meta: + model = get_user_model() + fields = ['email', 'password', 'name'] + extra_kwargs = {'password': {'write_only': True, 'min_length': 5}} + + def create(self, validated_data): + """Create and return a user with encrypted password""" + return get_user_model().objects.create_user(**validated_data) + + def update(self, instance, validated_data): + """Update and return user.""" + password = validated_data.pop('password', None) + user = super().update(instance, validated_data) + + if password: + user.set_password(password) + user.save() + + return user + + +class AuthTokenSerializer(serializers.Serializer): + """Serializer for the user auth token.""" + email = serializers.EmailField() + password = serializers.CharField( + style={'input_type': 'password'}, + trim_whitespace=False, + ) + + def validate(self, attrs): + """Validate and authenticate the user.""" + email = attrs.get('email') + password = attrs.get('password') + user = authenticate( + request=self.context.get('request'), + username=email, + password=password, + ) + if not user: + msg = _('Unable to authenticate with provided credentials.') + raise serializers.ValidationError(msg, code='authorization') + + attrs['user'] = user + return attrs diff --git a/app/user/tests/__init__.py b/app/user/tests/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/app/user/tests/test_user_api.py b/app/user/tests/test_user_api.py new file mode 100644 index 0000000..9df8ec1 --- /dev/null +++ b/app/user/tests/test_user_api.py @@ -0,0 +1,151 @@ +""" +Tests for the user API. +""" +from django.test import TestCase +from django.contrib.auth import get_user_model +from django.urls import reverse + +from rest_framework.test import APIClient +from rest_framework import status + +CREATE_USER_URL = reverse('user:create') +TOKEN_URL = reverse('user:token') +ME_URL = reverse('user:me') + +def create_user(**params): + """Create and return a new user.""" + return get_user_model().objects.create_user(**params) + + +class PublicUserApiTest(TestCase): + """Test the public features of the user API.""" + + def setUp(self): + self.client = APIClient() + + def test_create_user_success(self): + """Tests creating a user is successful.""" + payload = { + 'email': 'test@example.com', + 'password':'testpass123', + 'name': 'TestName', + } + res = self.client.post(CREATE_USER_URL, payload) + + self.assertEqual(res.status_code, status.HTTP_201_CREATED) + user = get_user_model().objects.get(email=payload['email']) + self.assertTrue(user.check_password(payload['password'])) + self.assertNotIn('password', res.data) + + def test_user_with_email_exists_error(self): + """Test error returned if user with email exists.""" + payload = { + 'email': 'test@example.com', + 'password':'testpass123', + 'name': 'Test Name', + } + create_user(**payload) + res = self.client.post(CREATE_USER_URL, payload) + + self.assertEqual(res.status_code, status.HTTP_400_BAD_REQUEST) + + def test_password_too_short_error(self): + """Test an error is returned if password less than 5 chars.""" + payload = { + 'email': 'test@example.com', + 'password':'pw', + 'name': 'Test Name', + } + res = self.client.post(CREATE_USER_URL, payload) + + self.assertEqual(res.status_code, status.HTTP_400_BAD_REQUEST) + user_exists = get_user_model().objects.filter( + email=payload['email'] + ).exists() + self.assertFalse(user_exists) + + def test_create_token_for_user(self): + """Test generate token for valid credentials.""" + user_details = { + 'name': 'Test Name', + 'email': 'test@example.com', + 'password':'test-user-password123', + } + create_user(**user_details) + + payload = { + 'email': user_details['email'], + 'password': user_details['password'], + } + res = self.client.post(TOKEN_URL, payload) + + self.assertIn('token', res.data) + self.assertEqual(res.status_code, status.HTTP_200_OK) + + def test_create_token_bad_credentials(self): + """Test returns error if credentials invalid.""" + create_user(email='test@example.com', password='goodpass') + + payload = {'email': 'test@example.com' ,'password': 'badpass'} + res = self.client.post(TOKEN_URL, payload) + + self.assertNotIn('token', res.data) + self.assertEqual(res.status_code, status.HTTP_400_BAD_REQUEST) + + def test_create_token_blank_password(self): + """Test posting a blank password returns an error.""" + payload = {'email': 'test@example.com' , 'password': ''} + res = self.client.post(TOKEN_URL, payload) + + self.assertNotIn('token', res.data) + self.assertEqual(res.status_code, status.HTTP_400_BAD_REQUEST) + + def test_retrive_user_unauthorized(self): + """Test authentication is required for users.""" + res = self.client.get(ME_URL) + + self.assertEqual(res.status_code, status.HTTP_401_UNAUTHORIZED) + + +class PrivateUserApiTests(TestCase): + """Test API requests that require authentication.""" + + def setUp(self): + self.user = create_user( + email='test@example.com', + password='testpass123', + name='Test Name', + ) + self.client = APIClient() + self.client.force_authenticate(user=self.user) + + def test_retrive_profile_success(self): + """Test retrieving profile for logged in user.""" + res = self.client.get(ME_URL) + + self.assertEqual(res.status_code, status.HTTP_200_OK) + self.assertEqual( + res.data, { + 'name': self.user.name, + 'email': self.user.email, + }) + + def test_post_me_not_allowed(self): + """Test POST is not allowed for the 'me' endpoint.""" + res = self.client.post(ME_URL, {}) + + self.assertAlmostEqual( + res.status_code, + status.HTTP_405_METHOD_NOT_ALLOWED + ) + + def test_update_user_profile(self): + """Test updating the user profile for the autenticated user.""" + payload = { 'name': 'Updated Name', 'password': 'newpassword123' } + + res = self.client.patch(ME_URL, payload) + + self.user.refresh_from_db() + self.assertEqual(self.user.name, payload['name']) + self.assertTrue(self.user.check_password(payload['password'])) + self.assertEqual(res.status_code, status.HTTP_200_OK) diff --git a/app/user/urls.py b/app/user/urls.py new file mode 100644 index 0000000..494dd50 --- /dev/null +++ b/app/user/urls.py @@ -0,0 +1,15 @@ +""" +URL mappings for the user API +""" +from django.urls import path + +from user import views + + +app_name='user' + +urlpatterns = [ + path('create/', views.CreateUserView.as_view(), name='create'), + path('token/', views.CreateTokenView.as_view(), name='token'), + path('me/', views.ManageUserView.as_view(), name='me'), +] diff --git a/app/user/views.py b/app/user/views.py new file mode 100644 index 0000000..49c7a22 --- /dev/null +++ b/app/user/views.py @@ -0,0 +1,29 @@ +""" +Views for the user API. +""" +from rest_framework import generics, authentication, permissions +from rest_framework.authtoken.views import ObtainAuthToken +from rest_framework.settings import api_settings + +from user.serializers import UserSerializer, AuthTokenSerializer + + +class CreateUserView(generics.CreateAPIView): + """Create a new user in the system.""" + serializer_class = UserSerializer + + +class CreateTokenView(ObtainAuthToken): + """Create a new auth token for user.""" + serializer_class = AuthTokenSerializer + renderer_classes = api_settings.DEFAULT_RENDERER_CLASSES + +class ManageUserView(generics.RetrieveUpdateAPIView): + """Manage the autenticated user.""" + serializer_class = UserSerializer + authentication_classes = [authentication.TokenAuthentication] + permission_classes = [permissions.IsAuthenticated] + + def get_object(self): + """Retrieve and return the authenticated user.""" + return self.request.user diff --git a/imgs_readme/api_swagger_00.png b/imgs_readme/api_swagger_00.png new file mode 100644 index 0000000000000000000000000000000000000000..04e7687abaa4a0551c8421383ca83a369f0fd86b GIT binary patch literal 41251 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 literal 0 HcmV?d00001 diff --git a/requirements.txt b/requirements.txt index e40fb74..c3ba626 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,3 +1,4 @@ Django==4.2.5 djangorestframework==3.14.0 psycopg2>=2.9.9 +drf-spectacular>=0.16