diff --git a/.circleci/config.yml b/.circleci/config.yml index b5006865..4169dd8d 100644 --- a/.circleci/config.yml +++ b/.circleci/config.yml @@ -24,7 +24,7 @@ jobs: name: Bootstrap the project command: | . venv/bin/activate - make + make test - persist_to_workspace: root: . diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index e1ba0053..cc285aca 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -21,4 +21,4 @@ jobs: run: pip install cookiecutter - name: Bootstrap the project - run: make + run: make test diff --git a/README.md b/README.md index 4cc6083f..ab114ed6 100644 --- a/README.md +++ b/README.md @@ -19,6 +19,7 @@ * [Whitenoise](http://whitenoise.evans.io) for effortless static files hosting * Sentry. Set `SENTRY_DSN` env var if you need it. * cloudflare-ready with [django-ipware](https://github.com/un33k/django-ipware) +* [black](https://github.com/psf/black) as uncompromising code formatter ## Optional next steps You definetely should consider this steps after installation: diff --git a/hooks/post_gen_project.sh b/hooks/post_gen_project.sh index f5760675..926215f4 100644 --- a/hooks/post_gen_project.sh +++ b/hooks/post_gen_project.sh @@ -21,6 +21,9 @@ echo Running initial migrations... ./manage.py migrate cd ../ +echo Apply formatting.. +make fmt + echo Running flake8.. make lint diff --git a/{{cookiecutter.project_slug}}/Makefile b/{{cookiecutter.project_slug}}/Makefile index 0e906369..e7203ba4 100644 --- a/{{cookiecutter.project_slug}}/Makefile +++ b/{{cookiecutter.project_slug}}/Makefile @@ -10,6 +10,10 @@ deps: dev-deps: deps pip-compile --extra=dev --output-file=dev-requirements.txt pyproject.toml +fmt: + cd src && isort . + cd src && black . + lint: dotenv-linter src/app/.env.ci flake8 src diff --git a/{{cookiecutter.project_slug}}/dev-requirements.txt b/{{cookiecutter.project_slug}}/dev-requirements.txt index 84b27bd4..0ee56ed4 100644 --- a/{{cookiecutter.project_slug}}/dev-requirements.txt +++ b/{{cookiecutter.project_slug}}/dev-requirements.txt @@ -121,7 +121,6 @@ flake8==5.0.4 # via # flake8-absolute-import # flake8-bugbear - # flake8-commas # flake8-django # flake8-eradicate # flake8-isort @@ -143,8 +142,6 @@ flake8-bugbear==22.10.27 # via django (pyproject.toml) flake8-cognitive-complexity==0.1.0 # via django (pyproject.toml) -flake8-commas==2.1.0 - # via django (pyproject.toml) flake8-django==1.1.5 # via django (pyproject.toml) flake8-eradicate==1.4.0 diff --git a/{{cookiecutter.project_slug}}/pyproject.toml b/{{cookiecutter.project_slug}}/pyproject.toml index 36760538..092ab223 100644 --- a/{{cookiecutter.project_slug}}/pyproject.toml +++ b/{{cookiecutter.project_slug}}/pyproject.toml @@ -34,6 +34,8 @@ dev = [ "pytest-mock", "pytest-randomly", + "black", + "dotenv-linter", "freezegun", @@ -42,9 +44,9 @@ dev = [ "jedi", "flake8-absolute-import", "flake8-aaa", + "flake8-black", "flake8-bugbear", "flake8-cognitive-complexity", - "flake8-commas", "flake8-django", "flake8-eradicate", "flake8-isort>=4.0.0", @@ -74,6 +76,7 @@ dev = [ [tool.flake8] max-line-length = 160 +inline-quotes = "\"" ignore = [ "DJ05", # URLs include() should set a namespace "E501", # Line too long @@ -82,6 +85,7 @@ ignore = [ "PT001", # Use @pytest.fixture() over @pytest.fixture "SIM102", # Use a single if-statement instead of nested if-statements "SIM113", # Use enumerate instead of manually incrementing a counter + "E203", # whitespace before ':', disabled for black purposes https://black.readthedocs.io/en/stable/the_black_code_style/current_style.html#slices ] exclude = [ "static", @@ -98,3 +102,15 @@ line_length = 160 extra_standard_library = ["pytest"] known_django = ["django", "restframework"] sections = ["FUTURE", "STDLIB", "THIRDPARTY", "DJANGO", "FIRSTPARTY", "LOCALFOLDER"] +use_parentheses = true +include_trailing_comma = true +multi_line_output = 3 + + +[tool.black] +exclude = ''' +/( + | migrations +)/ +''' +line_length = 160 diff --git a/{{cookiecutter.project_slug}}/src/.django-app-template/apps.py-tpl b/{{cookiecutter.project_slug}}/src/.django-app-template/apps.py-tpl index 27c7581c..865202f2 100644 --- a/{{cookiecutter.project_slug}}/src/.django-app-template/apps.py-tpl +++ b/{{cookiecutter.project_slug}}/src/.django-app-template/apps.py-tpl @@ -2,4 +2,4 @@ from app.base_config import AppConfig class {{ camel_case_app_name }}Config(AppConfig): - name = '{{ app_name }}' + name = "{{ app_name }}" diff --git a/{{cookiecutter.project_slug}}/src/a12n/api/throttling.py b/{{cookiecutter.project_slug}}/src/a12n/api/throttling.py index 106e6518..e2a690aa 100644 --- a/{{cookiecutter.project_slug}}/src/a12n/api/throttling.py +++ b/{{cookiecutter.project_slug}}/src/a12n/api/throttling.py @@ -5,4 +5,5 @@ class AuthAnonRateThrottle(ConfigurableThrottlingMixin, AnonRateThrottle): """Throttle for any authorization views.""" - scope = 'anon-auth' + + scope = "anon-auth" diff --git a/{{cookiecutter.project_slug}}/src/a12n/tests/jwt_views/test_obtain_jwt_view.py b/{{cookiecutter.project_slug}}/src/a12n/tests/jwt_views/test_obtain_jwt_view.py index 5ddb51b2..ec75c499 100644 --- a/{{cookiecutter.project_slug}}/src/a12n/tests/jwt_views/test_obtain_jwt_view.py +++ b/{{cookiecutter.project_slug}}/src/a12n/tests/jwt_views/test_obtain_jwt_view.py @@ -14,50 +14,60 @@ def _enable_django_axes(settings): @pytest.fixture def get_token(as_user): def _get_token(username, password, expected_status=201): - return as_user.post('/api/v1/auth/token/', { - 'username': username, - 'password': password, - }, format='json', expected_status=expected_status) + return as_user.post( + "/api/v1/auth/token/", + { + "username": username, + "password": password, + }, + format="json", + expected_status=expected_status, + ) return _get_token def _decode(response): - return json.loads(response.content.decode('utf-8', errors='ignore')) + return json.loads(response.content.decode("utf-8", errors="ignore")) def test_getting_token_ok(as_user, get_token): result = get_token(as_user.user.username, as_user.password) - assert 'token' in result + assert "token" in result def test_getting_token_is_token(as_user, get_token): result = get_token(as_user.user.username, as_user.password) - assert len(result['token']) > 32 # every stuff that is long enough, may be a JWT token + assert len(result["token"]) > 32 # every stuff that is long enough, may be a JWT token def test_getting_token_with_incorrect_password(as_user, get_token): - result = get_token(as_user.user.username, 'z3r0c00l', expected_status=400) + result = get_token(as_user.user.username, "z3r0c00l", expected_status=400) - assert 'nonFieldErrors' in result + assert "nonFieldErrors" in result def test_getting_token_with_incorrect_password_creates_access_attempt_log_entry(as_user, get_token): - get_token(as_user.user.username, 'z3r0c00l', expected_status=400) # act + get_token(as_user.user.username, "z3r0c00l", expected_status=400) # act assert AccessAttempt.objects.count() == 1 -@pytest.mark.parametrize(('extract_token', 'status_code'), [ # NOQA: AAA01 - (lambda response: response['token'], 200), - (lambda *args: 'eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJ1c2VybmFtZSI6InRpbW90aHk5NSIsImlhdCI6MjQ5MzI0NDgwMCwiZXhwIjoyNDkzMjQ1MTAwLCJqdGkiOiI2MWQ2MTE3YS1iZWNlLTQ5YWEtYWViYi1mOGI4MzBhZDBlNzgiLCJ1c2VyX2lkIjoxLCJvcmlnX2lhdCI6MjQ5MzI0NDgwMH0.YQnk0vSshNQRTAuq1ilddc9g3CZ0s9B0PQEIk5pWa9I', 401), - (lambda *args: 'sh1t', 401), -]) +@pytest.mark.parametrize( + ("extract_token", "status_code"), + [ + (lambda response: response["token"], 200), + ( + lambda *args: "eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJ1c2VybmFtZSI6InRpbW90aHk5NSIsImlhdCI6MjQ5MzI0NDgwMCwiZXhwIjoyNDkzMjQ1MTAwLCJqdGkiOiI2MWQ2MTE3YS1iZWNlLTQ5YWEtYWViYi1mOGI4MzBhZDBlNzgiLCJ1c2VyX2lkIjoxLCJvcmlnX2lhdCI6MjQ5MzI0NDgwMH0.YQnk0vSshNQRTAuq1ilddc9g3CZ0s9B0PQEIk5pWa9I", + 401, + ), + (lambda *args: "sh1t", 401), + ], +) def test_received_token_works(as_user, get_token, as_anon, extract_token, status_code): token = extract_token(get_token(as_user.user.username, as_user.password)) + as_anon.credentials(HTTP_AUTHORIZATION=f"Bearer {token}") - as_anon.credentials(HTTP_AUTHORIZATION=f'Bearer {token}') - - as_anon.get('/api/v1/users/me/', expected_status=status_code) + as_anon.get("/api/v1/users/me/", expected_status=status_code) # act diff --git a/{{cookiecutter.project_slug}}/src/a12n/tests/jwt_views/test_refresh_jwt_token.py b/{{cookiecutter.project_slug}}/src/a12n/tests/jwt_views/test_refresh_jwt_token.py index 390ec08f..f9d66cbe 100644 --- a/{{cookiecutter.project_slug}}/src/a12n/tests/jwt_views/test_refresh_jwt_token.py +++ b/{{cookiecutter.project_slug}}/src/a12n/tests/jwt_views/test_refresh_jwt_token.py @@ -6,62 +6,67 @@ pytestmark = [ pytest.mark.django_db, - pytest.mark.freeze_time('2049-01-05'), + pytest.mark.freeze_time("2049-01-05"), ] @pytest.fixture def refresh_token(as_user): def _refresh_token(token, expected_status=201): - return as_user.post('/api/v1/auth/token/refresh/', { - 'token': token, - }, format='json', expected_status=expected_status) + return as_user.post( + "/api/v1/auth/token/refresh/", + { + "token": token, + }, + format="json", + expected_status=expected_status, + ) return _refresh_token @pytest.fixture def initial_token(as_user): - with freeze_time('2049-01-03'): + with freeze_time("2049-01-03"): return get_jwt(as_user.user) def test_refresh_token_ok(initial_token, refresh_token): result = refresh_token(initial_token) - assert 'token' in result + assert "token" in result def test_refreshed_token_is_a_token(initial_token, refresh_token): result = refresh_token(initial_token) - assert len(result['token']) > 32 + assert len(result["token"]) > 32 def test_refreshed_token_is_new_one(initial_token, refresh_token): result = refresh_token(initial_token) - assert result['token'] != initial_token + assert result["token"] != initial_token def test_refresh_token_fails_with_incorrect_previous_token(refresh_token): - result = refresh_token('some-invalid-previous-token', expected_status=400) + result = refresh_token("some-invalid-previous-token", expected_status=400) - assert 'nonFieldErrors' in result + assert "nonFieldErrors" in result def test_token_is_not_allowed_to_refresh_if_expired(initial_token, refresh_token): - with freeze_time('2049-02-05'): + with freeze_time("2049-02-05"): result = refresh_token(initial_token, expected_status=400) - assert 'expired' in result['nonFieldErrors'][0] + assert "expired" in result["nonFieldErrors"][0] def test_received_token_works(as_anon, refresh_token, initial_token): - token = refresh_token(initial_token)['token'] - as_anon.credentials(HTTP_AUTHORIZATION=f'Bearer {token}') + token = refresh_token(initial_token)["token"] + as_anon.credentials(HTTP_AUTHORIZATION=f"Bearer {token}") - result = as_anon.get('/api/v1/users/me/') + result = as_anon.get("/api/v1/users/me/") assert result is not None diff --git a/{{cookiecutter.project_slug}}/src/a12n/urls.py b/{{cookiecutter.project_slug}}/src/a12n/urls.py index 70011c9a..955c2c52 100644 --- a/{{cookiecutter.project_slug}}/src/a12n/urls.py +++ b/{{cookiecutter.project_slug}}/src/a12n/urls.py @@ -2,8 +2,8 @@ from a12n.api import views -app_name = 'a12n' +app_name = "a12n" urlpatterns = [ - path('token/', views.ObtainJSONWebTokenView.as_view()), - path('token/refresh/', views.RefreshJSONWebTokenView.as_view()), + path("token/", views.ObtainJSONWebTokenView.as_view()), + path("token/refresh/", views.RefreshJSONWebTokenView.as_view()), ] diff --git a/{{cookiecutter.project_slug}}/src/app/admin/README.md b/{{cookiecutter.project_slug}}/src/app/admin/README.md index 26f560f6..ed2902db 100644 --- a/{{cookiecutter.project_slug}}/src/app/admin/README.md +++ b/{{cookiecutter.project_slug}}/src/app/admin/README.md @@ -10,6 +10,6 @@ from books.models import Book @admin.register(Book) class BookAdmin(ModelAdmin): fields = [ - 'name', + "name", ] ``` \ No newline at end of file diff --git a/{{cookiecutter.project_slug}}/src/app/admin/__init__.py b/{{cookiecutter.project_slug}}/src/app/admin/__init__.py index b37178f2..cb848394 100644 --- a/{{cookiecutter.project_slug}}/src/app/admin/__init__.py +++ b/{{cookiecutter.project_slug}}/src/app/admin/__init__.py @@ -3,6 +3,6 @@ from app.admin.model_admin import ModelAdmin __all__ = [ - 'admin', - 'ModelAdmin', + "admin", + "ModelAdmin", ] diff --git a/{{cookiecutter.project_slug}}/src/app/api/pagination.py b/{{cookiecutter.project_slug}}/src/app/api/pagination.py index c0a64d58..f690eb3e 100644 --- a/{{cookiecutter.project_slug}}/src/app/api/pagination.py +++ b/{{cookiecutter.project_slug}}/src/app/api/pagination.py @@ -4,5 +4,5 @@ class AppPagination(PageNumberPagination): - page_size_query_param = 'page_size' + page_size_query_param = "page_size" max_page_size = settings.MAX_PAGE_SIZE diff --git a/{{cookiecutter.project_slug}}/src/app/api/renderers.py b/{{cookiecutter.project_slug}}/src/app/api/renderers.py index eba15f98..89fded20 100644 --- a/{{cookiecutter.project_slug}}/src/app/api/renderers.py +++ b/{{cookiecutter.project_slug}}/src/app/api/renderers.py @@ -2,5 +2,5 @@ class AppJSONRenderer(CamelCaseJSONRenderer): - charset = 'utf-8' # force DRF to add charset header to the content-type - json_underscoreize = {'no_underscore_before_number': True} # https://github.com/vbabiy/djangorestframework-camel-case#underscoreize-options + charset = "utf-8" # force DRF to add charset header to the content-type + json_underscoreize = {"no_underscore_before_number": True} # https://github.com/vbabiy/djangorestframework-camel-case#underscoreize-options diff --git a/{{cookiecutter.project_slug}}/src/app/asgi.py b/{{cookiecutter.project_slug}}/src/app/asgi.py index 0c71a7f9..8b0b6000 100644 --- a/{{cookiecutter.project_slug}}/src/app/asgi.py +++ b/{{cookiecutter.project_slug}}/src/app/asgi.py @@ -11,6 +11,6 @@ from django.core.asgi import get_asgi_application -os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'app.settings') +os.environ.setdefault("DJANGO_SETTINGS_MODULE", "app.settings") application = get_asgi_application() diff --git a/{{cookiecutter.project_slug}}/src/app/base_config.py b/{{cookiecutter.project_slug}}/src/app/base_config.py index 6cd2a3bd..ab876777 100644 --- a/{{cookiecutter.project_slug}}/src/app/base_config.py +++ b/{{cookiecutter.project_slug}}/src/app/base_config.py @@ -14,7 +14,8 @@ class AppConfig(BaseAppConfig): Allthough, if you wish to use signals, place handlers to the `signals/handlers.py`: your code be automatically imported and used. """ + def ready(self) -> None: """Import a module with handlers if it exists to avoid boilerplate code.""" with contextlib.suppress(ModuleNotFoundError): - importlib.import_module('.signals.handlers', self.module.__name__) # type: ignore + importlib.import_module(".signals.handlers", self.module.__name__) # type: ignore diff --git a/{{cookiecutter.project_slug}}/src/app/conf/api.py b/{{cookiecutter.project_slug}}/src/app/conf/api.py index c56dc4de..94d115c6 100644 --- a/{{cookiecutter.project_slug}}/src/app/conf/api.py +++ b/{{cookiecutter.project_slug}}/src/app/conf/api.py @@ -3,53 +3,49 @@ # Django REST Framework # https://www.django-rest-framework.org/api-guide/settings/ -DISABLE_THROTTLING = env('DISABLE_THROTTLING', cast=bool, default=False) -MAX_PAGE_SIZE = env('MAX_PAGE_SIZE', cast=int, default=1000) +DISABLE_THROTTLING = env("DISABLE_THROTTLING", cast=bool, default=False) +MAX_PAGE_SIZE = env("MAX_PAGE_SIZE", cast=int, default=1000) REST_FRAMEWORK = { - 'DEFAULT_FILTER_BACKENDS': ( - 'django_filters.rest_framework.DjangoFilterBackend', - ), - 'DEFAULT_PERMISSION_CLASSES': ( - 'rest_framework.permissions.IsAuthenticatedOrReadOnly', - ), - 'DEFAULT_AUTHENTICATION_CLASSES': [ - 'rest_framework.authentication.TokenAuthentication', - 'rest_framework_jwt.authentication.JSONWebTokenAuthentication', + "DEFAULT_FILTER_BACKENDS": ("django_filters.rest_framework.DjangoFilterBackend",), + "DEFAULT_PERMISSION_CLASSES": ("rest_framework.permissions.IsAuthenticatedOrReadOnly",), + "DEFAULT_AUTHENTICATION_CLASSES": [ + "rest_framework.authentication.TokenAuthentication", + "rest_framework_jwt.authentication.JSONWebTokenAuthentication", ], - 'DEFAULT_RENDERER_CLASSES': [ - 'app.api.renderers.AppJSONRenderer', + "DEFAULT_RENDERER_CLASSES": [ + "app.api.renderers.AppJSONRenderer", ], - 'DEFAULT_PARSER_CLASSES': [ - 'djangorestframework_camel_case.parser.CamelCaseJSONParser', - 'djangorestframework_camel_case.parser.CamelCaseMultiPartParser', - 'djangorestframework_camel_case.parser.CamelCaseFormParser', + "DEFAULT_PARSER_CLASSES": [ + "djangorestframework_camel_case.parser.CamelCaseJSONParser", + "djangorestframework_camel_case.parser.CamelCaseMultiPartParser", + "djangorestframework_camel_case.parser.CamelCaseFormParser", ], - 'DEFAULT_VERSIONING_CLASS': 'rest_framework.versioning.NamespaceVersioning', - 'DEFAULT_PAGINATION_CLASS': 'app.api.pagination.AppPagination', - 'PAGE_SIZE': env('PAGE_SIZE', cast=int, default=20), - 'DEFAULT_THROTTLE_RATES': { - 'anon-auth': '10/min', + "DEFAULT_VERSIONING_CLASS": "rest_framework.versioning.NamespaceVersioning", + "DEFAULT_PAGINATION_CLASS": "app.api.pagination.AppPagination", + "PAGE_SIZE": env("PAGE_SIZE", cast=int, default=20), + "DEFAULT_THROTTLE_RATES": { + "anon-auth": "10/min", }, - 'DEFAULT_SCHEMA_CLASS': 'drf_spectacular.openapi.AutoSchema', + "DEFAULT_SCHEMA_CLASS": "drf_spectacular.openapi.AutoSchema", } # Adding session auth and browsable API at the developer machine -if env('DEBUG', cast=bool, default=False): - REST_FRAMEWORK['DEFAULT_AUTHENTICATION_CLASSES'].append('rest_framework.authentication.SessionAuthentication') - REST_FRAMEWORK['DEFAULT_RENDERER_CLASSES'].append('djangorestframework_camel_case.render.CamelCaseBrowsableAPIRenderer') +if env("DEBUG", cast=bool, default=False): + REST_FRAMEWORK["DEFAULT_AUTHENTICATION_CLASSES"].append("rest_framework.authentication.SessionAuthentication") + REST_FRAMEWORK["DEFAULT_RENDERER_CLASSES"].append("djangorestframework_camel_case.render.CamelCaseBrowsableAPIRenderer") # Set up drf_spectacular, https://drf-spectacular.readthedocs.io/en/latest/settings.html SPECTACULAR_SETTINGS = { - 'TITLE': 'Our fancy API', - 'DESCRIPTION': 'So great, needs no docs', - 'SWAGGER_UI_DIST': 'SIDECAR', - 'SWAGGER_UI_FAVICON_HREF': 'SIDECAR', - 'REDOC_DIST': 'SIDECAR', - 'CAMELIZE_NAMES': True, - 'POSTPROCESSING_HOOKS': [ - 'drf_spectacular.hooks.postprocess_schema_enums', - 'drf_spectacular.contrib.djangorestframework_camel_case.camelize_serializer_fields', + "TITLE": "Our fancy API", + "DESCRIPTION": "So great, needs no docs", + "SWAGGER_UI_DIST": "SIDECAR", + "SWAGGER_UI_FAVICON_HREF": "SIDECAR", + "REDOC_DIST": "SIDECAR", + "CAMELIZE_NAMES": True, + "POSTPROCESSING_HOOKS": [ + "drf_spectacular.hooks.postprocess_schema_enums", + "drf_spectacular.contrib.djangorestframework_camel_case.camelize_serializer_fields", ], } diff --git a/{{cookiecutter.project_slug}}/src/app/conf/auth.py b/{{cookiecutter.project_slug}}/src/app/conf/auth.py index d3ddf99e..b69e08d4 100644 --- a/{{cookiecutter.project_slug}}/src/app/conf/auth.py +++ b/{{cookiecutter.project_slug}}/src/app/conf/auth.py @@ -2,18 +2,18 @@ from app.conf.environ import env -AUTH_USER_MODEL = 'users.User' -AXES_ENABLED = env('AXES_ENABLED', cast=bool, default=True) +AUTH_USER_MODEL = "users.User" +AXES_ENABLED = env("AXES_ENABLED", cast=bool, default=True) AUTHENTICATION_BACKENDS = [ - 'axes.backends.AxesBackend', - 'django.contrib.auth.backends.ModelBackend', + "axes.backends.AxesBackend", + "django.contrib.auth.backends.ModelBackend", ] JWT_AUTH = { - 'JWT_EXPIRATION_DELTA': timedelta(days=14), - 'JWT_REFRESH_EXPIRATION_DELTA': timedelta(days=21), - 'JWT_ALLOW_REFRESH': True, + "JWT_EXPIRATION_DELTA": timedelta(days=14), + "JWT_REFRESH_EXPIRATION_DELTA": timedelta(days=21), + "JWT_ALLOW_REFRESH": True, } @@ -30,5 +30,5 @@ # PASSWORD_HASHERS = [ - 'django.contrib.auth.hashers.BCryptPasswordHasher', + "django.contrib.auth.hashers.BCryptPasswordHasher", ] diff --git a/{{cookiecutter.project_slug}}/src/app/conf/boilerplate.py b/{{cookiecutter.project_slug}}/src/app/conf/boilerplate.py index acd708f8..7e88e28c 100644 --- a/{{cookiecutter.project_slug}}/src/app/conf/boilerplate.py +++ b/{{cookiecutter.project_slug}}/src/app/conf/boilerplate.py @@ -2,9 +2,9 @@ BASE_DIR = os.path.dirname(os.path.dirname(os.path.abspath(__file__))) -ROOT_URLCONF = 'app.urls' +ROOT_URLCONF = "app.urls" # Disable built-in ./manage.py test command in favor of pytest -TEST_RUNNER = 'app.test.disable_test_command_runner.DisableTestCommandRunner' +TEST_RUNNER = "app.test.disable_test_command_runner.DisableTestCommandRunner" -WSGI_APPLICATION = 'app.wsgi.application' +WSGI_APPLICATION = "app.wsgi.application" diff --git a/{{cookiecutter.project_slug}}/src/app/conf/db.py b/{{cookiecutter.project_slug}}/src/app/conf/db.py index 3de978b4..935f531e 100644 --- a/{{cookiecutter.project_slug}}/src/app/conf/db.py +++ b/{{cookiecutter.project_slug}}/src/app/conf/db.py @@ -4,9 +4,9 @@ from app.conf.environ import env DATABASES = { - # read os.environ['DATABASE_URL'] and raises ImproperlyConfigured exception if not found - 'default': env.db(), + # read os.environ["DATABASE_URL"] and raises ImproperlyConfigured exception if not found + "default": env.db(), } # https://docs.djangoproject.com/en/3.2/releases/3.2/#customizing-type-of-auto-created-primary-keys -DEFAULT_AUTO_FIELD = 'django.db.models.AutoField' +DEFAULT_AUTO_FIELD = "django.db.models.AutoField" diff --git a/{{cookiecutter.project_slug}}/src/app/conf/environ.py b/{{cookiecutter.project_slug}}/src/app/conf/environ.py index e28bdf5b..d706412c 100644 --- a/{{cookiecutter.project_slug}}/src/app/conf/environ.py +++ b/{{cookiecutter.project_slug}}/src/app/conf/environ.py @@ -6,7 +6,7 @@ CI=(bool, False), ) -environ.Env.read_env('app/.env') # reading .env file +environ.Env.read_env("app/.env") # reading .env file __all__ = [ env, diff --git a/{{cookiecutter.project_slug}}/src/app/conf/healthchecks.py b/{{cookiecutter.project_slug}}/src/app/conf/healthchecks.py index 8adece5d..5a555ef4 100644 --- a/{{cookiecutter.project_slug}}/src/app/conf/healthchecks.py +++ b/{{cookiecutter.project_slug}}/src/app/conf/healthchecks.py @@ -3,5 +3,5 @@ HEALTH_CHECKS_ERROR_CODE = 503 HEALTH_CHECKS = { - 'db': 'django_healthchecks.contrib.check_database', + "db": "django_healthchecks.contrib.check_database", } diff --git a/{{cookiecutter.project_slug}}/src/app/conf/http.py b/{{cookiecutter.project_slug}}/src/app/conf/http.py index 6c1f9fd3..cfa86cf0 100644 --- a/{{cookiecutter.project_slug}}/src/app/conf/http.py +++ b/{{cookiecutter.project_slug}}/src/app/conf/http.py @@ -1,11 +1,11 @@ from app.conf.environ import env -ALLOWED_HOSTS = ['*'] # host validation is not necessary in 2020 +ALLOWED_HOSTS = ["*"] # host validation is not necessary in 2020 CSRF_TRUSTED_ORIGINS = [ - 'your.app.origin', + "your.app.origin", ] -if env('DEBUG'): - ABSOLUTE_HOST = 'http://localhost:3000' +if env("DEBUG"): + ABSOLUTE_HOST = "http://localhost:3000" else: - ABSOLUTE_HOST = 'https://your.app.com' + ABSOLUTE_HOST = "https://your.app.com" diff --git a/{{cookiecutter.project_slug}}/src/app/conf/i18n.py b/{{cookiecutter.project_slug}}/src/app/conf/i18n.py index c4a9f8dd..9565a6b8 100644 --- a/{{cookiecutter.project_slug}}/src/app/conf/i18n.py +++ b/{{cookiecutter.project_slug}}/src/app/conf/i18n.py @@ -1,7 +1,7 @@ # Internationalization # https://docs.djangoproject.com/en/3.0/topics/i18n/ -LANGUAGE_CODE = 'en-us' -LOCALE_PATHS = ['locale'] +LANGUAGE_CODE = "en-us" +LOCALE_PATHS = ["locale"] USE_L10N = True USE_i18N = True diff --git a/{{cookiecutter.project_slug}}/src/app/conf/installed_apps.py b/{{cookiecutter.project_slug}}/src/app/conf/installed_apps.py index ff5b2ad8..efab9b26 100644 --- a/{{cookiecutter.project_slug}}/src/app/conf/installed_apps.py +++ b/{{cookiecutter.project_slug}}/src/app/conf/installed_apps.py @@ -1,26 +1,25 @@ # Application definition APPS = [ - 'app', - 'a12n', - 'users', + "app", + "a12n", + "users", ] THIRD_PARTY_APPS = [ - 'drf_spectacular', - 'drf_spectacular_sidecar', - 'rest_framework', - 'rest_framework.authtoken', - 'rest_framework_jwt.blacklist', - 'django_filters', - 'axes', - - 'django.contrib.admin', - 'django.contrib.auth', - 'django.contrib.contenttypes', - 'django.contrib.sessions', - 'django.contrib.messages', - 'django.contrib.staticfiles', + "drf_spectacular", + "drf_spectacular_sidecar", + "rest_framework", + "rest_framework.authtoken", + "rest_framework_jwt.blacklist", + "django_filters", + "axes", + "django.contrib.admin", + "django.contrib.auth", + "django.contrib.contenttypes", + "django.contrib.sessions", + "django.contrib.messages", + "django.contrib.staticfiles", ] INSTALLED_APPS = APPS + THIRD_PARTY_APPS diff --git a/{{cookiecutter.project_slug}}/src/app/conf/media.py b/{{cookiecutter.project_slug}}/src/app/conf/media.py index 4c490476..7c655753 100644 --- a/{{cookiecutter.project_slug}}/src/app/conf/media.py +++ b/{{cookiecutter.project_slug}}/src/app/conf/media.py @@ -1,4 +1,4 @@ from app.conf.environ import env -MEDIA_URL = '/media/' -MEDIA_ROOT = env('MEDIA_ROOT', cast=str, default='media') +MEDIA_URL = "/media/" +MEDIA_ROOT = env("MEDIA_ROOT", cast=str, default="media") diff --git a/{{cookiecutter.project_slug}}/src/app/conf/middleware.py b/{{cookiecutter.project_slug}}/src/app/conf/middleware.py index a26ab579..a59a180e 100644 --- a/{{cookiecutter.project_slug}}/src/app/conf/middleware.py +++ b/{{cookiecutter.project_slug}}/src/app/conf/middleware.py @@ -1,12 +1,12 @@ MIDDLEWARE = [ - 'django.middleware.security.SecurityMiddleware', - 'whitenoise.middleware.WhiteNoiseMiddleware', - 'django.contrib.sessions.middleware.SessionMiddleware', - 'django.middleware.common.CommonMiddleware', - 'django.middleware.csrf.CsrfViewMiddleware', - 'django.contrib.auth.middleware.AuthenticationMiddleware', - 'django.contrib.messages.middleware.MessageMiddleware', - 'django.middleware.clickjacking.XFrameOptionsMiddleware', - 'app.middleware.real_ip.real_ip_middleware', - 'axes.middleware.AxesMiddleware', + "django.middleware.security.SecurityMiddleware", + "whitenoise.middleware.WhiteNoiseMiddleware", + "django.contrib.sessions.middleware.SessionMiddleware", + "django.middleware.common.CommonMiddleware", + "django.middleware.csrf.CsrfViewMiddleware", + "django.contrib.auth.middleware.AuthenticationMiddleware", + "django.contrib.messages.middleware.MessageMiddleware", + "django.middleware.clickjacking.XFrameOptionsMiddleware", + "app.middleware.real_ip.real_ip_middleware", + "axes.middleware.AxesMiddleware", ] diff --git a/{{cookiecutter.project_slug}}/src/app/conf/sentry.py b/{{cookiecutter.project_slug}}/src/app/conf/sentry.py index 54288170..3050ea95 100644 --- a/{{cookiecutter.project_slug}}/src/app/conf/sentry.py +++ b/{{cookiecutter.project_slug}}/src/app/conf/sentry.py @@ -3,9 +3,9 @@ # Sentry # https://sentry.io/for/django/ -SENTRY_DSN = env('SENTRY_DSN', cast=str, default='') +SENTRY_DSN = env("SENTRY_DSN", cast=str, default="") -if not env('DEBUG') and len(SENTRY_DSN): +if not env("DEBUG") and len(SENTRY_DSN): import sentry_sdk from sentry_sdk.integrations.django import DjangoIntegration diff --git a/{{cookiecutter.project_slug}}/src/app/conf/static.py b/{{cookiecutter.project_slug}}/src/app/conf/static.py index 9e676452..33530c17 100644 --- a/{{cookiecutter.project_slug}}/src/app/conf/static.py +++ b/{{cookiecutter.project_slug}}/src/app/conf/static.py @@ -3,5 +3,5 @@ # Static files (CSS, JavaScript, Images) # https://docs.djangoproject.com/en/3.0/howto/static-files/ -STATIC_URL = '/static/' -STATIC_ROOT = env('STATIC_ROOT', cast=str, default='static') +STATIC_URL = "/static/" +STATIC_ROOT = env("STATIC_ROOT", cast=str, default="static") diff --git a/{{cookiecutter.project_slug}}/src/app/conf/storage.py b/{{cookiecutter.project_slug}}/src/app/conf/storage.py index 66eceab8..761bdf2b 100644 --- a/{{cookiecutter.project_slug}}/src/app/conf/storage.py +++ b/{{cookiecutter.project_slug}}/src/app/conf/storage.py @@ -1,10 +1,10 @@ from app.conf.environ import env -DEFAULT_FILE_STORAGE = env('DEFAULT_FILE_STORAGE', cast=str, default='django.core.files.storage.FileSystemStorage') +DEFAULT_FILE_STORAGE = env("DEFAULT_FILE_STORAGE", cast=str, default="django.core.files.storage.FileSystemStorage") -AWS_ACCESS_KEY_ID = env('AWS_ACCESS_KEY_ID', default='') -AWS_SECRET_ACCESS_KEY = env('AWS_SECRET_ACCESS_KEY', default='') -AWS_STORAGE_BUCKET_NAME = env('AWS_STORAGE_BUCKET_NAME', default='') -AWS_S3_REGION_NAME = env('AWS_S3_REGION_NAME', default='') -AWS_S3_ENDPOINT_URL = env('AWS_S3_ENDPOINT_URL', default='') -AWS_DEFAULT_ACL = env('AWS_DEFAULT_ACL', default='') +AWS_ACCESS_KEY_ID = env("AWS_ACCESS_KEY_ID", default="") +AWS_SECRET_ACCESS_KEY = env("AWS_SECRET_ACCESS_KEY", default="") +AWS_STORAGE_BUCKET_NAME = env("AWS_STORAGE_BUCKET_NAME", default="") +AWS_S3_REGION_NAME = env("AWS_S3_REGION_NAME", default="") +AWS_S3_ENDPOINT_URL = env("AWS_S3_ENDPOINT_URL", default="") +AWS_DEFAULT_ACL = env("AWS_DEFAULT_ACL", default="") diff --git a/{{cookiecutter.project_slug}}/src/app/conf/templates.py b/{{cookiecutter.project_slug}}/src/app/conf/templates.py index 60abdcb9..258c316d 100644 --- a/{{cookiecutter.project_slug}}/src/app/conf/templates.py +++ b/{{cookiecutter.project_slug}}/src/app/conf/templates.py @@ -1,14 +1,14 @@ TEMPLATES = [ { - 'BACKEND': 'django.template.backends.django.DjangoTemplates', - 'DIRS': [], - 'APP_DIRS': True, - 'OPTIONS': { - 'context_processors': [ - 'django.template.context_processors.debug', - 'django.template.context_processors.request', - 'django.contrib.auth.context_processors.auth', - 'django.contrib.messages.context_processors.messages', + "BACKEND": "django.template.backends.django.DjangoTemplates", + "DIRS": [], + "APP_DIRS": True, + "OPTIONS": { + "context_processors": [ + "django.template.context_processors.debug", + "django.template.context_processors.request", + "django.contrib.auth.context_processors.auth", + "django.contrib.messages.context_processors.messages", ], }, }, diff --git a/{{cookiecutter.project_slug}}/src/app/conf/timezone.py b/{{cookiecutter.project_slug}}/src/app/conf/timezone.py index d47c4af9..bed6c7ab 100644 --- a/{{cookiecutter.project_slug}}/src/app/conf/timezone.py +++ b/{{cookiecutter.project_slug}}/src/app/conf/timezone.py @@ -1,2 +1,2 @@ USE_TZ = True -TIME_ZONE = 'UTC' +TIME_ZONE = "UTC" diff --git a/{{cookiecutter.project_slug}}/src/app/factory.py b/{{cookiecutter.project_slug}}/src/app/factory.py index 32e02882..fee88abc 100644 --- a/{{cookiecutter.project_slug}}/src/app/factory.py +++ b/{{cookiecutter.project_slug}}/src/app/factory.py @@ -15,7 +15,7 @@ def uploaded_image(self: FactoryProtocol) -> SimpleUploadedFile: DRF won't let you use invalid image, so the content is a real image. """ bytes_io = BytesIO() - Image.new('RGB', size=(10, 10), color=(0, 255, 255)).save(bytes_io, 'GIF') + Image.new("RGB", size=(10, 10), color=(0, 255, 255)).save(bytes_io, "GIF") bytes_io.seek(0) image_content = bytes_io.read() - return SimpleUploadedFile('image.gif', image_content, 'image/gif') + return SimpleUploadedFile("image.gif", image_content, "image/gif") diff --git a/{{cookiecutter.project_slug}}/src/app/fixtures/__init__.py b/{{cookiecutter.project_slug}}/src/app/fixtures/__init__.py index e6202ad0..8e7e465e 100644 --- a/{{cookiecutter.project_slug}}/src/app/fixtures/__init__.py +++ b/{{cookiecutter.project_slug}}/src/app/fixtures/__init__.py @@ -3,7 +3,7 @@ from app.fixtures.factory import factory __all__ = [ - 'as_anon', - 'as_user', - 'factory', + "as_anon", + "as_user", + "factory", ] diff --git a/{{cookiecutter.project_slug}}/src/app/management/commands/makemigrations.py b/{{cookiecutter.project_slug}}/src/app/management/commands/makemigrations.py index e0549429..557b5176 100644 --- a/{{cookiecutter.project_slug}}/src/app/management/commands/makemigrations.py +++ b/{{cookiecutter.project_slug}}/src/app/management/commands/makemigrations.py @@ -7,10 +7,10 @@ class MakemigrationsError(CommandError): class Command(BaseCommand): - """Disable automatic names for django migrations, thanks https://adamj.eu/tech/2020/02/24/how-to-disallow-auto-named-django-migrations/ - """ + """Disable automatic names for django migrations, thanks https://adamj.eu/tech/2020/02/24/how-to-disallow-auto-named-django-migrations/""" + def handle(self, *app_labels, **options): - if options['name'] is None and not any([options['dry_run'], options['check_changes']]): - raise MakemigrationsError('Migration name is required. Run again with `-n/--name` argument and specify name explicitly.') + if options["name"] is None and not any([options["dry_run"], options["check_changes"]]): + raise MakemigrationsError("Migration name is required. Run again with `-n/--name` argument and specify name explicitly.") super().handle(*app_labels, **options) diff --git a/{{cookiecutter.project_slug}}/src/app/management/commands/startapp.py b/{{cookiecutter.project_slug}}/src/app/management/commands/startapp.py index 2907cb31..4f41f201 100644 --- a/{{cookiecutter.project_slug}}/src/app/management/commands/startapp.py +++ b/{{cookiecutter.project_slug}}/src/app/management/commands/startapp.py @@ -6,8 +6,9 @@ class Command(BaseCommand): """Set custom template for all newly generated apps""" + def handle(self, **options): - if 'template' not in options or options['template'] is None: - options['template'] = path.join(settings.BASE_DIR, '.django-app-template') + if "template" not in options or options["template"] is None: + options["template"] = path.join(settings.BASE_DIR, ".django-app-template") super().handle(**options) diff --git a/{{cookiecutter.project_slug}}/src/app/middleware/real_ip.py b/{{cookiecutter.project_slug}}/src/app/middleware/real_ip.py index 92f2a4f7..a8b536a9 100644 --- a/{{cookiecutter.project_slug}}/src/app/middleware/real_ip.py +++ b/{{cookiecutter.project_slug}}/src/app/middleware/real_ip.py @@ -7,15 +7,16 @@ def real_ip_middleware(get_response: Callable) -> Callable: - """Set request.META['REMOTE_ADDR'] to ip guessed by django-ipware. + """Set request.META["REMOTE_ADDR"] to ip guessed by django-ipware. We need this to make sure all apps using ip detection in django way stay usable behind any kind of reverse proxy. For custom proxy configuration check out django-ipware docs at https://github.com/un33k/django-ipware """ + def middleware(request: HttpRequest) -> HttpResponse: - request.META['REMOTE_ADDR'] = get_client_ip(request)[0] + request.META["REMOTE_ADDR"] = get_client_ip(request)[0] return get_response(request) diff --git a/{{cookiecutter.project_slug}}/src/app/models.py b/{{cookiecutter.project_slug}}/src/app/models.py index 50234308..cec342ce 100644 --- a/{{cookiecutter.project_slug}}/src/app/models.py +++ b/{{cookiecutter.project_slug}}/src/app/models.py @@ -6,9 +6,9 @@ from django.db import models __all__ = [ - 'models', - 'DefaultModel', - 'TimestampedModel', + "models", + "DefaultModel", + "TimestampedModel", ] @@ -18,7 +18,7 @@ class Meta: def __str__(self) -> str: """Default name for all models""" - name = getattr(self, 'name', None) + name = getattr(self, "name", None) if name is not None: return str(name) @@ -29,8 +29,7 @@ def get_contenttype(cls) -> ContentType: return ContentType.objects.get_for_model(cls) def update_from_kwargs(self, **kwargs: dict[str, Any]) -> None: - """A shortcut method to update model instance from the kwargs. - """ + """A shortcut method to update model instance from the kwargs.""" for (key, value) in kwargs.items(): setattr(self, key, value) @@ -41,9 +40,8 @@ def setattr_and_save(self, key: str, value: Any) -> None: @classmethod def get_label(cls) -> str: - """Get a unique within the app model label - """ - return cls._meta.label_lower.split('.')[-1] + """Get a unique within the app model label""" + return cls._meta.label_lower.split(".")[-1] class TimestampedModel(DefaultModel, Timestamped): @@ -51,5 +49,6 @@ class TimestampedModel(DefaultModel, Timestamped): Default app model that has `created` and `updated` attributes. Currently based on https://github.com/audiolion/django-behaviors """ + class Meta: abstract = True diff --git a/{{cookiecutter.project_slug}}/src/app/services.py b/{{cookiecutter.project_slug}}/src/app/services.py index 9a7b68a6..fb5a9c41 100644 --- a/{{cookiecutter.project_slug}}/src/app/services.py +++ b/{{cookiecutter.project_slug}}/src/app/services.py @@ -18,7 +18,7 @@ class UserCreator(BaseService): def act(self) -> User: return User.objects.create(first_name=self.first_name, last_name=self.last_name) - # user = UserCreator(first_name='Ivan', last_name='Petrov')() + # user = UserCreator(first_name="Ivan", last_name="Petrov")() This is not ok: class UserCreator: @@ -27,6 +27,7 @@ def __call__(self, first_name: str, last_name: Optional[str]) -> User: For more implementation examples, check out https://github.com/tough-dev-school/education-backend/tree/master/src/orders/services """ + def __call__(self) -> None: self.validate() return self.act() @@ -41,4 +42,4 @@ def validate(self) -> None: @abstractmethod def act(self) -> None: - raise NotImplementedError('Please implement in the service class') + raise NotImplementedError("Please implement in the service class") diff --git a/{{cookiecutter.project_slug}}/src/app/settings.py b/{{cookiecutter.project_slug}}/src/app/settings.py index cea40c6e..f235565d 100644 --- a/{{cookiecutter.project_slug}}/src/app/settings.py +++ b/{{cookiecutter.project_slug}}/src/app/settings.py @@ -7,26 +7,26 @@ from app.conf.environ import env # SECURITY WARNING: keep the secret key used in production secret! -SECRET_KEY = env('SECRET_KEY') +SECRET_KEY = env("SECRET_KEY") # SECURITY WARNING: don't run with debug turned on in production! -DEBUG = env('DEBUG', cast=bool, default=False) -CI = env('CI', cast=bool, default=False) +DEBUG = env("DEBUG", cast=bool, default=False) +CI = env("CI", cast=bool, default=False) include( - 'conf/api.py', - 'conf/auth.py', - 'conf/boilerplate.py', - 'conf/db.py', - 'conf/healthchecks.py', - 'conf/http.py', - 'conf/i18n.py', - 'conf/installed_apps.py', - 'conf/media.py', - 'conf/middleware.py', - 'conf/storage.py', - 'conf/sentry.py', - 'conf/static.py', - 'conf/templates.py', - 'conf/timezone.py', + "conf/api.py", + "conf/auth.py", + "conf/boilerplate.py", + "conf/db.py", + "conf/healthchecks.py", + "conf/http.py", + "conf/i18n.py", + "conf/installed_apps.py", + "conf/media.py", + "conf/middleware.py", + "conf/storage.py", + "conf/sentry.py", + "conf/static.py", + "conf/templates.py", + "conf/timezone.py", ) diff --git a/{{cookiecutter.project_slug}}/src/app/testing/__init__.py b/{{cookiecutter.project_slug}}/src/app/testing/__init__.py index a0bd337f..89377995 100644 --- a/{{cookiecutter.project_slug}}/src/app/testing/__init__.py +++ b/{{cookiecutter.project_slug}}/src/app/testing/__init__.py @@ -3,7 +3,7 @@ from app.testing.factory import register __all__ = [ - 'ApiClient', - 'FixtureFactory', - 'register', + "ApiClient", + "FixtureFactory", + "register", ] diff --git a/{{cookiecutter.project_slug}}/src/app/testing/api.py b/{{cookiecutter.project_slug}}/src/app/testing/api.py index 885ea94f..f9d66ae2 100644 --- a/{{cookiecutter.project_slug}}/src/app/testing/api.py +++ b/{{cookiecutter.project_slug}}/src/app/testing/api.py @@ -15,39 +15,39 @@ def __init__(self, user: Optional[User] = None, *args, **kwargs) -> None: if user: self.user = user - self.password = ''.join([random.choice(string.hexdigits) for _ in range(6)]) + self.password = "".join([random.choice(string.hexdigits) for _ in range(6)]) self.user.set_password(self.password) self.user.save() token = Token.objects.create(user=self.user) self.credentials( - HTTP_AUTHORIZATION=f'Token {token}', - HTTP_X_CLIENT='testing', + HTTP_AUTHORIZATION=f"Token {token}", + HTTP_X_CLIENT="testing", ) def get(self, *args, **kwargs): - expected_status = kwargs.get('expected_status', 200) - return self._request('get', expected_status, *args, **kwargs) + expected_status = kwargs.get("expected_status", 200) + return self._request("get", expected_status, *args, **kwargs) def patch(self, *args, **kwargs): - expected_status = kwargs.get('expected_status', 200) - return self._request('patch', expected_status, *args, **kwargs) + expected_status = kwargs.get("expected_status", 200) + return self._request("patch", expected_status, *args, **kwargs) def post(self, *args, **kwargs): - expected_status = kwargs.get('expected_status', 201) - return self._request('post', expected_status, *args, **kwargs) + expected_status = kwargs.get("expected_status", 201) + return self._request("post", expected_status, *args, **kwargs) def put(self, *args, **kwargs): - expected_status = kwargs.get('expected_status', 200) - return self._request('put', expected_status, *args, **kwargs) + expected_status = kwargs.get("expected_status", 200) + return self._request("put", expected_status, *args, **kwargs) def delete(self, *args, **kwargs): - expected_status = kwargs.get('expected_status', 204) - return self._request('delete', expected_status, *args, **kwargs) + expected_status = kwargs.get("expected_status", 204) + return self._request("delete", expected_status, *args, **kwargs) def _request(self, method, expected, *args, **kwargs): - kwargs['format'] = kwargs.get('format', 'json') - as_response = kwargs.pop('as_response', False) + kwargs["format"] = kwargs.get("format", "json") + as_response = kwargs.pop("as_response", False) method = getattr(super(), method) response = method(*args, **kwargs) @@ -59,7 +59,7 @@ def _request(self, method, expected, *args, **kwargs): return content def _decode(self, response): - content = response.content.decode('utf-8', errors='ignore') + content = response.content.decode("utf-8", errors="ignore") if self.is_json(response): return json.loads(content) @@ -68,12 +68,12 @@ def _decode(self, response): @staticmethod def is_json(response) -> bool: - if response.has_header('content-type'): - return 'json' in response.get('content-type') + if response.has_header("content-type"): + return "json" in response.get("content-type") return False __all__ = [ - 'ApiClient', + "ApiClient", ] diff --git a/{{cookiecutter.project_slug}}/src/app/testing/factory.py b/{{cookiecutter.project_slug}}/src/app/testing/factory.py index b0cf4a87..bbe5ae26 100644 --- a/{{cookiecutter.project_slug}}/src/app/testing/factory.py +++ b/{{cookiecutter.project_slug}}/src/app/testing/factory.py @@ -16,12 +16,12 @@ class FixtureRegistry: def get(self, name: str) -> Callable: method = self.METHODS.get(name) if not method: - raise AttributeError(f'Factory method “{name}” not found.') + raise AttributeError(f"Factory method “{name}” not found.") return method class CycleFixtureFactory: - def __init__(self, factory: 'FixtureFactory', count: int) -> None: + def __init__(self, factory: "FixtureFactory", count: int) -> None: self.factory = factory self.count = count diff --git a/{{cookiecutter.project_slug}}/src/app/testing/mixer.py b/{{cookiecutter.project_slug}}/src/app/testing/mixer.py index ebee3c34..49e96dc2 100644 --- a/{{cookiecutter.project_slug}}/src/app/testing/mixer.py +++ b/{{cookiecutter.project_slug}}/src/app/testing/mixer.py @@ -3,7 +3,7 @@ from mixer.backend.django import mixer __all__ = [ - 'mixer', + "mixer", ] @@ -12,8 +12,8 @@ def _random_user_name() -> str: def _random_email() -> str: - uuid_as_str = str(uuid.uuid4()).replace('-', '_') - return f'{uuid_as_str}@mail.com' + uuid_as_str = str(uuid.uuid4()).replace("-", "_") + return f"{uuid_as_str}@mail.com" -mixer.register('users.User', username=_random_user_name, email=_random_email) +mixer.register("users.User", username=_random_user_name, email=_random_email) diff --git a/{{cookiecutter.project_slug}}/src/app/testing/runner.py b/{{cookiecutter.project_slug}}/src/app/testing/runner.py index 79c65b44..1de73672 100644 --- a/{{cookiecutter.project_slug}}/src/app/testing/runner.py +++ b/{{cookiecutter.project_slug}}/src/app/testing/runner.py @@ -7,4 +7,4 @@ def __init__(self, *args, **kwargs): pass def run_tests(self, *args): - raise CommandError('Please use command `pytest`.') + raise CommandError("Pytest here. Run it with `make test`") diff --git a/{{cookiecutter.project_slug}}/src/app/testing/types.py b/{{cookiecutter.project_slug}}/src/app/testing/types.py index be0152a8..ca949a84 100644 --- a/{{cookiecutter.project_slug}}/src/app/testing/types.py +++ b/{{cookiecutter.project_slug}}/src/app/testing/types.py @@ -8,5 +8,5 @@ class FactoryProtocol(Protocol): __all__ = [ - 'FactoryProtocol', + "FactoryProtocol", ] diff --git a/{{cookiecutter.project_slug}}/src/app/tests/test_health.py b/{{cookiecutter.project_slug}}/src/app/tests/test_health.py index 4f224088..57bf52f6 100644 --- a/{{cookiecutter.project_slug}}/src/app/tests/test_health.py +++ b/{{cookiecutter.project_slug}}/src/app/tests/test_health.py @@ -2,11 +2,11 @@ pytestmark = [ pytest.mark.django_db, - pytest.mark.filterwarnings('ignore:.*inspect.getargspec().*:DeprecationWarning'), + pytest.mark.filterwarnings("ignore:.*inspect.getargspec().*:DeprecationWarning"), ] def test(as_anon): - result = as_anon.get('/api/v1/healthchecks/db/') + result = as_anon.get("/api/v1/healthchecks/db/") - assert result == 'true' + assert result == "true" diff --git a/{{cookiecutter.project_slug}}/src/app/tests/test_remote_addr_midlleware.py b/{{cookiecutter.project_slug}}/src/app/tests/test_remote_addr_midlleware.py index 992f876c..44952417 100644 --- a/{{cookiecutter.project_slug}}/src/app/tests/test_remote_addr_midlleware.py +++ b/{{cookiecutter.project_slug}}/src/app/tests/test_remote_addr_midlleware.py @@ -9,7 +9,9 @@ @pytest.fixture(autouse=True) def _require_users_app_installed(settings): - assert apps.is_installed('users'), """ + assert apps.is_installed( + "users" + ), """ Stock f213/django users app should be installed to run this test. Make sure to test app.middleware.real_ip.real_ip_middleware on your own, if you drop @@ -19,10 +21,10 @@ def _require_users_app_installed(settings): @pytest.fixture def api(user): - return ApiClient(user=user, HTTP_X_FORWARDED_FOR='100.200.250.150, 10.0.0.1') + return ApiClient(user=user, HTTP_X_FORWARDED_FOR="100.200.250.150, 10.0.0.1") def test_remote_addr(api): - result = api.get('/api/v1/users/me/') + result = api.get("/api/v1/users/me/") - assert result['remoteAddr'] == '100.200.250.150' + assert result["remoteAddr"] == "100.200.250.150" diff --git a/{{cookiecutter.project_slug}}/src/app/tests/testing/factory/test_factory.py b/{{cookiecutter.project_slug}}/src/app/tests/testing/factory/test_factory.py index 1387f208..858df883 100644 --- a/{{cookiecutter.project_slug}}/src/app/tests/testing/factory/test_factory.py +++ b/{{cookiecutter.project_slug}}/src/app/tests/testing/factory/test_factory.py @@ -11,9 +11,8 @@ def fixture_factory() -> FixtureFactory: @pytest.fixture def registered_method(mocker): - mock = mocker.Mock(name='registered_method', - return_value='i should be returned after gettatr') - mock.__name__ = 'registered_method' + mock = mocker.Mock(name="registered_method", return_value="i should be returned after gettatr") + mock.__name__ = "registered_method" register(mock) return mock @@ -21,7 +20,7 @@ def registered_method(mocker): def test_call_getattr_returns_what_method_returned(fixture_factory: FixtureFactory, registered_method): result = fixture_factory.registered_method() - assert result == 'i should be returned after gettatr' + assert result == "i should be returned after gettatr" def test_registered_method_called_with_factory_instance(fixture_factory: FixtureFactory, registered_method): diff --git a/{{cookiecutter.project_slug}}/src/app/tests/testing/factory/test_registry.py b/{{cookiecutter.project_slug}}/src/app/tests/testing/factory/test_registry.py index 7997c8ff..0dcfc793 100644 --- a/{{cookiecutter.project_slug}}/src/app/tests/testing/factory/test_registry.py +++ b/{{cookiecutter.project_slug}}/src/app/tests/testing/factory/test_registry.py @@ -10,8 +10,8 @@ def fixture_registry() -> FixtureRegistry: def test_registry_raises_exception_if_no_method(fixture_registry: FixtureRegistry): - with pytest.raises(AttributeError, match=r'Factory method \“not_real\” not found\.'): - fixture_registry.get('not_real') + with pytest.raises(AttributeError, match=r"Factory method \“not_real\” not found\."): + fixture_registry.get("not_real") def test_registry_returns_correct_method_after_register_decorator(fixture_registry: FixtureRegistry): @@ -19,6 +19,6 @@ def test_registry_returns_correct_method_after_register_decorator(fixture_regist def some_method_to_add(): pass - method = fixture_registry.get('some_method_to_add') # act + method = fixture_registry.get("some_method_to_add") # act assert some_method_to_add == method diff --git a/{{cookiecutter.project_slug}}/src/app/urls/__init__.py b/{{cookiecutter.project_slug}}/src/app/urls/__init__.py index bc1384a3..319d5e65 100644 --- a/{{cookiecutter.project_slug}}/src/app/urls/__init__.py +++ b/{{cookiecutter.project_slug}}/src/app/urls/__init__.py @@ -3,10 +3,10 @@ from django.urls import path api = [ - path('v1/', include('app.urls.v1', namespace='v1')), + path("v1/", include("app.urls.v1", namespace="v1")), ] urlpatterns = [ - path('admin/', admin.site.urls), - path('api/', include(api)), + path("admin/", admin.site.urls), + path("api/", include(api)), ] diff --git a/{{cookiecutter.project_slug}}/src/app/urls/v1.py b/{{cookiecutter.project_slug}}/src/app/urls/v1.py index 80498d2b..dad4b295 100644 --- a/{{cookiecutter.project_slug}}/src/app/urls/v1.py +++ b/{{cookiecutter.project_slug}}/src/app/urls/v1.py @@ -4,11 +4,11 @@ from django.urls import include from django.urls import path -app_name = 'api_v1' +app_name = "api_v1" urlpatterns = [ - path('auth/', include('a12n.urls')), - path('users/', include('users.urls')), - path('healthchecks/', include('django_healthchecks.urls')), - path('docs/schema/', SpectacularAPIView.as_view(), name='schema'), - path('docs/swagger/', SpectacularSwaggerView.as_view(url_name='schema')), + path("auth/", include("a12n.urls")), + path("users/", include("users.urls")), + path("healthchecks/", include("django_healthchecks.urls")), + path("docs/schema/", SpectacularAPIView.as_view(), name="schema"), + path("docs/swagger/", SpectacularSwaggerView.as_view(url_name="schema")), ] diff --git a/{{cookiecutter.project_slug}}/src/app/wsgi.py b/{{cookiecutter.project_slug}}/src/app/wsgi.py index 863ddf4a..1c508c03 100644 --- a/{{cookiecutter.project_slug}}/src/app/wsgi.py +++ b/{{cookiecutter.project_slug}}/src/app/wsgi.py @@ -11,6 +11,6 @@ from django.core.wsgi import get_wsgi_application -os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'app.settings') +os.environ.setdefault("DJANGO_SETTINGS_MODULE", "app.settings") application = get_wsgi_application() diff --git a/{{cookiecutter.project_slug}}/src/conftest.py b/{{cookiecutter.project_slug}}/src/conftest.py index 6e78881a..542d1812 100644 --- a/{{cookiecutter.project_slug}}/src/conftest.py +++ b/{{cookiecutter.project_slug}}/src/conftest.py @@ -1,6 +1,6 @@ pytest_plugins = [ - 'app.factory', - 'app.fixtures', - 'users.factory', - 'users.fixtures', + "app.factory", + "app.fixtures", + "users.factory", + "users.fixtures", ] diff --git a/{{cookiecutter.project_slug}}/src/manage.py b/{{cookiecutter.project_slug}}/src/manage.py index 55851fea..661e27f5 100755 --- a/{{cookiecutter.project_slug}}/src/manage.py +++ b/{{cookiecutter.project_slug}}/src/manage.py @@ -5,17 +5,17 @@ def main() -> None: - os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'app.settings') + os.environ.setdefault("DJANGO_SETTINGS_MODULE", "app.settings") try: from django.core.management import execute_from_command_line except ImportError as exc: raise ImportError( - 'Could not import Django. Are you sure it is installed and ' - 'available on your PYTHONPATH environment variable? Did you ' - 'forget to activate a virtual environment?', + "Could not import Django. Are you sure it is installed and " + "available on your PYTHONPATH environment variable? Did you " + "forget to activate a virtual environment?", ) from exc execute_from_command_line(sys.argv) -if __name__ == '__main__': +if __name__ == "__main__": main() diff --git a/{{cookiecutter.project_slug}}/src/users/api/serializers.py b/{{cookiecutter.project_slug}}/src/users/api/serializers.py index 7a4279ed..d4786b21 100644 --- a/{{cookiecutter.project_slug}}/src/users/api/serializers.py +++ b/{{cookiecutter.project_slug}}/src/users/api/serializers.py @@ -9,13 +9,13 @@ class UserSerializer(serializers.ModelSerializer): class Meta: model = User fields = [ - 'id', - 'username', - 'first_name', - 'last_name', - 'email', - 'remote_addr', + "id", + "username", + "first_name", + "last_name", + "email", + "remote_addr", ] def get_remote_addr(self, obj: User) -> str: - return self.context['request'].META['REMOTE_ADDR'] + return self.context["request"].META["REMOTE_ADDR"] diff --git a/{{cookiecutter.project_slug}}/src/users/factory.py b/{{cookiecutter.project_slug}}/src/users/factory.py index 40e238c1..57a1c786 100644 --- a/{{cookiecutter.project_slug}}/src/users/factory.py +++ b/{{cookiecutter.project_slug}}/src/users/factory.py @@ -7,7 +7,7 @@ @register def user(self: FactoryProtocol, **kwargs: dict) -> User: - return self.mixer.blend('users.User', **kwargs) + return self.mixer.blend("users.User", **kwargs) @register diff --git a/{{cookiecutter.project_slug}}/src/users/fixtures.py b/{{cookiecutter.project_slug}}/src/users/fixtures.py index f864d0bf..b8d2b207 100644 --- a/{{cookiecutter.project_slug}}/src/users/fixtures.py +++ b/{{cookiecutter.project_slug}}/src/users/fixtures.py @@ -8,5 +8,5 @@ @pytest.fixture -def user(factory: 'FixtureFactory') -> User: +def user(factory: "FixtureFactory") -> User: return factory.user() diff --git a/{{cookiecutter.project_slug}}/src/users/tests/test_password_hashing.py b/{{cookiecutter.project_slug}}/src/users/tests/test_password_hashing.py index 28fc9362..39418069 100644 --- a/{{cookiecutter.project_slug}}/src/users/tests/test_password_hashing.py +++ b/{{cookiecutter.project_slug}}/src/users/tests/test_password_hashing.py @@ -8,8 +8,8 @@ def test(): user = User.objects.create(username=str(uuid.uuid4())) - user.set_password('l0ve') + user.set_password("l0ve") user.save() # act - assert user.password.startswith('bcrypt') + assert user.password.startswith("bcrypt") diff --git a/{{cookiecutter.project_slug}}/src/users/tests/test_whoami.py b/{{cookiecutter.project_slug}}/src/users/tests/test_whoami.py index 12bcd387..da6d257c 100644 --- a/{{cookiecutter.project_slug}}/src/users/tests/test_whoami.py +++ b/{{cookiecutter.project_slug}}/src/users/tests/test_whoami.py @@ -4,13 +4,13 @@ def test_ok(as_user, user): - result = as_user.get('/api/v1/users/me/') + result = as_user.get("/api/v1/users/me/") - assert result['id'] == user.pk - assert result['username'] == user.username + assert result["id"] == user.pk + assert result["username"] == user.username def test_anon(as_anon): - result = as_anon.get('/api/v1/users/me/', as_response=True) + result = as_anon.get("/api/v1/users/me/", as_response=True) assert result.status_code == 401 diff --git a/{{cookiecutter.project_slug}}/src/users/urls.py b/{{cookiecutter.project_slug}}/src/users/urls.py index 92627cb1..cf6239c9 100644 --- a/{{cookiecutter.project_slug}}/src/users/urls.py +++ b/{{cookiecutter.project_slug}}/src/users/urls.py @@ -2,7 +2,7 @@ from users.api import viewsets -app_name = 'users' +app_name = "users" urlpatterns = [ - path('me/', viewsets.SelfView.as_view()), + path("me/", viewsets.SelfView.as_view()), ]