Merge pull request #418 from jazzband/development

Improve Python and package versioning and test suite
This commit is contained in:
Aleksi Häkli 2019-03-13 15:40:43 +02:00 committed by GitHub
commit 2cc6876f28
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
16 changed files with 62 additions and 74 deletions

View file

@ -2,8 +2,6 @@ dist: xenial
language: python
cache: pip
python:
- pypy3.5
- 3.5
- 3.6
- 3.7
env:
@ -12,11 +10,6 @@ env:
- DJANGO=2.1
- DJANGO=master
matrix:
exclude:
- python: pypy3.5
env: DJANGO=master
- python: 3.5
env: DJANGO=master
allow_failures:
- python: 3.6
env: DJANGO=master

View file

@ -21,10 +21,13 @@ def get_cool_off_threshold(attempt_time: datetime = None) -> datetime:
Get threshold for fetching access attempts from the database.
"""
if attempt_time is None:
return now() - get_cool_off()
cool_off = get_cool_off()
if cool_off is None:
raise TypeError('Cool off threshold can not be calculated with settings.AXES_COOLOFF_TIME set to None')
return attempt_time - get_cool_off()
if attempt_time is None:
return now() - cool_off
return attempt_time - cool_off
def filter_user_attempts(request: AxesHttpRequest, credentials: dict = None) -> QuerySet:

View file

@ -1,13 +1,3 @@
# TODO: Remove these imports after django-appconf does not depend on django.utils.six
try:
from django.utils import six # noqa
except ImportError: # pragma: no cover
import sys
import warnings
sys.modules['django.utils.six'] = __import__('six')
warnings.warn('django.utils.six was patched for django-appconf backwards compatibility', ImportWarning)
from django.conf import settings
from django.utils.translation import gettext_lazy as _

View file

@ -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

View file

@ -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}')

View file

@ -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.')

View file

@ -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.')

View file

@ -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.')

View file

@ -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')

View file

@ -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

View file

@ -2,13 +2,35 @@ from unittest.mock import patch
from django.contrib.auth import get_user_model
from django.http import HttpRequest
from django.test import override_settings
from django.utils.timezone import now
from axes.attempts import is_user_attempt_whitelisted
from axes.attempts import is_user_attempt_whitelisted, get_cool_off_threshold
from axes.models import AccessAttempt
from axes.tests.base import AxesTestCase
from axes.utils import reset
class GetCoolOffThresholdTestCase(AxesTestCase):
@override_settings(AXES_COOLOFF_TIME=42)
def test_get_cool_off_threshold(self):
timestamp = now()
with patch('axes.attempts.now', return_value=timestamp):
attempt_time = timestamp
threshold_now = get_cool_off_threshold(attempt_time)
attempt_time = None
threshold_none = get_cool_off_threshold(attempt_time)
self.assertEqual(threshold_now, threshold_none)
@override_settings(AXES_COOLOFF_TIME=None)
def test_get_cool_off_threshold_error(self):
with self.assertRaises(TypeError):
get_cool_off_threshold()
class ResetTestCase(AxesTestCase):
def test_reset(self):
self.create_attempt()

View file

@ -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(

View file

@ -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(

View file

@ -1,5 +1,5 @@
[mypy]
python_version = 3.5
python_version = 3.6
ignore_missing_imports = True
[mypy-axes.migrations.*]

View file

@ -35,7 +35,7 @@ setup(
python_requires='~=3.5',
install_requires=[
'django',
'django-appconf',
'django-appconf>=1.0.3',
'django-ipware>=2.0.2',
'pytz',
'six',
@ -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',

View file

@ -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
mypy axes
setenv =
PYTHONDONTWRITEBYTECODE=1