diff --git a/docs/dev/commands.md b/docs/dev/commands.md index 6c1841d2..546a420b 100644 --- a/docs/dev/commands.md +++ b/docs/dev/commands.md @@ -1,4 +1,4 @@ -Generated on 2022-03-07 17:01:26.659077 +Generated on 2022-04-12 16:41:02.409639 ## _db_command.py diff --git a/docs/dev/modules.md b/docs/dev/modules.md index c351987c..e93b53b8 100644 --- a/docs/dev/modules.md +++ b/docs/dev/modules.md @@ -1,4 +1,4 @@ -Generated on 2022-03-07 17:01:26.659077 +Generated on 2022-04-12 16:41:02.409639 ## [admin.py](/docs/dev/modules/admin.py.md) @@ -75,6 +75,10 @@ Split read and write database connections if needed. DeskPro API Client used to post and retrieve support ticket information from the deskpro API. +## [exceptions.py](/docs/dev/modules/exceptions.py.md) + +# Functions + ## [export_views.py](/docs/dev/modules/export_views.py.md) Define export views used for IX-F export and advanced search file download. diff --git a/docs/dev/modules/admin.py.md b/docs/dev/modules/admin.py.md index e8677dca..6981719b 100644 --- a/docs/dev/modules/admin.py.md +++ b/docs/dev/modules/admin.py.md @@ -1,4 +1,4 @@ -Generated from admin.py on 2022-03-07 17:01:26.860132 +Generated from admin.py on 2022-04-12 16:41:02.631987 # peeringdb_server.admin @@ -245,6 +245,12 @@ These attributes / properties will be available on instances of the class ### Methods +#### \__init__ +`def __init__(self, *args, **kwargs)` + +Initialize self. See help(type(self)) for accurate signature. + +--- #### clean `def clean(self)` diff --git a/docs/dev/modules/admin_commandline_tools.py.md b/docs/dev/modules/admin_commandline_tools.py.md index 5af412aa..cfb99ae9 100644 --- a/docs/dev/modules/admin_commandline_tools.py.md +++ b/docs/dev/modules/admin_commandline_tools.py.md @@ -1,4 +1,4 @@ -Generated from admin_commandline_tools.py on 2022-03-07 17:01:26.860132 +Generated from admin_commandline_tools.py on 2022-04-12 16:41:02.631987 # peeringdb_server.admin_commandline_tools diff --git a/docs/dev/modules/api_cache.py.md b/docs/dev/modules/api_cache.py.md index bde0190f..6a456a19 100644 --- a/docs/dev/modules/api_cache.py.md +++ b/docs/dev/modules/api_cache.py.md @@ -1,4 +1,4 @@ -Generated from api_cache.py on 2022-03-07 17:01:26.860132 +Generated from api_cache.py on 2022-04-12 16:41:02.631987 # peeringdb_server.api_cache diff --git a/docs/dev/modules/api_key_views.py.md b/docs/dev/modules/api_key_views.py.md index 18633ae1..debe34eb 100644 --- a/docs/dev/modules/api_key_views.py.md +++ b/docs/dev/modules/api_key_views.py.md @@ -1,4 +1,4 @@ -Generated from api_key_views.py on 2022-03-07 17:01:26.860132 +Generated from api_key_views.py on 2022-04-12 16:41:02.631987 # peeringdb_server.api_key_views diff --git a/docs/dev/modules/api_schema.py.md b/docs/dev/modules/api_schema.py.md index 4199e6db..80bd8100 100644 --- a/docs/dev/modules/api_schema.py.md +++ b/docs/dev/modules/api_schema.py.md @@ -1,4 +1,4 @@ -Generated from api_schema.py on 2022-03-07 17:01:26.860132 +Generated from api_schema.py on 2022-04-12 16:41:02.631987 # peeringdb_server.api_schema diff --git a/docs/dev/modules/apps.py.md b/docs/dev/modules/apps.py.md index cd7f9138..ee5c0029 100644 --- a/docs/dev/modules/apps.py.md +++ b/docs/dev/modules/apps.py.md @@ -1,4 +1,4 @@ -Generated from apps.py on 2022-03-07 17:01:26.860132 +Generated from apps.py on 2022-04-12 16:41:02.631987 # peeringdb_server.apps diff --git a/docs/dev/modules/autocomplete_views.py.md b/docs/dev/modules/autocomplete_views.py.md index 078aad8d..253fcd3b 100644 --- a/docs/dev/modules/autocomplete_views.py.md +++ b/docs/dev/modules/autocomplete_views.py.md @@ -1,4 +1,4 @@ -Generated from autocomplete_views.py on 2022-03-07 17:01:26.659077 +Generated from autocomplete_views.py on 2022-04-12 16:41:02.409639 # peeringdb_server.autocomplete_views diff --git a/docs/dev/modules/context.py.md b/docs/dev/modules/context.py.md index 5804adc3..c8fcf53b 100644 --- a/docs/dev/modules/context.py.md +++ b/docs/dev/modules/context.py.md @@ -1,4 +1,4 @@ -Generated from context.py on 2022-03-07 17:01:26.860132 +Generated from context.py on 2022-04-12 16:41:02.631987 # peeringdb_server.context diff --git a/docs/dev/modules/data_views.py.md b/docs/dev/modules/data_views.py.md index ca205071..fb1f5e96 100644 --- a/docs/dev/modules/data_views.py.md +++ b/docs/dev/modules/data_views.py.md @@ -1,4 +1,4 @@ -Generated from data_views.py on 2022-03-07 17:01:26.860132 +Generated from data_views.py on 2022-04-12 16:41:02.631987 # peeringdb_server.data_views diff --git a/docs/dev/modules/db_router.py.md b/docs/dev/modules/db_router.py.md index 5c33380f..12484b25 100644 --- a/docs/dev/modules/db_router.py.md +++ b/docs/dev/modules/db_router.py.md @@ -1,4 +1,4 @@ -Generated from db_router.py on 2022-03-07 17:01:26.659077 +Generated from db_router.py on 2022-04-12 16:41:02.409639 # peeringdb_server.db_router diff --git a/docs/dev/modules/deskpro.py.md b/docs/dev/modules/deskpro.py.md index b9226b88..00f68751 100644 --- a/docs/dev/modules/deskpro.py.md +++ b/docs/dev/modules/deskpro.py.md @@ -1,4 +1,4 @@ -Generated from deskpro.py on 2022-03-07 17:01:26.860132 +Generated from deskpro.py on 2022-04-12 16:41:02.631987 # peeringdb_server.deskpro diff --git a/docs/dev/modules/exceptions.py.md b/docs/dev/modules/exceptions.py.md new file mode 100644 index 00000000..109d25d8 --- /dev/null +++ b/docs/dev/modules/exceptions.py.md @@ -0,0 +1,13 @@ +Generated from exceptions.py on 2022-04-12 16:41:02.631987 + +# peeringdb_server.exceptions + +# Functions +--- + +## format_wait_time +`def format_wait_time(wait_time)` + +Format wait time in seconds to human readable format + +--- \ No newline at end of file diff --git a/docs/dev/modules/export_views.py.md b/docs/dev/modules/export_views.py.md index 9fc71768..df182b52 100644 --- a/docs/dev/modules/export_views.py.md +++ b/docs/dev/modules/export_views.py.md @@ -1,4 +1,4 @@ -Generated from export_views.py on 2022-03-07 17:01:26.860132 +Generated from export_views.py on 2022-04-12 16:41:02.631987 # peeringdb_server.export_views diff --git a/docs/dev/modules/forms.py.md b/docs/dev/modules/forms.py.md index 61dc1d69..145f3aae 100644 --- a/docs/dev/modules/forms.py.md +++ b/docs/dev/modules/forms.py.md @@ -1,4 +1,4 @@ -Generated from forms.py on 2022-03-07 17:01:26.860132 +Generated from forms.py on 2022-04-12 16:41:02.631987 # peeringdb_server.forms diff --git a/docs/dev/modules/gendocs.py.md b/docs/dev/modules/gendocs.py.md index 6d572092..543b9f26 100644 --- a/docs/dev/modules/gendocs.py.md +++ b/docs/dev/modules/gendocs.py.md @@ -1,3 +1,3 @@ -Generated from gendocs.py on 2022-03-07 17:01:26.659077 +Generated from gendocs.py on 2022-04-12 16:41:02.409639 # peeringdb_server.gendocs diff --git a/docs/dev/modules/geo.py.md b/docs/dev/modules/geo.py.md index 68f89aa0..de90ba70 100644 --- a/docs/dev/modules/geo.py.md +++ b/docs/dev/modules/geo.py.md @@ -1,4 +1,4 @@ -Generated from geo.py on 2022-03-07 17:01:26.860132 +Generated from geo.py on 2022-04-12 16:41:02.631987 # peeringdb_server.geo @@ -38,6 +38,15 @@ Keyword arguments: - country - zipcode +--- +#### normalize_state +`def normalize_state(self, country_code, state)` + +Takes a 2-digit country code and a state name (e.g., "Wisconsin") +and returns a normalized state name (e.g., "WI") + +This will use django-cache if it exists + --- #### sanitize `def sanitize(self, **kwargs)` diff --git a/docs/dev/modules/import_views.py.md b/docs/dev/modules/import_views.py.md index 3b7b261f..1d212acd 100644 --- a/docs/dev/modules/import_views.py.md +++ b/docs/dev/modules/import_views.py.md @@ -1,4 +1,4 @@ -Generated from import_views.py on 2022-03-07 17:01:26.860132 +Generated from import_views.py on 2022-04-12 16:41:02.631987 # peeringdb_server.import_views diff --git a/docs/dev/modules/inet.py.md b/docs/dev/modules/inet.py.md index bc38c183..3910460a 100644 --- a/docs/dev/modules/inet.py.md +++ b/docs/dev/modules/inet.py.md @@ -1,4 +1,4 @@ -Generated from inet.py on 2022-03-07 17:01:26.860132 +Generated from inet.py on 2022-04-12 16:41:02.631987 # peeringdb_server.inet diff --git a/docs/dev/modules/ixf.py.md b/docs/dev/modules/ixf.py.md index e9f4e59a..bc96fc8f 100644 --- a/docs/dev/modules/ixf.py.md +++ b/docs/dev/modules/ixf.py.md @@ -1,4 +1,4 @@ -Generated from ixf.py on 2022-03-07 17:01:26.860132 +Generated from ixf.py on 2022-04-12 16:41:02.631987 # peeringdb_server.ixf diff --git a/docs/dev/modules/mail.py.md b/docs/dev/modules/mail.py.md index f3ac7b56..b0178dda 100644 --- a/docs/dev/modules/mail.py.md +++ b/docs/dev/modules/mail.py.md @@ -1,4 +1,4 @@ -Generated from mail.py on 2022-03-07 17:01:26.860132 +Generated from mail.py on 2022-04-12 16:41:02.631987 # peeringdb_server.mail diff --git a/docs/dev/modules/maintenance.py.md b/docs/dev/modules/maintenance.py.md index e33424be..f2e2b9da 100644 --- a/docs/dev/modules/maintenance.py.md +++ b/docs/dev/modules/maintenance.py.md @@ -1,4 +1,4 @@ -Generated from maintenance.py on 2022-03-07 17:01:26.860132 +Generated from maintenance.py on 2022-04-12 16:41:02.631987 # peeringdb_server.maintenance diff --git a/docs/dev/modules/middleware.py.md b/docs/dev/modules/middleware.py.md index dbada909..c6c38711 100644 --- a/docs/dev/modules/middleware.py.md +++ b/docs/dev/modules/middleware.py.md @@ -1,4 +1,4 @@ -Generated from middleware.py on 2022-03-07 17:01:26.860132 +Generated from middleware.py on 2022-04-12 16:41:02.631987 # peeringdb_server.middleware @@ -33,6 +33,17 @@ Initialize self. See help(type(self)) for accurate signature. --- +## HttpResponseUnauthorized + +``` +HttpResponseUnauthorized(django.http.response.HttpResponse) +``` + +An HTTP response class with a string as content. + +This content can be read, appended to, or replaced. + + ## PDBCommonMiddleware ``` @@ -65,3 +76,28 @@ Check for denied User-Agents and rewrite the URL based on settings.APPEND_SLASH and settings.PREPEND_WWW --- + +## PDBPermissionMiddleware + +``` +PDBPermissionMiddleware(django.utils.deprecation.MiddlewareMixin) +``` + +Middleware that checks if the current user has the correct permissions +to access the requested resource. + + +### Methods + +#### get_username_and_password +`def get_username_and_password(self, http_auth)` + +Get the username and password from the HTTP auth header. + +--- +#### response_unauthorized +`def response_unauthorized(self, request, status=None, message=None)` + +Return a Unauthorized response. + +--- diff --git a/docs/dev/modules/mock.py.md b/docs/dev/modules/mock.py.md index b56ba31b..d9e358a4 100644 --- a/docs/dev/modules/mock.py.md +++ b/docs/dev/modules/mock.py.md @@ -1,4 +1,4 @@ -Generated from mock.py on 2022-03-07 17:01:26.860132 +Generated from mock.py on 2022-04-12 16:41:02.631987 # peeringdb_server.mock diff --git a/docs/dev/modules/models.py.md b/docs/dev/modules/models.py.md index 47a22e7b..7db1a6fe 100644 --- a/docs/dev/modules/models.py.md +++ b/docs/dev/modules/models.py.md @@ -1,4 +1,4 @@ -Generated from models.py on 2022-03-07 17:01:26.860132 +Generated from models.py on 2022-04-12 16:41:02.631987 # peeringdb_server.models @@ -147,6 +147,21 @@ the default value will be returned. ### Methods +#### \__str__ +`def __str__(self)` + +Return str(self). + +--- +#### clean +`def clean(self)` + +Hook for doing any extra model-wide validation after clean() has been +called on every field by self.clean_fields. Any ValidationError raised +by this method will not be associated with a particular field; it will +have a special-case association with the field defined by NON_FIELD_ERRORS. + +--- #### set_value `def set_value(self, value)` diff --git a/docs/dev/modules/org_admin_views.py.md b/docs/dev/modules/org_admin_views.py.md index 211255d4..77beaa0d 100644 --- a/docs/dev/modules/org_admin_views.py.md +++ b/docs/dev/modules/org_admin_views.py.md @@ -1,4 +1,4 @@ -Generated from org_admin_views.py on 2022-03-07 17:01:26.860132 +Generated from org_admin_views.py on 2022-04-12 16:41:02.631987 # peeringdb_server.org_admin_views diff --git a/docs/dev/modules/permissions.py.md b/docs/dev/modules/permissions.py.md index f20d92dd..549424c5 100644 --- a/docs/dev/modules/permissions.py.md +++ b/docs/dev/modules/permissions.py.md @@ -1,4 +1,4 @@ -Generated from permissions.py on 2022-03-07 17:01:26.860132 +Generated from permissions.py on 2022-04-12 16:41:02.631987 # peeringdb_server.permissions diff --git a/docs/dev/modules/renderers.py.md b/docs/dev/modules/renderers.py.md index 6700cf63..edb09a06 100644 --- a/docs/dev/modules/renderers.py.md +++ b/docs/dev/modules/renderers.py.md @@ -1,4 +1,4 @@ -Generated from renderers.py on 2022-03-07 17:01:26.860132 +Generated from renderers.py on 2022-04-12 16:41:02.631987 # peeringdb_server.renderers diff --git a/docs/dev/modules/request.py.md b/docs/dev/modules/request.py.md index d9a877ac..e4b1cd39 100644 --- a/docs/dev/modules/request.py.md +++ b/docs/dev/modules/request.py.md @@ -1,4 +1,4 @@ -Generated from request.py on 2022-03-07 17:01:26.860132 +Generated from request.py on 2022-04-12 16:41:02.631987 # peeringdb_server.request diff --git a/docs/dev/modules/rest.py.md b/docs/dev/modules/rest.py.md index 080645a3..cd47bc1a 100644 --- a/docs/dev/modules/rest.py.md +++ b/docs/dev/modules/rest.py.md @@ -1,4 +1,4 @@ -Generated from rest.py on 2022-03-07 17:01:26.860132 +Generated from rest.py on 2022-04-12 16:41:02.631987 # peeringdb_server.rest diff --git a/docs/dev/modules/rest_throttles.py.md b/docs/dev/modules/rest_throttles.py.md index 52afca92..d0ed1dfc 100644 --- a/docs/dev/modules/rest_throttles.py.md +++ b/docs/dev/modules/rest_throttles.py.md @@ -1,4 +1,4 @@ -Generated from rest_throttles.py on 2022-03-07 17:01:26.860132 +Generated from rest_throttles.py on 2022-04-12 16:41:02.631987 # peeringdb_server.rest_throttles @@ -10,45 +10,21 @@ Custom rate limit handlers for the REST API. ## APIAnonUserThrottle ``` -APIAnonUserThrottle(rest_framework.throttling.AnonRateThrottle) +APIAnonUserThrottle(peeringdb_server.rest_throttles.TargetedRateThrottle) ``` -Rate limiting for anonymous users. +General rate limiting for anonymous users via the request ip-address -### Methods - -#### allow_request -`def allow_request(self, request, view)` - -Implement the check to see if the request should be throttled. - -On success calls `throttle_success`. -On failure calls `throttle_failure`. - ---- - ## APIUserThrottle ``` -APIUserThrottle(rest_framework.throttling.UserRateThrottle) +APIUserThrottle(peeringdb_server.rest_throttles.TargetedRateThrottle) ``` -Rate limiting for authenticated users. +General rate limiting for authenticated requests (users or orgs) -### Methods - -#### allow_request -`def allow_request(self, request, view)` - -Implement the check to see if the request should be throttled. - -On success calls `throttle_success`. -On failure calls `throttle_failure`. - ---- - ## FilterDistanceThrottle ``` @@ -120,3 +96,97 @@ Must be overridden. May return `None` if the request should not be throttled. --- + +## MelissaThrottle + +``` +MelissaThrottle(peeringdb_server.rest_throttles.TargetedRateThrottle) +``` + +Rate limits requests that do a melissa lookup (#1124) + + +## ResponseSizeThrottle + +``` +ResponseSizeThrottle(peeringdb_server.rest_throttles.TargetedRateThrottle) +``` + +Rate limit repeated requests based request content-size + +See #1126 for rationale + + +### Class Methods + +#### cache_response_size +`def cache_response_size(cls, request, size)` + +Caches the response size for the request + +The api renderer (renderers.py) calls this automatically +when it renders the response + +--- +#### expected_response_size +`def expected_response_size(cls, request)` + +Returns the expected response size (number of bytes) for the request as `int` + +It will return None if there is no cached response size for the request. + +--- +#### size_cache_key +`def size_cache_key(cls, request)` + +Returns the cache key to use for storing response size cache + +--- + +## TargetedRateThrottle + +``` +TargetedRateThrottle(rest_framework.throttling.SimpleRateThrottle) +``` + +Base class for targeted rate throttling depending +on authentication status + +Rate throttle by + - user (sess-auth, basic-auth, key), + - org (key), + - anonymous (inet, cdir) + + +### Methods + +#### \__init__ +`def __init__(self)` + +Initialize self. See help(type(self)) for accurate signature. + +--- +#### allow_request +`def allow_request(self, request, view)` + +Implement the check to see if the request should be throttled. + +On success calls `throttle_success`. +On failure calls `throttle_failure`. + +--- +#### get_cache_key +`def get_cache_key(self, request, view)` + +Should return a unique cache-key which can be used for throttling. +Must be overridden. + +May return `None` if the request should not be throttled. + +--- +#### get_rate +`def get_rate(self)` + +Determine the string representation of the allowed request rate. + +--- diff --git a/docs/dev/modules/search.py.md b/docs/dev/modules/search.py.md index 5d15ba48..c1c047a6 100644 --- a/docs/dev/modules/search.py.md +++ b/docs/dev/modules/search.py.md @@ -1,4 +1,4 @@ -Generated from search.py on 2022-03-07 17:01:26.860132 +Generated from search.py on 2022-04-12 16:41:02.631987 # peeringdb_server.search diff --git a/docs/dev/modules/search_indexes.py.md b/docs/dev/modules/search_indexes.py.md index 15b6c3f4..44047ca7 100644 --- a/docs/dev/modules/search_indexes.py.md +++ b/docs/dev/modules/search_indexes.py.md @@ -1,4 +1,4 @@ -Generated from search_indexes.py on 2022-03-07 17:01:26.860132 +Generated from search_indexes.py on 2022-04-12 16:41:02.631987 # peeringdb_server.search_indexes diff --git a/docs/dev/modules/serializers.py.md b/docs/dev/modules/serializers.py.md index 9c66b9eb..f07e80b4 100644 --- a/docs/dev/modules/serializers.py.md +++ b/docs/dev/modules/serializers.py.md @@ -1,4 +1,4 @@ -Generated from serializers.py on 2022-03-07 17:01:26.860132 +Generated from serializers.py on 2022-04-12 16:41:02.631987 # peeringdb_server.serializers @@ -165,6 +165,20 @@ and resave the model instance with normalized address fields. Can only be used if the model includes the GeocodeBaseMixin. +### Class Methods + +#### normalize_state_lookup +`def normalize_state_lookup(cls, filters)` + +for non-distance search the specifies state and country +attempt to normalize the state field using melissa global address +lookup. (#1079) + +this does NOT need to be done on distance search since distance search +already normalizes the search to geo-coordinates using melissa. + +--- + ### Methods #### _add_meta_information diff --git a/docs/dev/modules/settings.py.md b/docs/dev/modules/settings.py.md index 68fd8766..eee9fde1 100644 --- a/docs/dev/modules/settings.py.md +++ b/docs/dev/modules/settings.py.md @@ -1,4 +1,4 @@ -Generated from settings.py on 2022-03-07 17:01:26.860132 +Generated from settings.py on 2022-04-12 16:41:02.631987 # peeringdb_server.settings diff --git a/docs/dev/modules/signals.py.md b/docs/dev/modules/signals.py.md index 8f8c5f9d..74c28ca3 100644 --- a/docs/dev/modules/signals.py.md +++ b/docs/dev/modules/signals.py.md @@ -1,4 +1,4 @@ -Generated from signals.py on 2022-03-07 17:01:26.860132 +Generated from signals.py on 2022-04-12 16:41:02.631987 # peeringdb_server.signals diff --git a/docs/dev/modules/stats.py.md b/docs/dev/modules/stats.py.md index ade9345c..ab2ce1d5 100644 --- a/docs/dev/modules/stats.py.md +++ b/docs/dev/modules/stats.py.md @@ -1,4 +1,4 @@ -Generated from stats.py on 2022-03-07 17:01:26.860132 +Generated from stats.py on 2022-04-12 16:41:02.631987 # peeringdb_server.stats diff --git a/docs/dev/modules/urls.py.md b/docs/dev/modules/urls.py.md index c3f78e22..6a549a2b 100644 --- a/docs/dev/modules/urls.py.md +++ b/docs/dev/modules/urls.py.md @@ -1,4 +1,4 @@ -Generated from urls.py on 2022-03-07 17:01:26.860132 +Generated from urls.py on 2022-04-12 16:41:02.631987 # peeringdb_server.urls diff --git a/docs/dev/modules/util.py.md b/docs/dev/modules/util.py.md index 239c169b..aa3a4938 100644 --- a/docs/dev/modules/util.py.md +++ b/docs/dev/modules/util.py.md @@ -1,4 +1,4 @@ -Generated from util.py on 2022-03-07 17:01:26.860132 +Generated from util.py on 2022-04-12 16:41:02.631987 # peeringdb_server.util diff --git a/docs/dev/modules/validators.py.md b/docs/dev/modules/validators.py.md index c5feb856..cbcf0ba1 100644 --- a/docs/dev/modules/validators.py.md +++ b/docs/dev/modules/validators.py.md @@ -1,4 +1,4 @@ -Generated from validators.py on 2022-03-07 17:01:26.860132 +Generated from validators.py on 2022-04-12 16:41:02.631987 # peeringdb_server.validators @@ -18,6 +18,43 @@ Arguments: Raises: - ValidationError on failed validation +--- +## validate_api_rate +`def validate_api_rate(value)` + +Validates a number/time-unit format used to determine rate limits + +e.g., 10/second or 100/minute + +Will raise a ValidationError on failure + +Arguments: + +- value(`str`) + +Returns: + +- validated value (`str`) + +--- +## validate_bool +`def validate_bool(value)` + +Validates a boolean value + +This can be passed a string for `True` or `False` or an integer as 1, 0 as well +to convert and return a boolean value + +Will raise ValidationError on failure. + +Arguments: + +- value (`str`|`int`|`bool`) + +Returns: + +- validated value (`bool`) + --- ## validate_irr_as_set `def validate_irr_as_set(value)` diff --git a/docs/dev/modules/views.py.md b/docs/dev/modules/views.py.md index cde1a80b..d9f0c1cd 100644 --- a/docs/dev/modules/views.py.md +++ b/docs/dev/modules/views.py.md @@ -1,4 +1,4 @@ -Generated from views.py on 2022-03-07 17:01:26.860132 +Generated from views.py on 2022-04-12 16:41:02.631987 # peeringdb_server.views diff --git a/docs/img/schema.png b/docs/img/schema.png index 2dec25b0..41b45a06 100644 Binary files a/docs/img/schema.png and b/docs/img/schema.png differ diff --git a/mainsite/settings/__init__.py b/mainsite/settings/__init__.py index ec445d7b..e4fe5123 100644 --- a/mainsite/settings/__init__.py +++ b/mainsite/settings/__init__.py @@ -1,8 +1,10 @@ # Django settings import os +import sys import django.conf.global_settings +import structlog from mainsite.oauth2.scopes import SupportedScopes @@ -289,6 +291,7 @@ set_from_env( "OIDC_RSA_PRIVATE_KEY_ACTIVE_PATH", os.path.join(API_CACHE_ROOT, "keys", "oidc.key") ) + # Limits API_THROTTLE_ENABLED = True @@ -297,6 +300,55 @@ 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") +# Configuration for melissa request rate limiting in the api (#1124) + +set_option("API_THROTTLE_MELISSA_ENABLED_USER", False) +set_option("API_THROTTLE_MELISSA_RATE_USER", "10/minute") + +set_option("API_THROTTLE_MELISSA_ENABLED_ORG", False) +set_option("API_THROTTLE_MELISSA_RATE_ORG", "10/minute") + +set_option("API_THROTTLE_MELISSA_ENABLED_IP", False) +set_option("API_THROTTLE_MELISSA_RATE_IP", "1/minute") + +# Configuration for response-size rate limiting in the api (#1126) + + +# Anonymous (ip-address) - Size threshold (bytes, default = 1MB) +set_option("API_THROTTLE_RESPONSE_SIZE_THRESHOLD_IP", 1000000) +# Anonymous (ip-address) - Rate limit +set_option("API_THROTTLE_RESPONSE_SIZE_RATE_IP", "10/minute") +# Anonymous (ip-address) - On/Off toggle +set_option("API_THROTTLE_RESPONSE_SIZE_ENABLED_IP", False) + + +# Anonymous (cidr ipv4/24, ipv6/64) - Size threshold (bytes, default = 1MB) +set_option("API_THROTTLE_RESPONSE_SIZE_THRESHOLD_CIDR", 1000000) +# Anonymous (cidr ipv4/24, ipv6/64) - Rate limit +set_option("API_THROTTLE_RESPONSE_SIZE_RATE_CIDR", "10/minute") +# Anonymous (cidr ipv4/24, ipv6/64) - On/Off toggle +set_option("API_THROTTLE_RESPONSE_SIZE_ENABLED_CIDR", False) + + +# User - Size threshold (bytes, default = 1MB) +set_option("API_THROTTLE_RESPONSE_SIZE_THRESHOLD_USER", 1000000) +# User - Rate limit +set_option("API_THROTTLE_RESPONSE_SIZE_RATE_USER", "10/minute") +# User - On/Off toggle +set_option("API_THROTTLE_RESPONSE_SIZE_ENABLED_USER", False) + + +# Organization- Size threshold (bytes, default = 1MB) +set_option("API_THROTTLE_RESPONSE_SIZE_THRESHOLD_ORG", 1000000) +# Organization - Rate limit +set_option("API_THROTTLE_RESPONSE_SIZE_RATE_ORG", "10/minute") +# Organization - On/Off toggle +set_option("API_THROTTLE_RESPONSE_SIZE_ENABLED_ORG", False) + +# Expected response sizes are cached for n seconds (default = 31 days) +set_option("API_THROTTLE_RESPONSE_SIZE_CACHE_EXPIRY", 86400 * 31) + + # spatial queries require user auth set_option("API_DISTANCE_FILTER_REQUIRE_AUTH", True) @@ -415,15 +467,57 @@ DATABASES = { }, } +# Set file logging path +set_option("LOGFILE_PATH", os.path.join(BASE_DIR, "var/log/django.log")) + +if DEBUG: + set_option("DJANGO_LOG_LEVEL", "INFO") +else: + set_option("DJANGO_LOG_LEVEL", "ERROR") + +structlog.configure( + processors=[ + structlog.stdlib.filter_by_level, + structlog.processors.TimeStamper(fmt="iso"), + structlog.stdlib.add_logger_name, + structlog.stdlib.add_log_level, + structlog.stdlib.PositionalArgumentsFormatter(), + structlog.processors.StackInfoRenderer(), + structlog.processors.format_exc_info, + structlog.processors.UnicodeDecoder(), + structlog.stdlib.ProcessorFormatter.wrap_for_formatter, + ], + context_class=structlog.threadlocal.wrap_dict(dict), + logger_factory=structlog.stdlib.LoggerFactory(), + wrapper_class=structlog.stdlib.BoundLogger, + cache_logger_on_first_use=True, +) LOGGING = { "version": 1, "disable_existing_loggers": False, + "formatters": { + "json": { + "()": structlog.stdlib.ProcessorFormatter, + "processor": structlog.processors.JSONRenderer(), + }, + "color_console": { + "()": structlog.stdlib.ProcessorFormatter, + "processor": structlog.dev.ConsoleRenderer(), + }, + "key_value": { + "()": structlog.stdlib.ProcessorFormatter, + "processor": structlog.processors.KeyValueRenderer( + key_order=["timestamp", "level", "event", "logger"] + ), + }, + }, "handlers": { # Include the default Django email handler for errors # This is what you'd get without configuring logging at all. "mail_admins": { "class": "django.utils.log.AdminEmailHandler", + # only send emails for error logs "level": "ERROR", # But the emails are plain text by default - HTML is nicer "include_html": True, @@ -431,36 +525,47 @@ LOGGING = { # Log to a text file that can be rotated by logrotate "logfile": { "class": "logging.handlers.WatchedFileHandler", - "filename": os.path.join(BASE_DIR, "var/log/django.log"), + "filename": LOGFILE_PATH, + "formatter": "key_value", }, "console": { - "level": "DEBUG", "class": "logging.StreamHandler", + "formatter": "color_console", + "stream": sys.stdout, + }, + "console_json": { + "class": "logging.StreamHandler", + "formatter": "json", + "stream": sys.stdout, + }, + "console_debug": { + "class": "logging.StreamHandler", + "formatter": "color_console", + "stream": sys.stdout, + "level": "DEBUG", }, }, "loggers": { - # Again, default Django configuration to email unhandled exceptions - "django.request": { - "handlers": ["mail_admins"], - "level": "ERROR", + # Django log + "django": { + "handlers": ["mail_admins", "logfile", "console_debug"], + "level": DJANGO_LOG_LEVEL, "propagate": True, }, - # Might as well log any errors anywhere else in Django - "django": { - # 'handlers': ['console', 'logfile'], - # 'level': 'DEBUG', + # geo normalization / geo-coding + "peeringdb_server.geo": { "handlers": ["logfile"], - "level": "ERROR", + "level": "INFO", "propagate": False, }, - # Your own app - this assumes all your logger names start with "myapp." - "": { + # django-structlog specific + "django_structlog": { "handlers": ["logfile"], - "level": "WARNING", # Or maybe INFO or DEBUG - "propagate": False, + "level": "DEBUG", }, }, } + INSTALLED_APPS = [ "django.contrib.auth", "django.contrib.contenttypes", @@ -654,6 +759,7 @@ MIDDLEWARE += ( "peeringdb_server.middleware.PDBCommonMiddleware", "peeringdb_server.middleware.PDBPermissionMiddleware", "oauth2_provider.middleware.OAuth2TokenMiddleware", + "django_structlog.middlewares.RequestMiddleware", ) OAUTH2_PROVIDER = { @@ -696,6 +802,7 @@ REST_FRAMEWORK = { ], "DEFAULT_RENDERER_CLASSES": ("peeringdb_server.renderers.MetaJSONRenderer",), "DEFAULT_SCHEMA_CLASS": "peeringdb_server.api_schema.BaseSchema", + "EXCEPTION_HANDLER": "peeringdb_server.exceptions.rest_exception_handler", } if API_THROTTLE_ENABLED: @@ -704,13 +811,22 @@ if API_THROTTLE_ENABLED: "DEFAULT_THROTTLE_CLASSES": ( "peeringdb_server.rest_throttles.APIAnonUserThrottle", "peeringdb_server.rest_throttles.APIUserThrottle", + "peeringdb_server.rest_throttles.ResponseSizeThrottle", "peeringdb_server.rest_throttles.FilterDistanceThrottle", + "peeringdb_server.rest_throttles.MelissaThrottle", ), "DEFAULT_THROTTLE_RATES": { "anon": API_THROTTLE_RATE_ANON, "user": API_THROTTLE_RATE_USER, "filter_distance": API_THROTTLE_RATE_FILTER_DISTANCE, "ixf_import_request": API_THROTTLE_IXF_IMPORT, + "response_size_ip": API_THROTTLE_RESPONSE_SIZE_RATE_IP, + "response_size_cidr": API_THROTTLE_RESPONSE_SIZE_RATE_CIDR, + "response_size_user": API_THROTTLE_RESPONSE_SIZE_RATE_USER, + "response_size_org": API_THROTTLE_RESPONSE_SIZE_RATE_ORG, + "melissa_user": API_THROTTLE_MELISSA_RATE_USER, + "melissa_org": API_THROTTLE_MELISSA_RATE_ORG, + "melissa_ip": API_THROTTLE_MELISSA_RATE_IP, }, } ) @@ -979,11 +1095,14 @@ else: TEMPLATES[0]["OPTIONS"]["debug"] = DEBUG +# set custom throttling message +set_option( + "API_THROTTLE_RATE_ANON_MSG", "Request was throttled. Expected available in {time}." +) +set_option( + "API_THROTTLE_RATE_USER_MSG", "Request was throttled. Expected available in {time}." +) -if DEBUG: - # make all loggers use the console. - for logger in LOGGING["loggers"]: - LOGGING["loggers"][logger]["handlers"] = ["console"] if TUTORIAL_MODE: EMAIL_SUBJECT_PREFIX = "[PDB TUTORIAL] " diff --git a/peeringdb_server/admin.py b/peeringdb_server/admin.py index adb44fdf..c3a2732d 100644 --- a/peeringdb_server/admin.py +++ b/peeringdb_server/admin.py @@ -1257,6 +1257,22 @@ class NetworkAdminForm(StatusForm): super().__init__(*args, **kwargs) fk_handleref_filter(self, "org") + def clean_asn(self): + asn = self.cleaned_data["asn"] + if Network.objects.filter(asn=asn).exclude(id=self.instance.id).exists(): + # Clear ASN field from form + self.cleaned_data["asn"] = None + raise ValidationError(_("ASN is already in use by another network")) + return asn + + def clean_name(self): + name = self.cleaned_data["name"] + if Network.objects.filter(name=name).exclude(id=self.instance.id).exists(): + # Clear name field from form + self.cleaned_data["name"] = None + raise ValidationError(_("Name is already in use by another network")) + return name + class NetworkAdmin(ModelAdminWithVQCtrl, SoftDeleteAdmin): list_display = ("name", "asn", "aka", "name_long", "status", "created", "updated") @@ -1546,6 +1562,9 @@ class UserCreationForm(forms.UserCreationForm): def clean_username(self): username = self.cleaned_data["username"] + if username.startswith("apikey"): + raise forms.ValidationError(_('Usernames cannot start with "apikey"')) + try: User._default_manager.get(username=username) except User.DoesNotExist: @@ -2185,22 +2204,18 @@ class EnvironmentSettingForm(baseForms.ModelForm): class Meta: fields = ["setting", "value"] + def __init__(self, *args, **kwargs): + envsetting = kwargs.get("instance") + if envsetting: + kwargs["initial"] = {"value": envsetting.value} + return super().__init__(*args, **kwargs) + def clean(self): cleaned_data = super().clean() setting = cleaned_data.get("setting") value = cleaned_data.get("value") - if setting in ["API_THROTTLE_RATE_ANON", "API_THROTTLE_RATE_USER"]: - if re.match( - r"([/\d]+)\s*(?:minute|hour|seconds|day|week|month|year)", value - ): - return cleaned_data - else: - raise ValidationError( - _( - "Invalid setting! Acceptable value is a number followed by one of the following: minute, hour, seconds, day, week, month, year. eg (10/minute, 1/hour, 5/day, 1/week, 1/month, 1/year)" - ) - ) + cleaned_data["value"] = EnvironmentSetting.validate_value(setting, value) return cleaned_data diff --git a/peeringdb_server/admin_commandline_tools.py b/peeringdb_server/admin_commandline_tools.py index 4d6cff11..68d4df73 100644 --- a/peeringdb_server/admin_commandline_tools.py +++ b/peeringdb_server/admin_commandline_tools.py @@ -126,7 +126,7 @@ class CommandLineToolWrapper: def validate(self): pass - def _run(self, command, commit=False): + def _run(self, command, commit=False, user=None): r = io.StringIO() if self.maintenance and commit: @@ -150,10 +150,25 @@ class CommandLineToolWrapper: maintenance.off() if commit: - command.description = self.description - command.status = "done" - command.result = self.result - command.save() + if self.queue: + # if command was processed through the queue, update the existing + # command instance + command.description = self.description + command.status = "done" + command.result = self.result + command.save() + else: + # if command was processed in line with the http request it still + # needs to be persisted to the database + CommandLineTool.objects.create( + user=user, + tool=self.tool, + description=self.description, + status="done", + arguments=json.dumps({"args": self.args, "kwargs": self.kwargs}), + result=self.result, + ) + return self.result @transaction.atomic @@ -193,7 +208,7 @@ class CommandLineToolWrapper: return self.result else: with reversion.create_revision(): - return self._run(user, commit=commit) + return self._run(None, commit=commit, user=user) def download_link(self): return None diff --git a/peeringdb_server/exceptions.py b/peeringdb_server/exceptions.py new file mode 100644 index 00000000..5c83ba46 --- /dev/null +++ b/peeringdb_server/exceptions.py @@ -0,0 +1,34 @@ +from rest_framework.exceptions import Throttled +from rest_framework.views import exception_handler + + +def format_wait_time(wait_time): + """ + Format wait time in seconds to human readable format + """ + if wait_time < 60: + return f"{wait_time} seconds" + elif wait_time < 3600 and wait_time > 60: + return f"{wait_time // 60} minutes" + else: + return f"{wait_time // 3600} hours" + + +def rest_exception_handler(exc, context): + + response = exception_handler(exc, context) + request = context.get("request") + + if isinstance(exc, Throttled): + message = getattr( + request, + "throttle_response_message", + "Request was throttled. Expected available in {time}.", + ) + custom_response_data = { + "message": f"{message}".replace("{time}", format_wait_time(exc.wait)), + } + + response.data = custom_response_data + + return response diff --git a/peeringdb_server/geo.py b/peeringdb_server/geo.py index 62da01b5..75c8755d 100644 --- a/peeringdb_server/geo.py +++ b/peeringdb_server/geo.py @@ -4,7 +4,13 @@ Utilities for geocoding and geo normalization. import googlemaps import requests -from django.utils.translation import ugettext_lazy as _ +import structlog +from django.core.cache import cache +from django.utils.translation import gettext_lazy as _ + +from peeringdb_server.context import current_request + +logger = structlog.getLogger(__name__) class Timeout(IOError): @@ -110,6 +116,14 @@ class Melissa: self.key = key self.timeout = timeout + def log_request(self, url, **kwargs): + with current_request() as request: + if request: + source_url = request.build_absolute_uri()[:255] + logger.info("MELISSA", url=url, source=source_url) + else: + logger.info("MELISSA", url=url) + def sanitize(self, **kwargs): """ @@ -206,6 +220,9 @@ class Melissa: } try: + + self.log_request(self.global_address_url, **params) + response = requests.get( self.global_address_url, params=params, @@ -242,3 +259,24 @@ class Melissa: except (KeyError, IndexError): return None + + def normalize_state(self, country_code, state): + """ + Takes a 2-digit country code and a state name (e.g., "Wisconsin") + and returns a normalized state name (e.g., "WI") + + This will use django-cache if it exists + """ + + key = f"geo.normalize.state.{country_code}.{state}" + + value = cache.get(key) + if value is None: + result = self.global_address(country=country_code, address1=state) + try: + record = result["Records"][0] + value = record.get("AdministrativeArea") or state + except (KeyError, IndexError): + value = state + cache.set(key, value) + return value diff --git a/peeringdb_server/management/commands/pdb_fac_merge_undo.py b/peeringdb_server/management/commands/pdb_fac_merge_undo.py index 3581ec2e..5dbb663f 100644 --- a/peeringdb_server/management/commands/pdb_fac_merge_undo.py +++ b/peeringdb_server/management/commands/pdb_fac_merge_undo.py @@ -54,11 +54,11 @@ class Command(BaseCommand): return regex_facilities = r"Merging facilities (.+) -> (\d+)" - regex_netfac = r" - netfac NetworkFacility-netfac(\d+)$" - regex_ixfac = r" - ixfac InternetExchangeFacility-ixfac(\d+)$" + regex_netfac = r" - netfac NetworkFacility object \((\d+)\)$" + regex_ixfac = r" - ixfac InternetExchangeFacility object \((\d+)\)$" regex_source = r"Merging (.+) \((\d+)\) .." - regex_delete_netfac = r"soft deleting NetworkFacility-netfac(\d+)" - regex_delete_ixfac = r"soft deleting InternetExchangeFacility-ixfac(\d+)" + regex_delete_netfac = r"soft deleting NetworkFacility object \((\d+)\)" + regex_delete_ixfac = r"soft deleting InternetExchangeFacility object \((\d+)\)" sources = {} source = None diff --git a/peeringdb_server/management/commands/pdb_geo_normalize_existing.py b/peeringdb_server/management/commands/pdb_geo_normalize_existing.py index 55ea21d5..746aed1d 100644 --- a/peeringdb_server/management/commands/pdb_geo_normalize_existing.py +++ b/peeringdb_server/management/commands/pdb_geo_normalize_existing.py @@ -13,6 +13,7 @@ from django.core.management.base import BaseCommand from django.db import transaction from peeringdb_server import models +from peeringdb_server.geo import Melissa from peeringdb_server.serializers import AddressSerializer API_KEY = settings.MELISSA_KEY @@ -49,6 +50,11 @@ class Command(BaseCommand): action="store_true", help="Only parse the floor and suite", ) + parser.add_argument( + "--state-only", + action="store_true", + help="Only normalize state/province information", + ) parser.add_argument( "--csv", nargs="?", @@ -61,6 +67,10 @@ class Command(BaseCommand): ) def log(self, msg): + + if self.state_only: + msg = f"[state-only] {msg}" + if not self.commit: self.stdout.write(f"[pretend] {msg}") else: @@ -69,9 +79,12 @@ class Command(BaseCommand): def handle(self, *args, **options): self.commit = options.get("commit", False) self.floor_and_ste_parse_only = options.get("floor_and_suite_only", False) + self.state_only = options.get("state_only", False) self.pprint = options.get("pprint", False) self.csv_file = options.get("csv") + self.melissa = Melissa(API_KEY) + reftag = options.get("reftag") limit = options.get("limit") @@ -188,7 +201,10 @@ class Command(BaseCommand): ) try: - self._normalize(entity, output_dict, self.commit) + if self.state_only: + self._normalize_state(entity, output_dict, self.commit) + else: + self._normalize(entity, output_dict, self.commit) except ValidationError as exc: self.log(str(exc)) @@ -230,3 +246,21 @@ class Command(BaseCommand): instance.save() self.snapshot_model(instance, "_after", output_dict) + + def _normalize_state(self, instance, output_dict, save): + + if not instance.state: + self.snapshot_model(instance, "_after", output_dict) + return + + normalized_state = self.melissa.normalize_state( + f"{instance.country}", instance.state + ) + + if normalized_state != instance.state and normalized_state: + instance.state = normalized_state + + if save: + instance.save() + + self.snapshot_model(instance, "_after", output_dict) diff --git a/peeringdb_server/middleware.py b/peeringdb_server/middleware.py index 192bfa50..451695f7 100644 --- a/peeringdb_server/middleware.py +++ b/peeringdb_server/middleware.py @@ -70,6 +70,8 @@ class PDBPermissionMiddleware(MiddlewareMixin): to access the requested resource. """ + auth_id = None + def get_username_and_password(self, http_auth): """ Get the username and password from the HTTP auth header. @@ -109,7 +111,7 @@ class PDBPermissionMiddleware(MiddlewareMixin): user = authenticate(username=username, password=password) # if user is not authenticated return 401 Unauthorized if not user: - + self.auth_id = username return self.response_unauthorized( request, message="Invalid username or password", status=401 ) @@ -130,14 +132,16 @@ class PDBPermissionMiddleware(MiddlewareMixin): # If api key is not valid return 401 Unauthorized if not api_key: - + self.auth_id = "apikey_%s" % (req_key) + if len(req_key) > 16: + self.auth_id = self.auth_id[:16] return self.response_unauthorized( request, message="Invalid API key", status=401 ) # If API key is provided, check if the user has an active session if api_key: - + self.auth_id = "apikey_%s" % req_key if request.session.get("_auth_user_id") and request.user.id: if int(request.user.id) == int( request.session.get("_auth_user_id") @@ -148,3 +152,16 @@ class PDBPermissionMiddleware(MiddlewareMixin): message="Cannot authenticate through Authorization header while logged in. Please log out and try again.", status=400, ) + + def process_response(self, request, response): + + if self.auth_id: + # Sanitizes the auth_id + self.auth_id = self.auth_id.replace(" ", "_") + # If auth_id ends with a 401 make sure is it limited to 16 bytes + if response.status_code == 401 and len(self.auth_id) > 16: + if not self.auth_id.startswith("apikey_"): + self.auth_id = self.auth_id[:16] + + response["X-Auth-ID"] = self.auth_id + return response diff --git a/peeringdb_server/migrations/0082_api_throttle_msg.py b/peeringdb_server/migrations/0082_api_throttle_msg.py new file mode 100644 index 00000000..c00d4e20 --- /dev/null +++ b/peeringdb_server/migrations/0082_api_throttle_msg.py @@ -0,0 +1,38 @@ +# Generated by Django 3.2.12 on 2022-03-23 05:43 + +import django.core.validators +import django_inet.models +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ("peeringdb_server", "0081_status_dashboard"), + ] + + operations = [ + migrations.AlterField( + model_name="environmentsetting", + name="setting", + field=models.CharField( + choices=[ + ("API_THROTTLE_RATE_ANON", "API: Anonymous user API throttle rate"), + ( + "API_THROTTLE_RATE_USER", + "API: Authenticated user API throttle rate", + ), + ( + "API_THROTTLE_RATE_ANON_MSG", + "API: Anonymous user API throttle rate message", + ), + ( + "API_THROTTLE_RATE_USER_MSG", + "API: User API throttle rate message", + ), + ], + max_length=255, + unique=True, + ), + ), + ] diff --git a/peeringdb_server/migrations/0083_auto_20220412_1554.py b/peeringdb_server/migrations/0083_auto_20220412_1554.py new file mode 100644 index 00000000..27441252 --- /dev/null +++ b/peeringdb_server/migrations/0083_auto_20220412_1554.py @@ -0,0 +1,107 @@ +# Generated by Django 3.2.13 on 2022-04-12 15:54 + +import django.core.validators +import django_inet.models +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ("peeringdb_server", "0082_api_throttle_msg"), + ] + + operations = [ + migrations.AlterField( + model_name="environmentsetting", + name="setting", + field=models.CharField( + choices=[ + ("API_THROTTLE_RATE_ANON", "API: Anonymous API throttle rate"), + ("API_THROTTLE_RATE_USER", "API: Authenticated API throttle rate"), + ( + "API_THROTTLE_MELISSA_RATE_USER", + "API: Melissa request throttle rate for users", + ), + ( + "API_THROTTLE_MELISSA_ENABLED_USER", + "API: Melissa request throttle enabled for users", + ), + ( + "API_THROTTLE_MELISSA_RATE_ORG", + "API: Melissa request throttle rate for organizations", + ), + ( + "API_THROTTLE_MELISSA_ENABLED_ORG", + "API: Melissa request throttle enabled for organizations", + ), + ( + "API_THROTTLE_MELISSA_RATE_ANON", + "API: Melissa request throttle rate for anonymous requests (ips)", + ), + ( + "API_THROTTLE_MELISSA_ENABLED_ANON", + "API: Melissa request throttle enabled for anonymous requests (ips)", + ), + ( + "API_THROTTLE_RESPONSE_SIZE_THRESHOLD_CIDR", + "API: Response size throttle size threshold for ip blocks (bytes)", + ), + ( + "API_THROTTLE_RESPONSE_SIZE_RATE_CIDR", + "API: Response size throttle rate for ip blocks", + ), + ( + "API_THROTTLE_RESPONSE_SIZE_ENABLED_CIDR", + "API: Response size throttle enabled for ip blocks", + ), + ( + "API_THROTTLE_RESPONSE_SIZE_THRESHOLD_IP", + "API: Response size throttle size threshold for ip addresses (bytes)", + ), + ( + "API_THROTTLE_RESPONSE_SIZE_RATE_IP", + "API: Response size throttle rate for ip addresses", + ), + ( + "API_THROTTLE_RESPONSE_SIZE_ENABLED_IP", + "API: Response size throttle enabled for ip addresses", + ), + ( + "API_THROTTLE_RESPONSE_SIZE_THRESHOLD_USER", + "API: Response size throttle size threshold for authenticated users (bytes)", + ), + ( + "API_THROTTLE_RESPONSE_SIZE_RATE_USER", + "API: Response size throttle rate for authenticated users", + ), + ( + "API_THROTTLE_RESPONSE_SIZE_ENABLED_USER", + "API: Response size throttle enabled for authenticated users", + ), + ( + "API_THROTTLE_RESPONSE_SIZE_THRESHOLD_ORG", + "API: Response size throttle size threshold for organization api-keys (bytes)", + ), + ( + "API_THROTTLE_RESPONSE_SIZE_RATE_ORG", + "API: Response size throttle rate for organization api-keys", + ), + ( + "API_THROTTLE_RESPONSE_SIZE_ENABLED_ORG", + "API: Response size throttle enabled for organization api-keys", + ), + ( + "API_THROTTLE_RATE_ANON_MSG", + "API: Anonymous API throttle rate message", + ), + ( + "API_THROTTLE_RATE_USER_MSG", + "API: Authenticated API throttle rate message", + ), + ], + max_length=255, + unique=True, + ), + ), + ] diff --git a/peeringdb_server/models.py b/peeringdb_server/models.py index 1baa72cf..bc7d6248 100644 --- a/peeringdb_server/models.py +++ b/peeringdb_server/models.py @@ -67,6 +67,8 @@ from peeringdb_server.inet import RdapLookup, RdapNotFoundError from peeringdb_server.request import bypass_validation from peeringdb_server.validators import ( validate_address_space, + validate_api_rate, + validate_bool, validate_info_prefixes4, validate_info_prefixes6, validate_irr_as_set, @@ -5332,11 +5334,103 @@ class EnvironmentSetting(models.Model): # ), ( "API_THROTTLE_RATE_ANON", - _("API: Anonymous user API throttle rate"), + _("API: Anonymous API throttle rate"), ), ( "API_THROTTLE_RATE_USER", - _("API: Authenticated user API throttle rate"), + _("API: Authenticated API throttle rate"), + ), + # melissa rate throttle + ( + "API_THROTTLE_MELISSA_RATE_USER", + _("API: Melissa request throttle rate for users"), + ), + ( + "API_THROTTLE_MELISSA_ENABLED_USER", + _("API: Melissa request throttle enabled for users"), + ), + ( + "API_THROTTLE_MELISSA_RATE_ORG", + _("API: Melissa request throttle rate for organizations"), + ), + ( + "API_THROTTLE_MELISSA_ENABLED_ORG", + _("API: Melissa request throttle enabled for organizations"), + ), + ( + "API_THROTTLE_MELISSA_RATE_ANON", + _("API: Melissa request throttle rate for anonymous requests (ips)"), + ), + ( + "API_THROTTLE_MELISSA_ENABLED_ANON", + _("API: Melissa request throttle enabled for anonymous requests (ips)"), + ), + # api response size throttle: ip-block config + ( + "API_THROTTLE_RESPONSE_SIZE_THRESHOLD_CIDR", + _("API: Response size throttle size threshold for ip blocks (bytes)"), + ), + ( + "API_THROTTLE_RESPONSE_SIZE_RATE_CIDR", + _("API: Response size throttle rate for ip blocks"), + ), + ( + "API_THROTTLE_RESPONSE_SIZE_ENABLED_CIDR", + _("API: Response size throttle enabled for ip blocks"), + ), + # api response size throttle: ip address config + ( + "API_THROTTLE_RESPONSE_SIZE_THRESHOLD_IP", + _( + "API: Response size throttle size threshold for ip addresses (bytes)" + ), + ), + ( + "API_THROTTLE_RESPONSE_SIZE_RATE_IP", + _("API: Response size throttle rate for ip addresses"), + ), + ( + "API_THROTTLE_RESPONSE_SIZE_ENABLED_IP", + _("API: Response size throttle enabled for ip addresses"), + ), + # api response size throttle: user config + ( + "API_THROTTLE_RESPONSE_SIZE_THRESHOLD_USER", + _( + "API: Response size throttle size threshold for authenticated users (bytes)" + ), + ), + ( + "API_THROTTLE_RESPONSE_SIZE_RATE_USER", + _("API: Response size throttle rate for authenticated users"), + ), + ( + "API_THROTTLE_RESPONSE_SIZE_ENABLED_USER", + _("API: Response size throttle enabled for authenticated users"), + ), + # api response size throttle: org config + ( + "API_THROTTLE_RESPONSE_SIZE_THRESHOLD_ORG", + _( + "API: Response size throttle size threshold for organization api-keys (bytes)" + ), + ), + ( + "API_THROTTLE_RESPONSE_SIZE_RATE_ORG", + _("API: Response size throttle rate for organization api-keys"), + ), + ( + "API_THROTTLE_RESPONSE_SIZE_ENABLED_ORG", + _("API: Response size throttle enabled for organization api-keys"), + ), + # api throttling response messages + ( + "API_THROTTLE_RATE_ANON_MSG", + _("API: Anonymous API throttle rate message"), + ), + ( + "API_THROTTLE_RATE_USER_MSG", + _("API: Authenticated API throttle rate message"), ), ), unique=True, @@ -5373,6 +5467,45 @@ class EnvironmentSetting(models.Model): # "IXF_IMPORTER_DAYS_UNTIL_TICKET": "value_int", "API_THROTTLE_RATE_ANON": "value_str", "API_THROTTLE_RATE_USER": "value_str", + "API_THROTTLE_RESPONSE_SIZE_THRESHOLD_CIDR": "value_int", + "API_THROTTLE_RESPONSE_SIZE_RATE_CIDR": "value_str", + "API_THROTTLE_RESPONSE_SIZE_ENABLED_CIDR": "value_bool", + "API_THROTTLE_RESPONSE_SIZE_THRESHOLD_IP": "value_int", + "API_THROTTLE_RESPONSE_SIZE_RATE_IP": "value_str", + "API_THROTTLE_RESPONSE_SIZE_ENABLED_IP": "value_bool", + "API_THROTTLE_RESPONSE_SIZE_THRESHOLD_USER": "value_int", + "API_THROTTLE_RESPONSE_SIZE_RATE_USER": "value_str", + "API_THROTTLE_RESPONSE_SIZE_ENABLED_USER": "value_bool", + "API_THROTTLE_RESPONSE_SIZE_THRESHOLD_ORG": "value_int", + "API_THROTTLE_RESPONSE_SIZE_RATE_ORG": "value_str", + "API_THROTTLE_RESPONSE_SIZE_ENABLED_ORG": "value_bool", + "API_THROTTLE_MELISSA_RATE_USER": "value_str", + "API_THROTTLE_MELISSA_ENABLED_USER": "value_bool", + "API_THROTTLE_MELISSA_RATE_ORG": "value_str", + "API_THROTTLE_MELISSA_ENABLED_ORG": "value_bool", + "API_THROTTLE_MELISSA_RATE_IP": "value_str", + "API_THROTTLE_MELISSA_ENABLED_IP": "value_bool", + "API_THROTTLE_RATE_ANON_MSG": "value_str", + "API_THROTTLE_RATE_USER_MSG": "value_str", + } + + setting_validators = { + "API_THROTTLE_RATE_ANON": [validate_api_rate], + "API_THROTTLE_RATE_USER": [validate_api_rate], + "API_THROTTLE_RESPONSE_SIZE_RATE_CIDR": [validate_api_rate], + "API_THROTTLE_RESPONSE_SIZE_ENABLED_CIDR": [validate_bool], + "API_THROTTLE_RESPONSE_SIZE_RATE_IP": [validate_api_rate], + "API_THROTTLE_RESPONSE_SIZE_ENABLED_IP": [validate_bool], + "API_THROTTLE_RESPONSE_SIZE_RATE_USER": [validate_api_rate], + "API_THROTTLE_RESPONSE_SIZE_ENABLED_USER": [validate_bool], + "API_THROTTLE_RESPONSE_SIZE_RATE_ORG": [validate_api_rate], + "API_THROTTLE_RESPONSE_SIZE_ENABLED_ORG": [validate_bool], + "API_THROTTLE_MELISSA_RATE_USER": [validate_api_rate], + "API_THROTTLE_MELISSA_RATE_ORG": [validate_api_rate], + "API_THROTTLE_MELISSA_RATE_IP": [validate_api_rate], + "API_THROTTLE_MELISSA_ENABLED_USER": [validate_bool], + "API_THROTTLE_MELISSA_ENABLED_ORG": [validate_bool], + "API_THROTTLE_MELISSA_ENABLED_IP": [validate_bool], } @classmethod @@ -5391,6 +5524,15 @@ class EnvironmentSetting(models.Model): except cls.DoesNotExist: return getattr(settings, setting) + @classmethod + def validate_value(cls, setting, value): + if value is None: + return value + + for validator in cls.setting_validators.get(setting, []): + value = validator(value) + return value + @property def value(self): """ @@ -5398,11 +5540,23 @@ class EnvironmentSetting(models.Model): """ return getattr(self, self.setting_to_field[self.setting]) + def __str__(self): + return f"EnvironmentSetting `{self.setting}` ({self.id})" + + def clean(self): + self.validate_value(self.setting, self.value) + def set_value(self, value): """ Update the value for this setting. """ - setattr(self, self.setting_to_field[self.setting], value) + + setattr( + self, + self.setting_to_field[self.setting], + self.validate_value(self.setting, value), + ) + self.full_clean() self.save() diff --git a/peeringdb_server/renderers.py b/peeringdb_server/renderers.py index 72724844..88e5dad6 100644 --- a/peeringdb_server/renderers.py +++ b/peeringdb_server/renderers.py @@ -9,6 +9,8 @@ import json from rest_framework import renderers from rest_framework.utils import encoders +from peeringdb_server.rest_throttles import ResponseSizeThrottle + class JSONEncoder(encoders.JSONEncoder): """ @@ -78,6 +80,8 @@ class MetaJSONRenderer(MungeRenderer): if "request" in renderer_context: request = renderer_context.get("request") meta.update(getattr(request, "meta_response", {})) + else: + request = None res = renderer_context["response"] if res.status_code < 400: @@ -98,6 +102,12 @@ class MetaJSONRenderer(MungeRenderer): result["meta"] = meta - return super(self.__class__, self).render( + rendered_content = super(self.__class__, self).render( result, accepted_media_type, renderer_context ) + + # handle caching of response size (#1129) + if request: + ResponseSizeThrottle.cache_response_size(request, len(rendered_content)) + + return rendered_content diff --git a/peeringdb_server/rest.py b/peeringdb_server/rest.py index cfc9db47..8da12af5 100644 --- a/peeringdb_server/rest.py +++ b/peeringdb_server/rest.py @@ -361,6 +361,8 @@ class ModelViewSet(viewsets.ModelViewSet): # db field filters filters = {} + query_params = self.request.query_params + for k, v in list(self.request.query_params.items()): if k == "q": @@ -381,6 +383,12 @@ class ModelViewSet(viewsets.ModelViewSet): v = unidecode.unidecode(v) + # if country and state are specified, try to normalize state #1079 + if k == "state" and hasattr( + self.serializer_class, "normalize_state_lookup" + ): + v = self.serializer_class.normalize_state_lookup(query_params) + if k == "ipaddr6": v = coerce_ipaddr(v) diff --git a/peeringdb_server/rest_throttles.py b/peeringdb_server/rest_throttles.py index 3e0c24c0..4de27388 100644 --- a/peeringdb_server/rest_throttles.py +++ b/peeringdb_server/rest_throttles.py @@ -1,8 +1,11 @@ """ Custom rate limit handlers for the REST API. """ +import ipaddress +import re from django.conf import settings +from django.core.cache import cache from rest_framework import throttling from rest_framework.exceptions import PermissionDenied @@ -19,6 +22,167 @@ class IXFImportThrottle(throttling.UserRateThrottle): return f"{key}.{ix.id}" +class TargetedRateThrottle(throttling.SimpleRateThrottle): + + """ + Base class for targeted rate throttling depending + on authentication status + + Rate throttle by + - user (sess-auth, basic-auth, key), + - org (key), + - anonymous (inet, cdir) + """ + + scope_ip = "anon" + scope_cidr = "anon" + scope_user = "user" + scope_org = "user" + + def __init__(self): + pass + + def ident_prefix(self, request): + return "" + + def is_authenticated(self, request): + self.user = get_user_from_request(request) + self.org_key = get_org_key_from_request(request) + return self.user is not None or self.org_key is not None + + def set_throttle_response(self, request, msg_setting): + request.throttle_response_message = EnvironmentSetting.get_setting_value( + msg_setting + ) + + def _allow_request_user_auth(self, request, view, ident_prefix=""): + self.ident = f"{ident_prefix}user:{self.user.pk}" + self.scope = self.scope_user + self.rate = self.get_rate() + self.num_requests, self.duration = self.parse_rate(self.rate) + allowed = super().allow_request(request, view) + + if not allowed: + self.set_throttle_response(request, "API_THROTTLE_RATE_USER_MSG") + + return allowed + + def _allow_request_org_auth(self, request, view, ident_prefix=""): + + self.ident = f"{ident_prefix}org:{self.org_key.org_id}" + self.scope = self.scope_org + self.rate = self.get_rate() + self.num_requests, self.duration = self.parse_rate(self.rate) + + allowed = super().allow_request(request, view) + + if not allowed: + self.set_throttle_response(request, "API_THROTTLE_RATE_USER_MSG") + + return allowed + + def _allow_request_anon(self, request, view, ident_prefix=""): + + # first, check ip-address throttling + # this is the default throttling mechanism for SimpleRateThrottle + # so calling `get_ident` will give us the request ip-address + + ip_address = self.get_ident(request) + + if self.check_ip(request): + self.ident = ip_address + self.ident = f"{ident_prefix}{self.ident}" + self.scope = self.scope_ip + self.rate = self.get_rate() + self.num_requests, self.duration = self.parse_rate(self.rate) + allow_ip = super().allow_request(request, view) + else: + allow_ip = True + + # single ip was allowed, next check if the /24 block for the + # ip is allowed as well. + + if self.check_cidr(request): + + ip = ipaddress.ip_address(ip_address) + + if ip.version == 4: + self.ident = str( + ipaddress.ip_network(f"{ip_address}/32").supernet(new_prefix=24) + ) + else: + self.ident = str( + ipaddress.ip_network(f"{ip_address}/128").supernet(new_prefix=64) + ) + + self.ident = f"{ident_prefix}{self.ident}" + self.scope = self.scope_cidr + self.rate = self.get_rate() + self.num_requests, self.duration = self.parse_rate(self.rate) + allow_cidr = super().allow_request(request, view) + else: + allow_cidr = True + + # both the supernet as well as the single ip address + # need to pass to allow the request + + allowed = allow_ip and allow_cidr + + if not allowed: + self.set_throttle_response(request, "API_THROTTLE_RATE_ANON_MSG") + + return allowed + + def allow_request(self, request, view): + + self.is_authenticated(request) + + ident_prefix = self.ident_prefix(request) + + if self.user and self.check_user(request): + + # authenticated user + + return self._allow_request_user_auth(request, view, ident_prefix) + + if self.org_key and self.check_org(request): + + # organization + + return self._allow_request_org_auth(request, view, ident_prefix) + + # at this point if the request is authenticated its ok to let through + + if self.user or self.org_key: + return True + + # anonymous + + return self._allow_request_anon(request, view, ident_prefix) + + def check_user(self, request): + return True + + def check_org(self, request): + return True + + def check_ip(self, request): + return True + + def check_cidr(self, request): + return True + + def get_rate(self): + if hasattr(self, "_rate"): + return self._rate + return super().get_rate() + + def get_cache_key(self, request, view): + + cache_key = self.cache_format % {"scope": self.scope, "ident": self.ident} + return cache_key + + class FilterThrottle(throttling.SimpleRateThrottle): """ @@ -113,31 +277,220 @@ class FilterDistanceThrottle(FilterThrottle): filter_name = "distance" -class APIAnonUserThrottle(throttling.AnonRateThrottle): +class APIAnonUserThrottle(TargetedRateThrottle): + """ - Rate limiting for anonymous users. + General rate limiting for anonymous users via the request ip-address """ - filter_name = "anon" + def check_user(self, request): + return False - def allow_request(self, request, view): + def check_org(self, request): + return False - self.rate = EnvironmentSetting.get_setting_value("API_THROTTLE_RATE_ANON") - self.num_requests, self.duration = self.parse_rate(self.rate) + def check_ip(self, request): + self.scope = "anon" + self._rate = EnvironmentSetting.get_setting_value("API_THROTTLE_RATE_ANON") + return True - return super().allow_request(request, view) + def check_cidr(self, request): + return False -class APIUserThrottle(throttling.UserRateThrottle): +class APIUserThrottle(TargetedRateThrottle): """ - Rate limiting for authenticated users. + General rate limiting for authenticated requests (users or orgs) """ - filter_name = "user" + def check_user(self, request): + self.scope = "user" + self._rate = EnvironmentSetting.get_setting_value("API_THROTTLE_RATE_USER") + return True - def allow_request(self, request, view): + def check_org(self, request): + self.scope = "user" + self._rate = EnvironmentSetting.get_setting_value("API_THROTTLE_RATE_USER") + return True - self.rate = EnvironmentSetting.get_setting_value("API_THROTTLE_RATE_USER") - self.num_requests, self.duration = self.parse_rate(self.rate) + def check_ip(self, request): + return False - return super().allow_request(request, view) + def check_cidr(self, request): + return False + + +class ResponseSizeThrottle(TargetedRateThrottle): + + """ + Rate limit repeated requests based request content-size + + See #1126 for rationale + """ + + scope_user = "response_size_user" + scope_org = "response_size_org" + scope_ip = "response_size_ip" + scope_cidr = "response_size_cidr" + + @classmethod + def size_cache_key(cls, request): + """ + Returns the cache key to use for storing response size cache + """ + + # use the full request path plus appended query string + # for the cache key + + return f"request-size:{request.get_full_path()}" + + @classmethod + def cache_response_size(cls, request, size): + """ + Caches the response size for the request + + The api renderer (renderers.py) calls this automatically + when it renders the response + """ + + # This will be called for EVERY api request. + # + # Only write the response size cache if it does not exist yet + # or is expired otherwise it introduces and unnecessary database + # write operation at the back of each request. + + if cls.expected_response_size(request) is None: + cache.set( + cls.size_cache_key(request), + size, + settings.API_THROTTLE_RESPONSE_SIZE_CACHE_EXPIRY, + ) + + @classmethod + def expected_response_size(cls, request): + """ + Returns the expected response size (number of bytes) for the request as `int` + + It will return None if there is no cached response size for the request. + """ + + # Expected size was already determined for this request + # object, return it + + if hasattr(request, "_expected_response_size"): + return request._expected_response_size + + # Request content size is unkown at this point, so logic relies + # on cached size stored from a previous request to the same + # path + # + # if cache does not exist, its the first time this path is + # requested and it can be allowed through. + + size = cache.get(cls.size_cache_key(request)) + request._expected_response_size = size + + return size + + def ident_prefix(self, request): + return f"{request.get_full_path()}:" + + def check_user(self, request): + return self._check_source(request, "user") + + def check_org(self, request): + return self._check_source(request, "org") + + def check_ip(self, request): + return self._check_source(request, "ip") + + def check_cidr(self, request): + return self._check_source(request, "cidr") + + def _check_source(self, request, src): + suffix = src.upper() + enabled = EnvironmentSetting.get_setting_value( + f"API_THROTTLE_RESPONSE_SIZE_ENABLED_{suffix}" + ) + + if not enabled: + return False + + size = self.expected_response_size(request) + + if size is None: + return False + + limit = EnvironmentSetting.get_setting_value( + f"API_THROTTLE_RESPONSE_SIZE_THRESHOLD_{suffix}" + ) + + self._rate = EnvironmentSetting.get_setting_value( + f"API_THROTTLE_RESPONSE_SIZE_RATE_{suffix}" + ) + + return size >= limit + + +class MelissaThrottle(TargetedRateThrottle): + """ + Rate limits requests that do a melissa lookup (#1124) + """ + + scope_user = "melissa_user" + scope_org = "melissa_org" + scope_ip = "melissa_ip" + + def ident_prefix(self, request): + return "melissa:" + + def check_user(self, request): + return self._check_source(request, "user") + + def check_org(self, request): + return self._check_source(request, "org") + + def check_ip(self, request): + return self._check_source(request, "ip") + + def check_cidr(self, request): + return False + + def set_throttle_response(self, request, msg_setting): + reason = getattr(request, "_melissa_throttle_reason", "melissa") + super().set_throttle_response(request, msg_setting) + request.throttle_response_message += f" - {reason}" + + def _check_source(self, request, src): + suffix = src.upper() + enabled = EnvironmentSetting.get_setting_value( + f"API_THROTTLE_MELISSA_ENABLED_{suffix}" + ) + + rate_setting = f"API_THROTTLE_MELISSA_RATE_{suffix}" + + if not enabled: + return False + + # case 1 - `state` filter to api end points + + if re.match(r"^/api/(fac|org)$", request.path) and request.GET.get("state"): + self._rate = EnvironmentSetting.get_setting_value(rate_setting) + request._melissa_throttle_reason = ( + "geo address normalization query on api filter for `state` field" + ) + return True + + # case 2 -post/put to objects that trigger address normalization + + if re.match(r"^/api/(fac|org)/\d+$", request.path) and request.method in [ + "POST", + "PUT", + ]: + request._melissa_throttle_reason = ( + "saving object that requires geo address normalization" + ) + self._rate = EnvironmentSetting.get_setting_value(rate_setting) + return True + + return False diff --git a/peeringdb_server/serializers.py b/peeringdb_server/serializers.py index e1e6caf6..33294cdc 100644 --- a/peeringdb_server/serializers.py +++ b/peeringdb_server/serializers.py @@ -40,6 +40,7 @@ from peeringdb_server.deskpro import ( ticket_queue_asnauto_skipvq, ticket_queue_rdap_error, ) +from peeringdb_server.geo import Melissa from peeringdb_server.inet import ( RdapException, RdapInvalidRange, @@ -134,6 +135,34 @@ class GeocodeSerializerMixin: "if needed." ).format(settings.DEFAULT_FROM_EMAIL) + @classmethod + def normalize_state_lookup(cls, filters): + """ + for non-distance search the specifies state and country + attempt to normalize the state field using melissa global address + lookup. (#1079) + + this does NOT need to be done on distance search since distance search + already normalizes the search to geo-coordinates using melissa. + """ + + if "state" in filters and ("country" in filters or "country__in" in filters): + + # in the case where country__in is provided as a country filter + # there is no sensible way for us determine which country to use for the + # state normalization, for now simply use the first country in the list + # as this provides compatibility with how the advanced search form + # is wired to the api. + + if "country__in" in filters: + country = filters.get("country__in").split(",")[0] + else: + country = filters.get("country") + + melissa = Melissa(settings.MELISSA_KEY) + return melissa.normalize_state(country, filters["state"]) + return filters.get("state") + def _geosync_information_present(self, instance, validated_data): """ Determine if there is enough address information diff --git a/peeringdb_server/static/20c/twentyc.edit.js b/peeringdb_server/static/20c/twentyc.edit.js index 6acf9035..502dcf58 100644 --- a/peeringdb_server/static/20c/twentyc.edit.js +++ b/peeringdb_server/static/20c/twentyc.edit.js @@ -661,7 +661,12 @@ twentyc.editable.target.error_handlers.http_json = function(response, me, sender } } + } else if(response.status == 429) { + + info = ["Too Many Requests", response.responseJSON.message]; + } else { + if(response.responseJSON && response.responseJSON.non_field_errors) { info = []; var i; diff --git a/peeringdb_server/static/peeringdb.js b/peeringdb_server/static/peeringdb.js index 3bab2c7a..c07b3b51 100644 --- a/peeringdb_server/static/peeringdb.js +++ b/peeringdb_server/static/peeringdb.js @@ -2112,9 +2112,11 @@ twentyc.editable.target.register( info : info.join("
") }); } else { - if(r.responseJSON && r.responseJSON.meta && r.responseJSON.meta.error) + if(r.responseJSON && r.responseJSON.meta && r.responseJSON.meta.error) { var info = r.responseJSON.meta.error; - else if(r.status == 403) + info += "
" + r.responseJSON.message; + + } else if(r.status == 403) var info = gettext("You do not have permissions to perform this action") else var info = r.status+" "+r.statusText @@ -2235,7 +2237,12 @@ twentyc.editable.module.register( // is rendered as a link var ixlnk = $(''); - ixlnk.attr("href", "/ix/"+data.id); + if (data.status == "ok") { + ixlnk.attr("href", "/ix/"+data.id); + } + else { + ixlnk.attr("style", "text-decoration: none;"); + } ixlnk.text(data.name); row.find(".name").html(ixlnk); @@ -2251,7 +2258,12 @@ twentyc.editable.module.register( // is rendered as a link var netlnk = $(''); - netlnk.attr("href", "/net/"+data.id); + if (data.status == "ok") { + netlnk.attr("href", "/net/"+data.id); + } + else { + netlnk.attr("style", "text-decoration: none;"); + } netlnk.text(data.name); row.find(".name").html(netlnk); @@ -2267,7 +2279,14 @@ twentyc.editable.module.register( // is rendered as a link var faclnk = $(''); - faclnk.attr("href", "/fac/"+data.id); + + if (data.status == "ok") { + faclnk.attr("href", "/fac/"+data.id); + } + else { + faclnk.attr("style", "text-decoration: none;"); + } + faclnk.text(data.name); row.find(".name").html(faclnk); diff --git a/peeringdb_server/templates/site/advanced-search.html b/peeringdb_server/templates/site/advanced-search.html index bb2a07a5..9b871c61 100644 --- a/peeringdb_server/templates/site/advanced-search.html +++ b/peeringdb_server/templates/site/advanced-search.html @@ -99,7 +99,7 @@ if(d && d.info == "404 Not Found") { searchForm.find(".results-empty").show(); } else { - searchForm.trigger("action-error", {}); + searchForm.trigger("action-error", d); } }); target.search(); diff --git a/peeringdb_server/templates/site/view_organization_side.html b/peeringdb_server/templates/site/view_organization_side.html index e19b2920..fa0f67b7 100644 --- a/peeringdb_server/templates/site/view_organization_side.html +++ b/peeringdb_server/templates/site/view_organization_side.html @@ -48,7 +48,7 @@ × {% endif %}
- {{ n.name }} + {{ n.name }}
{% if permissions.can_edit and n.status != "ok" %}
@@ -116,7 +116,7 @@ {% if permissions.can_delete_net %} × {% endif %} - {{ n.name }} + {{ n.name }}
{% if permissions.can_edit and n.status != "ok" %}
@@ -184,7 +184,7 @@ × {% endif %}
- {{ n.name }} + {{ n.name }}
{% if permissions.can_edit and n.status != "ok" %}
diff --git a/peeringdb_server/templatetags/two_factor_ext.py b/peeringdb_server/templatetags/two_factor_ext.py index 2ffd5055..cfb9fd90 100644 --- a/peeringdb_server/templatetags/two_factor_ext.py +++ b/peeringdb_server/templatetags/two_factor_ext.py @@ -14,8 +14,9 @@ register = template.Library() def device_action(device): if isinstance(device, EmailDevice): return _("Email one time password") - elif device.method == "security-key": - return _("U2F security key") + elif device: + if device.method == "security-key": + return _("U2F security key") return two_factor.device_action(device) diff --git a/peeringdb_server/validators.py b/peeringdb_server/validators.py index b190daa6..58a31ea6 100644 --- a/peeringdb_server/validators.py +++ b/peeringdb_server/validators.py @@ -312,3 +312,62 @@ def validate_irr_as_set(value): validated.append(item) return " ".join(validated) + + +def validate_bool(value): + """ + Validates a boolean value + + This can be passed a string for `True` or `False` or an integer as 1, 0 as well + to convert and return a boolean value + + Will raise ValidationError on failure. + + Arguments: + + - value (`str`|`int`|`bool`) + + Returns: + + - validated value (`bool`) + """ + try: + if isinstance(value, bool): + return value + if isinstance(value, str): + if value.lower() == "true": + return True + if value.lower() == "false": + return False + return bool(int(value)) + except TypeError: + raise ValidationError(_("Needs to be 'True', 'False', 1 or 0")) + + +def validate_api_rate(value): + """ + Validates a number/time-unit format used to determine rate limits + + e.g., 10/second or 100/minute + + Will raise a ValidationError on failure + + Arguments: + + - value(`str`) + + Returns: + + - validated value (`str`) + """ + + value = str(value) + if re.match(r"([/\d]+)\s*(?:minute|hour|seconds|day|week|month|year)", value): + return value + else: + print(value) + raise ValidationError( + _( + "Invalid setting! Acceptable value is a number followed by one of the following: minute, hour, seconds, day, week, month, year. eg (10/minute, 1/hour, 5/day, 1/week, 1/month, 1/year)" + ) + ) diff --git a/peeringdb_server/views.py b/peeringdb_server/views.py index 3b9b9069..9ba2003a 100644 --- a/peeringdb_server/views.py +++ b/peeringdb_server/views.py @@ -874,7 +874,11 @@ def view_registration(request): return JsonResponse( {"password1": _("Needs to be at least 10 characters long")}, status=400 ) - + # filter out invalid username characters + if form.cleaned_data["username"].startswith("apikey"): + return JsonResponse( + {"username": _("Username cannot start with 'apikey'")}, status=400 + ) # create the user user = form.save() @@ -1056,7 +1060,7 @@ def view_organization(request, id): try: org = OrganizationSerializer.prefetch_related( Organization.objects, request, depth=2 - ).get(id=id, status__in=["ok", "pending"]) + ).get(id=id, status="ok") except ObjectDoesNotExist: return view_http_error_404(request) @@ -1257,7 +1261,7 @@ def view_facility(request, id): """ try: - facility = Facility.objects.get(id=id, status__in=["ok", "pending"]) + facility = Facility.objects.get(id=id, status="ok") except ObjectDoesNotExist: return view_http_error_404(request) @@ -1471,7 +1475,7 @@ def view_exchange(request, id): """ try: - exchange = InternetExchange.objects.get(id=id, status__in=["ok", "pending"]) + exchange = InternetExchange.objects.get(id=id, status="ok") except ObjectDoesNotExist: return view_http_error_404(request) @@ -1769,7 +1773,7 @@ def view_network(request, id): try: network = NetworkSerializer.prefetch_related( Network.objects, request, depth=2, selective=["poc_set"] - ).get(id=id, status__in=["ok", "pending"]) + ).get(id=id, status="ok") except ObjectDoesNotExist: return view_http_error_404(request) @@ -2362,7 +2366,12 @@ class LoginView(TwoFactorLoginView): context.update(**make_env()) if "other_devices" in context: - context["other_devices"] += [self.get_email_device()] + email_device = self.get_email_device() + + # If the user has an email device, we need to + # add the email device to the context + if email_device: + context["other_devices"] += [email_device] return context diff --git a/poetry.lock b/poetry.lock index d077c3a6..7aacea45 100644 --- a/poetry.lock +++ b/poetry.lock @@ -11,7 +11,7 @@ tests = ["pytest", "pytest-asyncio", "mypy (>=0.800)"] [[package]] name = "asn1crypto" -version = "1.4.0" +version = "1.5.1" description = "Fast ASN.1 parser and serializer with definitions for private keys, public keys, certificates, CRL, OCSP, CMS, PKCS#3, PKCS#7, PKCS#8, PKCS#12, PKCS#5, X.509 and TSP" category = "main" optional = false @@ -57,7 +57,7 @@ typecheck = ["mypy"] [[package]] name = "black" -version = "22.1.0" +version = "22.3.0" description = "The uncompromising code formatter." category = "dev" optional = false @@ -68,7 +68,7 @@ click = ">=8.0.0" mypy-extensions = ">=0.4.3" pathspec = ">=0.9.0" platformdirs = ">=2" -tomli = ">=1.1.0" +tomli = {version = ">=1.1.0", markers = "python_version < \"3.11\""} typing-extensions = {version = ">=3.10.0.0", markers = "python_version < \"3.10\""} [package.extras] @@ -79,17 +79,20 @@ uvloop = ["uvloop (>=0.15.2)"] [[package]] name = "bleach" -version = "4.1.0" +version = "5.0.0" description = "An easy safelist-based HTML-sanitizing tool." category = "main" optional = false -python-versions = ">=3.6" +python-versions = ">=3.7" [package.dependencies] -packaging = "*" six = ">=1.9.0" webencodings = "*" +[package.extras] +css = ["tinycss2 (>=1.1.0)"] +dev = ["pip-tools (==6.5.1)", "pytest (==7.1.1)", "flake8 (==4.0.1)", "tox (==3.24.5)", "sphinx (==4.3.2)", "twine (==4.0.0)", "wheel (==0.37.1)", "hashin (==0.17.0)", "black (==22.3.0)", "mypy (==0.942)"] + [[package]] name = "cbor2" version = "5.4.2.post1" @@ -153,11 +156,11 @@ unicode_backport = ["unicodedata2"] [[package]] name = "click" -version = "8.0.4" +version = "8.1.2" description = "Composable command line interface toolkit" category = "main" optional = false -python-versions = ">=3.6" +python-versions = ">=3.7" [package.dependencies] colorama = {version = "*", markers = "platform_system == \"Windows\""} @@ -211,7 +214,7 @@ toml = ["tomli"] [[package]] name = "cryptography" -version = "36.0.1" +version = "36.0.2" description = "cryptography is a package which provides cryptographic recipes and primitives to Python developers." category = "main" optional = false @@ -276,7 +279,7 @@ python-versions = ">=3.6" [[package]] name = "django" -version = "3.2.12" +version = "3.2.13" description = "A high-level Python Web framework that encourages rapid development and clean, pragmatic design." category = "main" optional = false @@ -293,7 +296,7 @@ bcrypt = ["bcrypt"] [[package]] name = "django-allauth" -version = "0.49.0" +version = "0.50.0" description = "Integrated set of Django applications addressing authentication, registration, account management as well as 3rd party (social) account authentication." category = "main" optional = false @@ -308,7 +311,7 @@ requests-oauthlib = ">=0.3.0" [[package]] name = "django-autocomplete-light" -version = "3.9.1" +version = "3.9.4" description = "Fresh autocompletes for Django" category = "main" optional = false @@ -434,7 +437,7 @@ python-versions = "*" [[package]] name = "django-handleref" -version = "1.0.1" +version = "1.0.2" description = "django object tracking" category = "main" optional = false @@ -474,6 +477,14 @@ category = "main" optional = false python-versions = ">=3.6.2,<4.0.0" +[[package]] +name = "django-ipware" +version = "4.0.2" +description = "A Django application to retrieve user's IP address" +category = "main" +optional = false +python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*, !=3.5.*" + [[package]] name = "django-oauth-toolkit" version = "1.6.1" @@ -504,11 +515,11 @@ qrcode = ["qrcode"] [[package]] name = "django-peeringdb" -version = "2.12.0" +version = "2.13.0" description = "PeeringDB Django models" category = "main" optional = false -python-versions = ">=3.6.2,<4.0.0" +python-versions = ">=3.7,<4.0" [package.dependencies] django_countries = ">1" @@ -602,6 +613,19 @@ Pillow = ">=6.2.0" [package.extras] test = ["testfixtures"] +[[package]] +name = "django-structlog" +version = "2.2.0" +description = "Structured Logging for Django" +category = "main" +optional = false +python-versions = "*" + +[package.dependencies] +django = ">=1.11" +django-ipware = "*" +structlog = "*" + [[package]] name = "django-tables2" version = "2.4.1" @@ -795,7 +819,7 @@ python-versions = ">=3.6,<4.0" [[package]] name = "identify" -version = "2.4.11" +version = "2.4.12" description = "File identification library for Python" category = "dev" optional = false @@ -814,7 +838,7 @@ python-versions = ">=3.5" [[package]] name = "importlib-metadata" -version = "4.11.2" +version = "4.11.3" description = "Read metadata from Python packages" category = "main" optional = false @@ -860,11 +884,11 @@ python-versions = "*" [[package]] name = "jinja2" -version = "3.0.3" +version = "3.1.1" description = "A very fast and expressive template engine." category = "main" optional = false -python-versions = ">=3.6" +python-versions = ">=3.7" [package.dependencies] MarkupSafe = ">=2.0" @@ -928,7 +952,7 @@ markdown = "*" [[package]] name = "markupsafe" -version = "2.1.0" +version = "2.1.1" description = "Safely add untrusted strings to HTML/XML markup." category = "main" optional = false @@ -952,7 +976,7 @@ python-versions = ">=3.6" [[package]] name = "mkdocs" -version = "1.2.3" +version = "1.3.0" description = "Project documentation with Markdown." category = "dev" optional = false @@ -961,8 +985,8 @@ python-versions = ">=3.6" [package.dependencies] click = ">=3.3" ghp-import = ">=1.0" -importlib-metadata = ">=3.10" -Jinja2 = ">=2.10.1" +importlib-metadata = ">=4.3" +Jinja2 = ">=2.10.2" Markdown = ">=3.2.1" mergedeep = ">=1.3.4" packaging = ">=20.5" @@ -1042,7 +1066,7 @@ coreapi = ">=2.2.0" name = "packaging" version = "21.3" description = "Core utilities for Python packages" -category = "main" +category = "dev" optional = false python-versions = ">=3.6" @@ -1051,7 +1075,7 @@ pyparsing = ">=2.0.2,<3.0.5 || >3.0.5" [[package]] name = "paramiko" -version = "2.9.2" +version = "2.10.3" description = "SSH2 protocol library" category = "dev" optional = false @@ -1061,6 +1085,7 @@ python-versions = "*" bcrypt = ">=3.1.3" cryptography = ">=2.5" pynacl = ">=1.0.1" +six = "*" [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)"] @@ -1106,7 +1131,7 @@ PyYAML = ">=3.11" [[package]] name = "phonenumbers" -version = "8.12.44" +version = "8.12.46" description = "Python version of Google's common library for parsing, formatting, storing and validating international phone numbers." category = "main" optional = false @@ -1114,12 +1139,16 @@ python-versions = "*" [[package]] name = "pillow" -version = "9.0.1" +version = "9.1.0" description = "Python Imaging Library (Fork)" category = "main" optional = false python-versions = ">=3.7" +[package.extras] +docs = ["olefile", "sphinx (>=2.4)", "sphinx-copybutton", "sphinx-issues (>=3.0.1)", "sphinx-removed-in", "sphinx-rtd-theme (>=1.0)", "sphinxext-opengraph"] +tests = ["check-manifest", "coverage", "defusedxml", "markdown2", "olefile", "packaging", "pyroma", "pytest", "pytest-cov", "pytest-timeout"] + [[package]] name = "platformdirs" version = "2.5.1" @@ -1146,11 +1175,11 @@ testing = ["pytest", "pytest-benchmark"] [[package]] name = "pre-commit" -version = "2.17.0" +version = "2.18.1" description = "A framework for managing and maintaining multi-language pre-commit hooks." category = "dev" optional = false -python-versions = ">=3.6.1" +python-versions = ">=3.7" [package.dependencies] cfgv = ">=2.0.0" @@ -1275,14 +1304,14 @@ test = ["flaky", "pretend", "pytest (>=3.0.1)"] [[package]] name = "pyparsing" -version = "3.0.7" -description = "Python parsing module" -category = "main" +version = "3.0.8" +description = "pyparsing module - Classes and methods to define and execute parsing grammars" +category = "dev" optional = false -python-versions = ">=3.6" +python-versions = ">=3.6.8" [package.extras] -diagrams = ["jinja2", "railroad-diagrams"] +diagrams = ["railroad-diagrams", "jinja2"] [[package]] name = "pyrsistent" @@ -1294,11 +1323,11 @@ python-versions = ">=3.7" [[package]] name = "pytest" -version = "7.0.1" +version = "7.1.1" description = "pytest: simple powerful testing with Python" category = "dev" optional = false -python-versions = ">=3.6" +python-versions = ">=3.7" [package.dependencies] atomicwrites = {version = ">=1.0", markers = "sys_platform == \"win32\""} @@ -1383,7 +1412,7 @@ six = ">=1.5" [[package]] name = "python-dotenv" -version = "0.19.2" +version = "0.20.0" description = "Read key-value pairs from a .env file and set them as environment variables" category = "dev" optional = false @@ -1409,7 +1438,7 @@ postgresql = ["psycopg2"] [[package]] name = "pytz" -version = "2021.3" +version = "2022.1" description = "World timezone definitions, modern and historical" category = "main" optional = false @@ -1417,11 +1446,11 @@ python-versions = "*" [[package]] name = "pyupgrade" -version = "2.31.0" +version = "2.32.0" description = "A tool to automatically upgrade syntax for newer versions." category = "dev" optional = false -python-versions = ">=3.6.1" +python-versions = ">=3.7" [package.dependencies] tokenize-rt = ">=3.2.0" @@ -1555,6 +1584,19 @@ category = "main" optional = false python-versions = ">=3.5" +[[package]] +name = "structlog" +version = "21.5.0" +description = "Structured Logging for Python" +category = "main" +optional = false +python-versions = ">=3.6" + +[package.extras] +dev = ["pre-commit", "rich", "cogapp", "tomli", "coverage", "freezegun (>=0.2.8)", "pretend", "pytest-asyncio", "pytest (>=6.0)", "simplejson", "furo", "sphinx", "sphinx-notfound-page", "sphinxcontrib-mermaid", "twisted"] +docs = ["furo", "sphinx", "sphinx-notfound-page", "sphinxcontrib-mermaid", "twisted"] +tests = ["coverage", "freezegun (>=0.2.8)", "pretend", "pytest-asyncio", "pytest (>=6.0)", "simplejson"] + [[package]] name = "texttable" version = "1.6.4" @@ -1617,7 +1659,7 @@ python-versions = ">=3.6" [[package]] name = "unidecode" -version = "1.3.3" +version = "1.3.4" description = "ASCII transliterations of Unicode text" category = "main" optional = false @@ -1633,14 +1675,14 @@ python-versions = ">=3.6" [[package]] name = "urllib3" -version = "1.26.8" +version = "1.26.9" description = "HTTP library with thread-safe connection pooling, file post, and more." category = "main" optional = false python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*, <4" [package.extras] -brotli = ["brotlipy (>=0.6.0)"] +brotli = ["brotlicffi (>=0.8.0)", "brotli (>=1.0.9)", "brotlipy (>=0.6.0)"] secure = ["pyOpenSSL (>=0.14)", "cryptography (>=1.3.4)", "idna (>=2.0.0)", "certifi", "ipaddress"] socks = ["PySocks (>=1.5.6,!=1.5.7,<2.0)"] @@ -1654,7 +1696,7 @@ python-versions = "*" [[package]] name = "virtualenv" -version = "20.13.3" +version = "20.14.1" description = "Virtual Python Environment builder" category = "dev" optional = false @@ -1672,7 +1714,7 @@ testing = ["coverage (>=4)", "coverage-enable-subprocess (>=1)", "flaky (>=3)", [[package]] name = "watchdog" -version = "2.1.6" +version = "2.1.7" description = "Filesystem events monitoring" category = "dev" optional = false @@ -1683,7 +1725,7 @@ watchmedo = ["PyYAML (>=3.10)"] [[package]] name = "webauthn" -version = "1.4.0" +version = "1.5.0" description = "Pythonic WebAuthn" category = "main" optional = false @@ -1725,7 +1767,7 @@ python-versions = "*" [[package]] name = "wrapt" -version = "1.13.3" +version = "1.14.0" description = "Module for decorators, wrappers and monkey patching." category = "main" optional = false @@ -1733,20 +1775,20 @@ python-versions = "!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,!=3.4.*,>=2.7" [[package]] name = "zipp" -version = "3.7.0" +version = "3.8.0" description = "Backport of pathlib-compatible object wrapper for zip files" category = "main" optional = false python-versions = ">=3.7" [package.extras] -docs = ["sphinx", "jaraco.packaging (>=8.2)", "rst.linker (>=1.9)"] -testing = ["pytest (>=6)", "pytest-checkdocs (>=2.4)", "pytest-flake8", "pytest-cov", "pytest-enabler (>=1.0.1)", "jaraco.itertools", "func-timeout", "pytest-black (>=0.3.7)", "pytest-mypy"] +docs = ["sphinx", "jaraco.packaging (>=9)", "rst.linker (>=1.9)"] +testing = ["pytest (>=6)", "pytest-checkdocs (>=2.4)", "pytest-flake8", "pytest-cov", "pytest-enabler (>=1.0.1)", "jaraco.itertools", "func-timeout", "pytest-black (>=0.3.7)", "pytest-mypy (>=0.9.1)"] [metadata] lock-version = "1.1" python-versions = "^3.9" -content-hash = "2dc27a998cf71be6f283ccf16394968092b5a04b2a960bba6d977daa5e1fefaa" +content-hash = "03d134d2c9719a3110f71c95150c20e0fcdb3878ec5d494b5bf1b088c7c112b7" [metadata.files] asgiref = [ @@ -1754,8 +1796,8 @@ asgiref = [ {file = "asgiref-3.5.0.tar.gz", hash = "sha256:2f8abc20f7248433085eda803936d98992f1343ddb022065779f37c5da0181d0"}, ] asn1crypto = [ - {file = "asn1crypto-1.4.0-py2.py3-none-any.whl", hash = "sha256:4bcdf33c861c7d40bdcd74d8e4dd7661aac320fcdf40b9a3f95b4ee12fde2fa8"}, - {file = "asn1crypto-1.4.0.tar.gz", hash = "sha256:f4f6e119474e58e04a2b1af817eb585b4fd72bdd89b998624712b5c99be7641c"}, + {file = "asn1crypto-1.5.1-py2.py3-none-any.whl", hash = "sha256:db4e40728b728508912cbb3d44f19ce188f218e9eba635821bb4b68564f8fd67"}, + {file = "asn1crypto-1.5.1.tar.gz", hash = "sha256:13ae38502be632115abf8a24cbe5f4da52e3b5231990aff31123c805306ccb9c"}, ] atomicwrites = [ {file = "atomicwrites-1.4.0-py2.py3-none-any.whl", hash = "sha256:6d1784dea7c0c8d4a5172b6c620f40b6e4cbfdf96d783691f2e1302a7b88e197"}, @@ -1778,33 +1820,33 @@ bcrypt = [ {file = "bcrypt-3.2.0.tar.gz", hash = "sha256:5b93c1726e50a93a033c36e5ca7fdcd29a5c7395af50a6892f5d9e7c6cfbfb29"}, ] black = [ - {file = "black-22.1.0-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:1297c63b9e1b96a3d0da2d85d11cd9bf8664251fd69ddac068b98dc4f34f73b6"}, - {file = "black-22.1.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:2ff96450d3ad9ea499fc4c60e425a1439c2120cbbc1ab959ff20f7c76ec7e866"}, - {file = "black-22.1.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:0e21e1f1efa65a50e3960edd068b6ae6d64ad6235bd8bfea116a03b21836af71"}, - {file = "black-22.1.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:e2f69158a7d120fd641d1fa9a921d898e20d52e44a74a6fbbcc570a62a6bc8ab"}, - {file = "black-22.1.0-cp310-cp310-win_amd64.whl", hash = "sha256:228b5ae2c8e3d6227e4bde5920d2fc66cc3400fde7bcc74f480cb07ef0b570d5"}, - {file = "black-22.1.0-cp36-cp36m-macosx_10_9_x86_64.whl", hash = "sha256:b1a5ed73ab4c482208d20434f700d514f66ffe2840f63a6252ecc43a9bc77e8a"}, - {file = "black-22.1.0-cp36-cp36m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:35944b7100af4a985abfcaa860b06af15590deb1f392f06c8683b4381e8eeaf0"}, - {file = "black-22.1.0-cp36-cp36m-win_amd64.whl", hash = "sha256:7835fee5238fc0a0baf6c9268fb816b5f5cd9b8793423a75e8cd663c48d073ba"}, - {file = "black-22.1.0-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:dae63f2dbf82882fa3b2a3c49c32bffe144970a573cd68d247af6560fc493ae1"}, - {file = "black-22.1.0-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:5fa1db02410b1924b6749c245ab38d30621564e658297484952f3d8a39fce7e8"}, - {file = "black-22.1.0-cp37-cp37m-win_amd64.whl", hash = "sha256:c8226f50b8c34a14608b848dc23a46e5d08397d009446353dad45e04af0c8e28"}, - {file = "black-22.1.0-cp38-cp38-macosx_10_9_universal2.whl", hash = "sha256:2d6f331c02f0f40aa51a22e479c8209d37fcd520c77721c034517d44eecf5912"}, - {file = "black-22.1.0-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:742ce9af3086e5bd07e58c8feb09dbb2b047b7f566eb5f5bc63fd455814979f3"}, - {file = "black-22.1.0-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:fdb8754b453fb15fad3f72cd9cad3e16776f0964d67cf30ebcbf10327a3777a3"}, - {file = "black-22.1.0-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f5660feab44c2e3cb24b2419b998846cbb01c23c7fe645fee45087efa3da2d61"}, - {file = "black-22.1.0-cp38-cp38-win_amd64.whl", hash = "sha256:6f2f01381f91c1efb1451998bd65a129b3ed6f64f79663a55fe0e9b74a5f81fd"}, - {file = "black-22.1.0-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:efbadd9b52c060a8fc3b9658744091cb33c31f830b3f074422ed27bad2b18e8f"}, - {file = "black-22.1.0-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:8871fcb4b447206904932b54b567923e5be802b9b19b744fdff092bd2f3118d0"}, - {file = "black-22.1.0-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:ccad888050f5393f0d6029deea2a33e5ae371fd182a697313bdbd835d3edaf9c"}, - {file = "black-22.1.0-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:07e5c049442d7ca1a2fc273c79d1aecbbf1bc858f62e8184abe1ad175c4f7cc2"}, - {file = "black-22.1.0-cp39-cp39-win_amd64.whl", hash = "sha256:373922fc66676133ddc3e754e4509196a8c392fec3f5ca4486673e685a421321"}, - {file = "black-22.1.0-py3-none-any.whl", hash = "sha256:3524739d76b6b3ed1132422bf9d82123cd1705086723bc3e235ca39fd21c667d"}, - {file = "black-22.1.0.tar.gz", hash = "sha256:a7c0192d35635f6fc1174be575cb7915e92e5dd629ee79fdaf0dcfa41a80afb5"}, + {file = "black-22.3.0-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:2497f9c2386572e28921fa8bec7be3e51de6801f7459dffd6e62492531c47e09"}, + {file = "black-22.3.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:5795a0375eb87bfe902e80e0c8cfaedf8af4d49694d69161e5bd3206c18618bb"}, + {file = "black-22.3.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:e3556168e2e5c49629f7b0f377070240bd5511e45e25a4497bb0073d9dda776a"}, + {file = "black-22.3.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:67c8301ec94e3bcc8906740fe071391bce40a862b7be0b86fb5382beefecd968"}, + {file = "black-22.3.0-cp310-cp310-win_amd64.whl", hash = "sha256:fd57160949179ec517d32ac2ac898b5f20d68ed1a9c977346efbac9c2f1e779d"}, + {file = "black-22.3.0-cp36-cp36m-macosx_10_9_x86_64.whl", hash = "sha256:cc1e1de68c8e5444e8f94c3670bb48a2beef0e91dddfd4fcc29595ebd90bb9ce"}, + {file = "black-22.3.0-cp36-cp36m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6d2fc92002d44746d3e7db7cf9313cf4452f43e9ea77a2c939defce3b10b5c82"}, + {file = "black-22.3.0-cp36-cp36m-win_amd64.whl", hash = "sha256:a6342964b43a99dbc72f72812bf88cad8f0217ae9acb47c0d4f141a6416d2d7b"}, + {file = "black-22.3.0-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:328efc0cc70ccb23429d6be184a15ce613f676bdfc85e5fe8ea2a9354b4e9015"}, + {file = "black-22.3.0-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:06f9d8846f2340dfac80ceb20200ea5d1b3f181dd0556b47af4e8e0b24fa0a6b"}, + {file = "black-22.3.0-cp37-cp37m-win_amd64.whl", hash = "sha256:ad4efa5fad66b903b4a5f96d91461d90b9507a812b3c5de657d544215bb7877a"}, + {file = "black-22.3.0-cp38-cp38-macosx_10_9_universal2.whl", hash = "sha256:e8477ec6bbfe0312c128e74644ac8a02ca06bcdb8982d4ee06f209be28cdf163"}, + {file = "black-22.3.0-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:637a4014c63fbf42a692d22b55d8ad6968a946b4a6ebc385c5505d9625b6a464"}, + {file = "black-22.3.0-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:863714200ada56cbc366dc9ae5291ceb936573155f8bf8e9de92aef51f3ad0f0"}, + {file = "black-22.3.0-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:10dbe6e6d2988049b4655b2b739f98785a884d4d6b85bc35133a8fb9a2233176"}, + {file = "black-22.3.0-cp38-cp38-win_amd64.whl", hash = "sha256:cee3e11161dde1b2a33a904b850b0899e0424cc331b7295f2a9698e79f9a69a0"}, + {file = "black-22.3.0-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:5891ef8abc06576985de8fa88e95ab70641de6c1fca97e2a15820a9b69e51b20"}, + {file = "black-22.3.0-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:30d78ba6bf080eeaf0b7b875d924b15cd46fec5fd044ddfbad38c8ea9171043a"}, + {file = "black-22.3.0-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:ee8f1f7228cce7dffc2b464f07ce769f478968bfb3dd1254a4c2eeed84928aad"}, + {file = "black-22.3.0-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6ee227b696ca60dd1c507be80a6bc849a5a6ab57ac7352aad1ffec9e8b805f21"}, + {file = "black-22.3.0-cp39-cp39-win_amd64.whl", hash = "sha256:9b542ced1ec0ceeff5b37d69838106a6348e60db7b8fdd245294dc1d26136265"}, + {file = "black-22.3.0-py3-none-any.whl", hash = "sha256:bc58025940a896d7e5356952228b68f793cf5fcb342be703c3a2669a1488cb72"}, + {file = "black-22.3.0.tar.gz", hash = "sha256:35020b8886c022ced9282b51b5a875b6d1ab0c387b31a065b84db7c33085ca79"}, ] bleach = [ - {file = "bleach-4.1.0-py2.py3-none-any.whl", hash = "sha256:4d2651ab93271d1129ac9cbc679f524565cc8a1b791909c4a51eac4446a15994"}, - {file = "bleach-4.1.0.tar.gz", hash = "sha256:0900d8b37eba61a802ee40ac0061f8c2b5dee29c1927dd1d233e075ebf5a71da"}, + {file = "bleach-5.0.0-py3-none-any.whl", hash = "sha256:08a1fe86d253b5c88c92cc3d810fd8048a16d15762e1e5b74d502256e5926aa1"}, + {file = "bleach-5.0.0.tar.gz", hash = "sha256:c6d6cc054bdc9c83b48b8083e236e5f00f238428666d2ce2e083eaa5fd568565"}, ] cbor2 = [ {file = "cbor2-5.4.2.post1-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:21a8778a92fae2fa713dfee2dc781fce64bc8fcb2e085368eff3a0b3434f83c7"}, @@ -1897,8 +1939,8 @@ charset-normalizer = [ {file = "charset_normalizer-2.0.12-py3-none-any.whl", hash = "sha256:6881edbebdb17b39b4eaaa821b438bf6eddffb4468cf344f09f89def34a8b1df"}, ] click = [ - {file = "click-8.0.4-py3-none-any.whl", hash = "sha256:6a7a62563bbfabfda3a38f3023a1db4a35978c0abd76f6c9605ecd6554d6d9b1"}, - {file = "click-8.0.4.tar.gz", hash = "sha256:8458d7b1287c5fb128c90e23381cf99dcde74beaf6c7ff6384ce84d6fe090adb"}, + {file = "click-8.1.2-py3-none-any.whl", hash = "sha256:24e1a4a9ec5bf6299411369b208c1df2188d9eb8d916302fe6bf03faed227f1e"}, + {file = "click-8.1.2.tar.gz", hash = "sha256:479707fe14d9ec9a0757618b7a100a0ae4c4e236fac5b7f80ca68028141a1a72"}, ] colorama = [ {file = "colorama-0.4.4-py2.py3-none-any.whl", hash = "sha256:9f47eda37229f68eee03b24b9748937c7dc3868f906e8ba69fbcbdd3bc5dc3e2"}, @@ -1956,26 +1998,26 @@ coverage = [ {file = "coverage-6.3.2.tar.gz", hash = "sha256:03e2a7826086b91ef345ff18742ee9fc47a6839ccd517061ef8fa1976e652ce9"}, ] cryptography = [ - {file = "cryptography-36.0.1-cp36-abi3-macosx_10_10_universal2.whl", hash = "sha256:73bc2d3f2444bcfeac67dd130ff2ea598ea5f20b40e36d19821b4df8c9c5037b"}, - {file = "cryptography-36.0.1-cp36-abi3-macosx_10_10_x86_64.whl", hash = "sha256:2d87cdcb378d3cfed944dac30596da1968f88fb96d7fc34fdae30a99054b2e31"}, - {file = "cryptography-36.0.1-cp36-abi3-manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:74d6c7e80609c0f4c2434b97b80c7f8fdfaa072ca4baab7e239a15d6d70ed73a"}, - {file = "cryptography-36.0.1-cp36-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.manylinux_2_24_aarch64.whl", hash = "sha256:6c0c021f35b421ebf5976abf2daacc47e235f8b6082d3396a2fe3ccd537ab173"}, - {file = "cryptography-36.0.1-cp36-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:5d59a9d55027a8b88fd9fd2826c4392bd487d74bf628bb9d39beecc62a644c12"}, - {file = "cryptography-36.0.1-cp36-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:0a817b961b46894c5ca8a66b599c745b9a3d9f822725221f0e0fe49dc043a3a3"}, - {file = "cryptography-36.0.1-cp36-abi3-manylinux_2_24_x86_64.whl", hash = "sha256:94ae132f0e40fe48f310bba63f477f14a43116f05ddb69d6fa31e93f05848ae2"}, - {file = "cryptography-36.0.1-cp36-abi3-musllinux_1_1_aarch64.whl", hash = "sha256:7be0eec337359c155df191d6ae00a5e8bbb63933883f4f5dffc439dac5348c3f"}, - {file = "cryptography-36.0.1-cp36-abi3-musllinux_1_1_x86_64.whl", hash = "sha256:e0344c14c9cb89e76eb6a060e67980c9e35b3f36691e15e1b7a9e58a0a6c6dc3"}, - {file = "cryptography-36.0.1-cp36-abi3-win32.whl", hash = "sha256:4caa4b893d8fad33cf1964d3e51842cd78ba87401ab1d2e44556826df849a8ca"}, - {file = "cryptography-36.0.1-cp36-abi3-win_amd64.whl", hash = "sha256:391432971a66cfaf94b21c24ab465a4cc3e8bf4a939c1ca5c3e3a6e0abebdbcf"}, - {file = "cryptography-36.0.1-pp37-pypy37_pp73-manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:bb5829d027ff82aa872d76158919045a7c1e91fbf241aec32cb07956e9ebd3c9"}, - {file = "cryptography-36.0.1-pp37-pypy37_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ebc15b1c22e55c4d5566e3ca4db8689470a0ca2babef8e3a9ee057a8b82ce4b1"}, - {file = "cryptography-36.0.1-pp37-pypy37_pp73-manylinux_2_24_x86_64.whl", hash = "sha256:596f3cd67e1b950bc372c33f1a28a0692080625592ea6392987dba7f09f17a94"}, - {file = "cryptography-36.0.1-pp38-pypy38_pp73-macosx_10_10_x86_64.whl", hash = "sha256:30ee1eb3ebe1644d1c3f183d115a8c04e4e603ed6ce8e394ed39eea4a98469ac"}, - {file = "cryptography-36.0.1-pp38-pypy38_pp73-manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:ec63da4e7e4a5f924b90af42eddf20b698a70e58d86a72d943857c4c6045b3ee"}, - {file = "cryptography-36.0.1-pp38-pypy38_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ca238ceb7ba0bdf6ce88c1b74a87bffcee5afbfa1e41e173b1ceb095b39add46"}, - {file = "cryptography-36.0.1-pp38-pypy38_pp73-manylinux_2_24_x86_64.whl", hash = "sha256:ca28641954f767f9822c24e927ad894d45d5a1e501767599647259cbf030b903"}, - {file = "cryptography-36.0.1-pp38-pypy38_pp73-win_amd64.whl", hash = "sha256:39bdf8e70eee6b1c7b289ec6e5d84d49a6bfa11f8b8646b5b3dfe41219153316"}, - {file = "cryptography-36.0.1.tar.gz", hash = "sha256:53e5c1dc3d7a953de055d77bef2ff607ceef7a2aac0353b5d630ab67f7423638"}, + {file = "cryptography-36.0.2-cp36-abi3-macosx_10_10_universal2.whl", hash = "sha256:4e2dddd38a5ba733be6a025a1475a9f45e4e41139d1321f412c6b360b19070b6"}, + {file = "cryptography-36.0.2-cp36-abi3-macosx_10_10_x86_64.whl", hash = "sha256:4881d09298cd0b669bb15b9cfe6166f16fc1277b4ed0d04a22f3d6430cb30f1d"}, + {file = "cryptography-36.0.2-cp36-abi3-manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:ea634401ca02367c1567f012317502ef3437522e2fc44a3ea1844de028fa4b84"}, + {file = "cryptography-36.0.2-cp36-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.manylinux_2_24_aarch64.whl", hash = "sha256:7be666cc4599b415f320839e36367b273db8501127b38316f3b9f22f17a0b815"}, + {file = "cryptography-36.0.2-cp36-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:8241cac0aae90b82d6b5c443b853723bcc66963970c67e56e71a2609dc4b5eaf"}, + {file = "cryptography-36.0.2-cp36-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:7b2d54e787a884ffc6e187262823b6feb06c338084bbe80d45166a1cb1c6c5bf"}, + {file = "cryptography-36.0.2-cp36-abi3-manylinux_2_24_x86_64.whl", hash = "sha256:c2c5250ff0d36fd58550252f54915776940e4e866f38f3a7866d92b32a654b86"}, + {file = "cryptography-36.0.2-cp36-abi3-musllinux_1_1_aarch64.whl", hash = "sha256:ec6597aa85ce03f3e507566b8bcdf9da2227ec86c4266bd5e6ab4d9e0cc8dab2"}, + {file = "cryptography-36.0.2-cp36-abi3-musllinux_1_1_x86_64.whl", hash = "sha256:ca9f686517ec2c4a4ce930207f75c00bf03d94e5063cbc00a1dc42531511b7eb"}, + {file = "cryptography-36.0.2-cp36-abi3-win32.whl", hash = "sha256:f64b232348ee82f13aac22856515ce0195837f6968aeaa94a3d0353ea2ec06a6"}, + {file = "cryptography-36.0.2-cp36-abi3-win_amd64.whl", hash = "sha256:53e0285b49fd0ab6e604f4c5d9c5ddd98de77018542e88366923f152dbeb3c29"}, + {file = "cryptography-36.0.2-pp37-pypy37_pp73-manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:32db5cc49c73f39aac27574522cecd0a4bb7384e71198bc65a0d23f901e89bb7"}, + {file = "cryptography-36.0.2-pp37-pypy37_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:d2b3d199647468d410994dbeb8cec5816fb74feb9368aedf300af709ef507e3e"}, + {file = "cryptography-36.0.2-pp37-pypy37_pp73-manylinux_2_24_x86_64.whl", hash = "sha256:da73d095f8590ad437cd5e9faf6628a218aa7c387e1fdf67b888b47ba56a17f0"}, + {file = "cryptography-36.0.2-pp38-pypy38_pp73-macosx_10_10_x86_64.whl", hash = "sha256:0a3bf09bb0b7a2c93ce7b98cb107e9170a90c51a0162a20af1c61c765b90e60b"}, + {file = "cryptography-36.0.2-pp38-pypy38_pp73-manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:8897b7b7ec077c819187a123174b645eb680c13df68354ed99f9b40a50898f77"}, + {file = "cryptography-36.0.2-pp38-pypy38_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:82740818f2f240a5da8dfb8943b360e4f24022b093207160c77cadade47d7c85"}, + {file = "cryptography-36.0.2-pp38-pypy38_pp73-manylinux_2_24_x86_64.whl", hash = "sha256:1f64a62b3b75e4005df19d3b5235abd43fa6358d5516cfc43d87aeba8d08dd51"}, + {file = "cryptography-36.0.2-pp38-pypy38_pp73-win_amd64.whl", hash = "sha256:e167b6b710c7f7bc54e67ef593f8731e1f45aa35f8a8a7b72d6e42ec76afd4b3"}, + {file = "cryptography-36.0.2.tar.gz", hash = "sha256:70f8f4f7bb2ac9f340655cbac89d68c527af5bb4387522a8413e841e3e6628c9"}, ] decorator = [ {file = "decorator-4.4.2-py2.py3-none-any.whl", hash = "sha256:41fa54c2a0cc4ba648be4fd43cff00aedf5b9465c9bf18d64325bc225f08f760"}, @@ -1998,14 +2040,14 @@ distro = [ {file = "distro-1.7.0.tar.gz", hash = "sha256:151aeccf60c216402932b52e40ee477a939f8d58898927378a02abbe852c1c39"}, ] django = [ - {file = "Django-3.2.12-py3-none-any.whl", hash = "sha256:9b06c289f9ba3a8abea16c9c9505f25107809fb933676f6c891ded270039d965"}, - {file = "Django-3.2.12.tar.gz", hash = "sha256:9772e6935703e59e993960832d66a614cf0233a1c5123bc6224ecc6ad69e41e2"}, + {file = "Django-3.2.13-py3-none-any.whl", hash = "sha256:b896ca61edc079eb6bbaa15cf6071eb69d6aac08cce5211583cfb41515644fdf"}, + {file = "Django-3.2.13.tar.gz", hash = "sha256:6d93497a0a9bf6ba0e0b1a29cccdc40efbfc76297255b1309b3a884a688ec4b6"}, ] django-allauth = [ - {file = "django-allauth-0.49.0.tar.gz", hash = "sha256:f5fbb67376177c6a9276516dde98bcb01ac4160a5a27f7b340914dd521d04f12"}, + {file = "django-allauth-0.50.0.tar.gz", hash = "sha256:ee3a174e249771caeb1d037e64b2704dd3c56cfec44f2058fae2214b224d35e8"}, ] django-autocomplete-light = [ - {file = "django-autocomplete-light-3.9.1.tar.gz", hash = "sha256:50bc562fe13c206cf53304d7f6a8b929729016ab1dae64d6176d4ccd6caff6ab"}, + {file = "django-autocomplete-light-3.9.4.tar.gz", hash = "sha256:0f6da75c1c7186698b867a467a8cdb359f0513fdd8e09288a0c2fb018ae3d94e"}, ] django-bootstrap3 = [ {file = "django-bootstrap3-21.2.tar.gz", hash = "sha256:9a7e95a053455c15cdcbc5dc7f9261f9aecf01d0558435b59e7b364765f2c3e2"}, @@ -2047,8 +2089,8 @@ django-grappelli = [ {file = "django_grappelli-3.0.3-py2.py3-none-any.whl", hash = "sha256:a98883407f995896f4c4d108ef3ef6cfc00e581211738c5a13c536950a95bcc8"}, ] django-handleref = [ - {file = "django-handleref-1.0.1.tar.gz", hash = "sha256:f822d98a896cfae6a8d3e70448b68e6ad0da0a2a6536d0d37b61342789d25054"}, - {file = "django_handleref-1.0.1-py3-none-any.whl", hash = "sha256:bc787708e57feed668822ca5ae12529db1eb83d41fa3299c8c1e3ea9a2fdb47e"}, + {file = "django-handleref-1.0.2.tar.gz", hash = "sha256:7e23d4697cece9b85d081aee4cff69c12273da0bb9f9c1dbcc5480064c825e31"}, + {file = "django_handleref-1.0.2-py3-none-any.whl", hash = "sha256:c6128fcf7e1863d5135e319947fcc8f113bb7a20026341fa5f106cb25ce93055"}, ] django-hashers-passlib = [ {file = "django-hashers-passlib-0.4.tar.gz", hash = "sha256:c8f937cf4a9a21957e28735d1ffd8df242dff863c2e4b92665d98509cd6ae0c4"}, @@ -2062,6 +2104,10 @@ django-inet = [ {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-ipware = [ + {file = "django-ipware-4.0.2.tar.gz", hash = "sha256:602a58325a4808bd19197fef2676a0b2da2df40d0ecf21be414b2ff48c72ad05"}, + {file = "django_ipware-4.0.2-py2.py3-none-any.whl", hash = "sha256:878dbb06a87e25550798e9ef3204ed70a200dd8b15e47dcef848cf08244f04c9"}, +] django-oauth-toolkit = [ {file = "django-oauth-toolkit-1.6.1.tar.gz", hash = "sha256:529acda23541ededac8c6a16f15ae767f94e503a22cb96c1e300aa4ff274ad50"}, {file = "django_oauth_toolkit-1.6.1-py3-none-any.whl", hash = "sha256:d206132ac272fdcf9d916df2e64d2df3ffb769eaad545f4010647c9c0a1deb01"}, @@ -2071,8 +2117,8 @@ django-otp = [ {file = "django_otp-1.1.3-py3-none-any.whl", hash = "sha256:8637be826c0465d0fd1710e4472efe9fc83883853a2141fefdbace9358d20003"}, ] django-peeringdb = [ - {file = "django-peeringdb-2.12.0.tar.gz", hash = "sha256:d2e2f898cb00bdfe85d69a6a15f63eaa48457d39ed12c2d0b517dbc8477799e3"}, - {file = "django_peeringdb-2.12.0-py3-none-any.whl", hash = "sha256:b5d65df4d1f1ec45795fed30a5dac0c79ef7a753e53b0ee7da6b43bf95ac28c5"}, + {file = "django-peeringdb-2.13.0.tar.gz", hash = "sha256:78c8aca930afb8a70847ad5fc3778b2eab4b1100e445fc1ded6f02cc96e13b28"}, + {file = "django_peeringdb-2.13.0-py3-none-any.whl", hash = "sha256:816d5aff0bc3d73eaf859d98586ce275d0ae63717f3c5843f97898a4354bf813"}, ] django-phonenumber-field = [ {file = "django-phonenumber-field-6.1.0.tar.gz", hash = "sha256:b1ff950f90a8911ff323ccf77c8f6fe4299a9f671fa61c8734a6994359f07446"}, @@ -2101,6 +2147,10 @@ django-simple-captcha = [ {file = "django-simple-captcha-0.5.17.tar.gz", hash = "sha256:9649e66dab4e71efacbfef02f48b83b91684898352a1ab56f1686ce71033b328"}, {file = "django_simple_captcha-0.5.17-py2.py3-none-any.whl", hash = "sha256:f9a07e5e9de264ba4039c9eaad66bc48188a21ceda5fcdc2fa13c5512141c2c9"}, ] +django-structlog = [ + {file = "django-structlog-2.2.0.tar.gz", hash = "sha256:0443d39d27bf34258a375a2dfd1a0ae51f24bedce88beae55c1b69f3c09af922"}, + {file = "django_structlog-2.2.0-py3-none-any.whl", hash = "sha256:d6eb180963c7baf381ad7035e9afc7ecb7031246cb640f16925fe72f7712a2a1"}, +] django-tables2 = [ {file = "django-tables2-2.4.1.tar.gz", hash = "sha256:6c72dd208358539e789e4c0efd7d151e43283a4aa4093a35f44c43489e7ddeaa"}, {file = "django_tables2-2.4.1-py2.py3-none-any.whl", hash = "sha256:50762bf3d7c61a4eb70e763c3e278650d7266bb78d0497fc8fafcf4e507c9a64"}, @@ -2157,16 +2207,16 @@ grainy = [ {file = "grainy-1.8.1.tar.gz", hash = "sha256:2cfd8d50b3f5cce3c463f3c5e86324442f61a7cd46dfe7b134ee926559e56556"}, ] identify = [ - {file = "identify-2.4.11-py2.py3-none-any.whl", hash = "sha256:fd906823ed1db23c7a48f9b176a1d71cb8abede1e21ebe614bac7bdd688d9213"}, - {file = "identify-2.4.11.tar.gz", hash = "sha256:2986942d3974c8f2e5019a190523b0b0e2a07cb8e89bf236727fb4b26f27f8fd"}, + {file = "identify-2.4.12-py2.py3-none-any.whl", hash = "sha256:5f06b14366bd1facb88b00540a1de05b69b310cbc2654db3c7e07fa3a4339323"}, + {file = "identify-2.4.12.tar.gz", hash = "sha256:3f3244a559290e7d3deb9e9adc7b33594c1bc85a9dd82e0f1be519bf12a1ec17"}, ] idna = [ {file = "idna-3.3-py3-none-any.whl", hash = "sha256:84d9dd047ffa80596e0f246e2eab0b391788b0503584e8945f2368256d2735ff"}, {file = "idna-3.3.tar.gz", hash = "sha256:9d643ff0a55b762d5cdb124b8eaa99c66322e2157b69160bc32796e824360e6d"}, ] importlib-metadata = [ - {file = "importlib_metadata-4.11.2-py3-none-any.whl", hash = "sha256:d16e8c1deb60de41b8e8ed21c1a7b947b0bc62fab7e1d470bcdf331cea2e6735"}, - {file = "importlib_metadata-4.11.2.tar.gz", hash = "sha256:b36ffa925fe3139b2f6ff11d6925ffd4fa7bc47870165e3ac260ac7b4f91e6ac"}, + {file = "importlib_metadata-4.11.3-py3-none-any.whl", hash = "sha256:1208431ca90a8cca1a6b8af391bb53c1a2db74e5d1cef6ddced95d4b2062edc6"}, + {file = "importlib_metadata-4.11.3.tar.gz", hash = "sha256:ea4c597ebf37142f827b8f39299579e31685c31d3a438b59f469406afd0f2539"}, ] iniconfig = [ {file = "iniconfig-1.1.1-py2.py3-none-any.whl", hash = "sha256:011e24c64b7f47f6ebd835bb12a743f2fbe9a26d4cecaa7f53bc4f35ee9da8b3"}, @@ -2181,8 +2231,8 @@ itypes = [ {file = "itypes-1.2.0.tar.gz", hash = "sha256:af886f129dea4a2a1e3d36595a2d139589e4dd287f5cab0b40e799ee81570ff1"}, ] jinja2 = [ - {file = "Jinja2-3.0.3-py3-none-any.whl", hash = "sha256:077ce6014f7b40d03b47d1f1ca4b0fc8328a692bd284016f806ed0eaca390ad8"}, - {file = "Jinja2-3.0.3.tar.gz", hash = "sha256:611bb273cd68f3b993fabdc4064fc858c5b47a973cb5aa7999ec1ba405c87cd7"}, + {file = "Jinja2-3.1.1-py3-none-any.whl", hash = "sha256:539835f51a74a69f41b848a9645dbdc35b4f20a3b601e2d9a7e22947b15ff119"}, + {file = "Jinja2-3.1.1.tar.gz", hash = "sha256:640bed4bb501cbd17194b3cace1dc2126f5b619cf068a726b98192a0fde74ae9"}, ] jsonschema = [ {file = "jsonschema-3.2.0-py2.py3-none-any.whl", hash = "sha256:4e5b3cf8216f577bee9ce139cbe72eca3ea4f292ec60928ff24758ce626cd163"}, @@ -2200,46 +2250,46 @@ markdown-include = [ {file = "markdown-include-0.6.0.tar.gz", hash = "sha256:6f5d680e36f7780c7f0f61dca53ca581bd50d1b56137ddcd6353efafa0c3e4a2"}, ] markupsafe = [ - {file = "MarkupSafe-2.1.0-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:3028252424c72b2602a323f70fbf50aa80a5d3aa616ea6add4ba21ae9cc9da4c"}, - {file = "MarkupSafe-2.1.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:290b02bab3c9e216da57c1d11d2ba73a9f73a614bbdcc027d299a60cdfabb11a"}, - {file = "MarkupSafe-2.1.0-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:6e104c0c2b4cd765b4e83909cde7ec61a1e313f8a75775897db321450e928cce"}, - {file = "MarkupSafe-2.1.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:24c3be29abb6b34052fd26fc7a8e0a49b1ee9d282e3665e8ad09a0a68faee5b3"}, - {file = "MarkupSafe-2.1.0-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:204730fd5fe2fe3b1e9ccadb2bd18ba8712b111dcabce185af0b3b5285a7c989"}, - {file = "MarkupSafe-2.1.0-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:d3b64c65328cb4cd252c94f83e66e3d7acf8891e60ebf588d7b493a55a1dbf26"}, - {file = "MarkupSafe-2.1.0-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:96de1932237abe0a13ba68b63e94113678c379dca45afa040a17b6e1ad7ed076"}, - {file = "MarkupSafe-2.1.0-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:75bb36f134883fdbe13d8e63b8675f5f12b80bb6627f7714c7d6c5becf22719f"}, - {file = "MarkupSafe-2.1.0-cp310-cp310-win32.whl", hash = "sha256:4056f752015dfa9828dce3140dbadd543b555afb3252507348c493def166d454"}, - {file = "MarkupSafe-2.1.0-cp310-cp310-win_amd64.whl", hash = "sha256:d4e702eea4a2903441f2735799d217f4ac1b55f7d8ad96ab7d4e25417cb0827c"}, - {file = "MarkupSafe-2.1.0-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:f0eddfcabd6936558ec020130f932d479930581171368fd728efcfb6ef0dd357"}, - {file = "MarkupSafe-2.1.0-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:5ddea4c352a488b5e1069069f2f501006b1a4362cb906bee9a193ef1245a7a61"}, - {file = "MarkupSafe-2.1.0-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:09c86c9643cceb1d87ca08cdc30160d1b7ab49a8a21564868921959bd16441b8"}, - {file = "MarkupSafe-2.1.0-cp37-cp37m-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:a0a0abef2ca47b33fb615b491ce31b055ef2430de52c5b3fb19a4042dbc5cadb"}, - {file = "MarkupSafe-2.1.0-cp37-cp37m-musllinux_1_1_aarch64.whl", hash = "sha256:736895a020e31b428b3382a7887bfea96102c529530299f426bf2e636aacec9e"}, - {file = "MarkupSafe-2.1.0-cp37-cp37m-musllinux_1_1_i686.whl", hash = "sha256:679cbb78914ab212c49c67ba2c7396dc599a8479de51b9a87b174700abd9ea49"}, - {file = "MarkupSafe-2.1.0-cp37-cp37m-musllinux_1_1_x86_64.whl", hash = "sha256:84ad5e29bf8bab3ad70fd707d3c05524862bddc54dc040982b0dbcff36481de7"}, - {file = "MarkupSafe-2.1.0-cp37-cp37m-win32.whl", hash = "sha256:8da5924cb1f9064589767b0f3fc39d03e3d0fb5aa29e0cb21d43106519bd624a"}, - {file = "MarkupSafe-2.1.0-cp37-cp37m-win_amd64.whl", hash = "sha256:454ffc1cbb75227d15667c09f164a0099159da0c1f3d2636aa648f12675491ad"}, - {file = "MarkupSafe-2.1.0-cp38-cp38-macosx_10_9_universal2.whl", hash = "sha256:142119fb14a1ef6d758912b25c4e803c3ff66920635c44078666fe7cc3f8f759"}, - {file = "MarkupSafe-2.1.0-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:b2a5a856019d2833c56a3dcac1b80fe795c95f401818ea963594b345929dffa7"}, - {file = "MarkupSafe-2.1.0-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:1d1fb9b2eec3c9714dd936860850300b51dbaa37404209c8d4cb66547884b7ed"}, - {file = "MarkupSafe-2.1.0-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:62c0285e91414f5c8f621a17b69fc0088394ccdaa961ef469e833dbff64bd5ea"}, - {file = "MarkupSafe-2.1.0-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:fc3150f85e2dbcf99e65238c842d1cfe69d3e7649b19864c1cc043213d9cd730"}, - {file = "MarkupSafe-2.1.0-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:f02cf7221d5cd915d7fa58ab64f7ee6dd0f6cddbb48683debf5d04ae9b1c2cc1"}, - {file = "MarkupSafe-2.1.0-cp38-cp38-musllinux_1_1_i686.whl", hash = "sha256:d5653619b3eb5cbd35bfba3c12d575db2a74d15e0e1c08bf1db788069d410ce8"}, - {file = "MarkupSafe-2.1.0-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:7d2f5d97fcbd004c03df8d8fe2b973fe2b14e7bfeb2cfa012eaa8759ce9a762f"}, - {file = "MarkupSafe-2.1.0-cp38-cp38-win32.whl", hash = "sha256:3cace1837bc84e63b3fd2dfce37f08f8c18aeb81ef5cf6bb9b51f625cb4e6cd8"}, - {file = "MarkupSafe-2.1.0-cp38-cp38-win_amd64.whl", hash = "sha256:fabbe18087c3d33c5824cb145ffca52eccd053061df1d79d4b66dafa5ad2a5ea"}, - {file = "MarkupSafe-2.1.0-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:023af8c54fe63530545f70dd2a2a7eed18d07a9a77b94e8bf1e2ff7f252db9a3"}, - {file = "MarkupSafe-2.1.0-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:d66624f04de4af8bbf1c7f21cc06649c1c69a7f84109179add573ce35e46d448"}, - {file = "MarkupSafe-2.1.0-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:c532d5ab79be0199fa2658e24a02fce8542df196e60665dd322409a03db6a52c"}, - {file = "MarkupSafe-2.1.0-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:e67ec74fada3841b8c5f4c4f197bea916025cb9aa3fe5abf7d52b655d042f956"}, - {file = "MarkupSafe-2.1.0-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:30c653fde75a6e5eb814d2a0a89378f83d1d3f502ab710904ee585c38888816c"}, - {file = "MarkupSafe-2.1.0-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:961eb86e5be7d0973789f30ebcf6caab60b844203f4396ece27310295a6082c7"}, - {file = "MarkupSafe-2.1.0-cp39-cp39-musllinux_1_1_i686.whl", hash = "sha256:598b65d74615c021423bd45c2bc5e9b59539c875a9bdb7e5f2a6b92dfcfc268d"}, - {file = "MarkupSafe-2.1.0-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:599941da468f2cf22bf90a84f6e2a65524e87be2fce844f96f2dd9a6c9d1e635"}, - {file = "MarkupSafe-2.1.0-cp39-cp39-win32.whl", hash = "sha256:e6f7f3f41faffaea6596da86ecc2389672fa949bd035251eab26dc6697451d05"}, - {file = "MarkupSafe-2.1.0-cp39-cp39-win_amd64.whl", hash = "sha256:b8811d48078d1cf2a6863dafb896e68406c5f513048451cd2ded0473133473c7"}, - {file = "MarkupSafe-2.1.0.tar.gz", hash = "sha256:80beaf63ddfbc64a0452b841d8036ca0611e049650e20afcb882f5d3c266d65f"}, + {file = "MarkupSafe-2.1.1-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:86b1f75c4e7c2ac2ccdaec2b9022845dbb81880ca318bb7a0a01fbf7813e3812"}, + {file = "MarkupSafe-2.1.1-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:f121a1420d4e173a5d96e47e9a0c0dcff965afdf1626d28de1460815f7c4ee7a"}, + {file = "MarkupSafe-2.1.1-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a49907dd8420c5685cfa064a1335b6754b74541bbb3706c259c02ed65b644b3e"}, + {file = "MarkupSafe-2.1.1-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:10c1bfff05d95783da83491be968e8fe789263689c02724e0c691933c52994f5"}, + {file = "MarkupSafe-2.1.1-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:b7bd98b796e2b6553da7225aeb61f447f80a1ca64f41d83612e6139ca5213aa4"}, + {file = "MarkupSafe-2.1.1-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:b09bf97215625a311f669476f44b8b318b075847b49316d3e28c08e41a7a573f"}, + {file = "MarkupSafe-2.1.1-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:694deca8d702d5db21ec83983ce0bb4b26a578e71fbdbd4fdcd387daa90e4d5e"}, + {file = "MarkupSafe-2.1.1-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:efc1913fd2ca4f334418481c7e595c00aad186563bbc1ec76067848c7ca0a933"}, + {file = "MarkupSafe-2.1.1-cp310-cp310-win32.whl", hash = "sha256:4a33dea2b688b3190ee12bd7cfa29d39c9ed176bda40bfa11099a3ce5d3a7ac6"}, + {file = "MarkupSafe-2.1.1-cp310-cp310-win_amd64.whl", hash = "sha256:dda30ba7e87fbbb7eab1ec9f58678558fd9a6b8b853530e176eabd064da81417"}, + {file = "MarkupSafe-2.1.1-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:671cd1187ed5e62818414afe79ed29da836dde67166a9fac6d435873c44fdd02"}, + {file = "MarkupSafe-2.1.1-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:3799351e2336dc91ea70b034983ee71cf2f9533cdff7c14c90ea126bfd95d65a"}, + {file = "MarkupSafe-2.1.1-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:e72591e9ecd94d7feb70c1cbd7be7b3ebea3f548870aa91e2732960fa4d57a37"}, + {file = "MarkupSafe-2.1.1-cp37-cp37m-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:6fbf47b5d3728c6aea2abb0589b5d30459e369baa772e0f37a0320185e87c980"}, + {file = "MarkupSafe-2.1.1-cp37-cp37m-musllinux_1_1_aarch64.whl", hash = "sha256:d5ee4f386140395a2c818d149221149c54849dfcfcb9f1debfe07a8b8bd63f9a"}, + {file = "MarkupSafe-2.1.1-cp37-cp37m-musllinux_1_1_i686.whl", hash = "sha256:bcb3ed405ed3222f9904899563d6fc492ff75cce56cba05e32eff40e6acbeaa3"}, + {file = "MarkupSafe-2.1.1-cp37-cp37m-musllinux_1_1_x86_64.whl", hash = "sha256:e1c0b87e09fa55a220f058d1d49d3fb8df88fbfab58558f1198e08c1e1de842a"}, + {file = "MarkupSafe-2.1.1-cp37-cp37m-win32.whl", hash = "sha256:8dc1c72a69aa7e082593c4a203dcf94ddb74bb5c8a731e4e1eb68d031e8498ff"}, + {file = "MarkupSafe-2.1.1-cp37-cp37m-win_amd64.whl", hash = "sha256:97a68e6ada378df82bc9f16b800ab77cbf4b2fada0081794318520138c088e4a"}, + {file = "MarkupSafe-2.1.1-cp38-cp38-macosx_10_9_universal2.whl", hash = "sha256:e8c843bbcda3a2f1e3c2ab25913c80a3c5376cd00c6e8c4a86a89a28c8dc5452"}, + {file = "MarkupSafe-2.1.1-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:0212a68688482dc52b2d45013df70d169f542b7394fc744c02a57374a4207003"}, + {file = "MarkupSafe-2.1.1-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:8e576a51ad59e4bfaac456023a78f6b5e6e7651dcd383bcc3e18d06f9b55d6d1"}, + {file = "MarkupSafe-2.1.1-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:4b9fe39a2ccc108a4accc2676e77da025ce383c108593d65cc909add5c3bd601"}, + {file = "MarkupSafe-2.1.1-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:96e37a3dc86e80bf81758c152fe66dbf60ed5eca3d26305edf01892257049925"}, + {file = "MarkupSafe-2.1.1-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:6d0072fea50feec76a4c418096652f2c3238eaa014b2f94aeb1d56a66b41403f"}, + {file = "MarkupSafe-2.1.1-cp38-cp38-musllinux_1_1_i686.whl", hash = "sha256:089cf3dbf0cd6c100f02945abeb18484bd1ee57a079aefd52cffd17fba910b88"}, + {file = "MarkupSafe-2.1.1-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:6a074d34ee7a5ce3effbc526b7083ec9731bb3cbf921bbe1d3005d4d2bdb3a63"}, + {file = "MarkupSafe-2.1.1-cp38-cp38-win32.whl", hash = "sha256:421be9fbf0ffe9ffd7a378aafebbf6f4602d564d34be190fc19a193232fd12b1"}, + {file = "MarkupSafe-2.1.1-cp38-cp38-win_amd64.whl", hash = "sha256:fc7b548b17d238737688817ab67deebb30e8073c95749d55538ed473130ec0c7"}, + {file = "MarkupSafe-2.1.1-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:e04e26803c9c3851c931eac40c695602c6295b8d432cbe78609649ad9bd2da8a"}, + {file = "MarkupSafe-2.1.1-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:b87db4360013327109564f0e591bd2a3b318547bcef31b468a92ee504d07ae4f"}, + {file = "MarkupSafe-2.1.1-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:99a2a507ed3ac881b975a2976d59f38c19386d128e7a9a18b7df6fff1fd4c1d6"}, + {file = "MarkupSafe-2.1.1-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:56442863ed2b06d19c37f94d999035e15ee982988920e12a5b4ba29b62ad1f77"}, + {file = "MarkupSafe-2.1.1-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:3ce11ee3f23f79dbd06fb3d63e2f6af7b12db1d46932fe7bd8afa259a5996603"}, + {file = "MarkupSafe-2.1.1-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:33b74d289bd2f5e527beadcaa3f401e0df0a89927c1559c8566c066fa4248ab7"}, + {file = "MarkupSafe-2.1.1-cp39-cp39-musllinux_1_1_i686.whl", hash = "sha256:43093fb83d8343aac0b1baa75516da6092f58f41200907ef92448ecab8825135"}, + {file = "MarkupSafe-2.1.1-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:8e3dcf21f367459434c18e71b2a9532d96547aef8a871872a5bd69a715c15f96"}, + {file = "MarkupSafe-2.1.1-cp39-cp39-win32.whl", hash = "sha256:d4306c36ca495956b6d568d276ac11fdd9c30a36f1b6eb928070dc5360b22e1c"}, + {file = "MarkupSafe-2.1.1-cp39-cp39-win_amd64.whl", hash = "sha256:46d00d6cfecdde84d40e572d63735ef81423ad31184100411e6e3388d405e247"}, + {file = "MarkupSafe-2.1.1.tar.gz", hash = "sha256:7f91197cc9e48f989d12e4e6fbc46495c446636dfc81b9ccf50bb0ec74b91d4b"}, ] mccabe = [ {file = "mccabe-0.6.1-py2.py3-none-any.whl", hash = "sha256:ab8a6258860da4b6677da4bd2fe5dc2c659cff31b3ee4f7f5d64e79735b80d42"}, @@ -2250,8 +2300,8 @@ mergedeep = [ {file = "mergedeep-1.3.4.tar.gz", hash = "sha256:0096d52e9dad9939c3d975a774666af186eda617e6ca84df4c94dec30004f2a8"}, ] mkdocs = [ - {file = "mkdocs-1.2.3-py3-none-any.whl", hash = "sha256:a1fa8c2d0c1305d7fc2b9d9f607c71778572a8b110fb26642aa00296c9e6d072"}, - {file = "mkdocs-1.2.3.tar.gz", hash = "sha256:89f5a094764381cda656af4298727c9f53dc3e602983087e1fe96ea1df24f4c1"}, + {file = "mkdocs-1.3.0-py3-none-any.whl", hash = "sha256:26bd2b03d739ac57a3e6eed0b7bcc86168703b719c27b99ad6ca91dc439aacde"}, + {file = "mkdocs-1.3.0.tar.gz", hash = "sha256:b504405b04da38795fec9b2e5e28f6aa3a73bb0960cb6d5d27ead28952bd35ea"}, ] munge = [ {file = "munge-1.2.1-py3-none-any.whl", hash = "sha256:65fb76389b54970beeef6a80b9e1b45cb230d34743cf11565af926ebc8af2627"}, @@ -2284,8 +2334,8 @@ packaging = [ {file = "packaging-21.3.tar.gz", hash = "sha256:dd47c42927d89ab911e606518907cc2d3a1f38bbd026385970643f9c5b8ecfeb"}, ] paramiko = [ - {file = "paramiko-2.9.2-py2.py3-none-any.whl", hash = "sha256:04097dbd96871691cdb34c13db1883066b8a13a0df2afd4cb0a92221f51c2603"}, - {file = "paramiko-2.9.2.tar.gz", hash = "sha256:944a9e5dbdd413ab6c7951ea46b0ab40713235a9c4c5ca81cfe45c6f14fa677b"}, + {file = "paramiko-2.10.3-py2.py3-none-any.whl", hash = "sha256:ac6593479f2b47a9422eca076b22cff9f795495e6733a64723efc75dd8c92101"}, + {file = "paramiko-2.10.3.tar.gz", hash = "sha256:ddb1977853aef82804b35d72a0e597b244fa326c404c350bd00c5b01dbfee71a"}, ] passlib = [ {file = "passlib-1.7.4-py2.py3-none-any.whl", hash = "sha256:aa6bca462b8d8bda89c70b382f0c298a20b5560af6cbfa2dce410c0a2fb669f1"}, @@ -2299,45 +2349,48 @@ peeringdb = [ {file = "peeringdb-1.1.0.tar.gz", hash = "sha256:927a34c31e5b93130a855bb4c8fd84dcf604b9939678f75918f7c1bd8a501471"}, ] phonenumbers = [ - {file = "phonenumbers-8.12.44-py2.py3-none-any.whl", hash = "sha256:cc1299cf37b309ecab6214297663ab86cb3d64ae37fd5b88e904fe7983a874a6"}, - {file = "phonenumbers-8.12.44.tar.gz", hash = "sha256:26cfd0257d1704fe2f88caff2caabb70d16a877b1e65b6aae51f9fbbe10aa8ce"}, + {file = "phonenumbers-8.12.46-py2.py3-none-any.whl", hash = "sha256:ae300d40c3e0b581372294a43b04359b24af94f631dd9451881561d5c702a78d"}, + {file = "phonenumbers-8.12.46.tar.gz", hash = "sha256:1c440f6336cb49893ff1a8326c70b4df693802ae981f210f545cd4215ac48133"}, ] pillow = [ - {file = "Pillow-9.0.1-1-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:a5d24e1d674dd9d72c66ad3ea9131322819ff86250b30dc5821cbafcfa0b96b4"}, - {file = "Pillow-9.0.1-1-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:2632d0f846b7c7600edf53c48f8f9f1e13e62f66a6dbc15191029d950bfed976"}, - {file = "Pillow-9.0.1-1-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:b9618823bd237c0d2575283f2939655f54d51b4527ec3972907a927acbcc5bfc"}, - {file = "Pillow-9.0.1-cp310-cp310-macosx_10_10_universal2.whl", hash = "sha256:9bfdb82cdfeccec50aad441afc332faf8606dfa5e8efd18a6692b5d6e79f00fd"}, - {file = "Pillow-9.0.1-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:5100b45a4638e3c00e4d2320d3193bdabb2d75e79793af7c3eb139e4f569f16f"}, - {file = "Pillow-9.0.1-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:528a2a692c65dd5cafc130de286030af251d2ee0483a5bf50c9348aefe834e8a"}, - {file = "Pillow-9.0.1-cp310-cp310-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:0f29d831e2151e0b7b39981756d201f7108d3d215896212ffe2e992d06bfe049"}, - {file = "Pillow-9.0.1-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:855c583f268edde09474b081e3ddcd5cf3b20c12f26e0d434e1386cc5d318e7a"}, - {file = "Pillow-9.0.1-cp310-cp310-win32.whl", hash = "sha256:d9d7942b624b04b895cb95af03a23407f17646815495ce4547f0e60e0b06f58e"}, - {file = "Pillow-9.0.1-cp310-cp310-win_amd64.whl", hash = "sha256:81c4b81611e3a3cb30e59b0cf05b888c675f97e3adb2c8672c3154047980726b"}, - {file = "Pillow-9.0.1-cp37-cp37m-macosx_10_10_x86_64.whl", hash = "sha256:413ce0bbf9fc6278b2d63309dfeefe452835e1c78398efb431bab0672fe9274e"}, - {file = "Pillow-9.0.1-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:80fe64a6deb6fcfdf7b8386f2cf216d329be6f2781f7d90304351811fb591360"}, - {file = "Pillow-9.0.1-cp37-cp37m-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:cef9c85ccbe9bee00909758936ea841ef12035296c748aaceee535969e27d31b"}, - {file = "Pillow-9.0.1-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:1d19397351f73a88904ad1aee421e800fe4bbcd1aeee6435fb62d0a05ccd1030"}, - {file = "Pillow-9.0.1-cp37-cp37m-win32.whl", hash = "sha256:d21237d0cd37acded35154e29aec853e945950321dd2ffd1a7d86fe686814669"}, - {file = "Pillow-9.0.1-cp37-cp37m-win_amd64.whl", hash = "sha256:ede5af4a2702444a832a800b8eb7f0a7a1c0eed55b644642e049c98d589e5092"}, - {file = "Pillow-9.0.1-cp38-cp38-macosx_10_10_x86_64.whl", hash = "sha256:b5b3f092fe345c03bca1e0b687dfbb39364b21ebb8ba90e3fa707374b7915204"}, - {file = "Pillow-9.0.1-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:335ace1a22325395c4ea88e00ba3dc89ca029bd66bd5a3c382d53e44f0ccd77e"}, - {file = "Pillow-9.0.1-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:db6d9fac65bd08cea7f3540b899977c6dee9edad959fa4eaf305940d9cbd861c"}, - {file = "Pillow-9.0.1-cp38-cp38-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:f154d173286a5d1863637a7dcd8c3437bb557520b01bddb0be0258dcb72696b5"}, - {file = "Pillow-9.0.1-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:14d4b1341ac07ae07eb2cc682f459bec932a380c3b122f5540432d8977e64eae"}, - {file = "Pillow-9.0.1-cp38-cp38-win32.whl", hash = "sha256:effb7749713d5317478bb3acb3f81d9d7c7f86726d41c1facca068a04cf5bb4c"}, - {file = "Pillow-9.0.1-cp38-cp38-win_amd64.whl", hash = "sha256:7f7609a718b177bf171ac93cea9fd2ddc0e03e84d8fa4e887bdfc39671d46b00"}, - {file = "Pillow-9.0.1-cp39-cp39-macosx_10_10_x86_64.whl", hash = "sha256:80ca33961ced9c63358056bd08403ff866512038883e74f3a4bf88ad3eb66838"}, - {file = "Pillow-9.0.1-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:1c3c33ac69cf059bbb9d1a71eeaba76781b450bc307e2291f8a4764d779a6b28"}, - {file = "Pillow-9.0.1-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:12875d118f21cf35604176872447cdb57b07126750a33748bac15e77f90f1f9c"}, - {file = "Pillow-9.0.1-cp39-cp39-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:514ceac913076feefbeaf89771fd6febde78b0c4c1b23aaeab082c41c694e81b"}, - {file = "Pillow-9.0.1-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:d3c5c79ab7dfce6d88f1ba639b77e77a17ea33a01b07b99840d6ed08031cb2a7"}, - {file = "Pillow-9.0.1-cp39-cp39-win32.whl", hash = "sha256:718856856ba31f14f13ba885ff13874be7fefc53984d2832458f12c38205f7f7"}, - {file = "Pillow-9.0.1-cp39-cp39-win_amd64.whl", hash = "sha256:f25ed6e28ddf50de7e7ea99d7a976d6a9c415f03adcaac9c41ff6ff41b6d86ac"}, - {file = "Pillow-9.0.1-pp37-pypy37_pp73-macosx_10_10_x86_64.whl", hash = "sha256:011233e0c42a4a7836498e98c1acf5e744c96a67dd5032a6f666cc1fb97eab97"}, - {file = "Pillow-9.0.1-pp37-pypy37_pp73-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:253e8a302a96df6927310a9d44e6103055e8fb96a6822f8b7f514bb7ef77de56"}, - {file = "Pillow-9.0.1-pp37-pypy37_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6295f6763749b89c994fcb6d8a7f7ce03c3992e695f89f00b741b4580b199b7e"}, - {file = "Pillow-9.0.1-pp38-pypy38_pp73-win_amd64.whl", hash = "sha256:a9f44cd7e162ac6191491d7249cceb02b8116b0f7e847ee33f739d7cb1ea1f70"}, - {file = "Pillow-9.0.1.tar.gz", hash = "sha256:6c8bc8238a7dfdaf7a75f5ec5a663f4173f8c367e5a39f87e720495e1eed75fa"}, + {file = "Pillow-9.1.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:af79d3fde1fc2e33561166d62e3b63f0cc3e47b5a3a2e5fea40d4917754734ea"}, + {file = "Pillow-9.1.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:55dd1cf09a1fd7c7b78425967aacae9b0d70125f7d3ab973fadc7b5abc3de652"}, + {file = "Pillow-9.1.0-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:66822d01e82506a19407d1afc104c3fcea3b81d5eb11485e593ad6b8492f995a"}, + {file = "Pillow-9.1.0-cp310-cp310-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:a5eaf3b42df2bcda61c53a742ee2c6e63f777d0e085bbc6b2ab7ed57deb13db7"}, + {file = "Pillow-9.1.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:01ce45deec9df310cbbee11104bae1a2a43308dd9c317f99235b6d3080ddd66e"}, + {file = "Pillow-9.1.0-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:aea7ce61328e15943d7b9eaca87e81f7c62ff90f669116f857262e9da4057ba3"}, + {file = "Pillow-9.1.0-cp310-cp310-win32.whl", hash = "sha256:7a053bd4d65a3294b153bdd7724dce864a1d548416a5ef61f6d03bf149205160"}, + {file = "Pillow-9.1.0-cp310-cp310-win_amd64.whl", hash = "sha256:97bda660702a856c2c9e12ec26fc6d187631ddfd896ff685814ab21ef0597033"}, + {file = "Pillow-9.1.0-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:21dee8466b42912335151d24c1665fcf44dc2ee47e021d233a40c3ca5adae59c"}, + {file = "Pillow-9.1.0-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:6b6d4050b208c8ff886fd3db6690bf04f9a48749d78b41b7a5bf24c236ab0165"}, + {file = "Pillow-9.1.0-cp37-cp37m-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:5cfca31ab4c13552a0f354c87fbd7f162a4fafd25e6b521bba93a57fe6a3700a"}, + {file = "Pillow-9.1.0-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ed742214068efa95e9844c2d9129e209ed63f61baa4d54dbf4cf8b5e2d30ccf2"}, + {file = "Pillow-9.1.0-cp37-cp37m-win32.whl", hash = "sha256:c9efef876c21788366ea1f50ecb39d5d6f65febe25ad1d4c0b8dff98843ac244"}, + {file = "Pillow-9.1.0-cp37-cp37m-win_amd64.whl", hash = "sha256:de344bcf6e2463bb25179d74d6e7989e375f906bcec8cb86edb8b12acbc7dfef"}, + {file = "Pillow-9.1.0-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:17869489de2fce6c36690a0c721bd3db176194af5f39249c1ac56d0bb0fcc512"}, + {file = "Pillow-9.1.0-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:25023a6209a4d7c42154073144608c9a71d3512b648a2f5d4465182cb93d3477"}, + {file = "Pillow-9.1.0-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:8782189c796eff29dbb37dd87afa4ad4d40fc90b2742704f94812851b725964b"}, + {file = "Pillow-9.1.0-cp38-cp38-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:463acf531f5d0925ca55904fa668bb3461c3ef6bc779e1d6d8a488092bdee378"}, + {file = "Pillow-9.1.0-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:3f42364485bfdab19c1373b5cd62f7c5ab7cc052e19644862ec8f15bb8af289e"}, + {file = "Pillow-9.1.0-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:3fddcdb619ba04491e8f771636583a7cc5a5051cd193ff1aa1ee8616d2a692c5"}, + {file = "Pillow-9.1.0-cp38-cp38-win32.whl", hash = "sha256:4fe29a070de394e449fd88ebe1624d1e2d7ddeed4c12e0b31624561b58948d9a"}, + {file = "Pillow-9.1.0-cp38-cp38-win_amd64.whl", hash = "sha256:c24f718f9dd73bb2b31a6201e6db5ea4a61fdd1d1c200f43ee585fc6dcd21b34"}, + {file = "Pillow-9.1.0-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:fb89397013cf302f282f0fc998bb7abf11d49dcff72c8ecb320f76ea6e2c5717"}, + {file = "Pillow-9.1.0-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:c870193cce4b76713a2b29be5d8327c8ccbe0d4a49bc22968aa1e680930f5581"}, + {file = "Pillow-9.1.0-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:69e5ddc609230d4408277af135c5b5c8fe7a54b2bdb8ad7c5100b86b3aab04c6"}, + {file = "Pillow-9.1.0-cp39-cp39-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:35be4a9f65441d9982240e6966c1eaa1c654c4e5e931eaf580130409e31804d4"}, + {file = "Pillow-9.1.0-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:82283af99c1c3a5ba1da44c67296d5aad19f11c535b551a5ae55328a317ce331"}, + {file = "Pillow-9.1.0-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:a325ac71914c5c043fa50441b36606e64a10cd262de12f7a179620f579752ff8"}, + {file = "Pillow-9.1.0-cp39-cp39-win32.whl", hash = "sha256:a598d8830f6ef5501002ae85c7dbfcd9c27cc4efc02a1989369303ba85573e58"}, + {file = "Pillow-9.1.0-cp39-cp39-win_amd64.whl", hash = "sha256:0c51cb9edac8a5abd069fd0758ac0a8bfe52c261ee0e330f363548aca6893595"}, + {file = "Pillow-9.1.0-pp37-pypy37_pp73-macosx_10_9_x86_64.whl", hash = "sha256:a336a4f74baf67e26f3acc4d61c913e378e931817cd1e2ef4dfb79d3e051b481"}, + {file = "Pillow-9.1.0-pp37-pypy37_pp73-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:eb1b89b11256b5b6cad5e7593f9061ac4624f7651f7a8eb4dfa37caa1dfaa4d0"}, + {file = "Pillow-9.1.0-pp37-pypy37_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:255c9d69754a4c90b0ee484967fc8818c7ff8311c6dddcc43a4340e10cd1636a"}, + {file = "Pillow-9.1.0-pp38-pypy38_pp73-macosx_10_9_x86_64.whl", hash = "sha256:5a3ecc026ea0e14d0ad7cd990ea7f48bfcb3eb4271034657dc9d06933c6629a7"}, + {file = "Pillow-9.1.0-pp38-pypy38_pp73-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:c5b0ff59785d93b3437c3703e3c64c178aabada51dea2a7f2c5eccf1bcf565a3"}, + {file = "Pillow-9.1.0-pp38-pypy38_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:c7110ec1701b0bf8df569a7592a196c9d07c764a0a74f65471ea56816f10e2c8"}, + {file = "Pillow-9.1.0-pp38-pypy38_pp73-win_amd64.whl", hash = "sha256:8d79c6f468215d1a8415aa53d9868a6b40c4682165b8cb62a221b1baa47db458"}, + {file = "Pillow-9.1.0.tar.gz", hash = "sha256:f401ed2bbb155e1ade150ccc63db1a4f6c1909d3d378f7d1235a44e90d75fb97"}, ] platformdirs = [ {file = "platformdirs-2.5.1-py3-none-any.whl", hash = "sha256:bcae7cab893c2d310a711b70b24efb93334febe65f8de776ee320b517471e227"}, @@ -2348,8 +2401,8 @@ pluggy = [ {file = "pluggy-1.0.0.tar.gz", hash = "sha256:4224373bacce55f955a878bf9cfa763c1e360858e330072059e10bad68531159"}, ] pre-commit = [ - {file = "pre_commit-2.17.0-py2.py3-none-any.whl", hash = "sha256:725fa7459782d7bec5ead072810e47351de01709be838c2ce1726b9591dad616"}, - {file = "pre_commit-2.17.0.tar.gz", hash = "sha256:c1a8040ff15ad3d648c70cc3e55b93e4d2d5b687320955505587fd79bbaed06a"}, + {file = "pre_commit-2.18.1-py2.py3-none-any.whl", hash = "sha256:02226e69564ebca1a070bd1f046af866aa1c318dbc430027c50ab832ed2b73f2"}, + {file = "pre_commit-2.18.1.tar.gz", hash = "sha256:5d445ee1fa8738d506881c5d84f83c62bb5be6b2838e32207433647e8e5ebe10"}, ] py = [ {file = "py-1.11.0-py2.py3-none-any.whl", hash = "sha256:607c53218732647dff4acdfcd50cb62615cedf612e72d1724fb1a0cc6405b378"}, @@ -2432,8 +2485,8 @@ pyopenssl = [ {file = "pyOpenSSL-22.0.0.tar.gz", hash = "sha256:660b1b1425aac4a1bea1d94168a85d99f0b3144c869dd4390d27629d0087f1bf"}, ] pyparsing = [ - {file = "pyparsing-3.0.7-py3-none-any.whl", hash = "sha256:a6c06a88f252e6c322f65faf8f418b16213b51bdfaece0524c1c1bc30c63c484"}, - {file = "pyparsing-3.0.7.tar.gz", hash = "sha256:18ee9022775d270c55187733956460083db60b37d0d0fb357445f3094eed3eea"}, + {file = "pyparsing-3.0.8-py3-none-any.whl", hash = "sha256:ef7b523f6356f763771559412c0d7134753f037822dad1b16945b7b846f7ad06"}, + {file = "pyparsing-3.0.8.tar.gz", hash = "sha256:7bf433498c016c4314268d95df76c81b842a4cb2b276fa3312cfb1e1d85f6954"}, ] pyrsistent = [ {file = "pyrsistent-0.18.1-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:df46c854f490f81210870e509818b729db4488e1f30f2a1ce1698b2295a878d1"}, @@ -2459,8 +2512,8 @@ pyrsistent = [ {file = "pyrsistent-0.18.1.tar.gz", hash = "sha256:d4d61f8b993a7255ba714df3aca52700f8125289f84f704cf80916517c46eb96"}, ] pytest = [ - {file = "pytest-7.0.1-py3-none-any.whl", hash = "sha256:9ce3ff477af913ecf6321fe337b93a2c0dcf2a0a1439c43f5452112c1e4280db"}, - {file = "pytest-7.0.1.tar.gz", hash = "sha256:e30905a0c131d3d94b89624a1cc5afec3e0ba2fbdb151867d8e0ebd49850f171"}, + {file = "pytest-7.1.1-py3-none-any.whl", hash = "sha256:92f723789a8fdd7180b6b06483874feca4c48a5c76968e03bb3e7f806a1869ea"}, + {file = "pytest-7.1.1.tar.gz", hash = "sha256:841132caef6b1ad17a9afde46dc4f6cfa59a05f9555aae5151f73bdf2820ca63"}, ] pytest-cov = [ {file = "pytest-cov-3.0.0.tar.gz", hash = "sha256:e7f0f5b1617d2210a2cabc266dfe2f4c75a8d32fb89eafb7ad9d06f6d076d470"}, @@ -2482,20 +2535,20 @@ python-dateutil = [ {file = "python_dateutil-2.8.2-py2.py3-none-any.whl", hash = "sha256:961d03dc3453ebbc59dbdea9e4e11c5651520a876d0f4db161e8674aae935da9"}, ] python-dotenv = [ - {file = "python-dotenv-0.19.2.tar.gz", hash = "sha256:a5de49a31e953b45ff2d2fd434bbc2670e8db5273606c1e737cc6b93eff3655f"}, - {file = "python_dotenv-0.19.2-py2.py3-none-any.whl", hash = "sha256:32b2bdc1873fd3a3c346da1c6db83d0053c3c62f28f1f38516070c4c8971b1d3"}, + {file = "python-dotenv-0.20.0.tar.gz", hash = "sha256:b7e3b04a59693c42c36f9ab1cc2acc46fa5df8c78e178fc33a8d4cd05c8d498f"}, + {file = "python_dotenv-0.20.0-py3-none-any.whl", hash = "sha256:d92a187be61fe482e4fd675b6d52200e7be63a12b724abbf931a40ce4fa92938"}, ] 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"}, ] pytz = [ - {file = "pytz-2021.3-py2.py3-none-any.whl", hash = "sha256:3672058bc3453457b622aab7a1c3bfd5ab0bdae451512f6cf25f64ed37f5b87c"}, - {file = "pytz-2021.3.tar.gz", hash = "sha256:acad2d8b20a1af07d4e4c9d2e9285c5ed9104354062f275f3fcd88dcef4f1326"}, + {file = "pytz-2022.1-py2.py3-none-any.whl", hash = "sha256:e68985985296d9a66a881eb3193b0906246245294a881e7c8afe623866ac6a5c"}, + {file = "pytz-2022.1.tar.gz", hash = "sha256:1e760e2fe6a8163bc0b3d9a19c4f84342afa0a2affebfaa84b01b978a02ecaa7"}, ] pyupgrade = [ - {file = "pyupgrade-2.31.0-py2.py3-none-any.whl", hash = "sha256:0a62c5055f854d7f36e155b7ee8920561bf0399c53edd975cf02436eef8937fc"}, - {file = "pyupgrade-2.31.0.tar.gz", hash = "sha256:80e2308cae2b11c3fdd091137495d99abf7e0cd98b501aa5758974991497c24c"}, + {file = "pyupgrade-2.32.0-py2.py3-none-any.whl", hash = "sha256:f45d4afb6ccdf7b0cea757958d0a11306324052668d9ff99d2bcb06bda46c00d"}, + {file = "pyupgrade-2.32.0.tar.gz", hash = "sha256:6878116d364b72f0c0011dd62dfe96425041a5f753da298b6eacde0f9fd9c004"}, ] pywin32 = [ {file = "pywin32-227-cp27-cp27m-win32.whl", hash = "sha256:371fcc39416d736401f0274dd64c2302728c9e034808e37381b5e1b22be4a6b0"}, @@ -2636,6 +2689,10 @@ sqlparse = [ {file = "sqlparse-0.4.2-py3-none-any.whl", hash = "sha256:48719e356bb8b42991bdbb1e8b83223757b93789c00910a616a071910ca4a64d"}, {file = "sqlparse-0.4.2.tar.gz", hash = "sha256:0c00730c74263a94e5a9919ade150dfc3b19c574389985446148402998287dae"}, ] +structlog = [ + {file = "structlog-21.5.0-py3-none-any.whl", hash = "sha256:fd7922e195262b337da85c2a91c84be94ccab1f8fd1957bd6986f6904e3761c8"}, + {file = "structlog-21.5.0.tar.gz", hash = "sha256:68c4c29c003714fe86834f347cb107452847ba52414390a7ee583472bde00fc9"}, +] texttable = [ {file = "texttable-1.6.4-py2.py3-none-any.whl", hash = "sha256:dd2b0eaebb2a9e167d1cefedab4700e5dcbdb076114eed30b58b97ed6b37d6f2"}, {file = "texttable-1.6.4.tar.gz", hash = "sha256:42ee7b9e15f7b225747c3fa08f43c5d6c83bc899f80ff9bae9319334824076e9"}, @@ -2669,52 +2726,53 @@ typing-extensions = [ {file = "typing_extensions-4.1.1.tar.gz", hash = "sha256:1a9462dcc3347a79b1f1c0271fbe79e844580bb598bafa1ed208b94da3cdcd42"}, ] unidecode = [ - {file = "Unidecode-1.3.3-py3-none-any.whl", hash = "sha256:a5a8a4b6fb033724ffba8502af2e65ca5bfc3dd53762dedaafe4b0134ad42e3c"}, - {file = "Unidecode-1.3.3.tar.gz", hash = "sha256:8521f2853fd250891dc27d156a9d30e61c4e76319da963c4a1c27083a909ac30"}, + {file = "Unidecode-1.3.4-py3-none-any.whl", hash = "sha256:afa04efcdd818a93237574791be9b2817d7077c25a068b00f8cff7baa4e59257"}, + {file = "Unidecode-1.3.4.tar.gz", hash = "sha256:8e4352fb93d5a735c788110d2e7ac8e8031eb06ccbfe8d324ab71735015f9342"}, ] uritemplate = [ {file = "uritemplate-4.1.1-py2.py3-none-any.whl", hash = "sha256:830c08b8d99bdd312ea4ead05994a38e8936266f84b9a7878232db50b044e02e"}, {file = "uritemplate-4.1.1.tar.gz", hash = "sha256:4346edfc5c3b79f694bccd6d6099a322bbeb628dbf2cd86eea55a456ce5124f0"}, ] urllib3 = [ - {file = "urllib3-1.26.8-py2.py3-none-any.whl", hash = "sha256:000ca7f471a233c2251c6c7023ee85305721bfdf18621ebff4fd17a8653427ed"}, - {file = "urllib3-1.26.8.tar.gz", hash = "sha256:0e7c33d9a63e7ddfcb86780aac87befc2fbddf46c58dbb487e0855f7ceec283c"}, + {file = "urllib3-1.26.9-py2.py3-none-any.whl", hash = "sha256:44ece4d53fb1706f667c9bd1c648f5469a2ec925fcf3a776667042d645472c14"}, + {file = "urllib3-1.26.9.tar.gz", hash = "sha256:aabaf16477806a5e1dd19aa41f8c2b7950dd3c746362d7e3223dbe6de6ac448e"}, ] uwsgi = [ {file = "uwsgi-2.0.20.tar.gz", hash = "sha256:88ab9867d8973d8ae84719cf233b7dafc54326fcaec89683c3f9f77c002cdff9"}, ] virtualenv = [ - {file = "virtualenv-20.13.3-py2.py3-none-any.whl", hash = "sha256:dd448d1ded9f14d1a4bfa6bfc0c5b96ae3be3f2d6c6c159b23ddcfd701baa021"}, - {file = "virtualenv-20.13.3.tar.gz", hash = "sha256:e9dd1a1359d70137559034c0f5433b34caf504af2dc756367be86a5a32967134"}, + {file = "virtualenv-20.14.1-py2.py3-none-any.whl", hash = "sha256:e617f16e25b42eb4f6e74096b9c9e37713cf10bf30168fb4a739f3fa8f898a3a"}, + {file = "virtualenv-20.14.1.tar.gz", hash = "sha256:ef589a79795589aada0c1c5b319486797c03b67ac3984c48c669c0e4f50df3a5"}, ] watchdog = [ - {file = "watchdog-2.1.6-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:9693f35162dc6208d10b10ddf0458cc09ad70c30ba689d9206e02cd836ce28a3"}, - {file = "watchdog-2.1.6-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:aba5c812f8ee8a3ff3be51887ca2d55fb8e268439ed44110d3846e4229eb0e8b"}, - {file = "watchdog-2.1.6-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:4ae38bf8ba6f39d5b83f78661273216e7db5b00f08be7592062cb1fc8b8ba542"}, - {file = "watchdog-2.1.6-cp36-cp36m-macosx_10_9_x86_64.whl", hash = "sha256:ad6f1796e37db2223d2a3f302f586f74c72c630b48a9872c1e7ae8e92e0ab669"}, - {file = "watchdog-2.1.6-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:922a69fa533cb0c793b483becaaa0845f655151e7256ec73630a1b2e9ebcb660"}, - {file = "watchdog-2.1.6-cp38-cp38-macosx_10_9_universal2.whl", hash = "sha256:b2fcf9402fde2672545b139694284dc3b665fd1be660d73eca6805197ef776a3"}, - {file = "watchdog-2.1.6-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:3386b367e950a11b0568062b70cc026c6f645428a698d33d39e013aaeda4cc04"}, - {file = "watchdog-2.1.6-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:8f1c00aa35f504197561060ca4c21d3cc079ba29cf6dd2fe61024c70160c990b"}, - {file = "watchdog-2.1.6-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:b52b88021b9541a60531142b0a451baca08d28b74a723d0c99b13c8c8d48d604"}, - {file = "watchdog-2.1.6-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:8047da932432aa32c515ec1447ea79ce578d0559362ca3605f8e9568f844e3c6"}, - {file = "watchdog-2.1.6-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:e92c2d33858c8f560671b448205a268096e17870dcf60a9bb3ac7bfbafb7f5f9"}, - {file = "watchdog-2.1.6-pp37-pypy37_pp73-macosx_10_9_x86_64.whl", hash = "sha256:b7d336912853d7b77f9b2c24eeed6a5065d0a0cc0d3b6a5a45ad6d1d05fb8cd8"}, - {file = "watchdog-2.1.6-py3-none-manylinux2014_aarch64.whl", hash = "sha256:cca7741c0fcc765568350cb139e92b7f9f3c9a08c4f32591d18ab0a6ac9e71b6"}, - {file = "watchdog-2.1.6-py3-none-manylinux2014_armv7l.whl", hash = "sha256:25fb5240b195d17de949588628fdf93032ebf163524ef08933db0ea1f99bd685"}, - {file = "watchdog-2.1.6-py3-none-manylinux2014_i686.whl", hash = "sha256:be9be735f827820a06340dff2ddea1fb7234561fa5e6300a62fe7f54d40546a0"}, - {file = "watchdog-2.1.6-py3-none-manylinux2014_ppc64.whl", hash = "sha256:d0d19fb2441947b58fbf91336638c2b9f4cc98e05e1045404d7a4cb7cddc7a65"}, - {file = "watchdog-2.1.6-py3-none-manylinux2014_ppc64le.whl", hash = "sha256:3becdb380d8916c873ad512f1701f8a92ce79ec6978ffde92919fd18d41da7fb"}, - {file = "watchdog-2.1.6-py3-none-manylinux2014_s390x.whl", hash = "sha256:ae67501c95606072aafa865b6ed47343ac6484472a2f95490ba151f6347acfc2"}, - {file = "watchdog-2.1.6-py3-none-manylinux2014_x86_64.whl", hash = "sha256:e0f30db709c939cabf64a6dc5babb276e6d823fd84464ab916f9b9ba5623ca15"}, - {file = "watchdog-2.1.6-py3-none-win32.whl", hash = "sha256:e02794ac791662a5eafc6ffeaf9bcc149035a0e48eb0a9d40a8feb4622605a3d"}, - {file = "watchdog-2.1.6-py3-none-win_amd64.whl", hash = "sha256:bd9ba4f332cf57b2c1f698be0728c020399ef3040577cde2939f2e045b39c1e5"}, - {file = "watchdog-2.1.6-py3-none-win_ia64.whl", hash = "sha256:a0f1c7edf116a12f7245be06120b1852275f9506a7d90227648b250755a03923"}, - {file = "watchdog-2.1.6.tar.gz", hash = "sha256:a36e75df6c767cbf46f61a91c70b3ba71811dfa0aca4a324d9407a06a8b7a2e7"}, + {file = "watchdog-2.1.7-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:177bae28ca723bc00846466016d34f8c1d6a621383b6caca86745918d55c7383"}, + {file = "watchdog-2.1.7-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:1d1cf7dfd747dec519486a98ef16097e6c480934ef115b16f18adb341df747a4"}, + {file = "watchdog-2.1.7-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:7f14ce6adea2af1bba495acdde0e510aecaeb13b33f7bd2f6324e551b26688ca"}, + {file = "watchdog-2.1.7-cp36-cp36m-macosx_10_9_x86_64.whl", hash = "sha256:4d0e98ac2e8dd803a56f4e10438b33a2d40390a72750cff4939b4b274e7906fa"}, + {file = "watchdog-2.1.7-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:81982c7884aac75017a6ecc72f1a4fedbae04181a8665a34afce9539fc1b3fab"}, + {file = "watchdog-2.1.7-cp38-cp38-macosx_10_9_universal2.whl", hash = "sha256:0b4a1fe6201c6e5a1926f5767b8664b45f0fcb429b62564a41f490ff1ce1dc7a"}, + {file = "watchdog-2.1.7-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:6e6ae29b72977f2e1ee3d0b760d7ee47896cb53e831cbeede3e64485e5633cc8"}, + {file = "watchdog-2.1.7-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:b9777664848160449e5b4260e0b7bc1ae0f6f4992a8b285db4ec1ef119ffa0e2"}, + {file = "watchdog-2.1.7-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:19b36d436578eb437e029c6b838e732ed08054956366f6dd11875434a62d2b99"}, + {file = "watchdog-2.1.7-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:b61acffaf5cd5d664af555c0850f9747cc5f2baf71e54bbac164c58398d6ca7b"}, + {file = "watchdog-2.1.7-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:1e877c70245424b06c41ac258023ea4bd0c8e4ff15d7c1368f17cd0ae6e351dd"}, + {file = "watchdog-2.1.7-pp37-pypy37_pp73-macosx_10_9_x86_64.whl", hash = "sha256:d802d65262a560278cf1a65ef7cae4e2bc7ecfe19e5451349e4c67e23c9dc420"}, + {file = "watchdog-2.1.7-pp38-pypy38_pp73-macosx_10_9_x86_64.whl", hash = "sha256:b3750ee5399e6e9c69eae8b125092b871ee9e2fcbd657a92747aea28f9056a5c"}, + {file = "watchdog-2.1.7-py3-none-manylinux2014_aarch64.whl", hash = "sha256:ed6d9aad09a2a948572224663ab00f8975fae242aa540509737bb4507133fa2d"}, + {file = "watchdog-2.1.7-py3-none-manylinux2014_armv7l.whl", hash = "sha256:b26e13e8008dcaea6a909e91d39b629a39635d1a8a7239dd35327c74f4388601"}, + {file = "watchdog-2.1.7-py3-none-manylinux2014_i686.whl", hash = "sha256:0908bb50f6f7de54d5d31ec3da1654cb7287c6b87bce371954561e6de379d690"}, + {file = "watchdog-2.1.7-py3-none-manylinux2014_ppc64.whl", hash = "sha256:bdcbf75580bf4b960fb659bbccd00123d83119619195f42d721e002c1621602f"}, + {file = "watchdog-2.1.7-py3-none-manylinux2014_ppc64le.whl", hash = "sha256:81a5861d0158a7e55fe149335fb2bbfa6f48cbcbd149b52dbe2cd9a544034bbd"}, + {file = "watchdog-2.1.7-py3-none-manylinux2014_s390x.whl", hash = "sha256:03b43d583df0f18782a0431b6e9e9965c5b3f7cf8ec36a00b930def67942c385"}, + {file = "watchdog-2.1.7-py3-none-manylinux2014_x86_64.whl", hash = "sha256:ae934e34c11aa8296c18f70bf66ed60e9870fcdb4cc19129a04ca83ab23e7055"}, + {file = "watchdog-2.1.7-py3-none-win32.whl", hash = "sha256:49639865e3db4be032a96695c98ac09eed39bbb43fe876bb217da8f8101689a6"}, + {file = "watchdog-2.1.7-py3-none-win_amd64.whl", hash = "sha256:340b875aecf4b0e6672076a6f05cfce6686935559bb6d34cebedee04126a9566"}, + {file = "watchdog-2.1.7-py3-none-win_ia64.whl", hash = "sha256:351e09b6d9374d5bcb947e6ac47a608ec25b9d70583e9db00b2fcdb97b00b572"}, + {file = "watchdog-2.1.7.tar.gz", hash = "sha256:3fd47815353be9c44eebc94cc28fe26b2b0c5bd889dafc4a5a7cbdf924143480"}, ] webauthn = [ - {file = "webauthn-1.4.0-py3-none-any.whl", hash = "sha256:114a1e7266c232a18d1af7ea44d8eeee7c226aef5bdac458cac4693d23ab3f10"}, - {file = "webauthn-1.4.0.tar.gz", hash = "sha256:3b2ac481b93f9b61c8f1bcfc6622a24efefb579d11fc7d643b6ac0028be45b2e"}, + {file = "webauthn-1.5.0-py3-none-any.whl", hash = "sha256:71d419d2c0337906d706fdb987666361e1a8ca1285f78375b8538c3cf56f21ec"}, + {file = "webauthn-1.5.0.tar.gz", hash = "sha256:c9e0629202f05095d12cf715adbcfcda1f1aae9c46f1c06596990a42c4d02fb4"}, ] webencodings = [ {file = "webencodings-0.5.1-py2.py3-none-any.whl", hash = "sha256:a0af1213f3c2226497a97e2b3aa01a7e4bee4f403f95be16fc9acd2947514a78"}, @@ -2730,59 +2788,72 @@ whoosh = [ {file = "Whoosh-2.7.4.zip", hash = "sha256:e0857375f63e9041e03fedd5b7541f97cf78917ac1b6b06c1fcc9b45375dda69"}, ] wrapt = [ - {file = "wrapt-1.13.3-cp27-cp27m-macosx_10_9_x86_64.whl", hash = "sha256:e05e60ff3b2b0342153be4d1b597bbcfd8330890056b9619f4ad6b8d5c96a81a"}, - {file = "wrapt-1.13.3-cp27-cp27m-manylinux1_i686.whl", hash = "sha256:85148f4225287b6a0665eef08a178c15097366d46b210574a658c1ff5b377489"}, - {file = "wrapt-1.13.3-cp27-cp27m-manylinux1_x86_64.whl", hash = "sha256:2dded5496e8f1592ec27079b28b6ad2a1ef0b9296d270f77b8e4a3a796cf6909"}, - {file = "wrapt-1.13.3-cp27-cp27m-manylinux2010_i686.whl", hash = "sha256:e94b7d9deaa4cc7bac9198a58a7240aaf87fe56c6277ee25fa5b3aa1edebd229"}, - {file = "wrapt-1.13.3-cp27-cp27m-manylinux2010_x86_64.whl", hash = "sha256:498e6217523111d07cd67e87a791f5e9ee769f9241fcf8a379696e25806965af"}, - {file = "wrapt-1.13.3-cp27-cp27mu-manylinux1_i686.whl", hash = "sha256:ec7e20258ecc5174029a0f391e1b948bf2906cd64c198a9b8b281b811cbc04de"}, - {file = "wrapt-1.13.3-cp27-cp27mu-manylinux1_x86_64.whl", hash = "sha256:87883690cae293541e08ba2da22cacaae0a092e0ed56bbba8d018cc486fbafbb"}, - {file = "wrapt-1.13.3-cp27-cp27mu-manylinux2010_i686.whl", hash = "sha256:f99c0489258086308aad4ae57da9e8ecf9e1f3f30fa35d5e170b4d4896554d80"}, - {file = "wrapt-1.13.3-cp27-cp27mu-manylinux2010_x86_64.whl", hash = "sha256:6a03d9917aee887690aa3f1747ce634e610f6db6f6b332b35c2dd89412912bca"}, - {file = "wrapt-1.13.3-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:936503cb0a6ed28dbfa87e8fcd0a56458822144e9d11a49ccee6d9a8adb2ac44"}, - {file = "wrapt-1.13.3-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_12_i686.manylinux2010_i686.whl", hash = "sha256:f9c51d9af9abb899bd34ace878fbec8bf357b3194a10c4e8e0a25512826ef056"}, - {file = "wrapt-1.13.3-cp310-cp310-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:220a869982ea9023e163ba915077816ca439489de6d2c09089b219f4e11b6785"}, - {file = "wrapt-1.13.3-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:0877fe981fd76b183711d767500e6b3111378ed2043c145e21816ee589d91096"}, - {file = "wrapt-1.13.3-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:43e69ffe47e3609a6aec0fe723001c60c65305784d964f5007d5b4fb1bc6bf33"}, - {file = "wrapt-1.13.3-cp310-cp310-win32.whl", hash = "sha256:78dea98c81915bbf510eb6a3c9c24915e4660302937b9ae05a0947164248020f"}, - {file = "wrapt-1.13.3-cp310-cp310-win_amd64.whl", hash = "sha256:ea3e746e29d4000cd98d572f3ee2a6050a4f784bb536f4ac1f035987fc1ed83e"}, - {file = "wrapt-1.13.3-cp35-cp35m-manylinux1_i686.whl", hash = "sha256:8c73c1a2ec7c98d7eaded149f6d225a692caa1bd7b2401a14125446e9e90410d"}, - {file = "wrapt-1.13.3-cp35-cp35m-manylinux1_x86_64.whl", hash = "sha256:086218a72ec7d986a3eddb7707c8c4526d677c7b35e355875a0fe2918b059179"}, - {file = "wrapt-1.13.3-cp35-cp35m-manylinux2010_i686.whl", hash = "sha256:e92d0d4fa68ea0c02d39f1e2f9cb5bc4b4a71e8c442207433d8db47ee79d7aa3"}, - {file = "wrapt-1.13.3-cp35-cp35m-manylinux2010_x86_64.whl", hash = "sha256:d4a5f6146cfa5c7ba0134249665acd322a70d1ea61732723c7d3e8cc0fa80755"}, - {file = "wrapt-1.13.3-cp35-cp35m-win32.whl", hash = "sha256:8aab36778fa9bba1a8f06a4919556f9f8c7b33102bd71b3ab307bb3fecb21851"}, - {file = "wrapt-1.13.3-cp35-cp35m-win_amd64.whl", hash = "sha256:944b180f61f5e36c0634d3202ba8509b986b5fbaf57db3e94df11abee244ba13"}, - {file = "wrapt-1.13.3-cp36-cp36m-macosx_10_9_x86_64.whl", hash = "sha256:2ebdde19cd3c8cdf8df3fc165bc7827334bc4e353465048b36f7deeae8ee0918"}, - {file = "wrapt-1.13.3-cp36-cp36m-manylinux_2_5_i686.manylinux1_i686.manylinux_2_12_i686.manylinux2010_i686.whl", hash = "sha256:610f5f83dd1e0ad40254c306f4764fcdc846641f120c3cf424ff57a19d5f7ade"}, - {file = "wrapt-1.13.3-cp36-cp36m-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:5601f44a0f38fed36cc07db004f0eedeaadbdcec90e4e90509480e7e6060a5bc"}, - {file = "wrapt-1.13.3-cp36-cp36m-musllinux_1_1_i686.whl", hash = "sha256:e6906d6f48437dfd80464f7d7af1740eadc572b9f7a4301e7dd3d65db285cacf"}, - {file = "wrapt-1.13.3-cp36-cp36m-musllinux_1_1_x86_64.whl", hash = "sha256:766b32c762e07e26f50d8a3468e3b4228b3736c805018e4b0ec8cc01ecd88125"}, - {file = "wrapt-1.13.3-cp36-cp36m-win32.whl", hash = "sha256:5f223101f21cfd41deec8ce3889dc59f88a59b409db028c469c9b20cfeefbe36"}, - {file = "wrapt-1.13.3-cp36-cp36m-win_amd64.whl", hash = "sha256:f122ccd12fdc69628786d0c947bdd9cb2733be8f800d88b5a37c57f1f1d73c10"}, - {file = "wrapt-1.13.3-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:46f7f3af321a573fc0c3586612db4decb7eb37172af1bc6173d81f5b66c2e068"}, - {file = "wrapt-1.13.3-cp37-cp37m-manylinux_2_5_i686.manylinux1_i686.manylinux_2_12_i686.manylinux2010_i686.whl", hash = "sha256:778fd096ee96890c10ce96187c76b3e99b2da44e08c9e24d5652f356873f6709"}, - {file = "wrapt-1.13.3-cp37-cp37m-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:0cb23d36ed03bf46b894cfec777eec754146d68429c30431c99ef28482b5c1df"}, - {file = "wrapt-1.13.3-cp37-cp37m-musllinux_1_1_i686.whl", hash = "sha256:96b81ae75591a795d8c90edc0bfaab44d3d41ffc1aae4d994c5aa21d9b8e19a2"}, - {file = "wrapt-1.13.3-cp37-cp37m-musllinux_1_1_x86_64.whl", hash = "sha256:7dd215e4e8514004c8d810a73e342c536547038fb130205ec4bba9f5de35d45b"}, - {file = "wrapt-1.13.3-cp37-cp37m-win32.whl", hash = "sha256:47f0a183743e7f71f29e4e21574ad3fa95676136f45b91afcf83f6a050914829"}, - {file = "wrapt-1.13.3-cp37-cp37m-win_amd64.whl", hash = "sha256:fd76c47f20984b43d93de9a82011bb6e5f8325df6c9ed4d8310029a55fa361ea"}, - {file = "wrapt-1.13.3-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:b73d4b78807bd299b38e4598b8e7bd34ed55d480160d2e7fdaabd9931afa65f9"}, - {file = "wrapt-1.13.3-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_12_i686.manylinux2010_i686.whl", hash = "sha256:ec9465dd69d5657b5d2fa6133b3e1e989ae27d29471a672416fd729b429eb554"}, - {file = "wrapt-1.13.3-cp38-cp38-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:dd91006848eb55af2159375134d724032a2d1d13bcc6f81cd8d3ed9f2b8e846c"}, - {file = "wrapt-1.13.3-cp38-cp38-musllinux_1_1_i686.whl", hash = "sha256:ae9de71eb60940e58207f8e71fe113c639da42adb02fb2bcbcaccc1ccecd092b"}, - {file = "wrapt-1.13.3-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:51799ca950cfee9396a87f4a1240622ac38973b6df5ef7a41e7f0b98797099ce"}, - {file = "wrapt-1.13.3-cp38-cp38-win32.whl", hash = "sha256:4b9c458732450ec42578b5642ac53e312092acf8c0bfce140ada5ca1ac556f79"}, - {file = "wrapt-1.13.3-cp38-cp38-win_amd64.whl", hash = "sha256:7dde79d007cd6dfa65afe404766057c2409316135cb892be4b1c768e3f3a11cb"}, - {file = "wrapt-1.13.3-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:981da26722bebb9247a0601e2922cedf8bb7a600e89c852d063313102de6f2cb"}, - {file = "wrapt-1.13.3-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_12_i686.manylinux2010_i686.whl", hash = "sha256:705e2af1f7be4707e49ced9153f8d72131090e52be9278b5dbb1498c749a1e32"}, - {file = "wrapt-1.13.3-cp39-cp39-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:25b1b1d5df495d82be1c9d2fad408f7ce5ca8a38085e2da41bb63c914baadff7"}, - {file = "wrapt-1.13.3-cp39-cp39-musllinux_1_1_i686.whl", hash = "sha256:77416e6b17926d953b5c666a3cb718d5945df63ecf922af0ee576206d7033b5e"}, - {file = "wrapt-1.13.3-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:865c0b50003616f05858b22174c40ffc27a38e67359fa1495605f96125f76640"}, - {file = "wrapt-1.13.3-cp39-cp39-win32.whl", hash = "sha256:0a017a667d1f7411816e4bf214646d0ad5b1da2c1ea13dec6c162736ff25a374"}, - {file = "wrapt-1.13.3-cp39-cp39-win_amd64.whl", hash = "sha256:81bd7c90d28a4b2e1df135bfbd7c23aee3050078ca6441bead44c42483f9ebfb"}, - {file = "wrapt-1.13.3.tar.gz", hash = "sha256:1fea9cd438686e6682271d36f3481a9f3636195578bab9ca3382e2f5f01fc185"}, + {file = "wrapt-1.14.0-cp27-cp27m-macosx_10_9_x86_64.whl", hash = "sha256:5a9a1889cc01ed2ed5f34574c90745fab1dd06ec2eee663e8ebeefe363e8efd7"}, + {file = "wrapt-1.14.0-cp27-cp27m-manylinux1_i686.whl", hash = "sha256:9a3ff5fb015f6feb78340143584d9f8a0b91b6293d6b5cf4295b3e95d179b88c"}, + {file = "wrapt-1.14.0-cp27-cp27m-manylinux1_x86_64.whl", hash = "sha256:4b847029e2d5e11fd536c9ac3136ddc3f54bc9488a75ef7d040a3900406a91eb"}, + {file = "wrapt-1.14.0-cp27-cp27m-manylinux2010_i686.whl", hash = "sha256:9a5a544861b21e0e7575b6023adebe7a8c6321127bb1d238eb40d99803a0e8bd"}, + {file = "wrapt-1.14.0-cp27-cp27m-manylinux2010_x86_64.whl", hash = "sha256:88236b90dda77f0394f878324cfbae05ae6fde8a84d548cfe73a75278d760291"}, + {file = "wrapt-1.14.0-cp27-cp27mu-manylinux1_i686.whl", hash = "sha256:f0408e2dbad9e82b4c960274214af533f856a199c9274bd4aff55d4634dedc33"}, + {file = "wrapt-1.14.0-cp27-cp27mu-manylinux1_x86_64.whl", hash = "sha256:9d8c68c4145041b4eeae96239802cfdfd9ef927754a5be3f50505f09f309d8c6"}, + {file = "wrapt-1.14.0-cp27-cp27mu-manylinux2010_i686.whl", hash = "sha256:22626dca56fd7f55a0733e604f1027277eb0f4f3d95ff28f15d27ac25a45f71b"}, + {file = "wrapt-1.14.0-cp27-cp27mu-manylinux2010_x86_64.whl", hash = "sha256:65bf3eb34721bf18b5a021a1ad7aa05947a1767d1aa272b725728014475ea7d5"}, + {file = "wrapt-1.14.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:09d16ae7a13cff43660155383a2372b4aa09109c7127aa3f24c3cf99b891c330"}, + {file = "wrapt-1.14.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:debaf04f813ada978d7d16c7dfa16f3c9c2ec9adf4656efdc4defdf841fc2f0c"}, + {file = "wrapt-1.14.0-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:748df39ed634851350efa87690c2237a678ed794fe9ede3f0d79f071ee042561"}, + {file = "wrapt-1.14.0-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:1807054aa7b61ad8d8103b3b30c9764de2e9d0c0978e9d3fc337e4e74bf25faa"}, + {file = "wrapt-1.14.0-cp310-cp310-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:763a73ab377390e2af26042f685a26787c402390f682443727b847e9496e4a2a"}, + {file = "wrapt-1.14.0-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:8529b07b49b2d89d6917cfa157d3ea1dfb4d319d51e23030664a827fe5fd2131"}, + {file = "wrapt-1.14.0-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:68aeefac31c1f73949662ba8affaf9950b9938b712fb9d428fa2a07e40ee57f8"}, + {file = "wrapt-1.14.0-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:59d7d92cee84a547d91267f0fea381c363121d70fe90b12cd88241bd9b0e1763"}, + {file = "wrapt-1.14.0-cp310-cp310-win32.whl", hash = "sha256:3a88254881e8a8c4784ecc9cb2249ff757fd94b911d5df9a5984961b96113fff"}, + {file = "wrapt-1.14.0-cp310-cp310-win_amd64.whl", hash = "sha256:9a242871b3d8eecc56d350e5e03ea1854de47b17f040446da0e47dc3e0b9ad4d"}, + {file = "wrapt-1.14.0-cp35-cp35m-manylinux1_i686.whl", hash = "sha256:a65bffd24409454b889af33b6c49d0d9bcd1a219b972fba975ac935f17bdf627"}, + {file = "wrapt-1.14.0-cp35-cp35m-manylinux1_x86_64.whl", hash = "sha256:9d9fcd06c952efa4b6b95f3d788a819b7f33d11bea377be6b8980c95e7d10775"}, + {file = "wrapt-1.14.0-cp35-cp35m-manylinux2010_i686.whl", hash = "sha256:db6a0ddc1282ceb9032e41853e659c9b638789be38e5b8ad7498caac00231c23"}, + {file = "wrapt-1.14.0-cp35-cp35m-manylinux2010_x86_64.whl", hash = "sha256:14e7e2c5f5fca67e9a6d5f753d21f138398cad2b1159913ec9e9a67745f09ba3"}, + {file = "wrapt-1.14.0-cp35-cp35m-win32.whl", hash = "sha256:6d9810d4f697d58fd66039ab959e6d37e63ab377008ef1d63904df25956c7db0"}, + {file = "wrapt-1.14.0-cp35-cp35m-win_amd64.whl", hash = "sha256:d808a5a5411982a09fef6b49aac62986274ab050e9d3e9817ad65b2791ed1425"}, + {file = "wrapt-1.14.0-cp36-cp36m-macosx_10_9_x86_64.whl", hash = "sha256:b77159d9862374da213f741af0c361720200ab7ad21b9f12556e0eb95912cd48"}, + {file = "wrapt-1.14.0-cp36-cp36m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:36a76a7527df8583112b24adc01748cd51a2d14e905b337a6fefa8b96fc708fb"}, + {file = "wrapt-1.14.0-cp36-cp36m-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:a0057b5435a65b933cbf5d859cd4956624df37b8bf0917c71756e4b3d9958b9e"}, + {file = "wrapt-1.14.0-cp36-cp36m-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:3a0a4ca02752ced5f37498827e49c414d694ad7cf451ee850e3ff160f2bee9d3"}, + {file = "wrapt-1.14.0-cp36-cp36m-musllinux_1_1_aarch64.whl", hash = "sha256:8c6be72eac3c14baa473620e04f74186c5d8f45d80f8f2b4eda6e1d18af808e8"}, + {file = "wrapt-1.14.0-cp36-cp36m-musllinux_1_1_i686.whl", hash = "sha256:21b1106bff6ece8cb203ef45b4f5778d7226c941c83aaaa1e1f0f4f32cc148cd"}, + {file = "wrapt-1.14.0-cp36-cp36m-musllinux_1_1_x86_64.whl", hash = "sha256:493da1f8b1bb8a623c16552fb4a1e164c0200447eb83d3f68b44315ead3f9036"}, + {file = "wrapt-1.14.0-cp36-cp36m-win32.whl", hash = "sha256:89ba3d548ee1e6291a20f3c7380c92f71e358ce8b9e48161401e087e0bc740f8"}, + {file = "wrapt-1.14.0-cp36-cp36m-win_amd64.whl", hash = "sha256:729d5e96566f44fccac6c4447ec2332636b4fe273f03da128fff8d5559782b06"}, + {file = "wrapt-1.14.0-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:891c353e95bb11abb548ca95c8b98050f3620a7378332eb90d6acdef35b401d4"}, + {file = "wrapt-1.14.0-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:23f96134a3aa24cc50614920cc087e22f87439053d886e474638c68c8d15dc80"}, + {file = "wrapt-1.14.0-cp37-cp37m-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:6807bcee549a8cb2f38f73f469703a1d8d5d990815c3004f21ddb68a567385ce"}, + {file = "wrapt-1.14.0-cp37-cp37m-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6915682f9a9bc4cf2908e83caf5895a685da1fbd20b6d485dafb8e218a338279"}, + {file = "wrapt-1.14.0-cp37-cp37m-musllinux_1_1_aarch64.whl", hash = "sha256:f2f3bc7cd9c9fcd39143f11342eb5963317bd54ecc98e3650ca22704b69d9653"}, + {file = "wrapt-1.14.0-cp37-cp37m-musllinux_1_1_i686.whl", hash = "sha256:3a71dbd792cc7a3d772ef8cd08d3048593f13d6f40a11f3427c000cf0a5b36a0"}, + {file = "wrapt-1.14.0-cp37-cp37m-musllinux_1_1_x86_64.whl", hash = "sha256:5a0898a640559dec00f3614ffb11d97a2666ee9a2a6bad1259c9facd01a1d4d9"}, + {file = "wrapt-1.14.0-cp37-cp37m-win32.whl", hash = "sha256:167e4793dc987f77fd476862d32fa404d42b71f6a85d3b38cbce711dba5e6b68"}, + {file = "wrapt-1.14.0-cp37-cp37m-win_amd64.whl", hash = "sha256:d066ffc5ed0be00cd0352c95800a519cf9e4b5dd34a028d301bdc7177c72daf3"}, + {file = "wrapt-1.14.0-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:d9bdfa74d369256e4218000a629978590fd7cb6cf6893251dad13d051090436d"}, + {file = "wrapt-1.14.0-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:2498762814dd7dd2a1d0248eda2afbc3dd9c11537bc8200a4b21789b6df6cd38"}, + {file = "wrapt-1.14.0-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:5f24ca7953f2643d59a9c87d6e272d8adddd4a53bb62b9208f36db408d7aafc7"}, + {file = "wrapt-1.14.0-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:5b835b86bd5a1bdbe257d610eecab07bf685b1af2a7563093e0e69180c1d4af1"}, + {file = "wrapt-1.14.0-cp38-cp38-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:b21650fa6907e523869e0396c5bd591cc326e5c1dd594dcdccac089561cacfb8"}, + {file = "wrapt-1.14.0-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:354d9fc6b1e44750e2a67b4b108841f5f5ea08853453ecbf44c81fdc2e0d50bd"}, + {file = "wrapt-1.14.0-cp38-cp38-musllinux_1_1_i686.whl", hash = "sha256:1f83e9c21cd5275991076b2ba1cd35418af3504667affb4745b48937e214bafe"}, + {file = "wrapt-1.14.0-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:61e1a064906ccba038aa3c4a5a82f6199749efbbb3cef0804ae5c37f550eded0"}, + {file = "wrapt-1.14.0-cp38-cp38-win32.whl", hash = "sha256:28c659878f684365d53cf59dc9a1929ea2eecd7ac65da762be8b1ba193f7e84f"}, + {file = "wrapt-1.14.0-cp38-cp38-win_amd64.whl", hash = "sha256:b0ed6ad6c9640671689c2dbe6244680fe8b897c08fd1fab2228429b66c518e5e"}, + {file = "wrapt-1.14.0-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:b3f7e671fb19734c872566e57ce7fc235fa953d7c181bb4ef138e17d607dc8a1"}, + {file = "wrapt-1.14.0-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:87fa943e8bbe40c8c1ba4086971a6fefbf75e9991217c55ed1bcb2f1985bd3d4"}, + {file = "wrapt-1.14.0-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:4775a574e9d84e0212f5b18886cace049a42e13e12009bb0491562a48bb2b758"}, + {file = "wrapt-1.14.0-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:9d57677238a0c5411c76097b8b93bdebb02eb845814c90f0b01727527a179e4d"}, + {file = "wrapt-1.14.0-cp39-cp39-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:00108411e0f34c52ce16f81f1d308a571df7784932cc7491d1e94be2ee93374b"}, + {file = "wrapt-1.14.0-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:d332eecf307fca852d02b63f35a7872de32d5ba8b4ec32da82f45df986b39ff6"}, + {file = "wrapt-1.14.0-cp39-cp39-musllinux_1_1_i686.whl", hash = "sha256:01f799def9b96a8ec1ef6b9c1bbaf2bbc859b87545efbecc4a78faea13d0e3a0"}, + {file = "wrapt-1.14.0-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:47045ed35481e857918ae78b54891fac0c1d197f22c95778e66302668309336c"}, + {file = "wrapt-1.14.0-cp39-cp39-win32.whl", hash = "sha256:2eca15d6b947cfff51ed76b2d60fd172c6ecd418ddab1c5126032d27f74bc350"}, + {file = "wrapt-1.14.0-cp39-cp39-win_amd64.whl", hash = "sha256:bb36fbb48b22985d13a6b496ea5fb9bb2a076fea943831643836c9f6febbcfdc"}, + {file = "wrapt-1.14.0.tar.gz", hash = "sha256:8323a43bd9c91f62bb7d4be74cc9ff10090e7ef820e27bfe8815c57e68261311"}, ] zipp = [ - {file = "zipp-3.7.0-py3-none-any.whl", hash = "sha256:b47250dd24f92b7dd6a0a8fc5244da14608f3ca90a5efcd37a3b1642fac9a375"}, - {file = "zipp-3.7.0.tar.gz", hash = "sha256:9f50f446828eb9d45b267433fd3e9da8d801f614129124863f9c51ebceafb87d"}, + {file = "zipp-3.8.0-py3-none-any.whl", hash = "sha256:c4f6e5bbf48e74f7a38e7cc5b0480ff42b0ae5178957d564d18932525d5cf099"}, + {file = "zipp-3.8.0.tar.gz", hash = "sha256:56bf8aadb83c24db6c4b577e13de374ccfb67da2078beba1d037c17980bf43ad"}, ] diff --git a/pyproject.toml b/pyproject.toml index 56ca79c1..ecfab59f 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -12,8 +12,8 @@ python = "^3.9" # core requirements django = ">=3.2, <4" django-inet = "^1.0" -django-handleref = "^1.0.1" -django-peeringdb = "==2.12.0" +django-handleref = "^1.0.2" +django-peeringdb = "==2.13.0" djangorestframework = ">=3.12,<3.13" mysqlclient = ">=1.3.9" peeringdb = ">=1.1.0, <2" @@ -40,7 +40,10 @@ django-oauth-toolkit = "==1.6.1" django-phonenumber-field = ">=0.6" django-ratelimit = ">=3" django-rest-swagger = ">=2.1.2" -djangorestframework-api-key = ">=2.0.0" + +# FIXME: djangorestframework-api-key 2.2.0 breaks migrations +djangorestframework-api-key = "==2.1.0" + django-tables2 = ">=1.0.4" django-vanilla-views = ">=1.0.2" django-security-keys = "^1.0.1" @@ -61,6 +64,7 @@ grainy = ">=1.7,<2" django-grainy = ">=1.9.1,<2" django-haystack = "<4" whoosh = "<3" +django-structlog = ">=2.2.0" [tool.poetry.dev-dependencies] diff --git a/tests/test_api_throttle.py b/tests/test_api_throttle.py index d5f26be7..bdc372b9 100644 --- a/tests/test_api_throttle.py +++ b/tests/test_api_throttle.py @@ -1,12 +1,18 @@ import pytest from django.core.cache import cache +from django.core.management import call_command from django.test import TestCase from rest_framework.response import Response -from rest_framework.test import APIRequestFactory +from rest_framework.test import APIClient, APIRequestFactory from peeringdb_server import models from peeringdb_server.rest import ModelViewSet -from peeringdb_server.rest_throttles import APIAnonUserThrottle, APIUserThrottle +from peeringdb_server.rest_throttles import ( + APIAnonUserThrottle, + APIUserThrottle, + MelissaThrottle, + ResponseSizeThrottle, +) class MockView(ModelViewSet): @@ -20,6 +26,33 @@ class MockView(ModelViewSet): return Response("example") +class MelissaMockView(ModelViewSet): + """ + Dummy view for testing melissa throttling + """ + + throttle_classes = (MelissaThrottle,) + + def get(self, request): + return Response("example") + + +class ResponseSizeMockView(ModelViewSet): + + """ + Dummy view for testing thorttling based on expected response size (#1126) + """ + + size = 1000 + + throttle_classes = (ResponseSizeThrottle,) + + def get(self, request): + r = Response("x" * self.size) + ResponseSizeThrottle.cache_response_size(request, self.size) + return r + + class APIThrottleTests(TestCase): """ API tests @@ -42,6 +75,16 @@ class APIThrottleTests(TestCase): ) env.save() + env = models.EnvironmentSetting( + setting="API_THROTTLE_RATE_ANON_MSG", value_str="Rate limit exceeded (anon)" + ) + env.save() + + env = models.EnvironmentSetting( + setting="API_THROTTLE_RATE_USER_MSG", value_str="Rate limit exceeded (user)" + ) + env.save() + def test_environment_throttle_setting(self): """ Test if default throttle settings are overridden by environment settings @@ -54,6 +97,14 @@ class APIThrottleTests(TestCase): models.EnvironmentSetting.get_setting_value("API_THROTTLE_RATE_USER") == "10/minute" ) + assert ( + models.EnvironmentSetting.get_setting_value("API_THROTTLE_RATE_ANON_MSG") + == "Rate limit exceeded (anon)" + ) + assert ( + models.EnvironmentSetting.get_setting_value("API_THROTTLE_RATE_USER_MSG") + == "Rate limit exceeded (user)" + ) def test_anon_requests_below_throttle_rate(self): """ @@ -81,10 +132,12 @@ class APIThrottleTests(TestCase): """ Ensure request rate is limited for anonymous users """ + request = self.factory.get("/") for dummy in range(11): response = MockView.as_view({"get": "get"})(request) assert response.status_code == 429 + assert "Rate limit exceeded (anon)" in response.data["message"] def test_authenticated_requests_above_throttle_rate(self): """ @@ -98,3 +151,370 @@ class APIThrottleTests(TestCase): for dummy in range(11): response = MockView.as_view({"get": "get"})(request) assert response.status_code == 429 + assert "Rate limit exceeded (user)" in response.data["message"] + + def test_response_size_ip_block(self): + """ + Ensure request rate is limited based on response size + for ip-block + """ + + request = self.factory.get("/") + request.META.update({"REMOTE_ADDR": "10.10.10.10"}) + + # by default ip-block response size rate limiting is disabled + # ip 10.10.10.10 requesting 10 times (all should be ok) + + for dummy in range(10): + response = ResponseSizeMockView.as_view({"get": "get"})(request) + assert response.status_code == 200 + + # turn on response size throttling for responses bigger than 500 bytes + # for ip blocks + + thold = models.EnvironmentSetting.objects.create( + setting="API_THROTTLE_RESPONSE_SIZE_THRESHOLD_CIDR", value_int=500 + ) + models.EnvironmentSetting.objects.create( + setting="API_THROTTLE_RESPONSE_SIZE_RATE_CIDR", value_str="3/minute" + ) + models.EnvironmentSetting.objects.create( + setting="API_THROTTLE_RESPONSE_SIZE_ENABLED_CIDR", value_bool=True + ) + + # ip 10.10.10.10 requesting 3 times (all should be ok) + for dummy in range(3): + response = ResponseSizeMockView.as_view({"get": "get"})(request) + assert response.status_code == 200 + + # ip 10.10.10.10 requesting 4th time (rate limited) + response = ResponseSizeMockView.as_view({"get": "get"})(request) + assert response.status_code == 429 + + # ip 10.10.10.11 requesting 1st time (rate limited) + request.META.update(REMOTE_ADDR="10.10.10.11") + response = ResponseSizeMockView.as_view({"get": "get"})(request) + assert response.status_code == 429 + + # ip 20.10.10.10 requesting 1st time (ok) + request.META.update(REMOTE_ADDR="20.10.10.10") + response = ResponseSizeMockView.as_view({"get": "get"})(request) + assert response.status_code == 200 + + # increase threshold, no longer rate limited + thold.value_int = 5000 + thold.save() + + # 10.10.10.10 requesting 3 times (all should be ok) + request.META.update(REMOTE_ADDR="10.10.10.10") + for dummy in range(3): + response = ResponseSizeMockView.as_view({"get": "get"})(request) + assert response.status_code == 200 + + def test_response_size_ip(self): + """ + Ensure request rate is limited based on response size + for ip-address + """ + + request = self.factory.get("/") + request.META.update({"REMOTE_ADDR": "10.10.10.10"}) + + # by default ip-address response size rate limiting is disabled + # ip 10.10.10.10 requesting 10 times (all should be ok) + + for dummy in range(10): + response = ResponseSizeMockView.as_view({"get": "get"})(request) + assert response.status_code == 200 + + # turn on response size throttling for responses bigger than 500 bytes + # for ip addresses + + thold = models.EnvironmentSetting.objects.create( + setting="API_THROTTLE_RESPONSE_SIZE_THRESHOLD_IP", value_int=500 + ) + models.EnvironmentSetting.objects.create( + setting="API_THROTTLE_RESPONSE_SIZE_RATE_IP", value_str="3/minute" + ) + models.EnvironmentSetting.objects.create( + setting="API_THROTTLE_RESPONSE_SIZE_ENABLED_IP", value_bool=True + ) + + # ip 10.10.10.10 requesting 3 times (all should be ok) + for dummy in range(3): + response = ResponseSizeMockView.as_view({"get": "get"})(request) + assert response.status_code == 200 + + # ip 10.10.10.10 requesting 4th time (rate limited) + response = ResponseSizeMockView.as_view({"get": "get"})(request) + assert response.status_code == 429 + + # ip 10.10.10.11 requesting 1st time (ok) + request.META.update(REMOTE_ADDR="10.10.10.11") + response = ResponseSizeMockView.as_view({"get": "get"})(request) + assert response.status_code == 200 + + # ip 20.10.10.10 requesting 1st time (ok) + request.META.update(REMOTE_ADDR="20.10.10.10") + response = ResponseSizeMockView.as_view({"get": "get"})(request) + assert response.status_code == 200 + + # increase threshold, no longer rate limited + thold.value_int = 5000 + thold.save() + + # 10.10.10.10 requesting 3 times (all should be ok) + request.META.update(REMOTE_ADDR="10.10.10.10") + for dummy in range(3): + response = ResponseSizeMockView.as_view({"get": "get"})(request) + assert response.status_code == 200 + + def test_response_size_user(self): + """ + Ensure request rate is limited based on response size + for authenticated users + """ + + user = models.User.objects.create_user(username="test") + user_b = models.User.objects.create_user(username="test_2") + request = self.factory.get("/") + request.user = user + + # by default user response size rate limiting is disabled + # ip 10.10.10.10 requesting 10 times (all should be ok) + + for dummy in range(10): + response = ResponseSizeMockView.as_view({"get": "get"})(request) + assert response.status_code == 200 + + # turn on response size throttling for responses bigger than 500 bytes + # for ip addresses + + thold = models.EnvironmentSetting.objects.create( + setting="API_THROTTLE_RESPONSE_SIZE_THRESHOLD_USER", value_int=500 + ) + models.EnvironmentSetting.objects.create( + setting="API_THROTTLE_RESPONSE_SIZE_RATE_USER", value_str="3/minute" + ) + models.EnvironmentSetting.objects.create( + setting="API_THROTTLE_RESPONSE_SIZE_ENABLED_USER", value_bool=True + ) + + # user requesting 3 times (all should be ok) + for dummy in range(3): + response = ResponseSizeMockView.as_view({"get": "get"})(request) + assert response.status_code == 200 + + # user requesting 4th time (rate limited) + response = ResponseSizeMockView.as_view({"get": "get"})(request) + assert response.status_code == 429 + + # diff user requesting 1st time (ok) + request.user = user_b + response = ResponseSizeMockView.as_view({"get": "get"})(request) + assert response.status_code == 200 + + # increase threshold, no longer rate limited + request.user = user + thold.value_int = 5000 + thold.save() + + # user requesting 3 times (all should be ok) + for dummy in range(3): + response = ResponseSizeMockView.as_view({"get": "get"})(request) + assert response.status_code == 200 + + def test_response_size_org_key(self): + """ + Ensure request rate is limited based on response size + for organizations + """ + + org = models.Organization.objects.create(name="test", status="ok") + org_b = models.Organization.objects.create(name="test b", status="ok") + + _, key = models.OrganizationAPIKey.objects.create_key( + name="test", org=org, email="test@localhost" + ) + _, key_b = models.OrganizationAPIKey.objects.create_key( + name="test b", org=org_b, email="test@localhost" + ) + + request = self.factory.get("/") + request.META["HTTP_AUTHORIZATION"] = f"Api-Key {key}" + + # by default user response size rate limiting is disabled + # requesting 10 times (all should be ok) + + for dummy in range(10): + response = ResponseSizeMockView.as_view({"get": "get"})(request) + assert response.status_code == 200 + + # turn on response size throttling for responses bigger than 500 bytes + # for ip addresses + + thold = models.EnvironmentSetting.objects.create( + setting="API_THROTTLE_RESPONSE_SIZE_THRESHOLD_ORG", value_int=500 + ) + models.EnvironmentSetting.objects.create( + setting="API_THROTTLE_RESPONSE_SIZE_RATE_ORG", value_str="3/minute" + ) + models.EnvironmentSetting.objects.create( + setting="API_THROTTLE_RESPONSE_SIZE_ENABLED_ORG", value_bool=True + ) + + # requesting 3 times (all should be ok) + for dummy in range(3): + response = ResponseSizeMockView.as_view({"get": "get"})(request) + assert response.status_code == 200 + + # requesting 4th time (rate limited) + response = ResponseSizeMockView.as_view({"get": "get"})(request) + assert response.status_code == 429 + + # diff org requesting 1st time (ok) + request.META.update(HTTP_AUTHORIZATION=f"Api-Key {key_b}") + response = ResponseSizeMockView.as_view({"get": "get"})(request) + assert response.status_code == 200 + + # increase threshold, no longer rate limited + thold.value_int = 5000 + thold.save() + + # requesting 3 times (all should be ok) + request.META.update(HTTP_AUTHORIZATION=f"Api-Key {key}") + for dummy in range(3): + response = ResponseSizeMockView.as_view({"get": "get"})(request) + assert response.status_code == 200 + + def test_melissa_ip(self): + """ + Ensure request rate is limited based on melissa enabled queries + for unauthenticated queries + """ + + request = self.factory.get("/api/fac", {"country": "US", "state": "IL"}) + request.META.update({"REMOTE_ADDR": "10.10.10.10"}) + + # by default melissa rate limiting is disabled + # requesting 10 times (all should be ok) + + for dummy in range(10): + response = MelissaMockView.as_view({"get": "get"})(request) + assert response.status_code == 200 + + # turn on response size throttling for responses bigger than 500 bytes + + models.EnvironmentSetting.objects.create( + setting="API_THROTTLE_MELISSA_RATE_IP", value_str="3/minute" + ) + models.EnvironmentSetting.objects.create( + setting="API_THROTTLE_MELISSA_ENABLED_IP", value_bool=True + ) + + # requesting 3 times (all should be ok) + for dummy in range(3): + response = MelissaMockView.as_view({"get": "get"})(request) + assert response.status_code == 200 + + # requesting 4th time (rate limited) + response = MelissaMockView.as_view({"get": "get"})(request) + assert response.status_code == 429 + assert "geo address normalization" in response.data["message"] + + # diff user requesting 1st time (ok) + request.META.update({"REMOTE_ADDR": "10.10.10.11"}) + response = MelissaMockView.as_view({"get": "get"})(request) + assert response.status_code == 200 + + def test_melissa_user(self): + """ + Ensure request rate is limited based on melissa enabled queries + for authenticated users + """ + + user = models.User.objects.create_user(username="test") + user_b = models.User.objects.create_user(username="test_2") + request = self.factory.get("/api/fac", {"country": "US", "state": "IL"}) + request.user = user + + # by default melissa rate limiting is disabled + # requesting 10 times (all should be ok) + + for dummy in range(10): + response = MelissaMockView.as_view({"get": "get"})(request) + assert response.status_code == 200 + + # turn on response size throttling for responses bigger than 500 bytes + + models.EnvironmentSetting.objects.create( + setting="API_THROTTLE_MELISSA_RATE_USER", value_str="3/minute" + ) + models.EnvironmentSetting.objects.create( + setting="API_THROTTLE_MELISSA_ENABLED_USER", value_bool=True + ) + + # requesting 3 times (all should be ok) + for dummy in range(3): + response = MelissaMockView.as_view({"get": "get"})(request) + assert response.status_code == 200 + + # requesting 4th time (rate limited) + response = MelissaMockView.as_view({"get": "get"})(request) + assert response.status_code == 429 + assert "geo address normalization" in response.data["message"] + + # diff user requesting 1st time (ok) + request.user = user_b + response = MelissaMockView.as_view({"get": "get"})(request) + assert response.status_code == 200 + + def test_melissa_org_key(self): + """ + Ensure request rate is limited based on melissa enabled queries + for organizations + """ + + org = models.Organization.objects.create(name="test", status="ok") + org_b = models.Organization.objects.create(name="test b", status="ok") + + _, key = models.OrganizationAPIKey.objects.create_key( + name="test", org=org, email="test@localhost" + ) + _, key_b = models.OrganizationAPIKey.objects.create_key( + name="test b", org=org_b, email="test@localhost" + ) + + request = self.factory.get("/api/fac", {"country": "US", "state": "IL"}) + request.META["HTTP_AUTHORIZATION"] = f"Api-Key {key}" + + # by default melissa rate limiting is disabled + # requesting 10 times (all should be ok) + + for dummy in range(10): + response = MelissaMockView.as_view({"get": "get"})(request) + assert response.status_code == 200 + + # turn on response size throttling for responses bigger than 500 bytes + + models.EnvironmentSetting.objects.create( + setting="API_THROTTLE_MELISSA_RATE_ORG", value_str="3/minute" + ) + models.EnvironmentSetting.objects.create( + setting="API_THROTTLE_MELISSA_ENABLED_ORG", value_bool=True + ) + + # requesting 3 times (all should be ok) + for dummy in range(3): + response = MelissaMockView.as_view({"get": "get"})(request) + assert response.status_code == 200 + + # requesting 4th time (rate limited) + response = MelissaMockView.as_view({"get": "get"})(request) + assert response.status_code == 429 + assert "geo address normalization" in response.data["message"] + + # diff org requesting 1st time (ok) + request.META.update(HTTP_AUTHORIZATION=f"Api-Key {key_b}") + response = MelissaMockView.as_view({"get": "get"})(request) + assert response.status_code == 200 diff --git a/tests/test_middleware.py b/tests/test_middleware.py index 7cc9b134..8993f99d 100644 --- a/tests/test_middleware.py +++ b/tests/test_middleware.py @@ -1,7 +1,14 @@ from django.http import HttpResponse -from django.test import RequestFactory, SimpleTestCase, override_settings +from django.test import ( + RequestFactory, + SimpleTestCase, + modify_settings, + override_settings, +) +from rest_framework.test import APIClient, APITestCase from peeringdb_server.middleware import PDBCommonMiddleware +from peeringdb_server.models import User, UserAPIKey def get_response_empty(request): @@ -19,3 +26,44 @@ class PDBCommonMiddlewareTest(SimpleTestCase): r = PDBCommonMiddleware(get_response_empty).process_request(request) self.assertEqual(r.status_code, 301) self.assertEqual(r.url, "http://www.testserver/path/") + + +@modify_settings( + MIDDLEWARE={ + "append": "peeringdb_server.middleware.PDBPermissionMiddleware", + } +) +class PDBPermissionMiddlewareTest(APITestCase): + + client = APIClient() + + def test_bogus_apikey_auth_id_response(self): + + self.client.credentials(HTTP_AUTHORIZATION="Api-Key bogus") + response = self.client.get("/api/fac") + self.assertEqual(response.status_code, 401) + self.assertEqual(response.headers.get("X-Auth-ID"), "apikey_bogus") + + def test_bogus_credentials_auth_id_response(self): + + self.client.credentials(HTTP_AUTHORIZATION="Basic Ym9ndXM6Ym9ndXM=") + response = self.client.get("/api/fac") + self.assertEqual(response.status_code, 401) + self.assertEqual(response.headers.get("X-Auth-ID"), "bogus") + + def test_auth_id_response(self): + user = User.objects.create(username="bogus") + user.set_password("bogus") + user.save() + + # Create an API key for the user + api_key, key = UserAPIKey.objects.create_key( + name="test", + user=user, + readonly=False, + ) + + self.client.credentials(HTTP_AUTHORIZATION="Api-Key %s" % key) + response = self.client.get("/api/fac") + self.assertEqual(response.status_code, 200) + assert response.headers.get("X-Auth-ID").startswith("apikey_") diff --git a/tests/test_views.py b/tests/test_views.py index 73822153..3ce5dc2a 100644 --- a/tests/test_views.py +++ b/tests/test_views.py @@ -9,6 +9,8 @@ from django_grainy.models import Group from rest_framework.test import APIClient from peeringdb_server.models import ( + Facility, + InternetExchange, Network, Organization, User, @@ -206,3 +208,39 @@ def test_bogus_basic_auth(): client = Client() response = client.get("/", **auth_headers) assert response.status_code == 401 + + +@pytest.mark.django_db +def test_pending_view(): + client = Client() + + org = Organization.objects.create(name="test org") + org.save() + + ix = InternetExchange.objects.create(name="test ix", org_id=org.id) + ix.save() + + fac = Facility.objects.create(name="test fac", org_id=org.id) + fac.save() + + # set object status to pending + + org.status = "pending" + org.save() + + ix.status = "pending" + ix.save() + + fac.status = "pending" + fac.save() + + # assert that pending objects returns 404 + + response = client.get(f"/org/{org.id}") + assert response.status_code == 404 + + response = client.get(f"/ix/{ix.id}") + assert response.status_code == 404 + + response = client.get(f"/fac/{fac.id}") + assert response.status_code == 404