From 18d7612e7a4559dd6511db2c7d9a581cf5556d9f Mon Sep 17 00:00:00 2001 From: Lev Lybin Date: Wed, 11 Sep 2024 18:53:20 +0700 Subject: [PATCH] Ruff formatter, GitHub Action poetry pyproject.toml single config added Django 5.1 pypi trusted publish --- .editorconfig | 24 +- .github/FUNDING.yml | 2 +- .github/workflows/publish.yml | 48 ++-- .github/workflows/ruff.yaml | 11 + .github/workflows/tests.yml | 98 ++++---- .gitignore | 109 +++++++-- MANIFEST.in | 4 - README.md | 60 +++-- drf_recaptcha/apps.py | 4 +- drf_recaptcha/checks.py | 11 +- drf_recaptcha/client.py | 13 +- drf_recaptcha/constants.py | 6 +- drf_recaptcha/fields.py | 22 +- drf_recaptcha/validators.py | 39 +-- poetry.lock | 395 ++++++++++++++++++++++++++++++ pyproject.toml | 125 ++++++++++ setup.cfg | 27 -- setup.py | 64 ----- tests/conftest.py | 4 +- tests/settings.py | 6 +- tests/test_fields.py | 20 +- tests/test_secret_key_priority.py | 30 +-- tests/test_serializers.py | 12 +- tests/test_testing.py | 21 +- tests/test_validator.py | 58 +++-- 25 files changed, 904 insertions(+), 309 deletions(-) create mode 100644 .github/workflows/ruff.yaml delete mode 100644 MANIFEST.in create mode 100644 poetry.lock create mode 100644 pyproject.toml delete mode 100644 setup.cfg delete mode 100644 setup.py diff --git a/.editorconfig b/.editorconfig index c781a2e..7954d15 100644 --- a/.editorconfig +++ b/.editorconfig @@ -3,16 +3,36 @@ root = true [*] +charset = utf-8 +end_of_line = lf indent_style = space indent_size = 4 insert_final_newline = true trim_trailing_whitespace = true -end_of_line = lf -charset = utf-8 # Docstrings and comments use max_line_length = 119 [*.py] max_line_length = 119 +# Use 2 spaces for frontend files +[{*.html,*.js,*.css}] +indent_size = 2 + +[{*.yml,*.yaml}] +indent_size = 2 + +# The JSON files contain newlines inconsistently +[*.json] +indent_size = 2 +insert_final_newline = false + +# Minified JavaScript files shouldn't be changed +[**.min.js] +insert_final_newline = false + +# Batch files use tabs for indentation +[{*.bat,*.sh,Dockerfile}] +indent_style = tab + [*.md] trim_trailing_whitespace = false diff --git a/.github/FUNDING.yml b/.github/FUNDING.yml index 9e9fb6a..bf629fc 100644 --- a/.github/FUNDING.yml +++ b/.github/FUNDING.yml @@ -1 +1 @@ -github: [llybin] +github: [ llybin ] diff --git a/.github/workflows/publish.yml b/.github/workflows/publish.yml index ec70354..2549c5f 100644 --- a/.github/workflows/publish.yml +++ b/.github/workflows/publish.yml @@ -1,39 +1,31 @@ -# This workflow will upload a Python Package using Twine when a release is created -# For more information see: https://help.github.com/en/actions/language-and-framework-guides/using-python-with-github-actions#publishing-to-package-registries - -# This workflow uses actions that are not certified by GitHub. -# They are provided by a third-party and are governed by -# separate terms of service, privacy policy, and support -# documentation. - name: Upload Python Package on: release: - types: [published] + types: [ published ] permissions: contents: read jobs: - deploy: - + pypi-publish: + name: Upload release to PyPI runs-on: ubuntu-latest - + permissions: + id-token: write steps: - - uses: actions/checkout@v3 - - name: Set up Python - uses: actions/setup-python@v3 - with: - python-version: '3.x' - - name: Install dependencies - run: | - python -m pip install --upgrade pip - pip install build - - name: Build package - run: python -m build - - name: Publish package - uses: pypa/gh-action-pypi-publish@27b31702a0e7fc50959f5ad993c78deac1bdfc29 - with: - user: __token__ - password: ${{ secrets.PYPI_API_TOKEN }} + - uses: actions/checkout@v4 + - name: Set up Python + uses: actions/setup-python@v5 + with: + python-version: '3.x' + - name: Install dependencies + run: | + python -m pip install --upgrade pip + pip install build + - name: Build package + run: python -m build + - name: Publish package distributions to PyPI + uses: pypa/gh-action-pypi-publish@release/v1 + with: + attestations: true diff --git a/.github/workflows/ruff.yaml b/.github/workflows/ruff.yaml new file mode 100644 index 0000000..5253ee4 --- /dev/null +++ b/.github/workflows/ruff.yaml @@ -0,0 +1,11 @@ +name: Ruff +on: [ push, pull_request ] +jobs: + ruff: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + - uses: chartboost/ruff-action@v1 + - uses: chartboost/ruff-action@v1 + with: + args: 'format --check' diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml index 2508d8b..1531e9c 100644 --- a/.github/workflows/tests.yml +++ b/.github/workflows/tests.yml @@ -10,49 +10,63 @@ jobs: fail-fast: false matrix: python-version: [ "3.8", "3.9", "3.10", "3.11", "3.12" ] - django-version: [ "3.2", "4.0", "4.1", "4.2", "5.0"] + django-version: [ "3.2", "4.0", "4.1", "4.2", "5.0", "5.1" ] drf-version: [ "3.11", "3.12", "3.13", "3.14", "3.15" ] exclude: - # https://docs.djangoproject.com/en/dev/faq/install/#what-python-version-can-i-use-with-django - - django-version: "4.2" - python-version: "3.8" - - django-version: "4.2" - python-version: "3.9" - - django-version: "5.0" - python-version: "3.8" - - django-version: "5.0" - python-version: "3.9" - # https://www.django-rest-framework.org/community/release-notes/ - - django-version: "4.2" - drf-version: "3.11" - - django-version: "4.2" - drf-version: "3.12" - - django-version: "4.2" - drf-version: "3.13" - - django-version: "5.0" - drf-version: "3.11" - - django-version: "5.0" - drf-version: "3.12" - - django-version: "5.0" - drf-version: "3.13" + # https://docs.djangoproject.com/en/dev/faq/install/#what-python-version-can-i-use-with-django + - django-version: "4.2" + python-version: "3.8" + - django-version: "4.2" + python-version: "3.9" + - django-version: "5.0" + python-version: "3.8" + - django-version: "5.0" + python-version: "3.9" + - django-version: "5.1" + python-version: "3.8" + - django-version: "5.1" + python-version: "3.9" + # https://www.django-rest-framework.org/community/release-notes/ + - django-version: "4.2" + drf-version: "3.11" + - django-version: "4.2" + drf-version: "3.12" + - django-version: "4.2" + drf-version: "3.13" + - django-version: "5.0" + drf-version: "3.11" + - django-version: "5.0" + drf-version: "3.12" + - django-version: "5.0" + drf-version: "3.13" + - django-version: "5.1" + drf-version: "3.11" + - django-version: "5.1" + drf-version: "3.12" + - django-version: "5.1" + drf-version: "3.13" steps: - - uses: actions/checkout@v4 - - name: Set up Python ${{ matrix.python-version }} - uses: actions/setup-python@v5 - with: - python-version: ${{ matrix.python-version }} - - name: Install dependencies - run: | - python -m pip install --upgrade pip - pip install -e . - python -m pip install setuptools==71.0.0 - python -m pip install "Django~=${{ matrix.django-version }}.0" - python -m pip install "djangorestframework~=${{ matrix.drf-version }}.0" - - name: Test with pytest - run: python setup.py test - - name: Send coverage to Codacy - env: + #---------------------------------------------- + # check-out repo and set-up python + #---------------------------------------------- + - name: Check out repository + uses: actions/checkout@v4 + - name: Set up Python ${{ matrix.python-version }} + uses: actions/setup-python@v5 + with: + python-version: ${{ matrix.python-version }} + - name: Install dependencies + run: | + pip install --upgrade pip + pip install poetry + poetry install --no-interaction + poetry run pip install "Django~=${{ matrix.django-version }}.0" + poetry run pip install "djangorestframework~=${{ matrix.drf-version }}.0" + - name: Test with pytest + run: poetry run pytest + - name: Send coverage to Codacy + env: CODACY_PROJECT_TOKEN: ${{ secrets.CODACY_PROJECT_TOKEN }} - run: | - pip install codacy-coverage==1.3.11 - [ -z "$CODACY_PROJECT_TOKEN" ] && echo "CODACY_PROJECT_TOKEN is empty!" || python-codacy-coverage -r ./coverage.xml + run: | + pip install codacy-coverage==1.3.11 + [ -z "$CODACY_PROJECT_TOKEN" ] && echo "CODACY_PROJECT_TOKEN is empty!" || python-codacy-coverage -r ./coverage.xml diff --git a/.gitignore b/.gitignore index ee3d39e..ddb89eb 100644 --- a/.gitignore +++ b/.gitignore @@ -1,19 +1,94 @@ -*.pyc -*.db -*~ -.* - -/site/ -/htmlcov/ -/coverage/ -/build/ -/dist/ -/*.egg-info/ -/env/ +# Byte-compiled / optimized / DLL files +__pycache__/ +*.py[cod] +*$py.class + +# C extensions +*.so + +# Distribution / packaging +.Python +build/ +develop-eggs/ +dist/ +downloads/ +eggs/ +.eggs/ +lib/ +lib64/ +parts/ +sdist/ +var/ +wheels/ +*.egg-info/ +.installed.cfg +*.egg MANIFEST -coverage.* -!.coveragerc -!.editorconfig -!.gitignore -!.github +# PyInstaller +# Usually these files are written by a python script from a template +# before PyInstaller builds the exe, so as to inject date/other infos into it. +*.manifest +*.spec + +# Installer logs +pip-log.txt +pip-delete-this-directory.txt + +# Unit test / coverage reports +htmlcov/ +.tox/ +.coverage +.coverage.* +.cache +nosetests.xml +coverage.xml +*.cover +.hypothesis/ +.pytest_cache/ + +# Translations +*.mo +*.pot + +# Django stuff: +*.log +local_settings.py +db.sqlite3 + +# Flask stuff: +instance/ +.webassets-cache + +# Scrapy stuff: +.scrapy + +# Sphinx documentation +docs/_build/ + +# PyBuilder +target/ + +# Jupyter Notebook +.ipynb_checkpoints + +# pyenv +.python-version + +# SageMath parsed files +*.sage.py + +# Environments +.env +.venv +env/ +venv/ +ENV/ +env.bak/ +venv.bak/ + +# mkdocs documentation +/site + +# mypy +.mypy_cache/ diff --git a/MANIFEST.in b/MANIFEST.in deleted file mode 100644 index f6315dc..0000000 --- a/MANIFEST.in +++ /dev/null @@ -1,4 +0,0 @@ -include README.md -include LICENSE.md -global-exclude __pycache__ -global-exclude *.py[co] diff --git a/README.md b/README.md index 20900e8..a0f855f 100644 --- a/README.md +++ b/README.md @@ -5,29 +5,29 @@ [![CI](https://github.com/llybin/drf-recaptcha/workflows/tests/badge.svg)](https://github.com/llybin/drf-recaptcha/actions) [![Codacy Badge](https://api.codacy.com/project/badge/Grade/a9b44d24cba74c75bca6472b2ee8da67)](https://www.codacy.com/app/llybin/drf-recaptcha?utm_source=github.com&utm_medium=referral&utm_content=llybin/drf-recaptcha&utm_campaign=Badge_Grade) [![Codacy Badge](https://api.codacy.com/project/badge/Coverage/a9b44d24cba74c75bca6472b2ee8da67)](https://www.codacy.com/app/llybin/drf-recaptcha?utm_source=github.com&utm_medium=referral&utm_content=llybin/drf-recaptcha&utm_campaign=Badge_Coverage) -[![Code style: black](https://img.shields.io/badge/code%20style-black-000000.svg)](https://github.com/psf/black) +[![Ruff](https://img.shields.io/endpoint?url=https://raw.githubusercontent.com/astral-sh/ruff/main/assets/badge/v2.json)](https://github.com/astral-sh/ruff) [![PyPI](https://img.shields.io/pypi/v/drf-recaptcha)](https://pypi.org/project/drf-recaptcha/) [![PyPI - Downloads](https://img.shields.io/pypi/dm/drf-recaptcha)](https://pypi.org/project/drf-recaptcha/) [![PyPI - License](https://img.shields.io/pypi/l/drf-recaptcha)](https://pypi.org/project/drf-recaptcha/) ## Requirements -* Python: 3.8, 3.9, 3.10, 3.11, 3.12 -* Django: 3.2, 4.0, 4.1, 4.2, 5.0 -* DRF: 3.11, 3.12, 3.13, 3.14, 3.15 +* Python: 3.8, 3.9, 3.10, 3.11, 3.12 +* Django: 3.2, 4.0, 4.1, 4.2, 5.0, 5.1 +* DRF: 3.11, 3.12, 3.13, 3.14, 3.15 ## Installation -1. [Sign up for reCAPTCHA](https://www.google.com/recaptcha/) -2. Install with `pip install drf-recaptcha` -3. Add `"drf_recaptcha"` to your `INSTALLED_APPS` settings. -4. Set in settings `DRF_RECAPTCHA_SECRET_KEY` +1. [Sign up for reCAPTCHA](https://www.google.com/recaptcha/) +2. Install with `pip install drf-recaptcha` +3. Add `"drf_recaptcha"` to your `INSTALLED_APPS` settings. +4. Set in settings `DRF_RECAPTCHA_SECRET_KEY` ```python INSTALLED_APPS = [ - ..., - "drf_recaptcha", - ..., + ..., + "drf_recaptcha", + ..., ] ... @@ -47,16 +47,19 @@ class V2Serializer(Serializer): recaptcha = ReCaptchaV2Field() ... + class GetOTPView(APIView): def post(self, request): serializer = V2Serializer(data=request.data, context={"request": request}) serializer.is_valid(raise_exception=True) ... + class V3Serializer(Serializer): recaptcha = ReCaptchaV3Field(action="example") ... + class V3WithScoreSerializer(Serializer): recaptcha = ReCaptchaV3Field( action="example", @@ -64,6 +67,7 @@ class V3WithScoreSerializer(Serializer): ) ... + class GetReCaptchaScore(APIView): def post(self, request): serializer = V3WithScoreSerializer(data=request.data, context={"request": request}) @@ -71,6 +75,7 @@ class GetReCaptchaScore(APIView): score = serializer.fields['recaptcha'].score ... + class FeedbackSerializer(ModelSerializer): recaptcha = ReCaptchaV2Field() @@ -83,7 +88,7 @@ class FeedbackSerializer(ModelSerializer): ... return attrs - + class DynamicContextSecretKey(APIView): def post(self, request): if request.platform == "android": @@ -99,11 +104,11 @@ class DynamicContextSecretKey(APIView): ) serializer.is_valid(raise_exception=True) ... - + class DynamicContextSecretKey(GenericAPIView): serializer_class = WithReCaptchaSerializer - + def get_serializer_context(self): if self.request.platform == "android": recaptcha_secret_key = "SPECIAL_FOR_ANDROID" @@ -112,10 +117,10 @@ class DynamicContextSecretKey(GenericAPIView): context = super().get_serializer_context() context.update({"recaptcha_secret_key": recaptcha_secret_key}) return context - + class MobileSerializer(Serializer): - recaptcha = ReCaptchaV3Field(secret_key="") + recaptcha = ReCaptchaV3Field(secret_key="SPECIAL_MOBILE_KEY", action="feedback") ... ``` @@ -125,19 +130,21 @@ class MobileSerializer(Serializer): `DRF_RECAPTCHA_DEFAULT_V3_SCORE` - by default: `0.5`. Type: float. -`DRF_RECAPTCHA_ACTION_V3_SCORES` - by default: `{}`. Type: dict. You can define specific score for each action e.g. `{"login": 0.6, "feedback": 0.3}` +`DRF_RECAPTCHA_ACTION_V3_SCORES` - by default: `{}`. Type: dict. You can define specific score for each action e.g. +`{"login": 0.6, "feedback": 0.3}` `DRF_RECAPTCHA_DOMAIN` - by default: `www.google.com`. Type: str. -`DRF_RECAPTCHA_PROXY` - by default: `{}`. Type: dict. e.g. `{'http': 'http://127.0.0.1:8000', 'https': 'https://127.0.0.1:8000'}` +`DRF_RECAPTCHA_PROXY` - by default: `{}`. Type: dict. e.g. +`{'http': 'http://127.0.0.1:8000', 'https': 'https://127.0.0.1:8000'}` `DRF_RECAPTCHA_VERIFY_REQUEST_TIMEOUT` - by default: `10`. Type: int. ### Priority of secret_key value -1. settings `DRF_RECAPTCHA_SECRET_KEY` -2. the argument `secret_key` of field -3. request.context["recaptcha_secret_key"] +1. settings `DRF_RECAPTCHA_SECRET_KEY` +2. the argument `secret_key` of field +3. request.context["recaptcha_secret_key"] ### Silence the check error @@ -157,14 +164,15 @@ Required score value: `0.0 - 1.0` If not defined or zero in current item then value from next item. -1. Value for action in settings `DRF_RECAPTCHA_ACTION_V3_SCORES` -2. Value in argument `required_score` of field -3. Default value in settings `DRF_RECAPTCHA_DEFAULT_V3_SCORE` -4. Default value `0.5` +1. Value for action in settings `DRF_RECAPTCHA_ACTION_V3_SCORES` +2. Value in argument `required_score` of field +3. Default value in settings `DRF_RECAPTCHA_DEFAULT_V3_SCORE` +4. Default value `0.5` ## Testing -Set `DRF_RECAPTCHA_TESTING=True` in settings, no request to Google, no warnings, `DRF_RECAPTCHA_SECRET_KEY` is not required, set returning verification result in setting below. +Set `DRF_RECAPTCHA_TESTING=True` in settings, no request to Google, no warnings, `DRF_RECAPTCHA_SECRET_KEY` is not +required, set returning verification result in setting below. `DRF_RECAPTCHA_TESTING_PASS=True|False` - all responses are pass, default `True`. diff --git a/drf_recaptcha/apps.py b/drf_recaptcha/apps.py index 77d90ca..58ed975 100644 --- a/drf_recaptcha/apps.py +++ b/drf_recaptcha/apps.py @@ -5,6 +5,6 @@ class DRFreCaptchaConfig(AppConfig): name = "drf_recaptcha" verbose_name = "Django REST framework reCAPTCHA" - def ready(self): + def ready(self): # noqa: PLR6301 # Add System checks - from .checks import recaptcha_system_check # NOQA + from .checks import recaptcha_system_check # noqa: F401 diff --git a/drf_recaptcha/checks.py b/drf_recaptcha/checks.py index 7800ff7..a066905 100644 --- a/drf_recaptcha/checks.py +++ b/drf_recaptcha/checks.py @@ -1,11 +1,10 @@ from django.conf import settings -from django.core.checks import Tags, Error, Warning, register -from django.core.exceptions import ImproperlyConfigured +from django.core import checks from drf_recaptcha.constants import TEST_V2_SECRET_KEY -@register(Tags.security) +@checks.register(checks.Tags.security) def recaptcha_system_check(app_configs, **kwargs): errors = [] @@ -16,18 +15,18 @@ def recaptcha_system_check(app_configs, **kwargs): secret_key = getattr(settings, "DRF_RECAPTCHA_SECRET_KEY", None) if not secret_key: errors.append( - Error("settings.DRF_RECAPTCHA_SECRET_KEY must be set."), + checks.Error("settings.DRF_RECAPTCHA_SECRET_KEY must be set."), ) elif secret_key == TEST_V2_SECRET_KEY: errors.append( - Warning( + checks.Warning( "Google test key for reCAPTCHA v2 is used now.\n" "If you use reCAPTCHA v2 - you will always get No CAPTCHA and all" " verification requests will pass.\n" "If you use reCAPTCHA v3 - all verification requests will fail.", hint="Update settings.DRF_RECAPTCHA_SECRET_KEY", id="drf_recaptcha.recaptcha_test_key_error", - ) + ), ) return errors diff --git a/drf_recaptcha/client.py b/drf_recaptcha/client.py index 2b10e72..890f34d 100644 --- a/drf_recaptcha/client.py +++ b/drf_recaptcha/client.py @@ -16,8 +16,9 @@ def __init__(self, is_valid, error_codes=None, extra_data=None): def recaptcha_request(params): request_object = Request( - url="https://%s/recaptcha/api/siteverify" - % getattr(settings, "DRF_RECAPTCHA_DOMAIN", DEFAULT_RECAPTCHA_DOMAIN), + url="https://{}/recaptcha/api/siteverify".format( + getattr(settings, "DRF_RECAPTCHA_DOMAIN", DEFAULT_RECAPTCHA_DOMAIN) + ), data=params, headers={ "Content-type": "application/x-www-form-urlencoded", @@ -40,9 +41,11 @@ def recaptcha_request(params): def submit(recaptcha_response, secret_key, remoteip): - params = urlencode( - {"secret": secret_key, "response": recaptcha_response, "remoteip": remoteip} - ) + params = urlencode({ + "secret": secret_key, + "response": recaptcha_response, + "remoteip": remoteip, + }) params = params.encode("utf-8") diff --git a/drf_recaptcha/constants.py b/drf_recaptcha/constants.py index 7f90fd1..bd75861 100644 --- a/drf_recaptcha/constants.py +++ b/drf_recaptcha/constants.py @@ -6,13 +6,15 @@ # For reCAPTCHA v2, use the following test keys. # You will always get No CAPTCHA and all verification requests will pass. -TEST_V2_SECRET_KEY = "6LeIxAcTAAAAAGG-vFI1TnRWxMZNFuojJ4WifJWe" +TEST_V2_SECRET_KEY = "6LeIxAcTAAAAAGG-vFI1TnRWxMZNFuojJ4WifJWe" # noqa: S105 DEFAULT_RECAPTCHA_DOMAIN = "www.google.com" # https://developers.google.com/recaptcha/docs/v3 # -# reCAPTCHA v3 returns a score (1.0 is very likely a good interaction, 0.0 is very likely a bot). +# reCAPTCHA v3 returns a score: +# 1.0 is very likely a good interaction +# 0.0 is very likely a bot # Based on the score, you can take variable action in the context of your site. DEFAULT_V3_SCORE = 0.5 diff --git a/drf_recaptcha/fields.py b/drf_recaptcha/fields.py index e7a63a1..2e51161 100644 --- a/drf_recaptcha/fields.py +++ b/drf_recaptcha/fields.py @@ -7,7 +7,7 @@ class ReCaptchaV2Field(CharField): - def __init__(self, secret_key: str = None, **kwargs): + def __init__(self, secret_key: str | None = None, **kwargs): super().__init__(**kwargs) self.write_only = True @@ -18,11 +18,14 @@ def __init__(self, secret_key: str = None, **kwargs): self.validators.append(validator) -def validate_v3_settings_score_value(value: int or float or None, action: str = None): +def validate_v3_settings_score_value( + value: int or float or None, + action: str | None = None, +): if value is None: return - if not isinstance(value, (int, float)): + if not isinstance(value, int | float): if action: message = f"Score value for action '{action}' should be int or float" else: @@ -30,7 +33,7 @@ def validate_v3_settings_score_value(value: int or float or None, action: str = raise ImproperlyConfigured(message) - if value < 0.0 or 1.0 < value: + if value < 0.0 or value > 1.0: if action: message = f"Score value for action '{action}' should be between 0.0 - 1.0" else: @@ -46,7 +49,8 @@ def get_v3_action_score_from_settings(action: str) -> int or float or None: return None if not isinstance(scores_from_settings, dict): - raise ImproperlyConfigured("DRF_RECAPTCHA_ACTION_V3_SCORES should be a dict.") + msg = "DRF_RECAPTCHA_ACTION_V3_SCORES should be a dict." + raise ImproperlyConfigured(msg) action_score_from_settings = scores_from_settings.get(action, None) validate_v3_settings_score_value(action_score_from_settings, action) @@ -55,7 +59,9 @@ def get_v3_action_score_from_settings(action: str) -> int or float or None: def get_v3_default_score_from_settings() -> int or float or None: default_score_from_settings = getattr( - settings, "DRF_RECAPTCHA_DEFAULT_V3_SCORE", None + settings, + "DRF_RECAPTCHA_DEFAULT_V3_SCORE", + None, ) validate_v3_settings_score_value(default_score_from_settings) return default_score_from_settings @@ -65,8 +71,8 @@ class ReCaptchaV3Field(CharField): def __init__( self, action: str, - required_score: float = None, - secret_key: str = None, + required_score: float | None = None, + secret_key: str | None = None, **kwargs, ): super().__init__(**kwargs) diff --git a/drf_recaptcha/validators.py b/drf_recaptcha/validators.py index 03c3123..446457e 100644 --- a/drf_recaptcha/validators.py +++ b/drf_recaptcha/validators.py @@ -31,7 +31,7 @@ def __call__(self, value, serializer_field): client_ip = self._get_client_ip_from_context(serializer_field) recaptcha_secret_key = self._get_secret_key_from_context_or_default( - serializer_field + serializer_field, ) check_captcha = self._get_captcha_response_with_payload( @@ -51,27 +51,34 @@ def _run_validation_as_testing(self): testing_result = getattr(settings, "DRF_RECAPTCHA_TESTING_PASS", True) if not testing_result: raise ValidationError( - self.messages["captcha_invalid"], code="captcha_invalid" + self.messages["captcha_invalid"], + code="captcha_invalid", ) def _get_secret_key_from_context_or_default(self, serializer_field) -> str: return serializer_field.context.get( - "recaptcha_secret_key", self.default_recaptcha_secret_key + "recaptcha_secret_key", + self.default_recaptcha_secret_key, ) @staticmethod def _get_client_ip_from_context(serializer_field): request = serializer_field.context.get("request") if not request: - raise ImproperlyConfigured( - "Couldn't get client ip address. Check your serializer gets context with request." + msg = ( + "Couldn't get client ip address. " + "Check your serializer gets context with request." ) + raise ImproperlyConfigured(msg) recaptcha_client_ip, _ = get_client_ip(request) return recaptcha_client_ip def _get_captcha_response_with_payload( - self, value: str, secret_key: str, client_ip: str + self, + value: str, + secret_key: str, + client_ip: str, ) -> "RecaptchaResponse": try: check_captcha = client.submit( @@ -79,9 +86,9 @@ def _get_captcha_response_with_payload( secret_key=secret_key, remoteip=client_ip, ) - except HTTPError: # Catch timeouts, etc + except HTTPError: # Catch timeouts, etc. logger.exception("Couldn't get response, HTTPError") - raise ValidationError(self.messages["captcha_error"], code="captcha_error") + raise ValidationError(self.messages["captcha_error"], code="captcha_error") # noqa: B904 return check_captcha @@ -90,12 +97,12 @@ def _pre_validate_response(self, check_captcha: "RecaptchaResponse") -> None: return logger.error( - "ReCAPTCHA validation failed due to: %s", check_captcha.error_codes + "ReCAPTCHA validation failed due to: %s", + check_captcha.error_codes, ) raise ValidationError(self.messages["captcha_invalid"], code="captcha_invalid") - def _process_response(self, check_captcha_response): - ... + def _process_response(self, check_captcha_response): ... class ReCaptchaV2Validator(ReCaptchaValidator): @@ -108,7 +115,7 @@ def _process_response(self, check_captcha_response): if score is not None: logger.error( "The response contains score, reCAPTCHA v2 response doesn't" - " contains score, probably secret key for reCAPTCHA v3" + " contains score, probably secret key for reCAPTCHA v3", ) raise ValidationError(self.messages["captcha_error"], code="captcha_error") @@ -125,7 +132,7 @@ def _process_response(self, check_captcha_response): if self.score is None: logger.error( "The response not contains score, reCAPTCHA v3 response must" - " contains score, probably secret key for reCAPTCHA v2" + " contains score, probably secret key for reCAPTCHA v2", ) raise ValidationError(self.messages["captcha_error"], code="captcha_error") @@ -139,7 +146,8 @@ def _process_response(self, check_captcha_response): action, ) raise ValidationError( - self.messages["captcha_invalid"], code="captcha_invalid" + self.messages["captcha_invalid"], + code="captcha_invalid", ) if self.recaptcha_action != action: @@ -150,5 +158,6 @@ def _process_response(self, check_captcha_response): self.recaptcha_action, ) raise ValidationError( - self.messages["captcha_invalid"], code="captcha_invalid" + self.messages["captcha_invalid"], + code="captcha_invalid", ) diff --git a/poetry.lock b/poetry.lock new file mode 100644 index 0000000..2ebde65 --- /dev/null +++ b/poetry.lock @@ -0,0 +1,395 @@ +# This file is automatically @generated by Poetry 1.8.3 and should not be changed by hand. + +[[package]] +name = "asgiref" +version = "3.8.1" +description = "ASGI specs, helper code, and adapters" +optional = false +python-versions = ">=3.8" +files = [ + {file = "asgiref-3.8.1-py3-none-any.whl", hash = "sha256:3e1e3ecc849832fe52ccf2cb6686b7a55f82bb1d6aee72a58826471390335e47"}, + {file = "asgiref-3.8.1.tar.gz", hash = "sha256:c343bd80a0bec947a9860adb4c432ffa7db769836c64238fc34bdc3fec84d590"}, +] + +[package.dependencies] +typing-extensions = {version = ">=4", markers = "python_version < \"3.11\""} + +[package.extras] +tests = ["mypy (>=0.800)", "pytest", "pytest-asyncio"] + +[[package]] +name = "backports-zoneinfo" +version = "0.2.1" +description = "Backport of the standard library zoneinfo module" +optional = false +python-versions = ">=3.6" +files = [ + {file = "backports.zoneinfo-0.2.1-cp36-cp36m-macosx_10_14_x86_64.whl", hash = "sha256:da6013fd84a690242c310d77ddb8441a559e9cb3d3d59ebac9aca1a57b2e18bc"}, + {file = "backports.zoneinfo-0.2.1-cp36-cp36m-manylinux1_i686.whl", hash = "sha256:89a48c0d158a3cc3f654da4c2de1ceba85263fafb861b98b59040a5086259722"}, + {file = "backports.zoneinfo-0.2.1-cp36-cp36m-manylinux1_x86_64.whl", hash = "sha256:1c5742112073a563c81f786e77514969acb58649bcdf6cdf0b4ed31a348d4546"}, + {file = "backports.zoneinfo-0.2.1-cp36-cp36m-win32.whl", hash = "sha256:e8236383a20872c0cdf5a62b554b27538db7fa1bbec52429d8d106effbaeca08"}, + {file = "backports.zoneinfo-0.2.1-cp36-cp36m-win_amd64.whl", hash = "sha256:8439c030a11780786a2002261569bdf362264f605dfa4d65090b64b05c9f79a7"}, + {file = "backports.zoneinfo-0.2.1-cp37-cp37m-macosx_10_14_x86_64.whl", hash = "sha256:f04e857b59d9d1ccc39ce2da1021d196e47234873820cbeaad210724b1ee28ac"}, + {file = "backports.zoneinfo-0.2.1-cp37-cp37m-manylinux1_i686.whl", hash = "sha256:17746bd546106fa389c51dbea67c8b7c8f0d14b5526a579ca6ccf5ed72c526cf"}, + {file = "backports.zoneinfo-0.2.1-cp37-cp37m-manylinux1_x86_64.whl", hash = "sha256:5c144945a7752ca544b4b78c8c41544cdfaf9786f25fe5ffb10e838e19a27570"}, + {file = "backports.zoneinfo-0.2.1-cp37-cp37m-win32.whl", hash = "sha256:e55b384612d93be96506932a786bbcde5a2db7a9e6a4bb4bffe8b733f5b9036b"}, + {file = "backports.zoneinfo-0.2.1-cp37-cp37m-win_amd64.whl", hash = "sha256:a76b38c52400b762e48131494ba26be363491ac4f9a04c1b7e92483d169f6582"}, + {file = "backports.zoneinfo-0.2.1-cp38-cp38-macosx_10_14_x86_64.whl", hash = "sha256:8961c0f32cd0336fb8e8ead11a1f8cd99ec07145ec2931122faaac1c8f7fd987"}, + {file = "backports.zoneinfo-0.2.1-cp38-cp38-manylinux1_i686.whl", hash = "sha256:e81b76cace8eda1fca50e345242ba977f9be6ae3945af8d46326d776b4cf78d1"}, + {file = "backports.zoneinfo-0.2.1-cp38-cp38-manylinux1_x86_64.whl", hash = "sha256:7b0a64cda4145548fed9efc10322770f929b944ce5cee6c0dfe0c87bf4c0c8c9"}, + {file = "backports.zoneinfo-0.2.1-cp38-cp38-win32.whl", hash = "sha256:1b13e654a55cd45672cb54ed12148cd33628f672548f373963b0bff67b217328"}, + {file = "backports.zoneinfo-0.2.1-cp38-cp38-win_amd64.whl", hash = "sha256:4a0f800587060bf8880f954dbef70de6c11bbe59c673c3d818921f042f9954a6"}, + {file = "backports.zoneinfo-0.2.1.tar.gz", hash = "sha256:fadbfe37f74051d024037f223b8e001611eac868b5c5b06144ef4d8b799862f2"}, +] + +[package.extras] +tzdata = ["tzdata"] + +[[package]] +name = "colorama" +version = "0.4.6" +description = "Cross-platform colored terminal text." +optional = false +python-versions = "!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,!=3.4.*,!=3.5.*,!=3.6.*,>=2.7" +files = [ + {file = "colorama-0.4.6-py2.py3-none-any.whl", hash = "sha256:4f1d9991f5acc0ca119f9d443620b77f9d6b33703e51011c16baf57afb285fc6"}, + {file = "colorama-0.4.6.tar.gz", hash = "sha256:08695f5cb7ed6e0531a20572697297273c47b8cae5a63ffc6d6ed5c201be6e44"}, +] + +[[package]] +name = "coverage" +version = "7.6.1" +description = "Code coverage measurement for Python" +optional = false +python-versions = ">=3.8" +files = [ + {file = "coverage-7.6.1-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:b06079abebbc0e89e6163b8e8f0e16270124c154dc6e4a47b413dd538859af16"}, + {file = "coverage-7.6.1-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:cf4b19715bccd7ee27b6b120e7e9dd56037b9c0681dcc1adc9ba9db3d417fa36"}, + {file = "coverage-7.6.1-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e61c0abb4c85b095a784ef23fdd4aede7a2628478e7baba7c5e3deba61070a02"}, + {file = "coverage-7.6.1-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:fd21f6ae3f08b41004dfb433fa895d858f3f5979e7762d052b12aef444e29afc"}, + {file = "coverage-7.6.1-cp310-cp310-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8f59d57baca39b32db42b83b2a7ba6f47ad9c394ec2076b084c3f029b7afca23"}, + {file = "coverage-7.6.1-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:a1ac0ae2b8bd743b88ed0502544847c3053d7171a3cff9228af618a068ed9c34"}, + {file = "coverage-7.6.1-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:e6a08c0be454c3b3beb105c0596ebdc2371fab6bb90c0c0297f4e58fd7e1012c"}, + {file = "coverage-7.6.1-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:f5796e664fe802da4f57a168c85359a8fbf3eab5e55cd4e4569fbacecc903959"}, + {file = "coverage-7.6.1-cp310-cp310-win32.whl", hash = "sha256:7bb65125fcbef8d989fa1dd0e8a060999497629ca5b0efbca209588a73356232"}, + {file = "coverage-7.6.1-cp310-cp310-win_amd64.whl", hash = "sha256:3115a95daa9bdba70aea750db7b96b37259a81a709223c8448fa97727d546fe0"}, + {file = "coverage-7.6.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:7dea0889685db8550f839fa202744652e87c60015029ce3f60e006f8c4462c93"}, + {file = "coverage-7.6.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:ed37bd3c3b063412f7620464a9ac1314d33100329f39799255fb8d3027da50d3"}, + {file = "coverage-7.6.1-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d85f5e9a5f8b73e2350097c3756ef7e785f55bd71205defa0bfdaf96c31616ff"}, + {file = "coverage-7.6.1-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:9bc572be474cafb617672c43fe989d6e48d3c83af02ce8de73fff1c6bb3c198d"}, + {file = "coverage-7.6.1-cp311-cp311-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:0c0420b573964c760df9e9e86d1a9a622d0d27f417e1a949a8a66dd7bcee7bc6"}, + {file = "coverage-7.6.1-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:1f4aa8219db826ce6be7099d559f8ec311549bfc4046f7f9fe9b5cea5c581c56"}, + {file = "coverage-7.6.1-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:fc5a77d0c516700ebad189b587de289a20a78324bc54baee03dd486f0855d234"}, + {file = "coverage-7.6.1-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:b48f312cca9621272ae49008c7f613337c53fadca647d6384cc129d2996d1133"}, + {file = "coverage-7.6.1-cp311-cp311-win32.whl", hash = "sha256:1125ca0e5fd475cbbba3bb67ae20bd2c23a98fac4e32412883f9bcbaa81c314c"}, + {file = "coverage-7.6.1-cp311-cp311-win_amd64.whl", hash = "sha256:8ae539519c4c040c5ffd0632784e21b2f03fc1340752af711f33e5be83a9d6c6"}, + {file = "coverage-7.6.1-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:95cae0efeb032af8458fc27d191f85d1717b1d4e49f7cb226cf526ff28179778"}, + {file = "coverage-7.6.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:5621a9175cf9d0b0c84c2ef2b12e9f5f5071357c4d2ea6ca1cf01814f45d2391"}, + {file = "coverage-7.6.1-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:260933720fdcd75340e7dbe9060655aff3af1f0c5d20f46b57f262ab6c86a5e8"}, + {file = "coverage-7.6.1-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:07e2ca0ad381b91350c0ed49d52699b625aab2b44b65e1b4e02fa9df0e92ad2d"}, + {file = "coverage-7.6.1-cp312-cp312-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:c44fee9975f04b33331cb8eb272827111efc8930cfd582e0320613263ca849ca"}, + {file = "coverage-7.6.1-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:877abb17e6339d96bf08e7a622d05095e72b71f8afd8a9fefc82cf30ed944163"}, + {file = "coverage-7.6.1-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:3e0cadcf6733c09154b461f1ca72d5416635e5e4ec4e536192180d34ec160f8a"}, + {file = "coverage-7.6.1-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:c3c02d12f837d9683e5ab2f3d9844dc57655b92c74e286c262e0fc54213c216d"}, + {file = "coverage-7.6.1-cp312-cp312-win32.whl", hash = "sha256:e05882b70b87a18d937ca6768ff33cc3f72847cbc4de4491c8e73880766718e5"}, + {file = "coverage-7.6.1-cp312-cp312-win_amd64.whl", hash = "sha256:b5d7b556859dd85f3a541db6a4e0167b86e7273e1cdc973e5b175166bb634fdb"}, + {file = "coverage-7.6.1-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:a4acd025ecc06185ba2b801f2de85546e0b8ac787cf9d3b06e7e2a69f925b106"}, + {file = "coverage-7.6.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:a6d3adcf24b624a7b778533480e32434a39ad8fa30c315208f6d3e5542aeb6e9"}, + {file = "coverage-7.6.1-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d0c212c49b6c10e6951362f7c6df3329f04c2b1c28499563d4035d964ab8e08c"}, + {file = "coverage-7.6.1-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:6e81d7a3e58882450ec4186ca59a3f20a5d4440f25b1cff6f0902ad890e6748a"}, + {file = "coverage-7.6.1-cp313-cp313-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:78b260de9790fd81e69401c2dc8b17da47c8038176a79092a89cb2b7d945d060"}, + {file = "coverage-7.6.1-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:a78d169acd38300060b28d600344a803628c3fd585c912cacc9ea8790fe96862"}, + {file = "coverage-7.6.1-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:2c09f4ce52cb99dd7505cd0fc8e0e37c77b87f46bc9c1eb03fe3bc9991085388"}, + {file = "coverage-7.6.1-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:6878ef48d4227aace338d88c48738a4258213cd7b74fd9a3d4d7582bb1d8a155"}, + {file = "coverage-7.6.1-cp313-cp313-win32.whl", hash = "sha256:44df346d5215a8c0e360307d46ffaabe0f5d3502c8a1cefd700b34baf31d411a"}, + {file = "coverage-7.6.1-cp313-cp313-win_amd64.whl", hash = "sha256:8284cf8c0dd272a247bc154eb6c95548722dce90d098c17a883ed36e67cdb129"}, + {file = "coverage-7.6.1-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:d3296782ca4eab572a1a4eca686d8bfb00226300dcefdf43faa25b5242ab8a3e"}, + {file = "coverage-7.6.1-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:502753043567491d3ff6d08629270127e0c31d4184c4c8d98f92c26f65019962"}, + {file = "coverage-7.6.1-cp313-cp313t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:6a89ecca80709d4076b95f89f308544ec8f7b4727e8a547913a35f16717856cb"}, + {file = "coverage-7.6.1-cp313-cp313t-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:a318d68e92e80af8b00fa99609796fdbcdfef3629c77c6283566c6f02c6d6704"}, + {file = "coverage-7.6.1-cp313-cp313t-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:13b0a73a0896988f053e4fbb7de6d93388e6dd292b0d87ee51d106f2c11b465b"}, + {file = "coverage-7.6.1-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:4421712dbfc5562150f7554f13dde997a2e932a6b5f352edcce948a815efee6f"}, + {file = "coverage-7.6.1-cp313-cp313t-musllinux_1_2_i686.whl", hash = "sha256:166811d20dfea725e2e4baa71fffd6c968a958577848d2131f39b60043400223"}, + {file = "coverage-7.6.1-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:225667980479a17db1048cb2bf8bfb39b8e5be8f164b8f6628b64f78a72cf9d3"}, + {file = "coverage-7.6.1-cp313-cp313t-win32.whl", hash = "sha256:170d444ab405852903b7d04ea9ae9b98f98ab6d7e63e1115e82620807519797f"}, + {file = "coverage-7.6.1-cp313-cp313t-win_amd64.whl", hash = "sha256:b9f222de8cded79c49bf184bdbc06630d4c58eec9459b939b4a690c82ed05657"}, + {file = "coverage-7.6.1-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:6db04803b6c7291985a761004e9060b2bca08da6d04f26a7f2294b8623a0c1a0"}, + {file = "coverage-7.6.1-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:f1adfc8ac319e1a348af294106bc6a8458a0f1633cc62a1446aebc30c5fa186a"}, + {file = "coverage-7.6.1-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a95324a9de9650a729239daea117df21f4b9868ce32e63f8b650ebe6cef5595b"}, + {file = "coverage-7.6.1-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:b43c03669dc4618ec25270b06ecd3ee4fa94c7f9b3c14bae6571ca00ef98b0d3"}, + {file = "coverage-7.6.1-cp38-cp38-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8929543a7192c13d177b770008bc4e8119f2e1f881d563fc6b6305d2d0ebe9de"}, + {file = "coverage-7.6.1-cp38-cp38-musllinux_1_2_aarch64.whl", hash = "sha256:a09ece4a69cf399510c8ab25e0950d9cf2b42f7b3cb0374f95d2e2ff594478a6"}, + {file = "coverage-7.6.1-cp38-cp38-musllinux_1_2_i686.whl", hash = "sha256:9054a0754de38d9dbd01a46621636689124d666bad1936d76c0341f7d71bf569"}, + {file = "coverage-7.6.1-cp38-cp38-musllinux_1_2_x86_64.whl", hash = "sha256:0dbde0f4aa9a16fa4d754356a8f2e36296ff4d83994b2c9d8398aa32f222f989"}, + {file = "coverage-7.6.1-cp38-cp38-win32.whl", hash = "sha256:da511e6ad4f7323ee5702e6633085fb76c2f893aaf8ce4c51a0ba4fc07580ea7"}, + {file = "coverage-7.6.1-cp38-cp38-win_amd64.whl", hash = "sha256:3f1156e3e8f2872197af3840d8ad307a9dd18e615dc64d9ee41696f287c57ad8"}, + {file = "coverage-7.6.1-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:abd5fd0db5f4dc9289408aaf34908072f805ff7792632250dcb36dc591d24255"}, + {file = "coverage-7.6.1-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:547f45fa1a93154bd82050a7f3cddbc1a7a4dd2a9bf5cb7d06f4ae29fe94eaf8"}, + {file = "coverage-7.6.1-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:645786266c8f18a931b65bfcefdbf6952dd0dea98feee39bd188607a9d307ed2"}, + {file = "coverage-7.6.1-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:9e0b2df163b8ed01d515807af24f63de04bebcecbd6c3bfeff88385789fdf75a"}, + {file = "coverage-7.6.1-cp39-cp39-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:609b06f178fe8e9f89ef676532760ec0b4deea15e9969bf754b37f7c40326dbc"}, + {file = "coverage-7.6.1-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:702855feff378050ae4f741045e19a32d57d19f3e0676d589df0575008ea5004"}, + {file = "coverage-7.6.1-cp39-cp39-musllinux_1_2_i686.whl", hash = "sha256:2bdb062ea438f22d99cba0d7829c2ef0af1d768d1e4a4f528087224c90b132cb"}, + {file = "coverage-7.6.1-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:9c56863d44bd1c4fe2abb8a4d6f5371d197f1ac0ebdee542f07f35895fc07f36"}, + {file = "coverage-7.6.1-cp39-cp39-win32.whl", hash = "sha256:6e2cd258d7d927d09493c8df1ce9174ad01b381d4729a9d8d4e38670ca24774c"}, + {file = "coverage-7.6.1-cp39-cp39-win_amd64.whl", hash = "sha256:06a737c882bd26d0d6ee7269b20b12f14a8704807a01056c80bb881a4b2ce6ca"}, + {file = "coverage-7.6.1-pp38.pp39.pp310-none-any.whl", hash = "sha256:e9a6e0eb86070e8ccaedfbd9d38fec54864f3125ab95419970575b42af7541df"}, + {file = "coverage-7.6.1.tar.gz", hash = "sha256:953510dfb7b12ab69d20135a0662397f077c59b1e6379a768e97c59d852ee51d"}, +] + +[package.dependencies] +tomli = {version = "*", optional = true, markers = "python_full_version <= \"3.11.0a6\" and extra == \"toml\""} + +[package.extras] +toml = ["tomli"] + +[[package]] +name = "django" +version = "3.2.25" +description = "A high-level Python Web framework that encourages rapid development and clean, pragmatic design." +optional = false +python-versions = ">=3.6" +files = [ + {file = "Django-3.2.25-py3-none-any.whl", hash = "sha256:a52ea7fcf280b16f7b739cec38fa6d3f8953a5456986944c3ca97e79882b4e38"}, + {file = "Django-3.2.25.tar.gz", hash = "sha256:7ca38a78654aee72378594d63e51636c04b8e28574f5505dff630895b5472777"}, +] + +[package.dependencies] +asgiref = ">=3.3.2,<4" +pytz = "*" +sqlparse = ">=0.2.2" + +[package.extras] +argon2 = ["argon2-cffi (>=19.1.0)"] +bcrypt = ["bcrypt"] + +[[package]] +name = "django-ipware" +version = "2.1.0" +description = "A Django utility application that returns client's real IP address" +optional = false +python-versions = "*" +files = [ + {file = "django-ipware-2.1.0.tar.gz", hash = "sha256:a7c7a8fd019dbdc9c357e6e582f65034e897572fc79a7e467674efa8aef9d00b"}, +] + +[[package]] +name = "djangorestframework" +version = "3.15.1" +description = "Web APIs for Django, made easy." +optional = false +python-versions = ">=3.6" +files = [ + {file = "djangorestframework-3.15.1-py3-none-any.whl", hash = "sha256:3ccc0475bce968608cf30d07fb17d8e52d1d7fc8bfe779c905463200750cbca6"}, + {file = "djangorestframework-3.15.1.tar.gz", hash = "sha256:f88fad74183dfc7144b2756d0d2ac716ea5b4c7c9840995ac3bfd8ec034333c1"}, +] + +[package.dependencies] +"backports.zoneinfo" = {version = "*", markers = "python_version < \"3.9\""} +django = ">=3.0" + +[[package]] +name = "exceptiongroup" +version = "1.2.2" +description = "Backport of PEP 654 (exception groups)" +optional = false +python-versions = ">=3.7" +files = [ + {file = "exceptiongroup-1.2.2-py3-none-any.whl", hash = "sha256:3111b9d131c238bec2f8f516e123e14ba243563fb135d3fe885990585aa7795b"}, + {file = "exceptiongroup-1.2.2.tar.gz", hash = "sha256:47c2edf7c6738fafb49fd34290706d1a1a2f4d1c6df275526b62cbb4aa5393cc"}, +] + +[package.extras] +test = ["pytest (>=6)"] + +[[package]] +name = "iniconfig" +version = "2.0.0" +description = "brain-dead simple config-ini parsing" +optional = false +python-versions = ">=3.7" +files = [ + {file = "iniconfig-2.0.0-py3-none-any.whl", hash = "sha256:b6a85871a79d2e3b22d2d1b94ac2824226a63c6b741c88f7ae975f18b6778374"}, + {file = "iniconfig-2.0.0.tar.gz", hash = "sha256:2d91e135bf72d31a410b17c16da610a82cb55f6b0477d1a902134b24a455b8b3"}, +] + +[[package]] +name = "packaging" +version = "24.1" +description = "Core utilities for Python packages" +optional = false +python-versions = ">=3.8" +files = [ + {file = "packaging-24.1-py3-none-any.whl", hash = "sha256:5b8f2217dbdbd2f7f384c41c628544e6d52f2d0f53c6d0c3ea61aa5d1d7ff124"}, + {file = "packaging-24.1.tar.gz", hash = "sha256:026ed72c8ed3fcce5bf8950572258698927fd1dbda10a5e981cdf0ac37f4f002"}, +] + +[[package]] +name = "pluggy" +version = "1.5.0" +description = "plugin and hook calling mechanisms for python" +optional = false +python-versions = ">=3.8" +files = [ + {file = "pluggy-1.5.0-py3-none-any.whl", hash = "sha256:44e1ad92c8ca002de6377e165f3e0f1be63266ab4d554740532335b9d75ea669"}, + {file = "pluggy-1.5.0.tar.gz", hash = "sha256:2cffa88e94fdc978c4c574f15f9e59b7f4201d439195c3715ca9e2486f1d0cf1"}, +] + +[package.extras] +dev = ["pre-commit", "tox"] +testing = ["pytest", "pytest-benchmark"] + +[[package]] +name = "pytest" +version = "8.3.3" +description = "pytest: simple powerful testing with Python" +optional = false +python-versions = ">=3.8" +files = [ + {file = "pytest-8.3.3-py3-none-any.whl", hash = "sha256:a6853c7375b2663155079443d2e45de913a911a11d669df02a50814944db57b2"}, + {file = "pytest-8.3.3.tar.gz", hash = "sha256:70b98107bd648308a7952b06e6ca9a50bc660be218d53c257cc1fc94fda10181"}, +] + +[package.dependencies] +colorama = {version = "*", markers = "sys_platform == \"win32\""} +exceptiongroup = {version = ">=1.0.0rc8", markers = "python_version < \"3.11\""} +iniconfig = "*" +packaging = "*" +pluggy = ">=1.5,<2" +tomli = {version = ">=1", markers = "python_version < \"3.11\""} + +[package.extras] +dev = ["argcomplete", "attrs (>=19.2)", "hypothesis (>=3.56)", "mock", "pygments (>=2.7.2)", "requests", "setuptools", "xmlschema"] + +[[package]] +name = "pytest-cov" +version = "5.0.0" +description = "Pytest plugin for measuring coverage." +optional = false +python-versions = ">=3.8" +files = [ + {file = "pytest-cov-5.0.0.tar.gz", hash = "sha256:5837b58e9f6ebd335b0f8060eecce69b662415b16dc503883a02f45dfeb14857"}, + {file = "pytest_cov-5.0.0-py3-none-any.whl", hash = "sha256:4f0764a1219df53214206bf1feea4633c3b558a2925c8b59f144f682861ce652"}, +] + +[package.dependencies] +coverage = {version = ">=5.2.1", extras = ["toml"]} +pytest = ">=4.6" + +[package.extras] +testing = ["fields", "hunter", "process-tests", "pytest-xdist", "virtualenv"] + +[[package]] +name = "pytest-django" +version = "4.9.0" +description = "A Django plugin for pytest." +optional = false +python-versions = ">=3.8" +files = [ + {file = "pytest_django-4.9.0-py3-none-any.whl", hash = "sha256:1d83692cb39188682dbb419ff0393867e9904094a549a7d38a3154d5731b2b99"}, + {file = "pytest_django-4.9.0.tar.gz", hash = "sha256:8bf7bc358c9ae6f6fc51b6cebb190fe20212196e6807121f11bd6a3b03428314"}, +] + +[package.dependencies] +pytest = ">=7.0.0" + +[package.extras] +docs = ["sphinx", "sphinx-rtd-theme"] +testing = ["Django", "django-configurations (>=2.0)"] + +[[package]] +name = "pytest-mock" +version = "3.14.0" +description = "Thin-wrapper around the mock package for easier use with pytest" +optional = false +python-versions = ">=3.8" +files = [ + {file = "pytest-mock-3.14.0.tar.gz", hash = "sha256:2719255a1efeceadbc056d6bf3df3d1c5015530fb40cf347c0f9afac88410bd0"}, + {file = "pytest_mock-3.14.0-py3-none-any.whl", hash = "sha256:0b72c38033392a5f4621342fe11e9219ac11ec9d375f8e2a0c164539e0d70f6f"}, +] + +[package.dependencies] +pytest = ">=6.2.5" + +[package.extras] +dev = ["pre-commit", "pytest-asyncio", "tox"] + +[[package]] +name = "pytz" +version = "2024.2" +description = "World timezone definitions, modern and historical" +optional = false +python-versions = "*" +files = [ + {file = "pytz-2024.2-py2.py3-none-any.whl", hash = "sha256:31c7c1817eb7fae7ca4b8c7ee50c72f93aa2dd863de768e1ef4245d426aa0725"}, + {file = "pytz-2024.2.tar.gz", hash = "sha256:2aa355083c50a0f93fa581709deac0c9ad65cca8a9e9beac660adcbd493c798a"}, +] + +[[package]] +name = "ruff" +version = "0.6.4" +description = "An extremely fast Python linter and code formatter, written in Rust." +optional = false +python-versions = ">=3.7" +files = [ + {file = "ruff-0.6.4-py3-none-linux_armv6l.whl", hash = "sha256:c4b153fc152af51855458e79e835fb6b933032921756cec9af7d0ba2aa01a258"}, + {file = "ruff-0.6.4-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:bedff9e4f004dad5f7f76a9d39c4ca98af526c9b1695068198b3bda8c085ef60"}, + {file = "ruff-0.6.4-py3-none-macosx_11_0_arm64.whl", hash = "sha256:d02a4127a86de23002e694d7ff19f905c51e338c72d8e09b56bfb60e1681724f"}, + {file = "ruff-0.6.4-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:7862f42fc1a4aca1ea3ffe8a11f67819d183a5693b228f0bb3a531f5e40336fc"}, + {file = "ruff-0.6.4-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:eebe4ff1967c838a1a9618a5a59a3b0a00406f8d7eefee97c70411fefc353617"}, + {file = "ruff-0.6.4-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:932063a03bac394866683e15710c25b8690ccdca1cf192b9a98260332ca93408"}, + {file = "ruff-0.6.4-py3-none-manylinux_2_17_ppc64.manylinux2014_ppc64.whl", hash = "sha256:50e30b437cebef547bd5c3edf9ce81343e5dd7c737cb36ccb4fe83573f3d392e"}, + {file = "ruff-0.6.4-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:c44536df7b93a587de690e124b89bd47306fddd59398a0fb12afd6133c7b3818"}, + {file = "ruff-0.6.4-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:0ea086601b22dc5e7693a78f3fcfc460cceabfdf3bdc36dc898792aba48fbad6"}, + {file = "ruff-0.6.4-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:0b52387d3289ccd227b62102c24714ed75fbba0b16ecc69a923a37e3b5e0aaaa"}, + {file = "ruff-0.6.4-py3-none-musllinux_1_2_aarch64.whl", hash = "sha256:0308610470fcc82969082fc83c76c0d362f562e2f0cdab0586516f03a4e06ec6"}, + {file = "ruff-0.6.4-py3-none-musllinux_1_2_armv7l.whl", hash = "sha256:803b96dea21795a6c9d5bfa9e96127cc9c31a1987802ca68f35e5c95aed3fc0d"}, + {file = "ruff-0.6.4-py3-none-musllinux_1_2_i686.whl", hash = "sha256:66dbfea86b663baab8fcae56c59f190caba9398df1488164e2df53e216248baa"}, + {file = "ruff-0.6.4-py3-none-musllinux_1_2_x86_64.whl", hash = "sha256:34d5efad480193c046c86608dbba2bccdc1c5fd11950fb271f8086e0c763a5d1"}, + {file = "ruff-0.6.4-py3-none-win32.whl", hash = "sha256:f0f8968feea5ce3777c0d8365653d5e91c40c31a81d95824ba61d871a11b8523"}, + {file = "ruff-0.6.4-py3-none-win_amd64.whl", hash = "sha256:549daccee5227282289390b0222d0fbee0275d1db6d514550d65420053021a58"}, + {file = "ruff-0.6.4-py3-none-win_arm64.whl", hash = "sha256:ac4b75e898ed189b3708c9ab3fc70b79a433219e1e87193b4f2b77251d058d14"}, + {file = "ruff-0.6.4.tar.gz", hash = "sha256:ac3b5bfbee99973f80aa1b7cbd1c9cbce200883bdd067300c22a6cc1c7fba212"}, +] + +[[package]] +name = "sqlparse" +version = "0.5.1" +description = "A non-validating SQL parser." +optional = false +python-versions = ">=3.8" +files = [ + {file = "sqlparse-0.5.1-py3-none-any.whl", hash = "sha256:773dcbf9a5ab44a090f3441e2180efe2560220203dc2f8c0b0fa141e18b505e4"}, + {file = "sqlparse-0.5.1.tar.gz", hash = "sha256:bb6b4df465655ef332548e24f08e205afc81b9ab86cb1c45657a7ff173a3a00e"}, +] + +[package.extras] +dev = ["build", "hatch"] +doc = ["sphinx"] + +[[package]] +name = "tomli" +version = "2.0.1" +description = "A lil' TOML parser" +optional = false +python-versions = ">=3.7" +files = [ + {file = "tomli-2.0.1-py3-none-any.whl", hash = "sha256:939de3e7a6161af0c887ef91b7d41a53e7c5a1ca976325f429cb46ea9bc30ecc"}, + {file = "tomli-2.0.1.tar.gz", hash = "sha256:de526c12914f0c550d15924c62d72abc48d6fe7364aa87328337a31007fe8a4f"}, +] + +[[package]] +name = "typing-extensions" +version = "4.12.2" +description = "Backported and Experimental Type Hints for Python 3.8+" +optional = false +python-versions = ">=3.8" +files = [ + {file = "typing_extensions-4.12.2-py3-none-any.whl", hash = "sha256:04e5ca0351e0f3f85c6853954072df659d0d13fac324d0072316b67d7794700d"}, + {file = "typing_extensions-4.12.2.tar.gz", hash = "sha256:1a7ead55c7e559dd4dee8856e3a88b41225abfe1ce8df57b7c13915fe121ffb8"}, +] + +[metadata] +lock-version = "2.0" +python-versions = "^3.8" +content-hash = "561c50dd2e4e27a6de677809ab59ecb0e2e8174ca9ab6ee982254a53271a63e1" diff --git a/pyproject.toml b/pyproject.toml new file mode 100644 index 0000000..69e210b --- /dev/null +++ b/pyproject.toml @@ -0,0 +1,125 @@ +[tool.poetry] +name = "drf-recaptcha" +version = "3.3.0" +description = "Django rest framework recaptcha field serializer" +authors = ["Lev Lybin "] +license = "MIT" +readme = "README.md" +homepage = "https://github.com/llybin/drf-recaptcha" +repository = "https://github.com/llybin/drf-recaptcha" +keywords = [ + "django", + "drf", + "rest", + "django-rest-framework", + "reCAPTCHA", + "reCAPTCHA v2", + "reCAPTCHA v3", +] +classifiers = [ + "Development Status :: 5 - Production/Stable", + "Environment :: Web Environment", + "Environment :: Plugins", + "Intended Audience :: Developers", + "Framework :: Django", + "Framework :: Django :: 3.2", + "Framework :: Django :: 4.0", + "Framework :: Django :: 4.1", + "Framework :: Django :: 4.2", + "Framework :: Django :: 5.0", + "Framework :: Django :: 5.1", + "Intended Audience :: Developers", + "License :: OSI Approved :: MIT License", + "Operating System :: OS Independent", + "Topic :: Internet :: WWW/HTTP :: Dynamic Content", + "Topic :: Security", + "Topic :: Software Development :: Libraries", + "Programming Language :: Python", + "Programming Language :: Python :: 3 :: Only", + "Programming Language :: Python :: 3", + "Programming Language :: Python :: 3.8", + "Programming Language :: Python :: 3.9", + "Programming Language :: Python :: 3.10", + "Programming Language :: Python :: 3.11", + "Programming Language :: Python :: 3.12", + "Programming Language :: Python :: 3.13", +] + +[tool.poetry.dependencies] +python = "^3.8" +django = "^3.2" +djangorestframework = "^3.11" +django-ipware = "^2.1" + +[tool.poetry.dev-dependencies] +coverage = { version = "*", extras = ["toml"] } +pytest = "*" +pytest-django = "*" +pytest-cov = "*" +pytest-mock = "*" +pytz = "*" +ruff = "*" + +[build-system] +requires = ["poetry>=0.12"] +build-backend = "poetry.masonry.api" + +[tool.ruff] +preview = true +target-version = "py312" +src = ["drf_recaptcha"] + +[tool.ruff.lint] +select = [ + "F", "E", "W", "C", "I", "N", "UP", "YTT", "ASYNC", "S", "BLE", "FBT", "B", "A", "COM", "C4", "DTZ", "T10", "DJ", + "Q", "EM", "FA", "ISC", "ICN", "LOG", "G", "INP", "PIE", "T20", "PYI", "PT", "RSE", "RET", "SLF", "SLOT", "SIM", + "TID", "TCH", "INT", "ARG", "PTH", "TD", "FIX", "ERA", "PGH", "PL", "TRY", "FLY", "PERF", "FURB", "RUF" +] +ignore = [ + "INP001", "RUF012", "PLC0415", "COM812", "ISC001", + "S310", "ARG001" +] + +[tool.ruff.lint.per-file-ignores] +"test_*.py" = ["ARG001", "S101", "PLR0913", "PLR0917", "PLR2004", "PLR6301", "SLF001"] +"settings.py" = ["E501", "RUF001"] + +[tool.pytest.ini_options] +DJANGO_SETTINGS_MODULE = "tests.settings" +addopts = "-ra --cache-clear --no-cov-on-fail --cov-report=term --cov-report xml:coverage.xml --cov=drf_recaptcha" +console_output_style = "count" +pythonpath = "." + +[tool.coverage.run] +branch = true +omit = [ + "*__init__*", + "*/tests/*", +] + +[tool.coverage.report] +precision = 2 +fail_under = 94 +show_missing = true +skip_covered = true +ignore_errors = true +exclude_also = [ + # Don't complain about missing debug-only code: + "def __repr__", + "if self\\.debug", + # Don't complain if tests don't hit defensive assertion code: + "raise AssertionError", + "raise NotImplementedError", + # Don't complain if non-runnable code isn't run: + "if 0:", + "if __name__ == .__main__.:", + # Don't complain about abstract methods, they aren't run: + "@(abc\\.)?abstractmethod", + # Have to re-enable the standard pragma: + "pragma: no cover", + "pragma: nocover", + "pragma: full coverage", + # Don't complain TYPE_CHECKING: + "if TYPE_CHECKING:", + "if typing.TYPE_CHECKING:", +] diff --git a/setup.cfg b/setup.cfg deleted file mode 100644 index be771ca..0000000 --- a/setup.cfg +++ /dev/null @@ -1,27 +0,0 @@ -[metadata] -license_file = LICENSE.md - -[tool:pytest] -DJANGO_SETTINGS_MODULE = tests.settings -addopts = -ra --cache-clear --no-cov-on-fail --cov-report=term --cov-report xml:coverage.xml --cov=drf_recaptcha -console_output_style = count - -[coverage:report] -exclude_lines = - pragma: no cover - def __repr__ - raise AssertionError - raise NotImplementedError - if __name__ == .__main__.: - -[isort] -line_length = 88 -use_parentheses = True -include_trailing_comma = True -multi_line_output = 3 -known_first_party = drf_recaptcha -known_third_party = ipware,rest_framework -known_django = django - -[aliases] -test = pytest diff --git a/setup.py b/setup.py deleted file mode 100644 index 0abaac2..0000000 --- a/setup.py +++ /dev/null @@ -1,64 +0,0 @@ -from setuptools import find_packages, setup - - -def read(f): - return open(f, encoding="utf-8").read() - - -setup( - name="drf-recaptcha", - version="3.2.0", - description="Django rest framework recaptcha field serializer.", - long_description=read("README.md"), - long_description_content_type="text/markdown", - author="Lev Lybin", - author_email="lev.lybin@gmail.com", - license="MIT", - url="https://github.com/llybin/drf-recaptcha", - packages=find_packages(exclude=["tests*"]), - install_requires=[ - "django>=3.2", - "djangorestframework>=3.11", - "django-ipware>=2.1", - ], - setup_requires=["pytest-runner"], - tests_require=["pytest", "pytest-django", "pytest-cov", "pytest-mock", "pytz"], - python_requires=">=3.8", - include_package_data=True, - zip_safe=False, - keywords=[ - "django", - "drf", - "rest", - "django-rest-framework", - "reCAPTCHA", - "reCAPTCHA v2", - "reCAPTCHA v3", - ], - classifiers=[ - "Development Status :: 5 - Production/Stable", - "Environment :: Web Environment", - "Environment :: Plugins", - "Intended Audience :: Developers", - "Framework :: Django", - "Framework :: Django :: 3.2", - "Framework :: Django :: 4.0", - "Framework :: Django :: 4.1", - "Framework :: Django :: 4.2", - "Framework :: Django :: 5.0", - "Intended Audience :: Developers", - "License :: OSI Approved :: MIT License", - "Operating System :: OS Independent", - "Topic :: Internet :: WWW/HTTP :: Dynamic Content", - "Topic :: Security", - "Topic :: Software Development :: Libraries", - "Programming Language :: Python", - "Programming Language :: Python :: 3 :: Only", - "Programming Language :: Python :: 3", - "Programming Language :: Python :: 3.8", - "Programming Language :: Python :: 3.9", - "Programming Language :: Python :: 3.10", - "Programming Language :: Python :: 3.11", - "Programming Language :: Python :: 3.12", - ], -) diff --git a/tests/conftest.py b/tests/conftest.py index dfab656..4fab5a4 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -14,7 +14,7 @@ def mocked_serializer_field(mocker: "MockerFixture"): @pytest.fixture def mocked_serializer_field_with_request_context(mocker: "MockerFixture"): return mocker.Mock( - context={"request": mocker.Mock(META={"HTTP_X_FORWARDED_FOR": "4.3.2.1"})} + context={"request": mocker.Mock(META={"HTTP_X_FORWARDED_FOR": "4.3.2.1"})}, ) @@ -24,5 +24,5 @@ def mocked_serializer_field_with_request_secret_key_context(mocker): context={ "request": mocker.Mock(META={"HTTP_X_FORWARDED_FOR": "4.3.2.1"}), "recaptcha_secret_key": "from-context", - } + }, ) diff --git a/tests/settings.py b/tests/settings.py index 134e6f0..e19cf9f 100644 --- a/tests/settings.py +++ b/tests/settings.py @@ -1,13 +1,13 @@ -SECRET_KEY = "TEST_SECRET_KEY" +SECRET_KEY = "TEST_SECRET_KEY" # noqa: S105 INSTALLED_APPS = [ "drf_recaptcha", ] DATABASES = { - "default": {"ENGINE": "django.db.backends.sqlite3", "NAME": "test.sqlite3"} + "default": {"ENGINE": "django.db.backends.sqlite3", "NAME": "test.sqlite3"}, } USE_TZ = False -DRF_RECAPTCHA_SECRET_KEY = "TEST_DRF_RECAPTCHA_SECRET_KEY" +DRF_RECAPTCHA_SECRET_KEY = "TEST_DRF_RECAPTCHA_SECRET_KEY" # noqa: S105 diff --git a/tests/test_fields.py b/tests/test_fields.py index c07a2f4..de85025 100644 --- a/tests/test_fields.py +++ b/tests/test_fields.py @@ -1,6 +1,5 @@ import pytest from django.core.exceptions import ImproperlyConfigured - from drf_recaptcha.fields import ReCaptchaV2Field, ReCaptchaV3Field from drf_recaptcha.validators import ReCaptchaV2Validator, ReCaptchaV3Validator @@ -34,7 +33,11 @@ def test_recaptcha_v3_field_write_only(params, expected): ], ) def test_recaptcha_v3_field_score_priority( - params, from_settings, settings_default, expected, settings + params, + from_settings, + settings_default, + expected, + settings, ): settings.DRF_RECAPTCHA_ACTION_V3_SCORES = from_settings settings.DRF_RECAPTCHA_DEFAULT_V3_SCORE = settings_default @@ -109,7 +112,11 @@ def test_recaptcha_has_recaptcha_validator(field_class, params, validator_class) ], ) def test_recaptcha_v3_field_score_improperly_configured( - params, from_settings, settings_default, error, settings + params, + from_settings, + settings_default, + error, + settings, ): settings.DRF_RECAPTCHA_ACTION_V3_SCORES = from_settings settings.DRF_RECAPTCHA_DEFAULT_V3_SCORE = settings_default @@ -127,9 +134,12 @@ def test_recaptcha_v3_field_score_improperly_configured( ], ) def test_recaptcha_v3_field_valid_scores( - params, from_settings, settings_default, expected, settings + params, + from_settings, + settings_default, + expected, + settings, ): - settings.DRF_RECAPTCHA_ACTION_V3_SCORES = from_settings settings.DRF_RECAPTCHA_DEFAULT_V3_SCORE = settings_default diff --git a/tests/test_secret_key_priority.py b/tests/test_secret_key_priority.py index 528bee6..e9884cb 100644 --- a/tests/test_secret_key_priority.py +++ b/tests/test_secret_key_priority.py @@ -1,12 +1,11 @@ import pytest -from rest_framework.serializers import Serializer - from drf_recaptcha.fields import ReCaptchaV2Field, ReCaptchaV3Field +from rest_framework.serializers import Serializer @pytest.fixture(autouse=True) def _default_recaptcha_settings(settings): - settings.DRF_RECAPTCHA_SECRET_KEY = "from-default-settings" + settings.DRF_RECAPTCHA_SECRET_KEY = "from-default-settings" # noqa: S105 TEST_CASES = [ @@ -34,7 +33,8 @@ def _default_recaptcha_settings(settings): @pytest.mark.parametrize( - ("field_params", "field_context", "expected_secret_key"), TEST_CASES + ("field_params", "field_context", "expected_secret_key"), + TEST_CASES, ) def test_secret_key_priority_for_v2( field_params, @@ -43,7 +43,7 @@ def test_secret_key_priority_for_v2( mocker, ): validator = mocker.patch( - "drf_recaptcha.fields.ReCaptchaV2Validator._get_captcha_response_with_payload" + "drf_recaptcha.fields.ReCaptchaV2Validator._get_captcha_response_with_payload", ) field_context["request"] = mocker.Mock(META={"HTTP_X_FORWARDED_FOR": "4.3.2.1"}) @@ -61,21 +61,23 @@ class _Serializer(Serializer): @pytest.mark.parametrize( - ("field_params", "field_context", "expected_secret_key"), TEST_CASES + ("field_params", "field_context", "expected_secret_key"), + TEST_CASES, ) def test_secret_key_priority_for_v3( - field_params, field_context, expected_secret_key, mocker + field_params, + field_context, + expected_secret_key, + mocker, ): validator = mocker.patch( - "drf_recaptcha.fields.ReCaptchaV3Validator._get_captcha_response_with_payload" + "drf_recaptcha.fields.ReCaptchaV3Validator._get_captcha_response_with_payload", ) field_context["request"] = mocker.Mock(META={"HTTP_X_FORWARDED_FOR": "4.3.2.1"}) - field_params.update( - { - "action": "some", - "required_score": 1, - } - ) + field_params.update({ + "action": "some", + "required_score": 1, + }) class _Serializer(Serializer): recaptcha = ReCaptchaV3Field(**field_params, required=True) diff --git a/tests/test_serializers.py b/tests/test_serializers.py index ae3e58c..bf57eec 100644 --- a/tests/test_serializers.py +++ b/tests/test_serializers.py @@ -1,12 +1,11 @@ import pytest from django.core.exceptions import ImproperlyConfigured +from drf_recaptcha.constants import TEST_V2_SECRET_KEY +from drf_recaptcha.fields import ReCaptchaV2Field, ReCaptchaV3Field from rest_framework.exceptions import ValidationError from rest_framework.serializers import Serializer from rest_framework.test import APIRequestFactory -from drf_recaptcha.constants import TEST_V2_SECRET_KEY -from drf_recaptcha.fields import ReCaptchaV2Field, ReCaptchaV3Field - @pytest.mark.parametrize( ("field_class", "params"), @@ -22,8 +21,8 @@ class TestSerializer(Serializer): serializer.is_valid(raise_exception=True) assert ( - str(exc_info.value) - == "Couldn't get client ip address. Check your serializer gets context with request." + str(exc_info.value) == "Couldn't get client ip address." + " Check your serializer gets context with request." ) @@ -55,5 +54,6 @@ class TestSerializer(Serializer): assert ( str(exc_info.value) - == "{'token': [ErrorDetail(string='Error verifying reCAPTCHA, please try again.', code='captcha_error')]}" + == "{'token': [ErrorDetail(string='Error verifying reCAPTCHA, " + "please try again.', code='captcha_error')]}" ) diff --git a/tests/test_testing.py b/tests/test_testing.py index f4c0470..dd2a03d 100644 --- a/tests/test_testing.py +++ b/tests/test_testing.py @@ -1,7 +1,6 @@ import pytest -from rest_framework.exceptions import ValidationError - from drf_recaptcha.validators import ReCaptchaV2Validator, ReCaptchaV3Validator +from rest_framework.exceptions import ValidationError @pytest.mark.parametrize( @@ -12,11 +11,14 @@ ], ) def test_recaptcha_validator_testing_success( - validator_class, params, settings, mocked_serializer_field + validator_class, + params, + settings, + mocked_serializer_field, ): settings.DRF_RECAPTCHA_TESTING = True - validator = validator_class(secret_key="TEST_SECRET_KEY", **params) + validator = validator_class(secret_key="TEST_SECRET_KEY", **params) # noqa: S106 try: validator("test_token", mocked_serializer_field) except ValidationError: @@ -31,17 +33,20 @@ def test_recaptcha_validator_testing_success( ], ) def test_recaptcha_validator_testing_fail( - validator_class, params, settings, mocked_serializer_field + validator_class, + params, + settings, + mocked_serializer_field, ): settings.DRF_RECAPTCHA_TESTING = True settings.DRF_RECAPTCHA_TESTING_PASS = False - validator = validator_class(secret_key="TEST_SECRET_KEY", **params) + validator = validator_class(secret_key="TEST_SECRET_KEY", **params) # noqa: S106 with pytest.raises(ValidationError) as exc_info: validator("test_token", mocked_serializer_field) assert ( - str(exc_info.value) - == "[ErrorDetail(string='Error verifying reCAPTCHA, please try again.', code='captcha_invalid')]" + str(exc_info.value) == "[ErrorDetail(string='Error verifying reCAPTCHA, " + "please try again.', code='captcha_invalid')]" ) diff --git a/tests/test_validator.py b/tests/test_validator.py index 868874a..66d50ed 100644 --- a/tests/test_validator.py +++ b/tests/test_validator.py @@ -1,8 +1,7 @@ import pytest -from rest_framework.serializers import ValidationError - from drf_recaptcha.client import RecaptchaResponse from drf_recaptcha.validators import ReCaptchaV2Validator, ReCaptchaV3Validator +from rest_framework.serializers import ValidationError @pytest.fixture @@ -17,17 +16,19 @@ def _drf_recaptcha_testing(settings): ReCaptchaV3Validator, {"action": "test_action", "required_score": 0.4}, RecaptchaResponse( - is_valid=True, extra_data={"score": 0.6, "action": "test_action"} + is_valid=True, + extra_data={"score": 0.6, "action": "test_action"}, ), ), ( ReCaptchaV3Validator, {"action": "test_action", "required_score": 0.5}, RecaptchaResponse( - is_valid=True, extra_data={"score": 0.5, "action": "test_action"} + is_valid=True, + extra_data={"score": 0.5, "action": "test_action"}, ), ), - ] + ], ) def validator_with_mocked_captcha_valid_response(request, mocker): validator_class = request.param[0] @@ -35,10 +36,11 @@ def validator_with_mocked_captcha_valid_response(request, mocker): response = request.param[2] validator_with_mocked_get_response = validator_class( - secret_key="TEST_SECRET_KEY", **params + secret_key="TEST_SECRET_KEY", # noqa: S106 + **params, ) validator_with_mocked_get_response._get_captcha_response_with_payload = mocker.Mock( - return_value=response + return_value=response, ) return validator_with_mocked_get_response @@ -50,7 +52,8 @@ def test_recaptcha_validator_call_success( ): try: validator_with_mocked_captcha_valid_response( - "test_token", mocked_serializer_field_with_request_context + "test_token", + mocked_serializer_field_with_request_context, ) except ValidationError: pytest.fail("Validation is not passed") @@ -63,47 +66,56 @@ def test_recaptcha_validator_call_success( ReCaptchaV2Validator, {}, RecaptchaResponse(is_valid=False), - "[ErrorDetail(string='Error verifying reCAPTCHA, please try again.', code='captcha_invalid')]", + "[ErrorDetail(string='Error verifying reCAPTCHA, please try again.'," + " code='captcha_invalid')]", ), ( ReCaptchaV2Validator, {}, RecaptchaResponse( - is_valid=True, extra_data={"score": 0.6, "action": "test_action"} + is_valid=True, + extra_data={"score": 0.6, "action": "test_action"}, ), - "[ErrorDetail(string='Error verifying reCAPTCHA, please try again.', code='captcha_error')]", + "[ErrorDetail(string='Error verifying reCAPTCHA, please try again.'," + " code='captcha_error')]", ), ( ReCaptchaV3Validator, {"action": "test_action", "required_score": 0.4}, RecaptchaResponse(is_valid=False), - "[ErrorDetail(string='Error verifying reCAPTCHA, please try again.', code='captcha_invalid')]", + "[ErrorDetail(string='Error verifying reCAPTCHA, please try again.'," + " code='captcha_invalid')]", ), ( ReCaptchaV3Validator, {"action": "test_action", "required_score": 0.4}, RecaptchaResponse(is_valid=True), - "[ErrorDetail(string='Error verifying reCAPTCHA, please try again.', code='captcha_error')]", + "[ErrorDetail(string='Error verifying reCAPTCHA, please try again.'," + " code='captcha_error')]", ), ( ReCaptchaV3Validator, {"action": "test_action", "required_score": 0.4}, RecaptchaResponse(is_valid=True, extra_data={"score": 0.3}), - "[ErrorDetail(string='Error verifying reCAPTCHA, please try again.', code='captcha_invalid')]", + "[ErrorDetail(string='Error verifying reCAPTCHA, please try again.'," + " code='captcha_invalid')]", ), ( ReCaptchaV3Validator, {"action": "test_action", "required_score": 0.4}, RecaptchaResponse(is_valid=True, extra_data={"score": 0.5}), - "[ErrorDetail(string='Error verifying reCAPTCHA, please try again.', code='captcha_invalid')]", + "[ErrorDetail(string='Error verifying reCAPTCHA, please try again.'," + " code='captcha_invalid')]", ), ( ReCaptchaV3Validator, {"action": "test_action", "required_score": 0.4}, RecaptchaResponse( - is_valid=True, extra_data={"score": 0.5, "action": "other_action"} + is_valid=True, + extra_data={"score": 0.5, "action": "other_action"}, ), - "[ErrorDetail(string='Error verifying reCAPTCHA, please try again.', code='captcha_invalid')]", + "[ErrorDetail(string='Error verifying reCAPTCHA, please try again.'," + " code='captcha_invalid')]", ), ], ) @@ -115,7 +127,7 @@ def test_recaptcha_validator_call_fail( mocked_serializer_field_with_request_context, mocker, ): - validator = validator_class(secret_key="TEST_SECRET_KEY", **params) + validator = validator_class(secret_key="TEST_SECRET_KEY", **params) # noqa: S106 validator._get_captcha_response_with_payload = mocker.Mock(return_value=response) with pytest.raises(ValidationError) as exc_info: @@ -129,11 +141,12 @@ def test_recaptcha_validator_get_response_called_with_correct_ip( mocked_serializer_field_with_request_context, ): validator_with_mocked_captcha_valid_response( - "test_token", mocked_serializer_field_with_request_context + "test_token", + mocked_serializer_field_with_request_context, ) validator_with_mocked_captcha_valid_response._get_captcha_response_with_payload.assert_called_once_with( - secret_key="TEST_SECRET_KEY", + secret_key="TEST_SECRET_KEY", # noqa: S106 client_ip="4.3.2.1", value="test_token", ) @@ -145,11 +158,12 @@ def test_recaptcha_validator_takes_secret_key_from_context( mocker, ): validator_with_mocked_captcha_valid_response( - "test_token", mocked_serializer_field_with_request_secret_key_context + "test_token", + mocked_serializer_field_with_request_secret_key_context, ) validator_with_mocked_captcha_valid_response._get_captcha_response_with_payload.assert_called_once_with( - secret_key="from-context", + secret_key="from-context", # noqa: S106 client_ip=mocker.ANY, value="test_token", )