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 %}
{% if permissions.can_edit and n.status != "ok" %}
{% if permissions.can_edit and n.status != "ok" %}
@@ -184,7 +184,7 @@
×
{% endif %}
{% 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