├── users ├── __init__.py ├── views │ ├── __init__.py │ ├── tests │ │ ├── __init__.py │ │ ├── test_user_login.py │ │ ├── test_user_profile.py │ │ └── test_user_register.py │ ├── user_register_view.py │ ├── user_login_view.py │ └── user_profile_view.py ├── migrations │ ├── __init__.py │ └── 0001_initial.py ├── models │ ├── tests │ │ ├── __init__.py │ │ └── test_custom_user.py │ ├── __init__.py │ └── custom_user_models.py ├── admin.py ├── apps.py ├── constants.py ├── urls.py └── serializers.py ├── videos ├── __init__.py ├── migrations │ └── __init__.py ├── models.py ├── tests.py ├── admin.py ├── views.py └── apps.py ├── tiktok_clone_back ├── __init__.py ├── asgi.py ├── wsgi.py ├── urls.py └── settings.py ├── .github ├── CODEOWNERS └── workflows │ └── django.yml ├── .coverage ├── sonar-project.properties ├── Pipfile ├── .env-example.properties ├── manage.py ├── utils └── logger_config.py ├── LICENSE ├── README.md ├── .gitignore ├── .pylintrc └── Pipfile.lock /users/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /videos/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /users/views/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /users/migrations/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /tiktok_clone_back/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /users/models/tests/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /users/views/tests/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /videos/migrations/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /.github/CODEOWNERS: -------------------------------------------------------------------------------- 1 | * @edinsonrequena -------------------------------------------------------------------------------- /videos/models.py: -------------------------------------------------------------------------------- 1 | from django.db import models 2 | 3 | # Create your models here. 4 | -------------------------------------------------------------------------------- /videos/tests.py: -------------------------------------------------------------------------------- 1 | from django.test import TestCase 2 | 3 | # Create your tests here. 4 | -------------------------------------------------------------------------------- /.coverage: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/EdinsonRequena/tiktok-backend-clone/HEAD/.coverage -------------------------------------------------------------------------------- /users/admin.py: -------------------------------------------------------------------------------- 1 | from django.contrib import admin 2 | 3 | # Register your models here. 4 | -------------------------------------------------------------------------------- /videos/admin.py: -------------------------------------------------------------------------------- 1 | from django.contrib import admin 2 | 3 | # Register your models here. 4 | -------------------------------------------------------------------------------- /videos/views.py: -------------------------------------------------------------------------------- 1 | from django.shortcuts import render 2 | 3 | # Create your views here. 4 | -------------------------------------------------------------------------------- /users/models/__init__.py: -------------------------------------------------------------------------------- 1 | from .custom_user_models import CustomUser # pylint: disable=unused-import 2 | -------------------------------------------------------------------------------- /users/apps.py: -------------------------------------------------------------------------------- 1 | from django.apps import AppConfig 2 | 3 | 4 | class UsersConfig(AppConfig): 5 | default_auto_field = 'django.db.models.BigAutoField' 6 | name = 'users' 7 | -------------------------------------------------------------------------------- /videos/apps.py: -------------------------------------------------------------------------------- 1 | from django.apps import AppConfig 2 | 3 | 4 | class VideosConfig(AppConfig): 5 | default_auto_field = 'django.db.models.BigAutoField' 6 | name = 'videos' 7 | -------------------------------------------------------------------------------- /users/constants.py: -------------------------------------------------------------------------------- 1 | USER_WITH_ID_NOT_FOUND = "User with ID %s does not exist." 2 | USER_NOT_FOUND_MESSAGE = "User not found." 3 | WRONG_CREDENTIALS_ERROR = "Wrong credentials." 4 | FAILED_LOGIN_ATTEMPT = "Failed login attempt for username: %s" 5 | MISSING_FIELD_ERROR = "Missing field: " 6 | -------------------------------------------------------------------------------- /sonar-project.properties: -------------------------------------------------------------------------------- 1 | sonar.projectKey=EdinsonRequena_tiktok-backend-clone 2 | sonar.organization=edinsonrequena 3 | sonar.projectName=tiktok-backend-clone 4 | sonar.projectVersion=1.0 5 | sonar.sources=. 6 | sonar.exclusions=**/migrations/**,**/__init__.py,**/tests/** 7 | sonar.python.coverage.reportPaths=coverage.xml 8 | sonar.python.version=3.11 -------------------------------------------------------------------------------- /Pipfile: -------------------------------------------------------------------------------- 1 | [[source]] 2 | url = "https://pypi.org/simple" 3 | verify_ssl = true 4 | name = "pypi" 5 | 6 | [packages] 7 | django = "*" 8 | psycopg2-binary = "*" 9 | python-decouple = "*" 10 | djangorestframework = "*" 11 | djangorestframework-simplejwt = "*" 12 | pillow = "*" 13 | pylint = "*" 14 | coverage = "*" 15 | pipfile = "*" 16 | pylint-django = "*" 17 | 18 | [dev-packages] 19 | 20 | [requires] 21 | python_version = "3.10" 22 | -------------------------------------------------------------------------------- /.env-example.properties: -------------------------------------------------------------------------------- 1 | DJANGO_SECRET_KEY=DJANGO_SECRET_KEY 2 | DEBUG=True 3 | 4 | # Postgres 5 | DATABASE_NAME=DATABASE_NAME 6 | DATABASE_USER=DATABASE_USER 7 | DATABASE_PASSWORD=DATABASE_PASSWORD 8 | DATABASE_HOST=DATABASE_HOST 9 | DATABASE_PORT=5432 10 | 11 | # tests 12 | TEST_DATABASE_NAME=TEST_DATABASE_NAME 13 | TEST_DATABASE_USER=TEST_DATABASE_USER 14 | TEST_DATABASE_PASSWORD=TEST_DATABASE_PASSWORD 15 | TEST_DATABASE_HOST=localhost 16 | TEST_DATABASE_PORT=5432 -------------------------------------------------------------------------------- /users/models/custom_user_models.py: -------------------------------------------------------------------------------- 1 | from django.db import models 2 | from django.contrib.auth.models import AbstractUser 3 | from django.utils.translation import gettext_lazy as _ 4 | 5 | 6 | class CustomUser(AbstractUser): 7 | email = models.EmailField(_('email address'), unique=True) 8 | bio = models.TextField(_("bio"), max_length=500, blank=True) 9 | profile_picture = models.ImageField( 10 | _("profile picture"), upload_to='profile_pictures/', null=True, blank=True) 11 | -------------------------------------------------------------------------------- /tiktok_clone_back/asgi.py: -------------------------------------------------------------------------------- 1 | """ 2 | ASGI config for tiktok_clone_back project. 3 | 4 | It exposes the ASGI callable as a module-level variable named ``application``. 5 | 6 | For more information on this file, see 7 | https://docs.djangoproject.com/en/5.0/howto/deployment/asgi/ 8 | """ 9 | 10 | import os 11 | 12 | from django.core.asgi import get_asgi_application 13 | 14 | os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'tiktok_clone_back.settings') 15 | 16 | application = get_asgi_application() 17 | -------------------------------------------------------------------------------- /tiktok_clone_back/wsgi.py: -------------------------------------------------------------------------------- 1 | """ 2 | WSGI config for tiktok_clone_back project. 3 | 4 | It exposes the WSGI callable as a module-level variable named ``application``. 5 | 6 | For more information on this file, see 7 | https://docs.djangoproject.com/en/5.0/howto/deployment/wsgi/ 8 | """ 9 | 10 | import os 11 | 12 | from django.core.wsgi import get_wsgi_application 13 | 14 | os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'tiktok_clone_back.settings') 15 | 16 | application = get_wsgi_application() 17 | -------------------------------------------------------------------------------- /users/urls.py: -------------------------------------------------------------------------------- 1 | from django.urls import path 2 | from users.views.user_login_view import LoginUserAPIView 3 | from users.views.user_profile_view import UserProfileAPIView 4 | from users.views.user_register_view import RegisterUserAPIView 5 | 6 | urlpatterns = [ 7 | path('users/register/', RegisterUserAPIView.as_view(), name='register_user'), 8 | path('users/login/', LoginUserAPIView.as_view(), name='login_user'), 9 | path('users//', UserProfileAPIView.as_view(), name='user_profile'), 10 | ] 11 | -------------------------------------------------------------------------------- /manage.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | """Django's command-line utility for administrative tasks.""" 3 | import os 4 | import sys 5 | 6 | 7 | def main(): 8 | """Run administrative tasks.""" 9 | os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'tiktok_clone_back.settings') 10 | try: 11 | from django.core.management import execute_from_command_line 12 | except ImportError as exc: 13 | raise ImportError( 14 | "Couldn't import Django. Are you sure it's installed and " 15 | "available on your PYTHONPATH environment variable? Did you " 16 | "forget to activate a virtual environment?" 17 | ) from exc 18 | execute_from_command_line(sys.argv) 19 | 20 | 21 | if __name__ == '__main__': 22 | main() 23 | -------------------------------------------------------------------------------- /utils/logger_config.py: -------------------------------------------------------------------------------- 1 | import logging 2 | 3 | LOG_FORMAT = '%(asctime)s - %(levelname)s - %(message)s' 4 | 5 | 6 | def configure_logger(): 7 | """ 8 | Configures a logger with a specific level and stream handler. 9 | 10 | Returns: 11 | logging.Logger: The configured logger object. 12 | 13 | Example: 14 | To use this logger in your application, you can call this function as follows: 15 | 16 | from utils import logger_config 17 | 18 | # Configure logger with custom level 19 | logger = logger_config.configure_logger() 20 | logger.error("An example error message.") 21 | """ 22 | 23 | logging.basicConfig(level=logging.DEBUG, format=LOG_FORMAT) 24 | logger = logging.getLogger(__name__) 25 | 26 | return logger 27 | -------------------------------------------------------------------------------- /tiktok_clone_back/urls.py: -------------------------------------------------------------------------------- 1 | """ 2 | URL configuration for tiktok_clone_back project. 3 | 4 | The `urlpatterns` list routes URLs to views. For more information please see: 5 | https://docs.djangoproject.com/en/5.0/topics/http/urls/ 6 | Examples: 7 | Function views 8 | 1. Add an import: from my_app import views 9 | 2. Add a URL to urlpatterns: path('', views.home, name='home') 10 | Class-based views 11 | 1. Add an import: from other_app.views import Home 12 | 2. Add a URL to urlpatterns: path('', Home.as_view(), name='home') 13 | Including another URLconf 14 | 1. Import the include() function: from django.urls import include, path 15 | 2. Add a URL to urlpatterns: path('blog/', include('blog.urls')) 16 | """ 17 | from django.urls import path, include 18 | from django.contrib import admin 19 | from rest_framework_simplejwt.views import TokenObtainPairView, TokenRefreshView 20 | 21 | 22 | urlpatterns = [ 23 | path('admin/', admin.site.urls), 24 | path('', include('users.urls')), 25 | path('api/token/', TokenObtainPairView.as_view(), name='token_obtain_pair'), 26 | path('api/token/refresh/', TokenRefreshView.as_view(), name='token_refresh'), 27 | ] 28 | -------------------------------------------------------------------------------- /users/serializers.py: -------------------------------------------------------------------------------- 1 | from typing import Any, Dict 2 | 3 | from rest_framework import serializers 4 | from django.contrib.auth.models import User 5 | from django.contrib.auth import get_user_model 6 | 7 | 8 | User = get_user_model() 9 | 10 | 11 | class CustomUserSerializer(serializers.ModelSerializer): 12 | """ 13 | Serializer for the User model with custom fields. 14 | 15 | This serializer is used for creating and updating User instances. 16 | It includes fields such as id, username, email, password, bio, and profile_picture. 17 | The password field is write-only, meaning it is not included in the serialized representation 18 | when sending data to the client. 19 | 20 | Methods: 21 | - create(validated_data: Dict[str, Any]) -> User: 22 | Creates a new User instance based on the validated data. 23 | """ 24 | 25 | class Meta: 26 | model = User 27 | fields = ['id', 'username', 'email', 28 | 'password', 'bio', 'profile_picture'] 29 | extra_kwargs: Dict[str, Dict[str, Any]] = { 30 | 'password': {'write_only': True} 31 | } 32 | 33 | def create(self, validated_data: Dict[str, Any]) -> User: 34 | user: User = User.objects.create_user( 35 | username=validated_data['username'], 36 | email=validated_data['email'], 37 | password=validated_data['password'] 38 | ) 39 | user.bio = validated_data.get('bio', '') 40 | user.profile_picture = validated_data.get('profile_picture', None) 41 | user.save() 42 | 43 | return user 44 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | BSD 3-Clause License 2 | 3 | Copyright (c) 2024, Edinson Adonis Requena Rivera 4 | 5 | Redistribution and use in source and binary forms, with or without 6 | modification, are permitted provided that the following conditions are met: 7 | 8 | 1. Redistributions of source code must retain the above copyright notice, this 9 | list of conditions and the following disclaimer. 10 | 11 | 2. Redistributions in binary form must reproduce the above copyright notice, 12 | this list of conditions and the following disclaimer in the documentation 13 | and/or other materials provided with the distribution. 14 | 15 | 3. Neither the name of the copyright holder nor the names of its 16 | contributors may be used to endorse or promote products derived from 17 | this software without specific prior written permission. 18 | 19 | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" 20 | AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE 21 | IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE 22 | DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE 23 | FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL 24 | DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR 25 | SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER 26 | CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, 27 | OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE 28 | OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 29 | -------------------------------------------------------------------------------- /users/views/tests/test_user_login.py: -------------------------------------------------------------------------------- 1 | from django.urls import reverse 2 | from rest_framework import status 3 | from rest_framework.test import APITestCase 4 | 5 | from users.models.custom_user_models import CustomUser 6 | 7 | 8 | class LoginUserAPIViewTest(APITestCase): 9 | """ 10 | Test module for the LoginUserAPIView class. 11 | """ 12 | 13 | url = reverse('login_user') 14 | 15 | def setUp(self): 16 | self.user = CustomUser.objects.create_user( 17 | username='testuser', email='test@example.com') 18 | self.user.set_password('testpassword123') 19 | self.user.save() 20 | 21 | self.valid_credentials = { 22 | 'username': 'testuser', 23 | 'password': 'testpassword123', 24 | } 25 | 26 | self.invalid_credentials = { 27 | 'username': 'testuser', 28 | 'password': 'wrongpassword', 29 | } 30 | 31 | def test_login_with_valid_credentials(self): 32 | """ 33 | Ensure that a user can login with valid credentials. 34 | """ 35 | 36 | response = self.client.post(self.url, self.valid_credentials) 37 | 38 | self.assertEqual(response.status_code, status.HTTP_200_OK) 39 | self.assertIn('refresh', response.data) 40 | self.assertIn('access', response.data) 41 | 42 | def test_login_with_invalid_credentials(self): 43 | """ 44 | Ensure that a user cannot login with invalid credentials. 45 | """ 46 | 47 | response = self.client.post(self.url, self.invalid_credentials) 48 | 49 | self.assertEqual(response.status_code, status.HTTP_400_BAD_REQUEST) 50 | self.assertIn('error', response.data) 51 | -------------------------------------------------------------------------------- /users/views/tests/test_user_profile.py: -------------------------------------------------------------------------------- 1 | from django.urls import reverse 2 | from rest_framework import status 3 | from rest_framework.test import APITestCase 4 | 5 | from users.models.custom_user_models import CustomUser 6 | 7 | 8 | class UserProfileAPIViewTest(APITestCase): 9 | """ 10 | Test module for the UserProfileAPIView class. 11 | """ 12 | 13 | def setUp(self): 14 | self.user_password = 'testpassword123' 15 | 16 | self.user = CustomUser.objects.create_user( 17 | username='testuser', email='test@example.com', password=self.user_password) 18 | 19 | self.url = reverse('user_profile', kwargs={'userid': self.user.pk}) 20 | self.client.login(username='testuser', password=self.user_password) 21 | 22 | self.update_data = { 23 | 'bio': 'Updated bio', 24 | } 25 | 26 | def test_retrieve_user_profile(self): 27 | """ 28 | Ensure that a user profile can be retrieved by user ID. 29 | """ 30 | 31 | response = self.client.get(self.url) 32 | 33 | self.assertEqual(response.status_code, status.HTTP_200_OK) 34 | self.assertEqual(response.data['email'], 'test@example.com') 35 | 36 | def test_update_user_profile(self): 37 | """ 38 | Ensure that a user profile can be updated by user ID. 39 | """ 40 | 41 | response = self.client.put(self.url, self.update_data, format='json') 42 | 43 | self.user.refresh_from_db() 44 | self.assertEqual(response.status_code, status.HTTP_200_OK) 45 | self.assertEqual(self.user.bio, 'Updated bio') 46 | 47 | def test_delete_user_profile(self): 48 | """ 49 | Ensure that a user can delete their own profile. 50 | """ 51 | 52 | self.client.login(username='testuser', password='testpassword123') 53 | response = self.client.delete(self.url) 54 | 55 | self.assertEqual(response.status_code, status.HTTP_204_NO_CONTENT) 56 | self.assertFalse(CustomUser.objects.filter(pk=self.user.pk).exists()) 57 | -------------------------------------------------------------------------------- /users/views/user_register_view.py: -------------------------------------------------------------------------------- 1 | from rest_framework import status 2 | from rest_framework.views import APIView 3 | from rest_framework.response import Response 4 | from rest_framework_simplejwt.tokens import RefreshToken 5 | 6 | from django.http import HttpRequest 7 | 8 | from users.serializers import CustomUserSerializer 9 | 10 | 11 | class RegisterUserAPIView(APIView): 12 | """ 13 | API view for registering a new user. 14 | 15 | Methods: 16 | - post: Register a new user with the provided data. 17 | 18 | Returns: 19 | - Response: The HTTP response object containing the serialized user data if the registration 20 | is successful, or the validation errors if the provided data is invalid. 21 | """ 22 | 23 | def post(self, request: HttpRequest) -> Response: 24 | """ 25 | Handle HTTP POST request to create a new user. 26 | 27 | Args: 28 | request (HttpRequest): The HTTP request object. 29 | 30 | Returns: 31 | Response: The HTTP response object. 32 | """ 33 | serializer = CustomUserSerializer(data=request.data) 34 | 35 | if serializer.is_valid(): 36 | user = self._create_user(serializer) 37 | refresh = RefreshToken.for_user(user) 38 | res_data = { 39 | 'user': serializer.data, 40 | 'refresh': str(refresh), 41 | 'access': str(refresh.access_token), 42 | } 43 | return Response(res_data, status=status.HTTP_201_CREATED) 44 | 45 | return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST) 46 | 47 | @staticmethod 48 | def _create_user(serializer): 49 | """ 50 | Creates a new user based on the provided serializer. 51 | 52 | Args: 53 | serializer: The serializer containing the user data. 54 | 55 | Returns: 56 | The created user object. 57 | """ 58 | user = serializer.save() 59 | user.set_password(serializer.validated_data['password']) 60 | user.save() 61 | return user 62 | -------------------------------------------------------------------------------- /users/views/user_login_view.py: -------------------------------------------------------------------------------- 1 | from rest_framework import status 2 | from rest_framework.views import APIView 3 | from rest_framework.response import Response 4 | from rest_framework_simplejwt.tokens import RefreshToken 5 | 6 | from django.http import HttpRequest 7 | from django.forms import ValidationError 8 | from django.contrib.auth import authenticate 9 | 10 | from utils import logger_config 11 | from users.constants import WRONG_CREDENTIALS_ERROR, FAILED_LOGIN_ATTEMPT, MISSING_FIELD_ERROR 12 | 13 | logger = logger_config.configure_logger() 14 | 15 | 16 | class LoginUserAPIView(APIView): 17 | """ 18 | API view for user login. 19 | 20 | This view handles the POST request for user login. It expects the 'username' and 'password' 21 | fields in the request data. 22 | 23 | Methods: 24 | - post(request: HttpRequest) -> Response: Handles the POST request for user login. 25 | 26 | Returns: 27 | - Response: The HTTP response object containing the refresh and access tokens if 28 | the login is successful, or an error message if the provided credentials are invalid. 29 | """ 30 | 31 | def post(self, request: HttpRequest) -> Response: 32 | """ 33 | Handle the HTTP POST request to authenticate a user. 34 | 35 | Args: 36 | request (HttpRequest): The HTTP request object. 37 | 38 | Returns: 39 | Response: The HTTP response object containing the authentication tokens if successful, 40 | or an error message if the credentials are invalid. 41 | """ 42 | try: 43 | username = request.data['username'] 44 | password = request.data['password'] 45 | except KeyError as e: 46 | raise ValidationError(f'{MISSING_FIELD_ERROR} {e.args[0]}') from e 47 | 48 | user = authenticate(username=username, password=password) 49 | 50 | if not user: 51 | logger.warning(FAILED_LOGIN_ATTEMPT, username) 52 | return Response({"error": WRONG_CREDENTIALS_ERROR}, status=status.HTTP_400_BAD_REQUEST) 53 | 54 | refresh = RefreshToken.for_user(user) 55 | return Response({ 56 | 'refresh': str(refresh), 57 | 'access': str(refresh.access_token), 58 | }, status=status.HTTP_200_OK) 59 | -------------------------------------------------------------------------------- /users/views/tests/test_user_register.py: -------------------------------------------------------------------------------- 1 | from django.urls import reverse 2 | from rest_framework import status 3 | from rest_framework.test import APITestCase 4 | 5 | from users.models.custom_user_models import CustomUser 6 | 7 | 8 | class RegisterUserAPIViewTest(APITestCase): 9 | """ 10 | Test module for the RegisterUserAPIView class. 11 | """ 12 | 13 | url = reverse('register_user') 14 | 15 | def setUp(self): 16 | self.valid_data = { 17 | 'username': 'newuser', 18 | 'email': 'newuser@example.com', 19 | 'password': 'testpassword123', 20 | 'bio': 'This is a bio' 21 | } 22 | self.invalid_data = { 23 | 'username': '', 24 | 'email': 'newuser@example.com', 25 | 'password': 'testpassword123', 26 | 'bio': 'This is a bio' 27 | } 28 | 29 | def test_user_registration_with_valid_data(self): 30 | """ 31 | Ensure that a user can be registered with valid data. 32 | """ 33 | 34 | response = self.client.post(self.url, self.valid_data) 35 | 36 | self.assertEqual(response.status_code, status.HTTP_201_CREATED) 37 | self.assertEqual(CustomUser.objects.count(), 1) 38 | self.assertEqual(CustomUser.objects.get().username, 'newuser') 39 | 40 | def test_user_receives_token_upon_registration(self): 41 | """ 42 | Ensure that a user receives a token upon registration. 43 | """ 44 | 45 | response = self.client.post(self.url, self.valid_data) 46 | 47 | self.assertEqual(response.status_code, status.HTTP_201_CREATED) 48 | self.assertIn('refresh', response.data) 49 | self.assertIn('access', response.data) 50 | 51 | def test_user_registration_with_invalid_data(self): 52 | """ 53 | Ensure that a user cannot be registered with invalid data. 54 | """ 55 | 56 | response = self.client.post(self.url, self.invalid_data) 57 | self.assertEqual(response.status_code, status.HTTP_400_BAD_REQUEST) 58 | 59 | def test_user_registration_duplicate_username(self): 60 | """ 61 | Ensure that a user cannot be registered with a username that already exists. 62 | """ 63 | 64 | self.client.post(self.url, self.valid_data) 65 | response = self.client.post(self.url, self.valid_data) 66 | self.assertEqual(response.status_code, status.HTTP_400_BAD_REQUEST) 67 | -------------------------------------------------------------------------------- /users/models/tests/test_custom_user.py: -------------------------------------------------------------------------------- 1 | import shutil 2 | 3 | from django.conf import settings 4 | from django.test import TestCase 5 | from django.core.files.uploadedfile import SimpleUploadedFile 6 | 7 | from users.models.custom_user_models import CustomUser 8 | 9 | 10 | class CustomUserModelTest(TestCase): 11 | """ 12 | Tests for the CustomUser model. 13 | 14 | Ensures that the CustomUser model is created correctly with all additional fields 15 | functioning as expected. 16 | """ 17 | 18 | @classmethod 19 | def setUpTestData(cls): 20 | cls.user = CustomUser.objects.create_user( 21 | username='testuser', 22 | email='testuser@example.com', 23 | password='testpass123', 24 | bio='A bio here', 25 | profile_picture=SimpleUploadedFile( 26 | name='test_image.jpg', 27 | content=b'file_content', 28 | content_type='image/jpeg' 29 | ) 30 | ) 31 | 32 | def test_user_creation(self): 33 | """ 34 | Confirm that the user is created correctly. 35 | """ 36 | 37 | self.assertEqual(self.user.username, 'testuser') 38 | 39 | def test_user_email(self): 40 | """ 41 | Confirm that the user's email is set correctly. 42 | """ 43 | 44 | self.assertEqual(self.user.email, 'testuser@example.com') 45 | 46 | def test_user_bio(self): 47 | """ 48 | Confirm that the user's bio is set correctly. 49 | """ 50 | 51 | self.assertEqual(self.user.bio, 'A bio here') 52 | 53 | def test_user_profile_picture(self): 54 | """ 55 | Confirm that the user's profile picture is set correctly. 56 | """ 57 | 58 | self.assertTrue( 59 | self.user.profile_picture.name.endswith('test_image.jpg')) 60 | 61 | def test_email_unique(self): 62 | """ 63 | Confirm that the 'email' field is unique across the CustomUser model. 64 | Attempts to create a user with an email that's already in use should raise an exception. 65 | """ 66 | with self.assertRaises(Exception): 67 | CustomUser.objects.create_user( 68 | username='testuser2', 69 | email='testuser@example.com', 70 | password='testpass123' 71 | ) 72 | 73 | def tearDown(self): 74 | shutil.rmtree(settings.MEDIA_ROOT, ignore_errors=True) 75 | -------------------------------------------------------------------------------- /.github/workflows/django.yml: -------------------------------------------------------------------------------- 1 | name: Django CI 2 | 3 | on: 4 | push: 5 | branches: [ develop ] 6 | pull_request: 7 | branches: [ develop ] 8 | 9 | jobs: 10 | build: 11 | 12 | runs-on: ubuntu-latest 13 | 14 | services: 15 | postgres: 16 | image: postgres:12 17 | env: 18 | POSTGRES_DB: test_db_name 19 | POSTGRES_USER: test_db_user 20 | POSTGRES_PASSWORD: test_db_password 21 | DJANGO_SECRET_KEY: ${{ secrets.DJANGO_SECRET_KEY }} 22 | ports: 23 | - 5432:5432 24 | options: >- 25 | --health-cmd pg_isready 26 | --health-interval 10s 27 | --health-timeout 5s 28 | --health-retries 5 29 | 30 | steps: 31 | - uses: actions/checkout@v2 32 | 33 | - name: Set up Python 3.11 34 | uses: actions/setup-python@v2 35 | with: 36 | python-version: 3.11 37 | 38 | - name: Install dependencies 39 | run: | 40 | pip install pipenv 41 | pipenv install --dev 42 | 43 | - name: Run migrations 44 | run: | 45 | pipenv run python manage.py migrate 46 | env: 47 | DJANGO_SECRET_KEY: ${{ secrets.DJANGO_SECRET_KEY }} 48 | DATABASE_NAME: ${{ secrets.TEST_DATABASE_NAME }} 49 | DATABASE_USER: ${{ secrets.TEST_DATABASE_USER }} 50 | DATABASE_PASSWORD: ${{ secrets.TEST_DATABASE_PASSWORD }} 51 | DATABASE_HOST: ${{ secrets.TEST_DATABASE_HOST }} 52 | DATABASE_PORT: ${{ secrets.TEST_DATABASE_PORT }} 53 | 54 | - name: Run tests 55 | run: | 56 | pipenv run python manage.py test 57 | env: 58 | DJANGO_SECRET_KEY: ${{ secrets.DJANGO_SECRET_KEY }} 59 | DATABASE_NAME: ${{ secrets.TEST_DATABASE_NAME }} 60 | DATABASE_USER: ${{ secrets.TEST_DATABASE_USER }} 61 | DATABASE_PASSWORD: ${{ secrets.TEST_DATABASE_PASSWORD }} 62 | DATABASE_HOST: ${{ secrets.TEST_DATABASE_HOST }} 63 | DATABASE_PORT: ${{ secrets.TEST_DATABASE_PORT }} 64 | 65 | - name: Install PostgreSQL client 66 | run: | 67 | sudo apt-get install postgresql-client 68 | 69 | - name: Check PostgreSQL service 70 | run: | 71 | pg_isready -h localhost -p 5432 -U test_db_user 72 | 73 | - name: Run tests with coverage 74 | run: | 75 | pipenv run coverage run --source='.' manage.py test 76 | pipenv run coverage xml -o coverage.xml 77 | env: 78 | DJANGO_SECRET_KEY: ${{ secrets.DJANGO_SECRET_KEY }} 79 | DATABASE_NAME: ${{ secrets.TEST_DATABASE_NAME }} 80 | DATABASE_USER: ${{ secrets.TEST_DATABASE_USER }} 81 | DATABASE_PASSWORD: ${{ secrets.TEST_DATABASE_PASSWORD }} 82 | DATABASE_HOST: ${{ secrets.TEST_DATABASE_HOST }} 83 | DATABASE_PORT: ${{ secrets.TEST_DATABASE_PORT }} 84 | 85 | - name: SonarCloud Scan 86 | uses: SonarSource/sonarcloud-github-action@master 87 | with: 88 | projectBaseDir: . 89 | env: 90 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 91 | SONAR_TOKEN: ${{ secrets.SONAR_CLOUD_TOKEN }} 92 | -------------------------------------------------------------------------------- /users/migrations/0001_initial.py: -------------------------------------------------------------------------------- 1 | # Generated by Django 5.0.1 on 2024-01-07 04:37 2 | 3 | import django.contrib.auth.models 4 | import django.contrib.auth.validators 5 | import django.utils.timezone 6 | from django.db import migrations, models 7 | 8 | 9 | class Migration(migrations.Migration): 10 | 11 | initial = True 12 | 13 | dependencies = [ 14 | ('auth', '0012_alter_user_first_name_max_length'), 15 | ] 16 | 17 | operations = [ 18 | migrations.CreateModel( 19 | name='CustomUser', 20 | fields=[ 21 | ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), 22 | ('password', models.CharField(max_length=128, verbose_name='password')), 23 | ('last_login', models.DateTimeField(blank=True, null=True, verbose_name='last login')), 24 | ('is_superuser', models.BooleanField(default=False, help_text='Designates that this user has all permissions without explicitly assigning them.', verbose_name='superuser status')), 25 | ('username', models.CharField(error_messages={'unique': 'A user with that username already exists.'}, help_text='Required. 150 characters or fewer. Letters, digits and @/./+/-/_ only.', max_length=150, unique=True, validators=[django.contrib.auth.validators.UnicodeUsernameValidator()], verbose_name='username')), 26 | ('first_name', models.CharField(blank=True, max_length=150, verbose_name='first name')), 27 | ('last_name', models.CharField(blank=True, max_length=150, verbose_name='last name')), 28 | ('is_staff', models.BooleanField(default=False, help_text='Designates whether the user can log into this admin site.', verbose_name='staff status')), 29 | ('is_active', models.BooleanField(default=True, help_text='Designates whether this user should be treated as active. Unselect this instead of deleting accounts.', verbose_name='active')), 30 | ('date_joined', models.DateTimeField(default=django.utils.timezone.now, verbose_name='date joined')), 31 | ('email', models.EmailField(max_length=254, unique=True, verbose_name='email address')), 32 | ('bio', models.TextField(blank=True, max_length=500, verbose_name='bio')), 33 | ('profile_picture', models.ImageField(blank=True, null=True, upload_to='profile_pictures/', verbose_name='profile picture')), 34 | ('groups', models.ManyToManyField(blank=True, help_text='The groups this user belongs to. A user will get all permissions granted to each of their groups.', related_name='user_set', related_query_name='user', to='auth.group', verbose_name='groups')), 35 | ('user_permissions', models.ManyToManyField(blank=True, help_text='Specific permissions for this user.', related_name='user_set', related_query_name='user', to='auth.permission', verbose_name='user permissions')), 36 | ], 37 | options={ 38 | 'verbose_name': 'user', 39 | 'verbose_name_plural': 'users', 40 | 'abstract': False, 41 | }, 42 | managers=[ 43 | ('objects', django.contrib.auth.models.UserManager()), 44 | ], 45 | ), 46 | ] 47 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # TikTok Backend Clone 2 | 3 | ## Description 4 | 5 | This project is a backend clone of the popular social media app TikTok, created for educational purposes. The aim is to recreate key functionalities of TikTok, applying technologies and best practices in software development that I've learned throughout my career. This repository contains the server-side code and API endpoints. 💻 6 | 7 | ## Features 8 | 9 | This project implements a variety of functionalities inspired by TikTok, structured into different endpoints to emulate key operations of a social network. The endpoints currently developed or in development are as follows: 10 | 11 | ### 1) Users (90%): 12 | - **User Registration**: Allows new users to create an account. `POST /users/register` 13 | - **User Login**: Authentication for user access. `POST /users/login` 14 | - **Get User Profile**: View specific user profile information. `GET /users/{userid}` 15 | - **Update User Profile**: Allows users to modify their profile. `PUT /users/{userid}` 16 | - **Delete User**: Remove a user from the system. `DELETE /users/{userid}` 17 | 18 | ### 2) Videos (TO DO 🚧): 19 | - **Upload Video**: Users can upload videos. `POST /videos` 20 | - **Get Video Details**: View specific details of a video. `GET /videos/{videoid}` 21 | - **List Videos**: Get a list of all available videos. `GET /videos` 22 | - **Delete Video**: Allows users to delete their videos. `DELETE /videos/{videoid}` 23 | 24 | ### 3) Interactions and Social Network (TO DO 🚧): 25 | - **Like a Video**: Users can 'like' videos. `POST /videos/{videoid}/like` 26 | - **Unlike a Video**: Remove 'like' from a video. `DELETE /videos/{videoid}/like` 27 | - **Comment on a Video**: Post comments on videos. `POST /videos/{videoid}/comment` 28 | - **Delete Comment**: Delete own comments from a video. `DELETE /videos/{videoid}/comment/{commentid}` 29 | - **Follow a User**: Follow other users. `POST /users/{userid}/follow` 30 | - **Unfollow a User**: Unfollow other users. `DELETE /users/{userid}/follow` 31 | 32 | ### 4) Feed and Discoveries (TO DO 🚧): 33 | - **Get Video Feed**: View a personalized feed of videos. `GET /feed` 34 | - **Search Videos/Users**: Search functionality in the platform. `GET /search` 35 | 36 | 37 | ## Technologies Used 38 | 39 | - Python 40 | - Django 41 | - Django Rest Framework 42 | - Django Rest Framework-simplejwt 43 | - PostgreSQL 44 | - pylint (for linting) 45 | - JWT (JSON Web Tokens) for authentication 46 | 47 | ## Getting Started 48 | 49 | To get started with the TikTok Clone backend, follow these steps: 50 | 51 | 1. Clone the repository: `git clone https://github.com/EdinsonRequena/tiktok-backend-clone` 52 | 2. Install the dependencies: `pipenv install Pipfile` 53 | 3. Set up the environment variables (e.g., database connection, Postgres credentials) 54 | 4. Start the server: `python manage.py runserver` 55 | 56 | ## Contributing 57 | 58 | Contributions are welcome! If you find any issues or have suggestions for improvements, please open an issue or submit a pull request. PRs must have a minimum of 80% test coverage to be accepted. 59 | 60 | ## Authors 61 | 62 | - Edinson Requena - [LinkedIn](https://www.linkedin.com/in/edinson-requena-9496a2178/) | [Twitter](https://twitter.com/RequenaEA) | [Instagram](https://www.instagram.com/edinsonrequena/) | [GitHub](https://github.com/EdinsonRequena) 63 | 64 | ## License 65 | 66 | This project is licensed under the [BSD 3-Clause](LICENSE). 67 | -------------------------------------------------------------------------------- /users/views/user_profile_view.py: -------------------------------------------------------------------------------- 1 | from rest_framework import status 2 | from rest_framework.views import APIView 3 | from rest_framework.response import Response 4 | 5 | from django.http import HttpRequest 6 | 7 | from utils import logger_config 8 | from users.serializers import CustomUserSerializer 9 | from users.models.custom_user_models import CustomUser 10 | from users.constants import USER_WITH_ID_NOT_FOUND, USER_NOT_FOUND_MESSAGE 11 | 12 | 13 | logger = logger_config.configure_logger() 14 | 15 | 16 | class UserProfileAPIView(APIView): 17 | """ 18 | API view for retrieving, updating, and deleting user profiles. 19 | 20 | Methods: 21 | - get: Retrieve a user profile by user ID. 22 | - put: Update a user profile by user ID. 23 | - delete: Delete a user profile by user ID. 24 | """ 25 | 26 | def get(self, request: HttpRequest, userid: int) -> Response: 27 | """ 28 | Retrieve a user by their ID. 29 | 30 | Args: 31 | request (HttpRequest): The HTTP request object. 32 | userid (int): The ID of the user to retrieve. 33 | 34 | Returns: 35 | Response: The serialized user data if found, 36 | or a 404 response if the user does not exist. 37 | """ 38 | try: 39 | user = CustomUser.objects.get(pk=userid) 40 | serializer = CustomUserSerializer(user) 41 | return Response(serializer.data, status=status.HTTP_200_OK) 42 | 43 | except CustomUser.DoesNotExist: 44 | logger.error(USER_WITH_ID_NOT_FOUND, userid) 45 | return Response({'error': USER_NOT_FOUND_MESSAGE}, status=status.HTTP_404_NOT_FOUND) 46 | 47 | def put(self, request: HttpRequest, userid: int) -> Response: 48 | """ 49 | Update a user's information. 50 | 51 | Args: 52 | request (HttpRequest): The HTTP request object. 53 | userid (int): The ID of the user to be updated. 54 | 55 | Returns: 56 | Response: The HTTP response containing the updated user data or an error message. 57 | """ 58 | try: 59 | user = CustomUser.objects.get(pk=userid) 60 | serializer = CustomUserSerializer( 61 | user, data=request.data, partial=True) 62 | 63 | if serializer.is_valid(): 64 | serializer.save() 65 | return Response( 66 | {'message': 'User has been updated successfully.', 67 | 'data': serializer.data, }, 68 | status.HTTP_200_OK, 69 | ) 70 | 71 | return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST) 72 | except CustomUser.DoesNotExist: 73 | logger.error(USER_WITH_ID_NOT_FOUND, userid) 74 | return Response({'error': USER_NOT_FOUND_MESSAGE}, status=status.HTTP_404_NOT_FOUND) 75 | 76 | def delete(self, request: HttpRequest, userid: int) -> Response: 77 | """ 78 | Deletes a user with the given userid. 79 | 80 | Args: 81 | request (HttpRequest): The HTTP request object. 82 | userid (int): The id of the user to be deleted. 83 | 84 | Returns: 85 | Response: The HTTP response indicating the success or failure of the deletion. 86 | """ 87 | # TODO: Add a check to ensure that the user is deleting their own profile. 88 | try: 89 | user = CustomUser.objects.get(pk=userid) 90 | user.delete() 91 | return Response( 92 | {'message': 'User has been deleted successfully.'}, 93 | status=status.HTTP_204_NO_CONTENT 94 | ) 95 | 96 | except CustomUser.DoesNotExist: 97 | logger.error(USER_WITH_ID_NOT_FOUND, userid) 98 | return Response({'error': USER_NOT_FOUND_MESSAGE}, status=status.HTTP_404_NOT_FOUND) 99 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Created by https://www.toptal.com/developers/gitignore/api/visualstudiocode,django,python,venv 2 | # Edit at https://www.toptal.com/developers/gitignore?templates=visualstudiocode,django,python,venv 3 | 4 | ### Django ### 5 | *.log 6 | *.pot 7 | *.pyc 8 | __pycache__/ 9 | local_settings.py 10 | db.sqlite3 11 | db.sqlite3-journal 12 | media 13 | 14 | # If your build process includes running collectstatic, then you probably don't need or want to include staticfiles/ 15 | # in your Git repository. Update and uncomment the following line accordingly. 16 | # /staticfiles/ 17 | 18 | ### Django.Python Stack ### 19 | # Byte-compiled / optimized / DLL files 20 | *.py[cod] 21 | *$py.class 22 | 23 | # C extensions 24 | *.so 25 | 26 | # Distribution / packaging 27 | .Python 28 | build/ 29 | develop-eggs/ 30 | dist/ 31 | downloads/ 32 | eggs/ 33 | .eggs/ 34 | lib/ 35 | lib64/ 36 | parts/ 37 | sdist/ 38 | var/ 39 | wheels/ 40 | share/python-wheels/ 41 | *.egg-info/ 42 | .installed.cfg 43 | *.egg 44 | MANIFEST 45 | 46 | # PyInstaller 47 | # Usually these files are written by a python script from a template 48 | # before PyInstaller builds the exe, so as to inject date/other infos into it. 49 | *.manifest 50 | *.spec 51 | 52 | # Installer logs 53 | pip-log.txt 54 | pip-delete-this-directory.txt 55 | 56 | # Unit test / coverage reports 57 | htmlcov/ 58 | .tox/ 59 | .nox/ 60 | .cache 61 | .hypothesis/ 62 | .pytest_cache/ 63 | 64 | # Translations 65 | *.mo 66 | 67 | # Django stuff: 68 | 69 | # Flask stuff: 70 | instance/ 71 | .webassets-cache 72 | 73 | # Scrapy stuff: 74 | .scrapy 75 | 76 | # Sphinx documentation 77 | docs/_build/ 78 | 79 | # PyBuilder 80 | .pybuilder/ 81 | target/ 82 | 83 | # Jupyter Notebook 84 | .ipynb_checkpoints 85 | 86 | # IPython 87 | profile_default/ 88 | ipython_config.py 89 | 90 | # pyenv 91 | # For a library or package, you might want to ignore these files since the code is 92 | # intended to run in multiple environments; otherwise, check them in: 93 | # .python-version 94 | 95 | # pipenv 96 | # According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control. 97 | # However, in case of collaboration, if having platform-specific dependencies or dependencies 98 | # having no cross-platform support, pipenv may install dependencies that don't work, or not 99 | # install all needed dependencies. 100 | #Pipfile.lock 101 | 102 | # poetry 103 | # Similar to Pipfile.lock, it is generally recommended to include poetry.lock in version control. 104 | # This is especially recommended for binary packages to ensure reproducibility, and is more 105 | # commonly ignored for libraries. 106 | # https://python-poetry.org/docs/basic-usage/#commit-your-poetrylock-file-to-version-control 107 | #poetry.lock 108 | 109 | # pdm 110 | # Similar to Pipfile.lock, it is generally recommended to include pdm.lock in version control. 111 | #pdm.lock 112 | # pdm stores project-wide configurations in .pdm.toml, but it is recommended to not include it 113 | # in version control. 114 | # https://pdm.fming.dev/#use-with-ide 115 | .pdm.toml 116 | 117 | # PEP 582; used by e.g. github.com/David-OConnor/pyflow and github.com/pdm-project/pdm 118 | __pypackages__/ 119 | 120 | # Celery stuff 121 | celerybeat-schedule 122 | celerybeat.pid 123 | 124 | # SageMath parsed files 125 | *.sage.py 126 | 127 | # Environments 128 | .env 129 | .venv 130 | env/ 131 | venv/ 132 | ENV/ 133 | env.bak/ 134 | venv.bak/ 135 | 136 | # Spyder project settings 137 | .spyderproject 138 | .spyproject 139 | 140 | # Rope project settings 141 | .ropeproject 142 | 143 | # mkdocs documentation 144 | /site 145 | 146 | # mypy 147 | .mypy_cache/ 148 | .dmypy.json 149 | dmypy.json 150 | 151 | # Pyre type checker 152 | .pyre/ 153 | 154 | # pytype static type analyzer 155 | .pytype/ 156 | 157 | # Cython debug symbols 158 | cython_debug/ 159 | 160 | # ruff 161 | .ruff_cache/ 162 | 163 | # LSP config files 164 | pyrightconfig.json 165 | 166 | ### venv ### 167 | # Virtualenv 168 | # http://iamzed.com/2009/05/07/a-primer-on-virtualenv/ 169 | [Bb]in 170 | [Ii]nclude 171 | [Ll]ib 172 | [Ll]ib64 173 | [Ll]ocal 174 | [Ss]cripts 175 | pyvenv.cfg 176 | pip-selfcheck.json 177 | 178 | ### VisualStudioCode ### 179 | .vscode/* 180 | !.vscode/settings.json 181 | !.vscode/tasks.json 182 | !.vscode/launch.json 183 | !.vscode/extensions.json 184 | !.vscode/*.code-snippets 185 | 186 | # Local History for Visual Studio Code 187 | .history/ 188 | 189 | # Built Visual Studio Code Extensions 190 | *.vsix 191 | 192 | ### VisualStudioCode Patch ### 193 | # Ignore all local history of files 194 | .history 195 | .ionide 196 | 197 | # env 198 | .env -------------------------------------------------------------------------------- /tiktok_clone_back/settings.py: -------------------------------------------------------------------------------- 1 | """ 2 | Django settings for tiktok_clone_back project. 3 | 4 | Generated by 'django-admin startproject' using Django 5.0.1. 5 | 6 | For more information on this file, see 7 | https://docs.djangoproject.com/en/5.0/topics/settings/ 8 | 9 | For the full list of settings and their values, see 10 | https://docs.djangoproject.com/en/5.0/ref/settings/ 11 | """ 12 | import os 13 | import sys 14 | from pathlib import Path 15 | 16 | from decouple import config 17 | 18 | 19 | # Build paths inside the project like this: BASE_DIR / 'subdir'. 20 | BASE_DIR = Path(__file__).resolve().parent.parent 21 | 22 | 23 | # Quick-start development settings - unsuitable for production 24 | # See https://docs.djangoproject.com/en/5.0/howto/deployment/checklist/ 25 | 26 | # SECURITY WARNING: keep the secret key used in production secret! 27 | SECRET_KEY = config('DJANGO_SECRET_KEY') 28 | # SECURITY WARNING: don't run with debug turned on in production! 29 | DEBUG = config('DJANGO_DEBUG', default=True, cast=bool) 30 | ALLOWED_HOSTS = [] 31 | 32 | 33 | # Application definition 34 | 35 | THIRD_PARTY_APPS = [ 36 | 'rest_framework', 37 | 'rest_framework_simplejwt.token_blacklist', 38 | ] 39 | 40 | LOCAL_APPS = [ 41 | 'users', 42 | 'videos', 43 | ] 44 | 45 | DJANGO_APPS = [ 46 | 'django.contrib.admin', 47 | 'django.contrib.auth', 48 | 'django.contrib.contenttypes', 49 | 'django.contrib.sessions', 50 | 'django.contrib.messages', 51 | 'django.contrib.staticfiles', 52 | ] 53 | 54 | INSTALLED_APPS = THIRD_PARTY_APPS + LOCAL_APPS + DJANGO_APPS 55 | 56 | REST_FRAMEWORK = { 57 | 'DEFAULT_AUTHENTICATION_CLASSES': ( 58 | 'rest_framework_simplejwt.authentication.JWTAuthentication', 59 | ), 60 | } 61 | 62 | 63 | AUTH_USER_MODEL = 'users.CustomUser' 64 | 65 | MIDDLEWARE = [ 66 | 'django.middleware.security.SecurityMiddleware', 67 | 'django.contrib.sessions.middleware.SessionMiddleware', 68 | 'django.middleware.common.CommonMiddleware', 69 | 'django.middleware.csrf.CsrfViewMiddleware', 70 | 'django.contrib.auth.middleware.AuthenticationMiddleware', 71 | 'django.contrib.messages.middleware.MessageMiddleware', 72 | 'django.middleware.clickjacking.XFrameOptionsMiddleware', 73 | ] 74 | 75 | ROOT_URLCONF = 'tiktok_clone_back.urls' 76 | 77 | TEMPLATES = [ 78 | { 79 | 'BACKEND': 'django.template.backends.django.DjangoTemplates', 80 | 'DIRS': [], 81 | 'APP_DIRS': True, 82 | 'OPTIONS': { 83 | 'context_processors': [ 84 | 'django.template.context_processors.debug', 85 | 'django.template.context_processors.request', 86 | 'django.contrib.auth.context_processors.auth', 87 | 'django.contrib.messages.context_processors.messages', 88 | ], 89 | }, 90 | }, 91 | ] 92 | 93 | WSGI_APPLICATION = 'tiktok_clone_back.wsgi.application' 94 | 95 | 96 | # Database 97 | # https://docs.djangoproject.com/en/5.0/ref/settings/#databases 98 | 99 | if 'test' in sys.argv or os.getenv('GITHUB_ACTIONS'): 100 | DATABASES = { 101 | 'default': { 102 | 'ENGINE': 'django.db.backends.postgresql', 103 | 'NAME': config('TEST_DATABASE_NAME', default='test_db_name'), 104 | 'USER': config('TEST_DATABASE_USER', default='test_db_user'), 105 | 'PASSWORD': config('TEST_DATABASE_PASSWORD', default='test_db_password'), 106 | 'HOST': config('TEST_DATABASE_HOST', default='localhost'), 107 | 'PORT': config('TEST_DATABASE_PORT', cast=int, default=5432), 108 | } 109 | } 110 | else: 111 | DATABASES = { 112 | 'default': { 113 | 'ENGINE': 'django.db.backends.postgresql', 114 | 'NAME': config('DATABASE_NAME'), 115 | 'USER': config('DATABASE_USER'), 116 | 'PASSWORD': config('DATABASE_PASSWORD'), 117 | 'HOST': config('DATABASE_HOST'), 118 | 'PORT': config('DATABASE_PORT', cast=int), 119 | } 120 | } 121 | 122 | 123 | # Password validation 124 | # https://docs.djangoproject.com/en/5.0/ref/settings/#auth-password-validators 125 | 126 | AUTH_PASSWORD_VALIDATORS = [ 127 | { 128 | 'NAME': 'django.contrib.auth.password_validation.UserAttributeSimilarityValidator', 129 | }, 130 | { 131 | 'NAME': 'django.contrib.auth.password_validation.MinimumLengthValidator', 132 | }, 133 | { 134 | 'NAME': 'django.contrib.auth.password_validation.CommonPasswordValidator', 135 | }, 136 | { 137 | 'NAME': 'django.contrib.auth.password_validation.NumericPasswordValidator', 138 | }, 139 | ] 140 | 141 | 142 | # Internationalization 143 | # https://docs.djangoproject.com/en/5.0/topics/i18n/ 144 | 145 | LANGUAGE_CODE = 'en-us' 146 | 147 | TIME_ZONE = 'UTC' 148 | 149 | USE_I18N = True 150 | 151 | USE_TZ = True 152 | 153 | 154 | # Static files (CSS, JavaScript, Images) 155 | # https://docs.djangoproject.com/en/5.0/howto/static-files/ 156 | 157 | STATIC_URL = 'static/' 158 | 159 | # Default primary key field type 160 | # https://docs.djangoproject.com/en/5.0/ref/settings/#default-auto-field 161 | 162 | DEFAULT_AUTO_FIELD = 'django.db.models.BigAutoField' 163 | 164 | 165 | # Tests 166 | if 'test' in sys.argv or 'test_coverage' in sys.argv: 167 | MEDIA_ROOT = os.path.join(BASE_DIR, 'test_media') 168 | -------------------------------------------------------------------------------- /.pylintrc: -------------------------------------------------------------------------------- 1 | [MAIN] 2 | 3 | # Analyse import fallback blocks. This can be used to support both Python 2 and 4 | # 3 compatible code, which means that the block might have code that exists 5 | # only in one or another interpreter, leading to false positives when analysed. 6 | analyse-fallback-blocks=no 7 | 8 | # Clear in-memory caches upon conclusion of linting. Useful if running pylint 9 | # in a server-like mode. 10 | clear-cache-post-run=no 11 | 12 | # Load and enable all available extensions. Use --list-extensions to see a list 13 | # all available extensions. 14 | #enable-all-extensions= 15 | 16 | # In error mode, messages with a category besides ERROR or FATAL are 17 | # suppressed, and no reports are done by default. Error mode is compatible with 18 | # disabling specific errors. 19 | #errors-only= 20 | 21 | # Always return a 0 (non-error) status code, even if lint errors are found. 22 | # This is primarily useful in continuous integration scripts. 23 | #exit-zero= 24 | 25 | # A comma-separated list of package or module names from where C extensions may 26 | # be loaded. Extensions are loading into the active Python interpreter and may 27 | # run arbitrary code. 28 | extension-pkg-allow-list= 29 | 30 | # A comma-separated list of package or module names from where C extensions may 31 | # be loaded. Extensions are loading into the active Python interpreter and may 32 | # run arbitrary code. (This is an alternative name to extension-pkg-allow-list 33 | # for backward compatibility.) 34 | extension-pkg-whitelist= 35 | 36 | # Return non-zero exit code if any of these messages/categories are detected, 37 | # even if score is above --fail-under value. Syntax same as enable. Messages 38 | # specified are enabled, while categories only check already-enabled messages. 39 | # example: --fail-on=warning,refactor, convention 40 | fail-on= 41 | 42 | # Specify a score threshold under which the program will exit with error. 43 | fail-under=10 44 | 45 | # Interpret the stdin as a python script, whose filename needs to be passed as 46 | # the module_or_package argument. 47 | #from-stdin= 48 | 49 | # Files or directories to be skipped. They should be base names, not paths. 50 | ignore=CVS 51 | 52 | # Add files or directories matching the regular expressions patterns to the 53 | # ignore-list. The regex matches against paths and can be in Posix or Windows 54 | # format. Because '\\' represents the directory delimiter on Windows systems, 55 | # it can't be used as an escape character. 56 | ignore-paths= 57 | 58 | # Files or directories matching the regular expression patterns are skipped. 59 | # The regex matches against base names, not paths. The default value ignores 60 | # Emacs file locks 61 | ignore-patterns=^\.# 62 | 63 | # List of module names for which member attributes should not be checked 64 | # (useful for modules/projects where namespaces are manipulated during runtime 65 | # and thus existing member attributes cannot be deduced by static analysis). It 66 | # supports qualified module names, as well as Unix pattern matching. 67 | ignored-modules= 68 | 69 | # Python code to execute, usually for sys.path manipulation such as 70 | # pygtk.require(). 71 | #init-hook= 72 | 73 | # Use multiple processes to speed up Pylint. Specifying 0 will auto-detect the 74 | # number of processors available to use, and will cap the count on Windows to 75 | # avoid hangs. 76 | jobs=1 77 | 78 | # Control the amount of potential inferred values when inferring a single 79 | # object. This can help the performance when dealing with large functions or 80 | # complex, nested conditions. 81 | limit-inference-results=2000 82 | 83 | # List of plugins (as comma separated values of python module names) to load, 84 | # usually to register additional checkers. 85 | load-plugins=pylint_django 86 | 87 | # Pickle collected data for later comparisons. 88 | persistent=yes 89 | 90 | # Minimum Python version to use for version dependent checks. Will default to 91 | # the version used to run pylint. 92 | py-version=3.11 93 | 94 | # Discover python modules and packages in the file system subtree. 95 | recursive=yes 96 | 97 | # Add paths to the list of the source roots. Supports globbing patterns. The 98 | # source root is an absolute path or a path relative to the current working 99 | # directory used to determine a package namespace for modules located under the 100 | # source root. 101 | source-roots= 102 | 103 | # When enabled, pylint would attempt to guess common misconfiguration and emit 104 | # user-friendly hints instead of false-positive error messages. 105 | suggestion-mode=yes 106 | 107 | # Allow loading of arbitrary C extensions. Extensions are imported into the 108 | # active Python interpreter and may run arbitrary code. 109 | unsafe-load-any-extension=no 110 | 111 | # In verbose mode, extra non-checker-related info will be displayed. 112 | #verbose= 113 | 114 | 115 | [BASIC] 116 | 117 | # Naming style matching correct argument names. 118 | argument-naming-style=snake_case 119 | 120 | # Regular expression matching correct argument names. Overrides argument- 121 | # naming-style. If left empty, argument names will be checked with the set 122 | # naming style. 123 | #argument-rgx= 124 | 125 | # Naming style matching correct attribute names. 126 | attr-naming-style=snake_case 127 | 128 | # Regular expression matching correct attribute names. Overrides attr-naming- 129 | # style. If left empty, attribute names will be checked with the set naming 130 | # style. 131 | #attr-rgx= 132 | 133 | # Bad variable names which should always be refused, separated by a comma. 134 | bad-names=foo, 135 | bar, 136 | baz, 137 | toto, 138 | tutu, 139 | tata, 140 | data, 141 | info, 142 | stuff, 143 | myobject, 144 | temp, 145 | tmp, 146 | test, 147 | l, 148 | O, 149 | var, 150 | var1, 151 | var2, 152 | var3, 153 | variable, 154 | thing, 155 | myvar, 156 | new, 157 | result 158 | 159 | # Bad variable names regexes, separated by a comma. If names match any regex, 160 | # they will always be refused 161 | bad-names-rgxs= 162 | 163 | # Naming style matching correct class attribute names. 164 | class-attribute-naming-style=snake_case 165 | 166 | # Regular expression matching correct class attribute names. Overrides class- 167 | # attribute-naming-style. If left empty, class attribute names will be checked 168 | # with the set naming style. 169 | #class-attribute-rgx= 170 | 171 | # Naming style matching correct class constant names. 172 | class-const-naming-style=UPPER_CASE 173 | 174 | # Regular expression matching correct class constant names. Overrides class- 175 | # const-naming-style. If left empty, class constant names will be checked with 176 | # the set naming style. 177 | #class-const-rgx= 178 | 179 | # Naming style matching correct class names. 180 | class-naming-style=PascalCase 181 | 182 | # Regular expression matching correct class names. Overrides class-naming- 183 | # style. If left empty, class names will be checked with the set naming style. 184 | #class-rgx= 185 | 186 | # Naming style matching correct constant names. 187 | const-naming-style=UPPER_CASE 188 | 189 | # Regular expression matching correct constant names. Overrides const-naming- 190 | # style. If left empty, constant names will be checked with the set naming 191 | # style. 192 | #const-rgx= 193 | 194 | # Minimum line length for functions/classes that require docstrings, shorter 195 | # ones are exempt. 196 | docstring-min-length=6 197 | 198 | # Naming style matching correct function names. 199 | function-naming-style=snake_case 200 | 201 | # Regular expression matching correct function names. Overrides function- 202 | # naming-style. If left empty, function names will be checked with the set 203 | # naming style. 204 | #function-rgx= 205 | 206 | # Good variable names which should always be accepted, separated by a comma. 207 | good-names=i, 208 | j, 209 | k, 210 | ex, 211 | Run, 212 | _ 213 | 214 | # Good variable names regexes, separated by a comma. If names match any regex, 215 | # they will always be accepted 216 | good-names-rgxs= 217 | 218 | # Include a hint for the correct naming format with invalid-name. 219 | include-naming-hint=yes 220 | 221 | # Naming style matching correct inline iteration names. 222 | inlinevar-naming-style=any 223 | 224 | # Regular expression matching correct inline iteration names. Overrides 225 | # inlinevar-naming-style. If left empty, inline iteration names will be checked 226 | # with the set naming style. 227 | #inlinevar-rgx= 228 | 229 | # Naming style matching correct method names. 230 | method-naming-style=snake_case 231 | 232 | # Regular expression matching correct method names. Overrides method-naming- 233 | # style. If left empty, method names will be checked with the set naming style. 234 | #method-rgx= 235 | 236 | # Naming style matching correct module names. 237 | module-naming-style=snake_case 238 | 239 | # Regular expression matching correct module names. Overrides module-naming- 240 | # style. If left empty, module names will be checked with the set naming style. 241 | #module-rgx= 242 | 243 | # Colon-delimited sets of names that determine each other's naming style when 244 | # the name regexes allow several styles. 245 | name-group= 246 | 247 | # Regular expression which should only match function or class names that do 248 | # not require a docstring. 249 | no-docstring-rgx=^_ 250 | 251 | # List of decorators that produce properties, such as abc.abstractproperty. Add 252 | # to this list to register other decorators that produce valid properties. 253 | # These decorators are taken in consideration only for invalid-name. 254 | property-classes=abc.abstractproperty 255 | 256 | # Regular expression matching correct type alias names. If left empty, type 257 | # alias names will be checked with the set naming style. 258 | #typealias-rgx= 259 | 260 | # Regular expression matching correct type variable names. If left empty, type 261 | # variable names will be checked with the set naming style. 262 | #typevar-rgx= 263 | 264 | # Naming style matching correct variable names. 265 | variable-naming-style=snake_case 266 | 267 | # Regular expression matching correct variable names. Overrides variable- 268 | # naming-style. If left empty, variable names will be checked with the set 269 | # naming style. 270 | #variable-rgx= 271 | 272 | 273 | [CLASSES] 274 | 275 | # Warn about protected attribute access inside special methods 276 | check-protected-access-in-special-methods=no 277 | 278 | # List of method names used to declare (i.e. assign) instance attributes. 279 | defining-attr-methods=__init__, 280 | __new__, 281 | setUp, 282 | asyncSetUp, 283 | __post_init__ 284 | 285 | # List of member names, which should be excluded from the protected access 286 | # warning. 287 | exclude-protected=_asdict,_fields,_replace,_source,_make,os._exit 288 | 289 | # List of valid names for the first argument in a class method. 290 | valid-classmethod-first-arg=cls 291 | 292 | # List of valid names for the first argument in a metaclass class method. 293 | valid-metaclass-classmethod-first-arg=mcs 294 | 295 | 296 | [DESIGN] 297 | 298 | # List of regular expressions of class ancestor names to ignore when counting 299 | # public methods (see R0903) 300 | exclude-too-few-public-methods= 301 | 302 | # List of qualified class names to ignore when counting class parents (see 303 | # R0901) 304 | ignored-parents= 305 | 306 | # Maximum number of arguments for function / method. 307 | max-args=5 308 | 309 | # Maximum number of attributes for a class (see R0902). 310 | max-attributes=7 311 | 312 | # Maximum number of boolean expressions in an if statement (see R0916). 313 | max-bool-expr=5 314 | 315 | # Maximum number of branch for function / method body. 316 | max-branches=12 317 | 318 | # Maximum number of locals for function / method body. 319 | max-locals=15 320 | 321 | # Maximum number of parents for a class (see R0901). 322 | max-parents=7 323 | 324 | # Maximum number of public methods for a class (see R0904). 325 | max-public-methods=20 326 | 327 | # Maximum number of return / yield for function / method body. 328 | max-returns=6 329 | 330 | # Maximum number of statements in function / method body. 331 | max-statements=50 332 | 333 | # Minimum number of public methods for a class (see R0903). 334 | min-public-methods=2 335 | 336 | 337 | [EXCEPTIONS] 338 | 339 | # Exceptions that will emit a warning when caught. 340 | overgeneral-exceptions=builtins.BaseException,builtins.Exception 341 | 342 | 343 | [FORMAT] 344 | 345 | # Expected format of line ending, e.g. empty (any line ending), LF or CRLF. 346 | expected-line-ending-format= 347 | 348 | # Regexp for a line that is allowed to be longer than the limit. 349 | ignore-long-lines=^\s*(# )??$ 350 | 351 | # Number of spaces of indent required inside a hanging or continued line. 352 | indent-after-paren=4 353 | 354 | # String used as indentation unit. This is usually " " (4 spaces) or "\t" (1 355 | # tab). 356 | indent-string=' ' 357 | 358 | # Maximum number of characters on a single line. 359 | max-line-length=100 360 | 361 | # Maximum number of lines in a module. 362 | max-module-lines=1000 363 | 364 | # Allow the body of a class to be on the same line as the declaration if body 365 | # contains single statement. 366 | single-line-class-stmt=no 367 | 368 | # Allow the body of an if to be on the same line as the test if there is no 369 | # else. 370 | single-line-if-stmt=no 371 | 372 | 373 | [IMPORTS] 374 | 375 | # List of modules that can be imported at any level, not just the top level 376 | # one. 377 | allow-any-import-level= 378 | 379 | # Allow explicit reexports by alias from a package __init__. 380 | allow-reexport-from-package=no 381 | 382 | # Allow wildcard imports from modules that define __all__. 383 | allow-wildcard-with-all=no 384 | 385 | # Deprecated modules which should not be used, separated by a comma. 386 | deprecated-modules= 387 | 388 | # Output a graph (.gv or any supported image format) of external dependencies 389 | # to the given file (report RP0402 must not be disabled). 390 | ext-import-graph= 391 | 392 | # Output a graph (.gv or any supported image format) of all (i.e. internal and 393 | # external) dependencies to the given file (report RP0402 must not be 394 | # disabled). 395 | import-graph= 396 | 397 | # Output a graph (.gv or any supported image format) of internal dependencies 398 | # to the given file (report RP0402 must not be disabled). 399 | int-import-graph= 400 | 401 | # Force import order to recognize a module as part of the standard 402 | # compatibility libraries. 403 | known-standard-library= 404 | 405 | # Force import order to recognize a module as part of a third party library. 406 | known-third-party=enchant 407 | 408 | # Couples of modules and preferred modules, separated by a comma. 409 | preferred-modules= 410 | 411 | 412 | [LOGGING] 413 | 414 | # Logging modules to check that the string format arguments are in logging 415 | # function parameter format. 416 | logging-format-style=old 417 | 418 | logging-modules=logging 419 | 420 | 421 | [MESSAGES CONTROL] 422 | 423 | # Only show warnings with the listed confidence levels. Leave empty to show 424 | # all. Valid levels: HIGH, CONTROL_FLOW, INFERENCE, INFERENCE_FAILURE, 425 | # UNDEFINED. 426 | confidence=HIGH, 427 | CONTROL_FLOW, 428 | INFERENCE, 429 | INFERENCE_FAILURE, 430 | UNDEFINED 431 | 432 | # Disable the message, report, category or checker with the given id(s). You 433 | # can either give multiple identifiers separated by comma (,) or put this 434 | # option multiple times (only on the command line, not in the configuration 435 | # file where it should appear only once). You can also use "--disable=all" to 436 | # disable everything first and then re-enable specific checks. For example, if 437 | # you want to run only the similarities checker, you can use "--disable=all 438 | # --enable=similarities". If you want to run only the classes checker, but have 439 | # no Warning level messages displayed, use "--disable=all --enable=classes 440 | # --disable=W". 441 | disable=raw-checker-failed, 442 | bad-inline-option, 443 | locally-disabled, 444 | file-ignored, 445 | suppressed-message, 446 | useless-suppression, 447 | deprecated-pragma, 448 | use-symbolic-message-instead, 449 | use-implicit-booleaness-not-comparison-to-string, 450 | use-implicit-booleaness-not-comparison-to-zero, 451 | missing-module-docstring, 452 | relative-beyond-top-level, 453 | no-member, 454 | 455 | # Enable the message, report, category or checker with the given id(s). You can 456 | # either give multiple identifier separated by comma (,) or put this option 457 | # multiple time (only on the command line, not in the configuration file where 458 | # it should appear only once). See also the "--disable" option for examples. 459 | enable= 460 | 461 | 462 | [METHOD_ARGS] 463 | 464 | # List of qualified names (i.e., library.method) which require a timeout 465 | # parameter e.g. 'requests.api.get,requests.api.post' 466 | timeout-methods=requests.api.delete,requests.api.get,requests.api.head,requests.api.options,requests.api.patch,requests.api.post,requests.api.put,requests.api.request 467 | 468 | 469 | [MISCELLANEOUS] 470 | 471 | # List of note tags to take in consideration, separated by a comma. 472 | notes=FIXME, 473 | XXX, 474 | TODO 475 | 476 | # Regular expression of note tags to take in consideration. 477 | notes-rgx= 478 | 479 | 480 | [REFACTORING] 481 | 482 | # Maximum number of nested blocks for function / method body 483 | max-nested-blocks=5 484 | 485 | # Complete name of functions that never returns. When checking for 486 | # inconsistent-return-statements if a never returning function is called then 487 | # it will be considered as an explicit return statement and no message will be 488 | # printed. 489 | never-returning-functions=sys.exit,argparse.parse_error 490 | 491 | 492 | [REPORTS] 493 | 494 | # Python expression which should return a score less than or equal to 10. You 495 | # have access to the variables 'fatal', 'error', 'warning', 'refactor', 496 | # 'convention', and 'info' which contain the number of messages in each 497 | # category, as well as 'statement' which is the total number of statements 498 | # analyzed. This score is used by the global evaluation report (RP0004). 499 | evaluation=max(0, 0 if fatal else 10.0 - ((float(5 * error + warning + refactor + convention) / statement) * 10)) 500 | 501 | # Template used to display messages. This is a python new-style format string 502 | # used to format the message information. See doc for all details. 503 | msg-template= 504 | 505 | # Set the output format. Available formats are: text, parseable, colorized, 506 | # json2 (improved json format), json (old json format) and msvs (visual 507 | # studio). You can also give a reporter class, e.g. 508 | # mypackage.mymodule.MyReporterClass. 509 | #output-format= 510 | 511 | # Tells whether to display a full report or only the messages. 512 | reports=no 513 | 514 | # Activate the evaluation score. 515 | score=yes 516 | 517 | 518 | [SIMILARITIES] 519 | 520 | # Comments are removed from the similarity computation 521 | ignore-comments=yes 522 | 523 | # Docstrings are removed from the similarity computation 524 | ignore-docstrings=yes 525 | 526 | # Imports are removed from the similarity computation 527 | ignore-imports=yes 528 | 529 | # Signatures are removed from the similarity computation 530 | ignore-signatures=yes 531 | 532 | # Minimum lines number of a similarity. 533 | min-similarity-lines=4 534 | 535 | 536 | [SPELLING] 537 | 538 | # Limits count of emitted suggestions for spelling mistakes. 539 | max-spelling-suggestions=4 540 | 541 | # Spelling dictionary name. No available dictionaries : You need to install 542 | # both the python package and the system dependency for enchant to work. 543 | spelling-dict= 544 | 545 | # List of comma separated words that should be considered directives if they 546 | # appear at the beginning of a comment and should not be checked. 547 | spelling-ignore-comment-directives=fmt: on,fmt: off,noqa:,noqa,nosec,isort:skip,mypy: 548 | 549 | # List of comma separated words that should not be checked. 550 | spelling-ignore-words= 551 | 552 | # A path to a file that contains the private dictionary; one word per line. 553 | spelling-private-dict-file= 554 | 555 | # Tells whether to store unknown words to the private dictionary (see the 556 | # --spelling-private-dict-file option) instead of raising a message. 557 | spelling-store-unknown-words=no 558 | 559 | 560 | [STRING] 561 | 562 | # This flag controls whether inconsistent-quotes generates a warning when the 563 | # character used as a quote delimiter is used inconsistently within a module. 564 | check-quote-consistency=no 565 | 566 | # This flag controls whether the implicit-str-concat should generate a warning 567 | # on implicit string concatenation in sequences defined over several lines. 568 | check-str-concat-over-line-jumps=no 569 | 570 | 571 | [TYPECHECK] 572 | 573 | # List of decorators that produce context managers, such as 574 | # contextlib.contextmanager. Add to this list to register other decorators that 575 | # produce valid context managers. 576 | contextmanager-decorators=contextlib.contextmanager 577 | 578 | # List of members which are set dynamically and missed by pylint inference 579 | # system, and so shouldn't trigger E1101 when accessed. Python regular 580 | # expressions are accepted. 581 | generated-members= 582 | 583 | # Tells whether to warn about missing members when the owner of the attribute 584 | # is inferred to be None. 585 | ignore-none=yes 586 | 587 | # This flag controls whether pylint should warn about no-member and similar 588 | # checks whenever an opaque object is returned when inferring. The inference 589 | # can return multiple potential results while evaluating a Python object, but 590 | # some branches might not be evaluated, which results in partial inference. In 591 | # that case, it might be useful to still emit no-member and other checks for 592 | # the rest of the inferred objects. 593 | ignore-on-opaque-inference=yes 594 | 595 | # List of symbolic message names to ignore for Mixin members. 596 | ignored-checks-for-mixins=no-member, 597 | not-async-context-manager, 598 | not-context-manager, 599 | attribute-defined-outside-init 600 | 601 | # List of class names for which member attributes should not be checked (useful 602 | # for classes with dynamically set attributes). This supports the use of 603 | # qualified names. 604 | ignored-classes=optparse.Values,thread._local,_thread._local,argparse.Namespace 605 | 606 | # Show a hint with possible names when a member name was not found. The aspect 607 | # of finding the hint is based on edit distance. 608 | missing-member-hint=yes 609 | 610 | # The minimum edit distance a name should have in order to be considered a 611 | # similar match for a missing member name. 612 | missing-member-hint-distance=1 613 | 614 | # The total number of similar names that should be taken in consideration when 615 | # showing a hint for a missing member. 616 | missing-member-max-choices=1 617 | 618 | # Regex pattern to define which classes are considered mixins. 619 | mixin-class-rgx=.*[Mm]ixin 620 | 621 | # List of decorators that change the signature of a decorated function. 622 | signature-mutators= 623 | 624 | 625 | [VARIABLES] 626 | 627 | # List of additional names supposed to be defined in builtins. Remember that 628 | # you should avoid defining new builtins when possible. 629 | additional-builtins= 630 | 631 | # Tells whether unused global variables should be treated as a violation. 632 | allow-global-unused-variables=yes 633 | 634 | # List of names allowed to shadow builtins 635 | allowed-redefined-builtins= 636 | 637 | # List of strings which can identify a callback function by name. A callback 638 | # name must start or end with one of those strings. 639 | callbacks=cb_, 640 | _cb 641 | 642 | # A regular expression matching the name of dummy variables (i.e. expected to 643 | # not be used). 644 | dummy-variables-rgx=_+$|(_[a-zA-Z0-9_]*[a-zA-Z0-9]+?$)|dummy|^ignored_|^unused_ 645 | 646 | # Argument names that match this expression will be ignored. 647 | ignored-argument-names=_.*|^ignored_|^unused_ 648 | 649 | # Tells whether we should check for unused import in __init__ files. 650 | init-import=no 651 | 652 | # List of qualified module names which can have objects that can redefine 653 | # builtins. 654 | redefining-builtins-modules=six.moves,past.builtins,future.builtins,builtins,io 655 | -------------------------------------------------------------------------------- /Pipfile.lock: -------------------------------------------------------------------------------- 1 | { 2 | "_meta": { 3 | "hash": { 4 | "sha256": "b606c4dfd4265ed35e8e69580bfa9068418001ad43466d343ccabb24afa05e3f" 5 | }, 6 | "pipfile-spec": 6, 7 | "requires": { 8 | "python_version": "3.10" 9 | }, 10 | "sources": [ 11 | { 12 | "name": "pypi", 13 | "url": "https://pypi.org/simple", 14 | "verify_ssl": true 15 | } 16 | ] 17 | }, 18 | "default": { 19 | "asgiref": { 20 | "hashes": [ 21 | "sha256:aef8a81283a34d0ab31630c9b7dfe70c812c95eba78171367ca8745e88124734", 22 | "sha256:d89f2d8cd8b56dada7d52fa7dc8075baa08fb836560710d38c292a7a3f78c04e" 23 | ], 24 | "markers": "python_version >= '3.9'", 25 | "version": "==3.10.0" 26 | }, 27 | "astroid": { 28 | "hashes": [ 29 | "sha256:4148645659b08b70d72460ed1921158027a9e53ae8b7234149b1400eddacbb93", 30 | "sha256:92fcf218b89f449cdf9f7b39a269f8d5d617b27be68434912e11e79203963a17" 31 | ], 32 | "markers": "python_full_version >= '3.8.0'", 33 | "version": "==3.0.3" 34 | }, 35 | "coverage": { 36 | "hashes": [ 37 | "sha256:0193657651f5399d433c92f8ae264aff31fc1d066deee4b831549526433f3f61", 38 | "sha256:02f2edb575d62172aa28fe00efe821ae31f25dc3d589055b3fb64d51e52e4ab1", 39 | "sha256:0491275c3b9971cdbd28a4595c2cb5838f08036bca31765bad5e17edf900b2c7", 40 | "sha256:077d366e724f24fc02dbfe9d946534357fda71af9764ff99d73c3c596001bbd7", 41 | "sha256:10e88e7f41e6197ea0429ae18f21ff521d4f4490aa33048f6c6f94c6045a6a75", 42 | "sha256:18e961aa13b6d47f758cc5879383d27b5b3f3dcd9ce8cdbfdc2571fe86feb4dd", 43 | "sha256:1a78b656a4d12b0490ca72651fe4d9f5e07e3c6461063a9b6265ee45eb2bdd35", 44 | "sha256:1ed4b95480952b1a26d863e546fa5094564aa0065e1e5f0d4d0041f293251d04", 45 | "sha256:23b27b8a698e749b61809fb637eb98ebf0e505710ec46a8aa6f1be7dc0dc43a6", 46 | "sha256:23f5881362dcb0e1a92b84b3c2809bdc90db892332daab81ad8f642d8ed55042", 47 | "sha256:32a8d985462e37cfdab611a6f95b09d7c091d07668fdc26e47a725ee575fe166", 48 | "sha256:3468cc8720402af37b6c6e7e2a9cdb9f6c16c728638a2ebc768ba1ef6f26c3a1", 49 | "sha256:379d4c7abad5afbe9d88cc31ea8ca262296480a86af945b08214eb1a556a3e4d", 50 | "sha256:3cacfaefe6089d477264001f90f55b7881ba615953414999c46cc9713ff93c8c", 51 | "sha256:3e3424c554391dc9ef4a92ad28665756566a28fecf47308f91841f6c49288e66", 52 | "sha256:46342fed0fff72efcda77040b14728049200cbba1279e0bf1188f1f2078c1d70", 53 | "sha256:536d609c6963c50055bab766d9951b6c394759190d03311f3e9fcf194ca909e1", 54 | "sha256:5d6850e6e36e332d5511a48a251790ddc545e16e8beaf046c03985c69ccb2676", 55 | "sha256:6008adeca04a445ea6ef31b2cbaf1d01d02986047606f7da266629afee982630", 56 | "sha256:64e723ca82a84053dd7bfcc986bdb34af8d9da83c521c19d6b472bc6880e191a", 57 | "sha256:6b00e21f86598b6330f0019b40fb397e705135040dbedc2ca9a93c7441178e74", 58 | "sha256:6d224f0c4c9c98290a6990259073f496fcec1b5cc613eecbd22786d398ded3ad", 59 | "sha256:6dceb61d40cbfcf45f51e59933c784a50846dc03211054bd76b421a713dcdf19", 60 | "sha256:7ac8f8eb153724f84885a1374999b7e45734bf93a87d8df1e7ce2146860edef6", 61 | "sha256:85ccc5fa54c2ed64bd91ed3b4a627b9cce04646a659512a051fa82a92c04a448", 62 | "sha256:869b5046d41abfea3e381dd143407b0d29b8282a904a19cb908fa24d090cc018", 63 | "sha256:8bdb0285a0202888d19ec6b6d23d5990410decb932b709f2b0dfe216d031d218", 64 | "sha256:8dfc5e195bbef80aabd81596ef52a1277ee7143fe419efc3c4d8ba2754671756", 65 | "sha256:8e738a492b6221f8dcf281b67129510835461132b03024830ac0e554311a5c54", 66 | "sha256:918440dea04521f499721c039863ef95433314b1db00ff826a02580c1f503e45", 67 | "sha256:9641e21670c68c7e57d2053ddf6c443e4f0a6e18e547e86af3fad0795414a628", 68 | "sha256:9d2f9d4cc2a53b38cabc2d6d80f7f9b7e3da26b2f53d48f05876fef7956b6968", 69 | "sha256:a07f61fc452c43cd5328b392e52555f7d1952400a1ad09086c4a8addccbd138d", 70 | "sha256:a3277f5fa7483c927fe3a7b017b39351610265308f5267ac6d4c2b64cc1d8d25", 71 | "sha256:a4a3907011d39dbc3e37bdc5df0a8c93853c369039b59efa33a7b6669de04c60", 72 | "sha256:aeb2c2688ed93b027eb0d26aa188ada34acb22dceea256d76390eea135083950", 73 | "sha256:b094116f0b6155e36a304ff912f89bbb5067157aff5f94060ff20bbabdc8da06", 74 | "sha256:b8ffb498a83d7e0305968289441914154fb0ef5d8b3157df02a90c6695978295", 75 | "sha256:b9bb62fac84d5f2ff523304e59e5c439955fb3b7f44e3d7b2085184db74d733b", 76 | "sha256:c61f66d93d712f6e03369b6a7769233bfda880b12f417eefdd4f16d1deb2fc4c", 77 | "sha256:ca6e61dc52f601d1d224526360cdeab0d0712ec104a2ce6cc5ccef6ed9a233bc", 78 | "sha256:ca7b26a5e456a843b9b6683eada193fc1f65c761b3a473941efe5a291f604c74", 79 | "sha256:d12c923757de24e4e2110cf8832d83a886a4cf215c6e61ed506006872b43a6d1", 80 | "sha256:d17bbc946f52ca67adf72a5ee783cd7cd3477f8f8796f59b4974a9b59cacc9ee", 81 | "sha256:dfd1e1b9f0898817babf840b77ce9fe655ecbe8b1b327983df485b30df8cc011", 82 | "sha256:e0860a348bf7004c812c8368d1fc7f77fe8e4c095d661a579196a9533778e156", 83 | "sha256:f2f5968608b1fe2a1d00d01ad1017ee27efd99b3437e08b83ded9b7af3f6f766", 84 | "sha256:f3771b23bb3675a06f5d885c3630b1d01ea6cac9e84a01aaf5508706dba546c5", 85 | "sha256:f68ef3660677e6624c8cace943e4765545f8191313a07288a53d3da188bd8581", 86 | "sha256:f86f368e1c7ce897bf2457b9eb61169a44e2ef797099fb5728482b8d69f3f016", 87 | "sha256:f90515974b39f4dea2f27c0959688621b46d96d5a626cf9c53dbc653a895c05c", 88 | "sha256:fe558371c1bdf3b8fa03e097c523fb9645b8730399c14fe7721ee9c9e2a545d3" 89 | ], 90 | "index": "pypi", 91 | "markers": "python_version >= '3.8'", 92 | "version": "==7.4.1" 93 | }, 94 | "dill": { 95 | "hashes": [ 96 | "sha256:3ebe3c479ad625c4553aca177444d89b486b1d84982eeacded644afc0cf797ca", 97 | "sha256:c36ca9ffb54365bdd2f8eb3eff7d2a21237f8452b57ace88b1ac615b7e815bd7" 98 | ], 99 | "markers": "python_version < '3.11'", 100 | "version": "==0.3.8" 101 | }, 102 | "django": { 103 | "hashes": [ 104 | "sha256:23254866a5bb9a2cfa6004e8b809ec6246eba4b58a7589bc2772f1bcc8456c7f", 105 | "sha256:37e687f7bd73ddf043e2b6b97cfe02fcbb11f2dbb3adccc6a2b18c6daa054d7f" 106 | ], 107 | "index": "pypi", 108 | "markers": "python_version >= '3.10'", 109 | "version": "==5.2.8" 110 | }, 111 | "djangorestframework": { 112 | "hashes": [ 113 | "sha256:166809528b1aced0a17dc66c24492af18049f2c9420dbd0be29422029cfc3ff7", 114 | "sha256:33a59f47fb9c85ede792cbf88bde71893bcda0667bc573f784649521f1102cec" 115 | ], 116 | "index": "pypi", 117 | "markers": "python_version >= '3.9'", 118 | "version": "==3.16.1" 119 | }, 120 | "djangorestframework-simplejwt": { 121 | "hashes": [ 122 | "sha256:2c30f3707053d384e9f315d11c2daccfcb548d4faa453111ca19a542b732e469", 123 | "sha256:e72c5572f51d7803021288e2057afcbd03f17fe11d484096f40a460abc76e87f" 124 | ], 125 | "index": "pypi", 126 | "markers": "python_version >= '3.9'", 127 | "version": "==5.5.1" 128 | }, 129 | "isort": { 130 | "hashes": [ 131 | "sha256:48fdfcb9face5d58a4f6dde2e72a1fb8dcaf8ab26f95ab49fab84c2ddefb0109", 132 | "sha256:8ca5e72a8d85860d5a3fa69b8745237f2939afe12dbf656afbcb47fe72d947a6" 133 | ], 134 | "markers": "python_full_version >= '3.8.0'", 135 | "version": "==5.13.2" 136 | }, 137 | "mccabe": { 138 | "hashes": [ 139 | "sha256:348e0240c33b60bbdf4e523192ef919f28cb2c3d7d5c7794f74009290f236325", 140 | "sha256:6c2d30ab6be0e4a46919781807b4f0d834ebdd6c6e3dca0bda5a15f863427b6e" 141 | ], 142 | "markers": "python_version >= '3.6'", 143 | "version": "==0.7.0" 144 | }, 145 | "pillow": { 146 | "hashes": [ 147 | "sha256:048ad577748b9fa4a99a0548c64f2cb8d672d5bf2e643a739ac8faff1164238c", 148 | "sha256:048eeade4c33fdf7e08da40ef402e748df113fd0b4584e32c4af74fe78baaeb2", 149 | "sha256:0ba26351b137ca4e0db0342d5d00d2e355eb29372c05afd544ebf47c0956ffeb", 150 | "sha256:0ea2a783a2bdf2a561808fe4a7a12e9aa3799b701ba305de596bc48b8bdfce9d", 151 | "sha256:1530e8f3a4b965eb6a7785cf17a426c779333eb62c9a7d1bbcf3ffd5bf77a4aa", 152 | "sha256:16563993329b79513f59142a6b02055e10514c1a8e86dca8b48a893e33cf91e3", 153 | "sha256:19aeb96d43902f0a783946a0a87dbdad5c84c936025b8419da0a0cd7724356b1", 154 | "sha256:1a1d1915db1a4fdb2754b9de292642a39a7fb28f1736699527bb649484fb966a", 155 | "sha256:1b87bd9d81d179bd8ab871603bd80d8645729939f90b71e62914e816a76fc6bd", 156 | "sha256:1dfc94946bc60ea375cc39cff0b8da6c7e5f8fcdc1d946beb8da5c216156ddd8", 157 | "sha256:2034f6759a722da3a3dbd91a81148cf884e91d1b747992ca288ab88c1de15999", 158 | "sha256:261ddb7ca91fcf71757979534fb4c128448b5b4c55cb6152d280312062f69599", 159 | "sha256:2ed854e716a89b1afcedea551cd85f2eb2a807613752ab997b9974aaa0d56936", 160 | "sha256:3102045a10945173d38336f6e71a8dc71bcaeed55c3123ad4af82c52807b9375", 161 | "sha256:339894035d0ede518b16073bdc2feef4c991ee991a29774b33e515f1d308e08d", 162 | "sha256:412444afb8c4c7a6cc11a47dade32982439925537e483be7c0ae0cf96c4f6a0b", 163 | "sha256:4203efca580f0dd6f882ca211f923168548f7ba334c189e9eab1178ab840bf60", 164 | "sha256:45ebc7b45406febf07fef35d856f0293a92e7417ae7933207e90bf9090b70572", 165 | "sha256:4b5ec25d8b17217d635f8935dbc1b9aa5907962fae29dff220f2659487891cd3", 166 | "sha256:4c8e73e99da7db1b4cad7f8d682cf6abad7844da39834c288fbfa394a47bbced", 167 | "sha256:4e6f7d1c414191c1199f8996d3f2282b9ebea0945693fb67392c75a3a320941f", 168 | "sha256:4eaa22f0d22b1a7e93ff0a596d57fdede2e550aecffb5a1ef1106aaece48e96b", 169 | "sha256:50b8eae8f7334ec826d6eeffaeeb00e36b5e24aa0b9df322c247539714c6df19", 170 | "sha256:50fd3f6b26e3441ae07b7c979309638b72abc1a25da31a81a7fbd9495713ef4f", 171 | "sha256:51243f1ed5161b9945011a7360e997729776f6e5d7005ba0c6879267d4c5139d", 172 | "sha256:5d512aafa1d32efa014fa041d38868fda85028e3f930a96f85d49c7d8ddc0383", 173 | "sha256:5f77cf66e96ae734717d341c145c5949c63180842a545c47a0ce7ae52ca83795", 174 | "sha256:6b02471b72526ab8a18c39cb7967b72d194ec53c1fd0a70b050565a0f366d355", 175 | "sha256:6fb1b30043271ec92dc65f6d9f0b7a830c210b8a96423074b15c7bc999975f57", 176 | "sha256:7161ec49ef0800947dc5570f86568a7bb36fa97dd09e9827dc02b718c5643f09", 177 | "sha256:72d622d262e463dfb7595202d229f5f3ab4b852289a1cd09650362db23b9eb0b", 178 | "sha256:74d28c17412d9caa1066f7a31df8403ec23d5268ba46cd0ad2c50fb82ae40462", 179 | "sha256:78618cdbccaa74d3f88d0ad6cb8ac3007f1a6fa5c6f19af64b55ca170bfa1edf", 180 | "sha256:793b4e24db2e8742ca6423d3fde8396db336698c55cd34b660663ee9e45ed37f", 181 | "sha256:798232c92e7665fe82ac085f9d8e8ca98826f8e27859d9a96b41d519ecd2e49a", 182 | "sha256:81d09caa7b27ef4e61cb7d8fbf1714f5aec1c6b6c5270ee53504981e6e9121ad", 183 | "sha256:8ab74c06ffdab957d7670c2a5a6e1a70181cd10b727cd788c4dd9005b6a8acd9", 184 | "sha256:8eb0908e954d093b02a543dc963984d6e99ad2b5e36503d8a0aaf040505f747d", 185 | "sha256:90b9e29824800e90c84e4022dd5cc16eb2d9605ee13f05d47641eb183cd73d45", 186 | "sha256:9797a6c8fe16f25749b371c02e2ade0efb51155e767a971c61734b1bf6293994", 187 | "sha256:9d2455fbf44c914840c793e89aa82d0e1763a14253a000743719ae5946814b2d", 188 | "sha256:9d3bea1c75f8c53ee4d505c3e67d8c158ad4df0d83170605b50b64025917f338", 189 | "sha256:9e2ec1e921fd07c7cda7962bad283acc2f2a9ccc1b971ee4b216b75fad6f0463", 190 | "sha256:9e91179a242bbc99be65e139e30690e081fe6cb91a8e77faf4c409653de39451", 191 | "sha256:a0eaa93d054751ee9964afa21c06247779b90440ca41d184aeb5d410f20ff591", 192 | "sha256:a2c405445c79c3f5a124573a051062300936b0281fee57637e706453e452746c", 193 | "sha256:aa7e402ce11f0885305bfb6afb3434b3cd8f53b563ac065452d9d5654c7b86fd", 194 | "sha256:aff76a55a8aa8364d25400a210a65ff59d0168e0b4285ba6bf2bd83cf675ba32", 195 | "sha256:b09b86b27a064c9624d0a6c54da01c1beaf5b6cadfa609cf63789b1d08a797b9", 196 | "sha256:b14f16f94cbc61215115b9b1236f9c18403c15dd3c52cf629072afa9d54c1cbf", 197 | "sha256:b50811d664d392f02f7761621303eba9d1b056fb1868c8cdf4231279645c25f5", 198 | "sha256:b7bc2176354defba3edc2b9a777744462da2f8e921fbaf61e52acb95bafa9828", 199 | "sha256:c78e1b00a87ce43bb37642c0812315b411e856a905d58d597750eb79802aaaa3", 200 | "sha256:c83341b89884e2b2e55886e8fbbf37c3fa5efd6c8907124aeb72f285ae5696e5", 201 | "sha256:ca2870d5d10d8726a27396d3ca4cf7976cec0f3cb706debe88e3a5bd4610f7d2", 202 | "sha256:ccce24b7ad89adb5a1e34a6ba96ac2530046763912806ad4c247356a8f33a67b", 203 | "sha256:cd5e14fbf22a87321b24c88669aad3a51ec052eb145315b3da3b7e3cc105b9a2", 204 | "sha256:ce49c67f4ea0609933d01c0731b34b8695a7a748d6c8d186f95e7d085d2fe475", 205 | "sha256:d33891be6df59d93df4d846640f0e46f1a807339f09e79a8040bc887bdcd7ed3", 206 | "sha256:d3b2348a78bc939b4fed6552abfd2e7988e0f81443ef3911a4b8498ca084f6eb", 207 | "sha256:d886f5d353333b4771d21267c7ecc75b710f1a73d72d03ca06df49b09015a9ef", 208 | "sha256:d93480005693d247f8346bc8ee28c72a2191bdf1f6b5db469c096c0c867ac015", 209 | "sha256:dc1a390a82755a8c26c9964d457d4c9cbec5405896cba94cf51f36ea0d855002", 210 | "sha256:dd78700f5788ae180b5ee8902c6aea5a5726bac7c364b202b4b3e3ba2d293170", 211 | "sha256:e46f38133e5a060d46bd630faa4d9fa0202377495df1f068a8299fd78c84de84", 212 | "sha256:e4b878386c4bf293578b48fc570b84ecfe477d3b77ba39a6e87150af77f40c57", 213 | "sha256:f0d0591a0aeaefdaf9a5e545e7485f89910c977087e7de2b6c388aec32011e9f", 214 | "sha256:fdcbb4068117dfd9ce0138d068ac512843c52295ed996ae6dd1faf537b6dbc27", 215 | "sha256:ff61bfd9253c3915e6d41c651d5f962da23eda633cf02262990094a18a55371a" 216 | ], 217 | "index": "pypi", 218 | "markers": "python_version >= '3.8'", 219 | "version": "==10.3.0" 220 | }, 221 | "pipfile": { 222 | "hashes": [ 223 | "sha256:f7d9f15de8b660986557eb3cc5391aa1a16207ac41bc378d03f414762d36c984" 224 | ], 225 | "index": "pypi", 226 | "version": "==0.0.2" 227 | }, 228 | "platformdirs": { 229 | "hashes": [ 230 | "sha256:0614df2a2f37e1a662acbd8e2b25b92ccf8632929bc6d43467e17fe89c75e068", 231 | "sha256:ef0cc731df711022c174543cb70a9b5bd22e5a9337c8624ef2c2ceb8ddad8768" 232 | ], 233 | "markers": "python_version >= '3.8'", 234 | "version": "==4.2.0" 235 | }, 236 | "psycopg2-binary": { 237 | "hashes": [ 238 | "sha256:03ef7df18daf2c4c07e2695e8cfd5ee7f748a1d54d802330985a78d2a5a6dca9", 239 | "sha256:0a602ea5aff39bb9fac6308e9c9d82b9a35c2bf288e184a816002c9fae930b77", 240 | "sha256:0c009475ee389757e6e34611d75f6e4f05f0cf5ebb76c6037508318e1a1e0d7e", 241 | "sha256:0ef4854e82c09e84cc63084a9e4ccd6d9b154f1dbdd283efb92ecd0b5e2b8c84", 242 | "sha256:1236ed0952fbd919c100bc839eaa4a39ebc397ed1c08a97fc45fee2a595aa1b3", 243 | "sha256:143072318f793f53819048fdfe30c321890af0c3ec7cb1dfc9cc87aa88241de2", 244 | "sha256:15208be1c50b99203fe88d15695f22a5bed95ab3f84354c494bcb1d08557df67", 245 | "sha256:1873aade94b74715be2246321c8650cabf5a0d098a95bab81145ffffa4c13876", 246 | "sha256:18d0ef97766055fec15b5de2c06dd8e7654705ce3e5e5eed3b6651a1d2a9a152", 247 | "sha256:1ea665f8ce695bcc37a90ee52de7a7980be5161375d42a0b6c6abedbf0d81f0f", 248 | "sha256:2293b001e319ab0d869d660a704942c9e2cce19745262a8aba2115ef41a0a42a", 249 | "sha256:246b123cc54bb5361588acc54218c8c9fb73068bf227a4a531d8ed56fa3ca7d6", 250 | "sha256:275ff571376626195ab95a746e6a04c7df8ea34638b99fc11160de91f2fef503", 251 | "sha256:281309265596e388ef483250db3640e5f414168c5a67e9c665cafce9492eda2f", 252 | "sha256:2d423c8d8a3c82d08fe8af900ad5b613ce3632a1249fd6a223941d0735fce493", 253 | "sha256:2e5afae772c00980525f6d6ecf7cbca55676296b580c0e6abb407f15f3706996", 254 | "sha256:30dcc86377618a4c8f3b72418df92e77be4254d8f89f14b8e8f57d6d43603c0f", 255 | "sha256:31a34c508c003a4347d389a9e6fcc2307cc2150eb516462a7a17512130de109e", 256 | "sha256:323ba25b92454adb36fa425dc5cf6f8f19f78948cbad2e7bc6cdf7b0d7982e59", 257 | "sha256:34eccd14566f8fe14b2b95bb13b11572f7c7d5c36da61caf414d23b91fcc5d94", 258 | "sha256:3a58c98a7e9c021f357348867f537017057c2ed7f77337fd914d0bedb35dace7", 259 | "sha256:3f78fd71c4f43a13d342be74ebbc0666fe1f555b8837eb113cb7416856c79682", 260 | "sha256:4154ad09dac630a0f13f37b583eae260c6aa885d67dfbccb5b02c33f31a6d420", 261 | "sha256:420f9bbf47a02616e8554e825208cb947969451978dceb77f95ad09c37791dae", 262 | "sha256:4686818798f9194d03c9129a4d9a702d9e113a89cb03bffe08c6cf799e053291", 263 | "sha256:57fede879f08d23c85140a360c6a77709113efd1c993923c59fde17aa27599fe", 264 | "sha256:60989127da422b74a04345096c10d416c2b41bd7bf2a380eb541059e4e999980", 265 | "sha256:64cf30263844fa208851ebb13b0732ce674d8ec6a0c86a4e160495d299ba3c93", 266 | "sha256:68fc1f1ba168724771e38bee37d940d2865cb0f562380a1fb1ffb428b75cb692", 267 | "sha256:6e6f98446430fdf41bd36d4faa6cb409f5140c1c2cf58ce0bbdaf16af7d3f119", 268 | "sha256:729177eaf0aefca0994ce4cffe96ad3c75e377c7b6f4efa59ebf003b6d398716", 269 | "sha256:72dffbd8b4194858d0941062a9766f8297e8868e1dd07a7b36212aaa90f49472", 270 | "sha256:75723c3c0fbbf34350b46a3199eb50638ab22a0228f93fb472ef4d9becc2382b", 271 | "sha256:77853062a2c45be16fd6b8d6de2a99278ee1d985a7bd8b103e97e41c034006d2", 272 | "sha256:78151aa3ec21dccd5cdef6c74c3e73386dcdfaf19bced944169697d7ac7482fc", 273 | "sha256:7f01846810177d829c7692f1f5ada8096762d9172af1b1a28d4ab5b77c923c1c", 274 | "sha256:804d99b24ad523a1fe18cc707bf741670332f7c7412e9d49cb5eab67e886b9b5", 275 | "sha256:81ff62668af011f9a48787564ab7eded4e9fb17a4a6a74af5ffa6a457400d2ab", 276 | "sha256:8359bf4791968c5a78c56103702000105501adb557f3cf772b2c207284273984", 277 | "sha256:83791a65b51ad6ee6cf0845634859d69a038ea9b03d7b26e703f94c7e93dbcf9", 278 | "sha256:8532fd6e6e2dc57bcb3bc90b079c60de896d2128c5d9d6f24a63875a95a088cf", 279 | "sha256:876801744b0dee379e4e3c38b76fc89f88834bb15bf92ee07d94acd06ec890a0", 280 | "sha256:8dbf6d1bc73f1d04ec1734bae3b4fb0ee3cb2a493d35ede9badbeb901fb40f6f", 281 | "sha256:8f8544b092a29a6ddd72f3556a9fcf249ec412e10ad28be6a0c0d948924f2212", 282 | "sha256:911dda9c487075abd54e644ccdf5e5c16773470a6a5d3826fda76699410066fb", 283 | "sha256:977646e05232579d2e7b9c59e21dbe5261f403a88417f6a6512e70d3f8a046be", 284 | "sha256:9dba73be7305b399924709b91682299794887cbbd88e38226ed9f6712eabee90", 285 | "sha256:a148c5d507bb9b4f2030a2025c545fccb0e1ef317393eaba42e7eabd28eb6041", 286 | "sha256:a6cdcc3ede532f4a4b96000b6362099591ab4a3e913d70bcbac2b56c872446f7", 287 | "sha256:ac05fb791acf5e1a3e39402641827780fe44d27e72567a000412c648a85ba860", 288 | "sha256:b0605eaed3eb239e87df0d5e3c6489daae3f7388d455d0c0b4df899519c6a38d", 289 | "sha256:b58b4710c7f4161b5e9dcbe73bb7c62d65670a87df7bcce9e1faaad43e715245", 290 | "sha256:b6356793b84728d9d50ead16ab43c187673831e9d4019013f1402c41b1db9b27", 291 | "sha256:b76bedd166805480ab069612119ea636f5ab8f8771e640ae103e05a4aae3e417", 292 | "sha256:bc7bb56d04601d443f24094e9e31ae6deec9ccb23581f75343feebaf30423359", 293 | "sha256:c2470da5418b76232f02a2fcd2229537bb2d5a7096674ce61859c3229f2eb202", 294 | "sha256:c332c8d69fb64979ebf76613c66b985414927a40f8defa16cf1bc028b7b0a7b0", 295 | "sha256:c6af2a6d4b7ee9615cbb162b0738f6e1fd1f5c3eda7e5da17861eacf4c717ea7", 296 | "sha256:c77e3d1862452565875eb31bdb45ac62502feabbd53429fdc39a1cc341d681ba", 297 | "sha256:ca08decd2697fdea0aea364b370b1249d47336aec935f87b8bbfd7da5b2ee9c1", 298 | "sha256:ca49a8119c6cbd77375ae303b0cfd8c11f011abbbd64601167ecca18a87e7cdd", 299 | "sha256:cb16c65dcb648d0a43a2521f2f0a2300f40639f6f8c1ecbc662141e4e3e1ee07", 300 | "sha256:d2997c458c690ec2bc6b0b7ecbafd02b029b7b4283078d3b32a852a7ce3ddd98", 301 | "sha256:d3f82c171b4ccd83bbaf35aa05e44e690113bd4f3b7b6cc54d2219b132f3ae55", 302 | "sha256:dc4926288b2a3e9fd7b50dc6a1909a13bbdadfc67d93f3374d984e56f885579d", 303 | "sha256:ead20f7913a9c1e894aebe47cccf9dc834e1618b7aa96155d2091a626e59c972", 304 | "sha256:ebdc36bea43063116f0486869652cb2ed7032dbc59fbcb4445c4862b5c1ecf7f", 305 | "sha256:ed1184ab8f113e8d660ce49a56390ca181f2981066acc27cf637d5c1e10ce46e", 306 | "sha256:ee825e70b1a209475622f7f7b776785bd68f34af6e7a46e2e42f27b659b5bc26", 307 | "sha256:f7ae5d65ccfbebdfa761585228eb4d0df3a8b15cfb53bd953e713e09fbb12957", 308 | "sha256:f7fc5a5acafb7d6ccca13bfa8c90f8c51f13d8fb87d95656d3950f0158d3ce53", 309 | "sha256:f9b5571d33660d5009a8b3c25dc1db560206e2d2f89d3df1cb32d72c0d117d52" 310 | ], 311 | "index": "pypi", 312 | "markers": "python_version >= '3.7'", 313 | "version": "==2.9.9" 314 | }, 315 | "pyjwt": { 316 | "hashes": [ 317 | "sha256:3cc5772eb20009233caf06e9d8a0577824723b44e6648ee0a2aedb6cf9381953", 318 | "sha256:dcdd193e30abefd5debf142f9adfcdd2b58004e644f25406ffaebd50bd98dacb" 319 | ], 320 | "markers": "python_version >= '3.9'", 321 | "version": "==2.10.1" 322 | }, 323 | "pylint": { 324 | "hashes": [ 325 | "sha256:58c2398b0301e049609a8429789ec6edf3aabe9b6c5fec916acd18639c16de8b", 326 | "sha256:7a1585285aefc5165db81083c3e06363a27448f6b467b3b0f30dbd0ac1f73810" 327 | ], 328 | "index": "pypi", 329 | "markers": "python_full_version >= '3.8.0'", 330 | "version": "==3.0.3" 331 | }, 332 | "pylint-django": { 333 | "hashes": [ 334 | "sha256:2f339e4bf55776958283395c5139c37700c91bd5ef1d8251ef6ac88b5abbba9b", 335 | "sha256:5abd5c2228e0e5e2a4cb6d0b4fc1d1cef1e773d0be911412f4dd4fc1a1a440b7" 336 | ], 337 | "index": "pypi", 338 | "markers": "python_version >= '3.7' and python_version < '4.0'", 339 | "version": "==2.5.5" 340 | }, 341 | "pylint-plugin-utils": { 342 | "hashes": [ 343 | "sha256:ae11664737aa2effbf26f973a9e0b6779ab7106ec0adc5fe104b0907ca04e507", 344 | "sha256:d3cebf68a38ba3fba23a873809155562571386d4c1b03e5b4c4cc26c3eee93e4" 345 | ], 346 | "markers": "python_version >= '3.7' and python_version < '4.0'", 347 | "version": "==0.8.2" 348 | }, 349 | "python-decouple": { 350 | "hashes": [ 351 | "sha256:ba6e2657d4f376ecc46f77a3a615e058d93ba5e465c01bbe57289bfb7cce680f", 352 | "sha256:d0d45340815b25f4de59c974b855bb38d03151d81b037d9e3f463b0c9f8cbd66" 353 | ], 354 | "index": "pypi", 355 | "version": "==3.8" 356 | }, 357 | "pytz": { 358 | "hashes": [ 359 | "sha256:2a29735ea9c18baf14b448846bde5a48030ed267578472d8955cd0e7443a9812", 360 | "sha256:328171f4e3623139da4983451950b28e95ac706e13f3f2630a879749e7a8b319" 361 | ], 362 | "version": "==2024.1" 363 | }, 364 | "sqlparse": { 365 | "hashes": [ 366 | "sha256:09f67787f56a0b16ecdbde1bfc7f5d9c3371ca683cfeaa8e6ff60b4807ec9272", 367 | "sha256:cf2196ed3418f3ba5de6af7e82c694a9fbdbfecccdfc72e281548517081f16ca" 368 | ], 369 | "markers": "python_version >= '3.8'", 370 | "version": "==0.5.3" 371 | }, 372 | "toml": { 373 | "hashes": [ 374 | "sha256:806143ae5bfb6a3c6e736a764057db0e6a0e05e338b5630894a5f779cabb4f9b", 375 | "sha256:b3bda1d108d5dd99f4a20d24d9c348e91c4db7ab1b749200bded2f839ccbe68f" 376 | ], 377 | "markers": "python_version >= '2.6' and python_version not in '3.0, 3.1, 3.2, 3.3'", 378 | "version": "==0.10.2" 379 | }, 380 | "tomli": { 381 | "hashes": [ 382 | "sha256:939de3e7a6161af0c887ef91b7d41a53e7c5a1ca976325f429cb46ea9bc30ecc", 383 | "sha256:de526c12914f0c550d15924c62d72abc48d6fe7364aa87328337a31007fe8a4f" 384 | ], 385 | "markers": "python_version < '3.11'", 386 | "version": "==2.0.1" 387 | }, 388 | "tomlkit": { 389 | "hashes": [ 390 | "sha256:75baf5012d06501f07bee5bf8e801b9f343e7aac5a92581f20f80ce632e6b5a4", 391 | "sha256:b0a645a9156dc7cb5d3a1f0d4bab66db287fcb8e0430bdd4664a095ea16414ba" 392 | ], 393 | "markers": "python_version >= '3.7'", 394 | "version": "==0.12.3" 395 | }, 396 | "typing-extensions": { 397 | "hashes": [ 398 | "sha256:0cea48d173cc12fa28ecabc3b837ea3cf6f38c6d1136f85cbaaf598984861466", 399 | "sha256:f0fa19c6845758ab08074a0cfa8b7aecb71c999ca73d62883bc25cc018c4e548" 400 | ], 401 | "markers": "python_version >= '3.9'", 402 | "version": "==4.15.0" 403 | } 404 | }, 405 | "develop": {} 406 | } 407 | --------------------------------------------------------------------------------