diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml index 4446c338..6c0ab6d4 100644 --- a/.github/workflows/tests.yml +++ b/.github/workflows/tests.yml @@ -39,7 +39,7 @@ jobs: - name: Run linters run: | source .venv/bin/activate - # flake8 . + # flake8 peeringdb_server black . --check isort . diff --git a/Ctl/dev/docker-compose.yml b/Ctl/dev/docker-compose.yml index 9f3b01da..d0d3c9a5 100644 --- a/Ctl/dev/docker-compose.yml +++ b/Ctl/dev/docker-compose.yml @@ -19,7 +19,7 @@ services: ports: - "13306:3306" volumes: - - ./peeringdb_database:/var/lib/mysql + - ./peeringdb_database:/var/lib/mysql:Z peeringdb: # user: "0:0" @@ -43,9 +43,9 @@ services: # this needs to be set in the shell, compose env vars aren't read yet - "${DJANGO_PORT:-8000}:8000" volumes: - - ../../peeringdb_server:/srv/www.peeringdb.com/peeringdb_server - - ../../mainsite:/srv/www.peeringdb.com/mainsite - - ../../tests:/srv/www.peeringdb.com/tests + - ../../peeringdb_server:/srv/www.peeringdb.com/peeringdb_server:Z + - ../../mainsite:/srv/www.peeringdb.com/mainsite:Z + - ../../tests:/srv/www.peeringdb.com/tests:Z # - ../../search-data:/srv/www.peeringdb.com/search-data volumes: diff --git a/mainsite/settings/__init__.py b/mainsite/settings/__init__.py index c1c8d795..97c544c7 100644 --- a/mainsite/settings/__init__.py +++ b/mainsite/settings/__init__.py @@ -191,7 +191,6 @@ def try_include(filename): except FileNotFoundError: print_debug(f"additional settings file '{filename}' was not found, skipping") - pass def read_file(name): @@ -256,6 +255,7 @@ API_THROTTLE_ENABLED = True set_option("API_THROTTLE_RATE_ANON", "100/second") set_option("API_THROTTLE_RATE_USER", "100/second") set_option("API_THROTTLE_RATE_FILTER_DISTANCE", "10/minute") +set_option("API_THROTTLE_IXF_IMPORT", "1/minute") # spatial queries require user auth set_option("API_DISTANCE_FILTER_REQUIRE_AUTH", True) @@ -329,7 +329,9 @@ SITE_ID = 1 TIME_ZONE = "UTC" USE_TZ = True -ADMINS = ("Support", SERVER_EMAIL) +ADMINS = [ + ("Support", SERVER_EMAIL), +] MANAGERS = ADMINS MEDIA_ROOT = os.path.abspath(os.path.join(BASE_DIR, "media")) @@ -436,7 +438,6 @@ INSTALLED_APPS = [ "crispy_forms", "django_countries", "django_inet", - "django_namespace_perms", "django_grainy", "django_peeringdb", "django_tables2", @@ -518,6 +519,13 @@ PASSWORD_HASHERS = ( ROOT_URLCONF = "mainsite.urls" CONN_MAX_AGE = 3600 +# starting with reversion 4.0 the reversion revision context +# no longer opens an atomic transaction context, so we need +# to ensure this ourselves for all the requests +ATOMIC_REQUESTS = True + +DEFAULT_AUTO_FIELD = "django.db.models.AutoField" + # email vars should be already set from the release environment file # override here from env if set @@ -635,6 +643,7 @@ if API_THROTTLE_ENABLED: "anon": API_THROTTLE_RATE_ANON, "user": API_THROTTLE_RATE_USER, "filter_distance": API_THROTTLE_RATE_FILTER_DISTANCE, + "ixf_import_request": API_THROTTLE_IXF_IMPORT, }, } ) diff --git a/peeringdb_server/admin.py b/peeringdb_server/admin.py index f274238d..f0ad2a16 100644 --- a/peeringdb_server/admin.py +++ b/peeringdb_server/admin.py @@ -2,11 +2,8 @@ import datetime import ipaddress import json import re -import time -from operator import or_ import django.urls -import django_grainy.models as django_grainy_models import reversion from django import forms as baseForms from django.conf import settings @@ -14,27 +11,19 @@ from django.conf.urls import url from django.contrib import admin, messages from django.contrib.admin import helpers from django.contrib.admin.actions import delete_selected -from django.contrib.admin.views.main import ChangeList -from django.contrib.auth import forms from django.contrib.auth.admin import UserAdmin from django.contrib.contenttypes.models import ContentType from django.core.exceptions import ValidationError from django.db.models import Q -from django.db.models.functions import Concat from django.db.utils import OperationalError from django.forms import DecimalField -from django.http import HttpResponseForbidden -from django.shortcuts import Http404, redirect +from django.shortcuts import redirect from django.template import loader from django.template.response import TemplateResponse from django.utils import html from django.utils.safestring import mark_safe -from django_grainy.admin import ( - GrainyGroupAdmin, - GrainyUserAdmin, - GroupPermissionInlineAdmin, - UserPermissionInlineAdmin, -) +from django.utils.translation import ugettext_lazy as _ +from django_grainy.admin import UserPermissionInlineAdmin from django_handleref.admin import VersionAdmin as HandleRefVersionAdmin from rest_framework_api_key.admin import APIKeyModelAdmin from rest_framework_api_key.models import APIKey @@ -51,7 +40,6 @@ from peeringdb_server.models import ( CommandLineTool, DeskProTicket, DeskProTicketCC, - EnvironmentSetting, Facility, GeoCoordinateCache, InternetExchange, @@ -86,8 +74,6 @@ from . import forms delete_selected.short_description = "HARD DELETE - Proceed with caution" -from django.utils.translation import ugettext_lazy as _ - # these app labels control permissions for the views # currently exposed in admin @@ -509,9 +495,6 @@ class SoftDeleteAdmin( def grainy_namespace(self, obj): return obj.grainy_namespace - def grainy_namespace(self, obj): - return obj.grainy_namespace - class ModelAdminWithVQCtrl: """ @@ -596,10 +579,10 @@ class IXLanInline(SanitizedAdmin, admin.StackedInline): model = IXLan extra = 0 form = StatusForm - exclude = ["arp_sponge"] + exclude = ["arp_sponge", "dot1q_support"] readonly_fields = ["ixf_import_attempt_info", "prefixes"] - def has_add_permission(self, request): + def has_add_permission(self, request, obj=None): return False def has_delete_permission(self, request, obj): @@ -678,7 +661,7 @@ class UserOrgAffiliationRequestInlineForm(baseForms.ModelForm): try: asn = self.cleaned_data.get("asn") if asn: - rdap_valid = RdapLookup().get_asn(asn).emails + RdapLookup().get_asn(asn).emails except RdapException as exc: raise ValidationError({"asn": rdap_pretty_error_message(exc)}) @@ -773,6 +756,7 @@ class IXLanAdmin(SoftDeleteAdmin): actions = [] list_display = ("ix", "name", "descr", "status") search_fields = ("name", "ix__name") + exclude = ("dot1q_support",) list_filter = (StatusFilter,) readonly_fields = ("id",) inlines = (IXLanPrefixInline, NetworkInternetExchangeInline) @@ -1491,6 +1475,12 @@ class UserOrgAffiliationRequestAdmin(ModelAdminWithUrlActions): class UserCreationForm(forms.UserCreationForm): + + # user creation through django-admin doesnt need + # captcha checking + + require_captcha = False + def clean_username(self): username = self.cleaned_data["username"] try: @@ -2038,14 +2028,6 @@ class IXFMemberDataAdmin(CustomResultLengthAdmin, admin.ModelAdmin): return qset.filter(requirement_of__isnull=True) - ids = [ - row.id - for row in qset.exclude(requirement_of__isnull=False) - if row.action != "noop" - ] - - return qset.filter(id__in=ids) - def ix(self, obj): return obj.ixlan.ix @@ -2081,7 +2063,7 @@ class IXFMemberDataAdmin(CustomResultLengthAdmin, admin.ModelAdmin): return self.readonly_fields + ("asn", "ipaddr4", "ipaddr6") return self.readonly_fields - def has_add_permission(self, request): + def has_add_permission(self, request, obj=None): return False def has_delete_permission(self, request, obj=None): diff --git a/peeringdb_server/admin_commandline_tools.py b/peeringdb_server/admin_commandline_tools.py index b7191538..736e2f6c 100644 --- a/peeringdb_server/admin_commandline_tools.py +++ b/peeringdb_server/admin_commandline_tools.py @@ -15,7 +15,6 @@ from peeringdb_server.models import ( CommandLineTool, Facility, InternetExchange, - IXLan, ) @@ -28,6 +27,7 @@ TOOL_MAP = {} def register_tool(cls): TOOL_MAP[cls.tool] = cls + return cls def get_tool(tool_id, form): @@ -78,7 +78,10 @@ class CommandLineToolWrapper: self.args = [] self.kwargs = {} self.form_instance = form - self.set_arguments(form.cleaned_data) + self.set_arguments(form.cleaned_data) # lgtm[py/init-calls-subclass] + # LGTM Note: as this is the last statement in the __init__ + # call this is unproblematic, however this should probably + # still be separated in the future (TODO) @property def name(self): @@ -220,7 +223,7 @@ class ToolRenumberLans(CommandLineToolWrapper): self.args[1], self.args[2], ) - except: + except Exception: # if a version of this command was run before, we still need to able # to display a somewhat useful discription, so fall back to this basic # display diff --git a/peeringdb_server/api_cache.py b/peeringdb_server/api_cache.py index 10645a0d..360522c2 100644 --- a/peeringdb_server/api_cache.py +++ b/peeringdb_server/api_cache.py @@ -1,11 +1,8 @@ -import collections import json import os from django.conf import settings -from peeringdb_server.models import InternetExchange, IXLan, Network - class CacheRedirect(Exception): """ @@ -16,7 +13,7 @@ class CacheRedirect(Exception): """ def __init__(self, loader): - super(Exception, self).__init__(self, "Result to be loaded from cache") + super().__init__(self, "Result to be loaded from cache") self.loader = loader diff --git a/peeringdb_server/api_key_views.py b/peeringdb_server/api_key_views.py index 0849ccff..cd3fa28e 100644 --- a/peeringdb_server/api_key_views.py +++ b/peeringdb_server/api_key_views.py @@ -1,22 +1,16 @@ """ Views for organization api key management """ -from django.conf import settings from django.contrib.auth.decorators import login_required from django.http import JsonResponse -from django.template import loader -from django.utils.translation import override from django.utils.translation import ugettext_lazy as _ from django.views.decorators.csrf import csrf_protect -from django_grainy.models import UserPermission -from django_handleref.models import HandleRefModel from grainy.const import PERM_READ from peeringdb_server.forms import OrgAdminUserPermissionForm, OrganizationAPIKeyForm from peeringdb_server.models import ( OrganizationAPIKey, OrganizationAPIPermission, - User, UserAPIKey, ) from peeringdb_server.org_admin_views import load_entity_permissions, org_admin_required diff --git a/peeringdb_server/api_schema.py b/peeringdb_server/api_schema.py index 20b425bc..a2dad28f 100644 --- a/peeringdb_server/api_schema.py +++ b/peeringdb_server/api_schema.py @@ -490,7 +490,7 @@ class BaseSchema(AutoSchema): Augment openapi schema for object creation """ - parameters = op_dict.get("parameters") + op_dict.get("parameters") serializer, model = self.get_classes(*op_args) if not model: @@ -559,7 +559,7 @@ class BaseSchema(AutoSchema): """ Augment openapi schema for update operation """ - parameters = op_dict.get("parameters") + op_dict.get("parameters") serializer, model = self.get_classes(*op_args) if not model: @@ -573,7 +573,7 @@ class BaseSchema(AutoSchema): """ Augment openapi schema for delete operation """ - parameters = op_dict.get("parameters") + op_dict.get("parameters") serializer, model = self.get_classes(*op_args) if not model: diff --git a/peeringdb_server/apps.py b/peeringdb_server/apps.py index 3caf49e2..2c5d1473 100644 --- a/peeringdb_server/apps.py +++ b/peeringdb_server/apps.py @@ -4,6 +4,7 @@ from django.apps import AppConfig class PeeringDBServerAppConfig(AppConfig): name = "peeringdb_server" verbose_name = "PeeringDB" + default_auto_field = "django.db.models.AutoField" def ready(self): - import peeringdb_server.signals + pass diff --git a/peeringdb_server/autocomplete_views.py b/peeringdb_server/autocomplete_views.py index 239cfba9..5d4d3919 100644 --- a/peeringdb_server/autocomplete_views.py +++ b/peeringdb_server/autocomplete_views.py @@ -43,7 +43,7 @@ class AutocompleteHTMLResponse(autocomplete.Select2QuerySetView): return False def render_to_response(self, context): - q = self.request.GET.get("q", None) + self.request.GET.get("q", None) return http.HttpResponse( "".join([i.get("text") for i in self.get_results(context)]), content_type="text/html", diff --git a/peeringdb_server/client_adaptor/__init__.py b/peeringdb_server/client_adaptor/__init__.py index 040771b4..56f4fe6c 100644 --- a/peeringdb_server/client_adaptor/__init__.py +++ b/peeringdb_server/client_adaptor/__init__.py @@ -1 +1 @@ -from peeringdb_server.client_adaptor.load import load_backend +from peeringdb_server.client_adaptor.load import load_backend # noqa diff --git a/peeringdb_server/client_adaptor/backend.py b/peeringdb_server/client_adaptor/backend.py index 2c25ac79..835f720b 100644 --- a/peeringdb_server/client_adaptor/backend.py +++ b/peeringdb_server/client_adaptor/backend.py @@ -1,10 +1,9 @@ import re from collections import defaultdict -from django.conf import settings from django.core.exceptions import ValidationError from django.db import IntegrityError -from django.db.models import DateTimeField, OneToOneRel +from django.db.models import OneToOneRel from django_peeringdb.client_adaptor.backend import Backend as BaseBackend from django_peeringdb.client_adaptor.backend import reftag_to_cls diff --git a/peeringdb_server/client_adaptor/load.py b/peeringdb_server/client_adaptor/load.py index bbb69f22..184f16c8 100644 --- a/peeringdb_server/client_adaptor/load.py +++ b/peeringdb_server/client_adaptor/load.py @@ -1,4 +1,4 @@ -from django_peeringdb.client_adaptor.load import DJANGO_DB_FIELDS, database_settings +from django_peeringdb.client_adaptor.load import database_settings __backend = None diff --git a/peeringdb_server/client_adaptor/setup.py b/peeringdb_server/client_adaptor/setup.py index 5313ef13..66a40ad6 100644 --- a/peeringdb_server/client_adaptor/setup.py +++ b/peeringdb_server/client_adaptor/setup.py @@ -1 +1 @@ -from django_peeringdb.client_adaptor.setup import configure +from django_peeringdb.client_adaptor.setup import configure # noqa diff --git a/peeringdb_server/data_views.py b/peeringdb_server/data_views.py index 2866a9f1..d2553daa 100644 --- a/peeringdb_server/data_views.py +++ b/peeringdb_server/data_views.py @@ -4,7 +4,6 @@ This holds JSON views for various data sets, Mostly these are needed for filling form-selects for editable mode """ -import datetime import django_countries import django_peeringdb.const as const @@ -231,7 +230,7 @@ def organizations(request): def languages(request): from django.conf import settings - cur_language = translation.get_language() + translation.get_language() return JsonResponse( {"locales": [{"id": id, "name": _(name)} for (id, name) in settings.LANGUAGES]} ) diff --git a/peeringdb_server/deskpro.py b/peeringdb_server/deskpro.py index e6188501..4cdc50c2 100644 --- a/peeringdb_server/deskpro.py +++ b/peeringdb_server/deskpro.py @@ -224,7 +224,12 @@ class APIClient: return {"Authorization": f"key {self.key}"} def parse_response(self, response, many=False): - r_json = response.json() + try: + r_json = response.json() + except Exception: + print(response.content) + raise + if "status" in r_json: if r_json["status"] >= 400: raise APIError(r_json["message"], r_json) @@ -251,6 +256,14 @@ class APIClient: ) return self.parse_response(response) + def update(self, endpoint, param): + response = requests.put( + f"{self.url}/{endpoint}", json=param, headers=self.auth_headers + ) + if response.status_code == 204: + return {} + return self.parse_response(response) + def require_person(self, email, user=None): """ @@ -326,6 +339,10 @@ class APIClient: ticket.deskpro_ref = ticket_response["ref"] ticket.deskpro_id = ticket_response["id"] + else: + + self.reopen_ticket(ticket) + self.create( f"tickets/{ticket.deskpro_id}/messages", { @@ -335,6 +352,28 @@ class APIClient: }, ) + def reopen_ticket(self, ticket): + + """ + For existing tickets we want to check their current status + on deskpro's side. + + If the ticket has already been resolved we need to set it + back to awaiting_agent before posting a new message to + it (see #920) + """ + + if not ticket.deskpro_id: + return + + endpoint = f"tickets/{ticket.deskpro_id}" + ticket_data = self.get(endpoint, param={}) + + if ticket_data and ticket_data.get("ticket_status") == "resolved": + print("ticket resolved already") + self.update(endpoint, {"status": "awaiting_agent"}) + print("Re-opened ticket (set to awaiting_agent)", ticket.deskpro_id) + class MockAPIClient(APIClient): @@ -346,6 +385,7 @@ class MockAPIClient(APIClient): """ def __init__(self, *args, **kwargs): + super().__init__("", "") self.ticket_count = 0 def get(self, endpoint, param): @@ -374,6 +414,7 @@ class FailingMockAPIClient(MockAPIClient): """ def __init__(self, *args, **kwargs): + super().__init__("", "") self.ticket_count = 0 def get(self, endpoint, param): diff --git a/peeringdb_server/export_views.py b/peeringdb_server/export_views.py index 5c388331..b63d9b62 100644 --- a/peeringdb_server/export_views.py +++ b/peeringdb_server/export_views.py @@ -1,7 +1,6 @@ import collections import csv import datetime -import io import json import urllib.error import urllib.parse @@ -12,7 +11,7 @@ from django.utils.translation import ugettext_lazy as _ from django.views import View from rest_framework.test import APIRequestFactory -from peeringdb_server.models import InternetExchange, IXLan, NetworkIXLan +from peeringdb_server.models import IXLan from peeringdb_server.renderers import JSONEncoder from peeringdb_server.rest import REFTAG_MAP as RestViewSets @@ -139,7 +138,7 @@ class ExportView(View): response_handler = getattr(self, f"response_{fmt}") response = response_handler(self.generate(request)) - if self.download == True: + if self.download is True: # send attachment header, triggering download on the client side filename = self.download_name.format(extension=self.extensions.get(fmt)) response["Content-Disposition"] = 'attachment; filename="{}"'.format( @@ -147,8 +146,10 @@ class ExportView(View): ) return response - except Exception as exc: - return JsonResponse({"non_field_errors": [str(exc)]}, status=400) + except Exception: + return JsonResponse( + {"non_field_errors": ["Internal Error (500)"]}, status=400 + ) def generate(self, request): """ @@ -258,9 +259,16 @@ class AdvancedSearchExportView(ExportView): return response.data - def get(self, request, tag, fmt): + def get(self, request, tag, fmt): # lgtm[py/inheritance/signature-mismatch] """ Handle export + + LGTM Notes: signature-mismatch: order of arguments are defined by the + url routing set up for this view. (e.g., //) + + The `get` method will never be called in a different + context where a mismatching signature would matter so + the lgtm warning can be ignored in this case """ self.tag = tag return super().get(request, fmt) diff --git a/peeringdb_server/forms.py b/peeringdb_server/forms.py index 907356e0..7e116f3f 100644 --- a/peeringdb_server/forms.py +++ b/peeringdb_server/forms.py @@ -8,11 +8,10 @@ from django.conf import settings as dj_settings from django.contrib.auth import forms as auth_forms from django.utils import timezone from django.utils.translation import ugettext_lazy as _ -from grainy.const import * +from grainy.const import PERM_CRUD, PERM_DENY, PERM_READ from peeringdb_server.inet import get_client_ip -from peeringdb_server.models import Organization, OrganizationAPIKey, User -from peeringdb_server.util import PERM_CRUD +from peeringdb_server.models import Organization, User class OrganizationAPIKeyForm(forms.Form): @@ -109,6 +108,8 @@ class UserCreationForm(auth_forms.UserCreationForm): captcha = forms.CharField(required=False) captcha_generator = CaptchaField(required=False) + require_captcha = True + class Meta: model = User fields = ( @@ -123,7 +124,9 @@ class UserCreationForm(auth_forms.UserCreationForm): recaptcha = self.cleaned_data.get("recaptcha", "") captcha = self.cleaned_data.get("captcha", "") - if not recaptcha and not captcha: + if not self.require_captcha: + return + elif not recaptcha and not captcha: raise forms.ValidationError( _("Please fill out the anti-spam challenge (captcha) field") ) diff --git a/peeringdb_server/import_views.py b/peeringdb_server/import_views.py index 2e53115b..e8205466 100644 --- a/peeringdb_server/import_views.py +++ b/peeringdb_server/import_views.py @@ -5,10 +5,10 @@ from django.conf import settings from django.contrib.auth import authenticate from django.http import HttpResponse, JsonResponse from django.utils.translation import ugettext_lazy as _ -from ratelimit.decorators import is_ratelimited, ratelimit +from ratelimit.decorators import ratelimit from peeringdb_server import ixf -from peeringdb_server.models import IXLan, Network, NetworkIXLan +from peeringdb_server.models import IXLan, Network from peeringdb_server.util import check_permissions RATELIMITS = settings.RATELIMITS @@ -104,7 +104,7 @@ def view_import_net_ixf_postmortem(request, net_id): try: limit = int(request.GET.get("limit", 25)) - except: + except Exception: limit = 25 errors = [] @@ -152,10 +152,10 @@ def view_import_net_ixf_preview(request, net_id): for ixlan in net.ixlan_set_ixf_enabled: importer = ixf.Importer() importer.cache_only = True - success = importer.update(ixlan, asn=net.asn, save=False) + importer.update(ixlan, asn=net.asn, save=False) # strip suggestions - log_data = [i for i in importer.log["data"] if not "suggest-" in i["action"]] + log_data = [i for i in importer.log["data"] if "suggest-" not in i["action"]] total_log["data"].extend(log_data) total_log["errors"].extend( diff --git a/peeringdb_server/inet.py b/peeringdb_server/inet.py index 5d20491b..c90c7d0d 100644 --- a/peeringdb_server/inet.py +++ b/peeringdb_server/inet.py @@ -1,14 +1,13 @@ import ipaddress -import re import rdap -import requests from django.utils.translation import ugettext_lazy as _ -from rdap import RdapAsn -from rdap.exceptions import RdapException, RdapHTTPError, RdapNotFoundError +from rdap.exceptions import RdapException, RdapNotFoundError from peeringdb_server import settings +RdapAsn = rdap.RdapAsn # noqa + # Valid IRR Source values # reference: http://www.irr.net/docs/list.html IRR_SOURCE = ( diff --git a/peeringdb_server/ixf.py b/peeringdb_server/ixf.py index a8c5c88e..6dd745c1 100644 --- a/peeringdb_server/ixf.py +++ b/peeringdb_server/ixf.py @@ -1,8 +1,6 @@ -import copy import datetime import ipaddress import json -import re from smtplib import SMTPException import requests @@ -19,7 +17,6 @@ from django.utils.translation import ugettext_lazy as _ import peeringdb_server.deskpro as deskpro from peeringdb_server.models import ( DeskProTicket, - DeskProTicketCC, EnvironmentSetting, IXFImportEmail, IXFMemberData, @@ -31,7 +28,6 @@ from peeringdb_server.models import ( NetworkProtocolsDisabled, User, ValidationErrorEncoder, - debug_mail, ) REASON_ENTRY_GONE_FROM_REMOTE = _( @@ -60,8 +56,8 @@ class MultipleVlansInPrefix(ValueError): """ def __init__(self, importer, *args, **kwargs): - feed_url = importer.ixlan.ixf_ixp_member_list_url - ix_name = importer.ixlan.ix.name + importer.ixlan.ixf_ixp_member_list_url + importer.ixlan.ix.name support_email = settings.DEFAULT_FROM_EMAIL super().__init__( _( @@ -198,7 +194,7 @@ class Importer: try: data = result.json() - except Exception as inst: + except Exception: data = {"pdb_error": _("No JSON could be parsed")} return data @@ -416,7 +412,6 @@ class Importer: """ invalid = None - vlan_list_found = False ipv4_addresses = {} ipv6_addresses = {} @@ -438,7 +433,6 @@ class Importer: if not vlans: continue - vlan_list_found = True # de-dupe reoccurring ipv4 / ipv6 addresses @@ -532,11 +526,12 @@ class Importer: # null ix-f error note on ixlan if it had error'd before if self.ixlan.ixf_ixp_import_error: - with reversion.create_revision(): - reversion.set_user(self.ticket_user) - self.ixlan.ixf_ixp_import_error = None - self.ixlan.ixf_ixp_import_error_notified = None - self.ixlan.save() + with transaction.atomic(): + with reversion.create_revision(): + reversion.set_user(self.ticket_user) + self.ixlan.ixf_ixp_import_error = None + self.ixlan.ixf_ixp_import_error_notified = None + self.ixlan.save() with transaction.atomic(): # process any netixlans that need to be deleted @@ -588,15 +583,14 @@ class Importer: """ ix = self.ixlan.ix - save_ix = False - ixf_member_data_changed = IXFMemberData.objects.filter( - updated__gte=self.now, ixlan=self.ixlan - ).exists() + # ixf_member_data_changed = IXFMemberData.objects.filter( + # updated__gte=self.now, ixlan=self.ixlan + # ).exists() - netixlan_data_changed = NetworkIXLan.objects.filter( - updated__gte=self.now, ixlan=self.ixlan - ).exists() + # netixlan_data_changed = NetworkIXLan.objects.filter( + # updated__gte=self.now, ixlan=self.ixlan + # ).exists() ix.ixf_last_import = self.now @@ -610,9 +604,10 @@ class Importer: ix._meta.get_field("updated").auto_now = False try: - with reversion.create_revision(): - reversion.set_user(self.ticket_user) - ix.save() + with transaction.atomic(): + with reversion.create_revision(): + reversion.set_user(self.ticket_user) + ix.save() finally: # always turn auto_now back on afterwards @@ -644,6 +639,7 @@ class Importer: break @reversion.create_revision() + @transaction.atomic() def process_saves(self): reversion.set_user(self.ticket_user) @@ -651,6 +647,7 @@ class Importer: self.apply_add_or_update(ixf_member) @reversion.create_revision() + @transaction.atomic() def process_deletions(self): """ Cycles all netixlans on the ixlan targeted by the importer and @@ -875,8 +872,7 @@ class Importer: asn = member["asnum"] for lan in vlan_list: - ipv4_valid = False - ipv6_valid = False + pass ipv4 = lan.get("ipv4", {}) ipv6 = lan.get("ipv6", {}) @@ -1577,6 +1573,8 @@ class Importer: ix_notifications = {} deskpro_notifications = {} + self.current_proposal = None + for notification in self.notifications: ixf_member_data = notification["ixf_member_data"] @@ -1585,6 +1583,7 @@ class Importer: notify_ix = notification["ix"] notify_net = notification["net"] context = notification["context"] + self.current_proposal = ixf_member_data # we don't care about resolved proposals @@ -1723,7 +1722,14 @@ class Importer: # consolidate proposals into net,ix and ix,net # groupings - consolidated = self.consolidate_proposals() + try: + consolidated = self.consolidate_proposals() + except Exception as exc: + if error_handler: + error_handler(exc, ixf_member_data=self.current_proposal) + return + else: + raise ticket_days = EnvironmentSetting.get_setting_value( "IXF_IMPORTER_DAYS_UNTIL_TICKET" @@ -1737,7 +1743,7 @@ class Importer: self._notify_proposal(recipient, data, ticket_days, template) except Exception as exc: if error_handler: - error_handler(exc, ixlan=self.ixlan) + error_handler(exc) else: raise @@ -1745,7 +1751,7 @@ class Importer: self.ticket_consolidated_proposals(consolidated["ac"]) except Exception as exc: if error_handler: - error_handler(exc, ixlan=self.ixlan) + error_handler(exc) else: raise @@ -1791,17 +1797,13 @@ class Importer: def ticket_aged_proposals(self): """ This function is currently disabled as per issue #860 - """ - return - """ Cycle through all IXFMemberData objects that and create tickets for those that are older than the period specified in IXF_IMPORTER_DAYS_UNTIL_TICKET and that don't have any ticket associated with them yet """ - """ if not self.save: return @@ -1842,6 +1844,7 @@ class Importer: ixf_member_data, typ, True, True, True, {}, ixf_member_data.action ) """ + return def ticket_proposal(self, ixf_member_data, typ, ac, ix, net, context, action): @@ -1951,6 +1954,7 @@ class Importer: ixf_m_d.save() @reversion.create_revision() + @transaction.atomic() def notify_error(self, error): """ @@ -1966,7 +1970,7 @@ class Importer: now = datetime.datetime.now(datetime.timezone.utc) notified = self.ixlan.ixf_ixp_import_error_notified - prev_error = self.ixlan.ixf_ixp_import_error + self.ixlan.ixf_ixp_import_error if notified: diff = (now - notified).total_seconds() / 3600 @@ -2012,7 +2016,7 @@ class Importer: resent_email = self._resend_email(email) if resent_email: resent_emails.append(resent_email) - except SMTPException as email_exception: + except SMTPException: pass return resent_emails diff --git a/peeringdb_server/mail.py b/peeringdb_server/mail.py index 5c58c0fb..e0828719 100644 --- a/peeringdb_server/mail.py +++ b/peeringdb_server/mail.py @@ -17,7 +17,7 @@ def mail_admins_with_from( return # set plain text message - msg_raw = strip_tags(msg) + strip_tags(msg) mail = EmailMultiAlternatives( f"{settings.EMAIL_SUBJECT_PREFIX}{subj}", msg, diff --git a/peeringdb_server/maintenance.py b/peeringdb_server/maintenance.py index fb2d167a..ee04fdd7 100644 --- a/peeringdb_server/maintenance.py +++ b/peeringdb_server/maintenance.py @@ -1,9 +1,7 @@ import os -from django.core.cache import cache from django.http import JsonResponse from django.urls import resolve, reverse -from django.utils.translation import ugettext_lazy as _ from rest_framework.viewsets import ModelViewSet from peeringdb_server import settings diff --git a/peeringdb_server/management/commands/_db_command.py b/peeringdb_server/management/commands/_db_command.py index 4f2506b9..20ba3415 100644 --- a/peeringdb_server/management/commands/_db_command.py +++ b/peeringdb_server/management/commands/_db_command.py @@ -1,9 +1,8 @@ import json -from optparse import make_option from django.contrib.contenttypes.models import ContentType -from django.core.management.base import BaseCommand, CommandError -from reversion.models import Revision, Version +from django.core.management.base import BaseCommand +from reversion.models import Version import peeringdb_server.models as pdbm @@ -29,8 +28,6 @@ class DBCommand(BaseCommand): def handle(self, *args, **options): - versions = Version.objects.all() - args = list(args) try: diff --git a/peeringdb_server/management/commands/pdb_api_cache.py b/peeringdb_server/management/commands/pdb_api_cache.py index 1462cb60..bcfc254f 100644 --- a/peeringdb_server/management/commands/pdb_api_cache.py +++ b/peeringdb_server/management/commands/pdb_api_cache.py @@ -2,7 +2,6 @@ import datetime import os import time import traceback -from optparse import make_option from django.conf import settings from django.core.management.base import BaseCommand diff --git a/peeringdb_server/management/commands/pdb_api_test.py b/peeringdb_server/management/commands/pdb_api_test.py index a00f342b..628ee047 100644 --- a/peeringdb_server/management/commands/pdb_api_test.py +++ b/peeringdb_server/management/commands/pdb_api_test.py @@ -6,7 +6,6 @@ import copy import datetime import ipaddress import json -import random import re import time import unittest @@ -18,7 +17,6 @@ from django.conf import settings from django.contrib.auth.models import Group from django.core.management import call_command from django.core.management.base import BaseCommand -from django.db.models import Sum from django.db.utils import IntegrityError from grainy.const import PERM_CREATE, PERM_DELETE, PERM_READ, PERM_UPDATE from rest_framework import serializers @@ -567,7 +565,7 @@ class TestJSON(unittest.TestCase): data_invalid[k] = v with pytest.raises(InvalidRequestException) as excinfo: - r = db.create(typ, data_invalid, return_response=True) + db.create(typ, data_invalid, return_response=True) assert "400 Bad Request" in str(excinfo.value) @@ -578,7 +576,7 @@ class TestJSON(unittest.TestCase): data_status[k] = v with pytest.raises(InvalidRequestException) as excinfo: - r = db.create(typ, data_status, return_response=True) + db.create(typ, data_status, return_response=True) assert "not yet been approved" in str(excinfo.value) # we test fail because of permissions @@ -1046,7 +1044,10 @@ class TestJSON(unittest.TestCase): ########################################################################## def test_user_001_GET_ixfac(self): - self.assert_get_handleref(self.db_user, "ixfac", SHARED["ixfac_r_ok"].id) + data = self.assert_get_handleref(self.db_user, "ixfac", SHARED["ixfac_r_ok"].id) + assert data.get("name") + assert data.get("city") + assert data.get("country") ########################################################################## @@ -1132,7 +1133,7 @@ class TestJSON(unittest.TestCase): org.usergroup.user_set.add(self.user_org_member) self.user_org_member.grainy_permissions.add_permission(org, "cr") - r_data = self.assert_create( + self.assert_create( self.db_org_member, "ix", data, @@ -1292,6 +1293,34 @@ class TestJSON(unittest.TestCase): ########################################################################## + def test_org_admin_002_POST_ix_request_ixf_import(self): + ix = SHARED["ix_rw_ok"] + + data = ( + self.db_org_admin._request( + f"ix/{ix.id}/request_ixf_import", method="POST", data="{}" + ) + .json() + .get("data")[0] + ) + + assert data["ixf_import_request"] + assert data["ixf_import_request_status"] == "queued" + + resp = self.db_org_admin._request( + f"ix/{ix.id}/request_ixf_import", method="POST", data="{}" + ) + + assert resp.status_code == 429 + + resp = self.db_org_member._request( + f"ix/{ix.id}/request_ixf_import", method="POST", data="{}" + ) + + assert resp.status_code in [401, 403] + + ########################################################################## + def test_org_admin_002_POST_PUT_DELETE_fac(self): data = self.make_data_fac() @@ -1491,7 +1520,7 @@ class TestJSON(unittest.TestCase): data = self.make_data_net(asn=SHARED["net_rw_dupe_deleted"].asn) with pytest.raises(InvalidRequestException) as excinfo: - r_data = self.db_org_admin.create("net", data, return_response=True) + self.db_org_admin.create("net", data, return_response=True) # check exception vs value assert "Network has been deleted. Please contact" in excinfo.value.extra["asn"] @@ -1507,7 +1536,7 @@ class TestJSON(unittest.TestCase): data = self.make_data_net(asn=9000900) with pytest.raises(PermissionDeniedException) as excinfo: - r_data = self.assert_create(self.db_org_admin, "as_set", data) + self.assert_create(self.db_org_admin, "as_set", data) assert "You do not have permission" in str(excinfo.value) with pytest.raises(PermissionDeniedException) as excinfo: @@ -1526,7 +1555,7 @@ class TestJSON(unittest.TestCase): data = self.make_data_net() for bogon_asn in inet.BOGON_ASN_RANGES: - r_data = self.assert_create( + self.assert_create( self.db_org_admin, "net", data, @@ -1542,7 +1571,7 @@ class TestJSON(unittest.TestCase): for bogon_asn in inet.TUTORIAL_ASN_RANGES: data = self.make_data_net(asn=bogon_asn[0]) - r_data = self.assert_create(self.db_org_admin, "net", data) + self.assert_create(self.db_org_admin, "net", data) pdb_settings.TUTORIAL_MODE = False @@ -1654,7 +1683,7 @@ class TestJSON(unittest.TestCase): data = self.make_data_ixlan(ix_id=SHARED["ix_rw_ok"].id) with self.assertRaises(Exception) as exc: - r_data = self.assert_create( + self.assert_create( self.db_org_admin, "ixlan", data, @@ -1688,6 +1717,17 @@ class TestJSON(unittest.TestCase): ########################################################################## + def test_org_admin_002_PUT_ixlan_dot1qsupport(self): + ixlan = SHARED["ixlan_rw_ok"] + data = self.assert_get_handleref(self.db_org_admin, "ixlan", ixlan.id) + data.update(dot1q_support=True) + self.db_org_admin.update("ixlan", **data) + + data = self.assert_get_handleref(self.db_org_admin, "ixlan", ixlan.id) + assert data["dot1q_support"] is False + + ########################################################################## + def test_org_admin_002_POST_PUT_DELETE_ixpfx(self): data = self.make_data_ixpfx( ixlan_id=SHARED["ixlan_rw_ok"].id, prefix="206.126.236.0/25" @@ -1762,6 +1802,9 @@ class TestJSON(unittest.TestCase): # test protected ixpfx cant be deleted prefix = IXLanPrefix.objects.get(id=SHARED["ixpfx_id"]) + prefix.status = "ok" + prefix.save() + NetworkIXLan.objects.create( network=SHARED["net_rw_ok"], asn=SHARED["net_rw_ok"].asn, @@ -1839,7 +1882,7 @@ class TestJSON(unittest.TestCase): # When we create this netixlan it should fail with a # non-field-error. - r_data = self.assert_create( + self.assert_create( self.db_org_admin, "netixlan", data, @@ -1857,7 +1900,7 @@ class TestJSON(unittest.TestCase): # Also fails with network contact that is # missing an email - r_data = self.assert_create( + self.assert_create( self.db_org_admin, "netixlan", data, @@ -2497,9 +2540,7 @@ class TestJSON(unittest.TestCase): SHARED["netixlan_r_ok"].speed = 1000 SHARED["netixlan_r_ok"].save() - netixlans = NetworkIXLan.handleref.undeleted() - capacity_set = netixlans.values("ixlan_id").annotate(capacity=Sum("speed")) - capacity_dict = {d["ixlan_id"]: d["capacity"] for d in capacity_set} + NetworkIXLan.handleref.undeleted() data = self.db_guest.all("ix", capacity=1000) assert len(data) == 1 @@ -3192,6 +3233,29 @@ class TestJSON(unittest.TestCase): ########################################################################## + def test_guest_005_list_filter_ixfac_related_name(self): + data = self.db_guest.all("ixfac", name=SHARED["fac_rw_ok"].name) + self.assertEqual(len(data), 1) + self.assert_data_integrity(data[0], "ixfac") + + ########################################################################## + + def test_guest_005_list_filter_ixfac_related_city(self): + data = self.db_guest.all("ixfac", city=SHARED["fac_rw_ok"].city) + self.assertEqual(len(data), 2) + self.assert_data_integrity(data[0], "ixfac") + + ########################################################################## + + def test_guest_005_list_filter_ixfac_related_country(self): + data = self.db_guest.all( + "ixfac", country="{}".format(SHARED["fac_rw_ok"].country) + ) + self.assertEqual(len(data), 2) + self.assert_data_integrity(data[0], "ixfac") + + ########################################################################## + def test_guest_005_list_filter_poc_related(self): self.assert_list_filter_related("poc", "net") return @@ -3234,11 +3298,9 @@ class TestJSON(unittest.TestCase): # there regardless. org = Organization.objects.create(name="org unaccented", status="ok") - net = Network.objects.create( - asn=12345, name="net unaccented", status="ok", org=org - ) - ix = InternetExchange.objects.create(org=org, name="ix unaccented", status="ok") - fac = Facility.objects.create(org=org, name="fac unaccented", status="ok") + Network.objects.create(asn=12345, name="net unaccented", status="ok", org=org) + InternetExchange.objects.create(org=org, name="ix unaccented", status="ok") + Facility.objects.create(org=org, name="fac unaccented", status="ok") for tag in ["org", "net", "ix", "fac"]: data = self.db_guest.all(tag, name=f"{tag} unãccented") @@ -3735,19 +3797,19 @@ class TestJSON(unittest.TestCase): data = self.make_data_fac() db = self.db_org_admin del data["tech_phone"] - r = db.create("fac", data, return_response=True).get("data") + db.create("fac", data, return_response=True).get("data") data = self.make_data_fac() del data["sales_phone"] - r = db.create("fac", data, return_response=True).get("data") + db.create("fac", data, return_response=True).get("data") data = self.make_data_ix(prefix=self.get_prefix4()) del data["tech_phone"] - r = db.create("ix", data, return_response=True).get("data") + db.create("ix", data, return_response=True).get("data") data = self.make_data_ix(prefix=self.get_prefix4()) del data["policy_phone"] - r = db.create("ix", data, return_response=True).get("data") + db.create("ix", data, return_response=True).get("data") def test_z_misc_002_dupe_netixlan_ip(self): @@ -4023,7 +4085,7 @@ class TestJSON(unittest.TestCase): # # should we allow re suggesting of deleted facilities? - re_add_data = self.assert_create( + self.assert_create( self.db_user, "fac", data, test_success=False, test_failures={"perms": {}} ) diff --git a/peeringdb_server/management/commands/pdb_batch_replace.py b/peeringdb_server/management/commands/pdb_batch_replace.py index e68ca620..1b20aa62 100644 --- a/peeringdb_server/management/commands/pdb_batch_replace.py +++ b/peeringdb_server/management/commands/pdb_batch_replace.py @@ -2,6 +2,7 @@ import re import reversion from django.core.management.base import BaseCommand, CommandError +from django.db import transaction import peeringdb_server.models as pdbm @@ -31,6 +32,7 @@ class Command(BaseCommand): print(f"[{self.target}] {msg}") @reversion.create_revision() + @transaction.atomic() def handle(self, *args, **options): self.commit = options.get("commit", False) @@ -48,7 +50,7 @@ class Command(BaseCommand): try: search_field, search_value = self.search.split(":") ref_tag, search_field = search_field.split(".") - except: + except Exception: raise CommandError( "Format for --search: .:" ) @@ -58,7 +60,7 @@ class Command(BaseCommand): replace_field = m.group(1) replace_search_value = m.group(2) replace_value = m.group(3) - except: + except Exception: raise CommandError( "Format for --replace: ::" ) @@ -78,7 +80,7 @@ class Command(BaseCommand): for e in q: val = getattr(e, search_field) - if re.search(search_value, val) != None: + if re.search(search_value, val) is not None: t_val = getattr(e, replace_field) r_val = None if replace_search_value == "*": diff --git a/peeringdb_server/management/commands/pdb_cleanup_vq.py b/peeringdb_server/management/commands/pdb_cleanup_vq.py index d9fec125..41bdf9de 100644 --- a/peeringdb_server/management/commands/pdb_cleanup_vq.py +++ b/peeringdb_server/management/commands/pdb_cleanup_vq.py @@ -1,8 +1,7 @@ -from datetime import datetime, timedelta, timezone +from datetime import datetime, timedelta from django.conf import settings from django.contrib.contenttypes.models import ContentType -from django.core.management.base import BaseCommand from django.utils import timezone from peeringdb_server.management.commands.pdb_base_command import PeeringDBBaseCommand @@ -15,20 +14,18 @@ class Command(PeeringDBBaseCommand): def add_arguments(self, parser): super().add_arguments(parser) - subparsers = parser.add_subparsers() - parser_users = subparsers.add_parser( - "users", - parents=[parser], - add_help=False, - help="Tool to remove outdated user verification requests", + parser.add_argument( + "objtype", + nargs="?", + choices=["users"], + help="What objects should be cleaned up", ) - parser_users.set_defaults(func=self._clean_users) def handle(self, *args, **options): super().handle(*args, **options) - if options.get("func"): - options["func"]() + if options.get("objtype") == "users": + self._clean_users() def _clean_users(self): model = User diff --git a/peeringdb_server/management/commands/pdb_fac_merge.py b/peeringdb_server/management/commands/pdb_fac_merge.py index 436eb682..abebfaee 100644 --- a/peeringdb_server/management/commands/pdb_fac_merge.py +++ b/peeringdb_server/management/commands/pdb_fac_merge.py @@ -2,6 +2,7 @@ import re import reversion from django.core.management.base import BaseCommand, CommandError +from django.db import transaction import peeringdb_server.models as pdbm from peeringdb_server.mail import mail_users_entity_merge @@ -43,6 +44,7 @@ class Command(BaseCommand): self.stdout.write(msg) @reversion.create_revision() + @transaction.atomic() def handle(self, *args, **options): self.commit = options.get("commit", False) self.moved = [] diff --git a/peeringdb_server/management/commands/pdb_fac_merge_undo.py b/peeringdb_server/management/commands/pdb_fac_merge_undo.py index 5c4b5a39..fb6809c7 100644 --- a/peeringdb_server/management/commands/pdb_fac_merge_undo.py +++ b/peeringdb_server/management/commands/pdb_fac_merge_undo.py @@ -2,6 +2,7 @@ import re import reversion from django.core.management.base import BaseCommand +from django.db import transaction from peeringdb_server.models import ( CommandLineTool, @@ -33,6 +34,7 @@ class Command(BaseCommand): self.stdout.write(msg) @reversion.create_revision() + @transaction.atomic() def handle(self, *args, **options): self.commit = options.get("commit", False) self.log_file = options.get("log") @@ -66,7 +68,7 @@ class Command(BaseCommand): id__in=match.group(1).split(", ") ) } - target = Facility.objects.get(id=match.group(2)) + Facility.objects.get(id=match.group(2)) for source in list(sources.values()): if source.org.status != "ok": diff --git a/peeringdb_server/management/commands/pdb_fix_930_users.py b/peeringdb_server/management/commands/pdb_fix_930_users.py index d6d4bea2..78f78ac7 100644 --- a/peeringdb_server/management/commands/pdb_fix_930_users.py +++ b/peeringdb_server/management/commands/pdb_fix_930_users.py @@ -1,5 +1,3 @@ -from django.core.management.base import BaseCommand - from peeringdb_server.management.commands.pdb_base_command import PeeringDBBaseCommand from peeringdb_server.models import User diff --git a/peeringdb_server/management/commands/pdb_fix_status_history.py b/peeringdb_server/management/commands/pdb_fix_status_history.py index 7276f3eb..3ebfa9bc 100644 --- a/peeringdb_server/management/commands/pdb_fix_status_history.py +++ b/peeringdb_server/management/commands/pdb_fix_status_history.py @@ -1,8 +1,8 @@ import json import reversion -from django.core import serializers from django.core.management.base import BaseCommand +from django.db import transaction from peeringdb_server.models import REFTAG_MAP @@ -79,6 +79,7 @@ class Command(BaseCommand): self.log(f"{fixed} revisions fixed for {model.__name__}") @reversion.create_revision() + @transaction.atomic() def process_entity(self, entity, most_recent_version): # force revision date to be same as that of the most recent version # so status change is archived at the correct date diff --git a/peeringdb_server/management/commands/pdb_generate_test_data.py b/peeringdb_server/management/commands/pdb_generate_test_data.py index 435b13aa..b65e7e0c 100644 --- a/peeringdb_server/management/commands/pdb_generate_test_data.py +++ b/peeringdb_server/management/commands/pdb_generate_test_data.py @@ -1,8 +1,8 @@ -import googlemaps import reversion from django.conf import settings from django.contrib.auth.models import Group from django.core.management.base import BaseCommand +from django.db import transaction from peeringdb_server import models from peeringdb_server.mock import Mock @@ -49,6 +49,7 @@ class Command(BaseCommand): Group.objects.filter(name__startswith="org.").delete() @reversion.create_revision() + @transaction.atomic() def generate(self): self.entities = {k: [] for k in list(models.REFTAG_MAP.keys())} queue = [ diff --git a/peeringdb_server/management/commands/pdb_geo_normalize_existing.py b/peeringdb_server/management/commands/pdb_geo_normalize_existing.py index d69d1564..399f66b3 100644 --- a/peeringdb_server/management/commands/pdb_geo_normalize_existing.py +++ b/peeringdb_server/management/commands/pdb_geo_normalize_existing.py @@ -1,5 +1,4 @@ import csv -import datetime import os import re from pprint import pprint @@ -8,6 +7,7 @@ import reversion from django.conf import settings from django.core.exceptions import ValidationError from django.core.management.base import BaseCommand +from django.db import transaction from peeringdb_server import models from peeringdb_server.serializers import AddressSerializer @@ -151,6 +151,7 @@ class Command(BaseCommand): dict_writer.writerows(output_list) @reversion.create_revision() + @transaction.atomic() def normalize(self, reftag, _id, limit=0): model = models.REFTAG_MAP.get(reftag) if not model: diff --git a/peeringdb_server/management/commands/pdb_geosync.py b/peeringdb_server/management/commands/pdb_geosync.py index ba44d63f..77239212 100644 --- a/peeringdb_server/management/commands/pdb_geosync.py +++ b/peeringdb_server/management/commands/pdb_geosync.py @@ -2,6 +2,7 @@ import googlemaps import reversion from django.conf import settings from django.core.management.base import BaseCommand +from django.db import transaction from peeringdb_server import models @@ -47,6 +48,7 @@ class Command(BaseCommand): self.sync(reftag, _id, limit=limit) @reversion.create_revision() + @transaction.atomic() def sync(self, reftag, _id, limit=0): model = models.REFTAG_MAP.get(reftag) if not model: diff --git a/peeringdb_server/management/commands/pdb_ixf_ixp_member_import.py b/peeringdb_server/management/commands/pdb_ixf_ixp_member_import.py index 808181e4..844d6dcd 100644 --- a/peeringdb_server/management/commands/pdb_ixf_ixp_member_import.py +++ b/peeringdb_server/management/commands/pdb_ixf_ixp_member_import.py @@ -3,7 +3,7 @@ import sys import traceback from django.conf import settings -from django.core.management.base import BaseCommand, CommandError +from django.core.management.base import BaseCommand from django.db import transaction from peeringdb_server import ixf @@ -12,8 +12,8 @@ from peeringdb_server.models import ( IXFImportEmail, IXFMemberData, IXLan, + InternetExchange, Network, - NetworkIXLan, ) @@ -29,6 +29,15 @@ class Command(BaseCommand): parser.add_argument( "--ixlan", type=int, nargs="*", help="Only process these ixlans" ) + parser.add_argument( + "--process-requested", + "-P", + type=int, + nargs="?", + help="Process manually requested imports. Specify a number to limit amount of requests to be processed.", + default=None, + dest="process_requested", + ) parser.add_argument("--debug", action="store_true", help="Show debug output") parser.add_argument( "--preview", action="store_true", help="Run in preview mode" @@ -77,12 +86,16 @@ class Command(BaseCommand): else: self.stdout.write(f"[Pretend] {msg}") - def store_runtime_error(self, error, ixlan=None): + def store_runtime_error(self, error, ixlan=None, ixf_member_data=None): + if ixf_member_data: + ixlan = ixf_member_data.ixlan error_str = "" if ixlan: error_str += f"Ixlan {ixlan.ix.name} (id={ixlan.id})" + "\n" if hasattr(ixlan, "ixf_ixp_member_list_url"): error_str += f"IX-F url: {ixlan.ixf_ixp_member_list_url}" + "\n" + if ixf_member_data: + error_str += f"Proposal: {ixf_member_data} (id={ixf_member_data.id})\n" error_str += f"ERROR: {error}" + "\n" error_str += traceback.format_exc() @@ -184,6 +197,25 @@ class Command(BaseCommand): self.preview = options.get("preview", False) self.cache = options.get("cache", False) self.skip_import = options.get("skip_import", False) + process_requested = options.get("process_requested", None) + ixlan_ids = options.get("ixlan") + asn = options.get("asn", 0) + + # err and out should go to the same buffer (#967) + if not self.preview: + self.stderr = self.stdout + + if process_requested is not None: + ixlan_ids = [] + for ix in InternetExchange.ixf_import_request_queue(): + if ix.id not in ixlan_ids: + ixlan_ids.append(ix.id) + + if not ixlan_ids: + self.log("No manual import requests") + return + + self.log(f"Processing manual requests for: {ixlan_ids}") self.active_reset_flags = self.initiate_reset_flags(**options) @@ -204,9 +236,6 @@ class Command(BaseCommand): if self.preview and self.commit: self.commit = False - ixlan_ids = options.get("ixlan") - asn = options.get("asn", 0) - if asn and not ixlan_ids: # if asn is specified, retrieve queryset for ixlans from # the network object @@ -257,6 +286,10 @@ class Command(BaseCommand): except Exception as inst: self.store_runtime_error(inst, ixlan=ixlan) + finally: + if process_requested is not None: + ixlan.ix.ixf_import_request_status = "finished" + ixlan.ix.save_without_timestamp() if self.preview: self.stdout.write(json.dumps(total_log, indent=2)) @@ -271,7 +304,10 @@ class Command(BaseCommand): self.stdout.write(f"New Emails: {importer.emails}") - if len(self.runtime_errors) > 0: + num_errors = len(self.runtime_errors) + + if num_errors > 0: + self.stdout.write(f"Errors: {num_errors}\n\n") self.write_runtime_errors() sys.exit(1) diff --git a/peeringdb_server/management/commands/pdb_ixp_merge.py b/peeringdb_server/management/commands/pdb_ixp_merge.py index e54f9351..32feef07 100644 --- a/peeringdb_server/management/commands/pdb_ixp_merge.py +++ b/peeringdb_server/management/commands/pdb_ixp_merge.py @@ -1,5 +1,6 @@ import reversion from django.core.management.base import BaseCommand +from django.db import transaction import peeringdb_server.models as pdbm from peeringdb_server.mail import mail_users_entity_merge @@ -25,6 +26,7 @@ class Command(BaseCommand): print(msg) @reversion.create_revision() + @transaction.atomic() def handle(self, *args, **options): args = list(args) diff --git a/peeringdb_server/management/commands/pdb_load_data.py b/peeringdb_server/management/commands/pdb_load_data.py index 41650fec..9be4804b 100644 --- a/peeringdb_server/management/commands/pdb_load_data.py +++ b/peeringdb_server/management/commands/pdb_load_data.py @@ -1,13 +1,7 @@ -import datetime -import logging - from confu.schema import apply_defaults from django.conf import settings -from django.core.exceptions import ObjectDoesNotExist, ValidationError -from django.core.management import call_command from django.core.management.base import BaseCommand -from django.db.models.signals import post_save, pre_delete, pre_save -from django_handleref.models import HandleRefModel +from django.db.models.signals import pre_save from django_peeringdb import models as djpdb_models import peeringdb._fetch diff --git a/peeringdb_server/management/commands/pdb_migrate_ixlans.py b/peeringdb_server/management/commands/pdb_migrate_ixlans.py index 089d49b6..a08e7452 100644 --- a/peeringdb_server/management/commands/pdb_migrate_ixlans.py +++ b/peeringdb_server/management/commands/pdb_migrate_ixlans.py @@ -5,8 +5,7 @@ import reversion from django.contrib.admin.models import LogEntry from django.contrib.contenttypes.models import ContentType from django.core.management.base import BaseCommand -from django.db import connection -from django.db.models import F +from django.db import connection, transaction from peeringdb_server.models import ( UTC, @@ -160,6 +159,7 @@ class Command(BaseCommand): self.log("Phase 1: Done") @reversion.create_revision() + @transaction.atomic() def create_missing_ixlan(self, ix): """ Creates an ixlan for an ix that doesn't have one @@ -214,6 +214,7 @@ class Command(BaseCommand): self.log("Phase 2: Done") @reversion.create_revision() + @transaction.atomic() def reparent_ixlan(self, ixlan): """ @@ -311,6 +312,7 @@ class Command(BaseCommand): self.post_migration_checks() + @transaction.atomic() def migrate_ixlan_id(self, ixlan, ixlans, trigger=None, tmp_id=False): """ diff --git a/peeringdb_server/management/commands/pdb_renumber_lans.py b/peeringdb_server/management/commands/pdb_renumber_lans.py index e4430875..06b72650 100644 --- a/peeringdb_server/management/commands/pdb_renumber_lans.py +++ b/peeringdb_server/management/commands/pdb_renumber_lans.py @@ -3,6 +3,7 @@ import ipaddress import reversion from django.core.exceptions import ValidationError from django.core.management.base import BaseCommand +from django.db import transaction from peeringdb_server.inet import renumber_ipaddress from peeringdb_server.models import IXLanPrefix, NetworkIXLan @@ -33,6 +34,7 @@ class Command(BaseCommand): self.stdout.write(msg) @reversion.create_revision() + @transaction.atomic() def renumber_lans(self, old, new): """ Renumber prefix and netixlan's that fall into that prefix @@ -55,8 +57,8 @@ class Command(BaseCommand): if self.ixlan: self.log(f"Only replacing in ixlan {self.ixlan.descriptive_name}") - prefixes = prefixes.filter(ixlan=ixlan) - netixlans = netixlans.filter(ixlan=ixlan) + prefixes = prefixes.filter(ixlan=self.ixlan) + netixlans = netixlans.filter(ixlan=self.ixlan) for prefix in prefixes: self.log(f"Renumbering {prefix.descriptive_name} -> {new_prefix}") diff --git a/peeringdb_server/management/commands/pdb_reversion_inspect.py b/peeringdb_server/management/commands/pdb_reversion_inspect.py index 28b1911e..6ef33321 100644 --- a/peeringdb_server/management/commands/pdb_reversion_inspect.py +++ b/peeringdb_server/management/commands/pdb_reversion_inspect.py @@ -36,7 +36,6 @@ class Command(BaseCommand): def handle(self, *args, **options): - versions = Version.objects.all() ref_tag = options.get("reftag") ids = [int(i) for i in options.get("id")] diff --git a/peeringdb_server/management/commands/pdb_sponsorship_notify.py b/peeringdb_server/management/commands/pdb_sponsorship_notify.py index 2f8965ea..c3afe1cf 100644 --- a/peeringdb_server/management/commands/pdb_sponsorship_notify.py +++ b/peeringdb_server/management/commands/pdb_sponsorship_notify.py @@ -19,6 +19,6 @@ class Command(BaseCommand): sponsorship.notify_date is None or sponsorship.notify_date < sponsorship.end_date ): - b = sponsorship.notify_expiration() + sponsorship.notify_expiration() # if b: # self.log("Sent expiration notices for %s, expired on %s" % (sponsorship.org.name, sponsorship.end_date)) diff --git a/peeringdb_server/management/commands/pdb_stats.py b/peeringdb_server/management/commands/pdb_stats.py index e6fdbdee..50ccd009 100644 --- a/peeringdb_server/management/commands/pdb_stats.py +++ b/peeringdb_server/management/commands/pdb_stats.py @@ -1,11 +1,9 @@ import datetime import json -import reversion from django.contrib.auth import get_user_model -from django.contrib.contenttypes.models import ContentType from django.core.management.base import BaseCommand -from reversion.models import Revision, Version +from reversion.models import Version from peeringdb_server.models import REFTAG_MAP, UTC diff --git a/peeringdb_server/management/commands/pdb_undelete.py b/peeringdb_server/management/commands/pdb_undelete.py index f40f9865..9055b21f 100644 --- a/peeringdb_server/management/commands/pdb_undelete.py +++ b/peeringdb_server/management/commands/pdb_undelete.py @@ -1,5 +1,4 @@ import json -import re import reversion from django.core.management.base import BaseCommand @@ -114,7 +113,7 @@ class Command(BaseCommand): ) try: status = json.loads(version.serialized_data)[0].get("fields")["status"] - except: + except Exception: status = None if status == "deleted": self.log_warn( @@ -132,7 +131,7 @@ class Command(BaseCommand): # relation parent try: relation = getattr(obj, field.name) - except: + except Exception: continue if relation.status == "deleted" and relation != parent: can_undelete_obj = False @@ -167,7 +166,7 @@ class Command(BaseCommand): # relation child try: relation = getattr(obj, field.name) - except: + except Exception: continue if not hasattr(field.related_model, "ref_tag"): continue diff --git a/peeringdb_server/management/commands/pdb_whois.py b/peeringdb_server/management/commands/pdb_whois.py index c472e598..f05f7bba 100644 --- a/peeringdb_server/management/commands/pdb_whois.py +++ b/peeringdb_server/management/commands/pdb_whois.py @@ -7,7 +7,7 @@ from peeringdb.whois import WhoisFormat from peeringdb_server import models, serializers from peeringdb_server.util import APIPermissionsApplicator -from ._db_command import CommandError, DBCommand +from ._db_command import DBCommand class Command(DBCommand): @@ -29,7 +29,7 @@ class Command(DBCommand): log.error("Unknown query type '%s'" % (args)) return # TODO - raise CommandError("unk query") + # raise CommandError("unk query") model = None diff --git a/peeringdb_server/management/commands/pdb_wipe.py b/peeringdb_server/management/commands/pdb_wipe.py index f199c0df..d9d77e98 100644 --- a/peeringdb_server/management/commands/pdb_wipe.py +++ b/peeringdb_server/management/commands/pdb_wipe.py @@ -1,15 +1,8 @@ from django.conf import settings -from django.contrib.auth.models import Group from django.core.management import call_command from django.core.management.base import BaseCommand -from peeringdb_server.models import ( - REFTAG_MAP, - NetworkContact, - Partnership, - Sponsorship, - User, -) +from peeringdb_server.models import REFTAG_MAP, Partnership, Sponsorship, User class Command(BaseCommand): diff --git a/peeringdb_server/migrations/0025_E164_phonenumbers.py b/peeringdb_server/migrations/0025_E164_phonenumbers.py index b754b2ad..811c1e9c 100644 --- a/peeringdb_server/migrations/0025_E164_phonenumbers.py +++ b/peeringdb_server/migrations/0025_E164_phonenumbers.py @@ -78,6 +78,18 @@ def forwards_func(apps, schema_editor): InternetExchange = apps.get_model("peeringdb_server", "InternetExchange") NetworkContact = apps.get_model("peeringdb_server", "NetworkContact") + invalid = [] + fixed = [] + + for ix in InternetExchange.handleref.filter(status__in=["ok", "pending"]): + _fix_number("ix", ix, "tech_phone", fixed, invalid) + _fix_number("ix", ix, "policy_phone", fixed, invalid) + + for poc in NetworkContact.handleref.filter(status__in=["ok", "pending"]): + _fix_number("poc", poc, "phone", fixed, invalid) + + """ This was used in production as a one time process + headers_invalid = [ "type", "id", @@ -98,17 +110,6 @@ def forwards_func(apps, schema_editor): "country", ] - invalid = [] - fixed = [] - - for ix in InternetExchange.handleref.filter(status__in=["ok", "pending"]): - _fix_number("ix", ix, "tech_phone", fixed, invalid) - _fix_number("ix", ix, "policy_phone", fixed, invalid) - - for poc in NetworkContact.handleref.filter(status__in=["ok", "pending"]): - _fix_number("poc", poc, "phone", fixed, invalid) - - """ This was used in production as a one time process print( "Invalid numbers: {} - written to invalid_phonenumbers.csv".format(len(invalid)) diff --git a/peeringdb_server/migrations/0044_ixlan_ixf_fields.py b/peeringdb_server/migrations/0044_ixlan_ixf_fields.py index a633a8a2..430e7355 100644 --- a/peeringdb_server/migrations/0044_ixlan_ixf_fields.py +++ b/peeringdb_server/migrations/0044_ixlan_ixf_fields.py @@ -2,54 +2,6 @@ from django.conf import settings from django.db import migrations, models -from django_namespace_perms.constants import PERM_CRUD, PERM_READ - - -def add_permissions(apps, schema_editor): - Group = apps.get_model("auth", "Group") - Organization = apps.get_model("peeringdb_server", "Organization") - GroupPermission = apps.get_model("django_namespace_perms", "GroupPermission") - - guest_group = Group.objects.filter(id=settings.GUEST_GROUP_ID).first() - user_group = Group.objects.filter(id=settings.USER_GROUP_ID).first() - - namespace = ( - "peeringdb.organization.{org}.internetexchange" - ".*.ixf_ixp_member_list_url.{visible}" - ) - - if guest_group: - GroupPermission.objects.create( - group=guest_group, - namespace=namespace.format(org="*", visible="public"), - permissions=PERM_READ, - ) - - if user_group: - GroupPermission.objects.create( - group=user_group, - namespace=namespace.format(org="*", visible="public"), - permissions=PERM_READ, - ) - - GroupPermission.objects.create( - group=user_group, - namespace=namespace.format(org="*", visible="users"), - permissions=PERM_READ, - ) - - for org in Organization.handleref.all(): - - GroupPermission.objects.create( - group=Group.objects.get(name=f"org.{org.id}.admin"), - namespace=namespace.format(org=org.id, visible="private"), - permissions=PERM_CRUD, - ) - GroupPermission.objects.create( - group=Group.objects.get(name=f"org.{org.id}"), - namespace=namespace.format(org=org.id, visible="private"), - permissions=PERM_READ, - ) class Migration(migrations.Migration): @@ -59,7 +11,6 @@ class Migration(migrations.Migration): ] operations = [ - migrations.RunPython(add_permissions), migrations.AddField( model_name="ixlan", name="ixf_ixp_member_list_url_visible", diff --git a/peeringdb_server/migrations/0060_nsp_to_grainy.py b/peeringdb_server/migrations/0060_nsp_to_grainy.py index 211d15f7..e8f1f315 100644 --- a/peeringdb_server/migrations/0060_nsp_to_grainy.py +++ b/peeringdb_server/migrations/0060_nsp_to_grainy.py @@ -2,28 +2,11 @@ from django.db import migrations - -def nsp_to_grainy(apps, schema_editor): - NSP_UserPermission = apps.get_model("django_namespace_perms", "UserPermission") - NSP_GroupPermission = apps.get_model("django_namespace_perms", "GroupPermission") - G_UserPermission = apps.get_model("django_grainy", "UserPermission") - G_GroupPermission = apps.get_model("django_grainy", "GroupPermission") - - for uperm in NSP_UserPermission.objects.all(): - print( - f"migrating {uperm.namespace} for user {uperm.user_id}: {uperm.permissions}" - ) - G_UserPermission.objects.get_or_create( - namespace=uperm.namespace, permission=uperm.permissions, user=uperm.user - ) - - for gperm in NSP_GroupPermission.objects.all(): - print( - f"migrating {gperm.namespace} for group {gperm.group_id}: {gperm.permissions}" - ) - G_GroupPermission.objects.get_or_create( - namespace=gperm.namespace, permission=gperm.permissions, group=gperm.group - ) +# django-namespaces-perms has been removed entirely +# this migration used to move nsp permissions to the +# grainy framework. +# +# empty migration until we squash class Migration(migrations.Migration): @@ -32,6 +15,4 @@ class Migration(migrations.Migration): ("peeringdb_server", "0059_ixf_typos"), ] - operations = [ - migrations.RunPython(nsp_to_grainy), - ] + operations = [] diff --git a/peeringdb_server/migrations/0073_manual_ixf_import.py b/peeringdb_server/migrations/0073_manual_ixf_import.py new file mode 100644 index 00000000..1e9ae53e --- /dev/null +++ b/peeringdb_server/migrations/0073_manual_ixf_import.py @@ -0,0 +1,52 @@ +# Generated by Django 3.2.5 on 2021-07-30 08:57 + +from django.conf import settings +from django.db import migrations, models +import django.db.models.deletion + + +class Migration(migrations.Migration): + + dependencies = [ + ("peeringdb_server", "0072_geocoordinatecache"), + ] + + operations = [ + migrations.AddField( + model_name="internetexchange", + name="ixf_import_request", + field=models.DateTimeField( + blank=True, + help_text="Date of most recent manual import request", + null=True, + verbose_name="Manual IX-F import request", + ), + ), + migrations.AddField( + model_name="internetexchange", + name="ixf_import_request_status", + field=models.CharField( + choices=[ + ("queued", "Queued"), + ("importing", "Importing"), + ("finished", "Finished"), + ], + default="queued", + help_text="The current status of the manual ix-f import request", + max_length=32, + verbose_name="Manual IX-F import status", + ), + ), + migrations.AddField( + model_name="internetexchange", + name="ixf_import_request_user", + field=models.ForeignKey( + blank=True, + help_text="The user that triggered the manual ix-f import request", + null=True, + on_delete=django.db.models.deletion.SET_NULL, + related_name="requested_ixf_imports", + to=settings.AUTH_USER_MODEL, + ), + ), + ] diff --git a/peeringdb_server/mock.py b/peeringdb_server/mock.py index b44d046f..546f422e 100644 --- a/peeringdb_server/mock.py +++ b/peeringdb_server/mock.py @@ -1,4 +1,3 @@ -import datetime import ipaddress import uuid @@ -283,6 +282,15 @@ class Mock: def ixf_last_import(self, data, reftag=None): return None + def ixf_import_request(self, data, reftag=None): + return None + + def ixf_import_request_status(self, data, reftag=None): + return "queued" + + def ixf_import_request_user(self, data, reftag=None): + return None + def ix_count(self, data, reftag=None): return 0 diff --git a/peeringdb_server/models.py b/peeringdb_server/models.py index f4d53ecf..6e3920ae 100644 --- a/peeringdb_server/models.py +++ b/peeringdb_server/models.py @@ -4,12 +4,9 @@ import json import re import uuid from itertools import chain -from pprint import pprint import django.urls -import django_grainy.decorators import django_peeringdb.models as pdb_models -import requests import reversion from allauth.account.models import EmailAddress, EmailConfirmation from allauth.socialaccount.models import SocialAccount @@ -29,7 +26,6 @@ from django.db import models, transaction from django.template import loader from django.utils import timezone from django.utils.functional import Promise -from django.utils.html import strip_tags from django.utils.http import urlquote from django.utils.translation import override from django.utils.translation import ugettext_lazy as _ @@ -181,8 +177,6 @@ class URLField(pdb_models.URLField): local defaults for URLField """ - pass - class ValidationErrorEncoder(json.JSONEncoder): def default(self, obj): @@ -228,8 +222,10 @@ class ProtectedMixin: return getattr(self, "_not_deletable_reason", None) def delete(self, hard=False, force=False): - if not self.deletable and not force: - raise ProtectedAction(self) + + if self.status in ["ok", "pending"]: + if not self.deletable and not force: + raise ProtectedAction(self) self.delete_cleanup() return super().delete(hard=hard) @@ -242,6 +238,13 @@ class ProtectedMixin: """ return + def save_without_timestamp(self): + self._meta.get_field("updated").auto_now = False + try: + self.save() + finally: + self._meta.get_field("updated").auto_now = True + class GeocodeBaseMixin(models.Model): """ @@ -695,6 +698,7 @@ class VerificationQueueItem(models.Model): ) @reversion.create_revision() + @transaction.atomic() def approve(self): """ Approve the verification queue item @@ -885,7 +889,7 @@ class Organization(ProtectedMixin, pdb_models.OrganizationBase, GeocodeBaseMixin rdap = RdapLookup().get_asn(net.asn) if rdap: r[net.asn] = rdap - except RdapNotFoundError as inst: + except RdapNotFoundError: pass return r @@ -1000,6 +1004,7 @@ class Organization(ProtectedMixin, pdb_models.OrganizationBase, GeocodeBaseMixin @classmethod @reversion.create_revision() + @transaction.atomic() def create_from_rdap(cls, rdap, asn, org_name=None): """ Creates organization from rdap result object @@ -1576,6 +1581,34 @@ class InternetExchange(ProtectedMixin, pdb_models.InternetExchangeBase): Describes a peeringdb exchange """ + ixf_import_request = models.DateTimeField( + _("Manual IX-F import request"), + help_text=_("Date of most recent manual import request"), + null=True, + blank=True, + ) + + ixf_import_request_status = models.CharField( + _("Manual IX-F import status"), + help_text=_("The current status of the manual ix-f import request"), + choices=( + ("queued", _("Queued")), + ("importing", _("Importing")), + ("finished", _("Finished")), + ), + max_length=32, + default="queued", + ) + + ixf_import_request_user = models.ForeignKey( + "peeringdb_server.User", + null=True, + blank=True, + help_text=_("The user that triggered the manual ix-f import request"), + on_delete=models.SET_NULL, + related_name="requested_ixf_imports", + ) + org = models.ForeignKey( Organization, on_delete=models.CASCADE, related_name="ix_set" ) @@ -1841,6 +1874,17 @@ class InternetExchange(ProtectedMixin, pdb_models.InternetExchangeBase): qset = qset.filter(id__in=qualifying) return qset + @classmethod + def ixf_import_request_queue(cls, limit=0): + qset = InternetExchange.objects.filter( + ixf_import_request__isnull=False, ixf_import_request_status="queued" + ).order_by("ixf_import_request") + + if limit: + qset = qset[:limit] + + return qset + @property def ixlan(self): """ @@ -1974,6 +2018,43 @@ class InternetExchange(ProtectedMixin, pdb_models.InternetExchangeBase): self._not_deletable_reason = None return True + @property + def ixf_import_request_recent_status(self): + """ + Returns the recent ixf import request status as a tuple + of value, display + """ + + if not self.ixf_import_request: + return "", "" + + value = self.ixf_import_request_status + display = self.get_ixf_import_request_status_display + + if self.ixf_import_request_status == "queued": + return value, display + + now = timezone.now() + delta = (now - self.ixf_import_request).total_seconds() + + if delta < 3600: + return value, display + + return "", "" + + @property + def ixf_import_css(self): + """ + returns the appropriate bootstrap alert class + depending on recent import request status + """ + status, _ = self.ixf_import_request_recent_status + if status == "queued": + return "alert alert-warning" + if status == "finished": + return "alert alert-success" + return "" + def vq_approve(self): """ Called when internet exchange is approved in verification @@ -2020,6 +2101,17 @@ class InternetExchange(ProtectedMixin, pdb_models.InternetExchangeBase): def clean(self): self.validate_phonenumbers() + def request_ixf_import(self, user=None): + self.ixf_import_request = timezone.now() + + if self.ixf_import_request_status == "importing": + raise ValidationError({"non_field_errors": ["Import is currently ongoing"]}) + + self.ixf_import_request_status = "queued" + self.ixf_import_request_user = user + + self.save_without_timestamp() + @grainy_model(namespace="ixfac", parent="ix") @reversion.register @@ -2035,6 +2127,44 @@ class InternetExchangeFacility(pdb_models.InternetExchangeFacilityBase): Facility, on_delete=models.CASCADE, default=0, related_name="ixfac_set" ) + @classmethod + def related_to_name(cls, value=None, filt=None, field="facility__name", qset=None): + """ + Filter queryset of ixfac objects related to facilities with name match + in facility__name according to filter + + Relationship through facility + """ + if not qset: + qset = cls.handleref.undeleted() + return qset.filter(**make_relation_filter(field, filt, value)) + + @classmethod + def related_to_country( + cls, value=None, filt=None, field="facility__country", qset=None + ): + """ + Filter queryset of ixfac objects related to country via match + in facility__country according to filter + + Relationship through facility + """ + if not qset: + qset = cls.handleref.filter(status="ok") + return qset.filter(**make_relation_filter(field, filt, value)) + + @classmethod + def related_to_city(cls, value=None, filt=None, field="facility__city", qset=None): + """ + Filter queryset of ixfac objects related to city via match + in facility__city according to filter + + Relationship through facility + """ + if not qset: + qset = cls.handleref.undeleted() + return qset.filter(**make_relation_filter(field, filt, value)) + @property def descriptive_name(self): """ @@ -2222,6 +2352,7 @@ class IXLan(pdb_models.IXLanBase): return super().clean() @reversion.create_revision() + @transaction.atomic() def add_netixlan(self, netixlan_info, save=True, save_others=True): """ This function allows for sane adding of netixlan object under @@ -2348,7 +2479,6 @@ class IXLan(pdb_models.IXLanBase): ixlan=self, network=netixlan_info.network, status="ok" ) created = True - reason = "New ip-address" # now we sync the data to our determined netixlan instance @@ -2456,6 +2586,7 @@ class IXLanIXFMemberImportLog(models.Model): verbose_name_plural = _("IX-F Import Logs") @reversion.create_revision() + @transaction.atomic() def rollback(self): """ Attempt to rollback the changes described in this log @@ -2471,7 +2602,7 @@ class IXLanIXFMemberImportLog(models.Model): for _entry in related.order_by("-id"): try: _entry.version_before.revert() - except: + except Exception: break elif entry.netixlan.status == "ok": @@ -2840,7 +2971,7 @@ class IXFMemberData(pdb_models.NetworkIXLanBase): for ixf_member_data in qset: action = ixf_member_data.action - error = ixf_member_data.error + ixf_member_data.error # not actionable for anyone @@ -2932,7 +3063,7 @@ class IXFMemberData(pdb_models.NetworkIXLanBase): try: error_data = json.loads(self.error) - except: + except Exception: return None IPADDR_EXIST = "already exists" @@ -3025,7 +3156,7 @@ class IXFMemberData(pdb_models.NetworkIXLanBase): @property def actionable_changes(self): - requirements = self.requirements + self.requirements _changes = self.changes @@ -3196,7 +3327,7 @@ class IXFMemberData(pdb_models.NetworkIXLanBase): Will return either "add", "modify", "delete" or "noop" """ - has_data = self.remote_data_missing == False + has_data = self.remote_data_missing is False action = "noop" @@ -3451,7 +3582,7 @@ class IXFMemberData(pdb_models.NetworkIXLanBase): action = self.action netixlan = self.netixlan - changes = self.changes + self.changes if action == "add": self.validate_speed() @@ -3788,7 +3919,7 @@ class IXLanPrefix(ProtectedMixin, pdb_models.IXLanPrefixBase): except ipaddress.AddressValueError: return False - except ValueError as inst: + except ValueError: return False @property @@ -3895,6 +4026,7 @@ class Network(pdb_models.NetworkBase): @classmethod @reversion.create_revision() + @transaction.atomic() def create_from_rdap(cls, rdap, asn, org): """ Creates network from rdap result object @@ -4361,7 +4493,7 @@ class NetworkIXLan(pdb_models.NetworkIXLanBase): as a unqiue record by asn, ip4 and ip6 address """ - net = self.network + self.network return (self.asn, self.ipaddr4, self.ipaddr6) @property diff --git a/peeringdb_server/org_admin_views.py b/peeringdb_server/org_admin_views.py index fce47b0a..0731082b 100644 --- a/peeringdb_server/org_admin_views.py +++ b/peeringdb_server/org_admin_views.py @@ -10,13 +10,12 @@ from django.utils.translation import ugettext_lazy as _ from django.views.decorators.csrf import csrf_protect from django_grainy.models import UserPermission from django_handleref.models import HandleRefModel -from grainy.const import * +from grainy.const import PERM_READ from peeringdb_server.models import ( Facility, InternetExchange, Network, - NetworkContact, Organization, User, UserOrgAffiliationRequest, @@ -485,8 +484,6 @@ def uoar_approve(request, **kwargs): except UserOrgAffiliationRequest.DoesNotExist: return JsonResponse({"status": "ok"}) - return JsonResponse({"status": "ok"}) - @login_required @csrf_protect @@ -502,9 +499,8 @@ def uoar_deny(request, **kwargs): uoar = UserOrgAffiliationRequest.objects.get(id=request.POST.get("id")) if uoar.org != org: return JsonResponse({}, status=403) - try: - user = uoar.user + uoar.user uoar.deny() except User.DoesNotExist: diff --git a/peeringdb_server/rest.py b/peeringdb_server/rest.py index a18550a2..88c45435 100644 --- a/peeringdb_server/rest.py +++ b/peeringdb_server/rest.py @@ -13,16 +13,14 @@ from django.db import connection from django.db.models import DateTimeField from django.utils import timezone from django.utils.translation import ugettext_lazy as _ -from django_grainy.rest import ModelViewSetPermissions, PermissionDenied -from haystack.query import SearchQuerySet -from rest_framework import routers, serializers, status, viewsets -from rest_framework.exceptions import ParseError -from rest_framework.exceptions import ValidationError as RestValidationError +from django_grainy.rest import PermissionDenied +from rest_framework import routers, status, viewsets +from rest_framework.decorators import action +from rest_framework.exceptions import ParseError, ValidationError as RestValidationError from rest_framework.response import Response from rest_framework.views import exception_handler from peeringdb_server.api_cache import APICacheLoader, CacheRedirect -from peeringdb_server.api_schema import BaseSchema from peeringdb_server.deskpro import ticket_queue_deletion_prevented from peeringdb_server.models import UTC, Network, ProtectedAction from peeringdb_server.permissions import ( @@ -35,6 +33,7 @@ from peeringdb_server.permissions import ( from peeringdb_server.search import make_name_search_query from peeringdb_server.serializers import ParentStatusException from peeringdb_server.util import coerce_ipaddr +from peeringdb_server.rest_throttles import IXFImportThrottle class DataException(ValueError): @@ -99,8 +98,8 @@ class RestRouter(routers.DefaultRouter): initkwargs={"suffix": "Instance"}, ), routers.DynamicRoute( - url=r"^{prefix}/{lookup}/{methodnamehyphen}$", - name="{basename}-{methodnamehyphen}", + url=r"^{prefix}/{lookup}/{url_path}$", + name="{basename}-{url_name}", detail=True, initkwargs={}, ), @@ -108,12 +107,10 @@ class RestRouter(routers.DefaultRouter): # Generated using @action or @link decorators on methods of the # viewset. routers.Route( - url=r"^{prefix}/{lookup}/{methodname}{trailing_slash}$", - mapping={ - "{httpmethod}": "{methodname}", - }, - name="{basename}-{methodnamehyphen}", - detail=False, + url=r"^{prefix}/{lookup}/{url_path}{trailing_slash}$", + name="{basename}-{url_name}", + mapping={}, + detail=True, initkwargs={}, ), ] @@ -302,7 +299,7 @@ class ModelViewSet(viewsets.ModelViewSet): raise RestValidationError({"detail": str(inst)}) except TypeError as inst: raise RestValidationError({"detail": str(inst)}) - except FieldError as inst: + except FieldError: raise RestValidationError({"detail": "Invalid query"}) else: @@ -398,7 +395,7 @@ class ModelViewSet(viewsets.ModelViewSet): # filter by function provided in suffix try: intyp = field_names.get(flt).get_internal_type() - except: + except Exception: intyp = "CharField" # for greater than date checks we want to force the time to 1 @@ -434,7 +431,7 @@ class ModelViewSet(viewsets.ModelViewSet): # filter exact matches try: intyp = field_names.get(k).get_internal_type() - except: + except Exception: intyp = "CharField" if intyp == "ForeignKey": filters["%s_id" % k] = v @@ -461,7 +458,7 @@ class ModelViewSet(viewsets.ModelViewSet): raise RestValidationError({"detail": str(inst[0])}) except TypeError as inst: raise RestValidationError({"detail": str(inst[0])}) - except FieldError as inst: + except FieldError: raise RestValidationError({"detail": "Invalid query"}) # check if request qualifies for a cache load @@ -604,7 +601,7 @@ class ModelViewSet(viewsets.ModelViewSet): if "_grainy" in r.data: del r.data["_grainy"] return r - except PermissionDenied as inst: + except PermissionDenied: return Response(status=status.HTTP_403_FORBIDDEN) except (ParentStatusException, DataException) as inst: return Response( @@ -637,7 +634,7 @@ class ModelViewSet(viewsets.ModelViewSet): del r.data["_grainy"] return r - except PermissionDenied as inst: + except PermissionDenied: return Response(status=status.HTTP_403_FORBIDDEN) except TypeError as inst: return Response( @@ -699,6 +696,36 @@ class ModelViewSet(viewsets.ModelViewSet): self.get_serializer().finalize_delete(request) +class InternetExchangeMixin: + + """ + Custom api endpoints for the internet exchange + object, exposed to api/ix/{id}/{action} + """ + + @action(detail=True, methods=["POST"], throttle_classes=[IXFImportThrottle]) + def request_ixf_import(self, request, *args, **kwargs): + + """ + Allows managers of an ix to request an ix-f import + #779 + """ + + ix = self.get_object() + + if not check_permissions_from_request(request, ix, "u"): + return Response(status=status.HTTP_403_FORBIDDEN) + + if request.user.is_authenticated: + # user or user api key + ix.request_ixf_import(request.user) + else: + # org key + ix.request_ixf_import() + + return self.retrieve(request, *args, **kwargs) + + # TODO: why are we doing the import like this??! pdb_serializers = importlib.import_module("peeringdb_server.serializers") router = RestRouter(trailing_slash=False) @@ -710,7 +737,7 @@ def ref_dict(): return {tag: view.model for tag, view, na in router.registry} -def model_view_set(model, methods=None): +def model_view_set(model, methods=None, mixins=None): """ shortcut for peeringdb models to generate viewset and register in the API urls """ @@ -727,7 +754,10 @@ def model_view_set(model, methods=None): } # create the type - viewset_t = type(model + "ViewSet", (ModelViewSet,), clsdict) + if not mixins: + viewset_t = type(model + "ViewSet", (ModelViewSet,), clsdict) + else: + viewset_t = type(model + "ViewSet", mixins + (ModelViewSet,), clsdict) if methods: viewset_t.http_method_names = methods @@ -740,7 +770,9 @@ def model_view_set(model, methods=None): FacilityViewSet = model_view_set("Facility") -InternetExchangeViewSet = model_view_set("InternetExchange") +InternetExchangeViewSet = model_view_set( + "InternetExchange", mixins=(InternetExchangeMixin,) +) InternetExchangeFacilityViewSet = model_view_set("InternetExchangeFacility") IXLanViewSet = model_view_set("IXLan", methods=["get", "put"]) IXLanPrefixViewSet = model_view_set("IXLanPrefix") @@ -809,6 +841,7 @@ class ASSetViewSet(ReadOnlyMixin, viewsets.ModelViewSet): router.register("as_set", ASSetViewSet, basename="as_set") + # set here in case we want to add more urls later urls = router.urls diff --git a/peeringdb_server/rest_throttles.py b/peeringdb_server/rest_throttles.py index 645b46fb..911dfb99 100644 --- a/peeringdb_server/rest_throttles.py +++ b/peeringdb_server/rest_throttles.py @@ -5,6 +5,15 @@ from rest_framework.exceptions import PermissionDenied from peeringdb_server.permissions import get_org_key_from_request, get_user_from_request +class IXFImportThrottle(throttling.UserRateThrottle): + scope = "ixf_import_request" + + def get_cache_key(self, request, view): + key = super().get_cache_key(request, view) + ix = view.get_object() + return f"{key}.{ix.id}" + + class FilterThrottle(throttling.SimpleRateThrottle): """ diff --git a/peeringdb_server/search.py b/peeringdb_server/search.py index 9e1dcba4..505e6db8 100644 --- a/peeringdb_server/search.py +++ b/peeringdb_server/search.py @@ -8,12 +8,8 @@ from haystack.query import SearchQuerySet from peeringdb_server.models import ( Facility, InternetExchange, - InternetExchangeFacility, - IXLan, IXLanPrefix, Network, - NetworkContact, - NetworkFacility, NetworkIXLan, Organization, ) @@ -110,7 +106,7 @@ def search(term, autocomplete=False): for sq in search_query[:limit]: model = sq.model - tag = model.HandleRef.tag + model.HandleRef.tag categorize(sq, result, pk_map) diff --git a/peeringdb_server/search_indexes.py b/peeringdb_server/search_indexes.py index 15732292..10579e2c 100644 --- a/peeringdb_server/search_indexes.py +++ b/peeringdb_server/search_indexes.py @@ -9,12 +9,8 @@ from haystack import indexes from peeringdb_server.models import ( Facility, InternetExchange, - InternetExchangeFacility, - IXLan, IXLanPrefix, Network, - NetworkContact, - NetworkFacility, NetworkIXLan, Organization, ) diff --git a/peeringdb_server/serializers.py b/peeringdb_server/serializers.py index 191fc6f9..e10dc628 100644 --- a/peeringdb_server/serializers.py +++ b/peeringdb_server/serializers.py @@ -1,26 +1,23 @@ import ipaddress import re -import reversion from django.conf import settings from django.contrib.contenttypes.models import ContentType from django.core.exceptions import FieldError, ValidationError from django.core.validators import URLValidator -from django.db import IntegrityError, models, transaction -from django.db.models import Case, IntegerField, Prefetch, Q, Sum, When +from django.db.models import Prefetch from django.db.models.expressions import RawSQL from django.db.models.fields.related import ( ForwardManyToOneDescriptor, ReverseManyToOneDescriptor, ) from django.db.models.query import QuerySet -from django.http import JsonResponse from django.utils.translation import ugettext_lazy as _ from django_grainy.rest import PermissionDenied # from drf_toolbox import serializers from django_handleref.rest.serializers import HandleRefSerializer -from django_inet.rest import IPAddressField, IPPrefixField +from django_inet.rest import IPAddressField, IPNetworkField from django_peeringdb.models.abstract import AddressModel from rest_framework import serializers, validators from rest_framework.exceptions import ValidationError as RestValidationError @@ -32,7 +29,6 @@ from peeringdb_server.deskpro import ( from peeringdb_server.inet import ( RdapException, RdapLookup, - RdapNotFoundError, get_prefix_protocol, rdap_pretty_error_message, ) @@ -49,7 +45,6 @@ from peeringdb_server.models import ( NetworkFacility, NetworkIXLan, Organization, - OrganizationAPIKey, VerificationQueueItem, ) from peeringdb_server.permissions import ( @@ -487,7 +482,7 @@ class AsnRdapValidator: asn = attrs.get(self.field) try: rdap = RdapLookup().get_asn(asn) - emails = rdap.emails + rdap.emails self.request.rdap_result = rdap except RdapException as exc: self.request.rdap_error = (asn, exc) @@ -554,14 +549,14 @@ class ParentStatusException(IOError): def __init__(self, parent, typ): if parent.status == "pending": - super(IOError, self).__init__( + super().__init__( _( "Object of type '%(type)s' cannot be created because its parent entity '%(parent_tag)s/%(parent_id)s' has not yet been approved" ) % {"type": typ, "parent_tag": parent.ref_tag, "parent_id": parent.id} ) elif parent.status == "deleted": - super(IOError, self).__init__( + super().__init__( _( "Object of type '%(type)s' cannot be created because its parent entity '%(parent_tag)s/%(parent_id)s' has been marked as deleted" ) @@ -961,8 +956,7 @@ class ModelSerializer(serializers.ModelSerializer): else: # sub element - l = self.in_list - if l: + if self.in_list: # sub element in set if d < j: return_full = False @@ -1196,7 +1190,7 @@ class ModelSerializer(serializers.ModelSerializer): # if field can't be nulled this will # fail and raise the original error instance.save() - except: + except Exception: raise exc rv = super().run_validation(data=data) @@ -1243,15 +1237,12 @@ class ModelSerializer(serializers.ModelSerializer): def finalize_create(self, request): """this will be called on the end of POST request to this serializer""" - pass def finalize_update(self, request): """this will be called on the end of PUT request to this serializer""" - pass def finalize_delete(self, request): """this will be called on the end of DELETE request to this serializer""" - pass class RequestAwareListSerializer(serializers.ListSerializer): @@ -1628,6 +1619,10 @@ class InternetExchangeFacilitySerializer(ModelSerializer): ix = serializers.SerializerMethodField() fac = serializers.SerializerMethodField() + name = serializers.SerializerMethodField() + country = serializers.SerializerMethodField() + city = serializers.SerializerMethodField() + def validate_create(self, data): # we don't want users to be able to create ixfacs if the parent # ix or fac status is pending or deleted @@ -1641,6 +1636,9 @@ class InternetExchangeFacilitySerializer(ModelSerializer): model = InternetExchangeFacility fields = [ "id", + "name", + "city", + "country", "ix_id", "ix", "fac_id", @@ -1661,7 +1659,18 @@ class InternetExchangeFacilitySerializer(ModelSerializer): @classmethod def prepare_query(cls, qset, **kwargs): - return qset.select_related("ix", "ix__org"), {} + qset = qset.select_related("ix", "ix__org", "facility") + + filters = get_relation_filters(["name", "country", "city"], cls, **kwargs) + for field, e in list(filters.items()): + for valid in ["name", "country", "city"]: + if validate_relation_filter_field(field, valid): + fn = getattr(cls.Meta.model, "related_to_%s" % valid) + field = f"facility__{valid}" + qset = fn(qset=qset, field=field, **e) + break + + return qset, filters def get_ix(self, inst): return self.sub_serializer(InternetExchangeSerializer, inst.ix) @@ -1669,6 +1678,15 @@ class InternetExchangeFacilitySerializer(ModelSerializer): def get_fac(self, inst): return self.sub_serializer(FacilitySerializer, inst.facility) + def get_name(self, inst): + return inst.facility.name + + def get_country(self, inst): + return inst.facility.country + + def get_city(self, inst): + return inst.facility.city + class NetworkContactSerializer(ModelSerializer): """ @@ -1869,7 +1887,7 @@ class NetworkIXLanSerializer(ModelSerializer): try: net = Network.objects.get(id=data.get("net_id")) data["asn"] = net.asn - except: + except Exception: pass return super().run_validation(data=data) @@ -2042,7 +2060,7 @@ class NetworkFacilitySerializer(ModelSerializer): try: net = Network.objects.get(id=data.get("net_id")) data["local_asn"] = net.asn - except: + except Exception: pass return super().run_validation(data=data) @@ -2282,7 +2300,7 @@ class NetworkSerializer(ModelSerializer): def create(self, validated_data): request = self._context.get("request") - user = request.user + request.user asn = validated_data.get("asn") @@ -2357,7 +2375,7 @@ class IXLanPrefixSerializer(ModelSerializer): ixlan = serializers.SerializerMethodField() - prefix = IPPrefixField( + prefix = IPNetworkField( validators=[ validators.UniqueValidator(queryset=IXLanPrefix.objects.all()), validate_address_space, @@ -2419,7 +2437,7 @@ class IXLanPrefixSerializer(ModelSerializer): # validate prefix against selected protocol # - # Note: While the IPPrefixField already has this validator set on it + # Note: While the IPNetworkField already has this validator set on it # there is no good way to set the field's version from the protocol # specified in the rest data at this point, so we instead opt to validate # it again here. @@ -2442,7 +2460,7 @@ class IXLanPrefixSerializer(ModelSerializer): # to actively set it to `False` let them know it is no # longer supported - if self.initial_data.get("in_dfz", True) == False: + if self.initial_data.get("in_dfz", True) is False: raise serializers.ValidationError( _( "The `in_dfz` property has been deprecated " @@ -2468,6 +2486,8 @@ class IXLanSerializer(ModelSerializer): - ix_id, handled by serializer """ + dot1q_support = serializers.SerializerMethodField() + ix_id = serializers.PrimaryKeyRelatedField( queryset=InternetExchange.objects.all(), source="ix" ) @@ -2528,6 +2548,11 @@ class IXLanSerializer(ModelSerializer): def get_ix(self, inst): return self.sub_serializer(InternetExchangeSerializer, inst.ix) + def get_dot1q_support(self, inst): + # as per #903 this should always return false as the field + # is now deprecated + return False + def validate(self, data): # Per issue 846 if data["ixf_ixp_member_list_url"] == "" and data["ixf_ixp_import_enabled"]: @@ -2582,7 +2607,7 @@ class InternetExchangeSerializer(ModelSerializer): # creation. It will be a required field during `POST` requests # but will be ignored during `PUT` so we cannot just do # required=True here - prefix = IPPrefixField( + prefix = IPNetworkField( validators=[ validators.UniqueValidator( queryset=IXLanPrefix.objects.filter(status__in=["ok", "pending"]) @@ -2636,6 +2661,8 @@ class InternetExchangeSerializer(ModelSerializer): "fac_count", "ixf_net_count", "ixf_last_import", + "ixf_import_request", + "ixf_import_request_status", "service_level", "terms", ] + HandleRefSerializer.Meta.fields diff --git a/peeringdb_server/settings.py b/peeringdb_server/settings.py index 876f2ad6..4cf8ec9b 100644 --- a/peeringdb_server/settings.py +++ b/peeringdb_server/settings.py @@ -1,5 +1,3 @@ -import os - from django.conf import settings PEERINGDB_VERSION = getattr(settings, "PEERINGDB_VERSION", "") diff --git a/peeringdb_server/signals.py b/peeringdb_server/signals.py index ddc30580..647471cd 100644 --- a/peeringdb_server/signals.py +++ b/peeringdb_server/signals.py @@ -14,7 +14,7 @@ from django.utils.translation import override from django.utils.translation import ugettext_lazy as _ from django_grainy.models import Group, GroupPermission from django_peeringdb.models.abstract import AddressModel -from grainy.const import * +from grainy.const import PERM_CRUD, PERM_READ import peeringdb_server.settings as pdb_settings from peeringdb_server.deskpro import ( @@ -28,17 +28,12 @@ from peeringdb_server.models import ( QUEUE_ENABLED, QUEUE_NOTIFY, Facility, - InternetExchange, Network, - NetworkContact, - NetworkFacility, NetworkIXLan, Organization, UserOrgAffiliationRequest, VerificationQueueItem, - is_suggested, ) -from peeringdb_server.util import PERM_CRUD def disable_auto_now_and_save(entity): @@ -170,7 +165,7 @@ def org_save(sender, **kwargs): # make the general member group for the org try: - group = Group.objects.get(name=inst.group_name) + Group.objects.get(name=inst.group_name) except Group.DoesNotExist: group = Group(name=inst.group_name) group.save() @@ -194,7 +189,7 @@ def org_save(sender, **kwargs): # make the admin group for the org try: - group = Group.objects.get(name=inst.admin_group_name) + Group.objects.get(name=inst.admin_group_name) except Group.DoesNotExist: group = Group(name=inst.admin_group_name) group.save() @@ -335,8 +330,8 @@ def uoar_creation(sender, instance, created=False, **kwargs): # Lookup RDAP information try: rdap_lookup = rdap = RdapLookup().get_asn(instance.asn) - ok = rdap_lookup.emails - except RdapException as inst: + rdap_lookup.emails + except RdapException: instance.deny() raise diff --git a/peeringdb_server/static/20c/twentyc.core.js b/peeringdb_server/static/20c/twentyc.core.js index 482a086c..2106ae5a 100644 --- a/peeringdb_server/static/20c/twentyc.core.js +++ b/peeringdb_server/static/20c/twentyc.core.js @@ -5,7 +5,7 @@ * @module twentyc */ -twentyc = {}; +twentyc = {}; // lgtm[js/missing-variable-declaration] /** * class helper functions @@ -497,7 +497,7 @@ twentyc.data.LoaderRegistry = twentyc.cls.extend( assign : function(id, loaderName) { // this will error if loaderName is not registered - var loader = this.get(loaderName); + this.get(loaderName); // link loader this._loaders[id] = loaderName; @@ -544,7 +544,7 @@ twentyc.data.loaders.register( }, retrieve : function(data) { var set = tc.u.get(data, this.dataId) - if(typeof set == undefined) + if(typeof set == "undefined") return {}; return set; }, diff --git a/peeringdb_server/static/20c/twentyc.edit.js b/peeringdb_server/static/20c/twentyc.edit.js index 5c80f57c..97c838e3 100644 --- a/peeringdb_server/static/20c/twentyc.edit.js +++ b/peeringdb_server/static/20c/twentyc.edit.js @@ -205,7 +205,7 @@ twentyc.editable.action.register( container.editable("loading-shim", "hide"); } - } + }; try { @@ -391,12 +391,12 @@ twentyc.editable.module.register( }, execute : function(trigger, container) { - var me = $(this), action = trigger.data("edit-action"); + var action = trigger.data("edit-action"); this.trigger = trigger; this.target = twentyc.editable.target.instantiate(container); - handler = new (twentyc.editable.action.get("module-action")) + var handler = new (twentyc.editable.action.get("module-action")) handler.loading_shim = this.loading_shim; handler.execute(this, action, trigger, container); }, @@ -479,7 +479,7 @@ twentyc.editable.module.register( }, add : function(rowId, trigger, container, data) { - var row = twentyc.editable.templates.copy(this.components.list.data("edit-template")) + var row = twentyc.editable.templates.copy(this.components.list.data("edit-template")); var k; row.attr("data-edit-id", rowId); row.data("edit-id", rowId); @@ -691,7 +691,7 @@ twentyc.editable.input = new (twentyc.cls.extend( if(it.action_on_enter && action) { element.on("keydown", function(e) { if(e.which == 13) { - handler = new (twentyc.editable.action.get(action)); + var handler = new (twentyc.editable.action.get(action)); handler.execute(element, container); } }); @@ -709,7 +709,6 @@ twentyc.editable.input = new (twentyc.cls.extend( var it = new (this.get(element.data("edit-type"))); - var par = element.parent() it.container = container; it.source = element; @@ -1067,7 +1066,7 @@ twentyc.editable.input.register( }, load : function(data) { - var k, v, opt; + var k, v; this.element.empty(); if(this.source.data("edit-data-all-entry")) { var allEntry = this.source.data("edit-data-all-entry").split(":") @@ -1164,8 +1163,7 @@ $.fn.editable = function(action, arg, dbg) { input, node, nodes, - closest, - result + closest // BELONGS (container), shortcut for first_closest:["data-edit-target", target] if(arg.belongs) { @@ -1189,7 +1187,7 @@ $.fn.editable = function(action, arg, dbg) { node = $(this[i]); if(node.data("edit-group")) continue; - nodes = $('[data-edit-group]').each(function(idx) { + $('[data-edit-group]').each(function(idx) { var other = $($(this).data("edit-group")); if(other.get(0) == node.get(0)) matched.push(this); diff --git a/peeringdb_server/static/20c/twentyc.filter-field.js b/peeringdb_server/static/20c/twentyc.filter-field.js index 1628ff83..adab59e9 100644 --- a/peeringdb_server/static/20c/twentyc.filter-field.js +++ b/peeringdb_server/static/20c/twentyc.filter-field.js @@ -37,7 +37,7 @@ twentyc.jq.plugin( target.children(".empty-result").first().show(); target.trigger('filter-done') - } + }; me.data("filter-callback", callback); @@ -73,7 +73,7 @@ twentyc.jq.plugin( var me = $(this); var myvalue = new String(me.data("filter-value")) var status = (value ? false : true); - if(myvalue && myvalue.toLowerCase().indexOf(value) > -1) { + if(myvalue.length && myvalue.toLowerCase().indexOf(value) > -1) { status = true; } if(!status) { diff --git a/peeringdb_server/static/peeringdb.js b/peeringdb_server/static/peeringdb.js index 7c4c8436..d54e51aa 100644 --- a/peeringdb_server/static/peeringdb.js +++ b/peeringdb_server/static/peeringdb.js @@ -244,7 +244,6 @@ PeeringDB = { var status = { incomplete : false}; $(this).find('[data-edit-name]').each(function() { var value = $(this).html().trim(); - var name = $(this).data("edit-name"); var field = $(this).prev('.view_field'); var group = field.data("notify-incomplete-group") @@ -313,7 +312,7 @@ PeeringDB = { } } -function moveCursorToEnd(el) { +function moveCursorToEnd(el) { // lgtm[js/unused-local-variable] if (typeof el.selectionStart == "number") { el.selectionStart = el.selectionEnd = el.value.length; } else if (typeof el.createTextRange != "undefined") { @@ -417,6 +416,26 @@ PeeringDB.ViewActions.actions.ix_ixf_preview = function(netId) { } +PeeringDB.ViewActions.actions.ix_ixf_request_import = function(ixId) { + var button = $('[data-view-action="ix_ixf_request_import"]') + button.attr("title","").tooltip("hide").attr("data-trigger", "manual"); + $.post(`/api/ix/${ixId}/request_ixf_import`).done( + () => { + $('.ixf-import-request-status').addClass("alert alert-warning").text(gettext("Queued")) + } + ).fail( + (response,typ,msg) => { + if(response.status == 429) { + let seconds = response.responseJSON.meta.error.match(/(\d+)/)[0] + button.attr('title', gettext("Please wait before requesting another import.")+" "+gettext("Available in: ")+seconds+" "+gettext("seconds")).tooltip("show"); + } else { + button.attr('title',msg).tooltip("show"); + } + } + ); +} + + PeeringDB.ViewActions.actions.net_ixf_preview = function(netId) { $("#ixf-preview-modal").modal("show"); var preview = new PeeringDB.IXFNetPreview() @@ -584,7 +603,6 @@ PeeringDB.IXFProposals = twentyc.cls.define( delete : function(row) { var data=this.collect(row); - var proposals = row.closest("[data-ixf-proposals-ix]") row.find('.loading-shim').show(); return PeeringDB.API.request( @@ -711,7 +729,6 @@ PeeringDB.IXFProposals = twentyc.cls.define( add : function(row) { var data=this.collect(row); - var proposals = row.closest("[data-ixf-proposals-ix]") row.find('.loading-shim').show(); row.find('.errors').hide() @@ -1220,8 +1237,7 @@ PeeringDB.InlineSearch = { }, apply_result : function(data) { - var i, row, rowNode, type, resultNodes = PeeringDB.InlineSearch.resultNodes; - + var i, row, rowNode, type; var count = 0; for(type in data) { @@ -1372,7 +1388,7 @@ twentyc.editable.module.register( data.perm_c = ((data.perms & this.PERM_CREATE) == this.PERM_CREATE) data.perm_d = ((data.perms & this.PERM_DELETE) == this.PERM_DELETE) - var row = this.listing_add(rowId, trigger, container, data); + this.listing_add(rowId, trigger, container, data); }, execute_add : function(trigger, container) { @@ -1386,7 +1402,6 @@ twentyc.editable.module.register( execute_remove : function(trigger, container) { this.components.add.editable("export", this.target.data); - var data = this.target.data; var row = this.row(trigger); this.prepare_data({perms:0, entity:row.data("edit-id")}); this.target.execute("remove", trigger, function(response) { @@ -1403,7 +1418,7 @@ twentyc.editable.module.register( }, load : function(keyPrefix) { - var me = this; target = this.get_target(); + var me = this, target = this.get_target(); if(!keyPrefix) { me.clear(); return; @@ -1412,6 +1427,7 @@ twentyc.editable.module.register( target.data = {"key_prefix":keyPrefix, "org_id":this.org_id()} target.execute(null, null, function(data) { me.clear(); + var k; for(k in data.user_permissions) { var perms = {}; perms.perms = data.user_permissions[k]; @@ -1481,7 +1497,7 @@ twentyc.editable.module.register( data.perm_c = ((data.perms & this.PERM_CREATE) == this.PERM_CREATE) data.perm_d = ((data.perms & this.PERM_DELETE) == this.PERM_DELETE) - var row = this.listing_add(rowId, trigger, container, data); + this.listing_add(rowId, trigger, container, data); }, execute_add : function(trigger, container) { @@ -1512,7 +1528,7 @@ twentyc.editable.module.register( }, load : function(userId) { - var me = this; target = this.get_target(); + var me = this, target = this.get_target(); if(!userId) { me.clear(); return; @@ -1521,6 +1537,7 @@ twentyc.editable.module.register( target.data= {"user_id":userId, "org_id":this.org_id()} target.execute(null, null, function(data) { me.clear(); + var k; for(k in data.key_perms) { var perms = {}; perms.perms = data.key_perms[k]; @@ -1586,8 +1603,6 @@ twentyc.editable.module.register( + "Keys cannot be recovered once " + "this message is exited or overwritten."); - const buttonText = gettext("I have written down my key") - var panel = $('
').addClass("alert alert-success marg-top-15"). append($('
').text(message)). append($('
').addClass("center marg-top-15").text(key)) @@ -1616,7 +1631,7 @@ twentyc.editable.module.register( this.target.execute("add", this.components.add, function(response) { if(response.readonly) response.name = response.name + " (read-only)"; - var row = this.add(data.entity, trigger, container, response); + this.add(data.entity, trigger, container, response); this.api_key_popin(response.key) }.bind(this)); }, @@ -1635,7 +1650,6 @@ twentyc.editable.module.register( var row = this.row(trigger); row.editable("export", this.target.data); var data = this.target.data; - var id = data.prefix = row.data("edit-id") this.target.execute("update", trigger, function(response) { }.bind(this)); }, @@ -1732,7 +1746,7 @@ twentyc.editable.target.register( data.country = data.country__in; delete data.country__in; data.distance = this.sender.find('[data-edit-name="distance"]').data("edit-input-instance").formatted(); - } else if(typeof(data.distance) != undefined){ + } else if(typeof(data.distance) != "undefined"){ delete data.distance; } @@ -1756,7 +1770,7 @@ twentyc.editable.target.register( if(parseInt(data.distance) > 0) { data.country = data.country__in; delete data.country__in; - } else if(typeof(data.distance) != undefined){ + } else if(typeof(data.distance) != "undefined"){ delete data.distance; } @@ -2027,7 +2041,6 @@ twentyc.editable.module.register( submit : function(id, data, row, trigger, container) { data._id = id; var me = this; - var sentData = data; this.target.data = data; this.target.args[2] = "update" this.target.context = row; @@ -2388,7 +2401,6 @@ twentyc.editable.input.register( wire : function() { var widget = this; var input = this.element; - var url = "/autocomplete/"+this.source.data("edit-autocomplete") var multi = this.source.data('edit-multiple') @@ -2726,8 +2738,8 @@ twentyc.editable.input.register( return true; if ( $.isNumeric(value) ){ return true - return false } + return false }, formatted: function() { @@ -2907,7 +2919,7 @@ twentyc.data.loaders.register( ); $.urlParam = function(name){ - var results = new RegExp('[\?&]' + name + '=([^&#]*)').exec(window.location.href); + var results = new RegExp('[?&]' + name + '=([^&#]*)').exec(window.location.href); if(!results) return 0; return results[1] || 0; diff --git a/peeringdb_server/templates/admin/change_list_with_regex_search.html b/peeringdb_server/templates/admin/change_list_with_regex_search.html index 889b6425..d69c170b 100644 --- a/peeringdb_server/templates/admin/change_list_with_regex_search.html +++ b/peeringdb_server/templates/admin/change_list_with_regex_search.html @@ -3,7 +3,7 @@ {% block search %} {{block.super}} {% blocktrans with doc_url="https://docs.python.org/3/library/re.html" %} -To use regex search, begin search term with ^ and end with $. +To use regex search, begin search term with ^ and end with $. {% endblocktrans %}

{% endblock %} diff --git a/peeringdb_server/templates/admin/peeringdb_server/commandlinetool/prepare_command.html b/peeringdb_server/templates/admin/peeringdb_server/commandlinetool/prepare_command.html index e2720b06..70030b88 100644 --- a/peeringdb_server/templates/admin/peeringdb_server/commandlinetool/prepare_command.html +++ b/peeringdb_server/templates/admin/peeringdb_server/commandlinetool/prepare_command.html @@ -1,5 +1,5 @@ {% extends "admin/base_site.html" %} -{% load i18n admin_urls admin_static static admin_modify %} +{% load i18n admin_urls static admin_modify %} {% block extrahead %}{{ block.super }} {{ media }} diff --git a/peeringdb_server/templates/admin/peeringdb_server/commandlinetool/preview_command.html b/peeringdb_server/templates/admin/peeringdb_server/commandlinetool/preview_command.html index 6cfb751d..8b510d13 100644 --- a/peeringdb_server/templates/admin/peeringdb_server/commandlinetool/preview_command.html +++ b/peeringdb_server/templates/admin/peeringdb_server/commandlinetool/preview_command.html @@ -1,5 +1,5 @@ {% extends "admin/admin_extended.html" %} -{% load i18n admin_urls admin_static static admin_modify %} +{% load i18n admin_urls static admin_modify %} {% block extrahead %}{{ block.super }} {{ media }} diff --git a/peeringdb_server/templates/admin/peeringdb_server/commandlinetool/run_command.html b/peeringdb_server/templates/admin/peeringdb_server/commandlinetool/run_command.html index b567bdd6..514738cb 100644 --- a/peeringdb_server/templates/admin/peeringdb_server/commandlinetool/run_command.html +++ b/peeringdb_server/templates/admin/peeringdb_server/commandlinetool/run_command.html @@ -1,5 +1,5 @@ {% extends "admin/admin_extended.html" %} -{% load i18n admin_urls admin_static static admin_modify %} +{% load i18n admin_urls static admin_modify %} {% block extrahead %}{{ block.super }} {{ media }} diff --git a/peeringdb_server/templates/site/org_manage_key_permissions.html b/peeringdb_server/templates/site/org_manage_key_permissions.html index a083544b..057806b3 100644 --- a/peeringdb_server/templates/site/org_manage_key_permissions.html +++ b/peeringdb_server/templates/site/org_manage_key_permissions.html @@ -53,13 +53,13 @@
{{ instance|org_permission_id_xl:entity }}
-
+
-
+
-
+
diff --git a/peeringdb_server/templates/site/view.html b/peeringdb_server/templates/site/view.html index 731b5997..4807dd90 100644 --- a/peeringdb_server/templates/site/view.html +++ b/peeringdb_server/templates/site/view.html @@ -199,7 +199,11 @@ {% elif row.type == "action" %}
{% for action in row.actions %} - + {% if action.action %} + + {% else %} + {{ action.label }} + {% endif %} {% endfor %}
{% elif row.type == "fmt-text" %} diff --git a/peeringdb_server/templates/site/view_organization_tools.html b/peeringdb_server/templates/site/view_organization_tools.html index cc1423cc..a9186836 100644 --- a/peeringdb_server/templates/site/view_organization_tools.html +++ b/peeringdb_server/templates/site/view_organization_tools.html @@ -738,13 +738,13 @@
{{ instance|org_permission_id_xl:entity }}
-
+
-
+
-
+
diff --git a/peeringdb_server/templatetags/util.py b/peeringdb_server/templatetags/util.py index 72e0a62d..4b2fde31 100644 --- a/peeringdb_server/templatetags/util.py +++ b/peeringdb_server/templatetags/util.py @@ -8,7 +8,7 @@ from django import template from django.utils.safestring import mark_safe from django.utils.translation import ugettext_lazy as _ from django_countries import countries -from django_namespace_perms.util import get_permission_flag +from django_grainy.helpers import int_flags from peeringdb_server.inet import RdapException from peeringdb_server.models import ( @@ -57,7 +57,7 @@ def org_permission_id_xl(org, id): @register.filter def check_perms(v, op): - flg = get_permission_flag(op) + flg = int_flags(op) return v & flg == flg @@ -84,7 +84,7 @@ def ownership_warning(org, user): if user.validate_rdap_relationship(rdap): b = True break - except RdapException as exc: + except RdapException: # we don't need to do anything with the rdap exception here, as it will # be raised apropriately when the request is sent off pass diff --git a/peeringdb_server/urls.py b/peeringdb_server/urls.py index fd97d7ff..cd146195 100644 --- a/peeringdb_server/urls.py +++ b/peeringdb_server/urls.py @@ -52,7 +52,6 @@ from peeringdb_server.views import ( view_network_by_asn, view_network_by_query, view_organization, - view_partnerships, view_password_change, view_password_reset, view_profile, diff --git a/peeringdb_server/util.py b/peeringdb_server/util.py index f17ee41a..9d1acbcc 100644 --- a/peeringdb_server/util.py +++ b/peeringdb_server/util.py @@ -2,9 +2,7 @@ import ipaddress from decimal import Decimal from django.conf import settings -from django_grainy.const import * -from django_grainy.util import Permissions, check_permissions, get_permissions -from grainy.const import * +from django_grainy.util import Permissions, check_permissions, get_permissions # noqa from grainy.core import NamespaceKeyApplicator diff --git a/peeringdb_server/validators.py b/peeringdb_server/validators.py index b89f97a9..a1b4ae95 100644 --- a/peeringdb_server/validators.py +++ b/peeringdb_server/validators.py @@ -38,7 +38,7 @@ def validate_phonenumber(phonenumber, country=None): parsed_number, phonenumbers.PhoneNumberFormat.E164 ) return f"{validated_number}" - except phonenumbers.phonenumberutil.NumberParseException as exc: + except phonenumbers.phonenumberutil.NumberParseException: raise ValidationError(_("Not a valid phone number (E.164)")) @@ -83,7 +83,7 @@ def validate_prefix(prefix): if isinstance(prefix, str): try: prefix = ipaddress.ip_network(prefix) - except ValueError as exc: + except ValueError: raise ValidationError(_("Invalid prefix: {}").format(prefix)) return prefix @@ -267,7 +267,6 @@ def validate_irr_as_set(value): ) set_found = False - typ = None types = [] for part in as_parts: diff --git a/peeringdb_server/views.py b/peeringdb_server/views.py index c0501b1c..054d6b87 100644 --- a/peeringdb_server/views.py +++ b/peeringdb_server/views.py @@ -1,6 +1,5 @@ import datetime import json -import os import re import uuid @@ -11,7 +10,6 @@ from django.conf import settings as dj_settings from django.contrib.auth import authenticate, login, logout from django.contrib.auth.decorators import login_required from django.core.exceptions import MultipleObjectsReturned, ObjectDoesNotExist -from django.db.models import Q from django.http import ( HttpResponse, HttpResponseBadRequest, @@ -31,12 +29,12 @@ from django.views.decorators.csrf import csrf_protect, ensure_csrf_cookie from django.views.decorators.http import require_http_methods from django_grainy.util import Permissions from django_otp.plugins.otp_email.models import EmailDevice -from grainy.const import * +from grainy.const import PERM_CREATE, PERM_CRUD, PERM_DELETE, PERM_UPDATE from oauth2_provider.decorators import protected_resource from oauth2_provider.oauth2_backends import get_oauthlib_core -from ratelimit.decorators import is_ratelimited, ratelimit +from ratelimit.decorators import ratelimit -from peeringdb_server import maintenance, settings +from peeringdb_server import settings from peeringdb_server.api_key_views import load_all_key_permissions from peeringdb_server.data_views import BOOL_CHOICE from peeringdb_server.deskpro import ticket_queue_rdap_error @@ -48,12 +46,7 @@ from peeringdb_server.forms import ( UserLocaleForm, UsernameRetrieveForm, ) -from peeringdb_server.inet import ( - RdapException, - RdapLookup, - RdapNotFoundError, - rdap_pretty_error_message, -) +from peeringdb_server.inet import RdapException, rdap_pretty_error_message from peeringdb_server.mail import mail_username_retrieve from peeringdb_server.models import ( PARTNERSHIP_LEVELS, @@ -83,7 +76,7 @@ from peeringdb_server.serializers import ( OrganizationSerializer, ) from peeringdb_server.stats import stats as global_stats -from peeringdb_server.util import PERM_CRUD, APIPermissionsApplicator, check_permissions +from peeringdb_server.util import APIPermissionsApplicator, check_permissions RATELIMITS = dj_settings.RATELIMITS @@ -492,9 +485,10 @@ def view_set_user_locale(request): request.user.set_locale(loc) translation.activate(loc) - request.session[translation.LANGUAGE_SESSION_KEY] = loc + response = JsonResponse({"status": "ok"}) + response.set_cookie(dj_settings.LANGUAGE_COOKIE_NAME, loc) - return JsonResponse({"status": "ok"}) + return response @protected_resource(scopes=["profile"]) @@ -1460,11 +1454,6 @@ def view_exchange(request, id): {"name": "ix_id", "value": exchange.id}, ], }, - { - "type": "flags", - "label": _("DOT1Q"), - "value": [{"name": "dot1q_support", "value": ixlan.dot1q_support}], - }, { "type": "number", "name": "mtu", @@ -1512,6 +1501,21 @@ def view_exchange(request, id): ], "admin": True, }, + { + "type": "action", + "label": _("IX-F Import"), + "actions": [ + { + "label": _("Request import"), + "action": "ixf_request_import", + }, + { + "label": exchange.ixf_import_request_recent_status[1], + "css": f"ixf-import-request-status {exchange.ixf_import_css}", + }, + ], + "admin": True, + }, {"type": "group_end"}, ] ) @@ -1972,10 +1976,10 @@ def view_advanced_search(request): env["not_fac_name"] = "" env["can_use_distance_filter"] = ( - dj_settings.API_DISTANCE_FILTER_REQUIRE_AUTH == False + dj_settings.API_DISTANCE_FILTER_REQUIRE_AUTH is False or request.user.is_authenticated ) and ( - dj_settings.API_DISTANCE_FILTER_REQUIRE_VERIFIED == False + dj_settings.API_DISTANCE_FILTER_REQUIRE_VERIFIED is False or (request.user.is_authenticated and request.user.is_verified_user) ) @@ -2239,9 +2243,10 @@ class LoginView(two_factor.views.LoginView): user_language = self.get_user().get_locale() translation.activate(user_language) - self.request.session[translation.LANGUAGE_SESSION_KEY] = user_language + response = redirect(self.get_success_url()) + response.set_cookie(dj_settings.LANGUAGE_COOKIE_NAME, user_language) - return redirect(self.get_success_url()) + return response @require_http_methods(["POST"]) @@ -2270,7 +2275,7 @@ def request_translation(request, data_type): } reply = requests.post(translationURL, params=call_params).json() - if not "data" in reply: + if "data" not in reply: return JsonResponse({"status": request.POST, "error": reply}) return JsonResponse( diff --git a/poetry.lock b/poetry.lock index 66dc3a0f..d4757635 100644 --- a/poetry.lock +++ b/poetry.lock @@ -6,6 +6,17 @@ category = "dev" optional = false python-versions = "*" +[[package]] +name = "asgiref" +version = "3.4.1" +description = "ASGI specs, helper code, and adapters" +category = "main" +optional = false +python-versions = ">=3.6" + +[package.extras] +tests = ["pytest", "pytest-asyncio", "mypy (>=0.800)"] + [[package]] name = "atomicwrites" version = "1.4.0" @@ -28,9 +39,37 @@ docs = ["furo", "sphinx", "zope.interface", "sphinx-notfound-page"] tests = ["coverage[toml] (>=5.0.2)", "hypothesis", "pympler", "pytest (>=4.3.0)", "six", "mypy", "pytest-mypy-plugins", "zope.interface"] tests_no_zope = ["coverage[toml] (>=5.0.2)", "hypothesis", "pympler", "pytest (>=4.3.0)", "six", "mypy", "pytest-mypy-plugins"] +[[package]] +name = "backports.entry-points-selectable" +version = "1.1.0" +description = "Compatibility shim providing selectable entry points for older implementations" +category = "dev" +optional = false +python-versions = ">=2.7" + +[package.extras] +docs = ["sphinx", "jaraco.packaging (>=8.2)", "rst.linker (>=1.9)"] +testing = ["pytest (>=4.6)", "pytest-flake8", "pytest-cov", "pytest-black (>=0.3.7)", "pytest-mypy", "pytest-checkdocs (>=2.4)", "pytest-enabler (>=1.0.1)"] + +[[package]] +name = "bcrypt" +version = "3.2.0" +description = "Modern password hashing for your software and your servers" +category = "dev" +optional = false +python-versions = ">=3.6" + +[package.dependencies] +cffi = ">=1.1" +six = ">=1.4.1" + +[package.extras] +tests = ["pytest (>=3.2.1,!=3.3.0)"] +typecheck = ["mypy"] + [[package]] name = "black" -version = "21.6b0" +version = "21.7b0" description = "The uncompromising code formatter." category = "dev" optional = false @@ -42,7 +81,7 @@ click = ">=7.1.2" mypy-extensions = ">=0.4.3" pathspec = ">=0.8.1,<1" regex = ">=2020.1.8" -toml = ">=0.10.1" +tomli = ">=0.2.6,<2.0.0" [package.extras] colorama = ["colorama (>=0.4.3)"] @@ -52,11 +91,11 @@ uvloop = ["uvloop (>=0.15.2)"] [[package]] name = "bleach" -version = "3.3.0" +version = "4.0.0" description = "An easy safelist-based HTML-sanitizing tool." category = "main" optional = false -python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*" +python-versions = ">=3.6" [package.dependencies] packaging = "*" @@ -102,12 +141,15 @@ python-versions = "*" six = "<=2.0.0" [[package]] -name = "chardet" -version = "4.0.0" -description = "Universal encoding detector for Python 2 and 3" +name = "charset-normalizer" +version = "2.0.4" +description = "The Real First Universal Charset Detector. Open, modern and actively maintained alternative to Chardet." category = "main" optional = false -python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*" +python-versions = ">=3.5.0" + +[package.extras] +unicode_backport = ["unicodedata2"] [[package]] name = "click" @@ -221,25 +263,34 @@ category = "dev" optional = false python-versions = "*" +[[package]] +name = "distro" +version = "1.6.0" +description = "Distro - an OS platform information API" +category = "dev" +optional = false +python-versions = "*" + [[package]] name = "django" -version = "2.2.24" +version = "3.2.6" description = "A high-level Python Web framework that encourages rapid development and clean, pragmatic design." category = "main" optional = false -python-versions = ">=3.5" +python-versions = ">=3.6" [package.dependencies] +asgiref = ">=3.3.2,<4" pytz = "*" sqlparse = ">=0.2.2" [package.extras] -argon2 = ["argon2-cffi (>=16.1.0)"] +argon2 = ["argon2-cffi (>=19.1.0)"] bcrypt = ["bcrypt"] [[package]] name = "django-allauth" -version = "0.44.0" +version = "0.45.0" description = "Integrated set of Django applications addressing authentication, registration, account management as well as 3rd party (social) account authentication." category = "main" optional = false @@ -282,7 +333,7 @@ Django = ">=2.2" [[package]] name = "django-cors-headers" -version = "3.7.0" +version = "3.8.0" description = "django-cors-headers is a Django application for handling the server headers required for Cross-Origin Resource Sharing (CORS)." category = "main" optional = false @@ -323,7 +374,7 @@ python-versions = ">=3.6" [[package]] name = "django-debug-toolbar" -version = "3.2.1" +version = "3.2.2" description = "A configurable set of panels that display various debug information about the current request/response." category = "main" optional = false @@ -377,14 +428,11 @@ python-versions = "*" [[package]] name = "django-handleref" -version = "0.6.0" -description = "track when an object was created or changed and allowe querying based on time and versioning" +version = "1.0.0" +description = "django object tracking" category = "main" optional = false -python-versions = "*" - -[package.dependencies] -six = ">=1.10,<2.0" +python-versions = ">=3.6.2,<4.0.0" [[package]] name = "django-hashers-passlib" @@ -411,19 +459,11 @@ Django = ">=2.2" [[package]] name = "django-inet" -version = "0.5.0" +version = "1.0.1" description = "django internet utilities" category = "main" optional = false -python-versions = "*" - -[[package]] -name = "django-namespace-perms" -version = "0.6.0" -description = "granular permissions for django" -category = "main" -optional = false -python-versions = "*" +python-versions = ">=3.6.2,<4.0.0" [[package]] name = "django-oauth-toolkit" @@ -456,17 +496,16 @@ qrcode = ["qrcode"] [[package]] name = "django-peeringdb" -version = "2.7.0" -description = "PeeringDB models and local synchronization for Django" +version = "2.8.0" +description = "PeeringDB Django models" category = "main" optional = false -python-versions = "*" +python-versions = ">=3.6.2,<4.0.0" [package.dependencies] -django_countries = ">=0.1.0" -django_handleref = ">=0.6,<0.7" -django_inet = ">=0.5,<0.6" -"twentyc.rpc" = ">=0.4.0,<0.5" +django_countries = ">1" +django_handleref = ">=1,<2" +django_inet = ">=1,<2" [[package]] name = "django-phonenumber-field" @@ -518,14 +557,14 @@ simplejson = "*" [[package]] name = "django-reversion" -version = "3.0.9" +version = "4.0.0" description = "An extension to the Django web framework that provides version control for model instances." category = "main" optional = false python-versions = ">=3.6" [package.dependencies] -django = ">=1.11" +django = ">=2.0" [[package]] name = "django-simple-captcha" @@ -607,6 +646,68 @@ category = "main" optional = false python-versions = ">=3.6" +[[package]] +name = "docker" +version = "5.0.0" +description = "A Python library for the Docker Engine API." +category = "dev" +optional = false +python-versions = ">=3.6" + +[package.dependencies] +paramiko = {version = ">=2.4.2", optional = true, markers = "extra == \"ssh\""} +pywin32 = {version = "227", markers = "sys_platform == \"win32\""} +requests = ">=2.14.2,<2.18.0 || >2.18.0" +websocket-client = ">=0.32.0" + +[package.extras] +ssh = ["paramiko (>=2.4.2)"] +tls = ["pyOpenSSL (>=17.5.0)", "cryptography (>=3.4.7)", "idna (>=2.0.0)"] + +[[package]] +name = "docker-compose" +version = "1.29.2" +description = "Multi-container orchestration for Docker" +category = "dev" +optional = false +python-versions = ">=3.4" + +[package.dependencies] +colorama = {version = ">=0.4,<1", markers = "sys_platform == \"win32\""} +distro = ">=1.5.0,<2" +docker = {version = ">=5", extras = ["ssh"]} +dockerpty = ">=0.4.1,<1" +docopt = ">=0.6.1,<1" +jsonschema = ">=2.5.1,<4" +python-dotenv = ">=0.13.0,<1" +PyYAML = ">=3.10,<6" +requests = ">=2.20.0,<3" +texttable = ">=0.9.0,<2" +websocket-client = ">=0.32.0,<1" + +[package.extras] +socks = ["PySocks (>=1.5.6,!=1.5.7,<2)"] +tests = ["ddt (>=1.2.2,<2)", "pytest (<6)"] + +[[package]] +name = "dockerpty" +version = "0.4.1" +description = "Python library to use the pseudo-tty of a docker container" +category = "dev" +optional = false +python-versions = "*" + +[package.dependencies] +six = ">=1.3.0" + +[[package]] +name = "docopt" +version = "0.6.2" +description = "Pythonic argument parser, that will make you smile" +category = "dev" +optional = false +python-versions = "*" + [[package]] name = "filelock" version = "3.0.12" @@ -638,7 +739,7 @@ python-versions = ">=2.6, !=3.0.*, !=3.1.*, !=3.2.*" [[package]] name = "googlemaps" -version = "4.4.5" +version = "4.5.3" description = "Python client library for Google Maps Platform" category = "main" optional = false @@ -657,7 +758,7 @@ python-versions = ">=3.6,<4.0" [[package]] name = "identify" -version = "2.2.11" +version = "2.2.13" description = "File identification library for Python" category = "dev" optional = false @@ -668,11 +769,11 @@ license = ["editdistance-s"] [[package]] name = "idna" -version = "2.10" +version = "3.2" description = "Internationalized Domain Names in Applications (IDNA)" category = "main" optional = false -python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*" +python-versions = ">=3.5" [[package]] name = "iniconfig" @@ -684,7 +785,7 @@ python-versions = "*" [[package]] name = "isort" -version = "5.9.2" +version = "5.9.3" description = "A Python utility / library to sort Python imports." category = "dev" optional = false @@ -737,7 +838,7 @@ format_nongpl = ["idna", "jsonpointer (>1.13)", "webcolors", "rfc3986-validator [[package]] name = "jwcrypto" -version = "0.9.1" +version = "1.0" description = "Implementation of JOSE Web standards" category = "main" optional = false @@ -746,7 +847,6 @@ python-versions = "*" [package.dependencies] cryptography = ">=2.3" deprecated = "*" -six = "*" [[package]] name = "markdown" @@ -851,6 +951,25 @@ python-versions = ">=3.6" [package.dependencies] pyparsing = ">=2.0.2" +[[package]] +name = "paramiko" +version = "2.7.2" +description = "SSH2 protocol library" +category = "dev" +optional = false +python-versions = "*" + +[package.dependencies] +bcrypt = ">=3.1.3" +cryptography = ">=2.5" +pynacl = ">=1.0.1" + +[package.extras] +all = ["pyasn1 (>=0.1.7)", "pynacl (>=1.0.1)", "bcrypt (>=3.1.3)", "invoke (>=1.3)", "gssapi (>=1.4.1)", "pywin32 (>=2.1.8)"] +ed25519 = ["pynacl (>=1.0.1)", "bcrypt (>=3.1.3)"] +gssapi = ["pyasn1 (>=0.1.7)", "gssapi (>=1.4.1)", "pywin32 (>=2.1.8)"] +invoke = ["invoke (>=1.3)"] + [[package]] name = "passlib" version = "1.7.4" @@ -867,11 +986,11 @@ totp = ["cryptography"] [[package]] name = "pathspec" -version = "0.8.1" +version = "0.9.0" description = "Utility library for gitignore style pattern matching of file paths." category = "dev" optional = false -python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*" +python-versions = "!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,!=3.4.*,>=2.7" [[package]] name = "peeringdb" @@ -889,7 +1008,7 @@ PyYAML = ">=3.11" [[package]] name = "phonenumbers" -version = "8.12.27" +version = "8.12.30" description = "Python version of Google's common library for parsing, formatting, storing and validating international phone numbers." category = "main" optional = false @@ -903,6 +1022,18 @@ category = "main" optional = false python-versions = ">=3.6" +[[package]] +name = "platformdirs" +version = "2.2.0" +description = "A small Python module for determining appropriate platform-specific dirs, e.g. a \"user data dir\"." +category = "dev" +optional = false +python-versions = ">=3.6" + +[package.extras] +docs = ["Sphinx (>=4)", "furo (>=2021.7.5b38)", "proselint (>=0.10.2)", "sphinx-autodoc-typehints (>=1.12)"] +test = ["appdirs (==1.4.4)", "pytest (>=6)", "pytest-cov (>=2.7)", "pytest-mock (>=3.6)"] + [[package]] name = "pluggy" version = "0.13.1" @@ -916,7 +1047,7 @@ dev = ["pre-commit", "tox"] [[package]] name = "pre-commit" -version = "2.13.0" +version = "2.14.0" description = "A framework for managing and maintaining multi-language pre-commit hooks." category = "dev" optional = false @@ -979,6 +1110,22 @@ dev = ["sphinx", "sphinx-rtd-theme", "zope.interface", "cryptography (>=3.3.1,<4 docs = ["sphinx", "sphinx-rtd-theme", "zope.interface"] tests = ["pytest (>=6.0.0,<7.0.0)", "coverage[toml] (==5.0.4)"] +[[package]] +name = "pynacl" +version = "1.4.0" +description = "Python binding to the Networking and Cryptography (NaCl) library" +category = "dev" +optional = false +python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*" + +[package.dependencies] +cffi = ">=1.4.1" +six = "*" + +[package.extras] +docs = ["sphinx (>=1.6.5)", "sphinx-rtd-theme"] +tests = ["pytest (>=3.2.1,!=3.3.0)", "hypothesis (>=3.27.0)"] + [[package]] name = "pyparsing" version = "2.4.7" @@ -1074,6 +1221,17 @@ pytest = ">=5.0" [package.extras] dev = ["pre-commit", "tox", "pytest-asyncio"] +[[package]] +name = "python-dotenv" +version = "0.19.0" +description = "Read key-value pairs from a .env file and set them as environment variables" +category = "dev" +optional = false +python-versions = ">=3.5" + +[package.extras] +cli = ["click (>=5.0)"] + [[package]] name = "python3-openid" version = "3.2.0" @@ -1099,7 +1257,7 @@ python-versions = "*" [[package]] name = "pyupgrade" -version = "2.20.0" +version = "2.23.3" description = "A tool to automatically upgrade syntax for newer versions." category = "dev" optional = false @@ -1108,6 +1266,14 @@ python-versions = ">=3.6.1" [package.dependencies] tokenize-rt = ">=3.2.0" +[[package]] +name = "pywin32" +version = "227" +description = "Python for Window Extensions" +category = "dev" +optional = false +python-versions = "*" + [[package]] name = "pyyaml" version = "5.4.1" @@ -1147,7 +1313,7 @@ munge = ">=1.0.0,<2.0.0" [[package]] name = "regex" -version = "2021.7.6" +version = "2021.8.3" description = "Alternative regular expression module, to replace re." category = "dev" optional = false @@ -1155,21 +1321,21 @@ python-versions = "*" [[package]] name = "requests" -version = "2.25.1" +version = "2.26.0" description = "Python HTTP for Humans." category = "main" optional = false -python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*" +python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*, !=3.5.*" [package.dependencies] certifi = ">=2017.4.17" -chardet = ">=3.0.2,<5" -idna = ">=2.5,<3" +charset-normalizer = {version = ">=2.0.0,<2.1.0", markers = "python_version >= \"3\""} +idna = {version = ">=2.5,<4", markers = "python_version >= \"3\""} urllib3 = ">=1.21.1,<1.27" [package.extras] -security = ["pyOpenSSL (>=0.14)", "cryptography (>=1.3.4)"] socks = ["PySocks (>=1.5.6,!=1.5.7)", "win-inet-pton"] +use_chardet_on_py3 = ["chardet (>=3.0.2,<5)"] [[package]] name = "requests-mock" @@ -1226,6 +1392,14 @@ category = "main" optional = false python-versions = ">=3.5" +[[package]] +name = "texttable" +version = "1.6.4" +description = "module for creating simple ASCII tables" +category = "dev" +optional = false +python-versions = "*" + [[package]] name = "tld" version = "0.12.6" @@ -1250,6 +1424,14 @@ category = "dev" optional = false python-versions = ">=2.6, !=3.0.*, !=3.1.*, !=3.2.*" +[[package]] +name = "tomli" +version = "1.2.1" +description = "A lil' TOML parser" +category = "dev" +optional = false +python-versions = ">=3.6" + [[package]] name = "twentyc.rpc" version = "0.4.0" @@ -1301,21 +1483,22 @@ python-versions = "*" [[package]] name = "virtualenv" -version = "20.4.7" +version = "20.7.2" description = "Virtual Python Environment builder" category = "dev" optional = false -python-versions = "!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,>=2.7" +python-versions = "!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,!=3.4.*,>=2.7" [package.dependencies] -appdirs = ">=1.4.3,<2" +"backports.entry-points-selectable" = ">=1.0.4" distlib = ">=0.3.1,<1" filelock = ">=3.0.0,<4" +platformdirs = ">=2,<3" six = ">=1.9.0,<2" [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 (>=4)", "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)", "packaging (>=20.0)", "xonsh (>=0.9.16)"] +testing = ["coverage (>=4)", "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)", "packaging (>=20.0)"] [[package]] name = "webencodings" @@ -1325,6 +1508,17 @@ category = "main" optional = false python-versions = "*" +[[package]] +name = "websocket-client" +version = "0.59.0" +description = "WebSocket client for Python with low level API options" +category = "dev" +optional = false +python-versions = ">=2.6, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*" + +[package.dependencies] +six = "*" + [[package]] name = "whoosh" version = "2.7.4" @@ -1344,13 +1538,17 @@ python-versions = "*" [metadata] lock-version = "1.1" python-versions = "^3.9" -content-hash = "585d0bfbbfc80bad2b768db9944d6ba50d128237049542c9d63f9290e82a1668" +content-hash = "ea974a9c93598d9222fd9bd19fd380e20e0ea94ff07d1388ae55742761d3cefe" [metadata.files] 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.4.1-py3-none-any.whl", hash = "sha256:ffc141aa908e6f175673e7b1b3b7af4fdb0ecb738fc5c8b88f69f055c2415214"}, + {file = "asgiref-3.4.1.tar.gz", hash = "sha256:4ef1ab46b484e3c706329cedeff284a5d40824200638503f5768edb6de7d58e9"}, +] atomicwrites = [ {file = "atomicwrites-1.4.0-py2.py3-none-any.whl", hash = "sha256:6d1784dea7c0c8d4a5172b6c620f40b6e4cbfdf96d783691f2e1302a7b88e197"}, {file = "atomicwrites-1.4.0.tar.gz", hash = "sha256:ae70396ad1a434f9c7046fd2dd196fc04b12f9e91ffb859164193be8b6168a7a"}, @@ -1359,13 +1557,26 @@ attrs = [ {file = "attrs-21.2.0-py2.py3-none-any.whl", hash = "sha256:149e90d6d8ac20db7a955ad60cf0e6881a3f20d37096140088356da6c716b0b1"}, {file = "attrs-21.2.0.tar.gz", hash = "sha256:ef6aaac3ca6cd92904cdd0d83f629a15f18053ec84e6432106f7a4d04ae4f5fb"}, ] +"backports.entry-points-selectable" = [ + {file = "backports.entry_points_selectable-1.1.0-py2.py3-none-any.whl", hash = "sha256:a6d9a871cde5e15b4c4a53e3d43ba890cc6861ec1332c9c2428c92f977192acc"}, + {file = "backports.entry_points_selectable-1.1.0.tar.gz", hash = "sha256:988468260ec1c196dab6ae1149260e2f5472c9110334e5d51adcb77867361f6a"}, +] +bcrypt = [ + {file = "bcrypt-3.2.0-cp36-abi3-macosx_10_9_x86_64.whl", hash = "sha256:c95d4cbebffafcdd28bd28bb4e25b31c50f6da605c81ffd9ad8a3d1b2ab7b1b6"}, + {file = "bcrypt-3.2.0-cp36-abi3-manylinux1_x86_64.whl", hash = "sha256:63d4e3ff96188e5898779b6057878fecf3f11cfe6ec3b313ea09955d587ec7a7"}, + {file = "bcrypt-3.2.0-cp36-abi3-manylinux2010_x86_64.whl", hash = "sha256:cd1ea2ff3038509ea95f687256c46b79f5fc382ad0aa3664d200047546d511d1"}, + {file = "bcrypt-3.2.0-cp36-abi3-manylinux2014_aarch64.whl", hash = "sha256:cdcdcb3972027f83fe24a48b1e90ea4b584d35f1cc279d76de6fc4b13376239d"}, + {file = "bcrypt-3.2.0-cp36-abi3-win32.whl", hash = "sha256:a67fb841b35c28a59cebed05fbd3e80eea26e6d75851f0574a9273c80f3e9b55"}, + {file = "bcrypt-3.2.0-cp36-abi3-win_amd64.whl", hash = "sha256:81fec756feff5b6818ea7ab031205e1d323d8943d237303baca2c5f9c7846f34"}, + {file = "bcrypt-3.2.0.tar.gz", hash = "sha256:5b93c1726e50a93a033c36e5ca7fdcd29a5c7395af50a6892f5d9e7c6cfbfb29"}, +] black = [ - {file = "black-21.6b0-py3-none-any.whl", hash = "sha256:dfb8c5a069012b2ab1e972e7b908f5fb42b6bbabcba0a788b86dc05067c7d9c7"}, - {file = "black-21.6b0.tar.gz", hash = "sha256:dc132348a88d103016726fe360cb9ede02cecf99b76e3660ce6c596be132ce04"}, + {file = "black-21.7b0-py3-none-any.whl", hash = "sha256:1c7aa6ada8ee864db745b22790a32f94b2795c253a75d6d9b5e439ff10d23116"}, + {file = "black-21.7b0.tar.gz", hash = "sha256:c8373c6491de9362e39271630b65b964607bc5c79c83783547d76c839b3aa219"}, ] bleach = [ - {file = "bleach-3.3.0-py2.py3-none-any.whl", hash = "sha256:6123ddc1052673e52bab52cdc955bcb57a015264a1c57d37bea2f6b817af0125"}, - {file = "bleach-3.3.0.tar.gz", hash = "sha256:98b3170739e5e83dd9dc19633f074727ad848cbedb6026708c8ac2d3b697a433"}, + {file = "bleach-4.0.0-py2.py3-none-any.whl", hash = "sha256:c1685a132e6a9a38bf93752e5faab33a9517a6c0bb2f37b785e47bf253bdb51d"}, + {file = "bleach-4.0.0.tar.gz", hash = "sha256:ffa9221c6ac29399cc50fcc33473366edd0cf8d5e2cbbbb63296dc327fb67cc8"}, ] certifi = [ {file = "certifi-2021.5.30-py2.py3-none-any.whl", hash = "sha256:50b1e4f8446b06f41be7dd6338db18e0990601dce795c2b1686458aa7e8fa7d8"}, @@ -1420,9 +1631,9 @@ cfgv = [ cfu = [ {file = "cfu-1.5.0.tar.gz", hash = "sha256:5052fdec7a808823893b73cb438c39a4f780d2c0bba8af06e02192af99424f60"}, ] -chardet = [ - {file = "chardet-4.0.0-py2.py3-none-any.whl", hash = "sha256:f864054d66fd9118f2e67044ac8981a54775ec5b67aed0441892edb553d21da5"}, - {file = "chardet-4.0.0.tar.gz", hash = "sha256:0d6f53a15db4120f2b08c94f11e7d93d2c911ee118b6b30a04ec3ee8310179fa"}, +charset-normalizer = [ + {file = "charset-normalizer-2.0.4.tar.gz", hash = "sha256:f23667ebe1084be45f6ae0538e4a5a865206544097e4e8bbcacf42cd02a348f3"}, + {file = "charset_normalizer-2.0.4-py3-none-any.whl", hash = "sha256:0c8911edd15d19223366a194a513099a302055a962bca2cec0f54b8b63175d8b"}, ] click = [ {file = "click-8.0.1-py3-none-any.whl", hash = "sha256:fba402a4a47334742d782209a7c79bc448911afe1149d07bdabdf480b3e2f4b6"}, @@ -1452,9 +1663,6 @@ coverage = [ {file = "coverage-5.5-cp27-cp27mu-manylinux1_x86_64.whl", hash = "sha256:8963a499849a1fc54b35b1c9f162f4108017b2e6db2c46c1bed93a72262ed083"}, {file = "coverage-5.5-cp27-cp27mu-manylinux2010_i686.whl", hash = "sha256:869a64f53488f40fa5b5b9dcb9e9b2962a66a87dab37790f3fcfb5144b996ef5"}, {file = "coverage-5.5-cp27-cp27mu-manylinux2010_x86_64.whl", hash = "sha256:4a7697d8cb0f27399b0e393c0b90f0f1e40c82023ea4d45d22bce7032a5d7b81"}, - {file = "coverage-5.5-cp310-cp310-macosx_10_14_x86_64.whl", hash = "sha256:8d0a0725ad7c1a0bcd8d1b437e191107d457e2ec1084b9f190630a4fb1af78e6"}, - {file = "coverage-5.5-cp310-cp310-manylinux1_x86_64.whl", hash = "sha256:51cb9476a3987c8967ebab3f0fe144819781fca264f57f89760037a2ea191cb0"}, - {file = "coverage-5.5-cp310-cp310-win_amd64.whl", hash = "sha256:c0891a6a97b09c1f3e073a890514d5012eb256845c451bd48f7968ef939bf4ae"}, {file = "coverage-5.5-cp35-cp35m-macosx_10_9_x86_64.whl", hash = "sha256:3487286bc29a5aa4b93a072e9592f22254291ce96a9fbc5251f566b6b7343cdb"}, {file = "coverage-5.5-cp35-cp35m-manylinux1_i686.whl", hash = "sha256:deee1077aae10d8fa88cb02c845cfba9b62c55e1183f52f6ae6a2df6a2187160"}, {file = "coverage-5.5-cp35-cp35m-manylinux1_x86_64.whl", hash = "sha256:f11642dddbb0253cc8853254301b51390ba0081750a8ac03f20ea8103f0c56b6"}, @@ -1524,12 +1732,16 @@ distlib = [ {file = "distlib-0.3.2-py2.py3-none-any.whl", hash = "sha256:23e223426b28491b1ced97dc3bbe183027419dfc7982b4fa2f05d5f3ff10711c"}, {file = "distlib-0.3.2.zip", hash = "sha256:106fef6dc37dd8c0e2c0a60d3fca3e77460a48907f335fa28420463a6f799736"}, ] +distro = [ + {file = "distro-1.6.0-py2.py3-none-any.whl", hash = "sha256:c8713330ab31a034623a9515663ed87696700b55f04556b97c39cd261aa70dc7"}, + {file = "distro-1.6.0.tar.gz", hash = "sha256:83f5e5a09f9c5f68f60173de572930effbcc0287bb84fdc4426cb4168c088424"}, +] django = [ - {file = "Django-2.2.24-py3-none-any.whl", hash = "sha256:f2084ceecff86b1e631c2cd4107d435daf4e12f1efcdf11061a73bf0b5e95f92"}, - {file = "Django-2.2.24.tar.gz", hash = "sha256:3339ff0e03dee13045aef6ae7b523edff75b6d726adf7a7a48f53d5a501f7db7"}, + {file = "Django-3.2.6-py3-none-any.whl", hash = "sha256:7f92413529aa0e291f3be78ab19be31aefb1e1c9a52cd59e130f505f27a51f13"}, + {file = "Django-3.2.6.tar.gz", hash = "sha256:f27f8544c9d4c383bbe007c57e3235918e258364577373d4920e9162837be022"}, ] django-allauth = [ - {file = "django-allauth-0.44.0.tar.gz", hash = "sha256:e51af457466022f52154d74c8523ac69375120fad2acce6e239635d85e610b25"}, + {file = "django-allauth-0.45.0.tar.gz", hash = "sha256:6d46be0e1480316ccd45476db3aefb39db70e038d2a543112d314b76bb999a4e"}, ] django-autocomplete-light = [ {file = "django-autocomplete-light-3.8.2.tar.gz", hash = "sha256:25f0ea71b59a8f1f97a8a564e33e429570b0ea77c5eac81f7beb283073b4ba90"}, @@ -1539,8 +1751,8 @@ django-bootstrap3 = [ {file = "django_bootstrap3-15.0.0-py3-none-any.whl", hash = "sha256:601c918a466e6a702f7b394a94826ba9f53c9cc879ccfc26579e3473eef80f53"}, ] django-cors-headers = [ - {file = "django-cors-headers-3.7.0.tar.gz", hash = "sha256:96069c4aaacace786a34ee7894ff680780ec2644e4268b31181044410fecd12e"}, - {file = "django_cors_headers-3.7.0-py3-none-any.whl", hash = "sha256:1ac2b1213de75a251e2ba04448da15f99bcfcbe164288ae6b5ff929dc49b372f"}, + {file = "django-cors-headers-3.8.0.tar.gz", hash = "sha256:4b8e13bf8d3df50ac4b986bd87085c3073dd56402ede109222ea34a774f9ec1b"}, + {file = "django_cors_headers-3.8.0-py3-none-any.whl", hash = "sha256:425c20ceffa42b9ac11b02611eece4ae6c5fef2ff0f039c14c1df20e00c80df8"}, ] django-cors-middleware = [ {file = "django-cors-middleware-1.5.0.tar.gz", hash = "sha256:856dbe4d7aae65844ccc68acb49c6da7dbf7cbacaf5bcf37019f4c0c60b3be84"}, @@ -1555,8 +1767,8 @@ django-crispy-forms = [ {file = "django_crispy_forms-1.12.0-py3-none-any.whl", hash = "sha256:a3320356c84d0cdc631e1ec7b8908aa0117bc2a5f0ab1d053d33eba08f584808"}, ] django-debug-toolbar = [ - {file = "django-debug-toolbar-3.2.1.tar.gz", hash = "sha256:a5ff2a54f24bf88286f9872836081078f4baa843dc3735ee88524e89f8821e33"}, - {file = "django_debug_toolbar-3.2.1-py3-none-any.whl", hash = "sha256:e759e63e3fe2d3110e0e519639c166816368701eab4a47fed75d7de7018467b9"}, + {file = "django-debug-toolbar-3.2.2.tar.gz", hash = "sha256:8c5b13795d4040008ee69ba82dcdd259c49db346cf7d0de6e561a49d191f0860"}, + {file = "django_debug_toolbar-3.2.2-py3-none-any.whl", hash = "sha256:d7bab7573fab35b0fd029163371b7182f5826c13da69734beb675c761d06a4d3"}, ] django-extensions = [ {file = "django-extensions-3.1.3.tar.gz", hash = "sha256:5f0fea7bf131ca303090352577a9e7f8bfbf5489bd9d9c8aea9401db28db34a0"}, @@ -1574,7 +1786,8 @@ django-grappelli = [ {file = "django_grappelli-2.15.1-py2.py3-none-any.whl", hash = "sha256:b9aa386d63a95c37f3741aac98bd545c70ff51cf9104a1419dd6568d2e2cf2ea"}, ] django-handleref = [ - {file = "django-handleref-0.6.0.tar.gz", hash = "sha256:93b001aed1e0133abf8fc1b63b4b4bd60b2ba1375fe5df82a335651f54240a07"}, + {file = "django-handleref-1.0.0.tar.gz", hash = "sha256:3256a0d06d1324e40e3556fc16ab20bd1993db9effef118cf53f9eb4b7e285de"}, + {file = "django_handleref-1.0.0-py3-none-any.whl", hash = "sha256:2614cb7c2e1d9997d7c5ac9c2ff5991f4af7d6ac695ebc7bdf600031561e1701"}, ] django-hashers-passlib = [ {file = "django-hashers-passlib-0.4.tar.gz", hash = "sha256:c8f937cf4a9a21957e28735d1ffd8df242dff863c2e4b92665d98509cd6ae0c4"}, @@ -1584,10 +1797,8 @@ django-haystack = [ {file = "django-haystack-3.0.tar.gz", hash = "sha256:d490f920afa85471dd1fa5000bc8eff4b704daacbe09aee1a64e75cbc426f3be"}, ] django-inet = [ - {file = "django-inet-0.5.0.tar.gz", hash = "sha256:842f0aed36f05168599d1b596cabd971a501e202483bc3bc8cb00e53533cf974"}, -] -django-namespace-perms = [ - {file = "django-namespace-perms-0.6.0.tar.gz", hash = "sha256:ebf35377ed85c4f9c080e5c3a0a4e2029debc639ad8f4850ac4e32329c7a8863"}, + {file = "django-inet-1.0.1.tar.gz", hash = "sha256:9e78ae538ee66263d383f8425b650463e7759f7ae90a93cd6b41096f282d5382"}, + {file = "django_inet-1.0.1-py3-none-any.whl", hash = "sha256:2a9544d4a9a5aa495480ff10fef9f69829765b7c4c95eb1bc21738a3608c843c"}, ] django-oauth-toolkit = [ {file = "django-oauth-toolkit-1.5.0.tar.gz", hash = "sha256:650e5ef2244d1d8db8f507137e0d1e8b8aad1f4086a4a610526e8851f9a38308"}, @@ -1598,7 +1809,8 @@ django-otp = [ {file = "django_otp-1.0.6-py3-none-any.whl", hash = "sha256:01b5888f0bde5125e139433aacb947e52d5c406fa56c9db43c3e8d75b5c323c4"}, ] django-peeringdb = [ - {file = "django-peeringdb-2.7.0.tar.gz", hash = "sha256:0d9ecbd974126f62a676860df8dc5221c17621c57c843c73589e4c556f6904a2"}, + {file = "django-peeringdb-2.8.0.tar.gz", hash = "sha256:b8f19d23a486dc82ec72d57063887a3fd4b1b5325ab9064916005e25a7765fa5"}, + {file = "django_peeringdb-2.8.0-py3-none-any.whl", hash = "sha256:ffde1032480728a2943d1cd89a1d6d7af99d2c982009ff2a1204907d17b49748"}, ] django-phonenumber-field = [ {file = "django-phonenumber-field-5.2.0.tar.gz", hash = "sha256:52b2e5970133ec5ab701218b802f7ab237229854dc95fd239b7e9e77dc43731d"}, @@ -1616,8 +1828,8 @@ django-rest-swagger = [ {file = "django_rest_swagger-2.2.0-py2.py3-none-any.whl", hash = "sha256:b039b0288bab4665cd45dc5d16f94b13911bc4ad0ed55f74ad3b90aa31c87c17"}, ] django-reversion = [ - {file = "django-reversion-3.0.9.tar.gz", hash = "sha256:a5af55f086a3f9c38be2f049c251e06005b9ed48ba7a109473736b1fc95a066f"}, - {file = "django_reversion-3.0.9-py3-none-any.whl", hash = "sha256:1b57127a136b969f4b843a915c72af271febe7f336469db6c27121f8adcad35c"}, + {file = "django-reversion-4.0.0.tar.gz", hash = "sha256:ad6d714b4b9b824e22b88d47201cc0f74b5c4294c8d4e1f8d7ac7c3631ef3188"}, + {file = "django_reversion-4.0.0-py3-none-any.whl", hash = "sha256:f059c654e38c0dd8dccd7f0990aa2f6d9ad22dab55c5e095f9596aeda8079dcd"}, ] django-simple-captcha = [ {file = "django-simple-captcha-0.5.14.zip", hash = "sha256:84b5c188e6ae50e9ecec5e5d734c5bc4d2a50fbbca7f59d2c12da9a3bbee5051"}, @@ -1643,6 +1855,20 @@ djangorestframework-api-key = [ {file = "djangorestframework-api-key-2.0.0.tar.gz", hash = "sha256:61cdb75f16dc4425e0c8587c71f1d890963422c51b4192eec259c6446d7de976"}, {file = "djangorestframework_api_key-2.0.0-py3-none-any.whl", hash = "sha256:631d1898510f6adfd4585539daf5f91630d3a92f1f4b1faa029bd45ccc379736"}, ] +docker = [ + {file = "docker-5.0.0-py2.py3-none-any.whl", hash = "sha256:fc961d622160e8021c10d1bcabc388c57d55fb1f917175afbe24af442e6879bd"}, + {file = "docker-5.0.0.tar.gz", hash = "sha256:3e8bc47534e0ca9331d72c32f2881bb13b93ded0bcdeab3c833fb7cf61c0a9a5"}, +] +docker-compose = [ + {file = "docker-compose-1.29.2.tar.gz", hash = "sha256:4c8cd9d21d237412793d18bd33110049ee9af8dab3fe2c213bbd0733959b09b7"}, + {file = "docker_compose-1.29.2-py2.py3-none-any.whl", hash = "sha256:8d5589373b35c8d3b1c8c1182c6e4a4ff14bffa3dd0b605fcd08f73c94cef809"}, +] +dockerpty = [ + {file = "dockerpty-0.4.1.tar.gz", hash = "sha256:69a9d69d573a0daa31bcd1c0774eeed5c15c295fe719c61aca550ed1393156ce"}, +] +docopt = [ + {file = "docopt-0.6.2.tar.gz", hash = "sha256:49b3a825280bd66b3aa83585ef59c4a8c82f2c8a522dbe754a8bc8d08c85c491"}, +] filelock = [ {file = "filelock-3.0.12-py3-none-any.whl", hash = "sha256:929b7d63ec5b7d6b71b0fa5ac14e030b3f70b75747cef1b10da9b879fef15836"}, {file = "filelock-3.0.12.tar.gz", hash = "sha256:18d82244ee114f543149c66a6e0c14e9c4f8a1044b5cdaadd0f82159d6a6ff59"}, @@ -1655,26 +1881,26 @@ future = [ {file = "future-0.18.2.tar.gz", hash = "sha256:b1bead90b70cf6ec3f0710ae53a525360fa360d306a86583adc6bf83a4db537d"}, ] googlemaps = [ - {file = "googlemaps-4.4.5.tar.gz", hash = "sha256:a91945e4c7cf835a7837f413d54617d0901f02aa16fe382f1b223cacf19539f0"}, + {file = "googlemaps-4.5.3.tar.gz", hash = "sha256:cd6a4bcf6fbdbbffeb5e4cffd7f9db223864cce96c9149fb48358b62050f3a2a"}, ] grainy = [ {file = "grainy-1.8.1.tar.gz", hash = "sha256:2cfd8d50b3f5cce3c463f3c5e86324442f61a7cd46dfe7b134ee926559e56556"}, ] identify = [ - {file = "identify-2.2.11-py2.py3-none-any.whl", hash = "sha256:7abaecbb414e385752e8ce02d8c494f4fbc780c975074b46172598a28f1ab839"}, - {file = "identify-2.2.11.tar.gz", hash = "sha256:a0e700637abcbd1caae58e0463861250095dfe330a8371733a471af706a4a29a"}, + {file = "identify-2.2.13-py2.py3-none-any.whl", hash = "sha256:7199679b5be13a6b40e6e19ea473e789b11b4e3b60986499b1f589ffb03c217c"}, + {file = "identify-2.2.13.tar.gz", hash = "sha256:7bc6e829392bd017236531963d2d937d66fc27cadc643ac0aba2ce9f26157c79"}, ] idna = [ - {file = "idna-2.10-py2.py3-none-any.whl", hash = "sha256:b97d804b1e9b523befed77c48dacec60e6dcb0b5391d57af6a65a312a90648c0"}, - {file = "idna-2.10.tar.gz", hash = "sha256:b307872f855b18632ce0c21c5e45be78c0ea7ae4c15c828c20788b26921eb3f6"}, + {file = "idna-3.2-py3-none-any.whl", hash = "sha256:14475042e284991034cb48e06f6851428fb14c4dc953acd9be9a5e95c7b6dd7a"}, + {file = "idna-3.2.tar.gz", hash = "sha256:467fbad99067910785144ce333826c71fb0e63a425657295239737f7ecd125f3"}, ] iniconfig = [ {file = "iniconfig-1.1.1-py2.py3-none-any.whl", hash = "sha256:011e24c64b7f47f6ebd835bb12a743f2fbe9a26d4cecaa7f53bc4f35ee9da8b3"}, {file = "iniconfig-1.1.1.tar.gz", hash = "sha256:bc3af051d7d14b2ee5ef9969666def0cd1a000e121eaea580d4a313df4b37f32"}, ] isort = [ - {file = "isort-5.9.2-py3-none-any.whl", hash = "sha256:eed17b53c3e7912425579853d078a0832820f023191561fcee9d7cae424e0813"}, - {file = "isort-5.9.2.tar.gz", hash = "sha256:f65ce5bd4cbc6abdfbe29afc2f0245538ab358c14590912df638033f157d555e"}, + {file = "isort-5.9.3-py3-none-any.whl", hash = "sha256:e17d6e2b81095c9db0a03a8025a957f334d6ea30b26f9ec70805411e5c7c81f2"}, + {file = "isort-5.9.3.tar.gz", hash = "sha256:9c2ea1e62d871267b78307fe511c0838ba0da28698c5732d54e2790bf3ba9899"}, ] itypes = [ {file = "itypes-1.2.0-py2.py3-none-any.whl", hash = "sha256:03da6872ca89d29aef62773672b2d408f490f80db48b23079a4b194c86dd04c6"}, @@ -1689,8 +1915,8 @@ jsonschema = [ {file = "jsonschema-3.2.0.tar.gz", hash = "sha256:c8a85b28d377cc7737e46e2d9f2b4f44ee3c0e1deac6bf46ddefc7187d30797a"}, ] jwcrypto = [ - {file = "jwcrypto-0.9.1-py2.py3-none-any.whl", hash = "sha256:12976a09895ec0076ce17c49ab7be64d6e63bcd7fd9a773e3fedf8011537a5f6"}, - {file = "jwcrypto-0.9.1.tar.gz", hash = "sha256:63531529218ba9869e14ef8c9e7b516865ede3facf9b0ef3d3ba68014da211f9"}, + {file = "jwcrypto-1.0-py2.py3-none-any.whl", hash = "sha256:db93a656d9a7a35dda5a68deb5c9f301f4e60507d8aef1559e0637b9ac497137"}, + {file = "jwcrypto-1.0.tar.gz", hash = "sha256:f88816eb0a41b8f006af978ced5f171f33782525006cdb055b536a40f4d46ac9"}, ] markdown = [ {file = "Markdown-3.3.4-py3-none-any.whl", hash = "sha256:96c3ba1261de2f7547b46a00ea8463832c921d3f9d6aba3f255a6f71386db20c"}, @@ -1766,22 +1992,31 @@ packaging = [ {file = "packaging-21.0-py3-none-any.whl", hash = "sha256:c86254f9220d55e31cc94d69bade760f0847da8000def4dfe1c6b872fd14ff14"}, {file = "packaging-21.0.tar.gz", hash = "sha256:7dc96269f53a4ccec5c0670940a4281106dd0bb343f47b7471f779df49c2fbe7"}, ] +paramiko = [ + {file = "paramiko-2.7.2-py2.py3-none-any.whl", hash = "sha256:4f3e316fef2ac628b05097a637af35685183111d4bc1b5979bd397c2ab7b5898"}, + {file = "paramiko-2.7.2.tar.gz", hash = "sha256:7f36f4ba2c0d81d219f4595e35f70d56cc94f9ac40a6acdf51d6ca210ce65035"}, +] passlib = [ {file = "passlib-1.7.4-py2.py3-none-any.whl", hash = "sha256:aa6bca462b8d8bda89c70b382f0c298a20b5560af6cbfa2dce410c0a2fb669f1"}, {file = "passlib-1.7.4.tar.gz", hash = "sha256:defd50f72b65c5402ab2c573830a6978e5f202ad0d984793c8dde2c4152ebe04"}, ] pathspec = [ - {file = "pathspec-0.8.1-py2.py3-none-any.whl", hash = "sha256:aa0cb481c4041bf52ffa7b0d8fa6cd3e88a2ca4879c533c9153882ee2556790d"}, - {file = "pathspec-0.8.1.tar.gz", hash = "sha256:86379d6b86d75816baba717e64b1a3a3469deb93bb76d613c9ce79edc5cb68fd"}, + {file = "pathspec-0.9.0-py2.py3-none-any.whl", hash = "sha256:7d15c4ddb0b5c802d161efc417ec1a2558ea2653c2e8ad9c19098201dc1c993a"}, + {file = "pathspec-0.9.0.tar.gz", hash = "sha256:e564499435a2673d586f6b2130bb5b95f04a3ba06f81b8f895b651a3c76aabb1"}, ] peeringdb = [ {file = "peeringdb-1.1.0.tar.gz", hash = "sha256:927a34c31e5b93130a855bb4c8fd84dcf604b9939678f75918f7c1bd8a501471"}, ] phonenumbers = [ - {file = "phonenumbers-8.12.27-py2.py3-none-any.whl", hash = "sha256:64ecba30985d580d03086598d977dba812ac499514f2e1f8a2795af2f0de1aa6"}, - {file = "phonenumbers-8.12.27.tar.gz", hash = "sha256:856f6bebf19eafe5ab1d50b2f4972f8d7474a7e1b8c0f9cf3263a26602ac81f3"}, + {file = "phonenumbers-8.12.30-py2.py3-none-any.whl", hash = "sha256:f059f0555f1e47591406729b9e516af417e6a61aa0a5458fd01b2548232715e0"}, + {file = "phonenumbers-8.12.30.tar.gz", hash = "sha256:9ca65c36f437881a8f7dac979a5733ae8fb5a0a436aecd47bd2c06494bdf0a20"}, ] pillow = [ + {file = "Pillow-8.3.1-1-cp36-cp36m-win_amd64.whl", hash = "sha256:fd7eef578f5b2200d066db1b50c4aa66410786201669fb76d5238b007918fb24"}, + {file = "Pillow-8.3.1-1-cp37-cp37m-win_amd64.whl", hash = "sha256:75e09042a3b39e0ea61ce37e941221313d51a9c26b8e54e12b3ececccb71718a"}, + {file = "Pillow-8.3.1-1-cp38-cp38-win_amd64.whl", hash = "sha256:c0e0550a404c69aab1e04ae89cca3e2a042b56ab043f7f729d984bf73ed2a093"}, + {file = "Pillow-8.3.1-1-cp39-cp39-win_amd64.whl", hash = "sha256:479ab11cbd69612acefa8286481f65c5dece2002ffaa4f9db62682379ca3bb77"}, + {file = "Pillow-8.3.1-1-pp37-pypy37_pp73-win_amd64.whl", hash = "sha256:f156d6ecfc747ee111c167f8faf5f4953761b5e66e91a4e6767e548d0f80129c"}, {file = "Pillow-8.3.1-cp36-cp36m-macosx_10_10_x86_64.whl", hash = "sha256:196560dba4da7a72c5e7085fccc5938ab4075fd37fe8b5468869724109812edd"}, {file = "Pillow-8.3.1-cp36-cp36m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:29c9569049d04aaacd690573a0398dbd8e0bf0255684fee512b413c2142ab723"}, {file = "Pillow-8.3.1-cp36-cp36m-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:c088a000dfdd88c184cc7271bfac8c5b82d9efa8637cd2b68183771e3cf56f04"}, @@ -1817,13 +2052,17 @@ pillow = [ {file = "Pillow-8.3.1-pp37-pypy37_pp73-win_amd64.whl", hash = "sha256:1c03e24be975e2afe70dfc5da6f187eea0b49a68bb2b69db0f30a61b7031cee4"}, {file = "Pillow-8.3.1.tar.gz", hash = "sha256:2cac53839bfc5cece8fdbe7f084d5e3ee61e1303cccc86511d351adcb9e2c792"}, ] +platformdirs = [ + {file = "platformdirs-2.2.0-py3-none-any.whl", hash = "sha256:4666d822218db6a262bdfdc9c39d21f23b4cfdb08af331a81e92751daf6c866c"}, + {file = "platformdirs-2.2.0.tar.gz", hash = "sha256:632daad3ab546bd8e6af0537d09805cec458dce201bccfe23012df73332e181e"}, +] pluggy = [ {file = "pluggy-0.13.1-py2.py3-none-any.whl", hash = "sha256:966c145cd83c96502c3c3868f50408687b38434af77734af1e9ca461a4081d2d"}, {file = "pluggy-0.13.1.tar.gz", hash = "sha256:15b2acde666561e1298d71b523007ed7364de07029219b604cf808bfa1c765b0"}, ] pre-commit = [ - {file = "pre_commit-2.13.0-py2.py3-none-any.whl", hash = "sha256:b679d0fddd5b9d6d98783ae5f10fd0c4c59954f375b70a58cbe1ce9bcf9809a4"}, - {file = "pre_commit-2.13.0.tar.gz", hash = "sha256:764972c60693dc668ba8e86eb29654ec3144501310f7198742a767bec385a378"}, + {file = "pre_commit-2.14.0-py2.py3-none-any.whl", hash = "sha256:ec3045ae62e1aa2eecfb8e86fa3025c2e3698f77394ef8d2011ce0aedd85b2d4"}, + {file = "pre_commit-2.14.0.tar.gz", hash = "sha256:2386eeb4cf6633712c7cc9ede83684d53c8cafca6b59f79c738098b51c6d206c"}, ] py = [ {file = "py-1.10.0-py2.py3-none-any.whl", hash = "sha256:3b80836aa6d1feeaa108e046da6423ab8f6ceda6468545ae8d02d9d58d18818a"}, @@ -1845,6 +2084,26 @@ pyjwt = [ {file = "PyJWT-2.1.0-py3-none-any.whl", hash = "sha256:934d73fbba91b0483d3857d1aff50e96b2a892384ee2c17417ed3203f173fca1"}, {file = "PyJWT-2.1.0.tar.gz", hash = "sha256:fba44e7898bbca160a2b2b501f492824fc8382485d3a6f11ba5d0c1937ce6130"}, ] +pynacl = [ + {file = "PyNaCl-1.4.0-cp27-cp27m-macosx_10_10_x86_64.whl", hash = "sha256:ea6841bc3a76fa4942ce00f3bda7d436fda21e2d91602b9e21b7ca9ecab8f3ff"}, + {file = "PyNaCl-1.4.0-cp27-cp27m-manylinux1_x86_64.whl", hash = "sha256:d452a6746f0a7e11121e64625109bc4468fc3100452817001dbe018bb8b08514"}, + {file = "PyNaCl-1.4.0-cp27-cp27m-win32.whl", hash = "sha256:2fe0fc5a2480361dcaf4e6e7cea00e078fcda07ba45f811b167e3f99e8cff574"}, + {file = "PyNaCl-1.4.0-cp27-cp27m-win_amd64.whl", hash = "sha256:f8851ab9041756003119368c1e6cd0b9c631f46d686b3904b18c0139f4419f80"}, + {file = "PyNaCl-1.4.0-cp27-cp27mu-manylinux1_x86_64.whl", hash = "sha256:7757ae33dae81c300487591c68790dfb5145c7d03324000433d9a2c141f82af7"}, + {file = "PyNaCl-1.4.0-cp35-abi3-macosx_10_10_x86_64.whl", hash = "sha256:757250ddb3bff1eecd7e41e65f7f833a8405fede0194319f87899690624f2122"}, + {file = "PyNaCl-1.4.0-cp35-abi3-manylinux1_x86_64.whl", hash = "sha256:30f9b96db44e09b3304f9ea95079b1b7316b2b4f3744fe3aaecccd95d547063d"}, + {file = "PyNaCl-1.4.0-cp35-abi3-win32.whl", hash = "sha256:4e10569f8cbed81cb7526ae137049759d2a8d57726d52c1a000a3ce366779634"}, + {file = "PyNaCl-1.4.0-cp35-abi3-win_amd64.whl", hash = "sha256:c914f78da4953b33d4685e3cdc7ce63401247a21425c16a39760e282075ac4a6"}, + {file = "PyNaCl-1.4.0-cp35-cp35m-win32.whl", hash = "sha256:06cbb4d9b2c4bd3c8dc0d267416aaed79906e7b33f114ddbf0911969794b1cc4"}, + {file = "PyNaCl-1.4.0-cp35-cp35m-win_amd64.whl", hash = "sha256:511d269ee845037b95c9781aa702f90ccc36036f95d0f31373a6a79bd8242e25"}, + {file = "PyNaCl-1.4.0-cp36-cp36m-win32.whl", hash = "sha256:11335f09060af52c97137d4ac54285bcb7df0cef29014a1a4efe64ac065434c4"}, + {file = "PyNaCl-1.4.0-cp36-cp36m-win_amd64.whl", hash = "sha256:cd401ccbc2a249a47a3a1724c2918fcd04be1f7b54eb2a5a71ff915db0ac51c6"}, + {file = "PyNaCl-1.4.0-cp37-cp37m-win32.whl", hash = "sha256:8122ba5f2a2169ca5da936b2e5a511740ffb73979381b4229d9188f6dcb22f1f"}, + {file = "PyNaCl-1.4.0-cp37-cp37m-win_amd64.whl", hash = "sha256:537a7ccbea22905a0ab36ea58577b39d1fa9b1884869d173b5cf111f006f689f"}, + {file = "PyNaCl-1.4.0-cp38-cp38-win32.whl", hash = "sha256:9c4a7ea4fb81536c1b1f5cc44d54a296f96ae78c1ebd2311bd0b60be45a48d96"}, + {file = "PyNaCl-1.4.0-cp38-cp38-win_amd64.whl", hash = "sha256:7c6092102219f59ff29788860ccb021e80fffd953920c4a8653889c029b2d420"}, + {file = "PyNaCl-1.4.0.tar.gz", hash = "sha256:54e9a2c849c742006516ad56a88f5c74bf2ce92c9f67435187c3c5953b346505"}, +] pyparsing = [ {file = "pyparsing-2.4.7-py2.py3-none-any.whl", hash = "sha256:ef9d7589ef3c200abe66653d3f1ab1033c3c419ae9b9bdb1240a85b024efc88b"}, {file = "pyparsing-2.4.7.tar.gz", hash = "sha256:c203ec8783bf771a155b207279b9bccb8dea02d8f0c9e5f8ead507bc3246ecc1"}, @@ -1891,6 +2150,10 @@ pytest-mock = [ {file = "pytest-mock-3.6.1.tar.gz", hash = "sha256:40217a058c52a63f1042f0784f62009e976ba824c418cced42e88d5f40ab0e62"}, {file = "pytest_mock-3.6.1-py3-none-any.whl", hash = "sha256:30c2f2cc9759e76eee674b81ea28c9f0b94f8f0445a1b87762cadf774f0df7e3"}, ] +python-dotenv = [ + {file = "python-dotenv-0.19.0.tar.gz", hash = "sha256:f521bc2ac9a8e03c736f62911605c5d83970021e3fa95b37d769e2bbbe9b6172"}, + {file = "python_dotenv-0.19.0-py2.py3-none-any.whl", hash = "sha256:aae25dc1ebe97c420f50b81fb0e5c949659af713f31fdb63c749ca68748f34b1"}, +] python3-openid = [ {file = "python3-openid-3.2.0.tar.gz", hash = "sha256:33fbf6928f401e0b790151ed2b5290b02545e8775f982485205a066f874aaeaf"}, {file = "python3_openid-3.2.0-py3-none-any.whl", hash = "sha256:6626f771e0417486701e0b4daff762e7212e820ca5b29fcc0d05f6f8736dfa6b"}, @@ -1900,8 +2163,22 @@ pytz = [ {file = "pytz-2021.1.tar.gz", hash = "sha256:83a4a90894bf38e243cf052c8b58f381bfe9a7a483f6a9cab140bc7f702ac4da"}, ] pyupgrade = [ - {file = "pyupgrade-2.20.0-py2.py3-none-any.whl", hash = "sha256:e646016d0e4a0fc171fd0a56177facf0f16dd2a7b30b2a3a08aa4ad9dd90e0d2"}, - {file = "pyupgrade-2.20.0.tar.gz", hash = "sha256:33848f4656fd8f35bfe073e6f23b0e249f010533ab06b6866d57f812b3e026a2"}, + {file = "pyupgrade-2.23.3-py2.py3-none-any.whl", hash = "sha256:ee2355a5f4bf8541eed3687545c59640e50789268cf1802cf214540e5bcc860a"}, + {file = "pyupgrade-2.23.3.tar.gz", hash = "sha256:c5262dcfea5b464bd0e4eb9c3b402bee604b9414885df11cf44ed1eca3037dc4"}, +] +pywin32 = [ + {file = "pywin32-227-cp27-cp27m-win32.whl", hash = "sha256:371fcc39416d736401f0274dd64c2302728c9e034808e37381b5e1b22be4a6b0"}, + {file = "pywin32-227-cp27-cp27m-win_amd64.whl", hash = "sha256:4cdad3e84191194ea6d0dd1b1b9bdda574ff563177d2adf2b4efec2a244fa116"}, + {file = "pywin32-227-cp35-cp35m-win32.whl", hash = "sha256:f4c5be1a293bae0076d93c88f37ee8da68136744588bc5e2be2f299a34ceb7aa"}, + {file = "pywin32-227-cp35-cp35m-win_amd64.whl", hash = "sha256:a929a4af626e530383a579431b70e512e736e9588106715215bf685a3ea508d4"}, + {file = "pywin32-227-cp36-cp36m-win32.whl", hash = "sha256:300a2db938e98c3e7e2093e4491439e62287d0d493fe07cce110db070b54c0be"}, + {file = "pywin32-227-cp36-cp36m-win_amd64.whl", hash = "sha256:9b31e009564fb95db160f154e2aa195ed66bcc4c058ed72850d047141b36f3a2"}, + {file = "pywin32-227-cp37-cp37m-win32.whl", hash = "sha256:47a3c7551376a865dd8d095a98deba954a98f326c6fe3c72d8726ca6e6b15507"}, + {file = "pywin32-227-cp37-cp37m-win_amd64.whl", hash = "sha256:31f88a89139cb2adc40f8f0e65ee56a8c585f629974f9e07622ba80199057511"}, + {file = "pywin32-227-cp38-cp38-win32.whl", hash = "sha256:7f18199fbf29ca99dff10e1f09451582ae9e372a892ff03a28528a24d55875bc"}, + {file = "pywin32-227-cp38-cp38-win_amd64.whl", hash = "sha256:7c1ae32c489dc012930787f06244426f8356e129184a02c25aef163917ce158e"}, + {file = "pywin32-227-cp39-cp39-win32.whl", hash = "sha256:c054c52ba46e7eb6b7d7dfae4dbd987a1bb48ee86debe3f245a2884ece46e295"}, + {file = "pywin32-227-cp39-cp39-win_amd64.whl", hash = "sha256:f27cec5e7f588c3d1051651830ecc00294f90728d19c3bf6916e6dba93ea357c"}, ] pyyaml = [ {file = "PyYAML-5.4.1-cp27-cp27m-macosx_10_9_x86_64.whl", hash = "sha256:3b2b1824fe7112845700f815ff6a489360226a5609b96ec2190a45e62a9fc922"}, @@ -1935,51 +2212,43 @@ rdap = [ {file = "rdap-1.2.1.tar.gz", hash = "sha256:f2b2d379ba6f9d19027d5cca05ffe35424259754c702f0ab48c2960f4c52d21b"}, ] regex = [ - {file = "regex-2021.7.6-cp36-cp36m-macosx_10_9_x86_64.whl", hash = "sha256:e6a1e5ca97d411a461041d057348e578dc344ecd2add3555aedba3b408c9f874"}, - {file = "regex-2021.7.6-cp36-cp36m-manylinux1_i686.whl", hash = "sha256:6afe6a627888c9a6cfbb603d1d017ce204cebd589d66e0703309b8048c3b0854"}, - {file = "regex-2021.7.6-cp36-cp36m-manylinux1_x86_64.whl", hash = "sha256:ccb3d2190476d00414aab36cca453e4596e8f70a206e2aa8db3d495a109153d2"}, - {file = "regex-2021.7.6-cp36-cp36m-manylinux2010_i686.whl", hash = "sha256:ed693137a9187052fc46eedfafdcb74e09917166362af4cc4fddc3b31560e93d"}, - {file = "regex-2021.7.6-cp36-cp36m-manylinux2010_x86_64.whl", hash = "sha256:99d8ab206a5270c1002bfcf25c51bf329ca951e5a169f3b43214fdda1f0b5f0d"}, - {file = "regex-2021.7.6-cp36-cp36m-manylinux2014_i686.whl", hash = "sha256:b85ac458354165405c8a84725de7bbd07b00d9f72c31a60ffbf96bb38d3e25fa"}, - {file = "regex-2021.7.6-cp36-cp36m-manylinux2014_x86_64.whl", hash = "sha256:3f5716923d3d0bfb27048242a6e0f14eecdb2e2a7fac47eda1d055288595f222"}, - {file = "regex-2021.7.6-cp36-cp36m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e5983c19d0beb6af88cb4d47afb92d96751fb3fa1784d8785b1cdf14c6519407"}, - {file = "regex-2021.7.6-cp36-cp36m-win32.whl", hash = "sha256:c92831dac113a6e0ab28bc98f33781383fe294df1a2c3dfd1e850114da35fd5b"}, - {file = "regex-2021.7.6-cp36-cp36m-win_amd64.whl", hash = "sha256:791aa1b300e5b6e5d597c37c346fb4d66422178566bbb426dd87eaae475053fb"}, - {file = "regex-2021.7.6-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:59506c6e8bd9306cd8a41511e32d16d5d1194110b8cfe5a11d102d8b63cf945d"}, - {file = "regex-2021.7.6-cp37-cp37m-manylinux1_i686.whl", hash = "sha256:564a4c8a29435d1f2256ba247a0315325ea63335508ad8ed938a4f14c4116a5d"}, - {file = "regex-2021.7.6-cp37-cp37m-manylinux1_x86_64.whl", hash = "sha256:59c00bb8dd8775473cbfb967925ad2c3ecc8886b3b2d0c90a8e2707e06c743f0"}, - {file = "regex-2021.7.6-cp37-cp37m-manylinux2010_i686.whl", hash = "sha256:9a854b916806c7e3b40e6616ac9e85d3cdb7649d9e6590653deb5b341a736cec"}, - {file = "regex-2021.7.6-cp37-cp37m-manylinux2010_x86_64.whl", hash = "sha256:db2b7df831c3187a37f3bb80ec095f249fa276dbe09abd3d35297fc250385694"}, - {file = "regex-2021.7.6-cp37-cp37m-manylinux2014_i686.whl", hash = "sha256:173bc44ff95bc1e96398c38f3629d86fa72e539c79900283afa895694229fe6a"}, - {file = "regex-2021.7.6-cp37-cp37m-manylinux2014_x86_64.whl", hash = "sha256:15dddb19823f5147e7517bb12635b3c82e6f2a3a6b696cc3e321522e8b9308ad"}, - {file = "regex-2021.7.6-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:2ddeabc7652024803666ea09f32dd1ed40a0579b6fbb2a213eba590683025895"}, - {file = "regex-2021.7.6-cp37-cp37m-win32.whl", hash = "sha256:f080248b3e029d052bf74a897b9d74cfb7643537fbde97fe8225a6467fb559b5"}, - {file = "regex-2021.7.6-cp37-cp37m-win_amd64.whl", hash = "sha256:d8bbce0c96462dbceaa7ac4a7dfbbee92745b801b24bce10a98d2f2b1ea9432f"}, - {file = "regex-2021.7.6-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:edd1a68f79b89b0c57339bce297ad5d5ffcc6ae7e1afdb10f1947706ed066c9c"}, - {file = "regex-2021.7.6-cp38-cp38-manylinux1_i686.whl", hash = "sha256:422dec1e7cbb2efbbe50e3f1de36b82906def93ed48da12d1714cabcd993d7f0"}, - {file = "regex-2021.7.6-cp38-cp38-manylinux1_x86_64.whl", hash = "sha256:cbe23b323988a04c3e5b0c387fe3f8f363bf06c0680daf775875d979e376bd26"}, - {file = "regex-2021.7.6-cp38-cp38-manylinux2010_i686.whl", hash = "sha256:0eb2c6e0fcec5e0f1d3bcc1133556563222a2ffd2211945d7b1480c1b1a42a6f"}, - {file = "regex-2021.7.6-cp38-cp38-manylinux2010_x86_64.whl", hash = "sha256:1c78780bf46d620ff4fff40728f98b8afd8b8e35c3efd638c7df67be2d5cddbf"}, - {file = "regex-2021.7.6-cp38-cp38-manylinux2014_i686.whl", hash = "sha256:bc84fb254a875a9f66616ed4538542fb7965db6356f3df571d783f7c8d256edd"}, - {file = "regex-2021.7.6-cp38-cp38-manylinux2014_x86_64.whl", hash = "sha256:598c0a79b4b851b922f504f9f39a863d83ebdfff787261a5ed061c21e67dd761"}, - {file = "regex-2021.7.6-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:875c355360d0f8d3d827e462b29ea7682bf52327d500a4f837e934e9e4656068"}, - {file = "regex-2021.7.6-cp38-cp38-win32.whl", hash = "sha256:e586f448df2bbc37dfadccdb7ccd125c62b4348cb90c10840d695592aa1b29e0"}, - {file = "regex-2021.7.6-cp38-cp38-win_amd64.whl", hash = "sha256:2fe5e71e11a54e3355fa272137d521a40aace5d937d08b494bed4529964c19c4"}, - {file = "regex-2021.7.6-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:6110bab7eab6566492618540c70edd4d2a18f40ca1d51d704f1d81c52d245026"}, - {file = "regex-2021.7.6-cp39-cp39-manylinux1_i686.whl", hash = "sha256:4f64fc59fd5b10557f6cd0937e1597af022ad9b27d454e182485f1db3008f417"}, - {file = "regex-2021.7.6-cp39-cp39-manylinux1_x86_64.whl", hash = "sha256:89e5528803566af4df368df2d6f503c84fbfb8249e6631c7b025fe23e6bd0cde"}, - {file = "regex-2021.7.6-cp39-cp39-manylinux2010_i686.whl", hash = "sha256:2366fe0479ca0e9afa534174faa2beae87847d208d457d200183f28c74eaea59"}, - {file = "regex-2021.7.6-cp39-cp39-manylinux2010_x86_64.whl", hash = "sha256:f9392a4555f3e4cb45310a65b403d86b589adc773898c25a39184b1ba4db8985"}, - {file = "regex-2021.7.6-cp39-cp39-manylinux2014_i686.whl", hash = "sha256:2bceeb491b38225b1fee4517107b8491ba54fba77cf22a12e996d96a3c55613d"}, - {file = "regex-2021.7.6-cp39-cp39-manylinux2014_x86_64.whl", hash = "sha256:f98dc35ab9a749276f1a4a38ab3e0e2ba1662ce710f6530f5b0a6656f1c32b58"}, - {file = "regex-2021.7.6-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:319eb2a8d0888fa6f1d9177705f341bc9455a2c8aca130016e52c7fe8d6c37a3"}, - {file = "regex-2021.7.6-cp39-cp39-win32.whl", hash = "sha256:eaf58b9e30e0e546cdc3ac06cf9165a1ca5b3de8221e9df679416ca667972035"}, - {file = "regex-2021.7.6-cp39-cp39-win_amd64.whl", hash = "sha256:4c9c3155fe74269f61e27617529b7f09552fbb12e44b1189cebbdb24294e6e1c"}, - {file = "regex-2021.7.6.tar.gz", hash = "sha256:8394e266005f2d8c6f0bc6780001f7afa3ef81a7a2111fa35058ded6fce79e4d"}, + {file = "regex-2021.8.3-cp36-cp36m-macosx_10_9_x86_64.whl", hash = "sha256:8764a78c5464ac6bde91a8c87dd718c27c1cabb7ed2b4beaf36d3e8e390567f9"}, + {file = "regex-2021.8.3-cp36-cp36m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:4551728b767f35f86b8e5ec19a363df87450c7376d7419c3cac5b9ceb4bce576"}, + {file = "regex-2021.8.3-cp36-cp36m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:577737ec3d4c195c4aef01b757905779a9e9aee608fa1cf0aec16b5576c893d3"}, + {file = "regex-2021.8.3-cp36-cp36m-manylinux_2_5_i686.manylinux1_i686.manylinux_2_12_i686.manylinux2010_i686.whl", hash = "sha256:c856ec9b42e5af4fe2d8e75970fcc3a2c15925cbcc6e7a9bcb44583b10b95e80"}, + {file = "regex-2021.8.3-cp36-cp36m-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:3835de96524a7b6869a6c710b26c90e94558c31006e96ca3cf6af6751b27dca1"}, + {file = "regex-2021.8.3-cp36-cp36m-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:cea56288eeda8b7511d507bbe7790d89ae7049daa5f51ae31a35ae3c05408531"}, + {file = "regex-2021.8.3-cp36-cp36m-win32.whl", hash = "sha256:a4eddbe2a715b2dd3849afbdeacf1cc283160b24e09baf64fa5675f51940419d"}, + {file = "regex-2021.8.3-cp36-cp36m-win_amd64.whl", hash = "sha256:57fece29f7cc55d882fe282d9de52f2f522bb85290555b49394102f3621751ee"}, + {file = "regex-2021.8.3-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:a5c6dbe09aff091adfa8c7cfc1a0e83fdb8021ddb2c183512775a14f1435fe16"}, + {file = "regex-2021.8.3-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ff4a8ad9638b7ca52313d8732f37ecd5fd3c8e3aff10a8ccb93176fd5b3812f6"}, + {file = "regex-2021.8.3-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:b63e3571b24a7959017573b6455e05b675050bbbea69408f35f3cb984ec54363"}, + {file = "regex-2021.8.3-cp37-cp37m-manylinux_2_5_i686.manylinux1_i686.manylinux_2_12_i686.manylinux2010_i686.whl", hash = "sha256:fbc20975eee093efa2071de80df7f972b7b35e560b213aafabcec7c0bd00bd8c"}, + {file = "regex-2021.8.3-cp37-cp37m-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:14caacd1853e40103f59571f169704367e79fb78fac3d6d09ac84d9197cadd16"}, + {file = "regex-2021.8.3-cp37-cp37m-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:bb350eb1060591d8e89d6bac4713d41006cd4d479f5e11db334a48ff8999512f"}, + {file = "regex-2021.8.3-cp37-cp37m-win32.whl", hash = "sha256:18fdc51458abc0a974822333bd3a932d4e06ba2a3243e9a1da305668bd62ec6d"}, + {file = "regex-2021.8.3-cp37-cp37m-win_amd64.whl", hash = "sha256:026beb631097a4a3def7299aa5825e05e057de3c6d72b139c37813bfa351274b"}, + {file = "regex-2021.8.3-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:16d9eaa8c7e91537516c20da37db975f09ac2e7772a0694b245076c6d68f85da"}, + {file = "regex-2021.8.3-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:3905c86cc4ab6d71635d6419a6f8d972cab7c634539bba6053c47354fd04452c"}, + {file = "regex-2021.8.3-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:937b20955806381e08e54bd9d71f83276d1f883264808521b70b33d98e4dec5d"}, + {file = "regex-2021.8.3-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_12_i686.manylinux2010_i686.whl", hash = "sha256:28e8af338240b6f39713a34e337c3813047896ace09d51593d6907c66c0708ba"}, + {file = "regex-2021.8.3-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:3c09d88a07483231119f5017904db8f60ad67906efac3f1baa31b9b7f7cca281"}, + {file = "regex-2021.8.3-cp38-cp38-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:85f568892422a0e96235eb8ea6c5a41c8ccbf55576a2260c0160800dbd7c4f20"}, + {file = "regex-2021.8.3-cp38-cp38-win32.whl", hash = "sha256:bf6d987edd4a44dd2fa2723fca2790f9442ae4de2c8438e53fcb1befdf5d823a"}, + {file = "regex-2021.8.3-cp38-cp38-win_amd64.whl", hash = "sha256:8fe58d9f6e3d1abf690174fd75800fda9bdc23d2a287e77758dc0e8567e38ce6"}, + {file = "regex-2021.8.3-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:7976d410e42be9ae7458c1816a416218364e06e162b82e42f7060737e711d9ce"}, + {file = "regex-2021.8.3-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:9569da9e78f0947b249370cb8fadf1015a193c359e7e442ac9ecc585d937f08d"}, + {file = "regex-2021.8.3-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:459bbe342c5b2dec5c5223e7c363f291558bc27982ef39ffd6569e8c082bdc83"}, + {file = "regex-2021.8.3-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_12_i686.manylinux2010_i686.whl", hash = "sha256:4f421e3cdd3a273bace013751c345f4ebeef08f05e8c10757533ada360b51a39"}, + {file = "regex-2021.8.3-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:ea212df6e5d3f60341aef46401d32fcfded85593af1d82b8b4a7a68cd67fdd6b"}, + {file = "regex-2021.8.3-cp39-cp39-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:a3b73390511edd2db2d34ff09aa0b2c08be974c71b4c0505b4a048d5dc128c2b"}, + {file = "regex-2021.8.3-cp39-cp39-win32.whl", hash = "sha256:f35567470ee6dbfb946f069ed5f5615b40edcbb5f1e6e1d3d2b114468d505fc6"}, + {file = "regex-2021.8.3-cp39-cp39-win_amd64.whl", hash = "sha256:bfa6a679410b394600eafd16336b2ce8de43e9b13f7fb9247d84ef5ad2b45e91"}, + {file = "regex-2021.8.3.tar.gz", hash = "sha256:8935937dad2c9b369c3d932b0edbc52a62647c2afb2fafc0c280f14a8bf56a6a"}, ] requests = [ - {file = "requests-2.25.1-py2.py3-none-any.whl", hash = "sha256:c210084e36a42ae6b9219e00e48287def368a26d03a048ddad7bfee44f75871e"}, - {file = "requests-2.25.1.tar.gz", hash = "sha256:27973dd4a904a4f13b263a19c866c13b92a39ed1c964655f025f3f8d3d75b804"}, + {file = "requests-2.26.0-py2.py3-none-any.whl", hash = "sha256:6c1246513ecd5ecd4528a0906f910e8f0f9c6b8ec72030dc9fd154dc1a6efd24"}, + {file = "requests-2.26.0.tar.gz", hash = "sha256:b8aa58f8cf793ffd8782d3d8cb19e66ef36f7aba4353eec859e74678b01b07a7"}, ] requests-mock = [ {file = "requests-mock-1.9.3.tar.gz", hash = "sha256:8d72abe54546c1fc9696fa1516672f1031d72a55a1d66c85184f972a24ba0eba"}, @@ -2038,6 +2307,10 @@ sqlparse = [ {file = "sqlparse-0.4.1-py3-none-any.whl", hash = "sha256:017cde379adbd6a1f15a61873f43e8274179378e95ef3fede90b5aa64d304ed0"}, {file = "sqlparse-0.4.1.tar.gz", hash = "sha256:0f91fd2e829c44362cbcfab3e9ae12e22badaa8a29ad5ff599f9ec109f0454e8"}, ] +texttable = [ + {file = "texttable-1.6.4-py2.py3-none-any.whl", hash = "sha256:dd2b0eaebb2a9e167d1cefedab4700e5dcbdb076114eed30b58b97ed6b37d6f2"}, + {file = "texttable-1.6.4.tar.gz", hash = "sha256:42ee7b9e15f7b225747c3fa08f43c5d6c83bc899f80ff9bae9319334824076e9"}, +] tld = [ {file = "tld-0.12.6-py27-none-any.whl", hash = "sha256:ef5b162d6fa295822dacd4fe4df1b62d8df2550795a97399a8905821b58d3702"}, {file = "tld-0.12.6-py35-none-any.whl", hash = "sha256:826bbe61dccc8d63144b51caef83e1373fbaac6f9ada46fca7846021f5d36fef"}, @@ -2055,6 +2328,10 @@ toml = [ {file = "toml-0.10.2-py2.py3-none-any.whl", hash = "sha256:806143ae5bfb6a3c6e736a764057db0e6a0e05e338b5630894a5f779cabb4f9b"}, {file = "toml-0.10.2.tar.gz", hash = "sha256:b3bda1d108d5dd99f4a20d24d9c348e91c4db7ab1b749200bded2f839ccbe68f"}, ] +tomli = [ + {file = "tomli-1.2.1-py3-none-any.whl", hash = "sha256:8dd0e9524d6f386271a36b41dbf6c57d8e32fd96fd22b6584679dc569d20899f"}, + {file = "tomli-1.2.1.tar.gz", hash = "sha256:a5b75cb6f3968abb47af1b40c1819dc519ea82bcc065776a866e8d74c5ca9442"}, +] "twentyc.rpc" = [ {file = "twentyc.rpc-0.4.0.tar.gz", hash = "sha256:c6a08a0fa8610332f430911061a662efee8c251a5568c1ffd592c566d9da0768"}, ] @@ -2074,13 +2351,17 @@ uwsgi = [ {file = "uWSGI-2.0.19.1.tar.gz", hash = "sha256:faa85e053c0b1be4d5585b0858d3a511d2cd10201802e8676060fd0a109e5869"}, ] virtualenv = [ - {file = "virtualenv-20.4.7-py2.py3-none-any.whl", hash = "sha256:2b0126166ea7c9c3661f5b8e06773d28f83322de7a3ff7d06f0aed18c9de6a76"}, - {file = "virtualenv-20.4.7.tar.gz", hash = "sha256:14fdf849f80dbb29a4eb6caa9875d476ee2a5cf76a5f5415fa2f1606010ab467"}, + {file = "virtualenv-20.7.2-py2.py3-none-any.whl", hash = "sha256:e4670891b3a03eb071748c569a87cceaefbf643c5bac46d996c5a45c34aa0f06"}, + {file = "virtualenv-20.7.2.tar.gz", hash = "sha256:9ef4e8ee4710826e98ff3075c9a4739e2cb1040de6a2a8d35db0055840dc96a0"}, ] webencodings = [ {file = "webencodings-0.5.1-py2.py3-none-any.whl", hash = "sha256:a0af1213f3c2226497a97e2b3aa01a7e4bee4f403f95be16fc9acd2947514a78"}, {file = "webencodings-0.5.1.tar.gz", hash = "sha256:b36a1c245f2d304965eb4e0a82848379241dc04b865afcc4aab16748587e1923"}, ] +websocket-client = [ + {file = "websocket-client-0.59.0.tar.gz", hash = "sha256:d376bd60eace9d437ab6d7ee16f4ab4e821c9dae591e1b783c58ebd8aaf80c5c"}, + {file = "websocket_client-0.59.0-py2.py3-none-any.whl", hash = "sha256:2e50d26ca593f70aba7b13a489435ef88b8fc3b5c5643c1ce8808ff9b40f0b32"}, +] whoosh = [ {file = "Whoosh-2.7.4-py2.py3-none-any.whl", hash = "sha256:aa39c3c3426e3fd107dcb4bde64ca1e276a65a889d9085a6e4b54ba82420a852"}, {file = "Whoosh-2.7.4.tar.gz", hash = "sha256:7ca5633dbfa9e0e0fa400d3151a8a0c4bec53bd2ecedc0a67705b17565c31a83"}, diff --git a/pyproject.toml b/pyproject.toml index a97b1f37..9298a660 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -10,11 +10,10 @@ license = "BSD-2-Clause" [tool.poetry.dependencies] python = "^3.9" # core requirements -django = ">=2.2, <2.3" -django-inet = ">=0.5.0, <0.6" -django-handleref = ">=0.6.0, <0.7" -django-namespace-perms = ">=0.6.0, <0.7" -django-peeringdb = "==2.7.0" +django = ">=3.2, <4" +django-inet = "^1.0" +django-handleref = "^1.0" +django-peeringdb = "==2.8.0" djangorestframework = ">=3.12,<3.13" mysqlclient = ">=1.3.9" peeringdb = ">=1.1.0, <2" @@ -47,7 +46,7 @@ phonenumbers = ">=8.11.1" rdap = "==1.2.1" unidecode = ">=1.0.23" # these should just be pulled in automatically? -django-reversion = ">=3, <4" +django-reversion = ">=4, <5" certifi = ">=2017.11.5" tld = ">=0.7.6" # deprecated from drf -- used by rest swagger @@ -75,6 +74,7 @@ isort = "^5.7.0" flake8 = "^3.8.4" pre-commit = "^2.13.0" pyupgrade = "^2.19.4" +docker-compose = "^1.29.2" [build-system] requires = [ "poetry>=0.12",] diff --git a/tests/test_cmd_ixf_ixp_member_import.py b/tests/test_cmd_ixf_ixp_member_import.py index 3fdf806a..97b88023 100644 --- a/tests/test_cmd_ixf_ixp_member_import.py +++ b/tests/test_cmd_ixf_ixp_member_import.py @@ -44,6 +44,23 @@ def test_reset_hints(entities, data_cmd_ixf_hints): assert DeskProTicket.objects.filter(body__contains="reset_hints").count() == 1 +@pytest.mark.django_db +def test_reset_process_requested(entities): + ixlan = entities["ixlan"] + ixlan.ixf_ixp_member_list_url = "localhost" + ixlan.ixf_ixp_import_enabled = True + ixlan.save() + + ixlan.ix.request_ixf_import() + + call_command("pdb_ixf_ixp_member_import", process_requested=0, commit=True) + + ixlan.ix.refresh_from_db() + + assert ixlan.ix.ixf_import_request_status == "finished" + assert ixlan.ix.ixf_import_request + + @pytest.mark.django_db def test_reset_dismissals(entities, data_cmd_ixf_dismissals): ixf_import_data = json.loads(data_cmd_ixf_dismissals.json) @@ -153,6 +170,8 @@ def test_runtime_errors(entities, capsys, mocker): side_effect=RuntimeError("Unexpected error"), ) + out = io.StringIO() + with pytest.raises(SystemExit) as pytest_wrapped_exit: call_command( "pdb_ixf_ixp_member_import", @@ -160,14 +179,15 @@ def test_runtime_errors(entities, capsys, mocker): commit=True, ixlan=[ixlan.id], asn=asn, + stdout=out, ) # Assert we are outputting the exception and traceback to the stderr - captured = capsys.readouterr() - assert "Unexpected error" in captured.err - assert str(ixlan.id) in captured.err - assert str(ixlan.ix.name) in captured.err - assert str(ixlan.ixf_ixp_member_list_url) in captured.err + captured = out.getvalue() + assert "Unexpected error" in captured + assert str(ixlan.id) in captured + assert str(ixlan.ix.name) in captured + assert str(ixlan.ixf_ixp_member_list_url) in captured # Assert we are exiting with status code 1 assert pytest_wrapped_exit.value.code == 1 diff --git a/tests/test_entity_protection.py b/tests/test_entity_protection.py index ae16f9a0..00ea57f3 100644 --- a/tests/test_entity_protection.py +++ b/tests/test_entity_protection.py @@ -182,6 +182,39 @@ def test_tech_poc_protection(role, deletable): poc.delete() +@pytest.mark.django_db +@pytest.mark.parametrize( + "role", + [ + "Technical", + "Policy", + "NOC", + "Abuse", + "Maintenance", + "Public Relations", + "Sales", + ], +) +def test_tech_poc_hard_delete_1013(role): + """ + Test that already soft-deleted pocs dont raise + a protected action error when hard-deleting (#1013) + """ + + call_command("pdb_generate_test_data", limit=2, commit=True) + + net = Network.objects.first() + + net.poc_set.all().delete() + + poc_a = NetworkContact.objects.create(status="ok", role="Technical", network=net) + poc_b = NetworkContact.objects.create(status="deleted", role=role, network=net) + poc_b.delete(hard=True) + + poc_a.delete(force=True) + poc_a.delete(hard=True) + + @pytest.mark.django_db def test_org_protection_sponsor(db): diff --git a/tests/test_inet_parse.py b/tests/test_inet_parse.py index 7f9d8eaa..f2d4157f 100644 --- a/tests/test_inet_parse.py +++ b/tests/test_inet_parse.py @@ -1,8 +1,6 @@ import pytest import pytest_filedata -from peeringdb_server.inet import RdapAsn - def assert_parsed(data, parsed): # dump in json format for easily adding expected