diff --git a/.travis.yml b/.travis.yml index 28b2079..e218cbc 100644 --- a/.travis.yml +++ b/.travis.yml @@ -1,13 +1,9 @@ sudo: false language: python python: - - "3.5" - "3.6" - "3.7" - "3.8" -env: - global: - - RUNTEST_ARGS="-v --noinput" install: - pip install tox-travis script: tox diff --git a/CHANGELOG.md b/CHANGELOG.md new file mode 100644 index 0000000..dd88817 --- /dev/null +++ b/CHANGELOG.md @@ -0,0 +1,19 @@ +Changelog +========= + +### 1.9.0 Aug 30, 2020 + - Switched to Poetry (https://python-poetry.org/) + more cleanup + - Switched to pytest and pytest-django + - Drop support for pre-Django 1.10 style MIDDLEWARES + - Switch to old style url to path in urls.py + - Added Black and re-formatted and linted all the code + - Added isort and sorted all imports + - Bugfix: Add rule in admin was throwing an exception + - Handling "unknown" values in X-Forwarded-For header; introduces IPRESTRICT_USE_PROXY_IF_UNKNOWN header + - More cleanup + +### 1.8.1 Aug 24, 2020 + - General clean-up after forking the project from muccg/django-iprestrict + - Added support for Django 3+ + - Dropped support for old releases of Django/Python + diff --git a/MANIFEST.in b/MANIFEST.in deleted file mode 100644 index 9374481..0000000 --- a/MANIFEST.in +++ /dev/null @@ -1,4 +0,0 @@ -include docs/* -recursive-include iprestrict/static * -recursive-include iprestrict/templates * -prune debian diff --git a/README.md b/README.md index 71ee2c3..a5fc0ed 100644 --- a/README.md +++ b/README.md @@ -1,8 +1,12 @@ django-iprestrict-redux ======================= -[![Build Status](https://travis-ci.org/sztamas/django-iprestrict-redux.png?branch=master)](https://travis-ci.org/sztamas/django-iprestrict-redux) + +[![Build Status](https://travis-ci.org/sztamas/django-iprestrict-redux.png?branch=master)](https://travis-ci.org/sztamas/django-iprestrict-redux) [![PyPI](https://badge.fury.io/py/django-iprestrict-redux.svg)](https://pypi.python.org/pypi/django-iprestrict-redux) -[![Documentation Status](https://readthedocs.org/projects/django-iprestrict-redux/badge/?version=latest)](http://django-iprestrict-redux.readthedocs.org/en/latest/?badge=latest) +[![Documentation Status](https://readthedocs.org/projects/django-iprestrict/badge/?version=latest)](http://django-iprestrict.readthedocs.org/en/latest/?badge=latest) +![GitHub](https://img.shields.io/github/license/sztamas/django-iprestrict-redux) +[![Code style: black](https://img.shields.io/badge/code%20style-black-000000.svg)](https://github.com/psf/black) +[![Imports: isort](https://img.shields.io/badge/%20imports-isort-%231674b1?style=flat&labelColor=ef8336)](https://pycqa.github.io/isort/) ___ diff --git a/common-requirements.txt b/common-requirements.txt deleted file mode 100644 index f2d1df9..0000000 --- a/common-requirements.txt +++ /dev/null @@ -1 +0,0 @@ -pycountry==17.5.14 diff --git a/debian/changelog b/debian/changelog deleted file mode 100644 index c6df166..0000000 --- a/debian/changelog +++ /dev/null @@ -1,11 +0,0 @@ -python-django-iprestrict (0.4.0) unstable; urgency=medium - - * New release. - - -- Rodney Lorrimar Sun, 17 Jan 2016 12:33:21 +0000 - -python-django-iprestrict (0.1) ccg; urgency=low - - * Initial Release. - - -- Rodney Lorrimar Thu, 02 Jan 2014 09:42:14 +0800 diff --git a/debian/compat b/debian/compat deleted file mode 100644 index 45a4fb7..0000000 --- a/debian/compat +++ /dev/null @@ -1 +0,0 @@ -8 diff --git a/debian/control b/debian/control deleted file mode 100644 index 55a9690..0000000 --- a/debian/control +++ /dev/null @@ -1,18 +0,0 @@ -Source: python-django-iprestrict -Section: web -Priority: extra -Maintainer: Rodney Lorrimar -Build-Depends: debhelper (>= 8.0.0) -Standards-Version: 3.9.4 -Homepage: http://django-iprestrict.readthedocs.org/en/latest/ -Vcs-Git: https://github.com/muccg/django-iprestrict.git -Vcs-Browser: https://github.com/muccg/django-iprestrict - -Package: python-django-iprestrict -Architecture: any -Depends: ${misc:Depends}, - ${python:Depends}, - python-django -Description: Restricts access to a Django application by client IP ranges. - Django IPRestrict is an app + middleware to restrict access to all or - sections of a Django project by client IP ranges. diff --git a/debian/copyright b/debian/copyright deleted file mode 100644 index 3c956d9..0000000 --- a/debian/copyright +++ /dev/null @@ -1,41 +0,0 @@ -Format: http://www.debian.org/doc/packaging-manuals/copyright-format/1.0/ -Upstream-Name: django-iprestrict -Source: https://github.com/muccg/django-iprestrict - -Files: * -Copyright: 2012 Tamas Szabo w - 2012 Centre for Comparative Genomics w -License: BSD-3-clause - -Files: debian/* -Copyright: 2013 Centre for Comparative Genomics -License: BSD-3-clause - -License: BSD-3-clause - Redistribution and use in source and binary forms, with or without - modification, are permitted provided that the following conditions - are met: - . - Redistributions of source code must retain the above copyright - notice, this list of conditions and the following disclaimer. - . - Redistributions in binary form must reproduce the above copyright - notice, this list of conditions and the following disclaimer in the - documentation and/or other materials provided with the distribution - . - Neither the name of the Django IPRestrict nor the names of its - contributors may be used to endorse or promote products derived from - this software without specific prior written permission. - . - THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS - "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT - LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR - A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT - HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, - INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, - BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS - OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED - AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT - LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY - WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE - POSSIBILITY OF SUCH DAMAGE. diff --git a/debian/docs b/debian/docs deleted file mode 100644 index c61076a..0000000 --- a/debian/docs +++ /dev/null @@ -1,6 +0,0 @@ -README.md -TODO -docs/changing_and_reloading_rules.rst -docs/configuration.rst -docs/index.rst -docs/installation.rst diff --git a/debian/rules b/debian/rules deleted file mode 100755 index 2c44dc7..0000000 --- a/debian/rules +++ /dev/null @@ -1,13 +0,0 @@ -#!/usr/bin/make -f -# -*- makefile -*- -# Sample debian/rules that uses debhelper. -# This file was originally written by Joey Hess and Craig Small. -# As a special exception, when this file is copied by dh-make into a -# dh-make output file, you may use that output file without restriction. -# This special exception was added by Craig Small in version 0.37 of dh-make. - -# Uncomment this to turn on verbose mode. -#export DH_VERBOSE=1 - -%: - dh $@ --with-python2 diff --git a/debian/source/format b/debian/source/format deleted file mode 100644 index 89ae9db..0000000 --- a/debian/source/format +++ /dev/null @@ -1 +0,0 @@ -3.0 (native) diff --git a/docs/configuration.rst b/docs/configuration.rst index 679e1c3..c1360f4 100644 --- a/docs/configuration.rst +++ b/docs/configuration.rst @@ -19,11 +19,11 @@ Enable Django Admin for at least the iprestrict application. Add the urls of iprestrict to your project. Ex in your root urls.py:: - from django.conf.urls import url, include + from django.urls import include, path urlpatterns = [ # ... snip ... - url(r'^iprestrict/', include('iprestrict.urls', namespace='iprestrict')), + path('iprestrict/', include('iprestrict.urls', namespace='iprestrict')), This configuration will allow you to configure and test your restriction rules. @@ -140,9 +140,9 @@ Go to *YOUR_URL/iprestrict/* page. You can use the page to enter any *URL* and * Enabling the middleware ----------------------- -Add ``iprestrict.middleware.IPRestrictMiddleware`` to your ``MIDDLEWARE`` in your settings file (or ``MIDDLEWARE_CLASSES`` for old versions of Django). Generally, you will want this middleware to run early, before your session, auth etc. middlewares (the ``superuser_required`` decorator may also not function correctly if placed out of order):: +Add ``iprestrict.middleware.IPRestrictMiddleware`` to your ``MIDDLEWARE`` in your settings file. Generally, you will want this middleware to run early, before your session, auth etc. middlewares (the ``superuser_required`` decorator may also not function correctly if placed out of order):: - MIDDLEWARE_CLASSES = ( + MIDDLEWARE = ( 'django.middleware.common.CommonMiddleware', 'iprestrict.middleware.IPRestrictMiddleware', ... diff --git a/docs/installation.rst b/docs/installation.rst index dbb7fd9..ccfcf15 100644 --- a/docs/installation.rst +++ b/docs/installation.rst @@ -30,15 +30,20 @@ The country based lookups are optional, if you need it you can install them with Development ^^^^^^^^^^^ -For development create a ``virtualenv``, activate it and then:: +For development you will need Poetry_. - pip install -e .[geoip,dev] +.. _Poetry https://python-poetry.org + + +Fork the project and then: + + poetry install To run the tests against the *python* and *Django* in your virtualenv:: - ./runtests.sh + pytest -To run the tests against all combinations of *python 2*, *python 3*, and supported *Django* versions:: +To run the tests against all combinations of supported *Python 3*, and *Django* versions:: tox diff --git a/iprestrict/.vimp b/iprestrict/.vimp deleted file mode 100644 index e69de29..0000000 diff --git a/iprestrict/admin.py b/iprestrict/admin.py index c8705f8..9212504 100644 --- a/iprestrict/admin.py +++ b/iprestrict/admin.py @@ -1,21 +1,29 @@ # -*- coding: utf-8 -*- from __future__ import unicode_literals -from django.contrib import admin -from django import forms import re + +from django import forms +from django.contrib import admin + from . import ip_utils as ipu from . import models from .geoip import is_valid_country_code - -NOT_LETTER = re.compile(r'[^A-Z]+') +NOT_LETTER = re.compile(r"[^A-Z]+") @admin.register(models.Rule) class RuleAdmin(admin.ModelAdmin): - exclude = ('rank',) - list_display = ('url_pattern', 'ip_group', 'reverse_ip_group', 'is_allowed', 'move_up_url', 'move_down_url') + exclude = ("rank",) + list_display = ( + "url_pattern", + "ip_group", + "reverse_ip_group", + "is_allowed", + "move_up_url", + "move_down_url", + ) class IPRangeForm(forms.ModelForm): @@ -25,27 +33,34 @@ class Meta: def clean(self): cleaned_data = super(IPRangeForm, self).clean() - first_ip = cleaned_data.get('first_ip') + first_ip = cleaned_data.get("first_ip") if first_ip is None: # first_ip is Mandatory, so just let the default validator catch this return cleaned_data version = ipu.get_version(first_ip) - last_ip = cleaned_data['last_ip'] - cidr = cleaned_data.get('cidr_prefix_length') + last_ip = cleaned_data["last_ip"] + cidr = cleaned_data.get("cidr_prefix_length") if cidr is not None: if version == ipu.IPv4 and not (1 <= cidr <= 31): - self.add_error('cidr_prefix_length', 'Must be a number between 1 and 31') + self.add_error( + "cidr_prefix_length", "Must be a number between 1 and 31" + ) return cleaned_data if version == ipu.IPv6 and not (1 <= cidr <= 127): - self.add_error('cidr_prefix_length', 'Must be a number between 1 and 127') + self.add_error( + "cidr_prefix_length", "Must be a number between 1 and 127" + ) return cleaned_data if last_ip and cidr: - raise forms.ValidationError("Don't specify the Last IP if you specified a CIDR prefix length") + raise forms.ValidationError( + "Don't specify the Last IP if you specified a CIDR prefix length" + ) if last_ip: if version != ipu.get_version(last_ip): raise forms.ValidationError( - "Last IP should be the same type as First IP (%s)" % version) + "Last IP should be the same type as First IP (%s)" % version + ) if ipu.to_number(first_ip) > ipu.to_number(last_ip): raise forms.ValidationError("Last IP should be greater than First IP") @@ -54,7 +69,7 @@ def clean(self): # the user specified. Making sure it is set to the first ip in the # subnet. start, end = ipu.cidr_to_range(first_ip, cidr) - cleaned_data['first_ip'] = ipu.to_ip(start, version=version) + cleaned_data["first_ip"] = ipu.to_ip(start, version=version) return cleaned_data @@ -63,8 +78,8 @@ class IPRangeInline(admin.TabularInline): model = models.IPRange form = IPRangeForm - fields = ['first_ip', 'cidr_prefix_length', 'last_ip', 'ip_type', 'description'] - readonly_fields = ['ip_type'] + fields = ["first_ip", "cidr_prefix_length", "last_ip", "ip_type", "description"] + readonly_fields = ["ip_type"] extra = 2 @@ -74,36 +89,43 @@ class Meta: exclude = () def clean_country_codes(self): - codes = self.cleaned_data['country_codes'] - codes = set(filter(lambda x: x != '', - NOT_LETTER.split(self.cleaned_data['country_codes'].upper()))) + codes = self.cleaned_data["country_codes"] + codes = set( + filter( + lambda x: x != "", + NOT_LETTER.split(self.cleaned_data["country_codes"].upper()), + ) + ) if not all(map(is_valid_country_code, codes)): incorrect = [c for c in codes if not is_valid_country_code(c)] - msg = ('""%s" must be a valid country code' if len(incorrect) == 1 else - '""%s" must be valid country codes') - raise forms.ValidationError(msg % ', '.join(incorrect)) + msg = ( + '""%s" must be a valid country code' + if len(incorrect) == 1 + else '""%s" must be valid country codes' + ) + raise forms.ValidationError(msg % ", ".join(incorrect)) codes = list(codes) codes.sort() - return ', '.join(codes) + return ", ".join(codes) class IPLocationInline(admin.TabularInline): model = models.IPLocation form = IPLocationForm - fields = ['country_codes'] + fields = ["country_codes"] extra = 2 @admin.register(models.RangeBasedIPGroup) class RangeBasedIPGroupAdmin(admin.ModelAdmin): - exclude = ('type',) + exclude = ("type",) inlines = [IPRangeInline] @admin.register(models.LocationBasedIPGroup) class LocationBasedIPGroupAdmin(admin.ModelAdmin): - exclude = ('type',) + exclude = ("type",) inlines = [IPLocationInline] diff --git a/iprestrict/decorators.py b/iprestrict/decorators.py index a2fb892..c9ef89d 100644 --- a/iprestrict/decorators.py +++ b/iprestrict/decorators.py @@ -4,8 +4,9 @@ # Based on django django.contrib.admin.views.decorators.staff_member_required. -def superuser_required(view_func=None, redirect_field_name=REDIRECT_FIELD_NAME, - login_url='admin:login'): +def superuser_required( + view_func=None, redirect_field_name=REDIRECT_FIELD_NAME, login_url="admin:login" +): """ Decorator for views that checks that the user is logged in and is a superuser member, redirecting to the login page if necessary. @@ -13,7 +14,7 @@ def superuser_required(view_func=None, redirect_field_name=REDIRECT_FIELD_NAME, actual_decorator = user_passes_test( lambda u: u.is_active and u.is_superuser, login_url=login_url, - redirect_field_name=redirect_field_name + redirect_field_name=redirect_field_name, ) if view_func: return actual_decorator(view_func) diff --git a/iprestrict/geoip.py b/iprestrict/geoip.py index 3740a78..55a0408 100644 --- a/iprestrict/geoip.py +++ b/iprestrict/geoip.py @@ -6,12 +6,13 @@ """ from __future__ import unicode_literals -from django.core.exceptions import ImproperlyConfigured from django.conf import settings +from django.core.exceptions import ImproperlyConfigured try: from django.contrib.gis.geoip2 import GeoIP2 from geoip2.errors import AddressNotFoundError + geoip_available = True except ImportError: geoip_available = False @@ -19,21 +20,23 @@ try: from pycountry import countries except ImportError: - if getattr(settings, 'IPRESTRICT_GEOIP_ENABLED', True): + if getattr(settings, "IPRESTRICT_GEOIP_ENABLED", True): raise ImproperlyConfigured( "You are using location based IP Groups, but the python package " "pycountry isn't installed. Please install pycountry or set 'IPRESTRICT_GEOIP_ENABLED' " - "to False in settings.py") + "to False in settings.py" + ) # Special value for IP addresses that have no country like localhost. # Using the 'XX' special value allows for rules being set up on the 'XX' country code # and giving more control to end-users on what to do for special cases like this -NO_COUNTRY = 'XX' +NO_COUNTRY = "XX" class AdaptedGeoIP2(object): - '''Makes GeoIP2 behave like GeoIP''' + """Makes GeoIP2 behave like GeoIP""" + def __init__(self, *args, **kwargs): self._geoip = GeoIP2() @@ -46,20 +49,21 @@ def country_code(self, ip): class OurGeoIP(object): - def country_code(self, ip): raise ImproperlyConfigured( "You are using location based IP Groups, " - "but 'IPRESTRICT_GEOIP_ENABLED' isn't set to True in settings.py") + "but 'IPRESTRICT_GEOIP_ENABLED' isn't set to True in settings.py" + ) _geoip = OurGeoIP() -if getattr(settings, 'IPRESTRICT_GEOIP_ENABLED', True): +if getattr(settings, "IPRESTRICT_GEOIP_ENABLED", True): if not geoip_available: raise ImproperlyConfigured( "'IPRESTRICT_GEOIP_ENABLED' is set to True, but geoip2 is NOT available " " to import. Make sure the geoip libraries are installed as described in the Django " - "documentation") + "documentation" + ) _geoip = AdaptedGeoIP2() diff --git a/iprestrict/ip_utils.py b/iprestrict/ip_utils.py index b29e454..4b3ccb5 100644 --- a/iprestrict/ip_utils.py +++ b/iprestrict/ip_utils.py @@ -1,13 +1,12 @@ # -*- coding: utf-8 -*- from __future__ import unicode_literals - -IPv4 = 'ipv4' -IPv6 = 'ipv6' +IPv4 = "ipv4" +IPv6 = "ipv6" def get_version(ip): - return IPv6 if ':' in ip else IPv4 + return IPv6 if ":" in ip else IPv4 def is_ipv4(ip): @@ -27,44 +26,46 @@ def ipv4_to_number(ip): def ipv6_to_number(ip): - if '.' in ip: + if "." in ip: ip = convert_mixed(ip) - if '::' in ip: + if "::" in ip: ip = explode(ip) - return _ip_to_number(ip, separator=':', group_size=2 ** 16, base=16) + return _ip_to_number(ip, separator=":", group_size=2 ** 16, base=16) def explode(ip): - if ip.count('::') > 1: + if ip.count("::") > 1: raise ValueError('Invalid ip address "%s". "::" can appear only once.' % ip) - pre, post = [reject_empty(x.split(':')) for x in ip.split('::')] - return ':'.join(pre + ['0'] * (8 - len(pre) - len(post)) + post) + pre, post = [reject_empty(x.split(":")) for x in ip.split("::")] + return ":".join(pre + ["0"] * (8 - len(pre) - len(post)) + post) def convert_mixed(ip): - last_colon = ip.rfind(':') - ipv6, ipv4 = ip[:last_colon+1], ip[last_colon+1:] - if ipv4.count('.') != 3: - raise ValueError('Invalid IPv6 address "%s". Dotted ipv4 part should be at the end.' % ip) - a, b, c, d = ipv4.split('.') + last_colon = ip.rfind(":") + ipv6, ipv4 = ip[: last_colon + 1], ip[last_colon + 1 :] + if ipv4.count(".") != 3: + raise ValueError( + 'Invalid IPv6 address "%s". Dotted ipv4 part should be at the end.' % ip + ) + a, b, c, d = ipv4.split(".") pre_last = 256 * int(a) + int(b) last = 256 * int(c) + int(d) - return '%s:%x:%x' % (ipv6, pre_last, last) + return "%s:%x:%x" % (ipv6, pre_last, last) def to_ip(number, version=IPv4): if version == IPv6: - separator = ':' + separator = ":" parts_count = 8 parts_length = 16 - fmt = '%x' + fmt = "%x" else: - separator = '.' + separator = "." parts_count = 4 parts_length = 8 - fmt = '%d' - mask = int('1' * parts_length, 2) + fmt = "%d" + mask = int("1" * parts_length, 2) parts = [] for i in range(parts_count): shifted_number = number >> (parts_length * i) @@ -76,14 +77,14 @@ def to_ip(number, version=IPv4): def cidr_to_range(ip, prefix_length): ip_length = 128 if is_ipv6(ip) else 32 ip = to_number(ip) - start_mask = int('1' * prefix_length, 2) << (ip_length - prefix_length) - end_mask = int('1' * (ip_length - prefix_length), 2) + start_mask = int("1" * prefix_length, 2) << (ip_length - prefix_length) + end_mask = int("1" * (ip_length - prefix_length), 2) start = ip & start_mask end = start | end_mask return (start, end) -def _ip_to_number(ip, separator='.', group_size=2 ** 8, base=10): +def _ip_to_number(ip, separator=".", group_size=2 ** 8, base=10): parts = ip.split(separator) parts = [int(p, base) for p in reversed(parts)] nr = 0 diff --git a/iprestrict/management/commands/_utils.py b/iprestrict/management/commands/_utils.py index d14acb2..eab647b 100644 --- a/iprestrict/management/commands/_utils.py +++ b/iprestrict/management/commands/_utils.py @@ -4,5 +4,7 @@ def warn_about_renamed_command(old_name, new_name): # DeprecationWarnings are ignored by default, so lets make sure that # the warnings are shown by using the default UserWarning instead - warnings.warn("The command '%s' has been deprecated and it will be removed in a future version. " - "Please use '%s' instead." % (old_name, new_name)) + warnings.warn( + "The command '%s' has been deprecated and it will be removed in a future version. " + "Please use '%s' instead." % (old_name, new_name) + ) diff --git a/iprestrict/management/commands/add_ip_to_group.py b/iprestrict/management/commands/add_ip_to_group.py index 448ffbb..a1118fb 100644 --- a/iprestrict/management/commands/add_ip_to_group.py +++ b/iprestrict/management/commands/add_ip_to_group.py @@ -4,22 +4,25 @@ from django.core.exceptions import ValidationError from django.core.management.base import BaseCommand, CommandError from django.core.validators import validate_ipv46_address + from ... import models class Command(BaseCommand): - help = 'Adds an IP address to an IP Group.' + help = "Adds an IP address to an IP Group." def add_arguments(self, parser): - parser.add_argument('group_name') - parser.add_argument('ip', nargs='+') + parser.add_argument("group_name") + parser.add_argument("ip", nargs="+") def handle(self, *args, **options): - group_name = options.get('group_name') - ips = options.get('ip') + group_name = options.get("group_name") + ips = options.get("ip") try: - ip_group = models.IPGroup.objects.get(name__iexact=group_name, type=models.TYPE_RANGE) + ip_group = models.IPGroup.objects.get( + name__iexact=group_name, type=models.TYPE_RANGE + ) except models.IPGroup.DoesNotExist: try: models.IPGroup.objects.get(name__iexact=group_name) diff --git a/iprestrict/management/commands/import_rules.py b/iprestrict/management/commands/import_rules.py index f4e17f8..2f63cf9 100644 --- a/iprestrict/management/commands/import_rules.py +++ b/iprestrict/management/commands/import_rules.py @@ -1,25 +1,25 @@ # -*- coding: utf-8 -*- from __future__ import unicode_literals -from django.core.management.base import BaseCommand from django.core.management import call_command +from django.core.management.base import BaseCommand from django.db import transaction from ... import models class Command(BaseCommand): - help = 'Replaces the current rules in the DB with the rules in the given fixture file(s).' + help = "Replaces the current rules in the DB with the rules in the given fixture file(s)." def add_arguments(self, parser): - parser.add_argument('fixture', nargs='+') + parser.add_argument("fixture", nargs="+") def handle(self, *args, **options): - fixtures = options.get('fixture', []) + fixtures = options.get("fixture", []) with transaction.atomic(): self.delete_existing_rules() - call_command('loaddata', *fixtures, **options) + call_command("loaddata", *fixtures, **options) def delete_existing_rules(self): models.Rule.objects.all().delete() diff --git a/iprestrict/management/commands/importrules.py b/iprestrict/management/commands/importrules.py index 4cb602e..780a36b 100644 --- a/iprestrict/management/commands/importrules.py +++ b/iprestrict/management/commands/importrules.py @@ -1,12 +1,11 @@ # -*- coding: utf-8 -*- from __future__ import unicode_literals -from ._utils import warn_about_renamed_command from . import import_rules +from ._utils import warn_about_renamed_command class Command(import_rules.Command): - def handle(self, *args, **options): - warn_about_renamed_command('importrules', 'import_rules') + warn_about_renamed_command("importrules", "import_rules") super(Command, self).handle(*args, **options) diff --git a/iprestrict/management/commands/reload_rules.py b/iprestrict/management/commands/reload_rules.py index 1dbaafb..0fa4901 100644 --- a/iprestrict/management/commands/reload_rules.py +++ b/iprestrict/management/commands/reload_rules.py @@ -2,21 +2,24 @@ from __future__ import unicode_literals from django.core.management.base import BaseCommand, CommandError + from ... import models from ...middleware import get_reload_rules_setting class Command(BaseCommand): - help = 'Requests the reload of the ip restriction rules from the DB.' + help = "Requests the reload of the ip restriction rules from the DB." def handle(self, *args, **options): - verbosity = int(options.get('verbosity', '1')) + verbosity = int(options.get("verbosity", "1")) reload_rules = get_reload_rules_setting() if not reload_rules: - raise CommandError("IPRESTRICT_RELOAD_RULES is set to False. " - "Your IPRestrict rules can't be changed dynamically.") + raise CommandError( + "IPRESTRICT_RELOAD_RULES is set to False. " + "Your IPRestrict rules can't be changed dynamically." + ) models.ReloadRulesRequest.request_reload() if verbosity >= 1: - self.stdout.write('Successfully requested reload of rules') + self.stdout.write("Successfully requested reload of rules") diff --git a/iprestrict/management/commands/reloadrules.py b/iprestrict/management/commands/reloadrules.py index 2ee6ba5..5e5c966 100644 --- a/iprestrict/management/commands/reloadrules.py +++ b/iprestrict/management/commands/reloadrules.py @@ -1,12 +1,11 @@ # -*- coding: utf-8 -*- from __future__ import unicode_literals -from ._utils import warn_about_renamed_command from . import reload_rules +from ._utils import warn_about_renamed_command class Command(reload_rules.Command): - def handle(self, *args, **options): - warn_about_renamed_command('reloadrules', 'reload_rules') + warn_about_renamed_command("reloadrules", "reload_rules") super(Command, self).handle(*args, **options) diff --git a/iprestrict/middleware.py b/iprestrict/middleware.py index 4882c91..9447e48 100644 --- a/iprestrict/middleware.py +++ b/iprestrict/middleware.py @@ -1,38 +1,32 @@ -# -*- coding: utf-8 -*- -from __future__ import unicode_literals - -from django.core import exceptions -from django.conf import settings import logging import warnings + +from django.conf import settings +from django.core import exceptions + from .models import ReloadRulesRequest from .restrictor import IPRestrictor -try: - from django.utils.deprecation import MiddlewareMixin -except ImportError: - class MiddlewareMixin(object): - def __init__(self, *args, **kwargs): - pass - logger = logging.getLogger(__name__) -class IPRestrictMiddleware(MiddlewareMixin): - restrictor = None - trusted_proxies = None - allow_proxies = None - reload_rules = None +class IPRestrictMiddleware: + def __init__(self, get_response): + self.get_response = get_response - def __init__(self, *args, **kwargs): - super(IPRestrictMiddleware, self).__init__(*args, **kwargs) self.restrictor = IPRestrictor() - self.trusted_proxies = tuple(get_setting('IPRESTRICT_TRUSTED_PROXIES', 'TRUSTED_PROXIES', [])) + self.trusted_proxies = tuple( + get_setting("IPRESTRICT_TRUSTED_PROXIES", "TRUSTED_PROXIES", []) + ) self.reload_rules = get_reload_rules_setting() - self.ignore_proxy_header = bool(get_setting('IPRESTRICT_IGNORE_PROXY_HEADER', 'IGNORE_PROXY_HEADER', False)) - self.trust_all_proxies = bool(get_setting('IPRESTRICT_TRUST_ALL_PROXIES', 'TRUST_ALL_PROXIES', False)) - - def process_request(self, request): + self.ignore_proxy_header = bool( + get_setting("IPRESTRICT_IGNORE_PROXY_HEADER", "IGNORE_PROXY_HEADER", False) + ) + self.trust_all_proxies = bool( + get_setting("IPRESTRICT_TRUST_ALL_PROXIES", "TRUST_ALL_PROXIES", False) + ) + + def __call__(self, request): if self.reload_rules: self.reload_rules_if_needed() @@ -40,31 +34,44 @@ def process_request(self, request): client_ip = self.extract_client_ip(request) if self.restrictor.is_restricted(url, client_ip): - logger.warn("Denying access of %s to %s" % (url, client_ip)) + logger.warning("Denying access of %s to %s" % (url, client_ip)) raise exceptions.PermissionDenied + response = self.get_response(request) + return response + def extract_client_ip(self, request): - client_ip = request.META['REMOTE_ADDR'] - if not self.ignore_proxy_header: - forwarded_for = self.get_forwarded_for(request) - if forwarded_for: - closest_proxy = client_ip - client_ip = forwarded_for.pop(0) - if self.trust_all_proxies: - return client_ip - proxies = [closest_proxy] + forwarded_for - for proxy in proxies: - if proxy not in self.trusted_proxies: - logger.warn("Client IP %s forwarded by untrusted proxy %s" % (client_ip, proxy)) - raise exceptions.PermissionDenied + client_ip = request.META["REMOTE_ADDR"] + if self.ignore_proxy_header: + return client_ip + + forwarded_for = self.get_forwarded_for(request) + if forwarded_for: + client_ip = self.extract_client_ip_proxied_request(client_ip, forwarded_for) + + return client_ip + + def extract_client_ip_proxied_request(self, client_ip, forwarded_for): + closest_proxy = client_ip + client_ip = forwarded_for.pop(0) + if self.trust_all_proxies: + return client_ip + + proxies = [closest_proxy] + forwarded_for + for proxy in proxies: + if proxy not in self.trusted_proxies: + logger.warning( + "Client IP %s forwarded by untrusted proxy %s" % (client_ip, proxy) + ) + raise exceptions.PermissionDenied + return client_ip def get_forwarded_for(self, request): - hdr = request.META.get('HTTP_X_FORWARDED_FOR') - if hdr is not None: - return [ip.strip() for ip in hdr.split(',')] - else: + hdr = request.META.get("HTTP_X_FORWARDED_FOR") + if hdr is None: return [] + return [ip.strip() for ip in hdr.split(",")] def reload_rules_if_needed(self): last_reload_request = ReloadRulesRequest.last_request() @@ -82,14 +89,16 @@ def get_setting(new_name, old_name, default=None): def get_reload_rules_setting(): - if hasattr(settings, 'DONT_RELOAD_RULES'): - warn_about_changed_setting('DONT_RELOAD_RULES', 'IPRESTRICT_RELOAD_RULES') - return not bool(getattr(settings, 'DONT_RELOAD_RULES')) - return bool(getattr(settings, 'IPRESTRICT_RELOAD_RULES', True)) + if hasattr(settings, "DONT_RELOAD_RULES"): + warn_about_changed_setting("DONT_RELOAD_RULES", "IPRESTRICT_RELOAD_RULES") + return not bool(getattr(settings, "DONT_RELOAD_RULES")) + return bool(getattr(settings, "IPRESTRICT_RELOAD_RULES", True)) def warn_about_changed_setting(old_name, new_name): # DeprecationWarnings are ignored by default, so lets make sure that # the warnings are shown by using the default UserWarning instead - warnings.warn("The setting name '%s' has been deprecated and it will be removed in a future version. " - "Please use '%s' instead." % (old_name, new_name)) + warnings.warn( + "The setting name '%s' has been deprecated and it will be removed in a future version. " + "Please use '%s' instead." % (old_name, new_name) + ) diff --git a/iprestrict/migrations/0001_initial.py b/iprestrict/migrations/0001_initial.py index 1bb951f..c5a015f 100644 --- a/iprestrict/migrations/0001_initial.py +++ b/iprestrict/migrations/0001_initial.py @@ -6,52 +6,103 @@ class Migration(migrations.Migration): - dependencies = [ - ] + dependencies = [] operations = [ migrations.CreateModel( - name='IPGroup', + name="IPGroup", fields=[ - ('id', models.AutoField(verbose_name='ID', serialize=False, auto_created=True, primary_key=True)), - ('name', models.CharField(max_length=100)), - ('description', models.TextField(null=True, blank=True)), + ( + "id", + models.AutoField( + verbose_name="ID", + serialize=False, + auto_created=True, + primary_key=True, + ), + ), + ("name", models.CharField(max_length=100)), + ("description", models.TextField(null=True, blank=True)), ], options={ - 'verbose_name': 'IP Group', + "verbose_name": "IP Group", }, ), migrations.CreateModel( - name='IPRange', + name="IPRange", fields=[ - ('id', models.AutoField(verbose_name='ID', serialize=False, auto_created=True, primary_key=True)), - ('first_ip', models.GenericIPAddressField()), - ('cidr_prefix_length', models.PositiveSmallIntegerField(null=True, blank=True)), - ('last_ip', models.GenericIPAddressField(null=True, blank=True)), - ('ip_group', models.ForeignKey(to='iprestrict.IPGroup', on_delete=models.CASCADE)), + ( + "id", + models.AutoField( + verbose_name="ID", + serialize=False, + auto_created=True, + primary_key=True, + ), + ), + ("first_ip", models.GenericIPAddressField()), + ( + "cidr_prefix_length", + models.PositiveSmallIntegerField(null=True, blank=True), + ), + ("last_ip", models.GenericIPAddressField(null=True, blank=True)), + ( + "ip_group", + models.ForeignKey( + to="iprestrict.IPGroup", on_delete=models.CASCADE + ), + ), ], options={ - 'verbose_name': 'IP Range', + "verbose_name": "IP Range", }, ), migrations.CreateModel( - name='ReloadRulesRequest', + name="ReloadRulesRequest", fields=[ - ('id', models.AutoField(verbose_name='ID', serialize=False, auto_created=True, primary_key=True)), - ('at', models.DateTimeField(auto_now_add=True)), + ( + "id", + models.AutoField( + verbose_name="ID", + serialize=False, + auto_created=True, + primary_key=True, + ), + ), + ("at", models.DateTimeField(auto_now_add=True)), ], ), migrations.CreateModel( - name='Rule', + name="Rule", fields=[ - ('id', models.AutoField(verbose_name='ID', serialize=False, auto_created=True, primary_key=True)), - ('url_pattern', models.CharField(max_length=500)), - ('action', models.CharField(default='D', max_length=1, choices=[('A', 'ALLOW'), ('D', 'DENY')])), - ('rank', models.IntegerField(blank=True)), - ('ip_group', models.ForeignKey(default=1, to='iprestrict.IPGroup', on_delete=models.CASCADE)), + ( + "id", + models.AutoField( + verbose_name="ID", + serialize=False, + auto_created=True, + primary_key=True, + ), + ), + ("url_pattern", models.CharField(max_length=500)), + ( + "action", + models.CharField( + default="D", + max_length=1, + choices=[("A", "ALLOW"), ("D", "DENY")], + ), + ), + ("rank", models.IntegerField(blank=True)), + ( + "ip_group", + models.ForeignKey( + default=1, to="iprestrict.IPGroup", on_delete=models.CASCADE + ), + ), ], options={ - 'ordering': ['rank', 'id'], + "ordering": ["rank", "id"], }, ), ] diff --git a/iprestrict/migrations/0002_initial_data.py b/iprestrict/migrations/0002_initial_data.py index b194d8b..3fce19d 100644 --- a/iprestrict/migrations/0002_initial_data.py +++ b/iprestrict/migrations/0002_initial_data.py @@ -5,53 +5,41 @@ def create_data(apps, schema_editor): - IPGroup = apps.get_model('iprestrict', 'IPGroup') + IPGroup = apps.get_model("iprestrict", "IPGroup") IPRange = apps.get_model("iprestrict", "IPRange") Rule = apps.get_model("iprestrict", "Rule") # Default IP Groups: ALL and localhost all_group = IPGroup.objects.create( - name='ALL', - description='Matches all IP Addresses') + name="ALL", description="Matches all IP Addresses" + ) localhost_group = IPGroup.objects.create( - name='localhost', - description='IP address of localhost') + name="localhost", description="IP address of localhost" + ) # IP ranges defining ALL and localhost for ipv4 and ipv6 IPRange.objects.create( - ip_group=all_group, - first_ip='0.0.0.0', - last_ip='255.255.255.255') + ip_group=all_group, first_ip="0.0.0.0", last_ip="255.255.255.255" + ) IPRange.objects.create( ip_group=all_group, - first_ip='0:0:0:0:0:0:0:0', - last_ip='ffff:ffff:ffff:ffff:ffff:ffff:ffff:ffff') - IPRange.objects.create( - ip_group=localhost_group, - first_ip='127.0.0.1', - last_ip=None) - IPRange.objects.create( - ip_group=localhost_group, - first_ip='::1', - last_ip=None) + first_ip="0:0:0:0:0:0:0:0", + last_ip="ffff:ffff:ffff:ffff:ffff:ffff:ffff:ffff", + ) + IPRange.objects.create(ip_group=localhost_group, first_ip="127.0.0.1", last_ip=None) + IPRange.objects.create(ip_group=localhost_group, first_ip="::1", last_ip=None) # Default rules: Allow all for localhost and Deny everything else Rule.objects.create( - ip_group=localhost_group, - action='A', - url_pattern='ALL', - rank=65535) - Rule.objects.create( - ip_group=all_group, - action='D', - url_pattern='ALL', - rank=65536) + ip_group=localhost_group, action="A", url_pattern="ALL", rank=65535 + ) + Rule.objects.create(ip_group=all_group, action="D", url_pattern="ALL", rank=65536) class Migration(migrations.Migration): dependencies = [ - ('iprestrict', '0001_initial'), + ("iprestrict", "0001_initial"), ] operations = [ diff --git a/iprestrict/migrations/0003_add_ipgroup_types.py b/iprestrict/migrations/0003_add_ipgroup_types.py index b985d45..58cdc4a 100644 --- a/iprestrict/migrations/0003_add_ipgroup_types.py +++ b/iprestrict/migrations/0003_add_ipgroup_types.py @@ -7,32 +7,34 @@ class Migration(migrations.Migration): dependencies = [ - ('iprestrict', '0002_initial_data'), + ("iprestrict", "0002_initial_data"), ] operations = [ migrations.CreateModel( - name='LocationBasedIPGroup', - fields=[ - ], + name="LocationBasedIPGroup", + fields=[], options={ - 'verbose_name': 'Location Based IP Group', - 'proxy': True, + "verbose_name": "Location Based IP Group", + "proxy": True, }, - bases=('iprestrict.ipgroup',), + bases=("iprestrict.ipgroup",), ), migrations.CreateModel( - name='RangeBasedIPGroup', - fields=[ - ], + name="RangeBasedIPGroup", + fields=[], options={ - 'proxy': True, + "proxy": True, }, - bases=('iprestrict.ipgroup',), + bases=("iprestrict.ipgroup",), ), migrations.AddField( - model_name='ipgroup', - name='type', - field=models.CharField(default='range', max_length=10, choices=[('location', 'location based'), ('range', 'range based')]), + model_name="ipgroup", + name="type", + field=models.CharField( + default="range", + max_length=10, + choices=[("location", "location based"), ("range", "range based")], + ), ), ] diff --git a/iprestrict/migrations/0004_add_iplocation.py b/iprestrict/migrations/0004_add_iplocation.py index fa24906..a23df0b 100644 --- a/iprestrict/migrations/0004_add_iplocation.py +++ b/iprestrict/migrations/0004_add_iplocation.py @@ -7,36 +7,54 @@ class Migration(migrations.Migration): dependencies = [ - ('iprestrict', '0003_add_ipgroup_types'), + ("iprestrict", "0003_add_ipgroup_types"), ] operations = [ migrations.CreateModel( - name='IPLocation', + name="IPLocation", fields=[ - ('id', models.AutoField(verbose_name='ID', serialize=False, auto_created=True, primary_key=True)), - ('country_codes', models.CharField(help_text='Comma-separated list of 2 character country codes', max_length=2000)), + ( + "id", + models.AutoField( + verbose_name="ID", + serialize=False, + auto_created=True, + primary_key=True, + ), + ), + ( + "country_codes", + models.CharField( + help_text="Comma-separated list of 2 character country codes", + max_length=2000, + ), + ), ], options={ - 'verbose_name': 'IP Location', + "verbose_name": "IP Location", }, ), migrations.AlterModelOptions( - name='ipgroup', + name="ipgroup", options={}, ), migrations.AlterModelOptions( - name='rangebasedipgroup', - options={'verbose_name': 'IP Group'}, + name="rangebasedipgroup", + options={"verbose_name": "IP Group"}, ), migrations.AlterField( - model_name='ipgroup', - name='type', - field=models.CharField(default='range', max_length=10, choices=[('location', 'Location based'), ('range', 'Range based')]), + model_name="ipgroup", + name="type", + field=models.CharField( + default="range", + max_length=10, + choices=[("location", "Location based"), ("range", "Range based")], + ), ), migrations.AddField( - model_name='iplocation', - name='ip_group', - field=models.ForeignKey(to='iprestrict.IPGroup', on_delete=models.CASCADE), + model_name="iplocation", + name="ip_group", + field=models.ForeignKey(to="iprestrict.IPGroup", on_delete=models.CASCADE), ), ] diff --git a/iprestrict/migrations/0005_add_reverse_ipgroup.py b/iprestrict/migrations/0005_add_reverse_ipgroup.py index 5e3bee9..bc01f49 100644 --- a/iprestrict/migrations/0005_add_reverse_ipgroup.py +++ b/iprestrict/migrations/0005_add_reverse_ipgroup.py @@ -7,17 +7,17 @@ class Migration(migrations.Migration): dependencies = [ - ('iprestrict', '0004_add_iplocation'), + ("iprestrict", "0004_add_iplocation"), ] operations = [ migrations.AlterModelOptions( - name='ipgroup', - options={'verbose_name': 'IP Group'}, + name="ipgroup", + options={"verbose_name": "IP Group"}, ), migrations.AddField( - model_name='rule', - name='reverse_ip_group', + model_name="rule", + name="reverse_ip_group", field=models.BooleanField(default=False), ), ] diff --git a/iprestrict/migrations/0006_auto_20161013_1327.py b/iprestrict/migrations/0006_auto_20161013_1327.py index 8b37fe1..ae759a1 100644 --- a/iprestrict/migrations/0006_auto_20161013_1327.py +++ b/iprestrict/migrations/0006_auto_20161013_1327.py @@ -8,23 +8,32 @@ class Migration(migrations.Migration): dependencies = [ - ('iprestrict', '0005_add_reverse_ipgroup'), + ("iprestrict", "0005_add_reverse_ipgroup"), ] operations = [ migrations.AlterField( - model_name='ipgroup', - name='type', - field=models.CharField(choices=[('location', 'Location based'), ('range', 'Range based')], default='range', max_length=10), + model_name="ipgroup", + name="type", + field=models.CharField( + choices=[("location", "Location based"), ("range", "Range based")], + default="range", + max_length=10, + ), ), migrations.AlterField( - model_name='iplocation', - name='country_codes', - field=models.CharField(help_text='Comma-separated list of 2 character country codes', max_length=2000), + model_name="iplocation", + name="country_codes", + field=models.CharField( + help_text="Comma-separated list of 2 character country codes", + max_length=2000, + ), ), migrations.AlterField( - model_name='rule', - name='action', - field=models.CharField(choices=[('A', 'ALLOW'), ('D', 'DENY')], default='D', max_length=1), + model_name="rule", + name="action", + field=models.CharField( + choices=[("A", "ALLOW"), ("D", "DENY")], default="D", max_length=1 + ), ), ] diff --git a/iprestrict/migrations/0007_iprange_description.py b/iprestrict/migrations/0007_iprange_description.py index 72d93dd..80e8378 100644 --- a/iprestrict/migrations/0007_iprange_description.py +++ b/iprestrict/migrations/0007_iprange_description.py @@ -8,13 +8,13 @@ class Migration(migrations.Migration): dependencies = [ - ('iprestrict', '0006_auto_20161013_1327'), + ("iprestrict", "0006_auto_20161013_1327"), ] operations = [ migrations.AddField( - model_name='iprange', - name='description', + model_name="iprange", + name="description", field=models.CharField(blank=True, max_length=500), ), ] diff --git a/iprestrict/migrations/__init__.py b/iprestrict/migrations/__init__.py index 817e254..2000409 100644 --- a/iprestrict/migrations/__init__.py +++ b/iprestrict/migrations/__init__.py @@ -1,5 +1,3 @@ # -*- coding: utf-8 -*- -from __future__ import ( - absolute_import, division, print_function, unicode_literals -) +from __future__ import absolute_import, division, print_function, unicode_literals diff --git a/iprestrict/models.py b/iprestrict/models.py index 05ff463..a58664d 100644 --- a/iprestrict/models.py +++ b/iprestrict/models.py @@ -14,11 +14,10 @@ from django.core.urlresolvers import reverse from . import ip_utils as ipu -from .geoip import get_geoip, NO_COUNTRY +from .geoip import NO_COUNTRY, get_geoip - -TYPE_LOCATION = 'location' -TYPE_RANGE = 'range' +TYPE_LOCATION = "location" +TYPE_RANGE = "range" geoip = get_geoip() @@ -33,8 +32,7 @@ def get_queryset(self): class IPGroup(models.Model): - TYPE_CHOICES = ((TYPE_LOCATION, 'Location based'), - (TYPE_RANGE, 'Range based')) + TYPE_CHOICES = ((TYPE_LOCATION, "Location based"), (TYPE_RANGE, "Range based")) TYPE = None name = models.CharField(max_length=100) @@ -42,7 +40,7 @@ class IPGroup(models.Model): type = models.CharField(max_length=10, default=TYPE_RANGE, choices=TYPE_CHOICES) class Meta: - verbose_name = 'IP Group' + verbose_name = "IP Group" objects = IPGroupManager() @@ -81,7 +79,7 @@ class RangeBasedIPGroup(IPGroup): class Meta: proxy = True - verbose_name = 'IP Group' + verbose_name = "IP Group" def load_ranges(self): self._ranges = {ipu.IPv4: [], ipu.IPv6: []} @@ -103,7 +101,7 @@ def matches(self, ip): return False def details_str(self): - return ', '.join([str(r) for r in self.ranges()]) + return ", ".join([str(r) for r in self.ranges()]) class LocationBasedIPGroup(IPGroup): @@ -111,12 +109,14 @@ class LocationBasedIPGroup(IPGroup): class Meta: proxy = True - verbose_name = 'Location Based IP Group' + verbose_name = "Location Based IP Group" def load_locations(self): - countries = ", ".join(self.iplocation_set.values_list('country_codes', flat=True)).split(', ') + countries = ", ".join( + self.iplocation_set.values_list("country_codes", flat=True) + ).split(", ") countries.sort() - self._countries = ', '.join(countries) + self._countries = ", ".join(countries) load = load_locations @@ -141,8 +141,7 @@ class Meta: @property def start(self): if self.cidr_prefix_length is not None: - start, end = ipu.cidr_to_range(self.first_ip, - self.cidr_prefix_length) + start, end = ipu.cidr_to_range(self.first_ip, self.cidr_prefix_length) return start else: return ipu.to_number(self.first_ip) @@ -152,15 +151,14 @@ def end(self): if self.last_ip is not None: return ipu.to_number(self.last_ip) if self.cidr_prefix_length is not None: - start, end = ipu.cidr_to_range(self.first_ip, - self.cidr_prefix_length) + start, end = ipu.cidr_to_range(self.first_ip, self.cidr_prefix_length) return end return self.start @property def ip_type(self): if not self.first_ip: - return '' + return "" return ipu.get_version(self.first_ip) def __contains__(self, ip): @@ -170,9 +168,9 @@ def __contains__(self, ip): def __str__(self): result = str(self.first_ip) if self.cidr_prefix_length is not None: - result += '/' + str(self.cidr_prefix_length) + result += "/" + str(self.cidr_prefix_length) elif self.last_ip is not None: - result += '-' + str(self.last_ip) + result += "-" + str(self.last_ip) return result __unicode__ = __str__ @@ -183,10 +181,12 @@ class Meta: verbose_name = "IP Location" ip_group = models.ForeignKey(IPGroup, on_delete=models.CASCADE) - country_codes = models.CharField(max_length=2000, help_text='Comma-separated list of 2 character country codes') + country_codes = models.CharField( + max_length=2000, help_text="Comma-separated list of 2 character country codes" + ) def __contains__(self, country_code): - return country_code in re.split(r'[^A-Z]+', self.country_codes) + return country_code in re.split(r"[^A-Z]+", self.country_codes) def __str__(self): return self.country_codes @@ -196,31 +196,32 @@ def __str__(self): class Rule(models.Model): class Meta: - ordering = ['rank', 'id'] + ordering = ["rank", "id"] - ACTION_CHOICES = ( - ('A', 'ALLOW'), - ('D', 'DENY') - ) + ACTION_CHOICES = (("A", "ALLOW"), ("D", "DENY")) url_pattern = models.CharField(max_length=500) ip_group = models.ForeignKey(IPGroup, default=1, on_delete=models.CASCADE) reverse_ip_group = models.BooleanField(default=False) - action = models.CharField(max_length=1, choices=ACTION_CHOICES, default='D') + action = models.CharField(max_length=1, choices=ACTION_CHOICES, default="D") rank = models.IntegerField(blank=True) def __init__(self, *args, **kwargs): - super(Rule, self).__init__(*args, **kwargs) - self.ip_group = typed_ip_group(self.ip_group) + super().__init__(*args, **kwargs) + # TODO review this code, it fails without the try except when using Add rule in admin + try: + self.ip_group = typed_ip_group(self.ip_group) + except IPGroup.DoesNotExist: + pass @property def regex(self): - if not hasattr(self, '_regex'): + if not hasattr(self, "_regex"): self._regex = re.compile(self.url_pattern) return self._regex def matches_url(self, url): - if self.url_pattern == 'ALL': + if self.url_pattern == "ALL": return True else: return self.regex.match(url) is not None @@ -232,15 +233,16 @@ def matches_ip(self, ip): return match def is_restricted(self): - return self.action != 'A' + return self.action != "A" def is_allowed(self): - return self.action == 'A' + return self.action == "A" + is_allowed.boolean = True - is_allowed.short_description = 'Is allowed?' + is_allowed.short_description = "Is allowed?" def action_str(self): - return 'Allowed' if self.is_allowed() else 'Denied' + return "Allowed" if self.is_allowed() else "Denied" def swap_with_rule(self, other): other.rank, self.rank = self.rank, other.rank @@ -248,20 +250,22 @@ def swap_with_rule(self, other): self.save() def move_up(self): - rules_above = Rule.objects.filter(rank__lt=self.rank).order_by('-rank') + rules_above = Rule.objects.filter(rank__lt=self.rank).order_by("-rank") if len(rules_above) == 0: return self.swap_with_rule(rules_above[0]) def move_up_url(self): - url = reverse('iprestrict:move_rule_up', args=[self.pk]) + url = reverse("iprestrict:move_rule_up", args=[self.pk]) return mark_safe('Move Up' % url) - move_up_url.short_description = 'Move Up' + + move_up_url.short_description = "Move Up" def move_down_url(self): - url = reverse('iprestrict:move_rule_down', args=[self.pk]) + url = reverse("iprestrict:move_rule_down", args=[self.pk]) return mark_safe('Move Down' % url) - move_down_url.short_description = 'Move Down' + + move_down_url.short_description = "Move Down" def move_down(self): rules_below = Rule.objects.filter(rank__gt=self.rank) @@ -271,8 +275,8 @@ def move_down(self): def save(self, *args, **kwargs): if self.rank is None: - max_aggr = Rule.objects.filter(rank__lt=65000).aggregate(models.Max('rank')) - max_rank = max_aggr.get('rank__max') + max_aggr = Rule.objects.filter(rank__lt=65000).aggregate(models.Max("rank")) + max_rank = max_aggr.get("rank__max") if max_rank is None: max_rank = 0 self.rank = max_rank + 1 diff --git a/iprestrict/restrictor.py b/iprestrict/restrictor.py index 13675a2..c280c60 100644 --- a/iprestrict/restrictor.py +++ b/iprestrict/restrictor.py @@ -20,6 +20,7 @@ def is_restricted(self, url, ip): def load_rules(self): # We are caching the rules, to avoid DB lookup on each request from .models import Rule + self.rules = list(Rule.objects.all()) self.last_reload = timezone.now() diff --git a/iprestrict/urls.py b/iprestrict/urls.py index 5704ebf..c32d76f 100644 --- a/iprestrict/urls.py +++ b/iprestrict/urls.py @@ -1,15 +1,12 @@ -# -*- coding: utf-8 -*- -from __future__ import unicode_literals +from django.urls import path -from django.conf.urls import url -from .views import test_rules_page, test_match, reload_rules -from .views import move_rule_up, move_rule_down +from . import views app_name = "iprestrict" urlpatterns = [ - url(r'^$', test_rules_page), - url(r'^move_rule_up/(?P\d+)[/]?$', move_rule_up, name='move_rule_up'), - url(r'^move_rule_down/(?P\d+)[/]?$', move_rule_down, name='move_rule_down'), - url(r'^reload_rules[/]?$', reload_rules, name='reload_rules'), - url(r'^test_match[/]?$', test_match, name='test_match'), + path(r"", views.test_rules_page), + path(r"move_rule_up//", views.move_rule_up, name="move_rule_up"), + path(r"move_rule_down//", views.move_rule_down, name="move_rule_down"), + path(r"reload_rules/", views.reload_rules, name="reload_rules"), + path(r"test_match/", views.test_match, name="test_match"), ] diff --git a/iprestrict/views.py b/iprestrict/views.py index 5ce4338..f5ce3d3 100644 --- a/iprestrict/views.py +++ b/iprestrict/views.py @@ -22,58 +22,60 @@ def move_rule_up(request, rule_id): rule = models.Rule.objects.get(pk=rule_id) rule.move_up() - return HttpResponseRedirect(reverse('admin:iprestrict_rule_changelist')) + return HttpResponseRedirect(reverse("admin:iprestrict_rule_changelist")) @superuser_required def move_rule_down(request, rule_id): rule = models.Rule.objects.get(pk=rule_id) rule.move_down() - return HttpResponseRedirect(reverse('admin:iprestrict_rule_changelist')) + return HttpResponseRedirect(reverse("admin:iprestrict_rule_changelist")) @superuser_required def reload_rules(request): models.ReloadRulesRequest.request_reload() - return HttpResponse('ok') + return HttpResponse("ok") @superuser_required def test_rules_page(request): - return render(request, 'iprestrict/test_rules.html') + return render(request, "iprestrict/test_rules.html") @superuser_required def test_match(request): request_dict = request.GET - if request.method == 'POST': + if request.method == "POST": request_dict = request.POST - url = request_dict['url'] - ip = request_dict['ip'] + url = request_dict["url"] + ip = request_dict["ip"] try: validate_ipv46_address(ip) except ValidationError: - return HttpResponse(_test_match_result('Error', msg='Invalid IP address')) + return HttpResponse(_test_match_result("Error", msg="Invalid IP address")) matching_rule_id, action = find_matching_rule(url, ip) rules = list_rules(matching_rule_id, url, ip) if matching_rule_id is None: - action = 'Allowed' - msg = 'No rules matched.' + action = "Allowed" + msg = "No rules matched." else: - msg = 'URL matched Rule highlighted below.' + msg = "URL matched Rule highlighted below." return HttpResponse(_test_match_result(action, msg, rules)) def _test_match_result(action, msg=None, rules=None): - return json.dumps({ - 'action': action, - 'msg': msg or '', - 'rules': rules or [], - }) + return json.dumps( + { + "action": action, + "msg": msg or "", + "rules": rules or [], + } + ) def find_matching_rule(url, ip): @@ -89,18 +91,18 @@ def list_rules(matching_rule_id, url, ip): def map_rule(r, matching_rule_id, url, ip): rule = { - 'url_pattern': { - 'value': r.url_pattern, - 'matchStatus': 'match' if r.matches_url(url) else 'noMatch' + "url_pattern": { + "value": r.url_pattern, + "matchStatus": "match" if r.matches_url(url) else "noMatch", }, - 'ip_group': { - 'name': r.ip_group.name, - 'reverse_ip_group': 'NOT' if r.reverse_ip_group else '', - 'ranges': r.ip_group.details_str(), - 'matchStatus': 'match' if r.matches_ip(ip) else 'noMatch' + "ip_group": { + "name": r.ip_group.name, + "reverse_ip_group": "NOT" if r.reverse_ip_group else "", + "ranges": r.ip_group.details_str(), + "matchStatus": "match" if r.matches_ip(ip) else "noMatch", }, - 'action': r.action_str(), + "action": r.action_str(), } if r.pk == matching_rule_id: - rule['matched'] = True + rule["matched"] = True return rule diff --git a/poetry.lock b/poetry.lock new file mode 100644 index 0000000..4e23afd --- /dev/null +++ b/poetry.lock @@ -0,0 +1,1207 @@ +[[package]] +category = "main" +description = "Async http client/server framework (asyncio)" +name = "aiohttp" +optional = true +python-versions = ">=3.5.3" +version = "3.6.2" + +[package.dependencies] +async-timeout = ">=3.0,<4.0" +attrs = ">=17.3.0" +chardet = ">=2.0,<4.0" +multidict = ">=4.5,<5.0" +yarl = ">=1.0,<2.0" + +[package.dependencies.idna-ssl] +python = "<3.7" +version = ">=1.0" + +[package.dependencies.typing-extensions] +python = "<3.7" +version = ">=3.6.5" + +[package.extras] +speedups = ["aiodns", "brotlipy", "cchardet"] + +[[package]] +category = "dev" +description = "A configurable sidebar-enabled Sphinx theme" +name = "alabaster" +optional = false +python-versions = "*" +version = "0.7.12" + +[[package]] +category = "dev" +description = "A small Python module for determining appropriate platform-specific dirs, e.g. a \"user data dir\"." +name = "appdirs" +optional = false +python-versions = "*" +version = "1.4.4" + +[[package]] +category = "dev" +description = "ASGI specs, helper code, and adapters" +name = "asgiref" +optional = false +python-versions = ">=3.5" +version = "3.2.10" + +[package.extras] +tests = ["pytest", "pytest-asyncio"] + +[[package]] +category = "main" +description = "Timeout context manager for asyncio programs" +name = "async-timeout" +optional = true +python-versions = ">=3.5.3" +version = "3.0.1" + +[[package]] +category = "dev" +description = "Atomic file writes." +marker = "sys_platform == \"win32\"" +name = "atomicwrites" +optional = false +python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*" +version = "1.4.0" + +[[package]] +category = "main" +description = "Classes Without Boilerplate" +name = "attrs" +optional = false +python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*" +version = "20.1.0" + +[package.extras] +dev = ["coverage (>=5.0.2)", "hypothesis", "pympler", "pytest (>=4.3.0)", "six", "zope.interface", "sphinx", "sphinx-rtd-theme", "pre-commit"] +docs = ["sphinx", "sphinx-rtd-theme", "zope.interface"] +tests = ["coverage (>=5.0.2)", "hypothesis", "pympler", "pytest (>=4.3.0)", "six", "zope.interface"] + +[[package]] +category = "dev" +description = "Internationalization utilities" +name = "babel" +optional = false +python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*" +version = "2.8.0" + +[package.dependencies] +pytz = ">=2015.7" + +[[package]] +category = "dev" +description = "The uncompromising code formatter." +name = "black" +optional = false +python-versions = ">=3.6" +version = "20.8b1" + +[package.dependencies] +appdirs = "*" +click = ">=7.1.2" +mypy-extensions = ">=0.4.3" +pathspec = ">=0.6,<1" +regex = ">=2020.1.8" +toml = ">=0.10.1" +typed-ast = ">=1.4.0" +typing-extensions = ">=3.7.4" + +[package.dependencies.dataclasses] +python = "<3.7" +version = ">=0.6" + +[package.extras] +colorama = ["colorama (>=0.4.3)"] +d = ["aiohttp (>=3.3.2)", "aiohttp-cors"] + +[[package]] +category = "main" +description = "Python package for providing Mozilla's CA Bundle." +name = "certifi" +optional = false +python-versions = "*" +version = "2020.6.20" + +[[package]] +category = "main" +description = "Universal encoding detector for Python 2 and 3" +name = "chardet" +optional = false +python-versions = "*" +version = "3.0.4" + +[[package]] +category = "dev" +description = "Composable command line interface toolkit" +name = "click" +optional = false +python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*" +version = "7.1.2" + +[[package]] +category = "dev" +description = "Cross-platform colored terminal text." +name = "colorama" +optional = false +python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*" +version = "0.4.3" + +[[package]] +category = "dev" +description = "A backport of the dataclasses module for Python 3.6" +marker = "python_version < \"3.7\"" +name = "dataclasses" +optional = false +python-versions = "*" +version = "0.6" + +[[package]] +category = "dev" +description = "Distribution utilities" +name = "distlib" +optional = false +python-versions = "*" +version = "0.3.1" + +[[package]] +category = "dev" +description = "A high-level Python Web framework that encourages rapid development and clean, pragmatic design." +name = "django" +optional = false +python-versions = ">=3.6" +version = "3.1" + +[package.dependencies] +asgiref = ">=3.2.10,<3.3.0" +pytz = "*" +sqlparse = ">=0.2.2" + +[package.extras] +argon2 = ["argon2-cffi (>=16.1.0)"] +bcrypt = ["bcrypt"] + +[[package]] +category = "dev" +description = "Extensions for Django" +name = "django-extensions" +optional = false +python-versions = ">=3.5" +version = "3.0.5" + +[[package]] +category = "dev" +description = "Docutils -- Python Documentation Utilities" +name = "docutils" +optional = false +python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*" +version = "0.16" + +[[package]] +category = "dev" +description = "A platform independent file lock." +name = "filelock" +optional = false +python-versions = "*" +version = "3.0.12" + +[[package]] +category = "dev" +description = "the modular source code checker: pep8 pyflakes and co" +name = "flake8" +optional = false +python-versions = "!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,>=2.7" +version = "3.8.3" + +[package.dependencies] +mccabe = ">=0.6.0,<0.7.0" +pycodestyle = ">=2.6.0a1,<2.7.0" +pyflakes = ">=2.2.0,<2.3.0" + +[package.dependencies.importlib-metadata] +python = "<3.8" +version = "*" + +[[package]] +category = "main" +description = "MaxMind GeoIP2 API" +name = "geoip2" +optional = true +python-versions = ">=3.6" +version = "4.0.2" + +[package.dependencies] +aiohttp = ">=3.6.2,<4.0.0" +maxminddb = ">=2.0.0,<3.0.0" +requests = ">=2.24.0,<3.0.0" +urllib3 = ">=1.25.2,<2.0.0" + +[[package]] +category = "main" +description = "Internationalized Domain Names in Applications (IDNA)" +name = "idna" +optional = false +python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*" +version = "2.10" + +[[package]] +category = "main" +description = "Patch ssl.match_hostname for Unicode(idna) domains support" +marker = "python_version < \"3.7\"" +name = "idna-ssl" +optional = true +python-versions = "*" +version = "1.1.0" + +[package.dependencies] +idna = ">=2.0" + +[[package]] +category = "dev" +description = "Getting image size from png/jpeg/jpeg2000/gif file" +name = "imagesize" +optional = false +python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*" +version = "1.2.0" + +[[package]] +category = "dev" +description = "Read metadata from Python packages" +marker = "python_version < \"3.8\"" +name = "importlib-metadata" +optional = false +python-versions = "!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,!=3.4.*,>=2.7" +version = "1.7.0" + +[package.dependencies] +zipp = ">=0.5" + +[package.extras] +docs = ["sphinx", "rst.linker"] +testing = ["packaging", "pep517", "importlib-resources (>=1.3)"] + +[[package]] +category = "dev" +description = "Read resources from Python packages" +marker = "python_version < \"3.7\"" +name = "importlib-resources" +optional = false +python-versions = "!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,!=3.4.*,>=2.7" +version = "3.0.0" + +[package.dependencies] +[package.dependencies.zipp] +python = "<3.8" +version = ">=0.4" + +[package.extras] +docs = ["sphinx", "rst.linker", "jaraco.packaging"] + +[[package]] +category = "dev" +description = "iniconfig: brain-dead simple config-ini parsing" +name = "iniconfig" +optional = false +python-versions = "*" +version = "1.0.1" + +[[package]] +category = "dev" +description = "A Python utility / library to sort Python imports." +name = "isort" +optional = false +python-versions = ">=3.6,<4.0" +version = "5.4.2" + +[package.dependencies] +[package.dependencies.colorama] +optional = true +version = ">=0.4.3,<0.5.0" + +[package.extras] +colors = ["colorama (>=0.4.3,<0.5.0)"] +pipfile_deprecated_finder = ["pipreqs", "requirementslib", "tomlkit (>=0.5.3)"] +requirements_deprecated_finder = ["pipreqs", "pip-api"] + +[[package]] +category = "dev" +description = "A very fast and expressive template engine." +name = "jinja2" +optional = false +python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*" +version = "2.11.2" + +[package.dependencies] +MarkupSafe = ">=0.23" + +[package.extras] +i18n = ["Babel (>=0.8)"] + +[[package]] +category = "dev" +description = "Safely add untrusted strings to HTML/XML markup." +name = "markupsafe" +optional = false +python-versions = ">=2.7,!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*" +version = "1.1.1" + +[[package]] +category = "main" +description = "Reader for the MaxMind DB format" +name = "maxminddb" +optional = true +python-versions = ">=3.6" +version = "2.0.2" + +[[package]] +category = "dev" +description = "McCabe checker, plugin for flake8" +name = "mccabe" +optional = false +python-versions = "*" +version = "0.6.1" + +[[package]] +category = "dev" +description = "More routines for operating on iterables, beyond itertools" +name = "more-itertools" +optional = false +python-versions = ">=3.5" +version = "8.5.0" + +[[package]] +category = "main" +description = "multidict implementation" +name = "multidict" +optional = true +python-versions = ">=3.5" +version = "4.7.6" + +[[package]] +category = "dev" +description = "Experimental type system extensions for programs checked with the mypy typechecker." +name = "mypy-extensions" +optional = false +python-versions = "*" +version = "0.4.3" + +[[package]] +category = "dev" +description = "Core utilities for Python packages" +name = "packaging" +optional = false +python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*" +version = "20.4" + +[package.dependencies] +pyparsing = ">=2.0.2" +six = "*" + +[[package]] +category = "dev" +description = "Utility library for gitignore style pattern matching of file paths." +name = "pathspec" +optional = false +python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*" +version = "0.8.0" + +[[package]] +category = "dev" +description = "Python style guide checker" +name = "pep8" +optional = false +python-versions = "*" +version = "1.7.1" + +[[package]] +category = "dev" +description = "plugin and hook calling mechanisms for python" +name = "pluggy" +optional = false +python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*" +version = "0.13.1" + +[package.dependencies] +[package.dependencies.importlib-metadata] +python = "<3.8" +version = ">=0.12" + +[package.extras] +dev = ["pre-commit", "tox"] + +[[package]] +category = "dev" +description = "library with cross-python path, ini-parsing, io, code, log facilities" +name = "py" +optional = false +python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*" +version = "1.9.0" + +[[package]] +category = "dev" +description = "Python style guide checker" +name = "pycodestyle" +optional = false +python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*" +version = "2.6.0" + +[[package]] +category = "main" +description = "ISO country, subdivision, language, currency and script definitions and their translations" +name = "pycountry" +optional = true +python-versions = "*" +version = "20.7.3" + +[[package]] +category = "dev" +description = "passive checker of Python programs" +name = "pyflakes" +optional = false +python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*" +version = "2.2.0" + +[[package]] +category = "dev" +description = "Pygments is a syntax highlighting package written in Python." +name = "pygments" +optional = false +python-versions = ">=3.5" +version = "2.6.1" + +[[package]] +category = "dev" +description = "Python parsing module" +name = "pyparsing" +optional = false +python-versions = ">=2.6, !=3.0.*, !=3.1.*, !=3.2.*" +version = "2.4.7" + +[[package]] +category = "dev" +description = "pytest: simple powerful testing with Python" +name = "pytest" +optional = false +python-versions = ">=3.5" +version = "6.0.1" + +[package.dependencies] +atomicwrites = ">=1.0" +attrs = ">=17.4.0" +colorama = "*" +iniconfig = "*" +more-itertools = ">=4.0.0" +packaging = "*" +pluggy = ">=0.12,<1.0" +py = ">=1.8.2" +toml = "*" + +[package.dependencies.importlib-metadata] +python = "<3.8" +version = ">=0.12" + +[package.extras] +checkqa_mypy = ["mypy (0.780)"] +testing = ["argcomplete", "hypothesis (>=3.56)", "mock", "nose", "requests", "xmlschema"] + +[[package]] +category = "dev" +description = "A Django plugin for pytest." +name = "pytest-django" +optional = false +python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*" +version = "3.9.0" + +[package.dependencies] +pytest = ">=3.6" + +[package.extras] +docs = ["sphinx", "sphinx-rtd-theme"] +testing = ["django", "django-configurations (>=2.0)", "six"] + +[[package]] +category = "dev" +description = "World timezone definitions, modern and historical" +name = "pytz" +optional = false +python-versions = "*" +version = "2020.1" + +[[package]] +category = "dev" +description = "Alternative regular expression module, to replace re." +name = "regex" +optional = false +python-versions = "*" +version = "2020.7.14" + +[[package]] +category = "main" +description = "Python HTTP for Humans." +name = "requests" +optional = false +python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*" +version = "2.24.0" + +[package.dependencies] +certifi = ">=2017.4.17" +chardet = ">=3.0.2,<4" +idna = ">=2.5,<3" +urllib3 = ">=1.21.1,<1.25.0 || >1.25.0,<1.25.1 || >1.25.1,<1.26" + +[package.extras] +security = ["pyOpenSSL (>=0.14)", "cryptography (>=1.3.4)"] +socks = ["PySocks (>=1.5.6,<1.5.7 || >1.5.7)", "win-inet-pton"] + +[[package]] +category = "dev" +description = "Python 2 and 3 compatibility utilities" +name = "six" +optional = false +python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*" +version = "1.15.0" + +[[package]] +category = "dev" +description = "This package provides 26 stemmers for 25 languages generated from Snowball algorithms." +name = "snowballstemmer" +optional = false +python-versions = "*" +version = "2.0.0" + +[[package]] +category = "dev" +description = "Python documentation generator" +name = "sphinx" +optional = false +python-versions = ">=3.5" +version = "3.2.1" + +[package.dependencies] +Jinja2 = ">=2.3" +Pygments = ">=2.0" +alabaster = ">=0.7,<0.8" +babel = ">=1.3" +colorama = ">=0.3.5" +docutils = ">=0.12" +imagesize = "*" +packaging = "*" +requests = ">=2.5.0" +setuptools = "*" +snowballstemmer = ">=1.1" +sphinxcontrib-applehelp = "*" +sphinxcontrib-devhelp = "*" +sphinxcontrib-htmlhelp = "*" +sphinxcontrib-jsmath = "*" +sphinxcontrib-qthelp = "*" +sphinxcontrib-serializinghtml = "*" + +[package.extras] +docs = ["sphinxcontrib-websupport"] +lint = ["flake8 (>=3.5.0)", "flake8-import-order", "mypy (>=0.780)", "docutils-stubs"] +test = ["pytest", "pytest-cov", "html5lib", "typed-ast", "cython"] + +[[package]] +category = "dev" +description = "sphinxcontrib-applehelp is a sphinx extension which outputs Apple help books" +name = "sphinxcontrib-applehelp" +optional = false +python-versions = ">=3.5" +version = "1.0.2" + +[package.extras] +lint = ["flake8", "mypy", "docutils-stubs"] +test = ["pytest"] + +[[package]] +category = "dev" +description = "sphinxcontrib-devhelp is a sphinx extension which outputs Devhelp document." +name = "sphinxcontrib-devhelp" +optional = false +python-versions = ">=3.5" +version = "1.0.2" + +[package.extras] +lint = ["flake8", "mypy", "docutils-stubs"] +test = ["pytest"] + +[[package]] +category = "dev" +description = "sphinxcontrib-htmlhelp is a sphinx extension which renders HTML help files" +name = "sphinxcontrib-htmlhelp" +optional = false +python-versions = ">=3.5" +version = "1.0.3" + +[package.extras] +lint = ["flake8", "mypy", "docutils-stubs"] +test = ["pytest", "html5lib"] + +[[package]] +category = "dev" +description = "A sphinx extension which renders display math in HTML via JavaScript" +name = "sphinxcontrib-jsmath" +optional = false +python-versions = ">=3.5" +version = "1.0.1" + +[package.extras] +test = ["pytest", "flake8", "mypy"] + +[[package]] +category = "dev" +description = "sphinxcontrib-qthelp is a sphinx extension which outputs QtHelp document." +name = "sphinxcontrib-qthelp" +optional = false +python-versions = ">=3.5" +version = "1.0.3" + +[package.extras] +lint = ["flake8", "mypy", "docutils-stubs"] +test = ["pytest"] + +[[package]] +category = "dev" +description = "sphinxcontrib-serializinghtml is a sphinx extension which outputs \"serialized\" HTML files (json and pickle)." +name = "sphinxcontrib-serializinghtml" +optional = false +python-versions = ">=3.5" +version = "1.1.4" + +[package.extras] +lint = ["flake8", "mypy", "docutils-stubs"] +test = ["pytest"] + +[[package]] +category = "dev" +description = "Non-validating SQL parser" +name = "sqlparse" +optional = false +python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*" +version = "0.3.1" + +[[package]] +category = "dev" +description = "Python Library for Tom's Obvious, Minimal Language" +name = "toml" +optional = false +python-versions = "*" +version = "0.10.1" + +[[package]] +category = "dev" +description = "tox is a generic virtualenv management and test command line tool" +name = "tox" +optional = false +python-versions = "!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,!=3.4.*,>=2.7" +version = "3.19.0" + +[package.dependencies] +colorama = ">=0.4.1" +filelock = ">=3.0.0" +packaging = ">=14" +pluggy = ">=0.12.0" +py = ">=1.4.17" +six = ">=1.14.0" +toml = ">=0.9.4" +virtualenv = ">=16.0.0,<20.0.0 || >20.0.0,<20.0.1 || >20.0.1,<20.0.2 || >20.0.2,<20.0.3 || >20.0.3,<20.0.4 || >20.0.4,<20.0.5 || >20.0.5,<20.0.6 || >20.0.6,<20.0.7 || >20.0.7" + +[package.dependencies.importlib-metadata] +python = "<3.8" +version = ">=0.12,<2" + +[package.extras] +docs = ["pygments-github-lexers (>=0.0.5)", "sphinx (>=2.0.0)", "sphinxcontrib-autoprogram (>=0.1.5)", "towncrier (>=18.5.0)"] +testing = ["flaky (>=3.4.0)", "freezegun (>=0.3.11)", "pathlib2 (>=2.3.3)", "psutil (>=5.6.1)", "pytest (>=4.0.0)", "pytest-cov (>=2.5.1)", "pytest-mock (>=1.10.0)", "pytest-randomly (>=1.0.0)", "pytest-xdist (>=1.22.2)"] + +[[package]] +category = "dev" +description = "a fork of Python 2 and 3 ast modules with type comment support" +name = "typed-ast" +optional = false +python-versions = "*" +version = "1.4.1" + +[[package]] +category = "main" +description = "Backported and Experimental Type Hints for Python 3.5+" +name = "typing-extensions" +optional = false +python-versions = "*" +version = "3.7.4.3" + +[[package]] +category = "main" +description = "HTTP library with thread-safe connection pooling, file post, and more." +name = "urllib3" +optional = false +python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*, <4" +version = "1.25.10" + +[package.extras] +brotli = ["brotlipy (>=0.6.0)"] +secure = ["certifi", "cryptography (>=1.3.4)", "idna (>=2.0.0)", "pyOpenSSL (>=0.14)", "ipaddress"] +socks = ["PySocks (>=1.5.6,<1.5.7 || >1.5.7,<2.0)"] + +[[package]] +category = "dev" +description = "Virtual Python Environment builder" +name = "virtualenv" +optional = false +python-versions = "!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,>=2.7" +version = "20.0.31" + +[package.dependencies] +appdirs = ">=1.4.3,<2" +distlib = ">=0.3.1,<1" +filelock = ">=3.0.0,<4" +six = ">=1.9.0,<2" + +[package.dependencies.importlib-metadata] +python = "<3.8" +version = ">=0.12,<2" + +[package.dependencies.importlib-resources] +python = "<3.7" +version = ">=1.0" + +[package.extras] +docs = ["proselint (>=0.10.2)", "sphinx (>=3)", "sphinx-argparse (>=0.2.5)", "sphinx-rtd-theme (>=0.4.3)", "towncrier (>=19.9.0rc1)"] +testing = ["coverage (>=5)", "coverage-enable-subprocess (>=1)", "flaky (>=3)", "pytest (>=4)", "pytest-env (>=0.6.2)", "pytest-freezegun (>=0.4.1)", "pytest-mock (>=2)", "pytest-randomly (>=1)", "pytest-timeout (>=1)", "pytest-xdist (>=1.31.0)", "packaging (>=20.0)", "xonsh (>=0.9.16)"] + +[[package]] +category = "dev" +description = "The comprehensive WSGI web application library." +name = "werkzeug" +optional = false +python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*" +version = "1.0.1" + +[package.extras] +dev = ["pytest", "pytest-timeout", "coverage", "tox", "sphinx", "pallets-sphinx-themes", "sphinx-issues"] +watchdog = ["watchdog"] + +[[package]] +category = "main" +description = "Yet another URL library" +name = "yarl" +optional = true +python-versions = ">=3.5" +version = "1.5.1" + +[package.dependencies] +idna = ">=2.0" +multidict = ">=4.0" + +[package.dependencies.typing-extensions] +python = "<3.8" +version = ">=3.7.4" + +[[package]] +category = "dev" +description = "Backport of pathlib-compatible object wrapper for zip files" +marker = "python_version < \"3.8\"" +name = "zipp" +optional = false +python-versions = ">=3.6" +version = "3.1.0" + +[package.extras] +docs = ["sphinx", "jaraco.packaging (>=3.2)", "rst.linker (>=1.9)"] +testing = ["jaraco.itertools", "func-timeout"] + +[extras] +geoip = ["pycountry", "geoip2"] + +[metadata] +content-hash = "e0957789514d7265ce48c5bcf7a9bf9a990ad4e1336c81c09bb3ce16ef08ccae" +lock-version = "1.0" +python-versions = "^3.6" + +[metadata.files] +aiohttp = [ + {file = "aiohttp-3.6.2-cp35-cp35m-macosx_10_13_x86_64.whl", hash = "sha256:1e984191d1ec186881ffaed4581092ba04f7c61582a177b187d3a2f07ed9719e"}, + {file = "aiohttp-3.6.2-cp35-cp35m-manylinux1_x86_64.whl", hash = "sha256:50aaad128e6ac62e7bf7bd1f0c0a24bc968a0c0590a726d5a955af193544bcec"}, + {file = "aiohttp-3.6.2-cp36-cp36m-macosx_10_13_x86_64.whl", hash = "sha256:65f31b622af739a802ca6fd1a3076fd0ae523f8485c52924a89561ba10c49b48"}, + {file = "aiohttp-3.6.2-cp36-cp36m-manylinux1_x86_64.whl", hash = "sha256:ae55bac364c405caa23a4f2d6cfecc6a0daada500274ffca4a9230e7129eac59"}, + {file = "aiohttp-3.6.2-cp36-cp36m-win32.whl", hash = "sha256:344c780466b73095a72c616fac5ea9c4665add7fc129f285fbdbca3cccf4612a"}, + {file = "aiohttp-3.6.2-cp36-cp36m-win_amd64.whl", hash = "sha256:4c6efd824d44ae697814a2a85604d8e992b875462c6655da161ff18fd4f29f17"}, + {file = "aiohttp-3.6.2-cp37-cp37m-macosx_10_13_x86_64.whl", hash = "sha256:2f4d1a4fdce595c947162333353d4a44952a724fba9ca3205a3df99a33d1307a"}, + {file = "aiohttp-3.6.2-cp37-cp37m-manylinux1_x86_64.whl", hash = "sha256:6206a135d072f88da3e71cc501c59d5abffa9d0bb43269a6dcd28d66bfafdbdd"}, + {file = "aiohttp-3.6.2-cp37-cp37m-win32.whl", hash = "sha256:b778ce0c909a2653741cb4b1ac7015b5c130ab9c897611df43ae6a58523cb965"}, + {file = "aiohttp-3.6.2-cp37-cp37m-win_amd64.whl", hash = "sha256:32e5f3b7e511aa850829fbe5aa32eb455e5534eaa4b1ce93231d00e2f76e5654"}, + {file = "aiohttp-3.6.2-py3-none-any.whl", hash = "sha256:460bd4237d2dbecc3b5ed57e122992f60188afe46e7319116da5eb8a9dfedba4"}, + {file = "aiohttp-3.6.2.tar.gz", hash = "sha256:259ab809ff0727d0e834ac5e8a283dc5e3e0ecc30c4d80b3cd17a4139ce1f326"}, +] +alabaster = [ + {file = "alabaster-0.7.12-py2.py3-none-any.whl", hash = "sha256:446438bdcca0e05bd45ea2de1668c1d9b032e1a9154c2c259092d77031ddd359"}, + {file = "alabaster-0.7.12.tar.gz", hash = "sha256:a661d72d58e6ea8a57f7a86e37d86716863ee5e92788398526d58b26a4e4dc02"}, +] +appdirs = [ + {file = "appdirs-1.4.4-py2.py3-none-any.whl", hash = "sha256:a841dacd6b99318a741b166adb07e19ee71a274450e68237b4650ca1055ab128"}, + {file = "appdirs-1.4.4.tar.gz", hash = "sha256:7d5d0167b2b1ba821647616af46a749d1c653740dd0d2415100fe26e27afdf41"}, +] +asgiref = [ + {file = "asgiref-3.2.10-py3-none-any.whl", hash = "sha256:9fc6fb5d39b8af147ba40765234fa822b39818b12cc80b35ad9b0cef3a476aed"}, + {file = "asgiref-3.2.10.tar.gz", hash = "sha256:7e51911ee147dd685c3c8b805c0ad0cb58d360987b56953878f8c06d2d1c6f1a"}, +] +async-timeout = [ + {file = "async-timeout-3.0.1.tar.gz", hash = "sha256:0c3c816a028d47f659d6ff5c745cb2acf1f966da1fe5c19c77a70282b25f4c5f"}, + {file = "async_timeout-3.0.1-py3-none-any.whl", hash = "sha256:4291ca197d287d274d0b6cb5d6f8f8f82d434ed288f962539ff18cc9012f9ea3"}, +] +atomicwrites = [ + {file = "atomicwrites-1.4.0-py2.py3-none-any.whl", hash = "sha256:6d1784dea7c0c8d4a5172b6c620f40b6e4cbfdf96d783691f2e1302a7b88e197"}, + {file = "atomicwrites-1.4.0.tar.gz", hash = "sha256:ae70396ad1a434f9c7046fd2dd196fc04b12f9e91ffb859164193be8b6168a7a"}, +] +attrs = [ + {file = "attrs-20.1.0-py2.py3-none-any.whl", hash = "sha256:2867b7b9f8326499ab5b0e2d12801fa5c98842d2cbd22b35112ae04bf85b4dff"}, + {file = "attrs-20.1.0.tar.gz", hash = "sha256:0ef97238856430dcf9228e07f316aefc17e8939fc8507e18c6501b761ef1a42a"}, +] +babel = [ + {file = "Babel-2.8.0-py2.py3-none-any.whl", hash = "sha256:d670ea0b10f8b723672d3a6abeb87b565b244da220d76b4dba1b66269ec152d4"}, + {file = "Babel-2.8.0.tar.gz", hash = "sha256:1aac2ae2d0d8ea368fa90906567f5c08463d98ade155c0c4bfedd6a0f7160e38"}, +] +black = [ + {file = "black-20.8b1-py3-none-any.whl", hash = "sha256:70b62ef1527c950db59062cda342ea224d772abdf6adc58b86a45421bab20a6b"}, + {file = "black-20.8b1.tar.gz", hash = "sha256:1c02557aa099101b9d21496f8a914e9ed2222ef70336404eeeac8edba836fbea"}, +] +certifi = [ + {file = "certifi-2020.6.20-py2.py3-none-any.whl", hash = "sha256:8fc0819f1f30ba15bdb34cceffb9ef04d99f420f68eb75d901e9560b8749fc41"}, + {file = "certifi-2020.6.20.tar.gz", hash = "sha256:5930595817496dd21bb8dc35dad090f1c2cd0adfaf21204bf6732ca5d8ee34d3"}, +] +chardet = [ + {file = "chardet-3.0.4-py2.py3-none-any.whl", hash = "sha256:fc323ffcaeaed0e0a02bf4d117757b98aed530d9ed4531e3e15460124c106691"}, + {file = "chardet-3.0.4.tar.gz", hash = "sha256:84ab92ed1c4d4f16916e05906b6b75a6c0fb5db821cc65e70cbd64a3e2a5eaae"}, +] +click = [ + {file = "click-7.1.2-py2.py3-none-any.whl", hash = "sha256:dacca89f4bfadd5de3d7489b7c8a566eee0d3676333fbb50030263894c38c0dc"}, + {file = "click-7.1.2.tar.gz", hash = "sha256:d2b5255c7c6349bc1bd1e59e08cd12acbbd63ce649f2588755783aa94dfb6b1a"}, +] +colorama = [ + {file = "colorama-0.4.3-py2.py3-none-any.whl", hash = "sha256:7d73d2a99753107a36ac6b455ee49046802e59d9d076ef8e47b61499fa29afff"}, + {file = "colorama-0.4.3.tar.gz", hash = "sha256:e96da0d330793e2cb9485e9ddfd918d456036c7149416295932478192f4436a1"}, +] +dataclasses = [ + {file = "dataclasses-0.6-py3-none-any.whl", hash = "sha256:454a69d788c7fda44efd71e259be79577822f5e3f53f029a22d08004e951dc9f"}, + {file = "dataclasses-0.6.tar.gz", hash = "sha256:6988bd2b895eef432d562370bb707d540f32f7360ab13da45340101bc2307d84"}, +] +distlib = [ + {file = "distlib-0.3.1-py2.py3-none-any.whl", hash = "sha256:8c09de2c67b3e7deef7184574fc060ab8a793e7adbb183d942c389c8b13c52fb"}, + {file = "distlib-0.3.1.zip", hash = "sha256:edf6116872c863e1aa9d5bb7cb5e05a022c519a4594dc703843343a9ddd9bff1"}, +] +django = [ + {file = "Django-3.1-py3-none-any.whl", hash = "sha256:1a63f5bb6ff4d7c42f62a519edc2adbb37f9b78068a5a862beff858b68e3dc8b"}, + {file = "Django-3.1.tar.gz", hash = "sha256:2d390268a13c655c97e0e2ede9d117007996db692c1bb93eabebd4fb7ea7012b"}, +] +django-extensions = [ + {file = "django-extensions-3.0.5.tar.gz", hash = "sha256:6306175ae8c78c18ea7aff794f5fa3a47de7d128666e6668bd40596895da7f84"}, + {file = "django_extensions-3.0.5-py2.py3-none-any.whl", hash = "sha256:40d4b7aec7bbe66dda8704fbfaf2e1b7e04ec4aea6b10dcbd78d8af7c37bfddb"}, +] +docutils = [ + {file = "docutils-0.16-py2.py3-none-any.whl", hash = "sha256:0c5b78adfbf7762415433f5515cd5c9e762339e23369dbe8000d84a4bf4ab3af"}, + {file = "docutils-0.16.tar.gz", hash = "sha256:c2de3a60e9e7d07be26b7f2b00ca0309c207e06c100f9cc2a94931fc75a478fc"}, +] +filelock = [ + {file = "filelock-3.0.12-py3-none-any.whl", hash = "sha256:929b7d63ec5b7d6b71b0fa5ac14e030b3f70b75747cef1b10da9b879fef15836"}, + {file = "filelock-3.0.12.tar.gz", hash = "sha256:18d82244ee114f543149c66a6e0c14e9c4f8a1044b5cdaadd0f82159d6a6ff59"}, +] +flake8 = [ + {file = "flake8-3.8.3-py2.py3-none-any.whl", hash = "sha256:15e351d19611c887e482fb960eae4d44845013cc142d42896e9862f775d8cf5c"}, + {file = "flake8-3.8.3.tar.gz", hash = "sha256:f04b9fcbac03b0a3e58c0ab3a0ecc462e023a9faf046d57794184028123aa208"}, +] +geoip2 = [ + {file = "geoip2-4.0.2-py2.py3-none-any.whl", hash = "sha256:eb052541e5a5fae2ca67ad98d0d8e5baab2caea9b36536da70aeb7449627c774"}, + {file = "geoip2-4.0.2.tar.gz", hash = "sha256:4afb5d899eac08444e461239c8afb165c90234adc0b5dc952792d9da74c9091b"}, +] +idna = [ + {file = "idna-2.10-py2.py3-none-any.whl", hash = "sha256:b97d804b1e9b523befed77c48dacec60e6dcb0b5391d57af6a65a312a90648c0"}, + {file = "idna-2.10.tar.gz", hash = "sha256:b307872f855b18632ce0c21c5e45be78c0ea7ae4c15c828c20788b26921eb3f6"}, +] +idna-ssl = [ + {file = "idna-ssl-1.1.0.tar.gz", hash = "sha256:a933e3bb13da54383f9e8f35dc4f9cb9eb9b3b78c6b36f311254d6d0d92c6c7c"}, +] +imagesize = [ + {file = "imagesize-1.2.0-py2.py3-none-any.whl", hash = "sha256:6965f19a6a2039c7d48bca7dba2473069ff854c36ae6f19d2cde309d998228a1"}, + {file = "imagesize-1.2.0.tar.gz", hash = "sha256:b1f6b5a4eab1f73479a50fb79fcf729514a900c341d8503d62a62dbc4127a2b1"}, +] +importlib-metadata = [ + {file = "importlib_metadata-1.7.0-py2.py3-none-any.whl", hash = "sha256:dc15b2969b4ce36305c51eebe62d418ac7791e9a157911d58bfb1f9ccd8e2070"}, + {file = "importlib_metadata-1.7.0.tar.gz", hash = "sha256:90bb658cdbbf6d1735b6341ce708fc7024a3e14e99ffdc5783edea9f9b077f83"}, +] +importlib-resources = [ + {file = "importlib_resources-3.0.0-py2.py3-none-any.whl", hash = "sha256:d028f66b66c0d5732dae86ba4276999855e162a749c92620a38c1d779ed138a7"}, + {file = "importlib_resources-3.0.0.tar.gz", hash = "sha256:19f745a6eca188b490b1428c8d1d4a0d2368759f32370ea8fb89cad2ab1106c3"}, +] +iniconfig = [ + {file = "iniconfig-1.0.1-py3-none-any.whl", hash = "sha256:80cf40c597eb564e86346103f609d74efce0f6b4d4f30ec8ce9e2c26411ba437"}, + {file = "iniconfig-1.0.1.tar.gz", hash = "sha256:e5f92f89355a67de0595932a6c6c02ab4afddc6fcdc0bfc5becd0d60884d3f69"}, +] +isort = [ + {file = "isort-5.4.2-py3-none-any.whl", hash = "sha256:60a1b97e33f61243d12647aaaa3e6cc6778f5eb9f42997650f1cc975b6008750"}, + {file = "isort-5.4.2.tar.gz", hash = "sha256:d488ba1c5a2db721669cc180180d5acf84ebdc5af7827f7aaeaa75f73cf0e2b8"}, +] +jinja2 = [ + {file = "Jinja2-2.11.2-py2.py3-none-any.whl", hash = "sha256:f0a4641d3cf955324a89c04f3d94663aa4d638abe8f733ecd3582848e1c37035"}, + {file = "Jinja2-2.11.2.tar.gz", hash = "sha256:89aab215427ef59c34ad58735269eb58b1a5808103067f7bb9d5836c651b3bb0"}, +] +markupsafe = [ + {file = "MarkupSafe-1.1.1-cp27-cp27m-macosx_10_6_intel.whl", hash = "sha256:09027a7803a62ca78792ad89403b1b7a73a01c8cb65909cd876f7fcebd79b161"}, + {file = "MarkupSafe-1.1.1-cp27-cp27m-manylinux1_i686.whl", hash = "sha256:e249096428b3ae81b08327a63a485ad0878de3fb939049038579ac0ef61e17e7"}, + {file = "MarkupSafe-1.1.1-cp27-cp27m-manylinux1_x86_64.whl", hash = "sha256:500d4957e52ddc3351cabf489e79c91c17f6e0899158447047588650b5e69183"}, + {file = "MarkupSafe-1.1.1-cp27-cp27m-win32.whl", hash = "sha256:b2051432115498d3562c084a49bba65d97cf251f5a331c64a12ee7e04dacc51b"}, + {file = "MarkupSafe-1.1.1-cp27-cp27m-win_amd64.whl", hash = "sha256:98c7086708b163d425c67c7a91bad6e466bb99d797aa64f965e9d25c12111a5e"}, + {file = "MarkupSafe-1.1.1-cp27-cp27mu-manylinux1_i686.whl", hash = "sha256:cd5df75523866410809ca100dc9681e301e3c27567cf498077e8551b6d20e42f"}, + {file = "MarkupSafe-1.1.1-cp27-cp27mu-manylinux1_x86_64.whl", hash = "sha256:43a55c2930bbc139570ac2452adf3d70cdbb3cfe5912c71cdce1c2c6bbd9c5d1"}, + {file = "MarkupSafe-1.1.1-cp34-cp34m-macosx_10_6_intel.whl", hash = "sha256:1027c282dad077d0bae18be6794e6b6b8c91d58ed8a8d89a89d59693b9131db5"}, + {file = "MarkupSafe-1.1.1-cp34-cp34m-manylinux1_i686.whl", hash = "sha256:62fe6c95e3ec8a7fad637b7f3d372c15ec1caa01ab47926cfdf7a75b40e0eac1"}, + {file = "MarkupSafe-1.1.1-cp34-cp34m-manylinux1_x86_64.whl", hash = "sha256:88e5fcfb52ee7b911e8bb6d6aa2fd21fbecc674eadd44118a9cc3863f938e735"}, + {file = "MarkupSafe-1.1.1-cp34-cp34m-win32.whl", hash = "sha256:ade5e387d2ad0d7ebf59146cc00c8044acbd863725f887353a10df825fc8ae21"}, + {file = "MarkupSafe-1.1.1-cp34-cp34m-win_amd64.whl", hash = "sha256:09c4b7f37d6c648cb13f9230d847adf22f8171b1ccc4d5682398e77f40309235"}, + {file = "MarkupSafe-1.1.1-cp35-cp35m-macosx_10_6_intel.whl", hash = "sha256:79855e1c5b8da654cf486b830bd42c06e8780cea587384cf6545b7d9ac013a0b"}, + {file = "MarkupSafe-1.1.1-cp35-cp35m-manylinux1_i686.whl", hash = "sha256:c8716a48d94b06bb3b2524c2b77e055fb313aeb4ea620c8dd03a105574ba704f"}, + {file = "MarkupSafe-1.1.1-cp35-cp35m-manylinux1_x86_64.whl", hash = "sha256:7c1699dfe0cf8ff607dbdcc1e9b9af1755371f92a68f706051cc8c37d447c905"}, + {file = "MarkupSafe-1.1.1-cp35-cp35m-win32.whl", hash = "sha256:6dd73240d2af64df90aa7c4e7481e23825ea70af4b4922f8ede5b9e35f78a3b1"}, + {file = "MarkupSafe-1.1.1-cp35-cp35m-win_amd64.whl", hash = "sha256:9add70b36c5666a2ed02b43b335fe19002ee5235efd4b8a89bfcf9005bebac0d"}, + {file = "MarkupSafe-1.1.1-cp36-cp36m-macosx_10_6_intel.whl", hash = "sha256:24982cc2533820871eba85ba648cd53d8623687ff11cbb805be4ff7b4c971aff"}, + {file = "MarkupSafe-1.1.1-cp36-cp36m-manylinux1_i686.whl", hash = "sha256:00bc623926325b26bb9605ae9eae8a215691f33cae5df11ca5424f06f2d1f473"}, + {file = "MarkupSafe-1.1.1-cp36-cp36m-manylinux1_x86_64.whl", hash = "sha256:717ba8fe3ae9cc0006d7c451f0bb265ee07739daf76355d06366154ee68d221e"}, + {file = "MarkupSafe-1.1.1-cp36-cp36m-win32.whl", hash = "sha256:535f6fc4d397c1563d08b88e485c3496cf5784e927af890fb3c3aac7f933ec66"}, + {file = "MarkupSafe-1.1.1-cp36-cp36m-win_amd64.whl", hash = "sha256:b1282f8c00509d99fef04d8ba936b156d419be841854fe901d8ae224c59f0be5"}, + {file = "MarkupSafe-1.1.1-cp37-cp37m-macosx_10_6_intel.whl", hash = "sha256:8defac2f2ccd6805ebf65f5eeb132adcf2ab57aa11fdf4c0dd5169a004710e7d"}, + {file = "MarkupSafe-1.1.1-cp37-cp37m-manylinux1_i686.whl", hash = "sha256:46c99d2de99945ec5cb54f23c8cd5689f6d7177305ebff350a58ce5f8de1669e"}, + {file = "MarkupSafe-1.1.1-cp37-cp37m-manylinux1_x86_64.whl", hash = "sha256:ba59edeaa2fc6114428f1637ffff42da1e311e29382d81b339c1817d37ec93c6"}, + {file = "MarkupSafe-1.1.1-cp37-cp37m-win32.whl", hash = "sha256:b00c1de48212e4cc9603895652c5c410df699856a2853135b3967591e4beebc2"}, + {file = "MarkupSafe-1.1.1-cp37-cp37m-win_amd64.whl", hash = "sha256:9bf40443012702a1d2070043cb6291650a0841ece432556f784f004937f0f32c"}, + {file = "MarkupSafe-1.1.1-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:6788b695d50a51edb699cb55e35487e430fa21f1ed838122d722e0ff0ac5ba15"}, + {file = "MarkupSafe-1.1.1-cp38-cp38-manylinux1_i686.whl", hash = "sha256:cdb132fc825c38e1aeec2c8aa9338310d29d337bebbd7baa06889d09a60a1fa2"}, + {file = "MarkupSafe-1.1.1-cp38-cp38-manylinux1_x86_64.whl", hash = "sha256:13d3144e1e340870b25e7b10b98d779608c02016d5184cfb9927a9f10c689f42"}, + {file = "MarkupSafe-1.1.1-cp38-cp38-win32.whl", hash = "sha256:596510de112c685489095da617b5bcbbac7dd6384aeebeda4df6025d0256a81b"}, + {file = "MarkupSafe-1.1.1-cp38-cp38-win_amd64.whl", hash = "sha256:e8313f01ba26fbbe36c7be1966a7b7424942f670f38e666995b88d012765b9be"}, + {file = "MarkupSafe-1.1.1.tar.gz", hash = "sha256:29872e92839765e546828bb7754a68c418d927cd064fd4708fab9fe9c8bb116b"}, +] +maxminddb = [ + {file = "maxminddb-2.0.2.tar.gz", hash = "sha256:b95d8ed21799e6604683669c7ed3c6a184fcd92434d5762dccdb139b4f29e597"}, +] +mccabe = [ + {file = "mccabe-0.6.1-py2.py3-none-any.whl", hash = "sha256:ab8a6258860da4b6677da4bd2fe5dc2c659cff31b3ee4f7f5d64e79735b80d42"}, + {file = "mccabe-0.6.1.tar.gz", hash = "sha256:dd8d182285a0fe56bace7f45b5e7d1a6ebcbf524e8f3bd87eb0f125271b8831f"}, +] +more-itertools = [ + {file = "more-itertools-8.5.0.tar.gz", hash = "sha256:6f83822ae94818eae2612063a5101a7311e68ae8002005b5e05f03fd74a86a20"}, + {file = "more_itertools-8.5.0-py3-none-any.whl", hash = "sha256:9b30f12df9393f0d28af9210ff8efe48d10c94f73e5daf886f10c4b0b0b4f03c"}, +] +multidict = [ + {file = "multidict-4.7.6-cp35-cp35m-macosx_10_14_x86_64.whl", hash = "sha256:275ca32383bc5d1894b6975bb4ca6a7ff16ab76fa622967625baeebcf8079000"}, + {file = "multidict-4.7.6-cp35-cp35m-manylinux1_x86_64.whl", hash = "sha256:1ece5a3369835c20ed57adadc663400b5525904e53bae59ec854a5d36b39b21a"}, + {file = "multidict-4.7.6-cp35-cp35m-win32.whl", hash = "sha256:5141c13374e6b25fe6bf092052ab55c0c03d21bd66c94a0e3ae371d3e4d865a5"}, + {file = "multidict-4.7.6-cp35-cp35m-win_amd64.whl", hash = "sha256:9456e90649005ad40558f4cf51dbb842e32807df75146c6d940b6f5abb4a78f3"}, + {file = "multidict-4.7.6-cp36-cp36m-macosx_10_14_x86_64.whl", hash = "sha256:e0d072ae0f2a179c375f67e3da300b47e1a83293c554450b29c900e50afaae87"}, + {file = "multidict-4.7.6-cp36-cp36m-manylinux1_x86_64.whl", hash = "sha256:3750f2205b800aac4bb03b5ae48025a64e474d2c6cc79547988ba1d4122a09e2"}, + {file = "multidict-4.7.6-cp36-cp36m-win32.whl", hash = "sha256:f07acae137b71af3bb548bd8da720956a3bc9f9a0b87733e0899226a2317aeb7"}, + {file = "multidict-4.7.6-cp36-cp36m-win_amd64.whl", hash = "sha256:6513728873f4326999429a8b00fc7ceddb2509b01d5fd3f3be7881a257b8d463"}, + {file = "multidict-4.7.6-cp37-cp37m-macosx_10_14_x86_64.whl", hash = "sha256:feed85993dbdb1dbc29102f50bca65bdc68f2c0c8d352468c25b54874f23c39d"}, + {file = "multidict-4.7.6-cp37-cp37m-manylinux1_x86_64.whl", hash = "sha256:fcfbb44c59af3f8ea984de67ec7c306f618a3ec771c2843804069917a8f2e255"}, + {file = "multidict-4.7.6-cp37-cp37m-win32.whl", hash = "sha256:4538273208e7294b2659b1602490f4ed3ab1c8cf9dbdd817e0e9db8e64be2507"}, + {file = "multidict-4.7.6-cp37-cp37m-win_amd64.whl", hash = "sha256:d14842362ed4cf63751648e7672f7174c9818459d169231d03c56e84daf90b7c"}, + {file = "multidict-4.7.6-cp38-cp38-macosx_10_14_x86_64.whl", hash = "sha256:c026fe9a05130e44157b98fea3ab12969e5b60691a276150db9eda71710cd10b"}, + {file = "multidict-4.7.6-cp38-cp38-manylinux1_x86_64.whl", hash = "sha256:51a4d210404ac61d32dada00a50ea7ba412e6ea945bbe992e4d7a595276d2ec7"}, + {file = "multidict-4.7.6-cp38-cp38-win32.whl", hash = "sha256:5cf311a0f5ef80fe73e4f4c0f0998ec08f954a6ec72b746f3c179e37de1d210d"}, + {file = "multidict-4.7.6-cp38-cp38-win_amd64.whl", hash = "sha256:7388d2ef3c55a8ba80da62ecfafa06a1c097c18032a501ffd4cabbc52d7f2b19"}, + {file = "multidict-4.7.6.tar.gz", hash = "sha256:fbb77a75e529021e7c4a8d4e823d88ef4d23674a202be4f5addffc72cbb91430"}, +] +mypy-extensions = [ + {file = "mypy_extensions-0.4.3-py2.py3-none-any.whl", hash = "sha256:090fedd75945a69ae91ce1303b5824f428daf5a028d2f6ab8a299250a846f15d"}, + {file = "mypy_extensions-0.4.3.tar.gz", hash = "sha256:2d82818f5bb3e369420cb3c4060a7970edba416647068eb4c5343488a6c604a8"}, +] +packaging = [ + {file = "packaging-20.4-py2.py3-none-any.whl", hash = "sha256:998416ba6962ae7fbd6596850b80e17859a5753ba17c32284f67bfff33784181"}, + {file = "packaging-20.4.tar.gz", hash = "sha256:4357f74f47b9c12db93624a82154e9b120fa8293699949152b22065d556079f8"}, +] +pathspec = [ + {file = "pathspec-0.8.0-py2.py3-none-any.whl", hash = "sha256:7d91249d21749788d07a2d0f94147accd8f845507400749ea19c1ec9054a12b0"}, + {file = "pathspec-0.8.0.tar.gz", hash = "sha256:da45173eb3a6f2a5a487efba21f050af2b41948be6ab52b6a1e3ff22bb8b7061"}, +] +pep8 = [ + {file = "pep8-1.7.1-py2.py3-none-any.whl", hash = "sha256:b22cfae5db09833bb9bd7c8463b53e1a9c9b39f12e304a8d0bba729c501827ee"}, + {file = "pep8-1.7.1.tar.gz", hash = "sha256:fe249b52e20498e59e0b5c5256aa52ee99fc295b26ec9eaa85776ffdb9fe6374"}, +] +pluggy = [ + {file = "pluggy-0.13.1-py2.py3-none-any.whl", hash = "sha256:966c145cd83c96502c3c3868f50408687b38434af77734af1e9ca461a4081d2d"}, + {file = "pluggy-0.13.1.tar.gz", hash = "sha256:15b2acde666561e1298d71b523007ed7364de07029219b604cf808bfa1c765b0"}, +] +py = [ + {file = "py-1.9.0-py2.py3-none-any.whl", hash = "sha256:366389d1db726cd2fcfc79732e75410e5fe4d31db13692115529d34069a043c2"}, + {file = "py-1.9.0.tar.gz", hash = "sha256:9ca6883ce56b4e8da7e79ac18787889fa5206c79dcc67fb065376cd2fe03f342"}, +] +pycodestyle = [ + {file = "pycodestyle-2.6.0-py2.py3-none-any.whl", hash = "sha256:2295e7b2f6b5bd100585ebcb1f616591b652db8a741695b3d8f5d28bdc934367"}, + {file = "pycodestyle-2.6.0.tar.gz", hash = "sha256:c58a7d2815e0e8d7972bf1803331fb0152f867bd89adf8a01dfd55085434192e"}, +] +pycountry = [ + {file = "pycountry-20.7.3.tar.gz", hash = "sha256:81084a53d3454344c0292deebc20fcd0a1488c136d4900312cbd465cf552cb42"}, +] +pyflakes = [ + {file = "pyflakes-2.2.0-py2.py3-none-any.whl", hash = "sha256:0d94e0e05a19e57a99444b6ddcf9a6eb2e5c68d3ca1e98e90707af8152c90a92"}, + {file = "pyflakes-2.2.0.tar.gz", hash = "sha256:35b2d75ee967ea93b55750aa9edbbf72813e06a66ba54438df2cfac9e3c27fc8"}, +] +pygments = [ + {file = "Pygments-2.6.1-py3-none-any.whl", hash = "sha256:ff7a40b4860b727ab48fad6360eb351cc1b33cbf9b15a0f689ca5353e9463324"}, + {file = "Pygments-2.6.1.tar.gz", hash = "sha256:647344a061c249a3b74e230c739f434d7ea4d8b1d5f3721bc0f3558049b38f44"}, +] +pyparsing = [ + {file = "pyparsing-2.4.7-py2.py3-none-any.whl", hash = "sha256:ef9d7589ef3c200abe66653d3f1ab1033c3c419ae9b9bdb1240a85b024efc88b"}, + {file = "pyparsing-2.4.7.tar.gz", hash = "sha256:c203ec8783bf771a155b207279b9bccb8dea02d8f0c9e5f8ead507bc3246ecc1"}, +] +pytest = [ + {file = "pytest-6.0.1-py3-none-any.whl", hash = "sha256:8b6007800c53fdacd5a5c192203f4e531eb2a1540ad9c752e052ec0f7143dbad"}, + {file = "pytest-6.0.1.tar.gz", hash = "sha256:85228d75db9f45e06e57ef9bf4429267f81ac7c0d742cc9ed63d09886a9fe6f4"}, +] +pytest-django = [ + {file = "pytest-django-3.9.0.tar.gz", hash = "sha256:664e5f42242e5e182519388f01b9f25d824a9feb7cd17d8f863c8d776f38baf9"}, + {file = "pytest_django-3.9.0-py2.py3-none-any.whl", hash = "sha256:64f99d565dd9497af412fcab2989fe40982c1282d4118ff422b407f3f7275ca5"}, +] +pytz = [ + {file = "pytz-2020.1-py2.py3-none-any.whl", hash = "sha256:a494d53b6d39c3c6e44c3bec237336e14305e4f29bbf800b599253057fbb79ed"}, + {file = "pytz-2020.1.tar.gz", hash = "sha256:c35965d010ce31b23eeb663ed3cc8c906275d6be1a34393a1d73a41febf4a048"}, +] +regex = [ + {file = "regex-2020.7.14-cp27-cp27m-win32.whl", hash = "sha256:e46d13f38cfcbb79bfdb2964b0fe12561fe633caf964a77a5f8d4e45fe5d2ef7"}, + {file = "regex-2020.7.14-cp27-cp27m-win_amd64.whl", hash = "sha256:6961548bba529cac7c07af2fd4d527c5b91bb8fe18995fed6044ac22b3d14644"}, + {file = "regex-2020.7.14-cp36-cp36m-manylinux1_i686.whl", hash = "sha256:c50a724d136ec10d920661f1442e4a8b010a4fe5aebd65e0c2241ea41dbe93dc"}, + {file = "regex-2020.7.14-cp36-cp36m-manylinux1_x86_64.whl", hash = "sha256:8a51f2c6d1f884e98846a0a9021ff6861bdb98457879f412fdc2b42d14494067"}, + {file = "regex-2020.7.14-cp36-cp36m-manylinux2010_i686.whl", hash = "sha256:9c568495e35599625f7b999774e29e8d6b01a6fb684d77dee1f56d41b11b40cd"}, + {file = "regex-2020.7.14-cp36-cp36m-manylinux2010_x86_64.whl", hash = "sha256:51178c738d559a2d1071ce0b0f56e57eb315bcf8f7d4cf127674b533e3101f88"}, + {file = "regex-2020.7.14-cp36-cp36m-win32.whl", hash = "sha256:9eddaafb3c48e0900690c1727fba226c4804b8e6127ea409689c3bb492d06de4"}, + {file = "regex-2020.7.14-cp36-cp36m-win_amd64.whl", hash = "sha256:14a53646369157baa0499513f96091eb70382eb50b2c82393d17d7ec81b7b85f"}, + {file = "regex-2020.7.14-cp37-cp37m-manylinux1_i686.whl", hash = "sha256:1269fef3167bb52631ad4fa7dd27bf635d5a0790b8e6222065d42e91bede4162"}, + {file = "regex-2020.7.14-cp37-cp37m-manylinux1_x86_64.whl", hash = "sha256:d0a5095d52b90ff38592bbdc2644f17c6d495762edf47d876049cfd2968fbccf"}, + {file = "regex-2020.7.14-cp37-cp37m-manylinux2010_i686.whl", hash = "sha256:4c037fd14c5f4e308b8370b447b469ca10e69427966527edcab07f52d88388f7"}, + {file = "regex-2020.7.14-cp37-cp37m-manylinux2010_x86_64.whl", hash = "sha256:bc3d98f621898b4a9bc7fecc00513eec8f40b5b83913d74ccb445f037d58cd89"}, + {file = "regex-2020.7.14-cp37-cp37m-win32.whl", hash = "sha256:46bac5ca10fb748d6c55843a931855e2727a7a22584f302dd9bb1506e69f83f6"}, + {file = "regex-2020.7.14-cp37-cp37m-win_amd64.whl", hash = "sha256:0dc64ee3f33cd7899f79a8d788abfbec168410be356ed9bd30bbd3f0a23a7204"}, + {file = "regex-2020.7.14-cp38-cp38-manylinux1_i686.whl", hash = "sha256:5ea81ea3dbd6767873c611687141ec7b06ed8bab43f68fad5b7be184a920dc99"}, + {file = "regex-2020.7.14-cp38-cp38-manylinux1_x86_64.whl", hash = "sha256:bbb332d45b32df41200380fff14712cb6093b61bd142272a10b16778c418e98e"}, + {file = "regex-2020.7.14-cp38-cp38-manylinux2010_i686.whl", hash = "sha256:c11d6033115dc4887c456565303f540c44197f4fc1a2bfb192224a301534888e"}, + {file = "regex-2020.7.14-cp38-cp38-manylinux2010_x86_64.whl", hash = "sha256:75aaa27aa521a182824d89e5ab0a1d16ca207318a6b65042b046053cfc8ed07a"}, + {file = "regex-2020.7.14-cp38-cp38-win32.whl", hash = "sha256:d6cff2276e502b86a25fd10c2a96973fdb45c7a977dca2138d661417f3728341"}, + {file = "regex-2020.7.14-cp38-cp38-win_amd64.whl", hash = "sha256:7a2dd66d2d4df34fa82c9dc85657c5e019b87932019947faece7983f2089a840"}, + {file = "regex-2020.7.14.tar.gz", hash = "sha256:3a3af27a8d23143c49a3420efe5b3f8cf1a48c6fc8bc6856b03f638abc1833bb"}, +] +requests = [ + {file = "requests-2.24.0-py2.py3-none-any.whl", hash = "sha256:fe75cc94a9443b9246fc7049224f75604b113c36acb93f87b80ed42c44cbb898"}, + {file = "requests-2.24.0.tar.gz", hash = "sha256:b3559a131db72c33ee969480840fff4bb6dd111de7dd27c8ee1f820f4f00231b"}, +] +six = [ + {file = "six-1.15.0-py2.py3-none-any.whl", hash = "sha256:8b74bedcbbbaca38ff6d7491d76f2b06b3592611af620f8426e82dddb04a5ced"}, + {file = "six-1.15.0.tar.gz", hash = "sha256:30639c035cdb23534cd4aa2dd52c3bf48f06e5f4a941509c8bafd8ce11080259"}, +] +snowballstemmer = [ + {file = "snowballstemmer-2.0.0-py2.py3-none-any.whl", hash = "sha256:209f257d7533fdb3cb73bdbd24f436239ca3b2fa67d56f6ff88e86be08cc5ef0"}, + {file = "snowballstemmer-2.0.0.tar.gz", hash = "sha256:df3bac3df4c2c01363f3dd2cfa78cce2840a79b9f1c2d2de9ce8d31683992f52"}, +] +sphinx = [ + {file = "Sphinx-3.2.1-py3-none-any.whl", hash = "sha256:ce6fd7ff5b215af39e2fcd44d4a321f6694b4530b6f2b2109b64d120773faea0"}, + {file = "Sphinx-3.2.1.tar.gz", hash = "sha256:321d6d9b16fa381a5306e5a0b76cd48ffbc588e6340059a729c6fdd66087e0e8"}, +] +sphinxcontrib-applehelp = [ + {file = "sphinxcontrib-applehelp-1.0.2.tar.gz", hash = "sha256:a072735ec80e7675e3f432fcae8610ecf509c5f1869d17e2eecff44389cdbc58"}, + {file = "sphinxcontrib_applehelp-1.0.2-py2.py3-none-any.whl", hash = "sha256:806111e5e962be97c29ec4c1e7fe277bfd19e9652fb1a4392105b43e01af885a"}, +] +sphinxcontrib-devhelp = [ + {file = "sphinxcontrib-devhelp-1.0.2.tar.gz", hash = "sha256:ff7f1afa7b9642e7060379360a67e9c41e8f3121f2ce9164266f61b9f4b338e4"}, + {file = "sphinxcontrib_devhelp-1.0.2-py2.py3-none-any.whl", hash = "sha256:8165223f9a335cc1af7ffe1ed31d2871f325254c0423bc0c4c7cd1c1e4734a2e"}, +] +sphinxcontrib-htmlhelp = [ + {file = "sphinxcontrib-htmlhelp-1.0.3.tar.gz", hash = "sha256:e8f5bb7e31b2dbb25b9cc435c8ab7a79787ebf7f906155729338f3156d93659b"}, + {file = "sphinxcontrib_htmlhelp-1.0.3-py2.py3-none-any.whl", hash = "sha256:3c0bc24a2c41e340ac37c85ced6dafc879ab485c095b1d65d2461ac2f7cca86f"}, +] +sphinxcontrib-jsmath = [ + {file = "sphinxcontrib-jsmath-1.0.1.tar.gz", hash = "sha256:a9925e4a4587247ed2191a22df5f6970656cb8ca2bd6284309578f2153e0c4b8"}, + {file = "sphinxcontrib_jsmath-1.0.1-py2.py3-none-any.whl", hash = "sha256:2ec2eaebfb78f3f2078e73666b1415417a116cc848b72e5172e596c871103178"}, +] +sphinxcontrib-qthelp = [ + {file = "sphinxcontrib-qthelp-1.0.3.tar.gz", hash = "sha256:4c33767ee058b70dba89a6fc5c1892c0d57a54be67ddd3e7875a18d14cba5a72"}, + {file = "sphinxcontrib_qthelp-1.0.3-py2.py3-none-any.whl", hash = "sha256:bd9fc24bcb748a8d51fd4ecaade681350aa63009a347a8c14e637895444dfab6"}, +] +sphinxcontrib-serializinghtml = [ + {file = "sphinxcontrib-serializinghtml-1.1.4.tar.gz", hash = "sha256:eaa0eccc86e982a9b939b2b82d12cc5d013385ba5eadcc7e4fed23f4405f77bc"}, + {file = "sphinxcontrib_serializinghtml-1.1.4-py2.py3-none-any.whl", hash = "sha256:f242a81d423f59617a8e5cf16f5d4d74e28ee9a66f9e5b637a18082991db5a9a"}, +] +sqlparse = [ + {file = "sqlparse-0.3.1-py2.py3-none-any.whl", hash = "sha256:022fb9c87b524d1f7862b3037e541f68597a730a8843245c349fc93e1643dc4e"}, + {file = "sqlparse-0.3.1.tar.gz", hash = "sha256:e162203737712307dfe78860cc56c8da8a852ab2ee33750e33aeadf38d12c548"}, +] +toml = [ + {file = "toml-0.10.1-py2.py3-none-any.whl", hash = "sha256:bda89d5935c2eac546d648028b9901107a595863cb36bae0c73ac804a9b4ce88"}, + {file = "toml-0.10.1.tar.gz", hash = "sha256:926b612be1e5ce0634a2ca03470f95169cf16f939018233a670519cb4ac58b0f"}, +] +tox = [ + {file = "tox-3.19.0-py2.py3-none-any.whl", hash = "sha256:3d94b6921a0b6dc90fd8128df83741f30bb41ccd6cd52d131a6a6944ca8f16e6"}, + {file = "tox-3.19.0.tar.gz", hash = "sha256:17e61a93afe5c49281fb969ab71f7a3f22d7586d1c56f9a74219910f356fe7d3"}, +] +typed-ast = [ + {file = "typed_ast-1.4.1-cp35-cp35m-manylinux1_i686.whl", hash = "sha256:73d785a950fc82dd2a25897d525d003f6378d1cb23ab305578394694202a58c3"}, + {file = "typed_ast-1.4.1-cp35-cp35m-manylinux1_x86_64.whl", hash = "sha256:aaee9905aee35ba5905cfb3c62f3e83b3bec7b39413f0a7f19be4e547ea01ebb"}, + {file = "typed_ast-1.4.1-cp35-cp35m-win32.whl", hash = "sha256:0c2c07682d61a629b68433afb159376e24e5b2fd4641d35424e462169c0a7919"}, + {file = "typed_ast-1.4.1-cp35-cp35m-win_amd64.whl", hash = "sha256:4083861b0aa07990b619bd7ddc365eb7fa4b817e99cf5f8d9cf21a42780f6e01"}, + {file = "typed_ast-1.4.1-cp36-cp36m-macosx_10_9_x86_64.whl", hash = "sha256:269151951236b0f9a6f04015a9004084a5ab0d5f19b57de779f908621e7d8b75"}, + {file = "typed_ast-1.4.1-cp36-cp36m-manylinux1_i686.whl", hash = "sha256:24995c843eb0ad11a4527b026b4dde3da70e1f2d8806c99b7b4a7cf491612652"}, + {file = "typed_ast-1.4.1-cp36-cp36m-manylinux1_x86_64.whl", hash = "sha256:fe460b922ec15dd205595c9b5b99e2f056fd98ae8f9f56b888e7a17dc2b757e7"}, + {file = "typed_ast-1.4.1-cp36-cp36m-win32.whl", hash = "sha256:4e3e5da80ccbebfff202a67bf900d081906c358ccc3d5e3c8aea42fdfdfd51c1"}, + {file = "typed_ast-1.4.1-cp36-cp36m-win_amd64.whl", hash = "sha256:249862707802d40f7f29f6e1aad8d84b5aa9e44552d2cc17384b209f091276aa"}, + {file = "typed_ast-1.4.1-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:8ce678dbaf790dbdb3eba24056d5364fb45944f33553dd5869b7580cdbb83614"}, + {file = "typed_ast-1.4.1-cp37-cp37m-manylinux1_i686.whl", hash = "sha256:c9e348e02e4d2b4a8b2eedb48210430658df6951fa484e59de33ff773fbd4b41"}, + {file = "typed_ast-1.4.1-cp37-cp37m-manylinux1_x86_64.whl", hash = "sha256:bcd3b13b56ea479b3650b82cabd6b5343a625b0ced5429e4ccad28a8973f301b"}, + {file = "typed_ast-1.4.1-cp37-cp37m-win32.whl", hash = "sha256:d5d33e9e7af3b34a40dc05f498939f0ebf187f07c385fd58d591c533ad8562fe"}, + {file = "typed_ast-1.4.1-cp37-cp37m-win_amd64.whl", hash = "sha256:0666aa36131496aed8f7be0410ff974562ab7eeac11ef351def9ea6fa28f6355"}, + {file = "typed_ast-1.4.1-cp38-cp38-macosx_10_15_x86_64.whl", hash = "sha256:d205b1b46085271b4e15f670058ce182bd1199e56b317bf2ec004b6a44f911f6"}, + {file = "typed_ast-1.4.1-cp38-cp38-manylinux1_i686.whl", hash = "sha256:6daac9731f172c2a22ade6ed0c00197ee7cc1221aa84cfdf9c31defeb059a907"}, + {file = "typed_ast-1.4.1-cp38-cp38-manylinux1_x86_64.whl", hash = "sha256:498b0f36cc7054c1fead3d7fc59d2150f4d5c6c56ba7fb150c013fbc683a8d2d"}, + {file = "typed_ast-1.4.1-cp38-cp38-win32.whl", hash = "sha256:715ff2f2df46121071622063fc7543d9b1fd19ebfc4f5c8895af64a77a8c852c"}, + {file = "typed_ast-1.4.1-cp38-cp38-win_amd64.whl", hash = "sha256:fc0fea399acb12edbf8a628ba8d2312f583bdbdb3335635db062fa98cf71fca4"}, + {file = "typed_ast-1.4.1-cp39-cp39-macosx_10_15_x86_64.whl", hash = "sha256:d43943ef777f9a1c42bf4e552ba23ac77a6351de620aa9acf64ad54933ad4d34"}, + {file = "typed_ast-1.4.1.tar.gz", hash = "sha256:8c8aaad94455178e3187ab22c8b01a3837f8ee50e09cf31f1ba129eb293ec30b"}, +] +typing-extensions = [ + {file = "typing_extensions-3.7.4.3-py2-none-any.whl", hash = "sha256:dafc7639cde7f1b6e1acc0f457842a83e722ccca8eef5270af2d74792619a89f"}, + {file = "typing_extensions-3.7.4.3-py3-none-any.whl", hash = "sha256:7cb407020f00f7bfc3cb3e7881628838e69d8f3fcab2f64742a5e76b2f841918"}, + {file = "typing_extensions-3.7.4.3.tar.gz", hash = "sha256:99d4073b617d30288f569d3f13d2bd7548c3a7e4c8de87db09a9d29bb3a4a60c"}, +] +urllib3 = [ + {file = "urllib3-1.25.10-py2.py3-none-any.whl", hash = "sha256:e7983572181f5e1522d9c98453462384ee92a0be7fac5f1413a1e35c56cc0461"}, + {file = "urllib3-1.25.10.tar.gz", hash = "sha256:91056c15fa70756691db97756772bb1eb9678fa585d9184f24534b100dc60f4a"}, +] +virtualenv = [ + {file = "virtualenv-20.0.31-py2.py3-none-any.whl", hash = "sha256:e0305af10299a7fb0d69393d8f04cb2965dda9351140d11ac8db4e5e3970451b"}, + {file = "virtualenv-20.0.31.tar.gz", hash = "sha256:43add625c53c596d38f971a465553f6318decc39d98512bc100fa1b1e839c8dc"}, +] +werkzeug = [ + {file = "Werkzeug-1.0.1-py2.py3-none-any.whl", hash = "sha256:2de2a5db0baeae7b2d2664949077c2ac63fbd16d98da0ff71837f7d1dea3fd43"}, + {file = "Werkzeug-1.0.1.tar.gz", hash = "sha256:6c80b1e5ad3665290ea39320b91e1be1e0d5f60652b964a3070216de83d2e47c"}, +] +yarl = [ + {file = "yarl-1.5.1-cp35-cp35m-macosx_10_14_x86_64.whl", hash = "sha256:db6db0f45d2c63ddb1a9d18d1b9b22f308e52c83638c26b422d520a815c4b3fb"}, + {file = "yarl-1.5.1-cp35-cp35m-manylinux1_x86_64.whl", hash = "sha256:17668ec6722b1b7a3a05cc0167659f6c95b436d25a36c2d52db0eca7d3f72593"}, + {file = "yarl-1.5.1-cp35-cp35m-win32.whl", hash = "sha256:040b237f58ff7d800e6e0fd89c8439b841f777dd99b4a9cca04d6935564b9409"}, + {file = "yarl-1.5.1-cp35-cp35m-win_amd64.whl", hash = "sha256:f18d68f2be6bf0e89f1521af2b1bb46e66ab0018faafa81d70f358153170a317"}, + {file = "yarl-1.5.1-cp36-cp36m-macosx_10_14_x86_64.whl", hash = "sha256:c52ce2883dc193824989a9b97a76ca86ecd1fa7955b14f87bf367a61b6232511"}, + {file = "yarl-1.5.1-cp36-cp36m-manylinux1_x86_64.whl", hash = "sha256:ce584af5de8830d8701b8979b18fcf450cef9a382b1a3c8ef189bedc408faf1e"}, + {file = "yarl-1.5.1-cp36-cp36m-win32.whl", hash = "sha256:df89642981b94e7db5596818499c4b2219028f2a528c9c37cc1de45bf2fd3a3f"}, + {file = "yarl-1.5.1-cp36-cp36m-win_amd64.whl", hash = "sha256:3a584b28086bc93c888a6c2aa5c92ed1ae20932f078c46509a66dce9ea5533f2"}, + {file = "yarl-1.5.1-cp37-cp37m-macosx_10_14_x86_64.whl", hash = "sha256:da456eeec17fa8aa4594d9a9f27c0b1060b6a75f2419fe0c00609587b2695f4a"}, + {file = "yarl-1.5.1-cp37-cp37m-manylinux1_x86_64.whl", hash = "sha256:bc2f976c0e918659f723401c4f834deb8a8e7798a71be4382e024bcc3f7e23a8"}, + {file = "yarl-1.5.1-cp37-cp37m-win32.whl", hash = "sha256:4439be27e4eee76c7632c2427ca5e73703151b22cae23e64adb243a9c2f565d8"}, + {file = "yarl-1.5.1-cp37-cp37m-win_amd64.whl", hash = "sha256:48e918b05850fffb070a496d2b5f97fc31d15d94ca33d3d08a4f86e26d4e7c5d"}, + {file = "yarl-1.5.1-cp38-cp38-macosx_10_14_x86_64.whl", hash = "sha256:9b930776c0ae0c691776f4d2891ebc5362af86f152dd0da463a6614074cb1b02"}, + {file = "yarl-1.5.1-cp38-cp38-manylinux1_x86_64.whl", hash = "sha256:b3b9ad80f8b68519cc3372a6ca85ae02cc5a8807723ac366b53c0f089db19e4a"}, + {file = "yarl-1.5.1-cp38-cp38-win32.whl", hash = "sha256:f379b7f83f23fe12823085cd6b906edc49df969eb99757f58ff382349a3303c6"}, + {file = "yarl-1.5.1-cp38-cp38-win_amd64.whl", hash = "sha256:9102b59e8337f9874638fcfc9ac3734a0cfadb100e47d55c20d0dc6087fb4692"}, + {file = "yarl-1.5.1.tar.gz", hash = "sha256:c22c75b5f394f3d47105045ea551e08a3e804dc7e01b37800ca35b58f856c3d6"}, +] +zipp = [ + {file = "zipp-3.1.0-py3-none-any.whl", hash = "sha256:aa36550ff0c0b7ef7fa639055d797116ee891440eac1a56f378e2d3179e0320b"}, + {file = "zipp-3.1.0.tar.gz", hash = "sha256:c599e4d75c98f6798c509911d08a22e6c021d074469042177c8c86fb92eefd96"}, +] diff --git a/pyproject.toml b/pyproject.toml new file mode 100644 index 0000000..8253708 --- /dev/null +++ b/pyproject.toml @@ -0,0 +1,51 @@ +[tool.poetry] +name = "django-iprestrict-redux" +version = "1.8.1" +description = "Django app + middleware to restrict access to all or sections of a Django project by client IP ranges" +authors = ["Tamas Szabo "] +maintainers = ["Tamas Szabo "] +license = "MIT" +repository = "https://github.com/sztamas/django-iprestrict-redux" +documentation = "http://django-iprestrict-redux.readthedocs.org/en/latest" +classifiers = [ + "Framework :: Django", + "Intended Audience :: Developers", + "License :: OSI Approved :: MIT License", + "Operating System :: OS Independent", + "Topic :: Software Development" +] + +packages = [ + { include = "iprestrict" } +] + +[tool.poetry.dependencies] +python = "^3.6" + +# Optional dependecies - extras +pycountry = {version = "^20.7.3", optional = true} +geoip2 = {version = "^4.0.2", optional = true} + +[tool.poetry.dev-dependencies] +Django = "^3.1" +tox = "^3.19.0" +pep8 = "^1.7.1" +flake8 = "^3.8.3" +Sphinx = "^3.2.1" +django-extensions = "^3.0.5" +Werkzeug = "^1.0.1" +pytest = "^6.0.1" +pytest-django = "^3.9.0" +black = "^20.8b1" +isort = {extras = ["colors"], version = "^5.4.2"} + +[tool.poetry.extras] +geoip = ["pycountry", "geoip2"] + +#[tool.isort] +#profile = "black" +#src_paths = ["iprestrict", "test"] + +[build-system] +requires = ["poetry>=0.12"] +build-backend = "poetry.masonry.api" diff --git a/requirements-2_2.txt b/requirements-2_2.txt deleted file mode 100644 index b0e8186..0000000 --- a/requirements-2_2.txt +++ /dev/null @@ -1,2 +0,0 @@ --r common-requirements.txt -Django>=2.2,<2.3 diff --git a/requirements-3_0.txt b/requirements-3_0.txt deleted file mode 100644 index 448d48f..0000000 --- a/requirements-3_0.txt +++ /dev/null @@ -1,2 +0,0 @@ --r common-requirements.txt -Django>=3.0,<3.1 diff --git a/requirements-3_1.txt b/requirements-3_1.txt deleted file mode 100644 index ff289fd..0000000 --- a/requirements-3_1.txt +++ /dev/null @@ -1,2 +0,0 @@ --r common-requirements.txt -Django>=3.1,<3.2 diff --git a/requirements.txt b/requirements.txt deleted file mode 120000 index 049af54..0000000 --- a/requirements.txt +++ /dev/null @@ -1 +0,0 @@ -requirements-3_1.txt \ No newline at end of file diff --git a/runtests.sh b/runtests.sh deleted file mode 100755 index 5ed8f3e..0000000 --- a/runtests.sh +++ /dev/null @@ -1,4 +0,0 @@ -#!/bin/sh - -export PYTHONPATH=. -exec django-admin.py test --settings=tests.test_settings tests diff --git a/setup.cfg b/setup.cfg deleted file mode 100644 index 3c6e79c..0000000 --- a/setup.cfg +++ /dev/null @@ -1,2 +0,0 @@ -[bdist_wheel] -universal=1 diff --git a/setup.py b/setup.py deleted file mode 100644 index 0aea082..0000000 --- a/setup.py +++ /dev/null @@ -1,71 +0,0 @@ -import os -import re -from setuptools import setup - - -def get_package_version(package): - version = re.compile(r"(?:__)?version(?:__)?\s*=\s\"(.*)\"", re.I) - initfile = os.path.join(os.path.dirname(__file__), package, "__init__.py") - for line in open(initfile): - m = version.match(line) - if m: - return m.group(1) - return "UNKNOWN" - - -def get_package_data(): - return [ - os.path.join(root, f) for d in ('templates', 'static') - for root, _, files in os.walk('iprestrict/{}'.format(d)) - for f in files] - - -setup( - name='django-iprestrict-redux', - version=get_package_version("iprestrict"), - description=('Django app + middleware to restrict access to all or sections of a ' - 'Django project by client IP ranges'), - long_description=('Django app + middleware to restrict access to all or sections of a ' - 'Django project by client IP ranges'), - author='Tamas Szabo, CCG - Murdoch University', - author_email='me@tamas-szabo.com', - url='https://github.com/sztamas/django-iprestrict-redux', - download_url='https://github.com/sztamas/django-iprestrict-redux/releases', - classifiers=[ - "Framework :: Django", - "Intended Audience :: Developers", - "License :: OSI Approved :: MIT License", - "Operating System :: OS Independent", - "Topic :: Software Development" - ], - packages=[ - 'iprestrict', - 'iprestrict.management', - 'iprestrict.management.commands', - 'iprestrict.migrations', - ], - package_data={ - 'iprestrict': get_package_data() - }, - include_package_data=True, - zip_safe=False, - install_requires=[ - 'Django>=2.2', - ], - extras_require={ - 'geoip': [ - 'pycountry==20.7.3', - 'geoip2==4.0.2', - ], - 'dev': [ - 'tox', - 'pep8', - 'flake8', - 'mock', - 'Sphinx', - 'django-extensions', - 'Werkzeug', - ], - }, - test_suite='tests.runtests.main', -) diff --git a/tests/__init__.py b/tests/__init__.py index 8b13789..e69de29 100644 --- a/tests/__init__.py +++ b/tests/__init__.py @@ -1 +0,0 @@ - diff --git a/tests/test_admin.py b/tests/test_admin.py index 41691a0..c29c080 100644 --- a/tests/test_admin.py +++ b/tests/test_admin.py @@ -9,7 +9,7 @@ class IPRangeFormTest(TestCase): def setUp(self): - self.all_group = models.IPGroup.objects.get(name='ALL') + self.all_group = models.IPGroup.objects.get(name="ALL") def test_empty(self): form_data = {} @@ -56,7 +56,9 @@ def test_cidr_prefix_length_invalid(self): form = admin.IPRangeForm(data=form_data) self.assertFalse(form.is_valid()) self.assertIn("cidr_prefix_length", form.errors) - self.assertIn("Must be a number between", "\n".join(form.errors["cidr_prefix_length"])) + self.assertIn( + "Must be a number between", "\n".join(form.errors["cidr_prefix_length"]) + ) def test_cidr_prefix_length_for_ipv6(self): form_data = { @@ -76,8 +78,9 @@ def test_ipv6_cidr_prefix_length_invalid(self): form = admin.IPRangeForm(data=form_data) self.assertFalse(form.is_valid()) self.assertIn("cidr_prefix_length", form.errors) - self.assertIn("Must be a number between", "\n".join(form.errors["cidr_prefix_length"])) - + self.assertIn( + "Must be a number between", "\n".join(form.errors["cidr_prefix_length"]) + ) def test_ip_types(self): form_data = { @@ -91,10 +94,24 @@ def test_ip_types(self): self.assertIn("__all__", form.errors) self.assertIn("same type", "\n".join(form.errors["__all__"])) + def test_last_ip_greater_than_first_ip(self): + form_data = { + "ip_group": self.all_group.pk, + "first_ip": "192.168.1.2", + "last_ip": "192.168.1.1", + } + form = admin.IPRangeForm(data=form_data) + self.assertFalse(form.is_valid()) + self.assertTrue(form.errors) + self.assertIn("__all__", form.errors) + self.assertIn("greater than", "\n".join(form.errors["__all__"])) + class IPLocationFormTest(TestCase): def setUp(self): - self.group = models.LocationBasedIPGroup.objects.create(name='Test Location Group') + self.group = models.LocationBasedIPGroup.objects.create( + name="Test Location Group" + ) def tearDown(self): self.group.delete() @@ -105,7 +122,7 @@ def with_country_codes(self, codes): "country_codes": codes, } - def assert_form_validity(self, codes, should_be_valid=True, msg=''): + def assert_form_validity(self, codes, should_be_valid=True, msg=""): form_data = self.with_country_codes(codes) form = admin.IPLocationForm(data=form_data) @@ -121,25 +138,37 @@ def test_empty_form_not_valid(self): self.assertFalse(form.is_valid()) def test_country_codes_validation(self): - self.assert_form_validity('', False, 'country_codes should be mandatory') - - form = self.assert_form_validity('AU', msg='AU should be a valid code') - self.assertEquals(form.cleaned_data['country_codes'], 'AU') - - form = self.assert_form_validity('au', msg='country codes are case insensitive') - self.assertEquals(form.cleaned_data['country_codes'], 'AU') - - form = self.assert_form_validity('$@! au 23454', msg='invalid characters should be stripped') - self.assertEquals(form.cleaned_data['country_codes'], 'AU') - - form = self.assert_form_validity('au, hu', msg='multiple countries should be ok') - self.assertEquals(form.cleaned_data['country_codes'], 'AU, HU') - - form = self.assert_form_validity('au123456789- @#$% hu', msg='any separator is ok') - self.assertEquals(form.cleaned_data['country_codes'], 'AU, HU') - - form = self.assert_form_validity('hu, au, br', msg='countries should be ordered') - self.assertEquals(form.cleaned_data['country_codes'], 'AU, BR, HU') - - self.assert_form_validity('HU, AU, ZZ', False, msg='invalid country codes should NOT be allowed') - self.assert_form_validity(NO_COUNTRY, msg='allow special country code for IPs wo country') + self.assert_form_validity("", False, "country_codes should be mandatory") + + form = self.assert_form_validity("AU", msg="AU should be a valid code") + self.assertEqual(form.cleaned_data["country_codes"], "AU") + + form = self.assert_form_validity("au", msg="country codes are case insensitive") + self.assertEqual(form.cleaned_data["country_codes"], "AU") + + form = self.assert_form_validity( + "$@! au 23454", msg="invalid characters should be stripped" + ) + self.assertEqual(form.cleaned_data["country_codes"], "AU") + + form = self.assert_form_validity( + "au, hu", msg="multiple countries should be ok" + ) + self.assertEqual(form.cleaned_data["country_codes"], "AU, HU") + + form = self.assert_form_validity( + "au123456789- @#$% hu", msg="any separator is ok" + ) + self.assertEqual(form.cleaned_data["country_codes"], "AU, HU") + + form = self.assert_form_validity( + "hu, au, br", msg="countries should be ordered" + ) + self.assertEqual(form.cleaned_data["country_codes"], "AU, BR, HU") + + self.assert_form_validity( + "HU, AU, ZZ", False, msg="invalid country codes should NOT be allowed" + ) + self.assert_form_validity( + NO_COUNTRY, msg="allow special country code for IPs wo country" + ) diff --git a/tests/test_commands.py b/tests/test_commands.py index 1b6fa7c..89f325f 100644 --- a/tests/test_commands.py +++ b/tests/test_commands.py @@ -1,28 +1,30 @@ # -*- coding: utf-8 -*- from __future__ import unicode_literals -from django.test import TestCase -from django.core.management.base import CommandError from django.core.management import call_command +from django.core.management.base import CommandError +from django.test import TestCase -from iprestrict import models from iprestrict import ip_utils as ipu +from iprestrict import models class AddIPToIPGroupTest(TestCase): - CMD = 'add_ip_to_group' + CMD = "add_ip_to_group" def setUp(self): - self.BLACKLIST = 'Blacklist' - self.IP1 = '192.168.222.1' - self.IP2 = '10.10.10.10' - self.IPv6 = '2001:db8:85a3::8a2e:0370:7334' + self.BLACKLIST = "Blacklist" + self.IP1 = "192.168.222.1" + self.IP2 = "10.10.10.10" + self.IPv6 = "2001:db8:85a3::8a2e:0370:7334" self.group = models.RangeBasedIPGroup.objects.create(name=self.BLACKLIST) - self.group = models.LocationBasedIPGroup.objects.create(name='locations') + self.group = models.LocationBasedIPGroup.objects.create(name="locations") def assertTooFewArguments(self, exception): - self.assertTrue('too few arguments' in str(exception) or - 'the following arguments are required' in str(exception)) + self.assertTrue( + "too few arguments" in str(exception) + or "the following arguments are required" in str(exception) + ) def test_arg_ip_group_is_required(self): with self.assertRaises(CommandError) as cm: @@ -36,25 +38,27 @@ def test_args_ip_group_and_at_least_one_ip_is_required(self): def test_arg_ip_group_has_to_exist(self): with self.assertRaises(CommandError) as cm: - call_command(self.CMD, 'DOES NOT EXIST', self.IP1) + call_command(self.CMD, "DOES NOT EXIST", self.IP1) self.assertIn("doesn't exist", str(cm.exception)) def test_arg_ip_group_has_to_be_range_based(self): with self.assertRaises(CommandError) as cm: - call_command(self.CMD, 'locations', self.IP1) - self.assertIn('only to a Range based', str(cm.exception)) + call_command(self.CMD, "locations", self.IP1) + self.assertIn("only to a Range based", str(cm.exception)) def test_arg_ip_has_to_be_a_valid_ip_address(self): with self.assertRaises(CommandError) as cm: - call_command(self.CMD, self.BLACKLIST, self.IP1.replace('222', '256')) - self.assertIn('not a valid IPv4 or IPv6', str(cm.exception)) + call_command(self.CMD, self.BLACKLIST, self.IP1.replace("222", "256")) + self.assertIn("not a valid IPv4 or IPv6", str(cm.exception)) def test_arg_ip_group_name_is_caseinsensitive(self): - call_command(self.CMD, 'blacklist', self.IP1) + call_command(self.CMD, "blacklist", self.IP1) def test_should_add_single_ip_to_ip_group(self): call_command(self.CMD, self.BLACKLIST, self.IP1) - ip_range = models.IPRange.objects.get(ip_group__name=self.BLACKLIST, first_ip=self.IP1) + ip_range = models.IPRange.objects.get( + ip_group__name=self.BLACKLIST, first_ip=self.IP1 + ) self.assertIsNone(ip_range.cidr_prefix_length) self.assertIsNone(ip_range.last_ip) @@ -77,10 +81,16 @@ def test_should_NOT_add_ip_if_ip_already_in_group(self): call_command(self.CMD, self.BLACKLIST, self.IP1) ip_count = models.IPRange.objects.filter(ip_group__name=self.BLACKLIST).count() call_command(self.CMD, self.BLACKLIST, self.IP1) - ip_count_same = models.IPRange.objects.filter(ip_group__name=self.BLACKLIST).count() + ip_count_same = models.IPRange.objects.filter( + ip_group__name=self.BLACKLIST + ).count() call_command(self.CMD, self.BLACKLIST, self.IP1, self.IP2) - ip_count_adds_other = models.IPRange.objects.filter(ip_group__name=self.BLACKLIST).count() + ip_count_adds_other = models.IPRange.objects.filter( + ip_group__name=self.BLACKLIST + ).count() self.assertEqual(ip_count, 1) self.assertEqual(ip_count_same, 1, "shouldn't add the same IP again") - self.assertEqual(ip_count_adds_other, 2, "should add the other IP but not the same IP") + self.assertEqual( + ip_count_adds_other, 2, "should add the other IP but not the same IP" + ) diff --git a/tests/test_ip_utils.py b/tests/test_ip_utils.py index e71ac6d..fed4051 100644 --- a/tests/test_ip_utils.py +++ b/tests/test_ip_utils.py @@ -2,53 +2,56 @@ from __future__ import unicode_literals from django.test import TestCase + from iprestrict import ip_utils as ipu class GetVersionTest(TestCase): def test_IPV4_if_no_colons(self): - self.assertEqual(ipu.IPv4, ipu.get_version('192.168.1.1')) + self.assertEqual(ipu.IPv4, ipu.get_version("192.168.1.1")) def test_IPV6_if_both_colons_and_dots(self): - self.assertEqual(ipu.IPv6, ipu.get_version('::ffff:129.144.52.38')) + self.assertEqual(ipu.IPv6, ipu.get_version("::ffff:129.144.52.38")) def test_IPV6_if_only_colons(self): - self.assertEqual(ipu.IPv6, ipu.get_version('::')) - self.assertEqual(ipu.IPv6, ipu.get_version('::1')) + self.assertEqual(ipu.IPv6, ipu.get_version("::")) + self.assertEqual(ipu.IPv6, ipu.get_version("::1")) class IPToNumberTest(TestCase): def test_ip_to_number_conversions(self): - self.assertEqual(0, ipu.to_number('0.0.0.0')) - self.assertEqual(1, ipu.to_number('0.0.0.1')) - self.assertEqual(256, ipu.to_number('0.0.1.0')) - self.assertEqual(65537, ipu.to_number('0.1.0.1')) - self.assertEqual(16842753, ipu.to_number('1.1.0.1')) + self.assertEqual(0, ipu.to_number("0.0.0.0")) + self.assertEqual(1, ipu.to_number("0.0.0.1")) + self.assertEqual(256, ipu.to_number("0.0.1.0")) + self.assertEqual(65537, ipu.to_number("0.1.0.1")) + self.assertEqual(16842753, ipu.to_number("1.1.0.1")) def test_ip_to_number_conversions_ipv6(self): - self.assertEqual(1, ipu.to_number('0:0:0:0:0:0:0:1')) - self.assertEqual(10, ipu.to_number('0:0:0:0:0:0:0:a')) - self.assertEqual((2 ** 16) ** 7 + 10, ipu.to_number('1:0:0:0:0:0:0:a')) + self.assertEqual(1, ipu.to_number("0:0:0:0:0:0:0:1")) + self.assertEqual(10, ipu.to_number("0:0:0:0:0:0:0:a")) + self.assertEqual((2 ** 16) ** 7 + 10, ipu.to_number("1:0:0:0:0:0:0:a")) # Zero collapse syntax - self.assertEqual(0, ipu.to_number('::')) - self.assertEqual(1, ipu.to_number('::1')) - self.assertEqual((2 ** 16) ** 7 + 10, ipu.to_number('1::a')) + self.assertEqual(0, ipu.to_number("::")) + self.assertEqual(1, ipu.to_number("::1")) + self.assertEqual((2 ** 16) ** 7 + 10, ipu.to_number("1::a")) # Mixed syntax - self.assertEqual(1, ipu.to_number('::0.0.0.1')) - self.assertEqual((2 ** 16) ** 7 + 10, ipu.to_number('1::0.0.0.10')) + self.assertEqual(1, ipu.to_number("::0.0.0.1")) + self.assertEqual((2 ** 16) ** 7 + 10, ipu.to_number("1::0.0.0.10")) class NumberToIPTest(TestCase): def test_number_to_ip_conversions(self): - self.assertEqual('0.0.0.0', ipu.to_ip(0)) - self.assertEqual('0.0.0.1', ipu.to_ip(1)) - self.assertEqual('0.0.1.0', ipu.to_ip(256)) - self.assertEqual('0.1.0.1', ipu.to_ip(65537)) - self.assertEqual('1.1.0.1', ipu.to_ip(16842753)) + self.assertEqual("0.0.0.0", ipu.to_ip(0)) + self.assertEqual("0.0.0.1", ipu.to_ip(1)) + self.assertEqual("0.0.1.0", ipu.to_ip(256)) + self.assertEqual("0.1.0.1", ipu.to_ip(65537)) + self.assertEqual("1.1.0.1", ipu.to_ip(16842753)) def test_number_to_ipv6_conversions(self): - self.assertEqual('0:0:0:0:0:0:0:0', ipu.to_ip(0, version=ipu.IPv6)) - self.assertEqual('0:0:0:0:0:0:0:1', ipu.to_ip(1, version=ipu.IPv6)) - self.assertEqual('1:0:0:0:0:0:0:a', ipu.to_ip((2 ** 16) ** 7 + 10, version=ipu.IPv6)) + self.assertEqual("0:0:0:0:0:0:0:0", ipu.to_ip(0, version=ipu.IPv6)) + self.assertEqual("0:0:0:0:0:0:0:1", ipu.to_ip(1, version=ipu.IPv6)) + self.assertEqual( + "1:0:0:0:0:0:0:a", ipu.to_ip((2 ** 16) ** 7 + 10, version=ipu.IPv6) + ) diff --git a/tests/test_middleware.py b/tests/test_middleware.py index 14746b6..bd6fe0d 100644 --- a/tests/test_middleware.py +++ b/tests/test_middleware.py @@ -1,56 +1,55 @@ # -*- coding: utf-8 -*- from __future__ import unicode_literals +from django.core import exceptions from django.test import TestCase from django.test.client import RequestFactory from django.test.utils import override_settings -from django.core import exceptions - from iprestrict import models -from iprestrict import restrictor from iprestrict.middleware import IPRestrictMiddleware -LOCAL_IP = '192.168.1.1' -PROXY = '1.1.1.1' +LOCAL_IP = "192.168.1.1" +PROXY = "1.1.1.1" class MiddlewareRestrictsTest(TestCase): - ''' + """ When the middleware is enabled it should restrict all IPs(but localhost)/URLs by default. - ''' + """ + def setUp(self): models.ReloadRulesRequest.request_reload() def assert_url_is_restricted(self, url): - response = self.client.get(url, REMOTE_ADDR = LOCAL_IP) + response = self.client.get(url, REMOTE_ADDR=LOCAL_IP) self.assertEqual(response.status_code, 403) def assert_ip_is_restricted(self, ip): - response = self.client.get('', REMOTE_ADDR = ip) + response = self.client.get("", REMOTE_ADDR=ip) self.assertEqual(response.status_code, 403) def test_middleware_restricts_every_url(self): - self.assert_url_is_restricted('') - self.assert_url_is_restricted('/every') - self.assert_url_is_restricted('/url') - self.assert_url_is_restricted('/is_restricted') - self.assert_url_is_restricted('/every/url/is_restricted') + self.assert_url_is_restricted("") + self.assert_url_is_restricted("/every") + self.assert_url_is_restricted("/url") + self.assert_url_is_restricted("/is_restricted") + self.assert_url_is_restricted("/every/url/is_restricted") def test_middleware_restricts_ips(self): - self.assert_ip_is_restricted('192.168.1.1') - self.assert_ip_is_restricted('10.10.10.1') - self.assert_ip_is_restricted('169.254.0.1') + self.assert_ip_is_restricted("192.168.1.1") + self.assert_ip_is_restricted("10.10.10.1") + self.assert_ip_is_restricted("169.254.0.1") def test_middleware_allows_localhost(self): - response = self.client.get('/some/url', REMOTE_ADDR = '127.0.0.1') + response = self.client.get("/some/url", REMOTE_ADDR="127.0.0.1") self.assertEqual(response.status_code, 404) def create_ip_allow_rule(ip=LOCAL_IP): - localip = models.RangeBasedIPGroup.objects.create(name='localip') + localip = models.RangeBasedIPGroup.objects.create(name="localip") models.IPRange.objects.create(ip_group=localip, first_ip=LOCAL_IP) - models.Rule.objects.create(url_pattern='ALL', ip_group = localip, action='A') + models.Rule.objects.create(url_pattern="ALL", ip_group=localip, action="A") class MiddlewareAllowsTest(TestCase): @@ -59,24 +58,24 @@ def setUp(self): models.ReloadRulesRequest.request_reload() def test_middleware_allows_localhost(self): - response = self.client.get('') + response = self.client.get("") self.assertEqual(response.status_code, 404) def test_middleware_allows_ip_just_added(self): - response = self.client.get('', REMOTE_ADDR = LOCAL_IP) + response = self.client.get("", REMOTE_ADDR=LOCAL_IP) self.assertEqual(response.status_code, 404) def test_middleware_restricts_other_ip(self): - response = self.client.get('', REMOTE_ADDR = '10.1.1.1') + response = self.client.get("", REMOTE_ADDR="10.1.1.1") self.assertEqual(response.status_code, 403) @override_settings(IPRESTRICT_TRUSTED_PROXIES=(PROXY,), ALLOW_PROXIES=False) def test_middleware_allows_if_proxy_is_trusted(self): - response = self.client.get('', REMOTE_ADDR = PROXY, HTTP_X_FORWARDED_FOR= LOCAL_IP) + response = self.client.get("", REMOTE_ADDR=PROXY, HTTP_X_FORWARDED_FOR=LOCAL_IP) self.assertEqual(response.status_code, 404) def test_middleware_restricts_if_proxy_is_not_trusted(self): - response = self.client.get('', REMOTE_ADDR = PROXY, HTTP_X_FORWARDED_FOR = LOCAL_IP) + response = self.client.get("", REMOTE_ADDR=PROXY, HTTP_X_FORWARDED_FOR=LOCAL_IP) self.assertEqual(response.status_code, 403) @@ -86,69 +85,79 @@ def setUp(self): def test_reload_with_custom_command(self): from django.core.management import call_command - call_command('reload_rules', verbosity=0) - response = self.client.get('', REMOTE_ADDR = LOCAL_IP) + call_command("reload_rules", verbosity=0) + + response = self.client.get("", REMOTE_ADDR=LOCAL_IP) self.assertEqual(response.status_code, 404) +def dummy_get_response(): + return None + + class MiddlewareExtractClientIpTest(TestCase): def setUp(self): - self.middleware = IPRestrictMiddleware() + self.middleware = IPRestrictMiddleware(dummy_get_response) self.factory = RequestFactory() - + def test_remote_addr_only(self): - self.middleware = IPRestrictMiddleware() - request = self.factory.get('', REMOTE_ADDR=LOCAL_IP) + request = self.factory.get("", REMOTE_ADDR=LOCAL_IP) client_ip = self.middleware.extract_client_ip(request) - self.assertEquals(client_ip, LOCAL_IP) + self.assertEqual(client_ip, LOCAL_IP) def test_remote_addr_empty(self): - self.middleware = IPRestrictMiddleware() - request = self.factory.get('', REMOTE_ADDR='') + request = self.factory.get("", REMOTE_ADDR="") client_ip = self.middleware.extract_client_ip(request) - self.assertEquals(client_ip, '') + self.assertEqual(client_ip, "") @override_settings(IPRESTRICT_TRUSTED_PROXIES=(PROXY,)) def test_single_proxy(self): - self.middleware = IPRestrictMiddleware() - request = self.factory.get('', REMOTE_ADDR=PROXY, HTTP_X_FORWARDED_FOR = LOCAL_IP) + self.middleware = IPRestrictMiddleware(dummy_get_response) + request = self.factory.get("", REMOTE_ADDR=PROXY, HTTP_X_FORWARDED_FOR=LOCAL_IP) client_ip = self.middleware.extract_client_ip(request) - self.assertEquals(client_ip, LOCAL_IP) + self.assertEqual(client_ip, LOCAL_IP) - @override_settings(IPRESTRICT_TRUSTED_PROXIES=(PROXY,'2.2.2.2','4.4.4.4')) + @override_settings(IPRESTRICT_TRUSTED_PROXIES=(PROXY, "2.2.2.2", "4.4.4.4")) def test_multiple_proxies_one_not_trusted(self): - self.middleware = IPRestrictMiddleware() - proxies = ['2.2.2.2', '3.3.3.3', '4.4.4.4'] - request = self.factory.get('', REMOTE_ADDR=PROXY, - HTTP_X_FORWARDED_FOR = ', '.join([LOCAL_IP] + proxies)) - + self.middleware = IPRestrictMiddleware(dummy_get_response) + proxies = ["2.2.2.2", "3.3.3.3", "4.4.4.4"] + request = self.factory.get( + "", REMOTE_ADDR=PROXY, HTTP_X_FORWARDED_FOR=", ".join([LOCAL_IP] + proxies) + ) + try: - client_ip = self.middleware.extract_client_ip(request) + _ = self.middleware.extract_client_ip(request) except exceptions.PermissionDenied: pass else: - self.fail('Should raise PermissionDenied exception') + self.fail("Should raise PermissionDenied exception") - @override_settings(IPRESTRICT_TRUSTED_PROXIES=(PROXY,'2.2.2.2','3.3.3.3', '4.4.4.4')) + @override_settings( + IPRESTRICT_TRUSTED_PROXIES=(PROXY, "2.2.2.2", "3.3.3.3", "4.4.4.4") + ) def test_multiple_proxies_all_trusted(self): - self.middleware = IPRestrictMiddleware() - proxies = ['2.2.2.2', '3.3.3.3', '4.4.4.4'] - request = self.factory.get('', REMOTE_ADDR=PROXY, - HTTP_X_FORWARDED_FOR = ', '.join([LOCAL_IP] + proxies)) - + self.middleware = IPRestrictMiddleware(dummy_get_response) + proxies = ["2.2.2.2", "3.3.3.3", "4.4.4.4"] + request = self.factory.get( + "", REMOTE_ADDR=PROXY, HTTP_X_FORWARDED_FOR=", ".join([LOCAL_IP] + proxies) + ) + client_ip = self.middleware.extract_client_ip(request) - self.assertEquals(client_ip, LOCAL_IP) + self.assertEqual(client_ip, LOCAL_IP) - @override_settings(IPRESTRICT_TRUSTED_PROXIES=(PROXY,), IPRESTRICT_TRUST_ALL_PROXIES=True) + @override_settings( + IPRESTRICT_TRUSTED_PROXIES=(PROXY,), IPRESTRICT_TRUST_ALL_PROXIES=True + ) def test_trust_all_proxies_on(self): - self.middleware = IPRestrictMiddleware() - proxies = ['1.1.1.1', '2.2.2.2', '3.3.3.3', '4.4.4.4'] - request = self.factory.get('', REMOTE_ADDR=PROXY, - HTTP_X_FORWARDED_FOR = ', '.join([LOCAL_IP] + proxies)) + self.middleware = IPRestrictMiddleware(dummy_get_response) + proxies = ["1.1.1.1", "2.2.2.2", "3.3.3.3", "4.4.4.4"] + request = self.factory.get( + "", REMOTE_ADDR=PROXY, HTTP_X_FORWARDED_FOR=", ".join([LOCAL_IP] + proxies) + ) client_ip = self.middleware.extract_client_ip(request) - self.assertEquals(client_ip, LOCAL_IP) + self.assertEqual(client_ip, LOCAL_IP) diff --git a/tests/test_models.py b/tests/test_models.py index 0c1a3ab..1841959 100644 --- a/tests/test_models.py +++ b/tests/test_models.py @@ -1,172 +1,179 @@ # -*- coding: utf-8 -*- from __future__ import unicode_literals +from unittest import mock + from django.test import TestCase -import mock -from django.http import HttpResponseForbidden -import iprestrict from iprestrict import models class RuleTest(TestCase): def test_restriction_methods_for_allow_rule(self): - rule = models.Rule(url_pattern='', action='A') + rule = models.Rule(url_pattern="", action="A") self.assertTrue(rule.is_allowed()) self.assertFalse(rule.is_restricted()) def test_restriction_methods_for_deny_rule(self): - rule = models.Rule(url_pattern='', action='D') + rule = models.Rule(url_pattern="", action="D") self.assertFalse(rule.is_allowed()) self.assertTrue(rule.is_restricted()) def test_matches_url_pattern_regex(self): - rule = models.Rule(url_pattern='^/pre/[a-d]+[/]?$') - self.assertTrue(rule.matches_url('/pre/a/')) - self.assertTrue(rule.matches_url('/pre/a')) - self.assertFalse(rule.matches_url('/pre/e/')) - self.assertFalse(rule.matches_url('/pre/a//')) + rule = models.Rule(url_pattern="^/pre/[a-d]+[/]?$") + self.assertTrue(rule.matches_url("/pre/a/")) + self.assertTrue(rule.matches_url("/pre/a")) + self.assertFalse(rule.matches_url("/pre/e/")) + self.assertFalse(rule.matches_url("/pre/a//")) def test_rank_is_automatically_assigned_on_creation(self): - rule1 = models.Rule.objects.create(url_pattern='1', action='A') - rule2 = models.Rule.objects.create(url_pattern='2', action='A') - rule10 = models.Rule.objects.create(url_pattern='10', action='A', rank=10) - rule11 = models.Rule.objects.create(url_pattern='10', action='A') - self.assertEquals(rule1.rank, 1) - self.assertEquals(rule2.rank, 2) - self.assertEquals(rule10.rank, 10) - self.assertEquals(rule11.rank, 11) + rule1 = models.Rule.objects.create(url_pattern="1", action="A") + rule2 = models.Rule.objects.create(url_pattern="2", action="A") + rule10 = models.Rule.objects.create(url_pattern="10", action="A", rank=10) + rule11 = models.Rule.objects.create(url_pattern="10", action="A") + self.assertEqual(rule1.rank, 1) + self.assertEqual(rule2.rank, 2) + self.assertEqual(rule10.rank, 10) + self.assertEqual(rule11.rank, 11) def test_rank_is_not_changed_on_update(self): - rule1 = models.Rule.objects.create(url_pattern='1', action='A') - rule2 = models.Rule.objects.create(url_pattern='2', action='A') - rule1.action = 'D' + rule1 = models.Rule.objects.create(url_pattern="1", action="A") + _ = models.Rule.objects.create(url_pattern="2", action="A") + rule1.action = "D" rule1.save() - self.assertEquals(rule1.rank, 1) + self.assertEqual(rule1.rank, 1) class RuleWithSampleRulesTests(TestCase): def setUp(self): - self.rule1 = models.Rule.objects.create(url_pattern='1', action='A') - self.rule2 = models.Rule.objects.create(url_pattern='2', action='A') + self.rule1 = models.Rule.objects.create(url_pattern="1", action="A") + self.rule2 = models.Rule.objects.create(url_pattern="2", action="A") def test_default_order_is_by_rank(self): rules = models.Rule.objects.all() - self.assertEqual(rules[0].url_pattern, '1') - self.assertEqual(rules[1].url_pattern, '2') + self.assertEqual(rules[0].url_pattern, "1") + self.assertEqual(rules[1].url_pattern, "2") def test_move_up(self): self.rule2.move_up() rules = models.Rule.objects.all() - self.assertEqual(rules[0].url_pattern, '2') - self.assertEqual(rules[1].url_pattern, '1') + self.assertEqual(rules[0].url_pattern, "2") + self.assertEqual(rules[1].url_pattern, "1") def test_move_up_first_rule_does_nothing(self): self.rule1.move_up() rules = models.Rule.objects.all() - self.assertEqual(rules[0].url_pattern, '1') - self.assertEqual(rules[1].url_pattern, '2') + self.assertEqual(rules[0].url_pattern, "1") + self.assertEqual(rules[1].url_pattern, "2") def test_move_down(self): self.rule1.move_down() rules = models.Rule.objects.all() - self.assertEqual(rules[0].url_pattern, '2') - self.assertEqual(rules[1].url_pattern, '1') + self.assertEqual(rules[0].url_pattern, "2") + self.assertEqual(rules[1].url_pattern, "1") def test_move_down_below_default_rule(self): self.rule1.move_down() self.rule1.move_down() rules = models.Rule.objects.all() - self.assertEqual(rules[0].url_pattern, '2') - self.assertEqual(rules[1].url_pattern, 'ALL') - self.assertEqual(rules[2].url_pattern, '1') + self.assertEqual(rules[0].url_pattern, "2") + self.assertEqual(rules[1].url_pattern, "ALL") + self.assertEqual(rules[2].url_pattern, "1") class RangeBasedIPGroupTest(TestCase): def test_first_ip_group_is_all(self): - '''An IP definition matching all should be inserted by default''' - all_group = models.RangeBasedIPGroup.objects.get(name='ALL') - self.assertTrue(all_group.matches('192.168.1.1')) - self.assertTrue(all_group.matches('200.200.200.200')) - self.assertTrue(all_group.matches('1.2.3.4')) + """An IP definition matching all should be inserted by default""" + all_group = models.RangeBasedIPGroup.objects.get(name="ALL") + self.assertTrue(all_group.matches("192.168.1.1")) + self.assertTrue(all_group.matches("200.200.200.200")) + self.assertTrue(all_group.matches("1.2.3.4")) def test_matches_with_ranges(self): - ipgroup = models.RangeBasedIPGroup.objects.create(name='Local IPs') - models.IPRange.objects.create(ip_group=ipgroup, first_ip='192.168.1.1', last_ip='192.168.1.10') - models.IPRange.objects.create(ip_group=ipgroup, first_ip='192.168.1.100', last_ip='192.168.1.110') + ipgroup = models.RangeBasedIPGroup.objects.create(name="Local IPs") + models.IPRange.objects.create( + ip_group=ipgroup, first_ip="192.168.1.1", last_ip="192.168.1.10" + ) + models.IPRange.objects.create( + ip_group=ipgroup, first_ip="192.168.1.100", last_ip="192.168.1.110" + ) ipgroup.load_ranges() - self.assertTrue(ipgroup.matches('192.168.1.1')) - self.assertTrue(ipgroup.matches('192.168.1.5')) - self.assertTrue(ipgroup.matches('192.168.1.10')) - self.assertTrue(ipgroup.matches('192.168.1.105')) - self.assertTrue(ipgroup.matches('192.168.1.100')) - self.assertFalse(ipgroup.matches('192.168.1.0')) - self.assertFalse(ipgroup.matches('192.168.1.11')) - self.assertFalse(ipgroup.matches('192.168.1.99')) + self.assertTrue(ipgroup.matches("192.168.1.1")) + self.assertTrue(ipgroup.matches("192.168.1.5")) + self.assertTrue(ipgroup.matches("192.168.1.10")) + self.assertTrue(ipgroup.matches("192.168.1.105")) + self.assertTrue(ipgroup.matches("192.168.1.100")) + self.assertFalse(ipgroup.matches("192.168.1.0")) + self.assertFalse(ipgroup.matches("192.168.1.11")) + self.assertFalse(ipgroup.matches("192.168.1.99")) def test_ipv6_and_ip4_are_separated(self): - ipgroup = models.RangeBasedIPGroup.objects.create(name='Local IPs') - models.IPRange.objects.create(ip_group=ipgroup, first_ip='::1') - models.IPRange.objects.create(ip_group=ipgroup, first_ip='0.0.0.2') + ipgroup = models.RangeBasedIPGroup.objects.create(name="Local IPs") + models.IPRange.objects.create(ip_group=ipgroup, first_ip="::1") + models.IPRange.objects.create(ip_group=ipgroup, first_ip="0.0.0.2") ipgroup.load_ranges() - self.assertFalse(ipgroup.matches('0.0.0.1')) - self.assertTrue(ipgroup.matches('0.0.0.2')) - self.assertFalse(ipgroup.matches('::2')) - self.assertTrue(ipgroup.matches('::1')) + self.assertFalse(ipgroup.matches("0.0.0.1")) + self.assertTrue(ipgroup.matches("0.0.0.2")) + self.assertFalse(ipgroup.matches("::2")) + self.assertTrue(ipgroup.matches("::1")) def test_matches_with_subnets(self): - ipgroup = models.RangeBasedIPGroup.objects.create(name='Local IPs') - models.IPRange.objects.create(ip_group=ipgroup, first_ip='192.168.1.0', cidr_prefix_length=30) + ipgroup = models.RangeBasedIPGroup.objects.create(name="Local IPs") + models.IPRange.objects.create( + ip_group=ipgroup, first_ip="192.168.1.0", cidr_prefix_length=30 + ) ipgroup.load_ranges() - self.assertTrue(ipgroup.matches('192.168.1.0')) - self.assertTrue(ipgroup.matches('192.168.1.1')) - self.assertTrue(ipgroup.matches('192.168.1.2')) - self.assertTrue(ipgroup.matches('192.168.1.3')) - self.assertFalse(ipgroup.matches('192.168.0.255')) - self.assertFalse(ipgroup.matches('192.168.1.4')) + self.assertTrue(ipgroup.matches("192.168.1.0")) + self.assertTrue(ipgroup.matches("192.168.1.1")) + self.assertTrue(ipgroup.matches("192.168.1.2")) + self.assertTrue(ipgroup.matches("192.168.1.3")) + self.assertFalse(ipgroup.matches("192.168.0.255")) + self.assertFalse(ipgroup.matches("192.168.1.4")) def test_matches_subnet_first_ip_not_correct(self): - ipgroup = models.RangeBasedIPGroup.objects.create(name='Local IPs') - models.IPRange.objects.create(ip_group=ipgroup, - first_ip='192.168.1.2', # Should be '192.168.1.1' - cidr_prefix_length=30) + ipgroup = models.RangeBasedIPGroup.objects.create(name="Local IPs") + models.IPRange.objects.create( + ip_group=ipgroup, + first_ip="192.168.1.2", # Should be '192.168.1.1' + cidr_prefix_length=30, + ) ipgroup.load_ranges() - self.assertTrue(ipgroup.matches('192.168.1.0')) - self.assertTrue(ipgroup.matches('192.168.1.1')) - self.assertTrue(ipgroup.matches('192.168.1.2')) - self.assertTrue(ipgroup.matches('192.168.1.3')) - self.assertFalse(ipgroup.matches('192.168.0.255')) - self.assertFalse(ipgroup.matches('192.168.1.4')) + self.assertTrue(ipgroup.matches("192.168.1.0")) + self.assertTrue(ipgroup.matches("192.168.1.1")) + self.assertTrue(ipgroup.matches("192.168.1.2")) + self.assertTrue(ipgroup.matches("192.168.1.3")) + self.assertFalse(ipgroup.matches("192.168.0.255")) + self.assertFalse(ipgroup.matches("192.168.1.4")) class LocationBasedIPGroupTest(TestCase): def setUp(self): - self.ipgroup = models.LocationBasedIPGroup.objects.create(name='Test Location') - models.IPLocation.objects.create(ip_group=self.ipgroup, country_codes='AU, HU') - models.IPLocation.objects.create(ip_group=self.ipgroup, country_codes='BR') + self.ipgroup = models.LocationBasedIPGroup.objects.create(name="Test Location") + models.IPLocation.objects.create(ip_group=self.ipgroup, country_codes="AU, HU") + models.IPLocation.objects.create(ip_group=self.ipgroup, country_codes="BR") def test_matches_ips(self): - with mock.patch('iprestrict.models.geoip') as m: + with mock.patch("iprestrict.models.geoip") as m: self.ipgroup.load_locations() # instructing geoip to return 'AU' for this IP - m.country_code.return_value = 'AU' - self.assertTrue(self.ipgroup.matches('192.168.1.1')) + m.country_code.return_value = "AU" + self.assertTrue(self.ipgroup.matches("192.168.1.1")) # instructing geoip to return 'HU' for this IP, etc. - m.country_code.return_value = 'HU' - self.assertTrue(self.ipgroup.matches('192.168.1.2')) + m.country_code.return_value = "HU" + self.assertTrue(self.ipgroup.matches("192.168.1.2")) - m.country_code.return_value = 'BR' - self.assertTrue(self.ipgroup.matches('192.168.1.3')) + m.country_code.return_value = "BR" + self.assertTrue(self.ipgroup.matches("192.168.1.3")) - m.country_code.return_value = 'FR' - self.assertFalse(self.ipgroup.matches('10.1.1.1')) + m.country_code.return_value = "FR" + self.assertFalse(self.ipgroup.matches("10.1.1.1")) diff --git a/tests/test_reloading.py b/tests/test_reloading.py index 5484557..78a68ea 100644 --- a/tests/test_reloading.py +++ b/tests/test_reloading.py @@ -1,48 +1,52 @@ # -*- coding: utf-8 -*- from __future__ import unicode_literals -from django.test import TestCase from django.contrib.auth.models import User from django.core.management import call_command +from django.test import TestCase from iprestrict import models class ReloadByViewTest(TestCase): - IP = '192.168.1.1' + IP = "192.168.1.1" def setUp(self): - admin = User.objects.create_user('admin', 'admin@nohost.org', 'pass') + admin = User.objects.create_user("admin", "admin@nohost.org", "pass") admin.is_staff = True admin.is_superuser = True admin.save() models.ReloadRulesRequest.request_reload() def add_allow_rule(self): - localip = models.RangeBasedIPGroup.objects.create(name='Local IP') + localip = models.RangeBasedIPGroup.objects.create(name="Local IP") models.IPRange.objects.create(ip_group=localip, first_ip=self.IP) - models.Rule.objects.create(url_pattern='ALL', ip_group = localip, action='A') + models.Rule.objects.create(url_pattern="ALL", ip_group=localip, action="A") def reload_rules(self): - self.client.login(username='admin', password='pass') - self.client.get('/iprestrict/reload_rules') + self.client.login(username="admin", password="pass") + self.client.get("/iprestrict/reload_rules/") def test_reload_view(self): # 1 - response = self.client.get('', REMOTE_ADDR = self.IP) - self.assertEqual(response.status_code, 403, 'Should be restricted') + response = self.client.get("", REMOTE_ADDR=self.IP) + self.assertEqual(response.status_code, 403, "Should be restricted") # 2 self.add_allow_rule() - response = self.client.get('', REMOTE_ADDR = self.IP) - self.assertEqual(response.status_code, 403, 'Should still be restricted - rules have not been reloaded') + response = self.client.get("", REMOTE_ADDR=self.IP) + self.assertEqual( + response.status_code, + 403, + "Should still be restricted - rules have not been reloaded", + ) # 3 reload rules self.reload_rules() - response = self.client.get('', REMOTE_ADDR = self.IP) - self.assertEqual(response.status_code, 404, 'Should be allowed now') + response = self.client.get("", REMOTE_ADDR=self.IP) + self.assertEqual(response.status_code, 404, "Should be allowed now") class ReloadByCommand(ReloadByViewTest): def reload_rules(self): - call_command('reload_rules') + call_command("reload_rules") diff --git a/tests/test_restrictor.py b/tests/test_restrictor.py index 136f90c..c18597a 100644 --- a/tests/test_restrictor.py +++ b/tests/test_restrictor.py @@ -1,113 +1,111 @@ # -*- coding: utf-8 -*- from __future__ import unicode_literals +from unittest import mock + from django.test import TestCase -import mock import iprestrict from iprestrict import models - -IP = '10.1.1.1' -SOME_URL = '/some/url' +IP = "10.1.1.1" +SOME_URL = "/some/url" class IPRestrictorDefaultRulesTest(TestCase): - def test_restrictor_restricts_all_by_default(self): restr = iprestrict.IPRestrictor() - self.assertTrue(restr.is_restricted('', IP)) + self.assertTrue(restr.is_restricted("", IP)) self.assertTrue(restr.is_restricted(SOME_URL, IP)) def test_allow_all_ips_for_one_url(self): - models.Rule.objects.create(url_pattern=SOME_URL, action='A', rank=1) + models.Rule.objects.create(url_pattern=SOME_URL, action="A", rank=1) restr = iprestrict.IPRestrictor() self.assertFalse(restr.is_restricted(SOME_URL, IP)) - self.assertFalse(restr.is_restricted(SOME_URL, '192.168.1.1')) + self.assertFalse(restr.is_restricted(SOME_URL, "192.168.1.1")) def test_caches_rules(self): restr = iprestrict.IPRestrictor() - models.Rule.objects.create(url_pattern=SOME_URL, action='A', rank=1) + models.Rule.objects.create(url_pattern=SOME_URL, action="A", rank=1) # Restrictor shouldn't read the rules dynamically self.assertTrue(restr.is_restricted(SOME_URL, IP)) def test_reload_rules(self): restr = iprestrict.IPRestrictor() - models.Rule.objects.create(url_pattern=SOME_URL, action='A', rank=1) + models.Rule.objects.create(url_pattern=SOME_URL, action="A", rank=1) restr.reload_rules() self.assertFalse(restr.is_restricted(SOME_URL, IP)) class IPRestrictorNoRulesTest(TestCase): - def setUp(self): models.Rule.objects.all().delete() self.restrictor = iprestrict.IPRestrictor() def test_restrictor_does_not_do_anything_with_empty_rules_table(self): - self.assertFalse(self.restrictor.is_restricted('', IP)) - self.assertFalse(self.restrictor.is_restricted(SOME_URL, '192.168.1.1')) + self.assertFalse(self.restrictor.is_restricted("", IP)) + self.assertFalse(self.restrictor.is_restricted(SOME_URL, "192.168.1.1")) def test_restrictor_considers_rules_by_rank(self): - rule = models.Rule.objects.create(url_pattern='ALL', action='A', rank=2) - rule = models.Rule.objects.create(url_pattern='/admin[/].*', action='D', rank=1) + _ = models.Rule.objects.create(url_pattern="ALL", action="A", rank=2) + _ = models.Rule.objects.create(url_pattern="/admin[/].*", action="D", rank=1) restr = iprestrict.IPRestrictor() - self.assertFalse(restr.is_restricted('/some/url', IP)) - self.assertTrue(restr.is_restricted('/admin/', IP)) - self.assertTrue(restr.is_restricted('/admin/somepage', IP)) + self.assertFalse(restr.is_restricted("/some/url", IP)) + self.assertTrue(restr.is_restricted("/admin/", IP)) + self.assertTrue(restr.is_restricted("/admin/somepage", IP)) class IPRestrictorOneUrlAllowedFromOneIpTest(TestCase): def setUp(self): - localip = models.RangeBasedIPGroup.objects.create(name='localip') + localip = models.RangeBasedIPGroup.objects.create(name="localip") models.IPRange.objects.create(ip_group=localip, first_ip=IP) - models.Rule.objects.create(url_pattern=SOME_URL, ip_group=localip, action='A') + models.Rule.objects.create(url_pattern=SOME_URL, ip_group=localip, action="A") self.restrictor = iprestrict.IPRestrictor() def test_ip_matches_url_does_not(self): - self.assertTrue(self.restrictor.is_restricted('', IP)) + self.assertTrue(self.restrictor.is_restricted("", IP)) def test_url_matches_ip_does_not(self): - self.assertTrue(self.restrictor.is_restricted(SOME_URL, '192.168.1.1')) + self.assertTrue(self.restrictor.is_restricted(SOME_URL, "192.168.1.1")) def test_both_match(self): self.assertFalse(self.restrictor.is_restricted(SOME_URL, IP)) def test_reload_if_ipgroup_changed(self): - self.assertTrue(self.restrictor.is_restricted(SOME_URL, '10.10.10.10')) - localhost = models.RangeBasedIPGroup.objects.get(name='localhost') - models.IPRange.objects.create(ip_group=localhost, first_ip='10.10.10.10') + self.assertTrue(self.restrictor.is_restricted(SOME_URL, "10.10.10.10")) + localhost = models.RangeBasedIPGroup.objects.get(name="localhost") + models.IPRange.objects.create(ip_group=localhost, first_ip="10.10.10.10") - self.assertTrue(self.restrictor.is_restricted(SOME_URL, '10.10.10.10')) + self.assertTrue(self.restrictor.is_restricted(SOME_URL, "10.10.10.10")) self.restrictor.reload_rules() - self.assertFalse(self.restrictor.is_restricted(SOME_URL, '10.10.10.10')) + self.assertFalse(self.restrictor.is_restricted(SOME_URL, "10.10.10.10")) class IPRestrictorOneIpAllowedIfNotFromOneCountryTest(TestCase): def setUp(self): - us = models.LocationBasedIPGroup.objects.create(name='US') - models.IPLocation.objects.create(ip_group=us, country_codes='US') - models.Rule.objects.create(url_pattern='ALL', ip_group=us, action='D') + us = models.LocationBasedIPGroup.objects.create(name="US") + models.IPLocation.objects.create(ip_group=us, country_codes="US") + models.Rule.objects.create(url_pattern="ALL", ip_group=us, action="D") self.us = us - localip = models.RangeBasedIPGroup.objects.create(name='localip') + localip = models.RangeBasedIPGroup.objects.create(name="localip") models.IPRange.objects.create(ip_group=localip, first_ip=IP) - models.Rule.objects.create(url_pattern=SOME_URL, ip_group=localip, action='A') + models.Rule.objects.create(url_pattern=SOME_URL, ip_group=localip, action="A") self.restrictor = iprestrict.IPRestrictor() def test_url_and_ip_match_but_country_denied(self): - with mock.patch('iprestrict.models.geoip') as m: - m.country_code.return_value = 'US' + with mock.patch("iprestrict.models.geoip") as m: + m.country_code.return_value = "US" self.assertTrue(self.restrictor.is_restricted(SOME_URL, IP)) def test_reload_if_ipgroup_changed(self): - with mock.patch('iprestrict.models.geoip') as m: - m.country_code.return_value = 'US' + with mock.patch("iprestrict.models.geoip") as m: + m.country_code.return_value = "US" self.assertTrue(self.restrictor.is_restricted(SOME_URL, IP)) location = self.us.iplocation_set.first() - location.country_codes = 'CU' + location.country_codes = "CU" location.save() self.assertTrue(self.restrictor.is_restricted(SOME_URL, IP)) @@ -117,21 +115,23 @@ def test_reload_if_ipgroup_changed(self): class IPRestrictorReversedLocationBasedIPGroupTest(TestCase): def setUp(self): - au = models.LocationBasedIPGroup.objects.create(name='Australian IPs') - models.IPLocation.objects.create(ip_group=au, country_codes='AU') + au = models.LocationBasedIPGroup.objects.create(name="Australian IPs") + models.IPLocation.objects.create(ip_group=au, country_codes="AU") # Deny non-AU IP addresses - models.Rule.objects.create(url_pattern='ALL', ip_group=au, reverse_ip_group=True, action='D') + models.Rule.objects.create( + url_pattern="ALL", ip_group=au, reverse_ip_group=True, action="D" + ) # Additionally allow just requests from localip - localip = models.RangeBasedIPGroup.objects.create(name='localip') + localip = models.RangeBasedIPGroup.objects.create(name="localip") models.IPRange.objects.create(ip_group=localip, first_ip=IP) - models.Rule.objects.create(url_pattern=SOME_URL, ip_group=localip, action='A') + models.Rule.objects.create(url_pattern=SOME_URL, ip_group=localip, action="A") self.restrictor = iprestrict.IPRestrictor() def test_url_and_ip_match_country_denied_then_allowed(self): - with mock.patch('iprestrict.models.geoip') as m: - m.country_code.return_value = 'US' + with mock.patch("iprestrict.models.geoip") as m: + m.country_code.return_value = "US" self.assertTrue(self.restrictor.is_restricted(SOME_URL, IP)) - m.country_code.return_value = 'AU' + m.country_code.return_value = "AU" self.assertFalse(self.restrictor.is_restricted(SOME_URL, IP)) diff --git a/tests/test_settings.py b/tests/test_settings.py index ae3bfeb..e91ad52 100644 --- a/tests/test_settings.py +++ b/tests/test_settings.py @@ -11,7 +11,7 @@ # See https://docs.djangoproject.com/en/1.8/howto/deployment/checklist/ # SECURITY WARNING: keep the secret key used in production secret! -SECRET_KEY = '$bsya1zyf4v)%%$1gk+^du%at@4crbg$far)gz5x%9ay8llgsq' +SECRET_KEY = "$bsya1zyf4v)%%$1gk+^du%at@4crbg$far)gz5x%9ay8llgsq" # SECURITY WARNING: don't run with debug turned on in production! DEBUG = True @@ -22,57 +22,59 @@ # Application definition INSTALLED_APPS = ( - 'django.contrib.admin', - 'django.contrib.auth', - 'django.contrib.contenttypes', - 'django.contrib.sessions', - 'django.contrib.messages', - 'django.contrib.staticfiles', - 'iprestrict', + "django.contrib.admin", + "django.contrib.auth", + "django.contrib.contenttypes", + "django.contrib.sessions", + "django.contrib.messages", + "django.contrib.staticfiles", + "iprestrict", ) MIDDLEWARE = ( - 'iprestrict.middleware.IPRestrictMiddleware', - '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', - 'django.middleware.security.SecurityMiddleware', + "iprestrict.middleware.IPRestrictMiddleware", + "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", + "django.middleware.security.SecurityMiddleware", ) # For Django <= 1.11 -MIDDLEWARE_CLASSES = MIDDLEWARE + ('django.contrib.auth.middleware.SessionAuthenticationMiddleware',) +MIDDLEWARE_CLASSES = MIDDLEWARE + ( + "django.contrib.auth.middleware.SessionAuthenticationMiddleware", +) -ROOT_URLCONF = 'tests.test_urls' +ROOT_URLCONF = "tests.test_urls" 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", ], }, }, ] -WSGI_APPLICATION = 'testsite_18.wsgi.application' +WSGI_APPLICATION = "testsite_18.wsgi.application" # Database # https://docs.djangoproject.com/en/1.8/ref/settings/#databases DATABASES = { - 'default': { - 'ENGINE': 'django.db.backends.sqlite3', - 'NAME': os.path.join(BASE_DIR, 'db.sqlite3'), + "default": { + "ENGINE": "django.db.backends.sqlite3", + "NAME": os.path.join(BASE_DIR, "db.sqlite3"), } } @@ -80,9 +82,9 @@ # Internationalization # https://docs.djangoproject.com/en/1.8/topics/i18n/ -LANGUAGE_CODE = 'en-us' +LANGUAGE_CODE = "en-us" -TIME_ZONE = 'UTC' +TIME_ZONE = "UTC" USE_I18N = True @@ -94,45 +96,39 @@ # Static files (CSS, JavaScript, Images) # https://docs.djangoproject.com/en/1.8/howto/static-files/ -STATIC_URL = '/static/' +STATIC_URL = "/static/" LOGGING = { - 'version': 1, - 'disable_existing_loggers': False, - 'formatters': { - 'simple': { - 'format': '%(levelname)s %(message)s' - }, - }, - 'filters': { - 'require_debug_false': { - '()': 'django.utils.log.RequireDebugFalse' - } + "version": 1, + "disable_existing_loggers": False, + "formatters": { + "simple": {"format": "%(levelname)s %(message)s"}, }, - 'handlers': { - 'mail_admins': { - 'level': 'ERROR', - 'filters': ['require_debug_false'], - 'class': 'django.utils.log.AdminEmailHandler' + "filters": {"require_debug_false": {"()": "django.utils.log.RequireDebugFalse"}}, + "handlers": { + "mail_admins": { + "level": "ERROR", + "filters": ["require_debug_false"], + "class": "django.utils.log.AdminEmailHandler", }, - 'console': { - 'level': 'DEBUG', - 'class': 'logging.StreamHandler', - 'formatter': 'simple' + "console": { + "level": "DEBUG", + "class": "logging.StreamHandler", + "formatter": "simple", }, }, - 'loggers': { - 'django.request': { - 'handlers': ['mail_admins'], - 'level': 'ERROR', - 'propagate': True, + "loggers": { + "django.request": { + "handlers": ["mail_admins"], + "level": "ERROR", + "propagate": True, }, - 'iprestrict': { - 'handlers': ['console'], - 'level': 'DEBUG', - 'propagate': True, - } - } + "iprestrict": { + "handlers": ["console"], + "level": "DEBUG", + "propagate": True, + }, + }, } IPRESTRICT_GEOIP_ENABLED = False diff --git a/tests/test_urls.py b/tests/test_urls.py index 53e0cde..b5d2eb0 100644 --- a/tests/test_urls.py +++ b/tests/test_urls.py @@ -1,32 +1,13 @@ -# -*- coding: utf-8 -*- # This file is to be used for testing only from __future__ import unicode_literals -import django - -from django.conf.urls import include, url -from django.contrib.staticfiles.urls import staticfiles_urlpatterns from django.contrib import admin +from django.contrib.staticfiles.urls import staticfiles_urlpatterns +from django.urls import include, path -try: - from django.urls import include, path - PRE_DJANGO_2 = False -except ImportError: - from django.conf.urls import include, url - PRE_DJANGO_2 = True - -import iprestrict.urls - - -if PRE_DJANGO_2: - urlpatterns = [ - url(r'^iprestrict/', include('iprestrict.urls')), - url(r'^admin/', include(admin.site.urls)), - ] -else: - urlpatterns = [ - path('iprestrict/', include('iprestrict.urls')), - path('admin/', admin.site.urls), - ] +urlpatterns = [ + path("iprestrict/", include("iprestrict.urls")), + path("admin/", admin.site.urls), +] urlpatterns += staticfiles_urlpatterns() diff --git a/testsites/testsite_31/testsite_31/settings.py b/testsites/testsite_31/testsite_31/settings.py index b7ebc8e..d9f25b9 100644 --- a/testsites/testsite_31/testsite_31/settings.py +++ b/testsites/testsite_31/testsite_31/settings.py @@ -32,6 +32,7 @@ INSTALLED_APPS = [ 'iprestrict', + 'django_extensions', 'polls.apps.PollsConfig', 'django.contrib.admin', 'django.contrib.auth', diff --git a/tox.ini b/tox.ini index 1e7dcd2..bcc12df 100644 --- a/tox.ini +++ b/tox.ini @@ -1,27 +1,46 @@ [tox] +isolated_build = True + envlist = - {py35,py36,py37,py38}-django-22 + {py36,py37,py38}-django-22 {py36,py37,py38}-django-{30,31} flake8 skip_missing_interpreters = True [travis] python= - 3.6: py36, flake8 + 3.6: lint, py36 [testenv] -commands = - ./runtests.sh {env:RUNTEST_ARGS:} +commands = python -m pytest deps = + pytest + pytest-django pycountry==20.7.3 - mock django-22: Django>=2.2,<2.3 django-30: Django>=3.0,<3.1 django-31: Django>=3.1,<3.2 -[testenv:flake8] +[testenv:lint] deps = Django pep8 flake8 -commands = flake8 iprestrict --max-line-length=120 --exclude=migrations + black + isort +commands = + isort --check-only iprestrict tests + black iprestrict tests --check + flake8 iprestrict tests + +[pytest] +DJANGO_SETTINGS_MODULE=tests.test_settings + +[flake8] +max-line-length = 120 +exclude = migrations +ignore = E203, W503 + +[isort] +profile = black +src_paths = iprestrict,test