mirror of
https://github.com/jazzband/django-axes.git
synced 2026-03-16 22:30:23 +00:00
Drop Python 3.5 support
Most of our users are already running on Python 3.6+ and dropping 3.5 and below in a future oriented release allows us to focus on implementing more readable codebases. Fixes #417 Signed-off-by: Aleksi Häkli <aleksi.hakli@iki.fi>
This commit is contained in:
parent
3bece1aaaa
commit
b46e7cce01
12 changed files with 32 additions and 52 deletions
|
|
@ -1,4 +1,3 @@
|
|||
from collections import OrderedDict
|
||||
from datetime import timedelta
|
||||
from hashlib import md5
|
||||
from ipaddress import ip_address
|
||||
|
|
@ -73,10 +72,10 @@ def get_cool_off_iso8601(delta: timedelta) -> str:
|
|||
hours, minutes = divmod(minutes, 60)
|
||||
days, hours = divmod(hours, 24)
|
||||
|
||||
days_str = '{:.0f}D'.format(days) if days else ''
|
||||
days_str = f'{days:.0f}D' if days else ''
|
||||
|
||||
time_str = ''.join(
|
||||
'{value:.0f}{designator}'.format(value=value, designator=designator)
|
||||
f'{value:.0f}{designator}'
|
||||
for value, designator
|
||||
in [
|
||||
[hours, 'H'],
|
||||
|
|
@ -87,11 +86,8 @@ def get_cool_off_iso8601(delta: timedelta) -> str:
|
|||
)
|
||||
|
||||
if time_str:
|
||||
template = 'P{days_str}T{time_str}'
|
||||
else:
|
||||
template = 'P{days_str}'
|
||||
|
||||
return template.format(days_str=days_str, time_str=time_str)
|
||||
return f'P{days_str}T{time_str}'
|
||||
return f'P{days_str}'
|
||||
|
||||
|
||||
def get_credentials(username: str = None, **kwargs) -> dict:
|
||||
|
|
@ -173,15 +169,15 @@ def get_client_http_accept(request: HttpRequest) -> str:
|
|||
return request.META.get('HTTP_ACCEPT', '<unknown>')[:1025]
|
||||
|
||||
|
||||
def get_client_parameters(username: str, ip_address: str, user_agent: str) -> OrderedDict:
|
||||
def get_client_parameters(username: str, ip_address: str, user_agent: str) -> dict:
|
||||
"""
|
||||
Get query parameters for filtering AccessAttempt queryset.
|
||||
|
||||
This method returns an OrderedDict that guarantees iteration order for keys and values,
|
||||
This method returns a dict that guarantees iteration order for keys and values,
|
||||
and can so be used in e.g. the generation of hash keys or other deterministic functions.
|
||||
"""
|
||||
|
||||
filter_kwargs = OrderedDict() # type: OrderedDict
|
||||
filter_kwargs = dict()
|
||||
|
||||
if settings.AXES_ONLY_USER_FAILURES:
|
||||
# 1. Only individual usernames can be tracked with parametrization
|
||||
|
|
@ -209,7 +205,7 @@ def get_client_str(username: str, ip_address: str, user_agent: str, path_info: s
|
|||
Example log format would be ``{username: "example", ip_address: "127.0.0.1", path_info: "/example/"}``
|
||||
"""
|
||||
|
||||
client_dict = OrderedDict() # type: OrderedDict
|
||||
client_dict = dict()
|
||||
|
||||
if settings.AXES_VERBOSE:
|
||||
# Verbose mode logs every attribute that is available
|
||||
|
|
@ -227,7 +223,7 @@ def get_client_str(username: str, ip_address: str, user_agent: str, path_info: s
|
|||
|
||||
# Template the internal dictionary representation into a readable and concatenated key: "value" format
|
||||
template = ', '.join(
|
||||
'{key}: "{value}"'.format(key=key, value=value)
|
||||
f'{key}: "{value}"'
|
||||
for key, value
|
||||
in client_dict.items()
|
||||
)
|
||||
|
|
@ -254,7 +250,7 @@ def get_query_str(query: Type[QueryDict], max_length: int = 1024) -> str:
|
|||
query_dict.pop(settings.AXES_PASSWORD_FORM_FIELD, None)
|
||||
|
||||
query_str = '\n'.join(
|
||||
'{key}={value}'.format(key=key, value=value)
|
||||
f'{key}={value}'
|
||||
for key, value
|
||||
in query_dict.items()
|
||||
)
|
||||
|
|
@ -381,6 +377,6 @@ def get_client_cache_key(request_or_attempt: Union[HttpRequest, Any], credential
|
|||
|
||||
cache_key_components = ''.join(filter_kwargs.values())
|
||||
cache_key_digest = md5(cache_key_components.encode()).hexdigest()
|
||||
cache_key = 'axes-{}'.format(cache_key_digest)
|
||||
cache_key = f'axes-{cache_key_digest}'
|
||||
|
||||
return cache_key
|
||||
|
|
|
|||
|
|
@ -8,8 +8,4 @@ class Command(BaseCommand):
|
|||
|
||||
def handle(self, *args, **options): # pylint: disable=unused-argument
|
||||
for obj in AccessAttempt.objects.all():
|
||||
self.stdout.write('{ip}\t{username}\t{failures_since_start}'.format(
|
||||
ip=obj.ip_address,
|
||||
username=obj.username,
|
||||
failures_since_start=obj.failures_since_start,
|
||||
))
|
||||
self.stdout.write(f'{obj.ip_address}\t{obj.username}\t{obj.failures_since_start}')
|
||||
|
|
|
|||
|
|
@ -10,6 +10,6 @@ class Command(BaseCommand):
|
|||
count = reset()
|
||||
|
||||
if count:
|
||||
self.stdout.write('{0} attempts removed.'.format(count))
|
||||
self.stdout.write(f'{count} attempts removed.')
|
||||
else:
|
||||
self.stdout.write('No attempts found.')
|
||||
|
|
|
|||
|
|
@ -16,6 +16,6 @@ class Command(BaseCommand):
|
|||
count += reset(ip=ip)
|
||||
|
||||
if count:
|
||||
self.stdout.write('{0} attempts removed.'.format(count))
|
||||
self.stdout.write(f'{count} attempts removed.')
|
||||
else:
|
||||
self.stdout.write('No attempts found.')
|
||||
|
|
|
|||
|
|
@ -16,6 +16,6 @@ class Command(BaseCommand):
|
|||
count += reset(username=username)
|
||||
|
||||
if count:
|
||||
self.stdout.write('{0} attempts removed.'.format(count))
|
||||
self.stdout.write(f'{count} attempts removed.')
|
||||
else:
|
||||
self.stdout.write('No attempts found.')
|
||||
|
|
|
|||
|
|
@ -57,7 +57,7 @@ class AccessAttempt(AccessBase):
|
|||
)
|
||||
|
||||
def __str__(self):
|
||||
return 'Attempted Access: {}'.format(self.attempt_time)
|
||||
return f'Attempted Access: {self.attempt_time}'
|
||||
|
||||
class Meta:
|
||||
verbose_name = _('access attempt')
|
||||
|
|
@ -79,7 +79,7 @@ class AccessLog(AccessBase):
|
|||
)
|
||||
|
||||
def __str__(self):
|
||||
return 'Access Log for {} @ {}'.format(self.username, self.attempt_time)
|
||||
return f'Access Log for {self.username} @ {self.attempt_time}'
|
||||
|
||||
class Meta:
|
||||
verbose_name = _('access log')
|
||||
|
|
|
|||
|
|
@ -1,4 +1,4 @@
|
|||
from datetime import datetime # noqa
|
||||
from datetime import datetime
|
||||
|
||||
from django.http import HttpRequest
|
||||
|
||||
|
|
@ -8,12 +8,8 @@ class AxesHttpRequest(HttpRequest):
|
|||
Type definition for the HTTP request Axes uses.
|
||||
"""
|
||||
|
||||
def __init__(self):
|
||||
super().__init__()
|
||||
|
||||
# TODO: Move attribute definitions to class level in Python 3.6+
|
||||
self.axes_attempt_time = None # type: datetime
|
||||
self.axes_ip_address = None # type: str
|
||||
self.axes_user_agent = None # type: str
|
||||
self.axes_path_info = None # type: str
|
||||
self.axes_http_accept = None # type: str
|
||||
axes_attempt_time: datetime
|
||||
axes_ip_address: str
|
||||
axes_user_agent: str
|
||||
axes_path_info: str
|
||||
axes_http_accept: str
|
||||
|
|
|
|||
|
|
@ -94,9 +94,7 @@ class AxesHandlerTestCase(AxesTestCase):
|
|||
|
||||
def check_empty_request(self, log, handler):
|
||||
AxesProxyHandler.user_login_failed(sender=None, credentials={}, request=None)
|
||||
log.error.assert_called_with(
|
||||
'AXES: {handler}.user_login_failed does not function without a request.'.format(handler=handler)
|
||||
)
|
||||
log.error.assert_called_with(f'AXES: {handler}.user_login_failed does not function without a request.')
|
||||
|
||||
|
||||
@override_settings(
|
||||
|
|
|
|||
|
|
@ -258,10 +258,10 @@ class ClientCacheKeyTestCase(AxesTestCase):
|
|||
Test the cache key format.
|
||||
"""
|
||||
|
||||
cache_hash_digest = md5(self.ip_address.encode()).hexdigest()
|
||||
|
||||
# Getting cache key from request
|
||||
cache_hash_key = 'axes-{}'.format(
|
||||
md5(self.ip_address.encode()).hexdigest()
|
||||
)
|
||||
cache_hash_key = f'axes-{cache_hash_digest}'
|
||||
|
||||
request_factory = RequestFactory()
|
||||
request = request_factory.post(
|
||||
|
|
@ -295,10 +295,8 @@ class ClientCacheKeyTestCase(AxesTestCase):
|
|||
|
||||
# Getting cache key from request
|
||||
ip_address = self.ip_address
|
||||
cache_hash_key = 'axes-{}'.format(
|
||||
md5(ip_address.encode()).hexdigest()
|
||||
)
|
||||
|
||||
cache_hash_digest = md5(ip_address.encode()).hexdigest()
|
||||
cache_hash_key = f'axes-{cache_hash_digest}'
|
||||
|
||||
request_factory = RequestFactory()
|
||||
request = request_factory.post(
|
||||
|
|
|
|||
2
mypy.ini
2
mypy.ini
|
|
@ -1,5 +1,5 @@
|
|||
[mypy]
|
||||
python_version = 3.5
|
||||
python_version = 3.6
|
||||
ignore_missing_imports = True
|
||||
|
||||
[mypy-axes.migrations.*]
|
||||
|
|
|
|||
1
setup.py
1
setup.py
|
|
@ -55,7 +55,6 @@ setup(
|
|||
'Operating System :: OS Independent',
|
||||
'Programming Language :: Python',
|
||||
'Programming Language :: Python :: 3',
|
||||
'Programming Language :: Python :: 3.5',
|
||||
'Programming Language :: Python :: 3.6',
|
||||
'Programming Language :: Python :: 3.7',
|
||||
'Topic :: Internet :: Log Analysis',
|
||||
|
|
|
|||
7
tox.ini
7
tox.ini
|
|
@ -1,12 +1,9 @@
|
|||
[tox]
|
||||
envlist =
|
||||
py{py3,35,36,37}-django{111,20,21}
|
||||
py{36,37}-djangomaster
|
||||
py{36,37}-django{111,20,21,master}
|
||||
|
||||
[travis]
|
||||
python =
|
||||
pypy3.5: pypy3
|
||||
3.5: py35
|
||||
3.6: py36
|
||||
3.7: py37
|
||||
|
||||
|
|
@ -28,6 +25,6 @@ usedevelop = True
|
|||
commands =
|
||||
pytest
|
||||
prospector
|
||||
py{35,36,37}: mypy axes
|
||||
py{36,37}: mypy axes
|
||||
setenv =
|
||||
PYTHONDONTWRITEBYTECODE=1
|
||||
|
|
|
|||
Loading…
Reference in a new issue