Merge branch 'main' into nl/1064-Change-domain-application-to-domain-requests

This commit is contained in:
CocoByte 2024-03-07 13:57:44 -07:00
commit c2679ff9b5
No known key found for this signature in database
GPG key ID: BBFAA2526384C97F
23 changed files with 805 additions and 1058 deletions

46
.github/workflows/createcachetable.yaml vendored Normal file
View file

@ -0,0 +1,46 @@
# This workflow can be run from the CLI for any environment
# gh workflow run createcachetable.yaml -f environment=ENVIRONMENT
# OR
# cf run-task getgov-ENVIRONMENT --command 'python manage.py createcachetable' --name createcachetable
name: Create cache table
run-name: Create cache table for ${{ github.event.inputs.environment }}
on:
workflow_dispatch:
inputs:
environment:
type: choice
description: Which environment should we create cache table for?
options:
- stable
- staging
- development
- backup
- ky
- es
- nl
- rh
- za
- gd
- rb
- ko
- ab
- rjm
- dk
jobs:
createcachetable:
runs-on: ubuntu-latest
env:
CF_USERNAME: CF_${{ github.event.inputs.environment }}_USERNAME
CF_PASSWORD: CF_${{ github.event.inputs.environment }}_PASSWORD
steps:
- name: Create cache table for ${{ github.event.inputs.environment }}
uses: cloud-gov/cg-cli-tools@main
with:
cf_username: ${{ secrets[env.CF_USERNAME] }}
cf_password: ${{ secrets[env.CF_PASSWORD] }}
cf_org: cisa-dotgov
cf_space: ${{ github.event.inputs.environment }}
cf_command: "run-task getgov-${{ github.event.inputs.environment }} --command 'python manage.py createcachetable' --name createcachetable"

View file

@ -0,0 +1,74 @@
# 24. Production Release Cadence
Date: 2024-14-02
## Status
In Review
## Context
We experienced problems with our Cloudfront caching infrastructure early in our November launch. In response, we turned off caching across the application. We would like to utilize caching again without incurring the same issues.
Details:
Originally, Cloudfront was utilized to provide caching capabilities in our application. All incoming HTTP requests first go through a Cloudfront endpoint, which has a caching infrastructure enabled by default. Cloudfront then decides whether to pass each request to our running Django app inside cloud.gov or if it will respond to with cached data. The big problem with this feature is Cloudfront's caching has a default timeout of 24-hours, which we cannot control. This led to issues on our November launch; Incidents reported include the following...
- Users couldn't utilize login.gov properly and had to wait a day before they would be able to login. This was traced back to the 24-hour cache timeout.
- Changes made by admins would not be reflected in the app (due to the cached data not updating)
To resolve these issues, we added "no cache" headers throughout our application. Currently, every single HTTP response that comes from Django says "Cache control: no cache" in the headers, which instructs Cloudfront not to cache the associated data. This effectively removes Cloudfront caching for us.
Although we could leave our architecture as-is, we decided to investigate options for improving our use of caching (instead of just disabling it completely).
## Considered Options
**Option 1:** Cache static resources using Whitenoise
Caching static resources should pose little risk to our application's functionality. Currently, every static resource from /public/... is hitting our Django application inside of Cloud.gov. We already use a Django plugin called whitenoise that can do hash-based linking to static assets so that they can be cached forever by Cloudfront. (If the content changes, then the hash changes, then it results in a different filename.)
See ticket [#1371](https://github.com/cisagov/manage.get.gov/issues/1371) for more information.
**Option 2:** Leave things as-is (we had some discussion on whether or not caching static pages will make enough of a difference to be worth the effort)
## Decision
We decided on Option 2 - leave things as-is (for now).
Preliminary analysis suggest that implementing caching on static pages will result in negligible improvements to our application load time. A quick look at Kibana logs suggests most of these resources take less than 10ms to load...
![Kibana RTR Logs](../doc-images/caching-rtr-logs.png)
If we look at average load times in Kibana (here is [the Kibana page with preloaded query](https://logs.fr.cloud.gov/app/visualize#/create?_a=(filters:!(('$state':(store:appState),meta:(alias:!n,disabled:!f,index:'logs-app*',key:'@cf.app',negate:!f,params:(query:getgov-stable),type:phrase),query:(match_phrase:('@cf.app':getgov-stable)))),linked:!f,query:(language:lucene,query:''),uiState:(),vis:(aggs:!((enabled:!t,id:'1',params:(customLabel:'Average%20Response%20Time%20in%20ms',field:rtr.response_time_ms),schema:metric,type:avg),(enabled:!t,id:'2',params:(drop_partials:!f,extended_bounds:(),field:'@timestamp',interval:d,min_doc_count:1,scaleMetricValues:!f,timeRange:(from:now-20d,to:now),useNormalizedEsInterval:!t),schema:segment,type:date_histogram)),params:(addLegend:!t,addTimeMarker:!f,addTooltip:!t,categoryAxes:!((id:CategoryAxis-1,labels:(filter:!t,show:!t,truncate:100),position:bottom,scale:(type:linear),show:!t,style:(),title:(),type:category)),grid:(categoryLines:!f),labels:(show:!f),legendPosition:right,seriesParams:!((data:(id:'1',label:'Average%20Response%20Time%20in%20ms'),drawLinesBetweenPoints:!t,lineWidth:2,mode:stacked,show:!t,showCircles:!t,type:histogram,valueAxis:ValueAxis-1)),thresholdLine:(color:%23E7664C,show:!f,style:full,value:10,width:1),times:!(),type:histogram,valueAxes:!((id:ValueAxis-1,labels:(filter:!f,rotate:0,show:!t,truncate:100),name:LeftAxis-1,position:left,scale:(mode:normal,type:linear),show:!t,style:(),title:(text:'Average%20Response%20Time%20in%20ms'),type:value))),title:'',type:histogram))&_g=(filters:!(),refreshInterval:(pause:!t,value:0),time:(from:now-2w,to:now))&indexPattern=logs-app*&type=histogram)), it looks like we are doing great for load times in stable (using the rtr.response_time_ms metric), staying under 200ms (in the last 4 weeks) and usually hovering around 40-80ms. Some google searching suggests that "an ideal page load time is between 0-2 seconds, but 3 seconds is also considered to be an acceptable score. Anything above 3 seconds increases the likelihood of visitors leaving your site." (Quote shamelessly copied from Sematex)
![Kibana Average Load Times Graph](../doc-images/caching-average-load-times.png)
NOTE: While we considered implementing caching in a sandbox (See footnote) in order to examine risks and benefits of OPTION 1 in more detail, this incurred more overhead than expected (mainly due to poor documentation). Therefore, we decided it was not worth the investment.
Therefore, implementing caching using Whitenoise is not currently worth it for the following reasons;
- Minimal gains: We would only be caching static files which would not result in a large performance boost
- Risks: Incurs risk of unforeseen loading issues (we cant entirely rule out that we wont run into issues like we did in our November launch incident). Although we dont think static files should pose a problem, due diligence would call us to monitor for any unforeseen issues that might arise, which adds cost to this project that doesnt seem proportional to the gains.
- Maintenance: We would have to provide custom settings in cloudfront (coordinated through Cameron) for any sandboxes and other environments where caching is enabled. If we move down the route of utilizing CDN, it would be good for every environment to have this service enabled so our dev environments reflect stable settings. This could possibly introduce some overhead and maintenance issues. (Although further investigation might reveal these to be negligible.)
Overall, it is recommended that we SHELVE this caching endeavor for a future scenario where we have exhausted other (likely more lucrative) options for performance improvements. If we then still need to make improvements to our load times, perhaps we can revisit this and examine caching not only static files, but other resources as well (with caution).
## Consequences
We will forgo (negligible) load-time improvements by leaving caching off.
## (Footnote - How to implement caching)
Here are notes for implementing caching using whitenoise should we decide to pick this up again in the future;
1 - Add caching capability to a sandbox using the following steps (or [following documentation for command line](https://cloud.gov/docs/services/external-domain-service/))
- Log-in to the cloud.gov website
- [Navigate to "Services"](https://dashboard.fr.cloud.gov/services). Click "Add Service"...
- Choose "Marketplace Service"
- For the fields, select Cloud Foundry, Organization = "cisa-dotgov", Space = "[your sandbox. eg. "nl"]". Click "Next"
- For the Service, select "External Domain". Click "Next"
- For the Plan, select "domain-with-cdn" (here is [documentation on CDN](https://cloud.gov/docs/management/custom-domains/))
- If you choose to bind the app, a JSON string will be required (we believe this should do it: {"domains": "example.gov"}, where "example.gov" is replaced with the domain name you want to use for this application)
Before you can continue, work with Cameron to setup the DNS in AWS (use the following documentation linked below):
https://cloud.gov/docs/services/external-domain-service/
- Once the DNS is setup, you *should* be able to continue. We did not test this.
2- Enable caching in the code with Whitenoise (see [documentation on Whitenoise Caching](https://whitenoise.readthedocs.io/en/latest/djangohtml#add-compression-and-caching-support))
3- Take performance measurements before/after caching is enabled to determine cost-benefits of implementing caching. (NOTE: [lighthouse](https://developer.chrome.com/blog/lighthouse-load-performance) might be useful for this step)

Binary file not shown.

After

Width:  |  Height:  |  Size: 434 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 200 KiB

View file

@ -90,6 +90,9 @@ cd src/
cd .. cd ..
cf push getgov-$1 -f ops/manifests/manifest-$1.yaml cf push getgov-$1 -f ops/manifests/manifest-$1.yaml
echo "Creating cache table..."
cf run-task getgov-$1 --command 'python manage.py createcachetable' --name createcachetable
read -p "Please provide the email of the space developer: " -r read -p "Please provide the email of the space developer: " -r
cf set-space-role $REPLY cisa-dotgov $1 SpaceDeveloper cf set-space-role $REPLY cisa-dotgov $1 SpaceDeveloper

View file

@ -29,7 +29,7 @@ django-login-required-middleware = "*"
greenlet = "*" greenlet = "*"
gevent = "*" gevent = "*"
fred-epplib = {git = "https://github.com/cisagov/epplib.git", ref = "master"} fred-epplib = {git = "https://github.com/cisagov/epplib.git", ref = "master"}
geventconnpool = {git = "https://github.com/rasky/geventconnpool.git", ref = "1bbb93a714a331a069adf27265fe582d9ba7ecd4"} tblib = "*"
[dev-packages] [dev-packages]
django-debug-toolbar = "*" django-debug-toolbar = "*"

583
src/Pipfile.lock generated
View file

@ -1,7 +1,7 @@
{ {
"_meta": { "_meta": {
"hash": { "hash": {
"sha256": "a672aeb8951fd850e90ad87c6f03cf71e2fc2b387d56fd3942361cb0b45bb449" "sha256": "b5d93b1b9ccafc37019276a222957544bab3f1f46b5dab8a0f2ffc2e5c9e1678"
}, },
"pipfile-spec": 6, "pipfile-spec": 6,
"requires": {}, "requires": {},
@ -32,29 +32,29 @@
}, },
"boto3": { "boto3": {
"hashes": [ "hashes": [
"sha256:65acfe7f1cf2a9b7df3d4edb87c8022e02685825bd1957e7bb678cc0d09f5e5f", "sha256:8b3f5cc7fbedcbb22271c328039df8a6ab343001e746e0cdb24774c426cadcf8",
"sha256:73f5ec89cb3ddb3ed577317889fd2f2df783f66b6502a9a4239979607e33bf74" "sha256:f201b6a416f809283d554c652211eecec9fe3a52ed4063dab3f3e7aea7571d9c"
], ],
"index": "pypi", "index": "pypi",
"markers": "python_version >= '3.8'", "markers": "python_version >= '3.8'",
"version": "==1.34.37" "version": "==1.34.54"
}, },
"botocore": { "botocore": {
"hashes": [ "hashes": [
"sha256:2a5bf33aacd2d970afd3d492e179e06ea98a5469030d5cfe7a2ad9995f7bb2ef", "sha256:4061ff4be3efcf53547ebadf2c94d419dfc8be7beec24e9fa1819599ffd936fa",
"sha256:3c46ddb1679e6ef45ca78b48665398636bda532a07cd476e4b500697d13d9a99" "sha256:bf215d93e9d5544c593962780d194e74c6ee40b883d0b885e62ef35fc0ec01e5"
], ],
"markers": "python_version >= '3.8'", "markers": "python_version >= '3.8'",
"version": "==1.34.37" "version": "==1.34.54"
}, },
"cachetools": { "cachetools": {
"hashes": [ "hashes": [
"sha256:086ee420196f7b2ab9ca2db2520aca326318b68fe5ba8bc4d49cca91add450f2", "sha256:0abad1021d3f8325b2fc1d2e9c8b9c9d57b04c3932657a72465447332c24d945",
"sha256:861f35a13a451f94e301ce2bec7cac63e881232ccce7ed67fab9b5df4d3beaa1" "sha256:ba29e2dfa0b8b556606f097407ed1aa62080ee108ab0dc5ec9d6a723a007d105"
], ],
"index": "pypi", "index": "pypi",
"markers": "python_version >= '3.7'", "markers": "python_version >= '3.7'",
"version": "==5.3.2" "version": "==5.3.3"
}, },
"certifi": { "certifi": {
"hashes": [ "hashes": [
@ -228,41 +228,41 @@
}, },
"cryptography": { "cryptography": {
"hashes": [ "hashes": [
"sha256:087887e55e0b9c8724cf05361357875adb5c20dec27e5816b653492980d20380", "sha256:0270572b8bd2c833c3981724b8ee9747b3ec96f699a9665470018594301439ee",
"sha256:09a77e5b2e8ca732a19a90c5bca2d124621a1edb5438c5daa2d2738bfeb02589", "sha256:111a0d8553afcf8eb02a4fea6ca4f59d48ddb34497aa8706a6cf536f1a5ec576",
"sha256:130c0f77022b2b9c99d8cebcdd834d81705f61c68e91ddd614ce74c657f8b3ea", "sha256:16a48c23a62a2f4a285699dba2e4ff2d1cff3115b9df052cdd976a18856d8e3d",
"sha256:141e2aa5ba100d3788c0ad7919b288f89d1fe015878b9659b307c9ef867d3a65", "sha256:1b95b98b0d2af784078fa69f637135e3c317091b615cd0905f8b8a087e86fa30",
"sha256:28cb2c41f131a5758d6ba6a0504150d644054fd9f3203a1e8e8d7ac3aea7f73a", "sha256:1f71c10d1e88467126f0efd484bd44bca5e14c664ec2ede64c32f20875c0d413",
"sha256:2f9f14185962e6a04ab32d1abe34eae8a9001569ee4edb64d2304bf0d65c53f3", "sha256:2424ff4c4ac7f6b8177b53c17ed5d8fa74ae5955656867f5a8affaca36a27abb",
"sha256:320948ab49883557a256eab46149df79435a22d2fefd6a66fe6946f1b9d9d008", "sha256:2bce03af1ce5a5567ab89bd90d11e7bbdff56b8af3acbbec1faded8f44cb06da",
"sha256:36d4b7c4be6411f58f60d9ce555a73df8406d484ba12a63549c88bd64f7967f1", "sha256:329906dcc7b20ff3cad13c069a78124ed8247adcac44b10bea1130e36caae0b4",
"sha256:3b15c678f27d66d247132cbf13df2f75255627bcc9b6a570f7d2fd08e8c081d2", "sha256:37dd623507659e08be98eec89323469e8c7b4c1407c85112634ae3dbdb926fdd",
"sha256:3dbd37e14ce795b4af61b89b037d4bc157f2cb23e676fa16932185a04dfbf635", "sha256:3eaafe47ec0d0ffcc9349e1708be2aaea4c6dd4978d76bf6eb0cb2c13636c6fc",
"sha256:4383b47f45b14459cab66048d384614019965ba6c1a1a141f11b5a551cace1b2", "sha256:5e6275c09d2badf57aea3afa80d975444f4be8d3bc58f7f80d2a484c6f9485c8",
"sha256:44c95c0e96b3cb628e8452ec060413a49002a247b2b9938989e23a2c8291fc90", "sha256:6fe07eec95dfd477eb9530aef5bead34fec819b3aaf6c5bd6d20565da607bfe1",
"sha256:4b063d3413f853e056161eb0c7724822a9740ad3caa24b8424d776cebf98e7ee", "sha256:7367d7b2eca6513681127ebad53b2582911d1736dc2ffc19f2c3ae49997496bc",
"sha256:52ed9ebf8ac602385126c9a2fe951db36f2cb0c2538d22971487f89d0de4065a", "sha256:7cde5f38e614f55e28d831754e8a3bacf9ace5d1566235e39d91b35502d6936e",
"sha256:55d1580e2d7e17f45d19d3b12098e352f3a37fe86d380bf45846ef257054b242", "sha256:9481ffe3cf013b71b2428b905c4f7a9a4f76ec03065b05ff499bb5682a8d9ad8",
"sha256:5ef9bc3d046ce83c4bbf4c25e1e0547b9c441c01d30922d812e887dc5f125c12", "sha256:98d8dc6d012b82287f2c3d26ce1d2dd130ec200c8679b6213b3c73c08b2b7940",
"sha256:5fa82a26f92871eca593b53359c12ad7949772462f887c35edaf36f87953c0e2", "sha256:a011a644f6d7d03736214d38832e030d8268bcff4a41f728e6030325fea3e400",
"sha256:61321672b3ac7aade25c40449ccedbc6db72c7f5f0fdf34def5e2f8b51ca530d", "sha256:a2913c5375154b6ef2e91c10b5720ea6e21007412f6437504ffea2109b5a33d7",
"sha256:701171f825dcab90969596ce2af253143b93b08f1a716d4b2a9d2db5084ef7be", "sha256:a30596bae9403a342c978fb47d9b0ee277699fa53bbafad14706af51fe543d16",
"sha256:841ec8af7a8491ac76ec5a9522226e287187a3107e12b7d686ad354bb78facee", "sha256:b03c2ae5d2f0fc05f9a2c0c997e1bc18c8229f392234e8a0194f202169ccd278",
"sha256:8a06641fb07d4e8f6c7dda4fc3f8871d327803ab6542e33831c7ccfdcb4d0ad6", "sha256:b6cd2203306b63e41acdf39aa93b86fb566049aeb6dc489b70e34bcd07adca74",
"sha256:8e88bb9eafbf6a4014d55fb222e7360eef53e613215085e65a13290577394529", "sha256:b7ffe927ee6531c78f81aa17e684e2ff617daeba7f189f911065b2ea2d526dec",
"sha256:a00aee5d1b6c20620161984f8ab2ab69134466c51f58c052c11b076715e72929", "sha256:b8cac287fafc4ad485b8a9b67d0ee80c66bf3574f655d3b97ef2e1082360faf1",
"sha256:a047682d324ba56e61b7ea7c7299d51e61fd3bca7dad2ccc39b72bd0118d60a1", "sha256:ba334e6e4b1d92442b75ddacc615c5476d4ad55cc29b15d590cc6b86efa487e2",
"sha256:a7ef8dd0bf2e1d0a27042b231a3baac6883cdd5557036f5e8df7139255feaac6", "sha256:ba3e4a42397c25b7ff88cdec6e2a16c2be18720f317506ee25210f6d31925f9c",
"sha256:ad28cff53f60d99a928dfcf1e861e0b2ceb2bc1f08a074fdd601b314e1cc9e0a", "sha256:c41fb5e6a5fe9ebcd58ca3abfeb51dffb5d83d6775405305bfa8715b76521922",
"sha256:b9097a208875fc7bbeb1286d0125d90bdfed961f61f214d3f5be62cd4ed8a446", "sha256:cd2030f6650c089aeb304cf093f3244d34745ce0cfcc39f20c6fbfe030102e2a",
"sha256:b97fe7d7991c25e6a31e5d5e795986b18fbbb3107b873d5f3ae6dc9a103278e9", "sha256:cd65d75953847815962c84a4654a84850b2bb4aed3f26fadcc1c13892e1e29f6",
"sha256:e0ec52ba3c7f1b7d813cd52649a5b3ef1fc0d433219dc8c93827c57eab6cf888", "sha256:e4985a790f921508f36f81831817cbc03b102d643b5fcb81cd33df3fa291a1a1",
"sha256:ea2c3ffb662fec8bbbfce5602e2c159ff097a4631d96235fcf0fb00e59e3ece4", "sha256:e807b3188f9eb0eaa7bbb579b462c5ace579f1cedb28107ce8b48a9f7ad3679e",
"sha256:fa3dec4ba8fb6e662770b74f62f1a0c7d4e37e25b58b2bf2c1be4c95372b4a33", "sha256:f12764b8fffc7a123f641d7d049d382b73f96a34117e0b637b80643169cec8ac",
"sha256:fbeb725c9dc799a574518109336acccaf1303c30d45c075c665c0793c2f79a7f" "sha256:f8837fe1d6ac4a8052a9a8ddab256bc006242696f03368a4009be7ee3075cdb7"
], ],
"markers": "python_version >= '3.7'", "markers": "python_version >= '3.7'",
"version": "==42.0.2" "version": "==42.0.5"
}, },
"defusedxml": { "defusedxml": {
"hashes": [ "hashes": [
@ -330,11 +330,11 @@
}, },
"django-csp": { "django-csp": {
"hashes": [ "hashes": [
"sha256:01443a07723f9a479d498bd7bb63571aaa771e690f64bde515db6cdb76e8041a", "sha256:19b2978b03fcd73517d7d67acbc04fbbcaec0facc3e83baa502965892d1e0719",
"sha256:01eda02ad3f10261c74131cdc0b5a6a62b7c7ad4fd017fbefb7a14776e0a9727" "sha256:ef0f1a9f7d8da68ae6e169c02e9ac661c0ecf04db70e0d1d85640512a68471c0"
], ],
"index": "pypi", "index": "pypi",
"version": "==3.7" "version": "==3.8"
}, },
"django-fsm": { "django-fsm": {
"hashes": [ "hashes": [
@ -384,12 +384,12 @@
}, },
"faker": { "faker": {
"hashes": [ "hashes": [
"sha256:60e89e5c0b584e285a7db05eceba35011a241954afdab2853cb246c8a56700a2", "sha256:117ce1a2805c1bc5ca753b3dc6f9d567732893b2294b827d3164261ee8f20267",
"sha256:b7f76bb1b2ac4cdc54442d955e36e477c387000f31ce46887fb9722a041be60b" "sha256:458d93580de34403a8dec1e8d5e6be2fee96c4deca63b95d71df7a6a80a690de"
], ],
"index": "pypi", "index": "pypi",
"markers": "python_version >= '3.8'", "markers": "python_version >= '3.8'",
"version": "==23.1.0" "version": "==23.3.0"
}, },
"fred-epplib": { "fred-epplib": {
"git": "https://github.com/cisagov/epplib.git", "git": "https://github.com/cisagov/epplib.git",
@ -404,61 +404,59 @@
}, },
"future": { "future": {
"hashes": [ "hashes": [
"sha256:34a17436ed1e96697a86f9de3d15a3b0be01d8bc8de9c1dffd59fb8234ed5307" "sha256:929292d34f5872e70396626ef385ec22355a1fae8ad29e1a734c3e43f9fbc216",
"sha256:bd2968309307861edae1458a4f8a4f3598c03be43b97521076aebf5d94c07b05"
], ],
"markers": "python_version >= '2.6' and python_version not in '3.0, 3.1, 3.2, 3.3'", "markers": "python_version >= '2.6' and python_version not in '3.0, 3.1, 3.2, 3.3'",
"version": "==0.18.3" "version": "==1.0.0"
}, },
"gevent": { "gevent": {
"hashes": [ "hashes": [
"sha256:272cffdf535978d59c38ed837916dfd2b5d193be1e9e5dcc60a5f4d5025dd98a", "sha256:03aa5879acd6b7076f6a2a307410fb1e0d288b84b03cdfd8c74db8b4bc882fc5",
"sha256:2c7b5c9912378e5f5ccf180d1fdb1e83f42b71823483066eddbe10ef1a2fcaa2", "sha256:117e5837bc74a1673605fb53f8bfe22feb6e5afa411f524c835b2ddf768db0de",
"sha256:36a549d632c14684bcbbd3014a6ce2666c5f2a500f34d58d32df6c9ea38b6535", "sha256:141a2b24ad14f7b9576965c0c84927fc85f824a9bb19f6ec1e61e845d87c9cd8",
"sha256:4368f341a5f51611411ec3fc62426f52ac3d6d42eaee9ed0f9eebe715c80184e", "sha256:14532a67f7cb29fb055a0e9b39f16b88ed22c66b96641df8c04bdc38c26b9ea5",
"sha256:43daf68496c03a35287b8b617f9f91e0e7c0d042aebcc060cadc3f049aadd653", "sha256:1dffb395e500613e0452b9503153f8f7ba587c67dd4a85fc7cd7aa7430cb02cc",
"sha256:455e5ee8103f722b503fa45dedb04f3ffdec978c1524647f8ba72b4f08490af1", "sha256:2955eea9c44c842c626feebf4459c42ce168685aa99594e049d03bedf53c2800",
"sha256:45792c45d60f6ce3d19651d7fde0bc13e01b56bb4db60d3f32ab7d9ec467374c", "sha256:2ae3a25ecce0a5b0cd0808ab716bfca180230112bb4bc89b46ae0061d62d4afe",
"sha256:4e24c2af9638d6c989caffc691a039d7c7022a31c0363da367c0d32ceb4a0648", "sha256:2e9ac06f225b696cdedbb22f9e805e2dd87bf82e8fa5e17756f94e88a9d37cf7",
"sha256:52b4abf28e837f1865a9bdeef58ff6afd07d1d888b70b6804557e7908032e599", "sha256:368a277bd9278ddb0fde308e6a43f544222d76ed0c4166e0d9f6b036586819d9",
"sha256:52e9f12cd1cda96603ce6b113d934f1aafb873e2c13182cf8e86d2c5c41982ea", "sha256:3adfb96637f44010be8abd1b5e73b5070f851b817a0b182e601202f20fa06533",
"sha256:5f3c781c84794926d853d6fb58554dc0dcc800ba25c41d42f6959c344b4db5a6", "sha256:3d5325ccfadfd3dcf72ff88a92fb8fc0b56cacc7225f0f4b6dcf186c1a6eeabc",
"sha256:62d121344f7465e3739989ad6b91f53a6ca9110518231553fe5846dbe1b4518f", "sha256:432fc76f680acf7cf188c2ee0f5d3ab73b63c1f03114c7cd8a34cebbe5aa2056",
"sha256:65883ac026731ac112184680d1f0f1e39fa6f4389fd1fc0bf46cc1388e2599f9", "sha256:44098038d5e2749b0784aabb27f1fcbb3f43edebedf64d0af0d26955611be8d6",
"sha256:707904027d7130ff3e59ea387dddceedb133cc742b00b3ffe696d567147a9c9e", "sha256:5a1df555431f5cd5cc189a6ee3544d24f8c52f2529134685f1e878c4972ab026",
"sha256:72c002235390d46f94938a96920d8856d4ffd9ddf62a303a0d7c118894097e34", "sha256:6c47ae7d1174617b3509f5d884935e788f325eb8f1a7efc95d295c68d83cce40",
"sha256:7532c17bc6c1cbac265e751b95000961715adef35a25d2b0b1813aa7263fb397", "sha256:6f947a9abc1a129858391b3d9334c45041c08a0f23d14333d5b844b6e5c17a07",
"sha256:78eebaf5e73ff91d34df48f4e35581ab4c84e22dd5338ef32714264063c57507", "sha256:782a771424fe74bc7e75c228a1da671578c2ba4ddb2ca09b8f959abdf787331e",
"sha256:7c1abc6f25f475adc33e5fc2dbcc26a732608ac5375d0d306228738a9ae14d3b", "sha256:7899a38d0ae7e817e99adb217f586d0a4620e315e4de577444ebeeed2c5729be",
"sha256:7c28e38dcde327c217fdafb9d5d17d3e772f636f35df15ffae2d933a5587addd", "sha256:7b00f8c9065de3ad226f7979154a7b27f3b9151c8055c162332369262fc025d8",
"sha256:7ccf0fd378257cb77d91c116e15c99e533374a8153632c48a3ecae7f7f4f09fe", "sha256:8f4b8e777d39013595a7740b4463e61b1cfe5f462f1b609b28fbc1e4c4ff01e5",
"sha256:921dda1c0b84e3d3b1778efa362d61ed29e2b215b90f81d498eb4d8eafcd0b7a", "sha256:90cbac1ec05b305a1b90ede61ef73126afdeb5a804ae04480d6da12c56378df1",
"sha256:a2898b7048771917d85a1d548fd378e8a7b2ca963db8e17c6d90c76b495e0e2b", "sha256:918cdf8751b24986f915d743225ad6b702f83e1106e08a63b736e3a4c6ead789",
"sha256:a3c5e9b1f766a7a64833334a18539a362fb563f6c4682f9634dea72cbe24f771", "sha256:9202f22ef811053077d01f43cc02b4aaf4472792f9fd0f5081b0b05c926cca19",
"sha256:ada07076b380918829250201df1d016bdafb3acf352f35e5693b59dceee8dd2e", "sha256:94138682e68ec197db42ad7442d3cf9b328069c3ad8e4e5022e6b5cd3e7ffae5",
"sha256:b101086f109168b23fa3586fccd1133494bdb97f86920a24dc0b23984dc30b69", "sha256:968581d1717bbcf170758580f5f97a2925854943c45a19be4d47299507db2eb7",
"sha256:bf456bd6b992eb0e1e869e2fd0caf817f0253e55ca7977fd0e72d0336a8c1c6a", "sha256:9d8d0642c63d453179058abc4143e30718b19a85cbf58c2744c9a63f06a1d388",
"sha256:bf7af500da05363e66f122896012acb6e101a552682f2352b618e541c941a011", "sha256:a7ceb59986456ce851160867ce4929edaffbd2f069ae25717150199f8e1548b8",
"sha256:c3e5d2fa532e4d3450595244de8ccf51f5721a05088813c1abd93ad274fe15e7", "sha256:b9913c45d1be52d7a5db0c63977eebb51f68a2d5e6fd922d1d9b5e5fd758cc98",
"sha256:c84d34256c243b0a53d4335ef0bc76c735873986d478c53073861a92566a8d71", "sha256:bde283313daf0b34a8d1bab30325f5cb0f4e11b5869dbe5bc61f8fe09a8f66f3",
"sha256:d163d59f1be5a4c4efcdd13c2177baaf24aadf721fdf2e1af9ee54a998d160f5", "sha256:bf5b9c72b884c6f0c4ed26ef204ee1f768b9437330422492c319470954bc4cc7",
"sha256:d57737860bfc332b9b5aa438963986afe90f49645f6e053140cfa0fa1bdae1ae", "sha256:ca80b121bbec76d7794fcb45e65a7eca660a76cc1a104ed439cdbd7df5f0b060",
"sha256:dbb22a9bbd6a13e925815ce70b940d1578dbe5d4013f20d23e8a11eddf8d14a7", "sha256:cdf66977a976d6a3cfb006afdf825d1482f84f7b81179db33941f2fc9673bb1d",
"sha256:dcb8612787a7f4626aa881ff15ff25439561a429f5b303048f0fca8a1c781c39", "sha256:d4faf846ed132fd7ebfbbf4fde588a62d21faa0faa06e6f468b7faa6f436b661",
"sha256:dd6c32ab977ecf7c7b8c2611ed95fa4aaebd69b74bf08f4b4960ad516861517d", "sha256:d7f87c2c02e03d99b95cfa6f7a776409083a9e4d468912e18c7680437b29222c",
"sha256:de350fde10efa87ea60d742901e1053eb2127ebd8b59a7d3b90597eb4e586599", "sha256:dd23df885318391856415e20acfd51a985cba6919f0be78ed89f5db9ff3a31cb",
"sha256:e1ead6863e596a8cc2a03e26a7a0981f84b6b3e956101135ff6d02df4d9a6b07", "sha256:f5de3c676e57177b38857f6e3cdfbe8f38d1cd754b63200c0615eaa31f514b4f",
"sha256:ed7a048d3e526a5c1d55c44cb3bc06cfdc1947d06d45006cc4cf60dedc628904", "sha256:f5e8e8d60e18d5f7fd49983f0c4696deeddaf6e608fbab33397671e2fcc6cc91",
"sha256:f632487c87866094546a74eefbca2c74c1d03638b715b6feb12e80120960185a", "sha256:f7cac622e11b4253ac4536a654fe221249065d9a69feb6cdcd4d9af3503602e0",
"sha256:fae8d5b5b8fa2a8f63b39f5447168b02db10c888a3e387ed7af2bd1b8612e543", "sha256:f8a04cf0c5b7139bc6368b461257d4a757ea2fe89b3773e494d235b7dd51119f",
"sha256:fde6402c5432b835fbb7698f1c7f2809c8d6b2bd9d047ac1f5a7c1d5aa569303" "sha256:f8bb35ce57a63c9a6896c71a285818a3922d8ca05d150fd1fe49a7f57287b836",
"sha256:fbfdce91239fe306772faab57597186710d5699213f4df099d1612da7320d682"
], ],
"index": "pypi", "index": "pypi",
"markers": "python_version >= '3.8'", "markers": "python_version >= '3.8'",
"version": "==23.9.1" "version": "==24.2.1"
},
"geventconnpool": {
"git": "https://github.com/rasky/geventconnpool.git",
"ref": "1bbb93a714a331a069adf27265fe582d9ba7ecd4"
}, },
"greenlet": { "greenlet": {
"hashes": [ "hashes": [
@ -710,11 +708,11 @@
}, },
"marshmallow": { "marshmallow": {
"hashes": [ "hashes": [
"sha256:4c1daff273513dc5eb24b219a8035559dc573c8f322558ef85f5438ddd1236dd", "sha256:20f53be28c6e374a711a16165fb22a8dc6003e3f7cda1285e3ca777b9193885b",
"sha256:c21d4b98fee747c130e6bc8f45c4b3199ea66bc00c12ee1f639f0aeca034d5e9" "sha256:e7997f83571c7fd476042c2c188e4ee8a78900ca5e74bd9c8097afa56624e9bd"
], ],
"markers": "python_version >= '3.8'", "markers": "python_version >= '3.8'",
"version": "==3.20.2" "version": "==3.21.0"
}, },
"oic": { "oic": {
"hashes": [ "hashes": [
@ -742,10 +740,10 @@
}, },
"phonenumberslite": { "phonenumberslite": {
"hashes": [ "hashes": [
"sha256:2b04a53401d01ab42564c1abc762fc9808ad398e71dacfa3b38d4321e112ecb3", "sha256:137d53d5d78dca30bc2becf81a3e2ac74deb8f0997e9bbe44de515ece4bd92bd",
"sha256:74e3ee63dfa2bb562ce2e6ce74ce76ae74a2f81472005b80343235fb43426db4" "sha256:e1f4359bff90c86d1b52db0e726d3334df00cc7d9c9c2ef66561d5f7a774d4ba"
], ],
"version": "==8.13.29" "version": "==8.13.31"
}, },
"psycopg2-binary": { "psycopg2-binary": {
"hashes": [ "hashes": [
@ -874,104 +872,104 @@
}, },
"pydantic": { "pydantic": {
"hashes": [ "hashes": [
"sha256:0b6a909df3192245cb736509a92ff69e4fef76116feffec68e93a567347bae6f", "sha256:72c6034df47f46ccdf81869fddb81aade68056003900a8724a4f160700016a2a",
"sha256:4fd5c182a2488dc63e6d32737ff19937888001e2a6d86e94b3f233104a5d1fa9" "sha256:e07805c4c7f5c6826e33a1d4c9d47950d7eaf34868e2690f8594d2e30241f11f"
], ],
"markers": "python_version >= '3.8'", "markers": "python_version >= '3.8'",
"version": "==2.6.1" "version": "==2.6.3"
}, },
"pydantic-core": { "pydantic-core": {
"hashes": [ "hashes": [
"sha256:02906e7306cb8c5901a1feb61f9ab5e5c690dbbeaa04d84c1b9ae2a01ebe9379", "sha256:00ee1c97b5364b84cb0bd82e9bbf645d5e2871fb8c58059d158412fee2d33d8a",
"sha256:0ba503850d8b8dcc18391f10de896ae51d37fe5fe43dbfb6a35c5c5cad271a06", "sha256:0d32576b1de5a30d9a97f300cc6a3f4694c428d956adbc7e6e2f9cad279e45ed",
"sha256:16aa02e7a0f539098e215fc193c8926c897175d64c7926d00a36188917717a05", "sha256:0df446663464884297c793874573549229f9eca73b59360878f382a0fc085979",
"sha256:18de31781cdc7e7b28678df7c2d7882f9692ad060bc6ee3c94eb15a5d733f8f7", "sha256:0f56ae86b60ea987ae8bcd6654a887238fd53d1384f9b222ac457070b7ac4cff",
"sha256:22c5f022799f3cd6741e24f0443ead92ef42be93ffda0d29b2597208c94c3753", "sha256:13dcc4802961b5f843a9385fc821a0b0135e8c07fc3d9949fd49627c1a5e6ae5",
"sha256:2924b89b16420712e9bb8192396026a8fbd6d8726224f918353ac19c4c043d2a", "sha256:162e498303d2b1c036b957a1278fa0899d02b2842f1ff901b6395104c5554a45",
"sha256:308974fdf98046db28440eb3377abba274808bf66262e042c412eb2adf852731", "sha256:1b662180108c55dfbf1280d865b2d116633d436cfc0bba82323554873967b340",
"sha256:396fdf88b1b503c9c59c84a08b6833ec0c3b5ad1a83230252a9e17b7dfb4cffc", "sha256:1cac689f80a3abab2d3c0048b29eea5751114054f032a941a32de4c852c59cad",
"sha256:3ac426704840877a285d03a445e162eb258924f014e2f074e209d9b4ff7bf380", "sha256:21b888c973e4f26b7a96491c0965a8a312e13be108022ee510248fe379a5fa23",
"sha256:3b052c753c4babf2d1edc034c97851f867c87d6f3ea63a12e2700f159f5c41c3", "sha256:287073c66748f624be4cef893ef9174e3eb88fe0b8a78dc22e88eca4bc357ca6",
"sha256:3fab4e75b8c525a4776e7630b9ee48aea50107fea6ca9f593c98da3f4d11bf7c", "sha256:2a1ef6a36fdbf71538142ed604ad19b82f67b05749512e47f247a6ddd06afdc7",
"sha256:406fac1d09edc613020ce9cf3f2ccf1a1b2f57ab00552b4c18e3d5276c67eb11", "sha256:2a72fb9963cba4cd5793854fd12f4cfee731e86df140f59ff52a49b3552db241",
"sha256:40a0bd0bed96dae5712dab2aba7d334a6c67cbcac2ddfca7dbcc4a8176445990", "sha256:2acca2be4bb2f2147ada8cac612f8a98fc09f41c89f87add7256ad27332c2fda",
"sha256:41dac3b9fce187a25c6253ec79a3f9e2a7e761eb08690e90415069ea4a68ff7a", "sha256:2f583bd01bbfbff4eaee0868e6fc607efdfcc2b03c1c766b06a707abbc856187",
"sha256:459c0d338cc55d099798618f714b21b7ece17eb1a87879f2da20a3ff4c7628e2", "sha256:33809aebac276089b78db106ee692bdc9044710e26f24a9a2eaa35a0f9fa70ba",
"sha256:459d6be6134ce3b38e0ef76f8a672924460c455d45f1ad8fdade36796df1ddc8", "sha256:36fa178aacbc277bc6b62a2c3da95226520da4f4e9e206fdf076484363895d2c",
"sha256:46b0d5520dbcafea9a8645a8164658777686c5c524d381d983317d29687cce97", "sha256:4204e773b4b408062960e65468d5346bdfe139247ee5f1ca2a378983e11388a2",
"sha256:47924039e785a04d4a4fa49455e51b4eb3422d6eaacfde9fc9abf8fdef164e8a", "sha256:4384a8f68ddb31a0b0c3deae88765f5868a1b9148939c3f4121233314ad5532c",
"sha256:4bfcbde6e06c56b30668a0c872d75a7ef3025dc3c1823a13cf29a0e9b33f67e8", "sha256:456855f57b413f077dff513a5a28ed838dbbb15082ba00f80750377eed23d132",
"sha256:4f9ee4febb249c591d07b2d4dd36ebcad0ccd128962aaa1801508320896575ef", "sha256:49d5d58abd4b83fb8ce763be7794d09b2f50f10aa65c0f0c1696c677edeb7cbf",
"sha256:55749f745ebf154c0d63d46c8c58594d8894b161928aa41adbb0709c1fe78b77", "sha256:4ac6b4ce1e7283d715c4b729d8f9dab9627586dafce81d9eaa009dd7f25dd972",
"sha256:5864b0242f74b9dd0b78fd39db1768bc3f00d1ffc14e596fd3e3f2ce43436a33", "sha256:4df8a199d9f6afc5ae9a65f8f95ee52cae389a8c6b20163762bde0426275b7db",
"sha256:5f60f920691a620b03082692c378661947d09415743e437a7478c309eb0e4f82", "sha256:500960cb3a0543a724a81ba859da816e8cf01b0e6aaeedf2c3775d12ee49cade",
"sha256:60eb8ceaa40a41540b9acae6ae7c1f0a67d233c40dc4359c256ad2ad85bdf5e5", "sha256:519ae0312616026bf4cedc0fe459e982734f3ca82ee8c7246c19b650b60a5ee4",
"sha256:69a7b96b59322a81c2203be537957313b07dd333105b73db0b69212c7d867b4b", "sha256:578114bc803a4c1ff9946d977c221e4376620a46cf78da267d946397dc9514a8",
"sha256:6ad84731a26bcfb299f9eab56c7932d46f9cad51c52768cace09e92a19e4cf55", "sha256:5c5cbc703168d1b7a838668998308018a2718c2130595e8e190220238addc96f",
"sha256:6db58c22ac6c81aeac33912fb1af0e930bc9774166cdd56eade913d5f2fff35e", "sha256:6162f8d2dc27ba21027f261e4fa26f8bcb3cf9784b7f9499466a311ac284b5b9",
"sha256:70651ff6e663428cea902dac297066d5c6e5423fda345a4ca62430575364d62b", "sha256:704d35ecc7e9c31d48926150afada60401c55efa3b46cd1ded5a01bdffaf1d48",
"sha256:72f7919af5de5ecfaf1eba47bf9a5d8aa089a3340277276e5636d16ee97614d7", "sha256:716b542728d4c742353448765aa7cdaa519a7b82f9564130e2b3f6766018c9ec",
"sha256:732bd062c9e5d9582a30e8751461c1917dd1ccbdd6cafb032f02c86b20d2e7ec", "sha256:72282ad4892a9fb2da25defeac8c2e84352c108705c972db82ab121d15f14e6d",
"sha256:7924e54f7ce5d253d6160090ddc6df25ed2feea25bfb3339b424a9dd591688bc", "sha256:7233d65d9d651242a68801159763d09e9ec96e8a158dbf118dc090cd77a104c9",
"sha256:7afb844041e707ac9ad9acad2188a90bffce2c770e6dc2318be0c9916aef1469", "sha256:732da3243e1b8d3eab8c6ae23ae6a58548849d2e4a4e03a1924c8ddf71a387cb",
"sha256:7b883af50eaa6bb3299780651e5be921e88050ccf00e3e583b1e92020333304b", "sha256:75b81e678d1c1ede0785c7f46690621e4c6e63ccd9192af1f0bd9d504bbb6bf4",
"sha256:7beec26729d496a12fd23cf8da9944ee338c8b8a17035a560b585c36fe81af20", "sha256:75f76ee558751746d6a38f89d60b6228fa174e5172d143886af0f85aa306fd89",
"sha256:7bf26c2e2ea59d32807081ad51968133af3025c4ba5753e6a794683d2c91bf6e", "sha256:7ee8d5f878dccb6d499ba4d30d757111847b6849ae07acdd1205fffa1fc1253c",
"sha256:7c31669e0c8cc68400ef0c730c3a1e11317ba76b892deeefaf52dcb41d56ed5d", "sha256:7f752826b5b8361193df55afcdf8ca6a57d0232653494ba473630a83ba50d8c9",
"sha256:7e6231aa5bdacda78e96ad7b07d0c312f34ba35d717115f4b4bff6cb87224f0f", "sha256:86b3d0033580bd6bbe07590152007275bd7af95f98eaa5bd36f3da219dcd93da",
"sha256:870dbfa94de9b8866b37b867a2cb37a60c401d9deb4a9ea392abf11a1f98037b", "sha256:8d62da299c6ecb04df729e4b5c52dc0d53f4f8430b4492b93aa8de1f541c4aac",
"sha256:88646cae28eb1dd5cd1e09605680c2b043b64d7481cdad7f5003ebef401a3039", "sha256:8e47755d8152c1ab5b55928ab422a76e2e7b22b5ed8e90a7d584268dd49e9c6b",
"sha256:8aafeedb6597a163a9c9727d8a8bd363a93277701b7bfd2749fbefee2396469e", "sha256:9091632a25b8b87b9a605ec0e61f241c456e9248bfdcf7abdf344fdb169c81cf",
"sha256:8bde5b48c65b8e807409e6f20baee5d2cd880e0fad00b1a811ebc43e39a00ab2", "sha256:936e5db01dd49476fa8f4383c259b8b1303d5dd5fb34c97de194560698cc2c5e",
"sha256:8f9142a6ed83d90c94a3efd7af8873bf7cefed2d3d44387bf848888482e2d25f", "sha256:99b6add4c0b39a513d323d3b93bc173dac663c27b99860dd5bf491b240d26137",
"sha256:936a787f83db1f2115ee829dd615c4f684ee48ac4de5779ab4300994d8af325b", "sha256:9c865a7ee6f93783bd5d781af5a4c43dadc37053a5b42f7d18dc019f8c9d2bd1",
"sha256:98dc6f4f2095fc7ad277782a7c2c88296badcad92316b5a6e530930b1d475ebc", "sha256:a425479ee40ff021f8216c9d07a6a3b54b31c8267c6e17aa88b70d7ebd0e5e5b",
"sha256:9957433c3a1b67bdd4c63717eaf174ebb749510d5ea612cd4e83f2d9142f3fc8", "sha256:a4b2bf78342c40b3dc830880106f54328928ff03e357935ad26c7128bbd66ce8",
"sha256:99af961d72ac731aae2a1b55ccbdae0733d816f8bfb97b41909e143de735f522", "sha256:a6b1bb0827f56654b4437955555dc3aeeebeddc47c2d7ed575477f082622c49e",
"sha256:9b5f13857da99325dcabe1cc4e9e6a3d7b2e2c726248ba5dd4be3e8e4a0b6d0e", "sha256:aaf09e615a0bf98d406657e0008e4a8701b11481840be7d31755dc9f97c44053",
"sha256:9d776d30cde7e541b8180103c3f294ef7c1862fd45d81738d156d00551005784", "sha256:b1f6f5938d63c6139860f044e2538baeee6f0b251a1816e7adb6cbce106a1f01",
"sha256:9da90d393a8227d717c19f5397688a38635afec89f2e2d7af0df037f3249c39a", "sha256:b29eeb887aa931c2fcef5aa515d9d176d25006794610c264ddc114c053bf96fe",
"sha256:a3b7352b48fbc8b446b75f3069124e87f599d25afb8baa96a550256c031bb890", "sha256:b3992a322a5617ded0a9f23fd06dbc1e4bd7cf39bc4ccf344b10f80af58beacd",
"sha256:a477932664d9611d7a0816cc3c0eb1f8856f8a42435488280dfbf4395e141485", "sha256:b5b6079cc452a7c53dd378c6f881ac528246b3ac9aae0f8eef98498a75657805",
"sha256:a7e41e3ada4cca5f22b478c08e973c930e5e6c7ba3588fb8e35f2398cdcc1545", "sha256:b60cc1a081f80a2105a59385b92d82278b15d80ebb3adb200542ae165cd7d183",
"sha256:a90fec23b4b05a09ad988e7a4f4e081711a90eb2a55b9c984d8b74597599180f", "sha256:b926dd38db1519ed3043a4de50214e0d600d404099c3392f098a7f9d75029ff8",
"sha256:a9e523474998fb33f7c1a4d55f5504c908d57add624599e095c20fa575b8d943", "sha256:bd87f48924f360e5d1c5f770d6155ce0e7d83f7b4e10c2f9ec001c73cf475c99",
"sha256:aa057095f621dad24a1e906747179a69780ef45cc8f69e97463692adbcdae878", "sha256:bda1ee3e08252b8d41fa5537413ffdddd58fa73107171a126d3b9ff001b9b820",
"sha256:aa6c8c582036275997a733427b88031a32ffa5dfc3124dc25a730658c47a572f", "sha256:be0ec334369316fa73448cc8c982c01e5d2a81c95969d58b8f6e272884df0074",
"sha256:ae34418b6b389d601b31153b84dce480351a352e0bb763684a1b993d6be30f17", "sha256:c6119dc90483a5cb50a1306adb8d52c66e447da88ea44f323e0ae1a5fcb14256",
"sha256:b0d7a9165167269758145756db43a133608a531b1e5bb6a626b9ee24bc38a8f7", "sha256:c9803edf8e29bd825f43481f19c37f50d2b01899448273b3a7758441b512acf8",
"sha256:b30b0dd58a4509c3bd7eefddf6338565c4905406aee0c6e4a5293841411a1286", "sha256:c9bd22a2a639e26171068f8ebb5400ce2c1bc7d17959f60a3b753ae13c632975",
"sha256:b8f9186ca45aee030dc8234118b9c0784ad91a0bb27fc4e7d9d6608a5e3d386c", "sha256:cbcc558401de90a746d02ef330c528f2e668c83350f045833543cd57ecead1ad",
"sha256:b94cbda27267423411c928208e89adddf2ea5dd5f74b9528513f0358bba019cb", "sha256:cf6204fe865da605285c34cf1172879d0314ff267b1c35ff59de7154f35fdc2e",
"sha256:cc6f6c9be0ab6da37bc77c2dda5f14b1d532d5dbef00311ee6e13357a418e646", "sha256:d33dd21f572545649f90c38c227cc8631268ba25c460b5569abebdd0ec5974ca",
"sha256:ce232a6170dd6532096cadbf6185271e4e8c70fc9217ebe105923ac105da9978", "sha256:d89ca19cdd0dd5f31606a9329e309d4fcbb3df860960acec32630297d61820df",
"sha256:cf903310a34e14651c9de056fcc12ce090560864d5a2bb0174b971685684e1d8", "sha256:d8f99b147ff3fcf6b3cc60cb0c39ea443884d5559a30b1481e92495f2310ff2b",
"sha256:d5362d099c244a2d2f9659fb3c9db7c735f0004765bbe06b99be69fbd87c3f15", "sha256:d937653a696465677ed583124b94a4b2d79f5e30b2c46115a68e482c6a591c8a",
"sha256:dffaf740fe2e147fedcb6b561353a16243e654f7fe8e701b1b9db148242e1272", "sha256:dcca5d2bf65c6fb591fff92da03f94cd4f315972f97c21975398bd4bd046854a",
"sha256:e0f686549e32ccdb02ae6f25eee40cc33900910085de6aa3790effd391ae10c2", "sha256:ded1c35f15c9dea16ead9bffcde9bb5c7c031bff076355dc58dcb1cb436c4721",
"sha256:e4b52776a2e3230f4854907a1e0946eec04d41b1fc64069ee774876bbe0eab55", "sha256:e3e70c94a0c3841e6aa831edab1619ad5c511199be94d0c11ba75fe06efe107a",
"sha256:e4ba0884a91f1aecce75202473ab138724aa4fb26d7707f2e1fa6c3e68c84fbf", "sha256:e56f8186d6210ac7ece503193ec84104da7ceb98f68ce18c07282fcc2452e76f",
"sha256:e6294e76b0380bb7a61eb8a39273c40b20beb35e8c87ee101062834ced19c545", "sha256:e7774b570e61cb998490c5235740d475413a1f6de823169b4cf94e2fe9e9f6b2",
"sha256:ebb892ed8599b23fa8f1799e13a12c87a97a6c9d0f497525ce9858564c4575a4", "sha256:e7c6ed0dc9d8e65f24f5824291550139fe6f37fac03788d4580da0d33bc00c97",
"sha256:eca58e319f4fd6df004762419612122b2c7e7d95ffafc37e890252f869f3fb2a", "sha256:ec08be75bb268473677edb83ba71e7e74b43c008e4a7b1907c6d57e940bf34b6",
"sha256:ed957db4c33bc99895f3a1672eca7e80e8cda8bd1e29a80536b4ec2153fa9804", "sha256:ecdf6bf5f578615f2e985a5e1f6572e23aa632c4bd1dc67f8f406d445ac115ed",
"sha256:ef551c053692b1e39e3f7950ce2296536728871110e7d75c4e7753fb30ca87f4", "sha256:ed25e1835c00a332cb10c683cd39da96a719ab1dfc08427d476bce41b92531fc",
"sha256:ef6113cd31411eaf9b39fc5a8848e71c72656fd418882488598758b2c8c6dfa0", "sha256:f4cb85f693044e0f71f394ff76c98ddc1bc0953e48c061725e540396d5c8a2e1",
"sha256:f685dbc1fdadb1dcd5b5e51e0a378d4685a891b2ddaf8e2bba89bd3a7144e44a", "sha256:f53aace168a2a10582e570b7736cc5bef12cae9cf21775e3eafac597e8551fbe",
"sha256:f8ed79883b4328b7f0bd142733d99c8e6b22703e908ec63d930b06be3a0e7113", "sha256:f651dd19363c632f4abe3480a7c87a9773be27cfe1341aef06e8759599454120",
"sha256:fe56851c3f1d6f5384b3051c536cc81b3a93a73faf931f404fef95217cf1e10d", "sha256:fc4ad7f7ee1a13d9cb49d8198cd7d7e3aa93e425f371a68235f784e99741561f",
"sha256:ff7c97eb7a29aba230389a2661edf2e9e06ce616c7e35aa764879b6894a44b25" "sha256:fee427241c2d9fb7192b658190f9f5fd6dfe41e02f3c1489d2ec1e6a5ab1e04a"
], ],
"markers": "python_version >= '3.8'", "markers": "python_version >= '3.8'",
"version": "==2.16.2" "version": "==2.16.3"
}, },
"pydantic-settings": { "pydantic-settings": {
"hashes": [ "hashes": [
"sha256:26b1492e0a24755626ac5e6d715e9077ab7ad4fb5f19a8b7ed7011d52f36141c", "sha256:00b9f6a5e95553590434c0fa01ead0b216c3e10bc54ae02e37f359948643c5ed",
"sha256:7621c0cb5d90d1140d2f0ef557bdf03573aac7035948109adf2574770b77605a" "sha256:0235391d26db4d2190cb9b31051c4b46882d28a51533f97440867f012d4da091"
], ],
"markers": "python_version >= '3.8'", "markers": "python_version >= '3.8'",
"version": "==2.1.0" "version": "==2.2.1"
}, },
"pyjwkest": { "pyjwkest": {
"hashes": [ "hashes": [
@ -982,11 +980,11 @@
}, },
"python-dateutil": { "python-dateutil": {
"hashes": [ "hashes": [
"sha256:0123cacc1627ae19ddf3c27a5de5bd67ee4586fbdd6440d9748f8abb483d3e86", "sha256:37dd54208da7e1cd875388217d5e00ebd4179249f90fb72437e91a35459a0ad3",
"sha256:961d03dc3453ebbc59dbdea9e4e11c5651520a876d0f4db161e8674aae935da9" "sha256:a8b2bc7bffae282281c8140a97d3aa9c14da0b136dfe83f850eea9a5f7470427"
], ],
"markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3'", "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3'",
"version": "==2.8.2" "version": "==2.9.0.post0"
}, },
"python-dotenv": { "python-dotenv": {
"hashes": [ "hashes": [
@ -1015,11 +1013,11 @@
}, },
"setuptools": { "setuptools": {
"hashes": [ "hashes": [
"sha256:385eb4edd9c9d5c17540511303e39a147ce2fc04bc55289c322b9e5904fe2c05", "sha256:02fa291a0471b3a18b2b2481ed902af520c69e8ae0919c13da936542754b4c56",
"sha256:be1af57fc409f93647f2e8e4573a142ed38724b8cdd389706a867bb4efcf1e78" "sha256:5c0806c7d9af348e6dd3777b4f4dbb42c7ad85b190104837488eab9a7c945cf8"
], ],
"markers": "python_version >= '3.8'", "markers": "python_version >= '3.8'",
"version": "==69.0.3" "version": "==69.1.1"
}, },
"six": { "six": {
"hashes": [ "hashes": [
@ -1037,14 +1035,23 @@
"markers": "python_version >= '3.5'", "markers": "python_version >= '3.5'",
"version": "==0.4.4" "version": "==0.4.4"
}, },
"typing-extensions": { "tblib": {
"hashes": [ "hashes": [
"sha256:23478f88c37f27d76ac8aee6c905017a143b0b1b886c3c9f66bc2fd94f9f5783", "sha256:80a6c77e59b55e83911e1e607c649836a69c103963c5f28a46cbeef44acf8129",
"sha256:af72aea155e91adfc61c3ae9e0e342dbc0cba726d6cba4b6c72c1f34e47291cd" "sha256:93622790a0a29e04f0346458face1e144dc4d32f493714c6c3dff82a4adb77e6"
], ],
"index": "pypi", "index": "pypi",
"markers": "python_version >= '3.8'", "markers": "python_version >= '3.8'",
"version": "==4.9.0" "version": "==3.0.0"
},
"typing-extensions": {
"hashes": [
"sha256:69b1a937c3a517342112fb4c6df7e72fc39a38e7891a5730ed4985b5214b5475",
"sha256:b0abd7c89e8fb96f98db18d86106ff1d90ab692004eb746cf6eda2682f91b3cb"
],
"index": "pypi",
"markers": "python_version >= '3.8'",
"version": "==4.10.0"
}, },
"urllib3": { "urllib3": {
"hashes": [ "hashes": [
@ -1073,45 +1080,45 @@
}, },
"zope.interface": { "zope.interface": {
"hashes": [ "hashes": [
"sha256:0c8cf55261e15590065039696607f6c9c1aeda700ceee40c70478552d323b3ff", "sha256:02adbab560683c4eca3789cc0ac487dcc5f5a81cc48695ec247f00803cafe2fe",
"sha256:13b7d0f2a67eb83c385880489dbb80145e9d344427b4262c49fbf2581677c11c", "sha256:14e02a6fc1772b458ebb6be1c276528b362041217b9ca37e52ecea2cbdce9fac",
"sha256:1f294a15f7723fc0d3b40701ca9b446133ec713eafc1cc6afa7b3d98666ee1ac", "sha256:25e0af9663eeac6b61b231b43c52293c2cb7f0c232d914bdcbfd3e3bd5c182ad",
"sha256:239a4a08525c080ff833560171d23b249f7f4d17fcbf9316ef4159f44997616f", "sha256:2606955a06c6852a6cff4abeca38346ed01e83f11e960caa9a821b3626a4467b",
"sha256:2f8d89721834524a813f37fa174bac074ec3d179858e4ad1b7efd4401f8ac45d", "sha256:396f5c94654301819a7f3a702c5830f0ea7468d7b154d124ceac823e2419d000",
"sha256:2fdc7ccbd6eb6b7df5353012fbed6c3c5d04ceaca0038f75e601060e95345309", "sha256:3b240883fb43160574f8f738e6d09ddbdbf8fa3e8cea051603d9edfd947d9328",
"sha256:34c15ca9248f2e095ef2e93af2d633358c5f048c49fbfddf5fdfc47d5e263736", "sha256:3b6c62813c63c543a06394a636978b22dffa8c5410affc9331ce6cdb5bfa8565",
"sha256:387545206c56b0315fbadb0431d5129c797f92dc59e276b3ce82db07ac1c6179", "sha256:4ae9793f114cee5c464cc0b821ae4d36e1eba961542c6086f391a61aee167b6f",
"sha256:43b576c34ef0c1f5a4981163b551a8781896f2a37f71b8655fd20b5af0386abb", "sha256:4bce517b85f5debe07b186fc7102b332676760f2e0c92b7185dd49c138734b70",
"sha256:57d0a8ce40ce440f96a2c77824ee94bf0d0925e6089df7366c2272ccefcb7941", "sha256:4d45d2ba8195850e3e829f1f0016066a122bfa362cc9dc212527fc3d51369037",
"sha256:5a804abc126b33824a44a7aa94f06cd211a18bbf31898ba04bd0924fbe9d282d", "sha256:4dd374927c00764fcd6fe1046bea243ebdf403fba97a937493ae4be2c8912c2b",
"sha256:67be3ca75012c6e9b109860820a8b6c9a84bfb036fbd1076246b98e56951ca92", "sha256:506f5410b36e5ba494136d9fa04c548eaf1a0d9c442b0b0e7a0944db7620e0ab",
"sha256:6af47f10cfc54c2ba2d825220f180cc1e2d4914d783d6fc0cd93d43d7bc1c78b", "sha256:59f7374769b326a217d0b2366f1c176a45a4ff21e8f7cebb3b4a3537077eff85",
"sha256:6dc998f6de015723196a904045e5a2217f3590b62ea31990672e31fbc5370b41", "sha256:5ee9789a20b0081dc469f65ff6c5007e67a940d5541419ca03ef20c6213dd099",
"sha256:70d2cef1bf529bff41559be2de9d44d47b002f65e17f43c73ddefc92f32bf00f", "sha256:6fc711acc4a1c702ca931fdbf7bf7c86f2a27d564c85c4964772dadf0e3c52f5",
"sha256:7ebc4d34e7620c4f0da7bf162c81978fce0ea820e4fa1e8fc40ee763839805f3", "sha256:75d2ec3d9b401df759b87bc9e19d1b24db73083147089b43ae748aefa63067ef",
"sha256:964a7af27379ff4357dad1256d9f215047e70e93009e532d36dcb8909036033d", "sha256:76e0531d86523be7a46e15d379b0e975a9db84316617c0efe4af8338dc45b80c",
"sha256:97806e9ca3651588c1baaebb8d0c5ee3db95430b612db354c199b57378312ee8", "sha256:8af82afc5998e1f307d5e72712526dba07403c73a9e287d906a8aa2b1f2e33dd",
"sha256:9b9bc671626281f6045ad61d93a60f52fd5e8209b1610972cf0ef1bbe6d808e3", "sha256:8f5d2c39f3283e461de3655e03faf10e4742bb87387113f787a7724f32db1e48",
"sha256:9ffdaa5290422ac0f1688cb8adb1b94ca56cee3ad11f29f2ae301df8aecba7d1", "sha256:97785604824981ec8c81850dd25c8071d5ce04717a34296eeac771231fbdd5cd",
"sha256:a0da79117952a9a41253696ed3e8b560a425197d4e41634a23b1507efe3273f1", "sha256:a3046e8ab29b590d723821d0785598e0b2e32b636a0272a38409be43e3ae0550",
"sha256:a41f87bb93b8048fe866fa9e3d0c51e27fe55149035dcf5f43da4b56732c0a40", "sha256:abb0b3f2cb606981c7432f690db23506b1db5899620ad274e29dbbbdd740e797",
"sha256:aa6fd016e9644406d0a61313e50348c706e911dca29736a3266fc9e28ec4ca6d", "sha256:ac7c2046d907e3b4e2605a130d162b1b783c170292a11216479bb1deb7cadebe",
"sha256:ad54ed57bdfa3254d23ae04a4b1ce405954969c1b0550cc2d1d2990e8b439de1", "sha256:af27b3fe5b6bf9cd01b8e1c5ddea0a0d0a1b8c37dc1c7452f1e90bf817539c6d",
"sha256:b012d023b4fb59183909b45d7f97fb493ef7a46d2838a5e716e3155081894605", "sha256:b386b8b9d2b6a5e1e4eadd4e62335571244cb9193b7328c2b6e38b64cfda4f0e",
"sha256:b51b64432eed4c0744241e9ce5c70dcfecac866dff720e746d0a9c82f371dfa7", "sha256:b66335bbdbb4c004c25ae01cc4a54fd199afbc1fd164233813c6d3c2293bb7e1",
"sha256:bbe81def9cf3e46f16ce01d9bfd8bea595e06505e51b7baf45115c77352675fd", "sha256:d54f66c511ea01b9ef1d1a57420a93fbb9d48a08ec239f7d9c581092033156d0",
"sha256:c9559138690e1bd4ea6cd0954d22d1e9251e8025ce9ede5d0af0ceae4a401e43", "sha256:de125151a53ecdb39df3cb3deb9951ed834dd6a110a9e795d985b10bb6db4532",
"sha256:e30506bcb03de8983f78884807e4fd95d8db6e65b69257eea05d13d519b83ac0", "sha256:de7916380abaef4bb4891740879b1afcba2045aee51799dfd6d6ca9bdc71f35f",
"sha256:e33e86fd65f369f10608b08729c8f1c92ec7e0e485964670b4d2633a4812d36b", "sha256:e2fefad268ff5c5b314794e27e359e48aeb9c8bb2cbb5748a071757a56f6bb8f",
"sha256:e441e8b7d587af0414d25e8d05e27040d78581388eed4c54c30c0c91aad3a379", "sha256:e7b2bed4eea047a949296e618552d3fed00632dc1b795ee430289bdd0e3717f3",
"sha256:e8bb9c990ca9027b4214fa543fd4025818dc95f8b7abce79d61dc8a2112b561a", "sha256:e87698e2fea5ca2f0a99dff0a64ce8110ea857b640de536c76d92aaa2a91ff3a",
"sha256:ef43ee91c193f827e49599e824385ec7c7f3cd152d74cb1dfe02cb135f264d83", "sha256:ede888382882f07b9e4cd942255921ffd9f2901684198b88e247c7eabd27a000",
"sha256:ef467d86d3cfde8b39ea1b35090208b0447caaabd38405420830f7fd85fbdd56", "sha256:f444de0565db46d26c9fa931ca14f497900a295bd5eba480fc3fad25af8c763e",
"sha256:f89b28772fc2562ed9ad871c865f5320ef761a7fcc188a935e21fe8b31a38ca9", "sha256:fa994e8937e8ccc7e87395b7b35092818905cf27c651e3ff3e7f29729f5ce3ce",
"sha256:fddbab55a2473f1d3b8833ec6b7ac31e8211b0aa608df5ab09ce07f3727326de" "sha256:febceb04ee7dd2aef08c2ff3d6f8a07de3052fc90137c507b0ede3ea80c21440"
], ],
"markers": "python_version >= '3.7'", "markers": "python_version >= '3.7'",
"version": "==6.1" "version": "==6.2"
} }
}, },
"develop": { "develop": {
@ -1142,32 +1149,32 @@
}, },
"black": { "black": {
"hashes": [ "hashes": [
"sha256:0269dfdea12442022e88043d2910429bed717b2d04523867a85dacce535916b8", "sha256:057c3dc602eaa6fdc451069bd027a1b2635028b575a6c3acfd63193ced20d9c8",
"sha256:07204d078e25327aad9ed2c64790d681238686bce254c910de640c7cc4fc3aa6", "sha256:08654d0797e65f2423f850fc8e16a0ce50925f9337fb4a4a176a7aa4026e63f8",
"sha256:08b34e85170d368c37ca7bf81cf67ac863c9d1963b2c1780c39102187ec8dd62", "sha256:163baf4ef40e6897a2a9b83890e59141cc8c2a98f2dda5080dc15c00ee1e62cd",
"sha256:1a95915c98d6e32ca43809d46d932e2abc5f1f7d582ffbe65a5b4d1588af7445", "sha256:1e08fb9a15c914b81dd734ddd7fb10513016e5ce7e6704bdd5e1251ceee51ac9",
"sha256:2588021038bd5ada078de606f2a804cadd0a3cc6a79cb3e9bb3a8bf581325a4c", "sha256:4dd76e9468d5536abd40ffbc7a247f83b2324f0c050556d9c371c2b9a9a95e31",
"sha256:2fa6a0e965779c8f2afb286f9ef798df770ba2b6cee063c650b96adec22c056a", "sha256:4f9de21bafcba9683853f6c96c2d515e364aee631b178eaa5145fc1c61a3cc92",
"sha256:34afe9da5056aa123b8bfda1664bfe6fb4e9c6f311d8e4a6eb089da9a9173bf9", "sha256:61a0391772490ddfb8a693c067df1ef5227257e72b0e4108482b8d41b5aee13f",
"sha256:3897ae5a21ca132efa219c029cce5e6bfc9c3d34ed7e892113d199c0b1b444a2", "sha256:6981eae48b3b33399c8757036c7f5d48a535b962a7c2310d19361edeef64ce29",
"sha256:40657e1b78212d582a0edecafef133cf1dd02e6677f539b669db4746150d38f6", "sha256:7e53a8c630f71db01b28cd9602a1ada68c937cbf2c333e6ed041390d6968faf4",
"sha256:48b5760dcbfe5cf97fd4fba23946681f3a81514c6ab8a45b50da67ac8fbc6c7b", "sha256:810d445ae6069ce64030c78ff6127cd9cd178a9ac3361435708b907d8a04c693",
"sha256:5242ecd9e990aeb995b6d03dc3b2d112d4a78f2083e5a8e86d566340ae80fec4", "sha256:93601c2deb321b4bad8f95df408e3fb3943d85012dddb6121336b8e24a0d1218",
"sha256:5cdc2e2195212208fbcae579b931407c1fa9997584f0a415421748aeafff1168", "sha256:992e451b04667116680cb88f63449267c13e1ad134f30087dec8527242e9862a",
"sha256:5d7b06ea8816cbd4becfe5f70accae953c53c0e53aa98730ceccb0395520ee5d", "sha256:9db528bccb9e8e20c08e716b3b09c6bdd64da0dd129b11e160bf082d4642ac23",
"sha256:7258c27115c1e3b5de9ac6c4f9957e3ee2c02c0b39222a24dc7aa03ba0e986f5", "sha256:a0057f800de6acc4407fe75bb147b0c2b5cbb7c3ed110d3e5999cd01184d53b0",
"sha256:854c06fb86fd854140f37fb24dbf10621f5dab9e3b0c29a690ba595e3d543024", "sha256:ba15742a13de85e9b8f3239c8f807723991fbfae24bad92d34a2b12e81904982",
"sha256:a21725862d0e855ae05da1dd25e3825ed712eaaccef6b03017fe0853a01aa45e", "sha256:bce4f25c27c3435e4dace4815bcb2008b87e167e3bf4ee47ccdc5ce906eb4894",
"sha256:a83fe522d9698d8f9a101b860b1ee154c1d25f8a82ceb807d319f085b2627c5b", "sha256:ca610d29415ee1a30a3f30fab7a8f4144e9d34c89a235d81292a1edb2b55f540",
"sha256:b3d64db762eae4a5ce04b6e3dd745dcca0fb9560eb931a5be97472e38652a161", "sha256:d533d5e3259720fdbc1b37444491b024003e012c5173f7d06825a77508085430",
"sha256:e298d588744efda02379521a19639ebcd314fba7a49be22136204d7ed1782717", "sha256:d84f29eb3ee44859052073b7636533ec995bd0f64e2fb43aeceefc70090e752b",
"sha256:e2c8dfa14677f90d976f68e0c923947ae68fa3961d61ee30976c388adc0b02c8", "sha256:e37c99f89929af50ffaf912454b3e3b47fd64109659026b678c091a4cd450fb2",
"sha256:ecba2a15dfb2d97105be74bbfe5128bc5e9fa8477d8c46766505c1dda5883aac", "sha256:e8a6ae970537e67830776488bca52000eaa37fa63b9988e8c487458d9cd5ace6",
"sha256:fc1ec9aa6f4d98d022101e015261c056ddebe3da6a8ccfc2c792cbe0349d48b7" "sha256:faf2ee02e6612577ba0181f4347bcbcf591eb122f7841ae5ba233d12c39dcb4d"
], ],
"index": "pypi", "index": "pypi",
"markers": "python_version >= '3.8'", "markers": "python_version >= '3.8'",
"version": "==24.1.1" "version": "==24.2.0"
}, },
"blinker": { "blinker": {
"hashes": [ "hashes": [
@ -1179,12 +1186,12 @@
}, },
"boto3": { "boto3": {
"hashes": [ "hashes": [
"sha256:65acfe7f1cf2a9b7df3d4edb87c8022e02685825bd1957e7bb678cc0d09f5e5f", "sha256:8b3f5cc7fbedcbb22271c328039df8a6ab343001e746e0cdb24774c426cadcf8",
"sha256:73f5ec89cb3ddb3ed577317889fd2f2df783f66b6502a9a4239979607e33bf74" "sha256:f201b6a416f809283d554c652211eecec9fe3a52ed4063dab3f3e7aea7571d9c"
], ],
"index": "pypi", "index": "pypi",
"markers": "python_version >= '3.8'", "markers": "python_version >= '3.8'",
"version": "==1.34.37" "version": "==1.34.54"
}, },
"boto3-mocking": { "boto3-mocking": {
"hashes": [ "hashes": [
@ -1197,28 +1204,28 @@
}, },
"boto3-stubs": { "boto3-stubs": {
"hashes": [ "hashes": [
"sha256:97b5ca3d3145385acde5af46ca2da3fc74f433545034c36183f389e99771516e", "sha256:7db5194e47f76e0010cd00b6ad9725db114d6a3fd04e52ceed3ef1181fe326bc",
"sha256:c6618c7126bac0337c05e161e9c428febc57d6a24d7ff62de46e67761f402c57" "sha256:c7b2e8b99f4896cf1226df47d4badaaa8df7426008c96a428bf00205695669e9"
], ],
"index": "pypi", "index": "pypi",
"markers": "python_version >= '3.8'", "markers": "python_version >= '3.8'",
"version": "==1.34.37" "version": "==1.34.54"
}, },
"botocore": { "botocore": {
"hashes": [ "hashes": [
"sha256:2a5bf33aacd2d970afd3d492e179e06ea98a5469030d5cfe7a2ad9995f7bb2ef", "sha256:4061ff4be3efcf53547ebadf2c94d419dfc8be7beec24e9fa1819599ffd936fa",
"sha256:3c46ddb1679e6ef45ca78b48665398636bda532a07cd476e4b500697d13d9a99" "sha256:bf215d93e9d5544c593962780d194e74c6ee40b883d0b885e62ef35fc0ec01e5"
], ],
"markers": "python_version >= '3.8'", "markers": "python_version >= '3.8'",
"version": "==1.34.37" "version": "==1.34.54"
}, },
"botocore-stubs": { "botocore-stubs": {
"hashes": [ "hashes": [
"sha256:087cd42973edcb5527dc97eec87fa29fffecc39691249486e02045677d4a2dbe", "sha256:958f0084322dc9e549f73151b686fa51b15858fb2b3a573b9f4367f073fff463",
"sha256:d6bcea8a6872aa46d389027dc5c022241fd0a2047a8b858aa5005e6151ed30a7" "sha256:bcc35bfbd14d1261813681c40108f2ce85fdf082c15b0a04016d3c22dd93b73f"
], ],
"markers": "python_version >= '3.8' and python_version < '4.0'", "markers": "python_version >= '3.8' and python_version < '4.0'",
"version": "==1.34.37" "version": "==1.34.54"
}, },
"click": { "click": {
"hashes": [ "hashes": [
@ -1427,11 +1434,11 @@
}, },
"python-dateutil": { "python-dateutil": {
"hashes": [ "hashes": [
"sha256:0123cacc1627ae19ddf3c27a5de5bd67ee4586fbdd6440d9748f8abb483d3e86", "sha256:37dd54208da7e1cd875388217d5e00ebd4179249f90fb72437e91a35459a0ad3",
"sha256:961d03dc3453ebbc59dbdea9e4e11c5651520a876d0f4db161e8674aae935da9" "sha256:a8b2bc7bffae282281c8140a97d3aa9c14da0b136dfe83f850eea9a5f7470427"
], ],
"markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3'", "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3'",
"version": "==2.8.2" "version": "==2.9.0.post0"
}, },
"pyyaml": { "pyyaml": {
"hashes": [ "hashes": [
@ -1492,11 +1499,11 @@
}, },
"rich": { "rich": {
"hashes": [ "hashes": [
"sha256:5cb5123b5cf9ee70584244246816e9114227e0b98ad9176eede6ad54bf5403fa", "sha256:4edbae314f59eb482f54e9e30bf00d33350aaa94f4bfcd4e9e3110e64d0d7222",
"sha256:6da14c108c4866ee9520bbffa71f6fe3962e193b7da68720583850cd4548e235" "sha256:9be308cb1fe2f1f57d67ce99e95af38a1e2bc71ad9813b0e247cf7ffbcc3a432"
], ],
"markers": "python_full_version >= '3.7.0'", "markers": "python_full_version >= '3.7.0'",
"version": "==13.7.0" "version": "==13.7.1"
}, },
"s3transfer": { "s3transfer": {
"hashes": [ "hashes": [
@ -1532,11 +1539,11 @@
}, },
"stevedore": { "stevedore": {
"hashes": [ "hashes": [
"sha256:8cc040628f3cea5d7128f2e76cf486b2251a4e543c7b938f58d9a377f6694a2d", "sha256:1c15d95766ca0569cad14cb6272d4d31dae66b011a929d7c18219c176ea1b5c9",
"sha256:a54534acf9b89bc7ed264807013b505bf07f74dbe4bcfa37d32bd063870b087c" "sha256:46b93ca40e1114cea93d738a6c1e365396981bb6bb78c27045b7587c9473544d"
], ],
"markers": "python_version >= '3.8'", "markers": "python_version >= '3.8'",
"version": "==5.1.0" "version": "==5.2.0"
}, },
"tomli": { "tomli": {
"hashes": [ "hashes": [
@ -1548,11 +1555,11 @@
}, },
"types-awscrt": { "types-awscrt": {
"hashes": [ "hashes": [
"sha256:06a859189a329ca8e66d56ceeef2391488e39b878fbd2141f115eab4d416fe22", "sha256:61811bbf4de95248939f9276a434be93d2b95f6ccfe8aa94e56999e9778cfcc2",
"sha256:f61a120d3e98ee1387bc5ca4b93437f258cc5c2af1f55f8634ec4cee5729f178" "sha256:79d5bfb01f64701b6cf442e89a37d9c4dc6dbb79a46f2f611739b2418d30ecfd"
], ],
"markers": "python_version >= '3.7' and python_version < '4.0'", "markers": "python_version >= '3.7' and python_version < '4.0'",
"version": "==0.20.3" "version": "==0.20.5"
}, },
"types-cachetools": { "types-cachetools": {
"hashes": [ "hashes": [
@ -1580,12 +1587,12 @@
}, },
"types-requests": { "types-requests": {
"hashes": [ "hashes": [
"sha256:03a28ce1d7cd54199148e043b2079cdded22d6795d19a2c2a6791a4b2b5e2eb5", "sha256:a82807ec6ddce8f00fe0e949da6d6bc1fbf1715420218a9640d695f70a9e5a9b",
"sha256:9592a9a4cb92d6d75d9b491a41477272b710e021011a2a3061157e2fb1f1a5d1" "sha256:f1721dba8385958f504a5386240b92de4734e047a08a40751c1654d1ac3349c5"
], ],
"index": "pypi", "index": "pypi",
"markers": "python_version >= '3.8'", "markers": "python_version >= '3.8'",
"version": "==2.31.0.20240125" "version": "==2.31.0.20240218"
}, },
"types-s3transfer": { "types-s3transfer": {
"hashes": [ "hashes": [
@ -1597,12 +1604,12 @@
}, },
"typing-extensions": { "typing-extensions": {
"hashes": [ "hashes": [
"sha256:23478f88c37f27d76ac8aee6c905017a143b0b1b886c3c9f66bc2fd94f9f5783", "sha256:69b1a937c3a517342112fb4c6df7e72fc39a38e7891a5730ed4985b5214b5475",
"sha256:af72aea155e91adfc61c3ae9e0e342dbc0cba726d6cba4b6c72c1f34e47291cd" "sha256:b0abd7c89e8fb96f98db18d86106ff1d90ab692004eb746cf6eda2682f91b3cb"
], ],
"index": "pypi", "index": "pypi",
"markers": "python_version >= '3.8'", "markers": "python_version >= '3.8'",
"version": "==4.9.0" "version": "==4.10.0"
}, },
"urllib3": { "urllib3": {
"hashes": [ "hashes": [

View file

@ -66,6 +66,7 @@ services:
command: > command: >
bash -c " python manage.py migrate && bash -c " python manage.py migrate &&
python manage.py load && python manage.py load &&
python manage.py createcachetable &&
python manage.py runserver 0.0.0.0:8080" python manage.py runserver 0.0.0.0:8080"
db: db:

View file

@ -2,10 +2,6 @@
import logging import logging
from time import sleep
from gevent import Timeout
from epplibwrapper.utility.pool_status import PoolStatus
try: try:
from epplib.client import Client from epplib.client import Client
from epplib import commands from epplib import commands
@ -18,8 +14,6 @@ from django.conf import settings
from .cert import Cert, Key from .cert import Cert, Key
from .errors import ErrorCode, LoginError, RegistryError from .errors import ErrorCode, LoginError, RegistryError
from .socket import Socket
from .utility.pool import EPPConnectionPool
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)
@ -43,8 +37,12 @@ class EPPLibWrapper:
ATTN: This should not be used directly. Use `Domain` from domain.py. ATTN: This should not be used directly. Use `Domain` from domain.py.
""" """
def __init__(self, start_connection_pool=True) -> None: def __init__(self) -> None:
"""Initialize settings which will be used for all connections.""" """Initialize settings which will be used for all connections."""
# set _client to None initially. In the event that the __init__ fails
# before _client initializes, app should still start and be in a state
# that it can attempt _client initialization on send attempts
self._client = None # type: ignore
# prepare (but do not send) a Login command # prepare (but do not send) a Login command
self._login = commands.Login( self._login = commands.Login(
cl_id=settings.SECRET_REGISTRY_CL_ID, cl_id=settings.SECRET_REGISTRY_CL_ID,
@ -54,9 +52,19 @@ class EPPLibWrapper:
"urn:ietf:params:xml:ns:contact-1.0", "urn:ietf:params:xml:ns:contact-1.0",
], ],
) )
try:
self._initialize_client()
except Exception:
logger.warning("Unable to configure epplib. Registrar cannot contact registry.")
def _initialize_client(self) -> None:
"""Initialize a client, assuming _login defined. Sets _client to initialized
client. Raises errors if initialization fails.
This method will be called at app initialization, and also during retries."""
# establish a client object with a TCP socket transport # establish a client object with a TCP socket transport
self._client = Client( # note that type: ignore added in several places because linter complains
# about _client initially being set to None, and None type doesn't match code
self._client = Client( # type: ignore
SocketTransport( SocketTransport(
settings.SECRET_REGISTRY_HOSTNAME, settings.SECRET_REGISTRY_HOSTNAME,
cert_file=CERT.filename, cert_file=CERT.filename,
@ -64,176 +72,95 @@ class EPPLibWrapper:
password=settings.SECRET_REGISTRY_KEY_PASSPHRASE, password=settings.SECRET_REGISTRY_KEY_PASSPHRASE,
) )
) )
try:
# use the _client object to connect
self._client.connect() # type: ignore
response = self._client.send(self._login) # type: ignore
if response.code >= 2000: # type: ignore
self._client.close() # type: ignore
raise LoginError(response.msg) # type: ignore
except TransportError as err:
message = "_initialize_client failed to execute due to a connection error."
logger.error(f"{message} Error: {err}")
raise RegistryError(message, code=ErrorCode.TRANSPORT_ERROR) from err
except LoginError as err:
raise err
except Exception as err:
message = "_initialize_client failed to execute due to an unknown error."
logger.error(f"{message} Error: {err}")
raise RegistryError(message) from err
self.pool_options = { def _disconnect(self) -> None:
# Pool size """Close the connection."""
"size": settings.EPP_CONNECTION_POOL_SIZE, try:
# Which errors the pool should look out for. self._client.send(commands.Logout()) # type: ignore
# Avoid changing this unless necessary, self._client.close() # type: ignore
# it can and will break things. except Exception:
"exc_classes": (TransportError,), logger.warning("Connection to registry was not cleanly closed.")
# Occasionally pings the registry to keep the connection alive.
# Value in seconds => (keepalive / size)
"keepalive": settings.POOL_KEEP_ALIVE,
}
self._pool = None
# Tracks the status of the pool
self.pool_status = PoolStatus()
if start_connection_pool:
self.start_connection_pool()
def _send(self, command): def _send(self, command):
"""Helper function used by `send`.""" """Helper function used by `send`."""
cmd_type = command.__class__.__name__ cmd_type = command.__class__.__name__
# Start a timeout to check if the pool is hanging
timeout = Timeout(settings.POOL_TIMEOUT)
timeout.start()
try: try:
if not self.pool_status.connection_success: # check for the condition that the _client was not initialized properly
raise LoginError("Couldn't connect to the registry after three attempts") # at app initialization
with self._pool.get() as connection: if self._client is None:
response = connection.send(command) self._initialize_client()
except Timeout as t: response = self._client.send(command)
# If more than one pool exists,
# multiple timeouts can be floating around.
# We need to be specific as to which we are targeting.
if t is timeout:
# Flag that the pool is frozen,
# then restart the pool.
self.pool_status.pool_hanging = True
logger.error("Pool timed out")
self.start_connection_pool()
except (ValueError, ParsingError) as err: except (ValueError, ParsingError) as err:
message = f"{cmd_type} failed to execute due to some syntax error." message = f"{cmd_type} failed to execute due to some syntax error."
logger.error(f"{message} Error: {err}", exc_info=True) logger.error(f"{message} Error: {err}")
raise RegistryError(message) from err raise RegistryError(message) from err
except TransportError as err: except TransportError as err:
message = f"{cmd_type} failed to execute due to a connection error." message = f"{cmd_type} failed to execute due to a connection error."
logger.error(f"{message} Error: {err}", exc_info=True) logger.error(f"{message} Error: {err}")
raise RegistryError(message, code=ErrorCode.TRANSPORT_ERROR) from err raise RegistryError(message, code=ErrorCode.TRANSPORT_ERROR) from err
except LoginError as err: except LoginError as err:
# For linter due to it not liking this line length # For linter due to it not liking this line length
text = "failed to execute due to a registry login error." text = "failed to execute due to a registry login error."
message = f"{cmd_type} {text}" message = f"{cmd_type} {text}"
logger.error(f"{message} Error: {err}", exc_info=True) logger.error(f"{message} Error: {err}")
raise RegistryError(message) from err raise RegistryError(message) from err
except Exception as err: except Exception as err:
message = f"{cmd_type} failed to execute due to an unknown error." message = f"{cmd_type} failed to execute due to an unknown error."
logger.error(f"{message} Error: {err}", exc_info=True) logger.error(f"{message} Error: {err}")
raise RegistryError(message) from err raise RegistryError(message) from err
else: else:
if response.code >= 2000: if response.code >= 2000:
raise RegistryError(response.msg, code=response.code) raise RegistryError(response.msg, code=response.code)
else: else:
return response return response
finally:
# Close the timeout no matter what happens def _retry(self, command):
timeout.close() """Retry sending a command through EPP by re-initializing the client
and then sending the command."""
# re-initialize by disconnecting and initial
self._disconnect()
self._initialize_client()
return self._send(command)
def send(self, command, *, cleaned=False): def send(self, command, *, cleaned=False):
"""Login, send the command, then close the connection. Tries 3 times.""" """Login, the send the command. Retry once if an error is found"""
# try to prevent use of this method without appropriate safeguards # try to prevent use of this method without appropriate safeguards
cmd_type = command.__class__.__name__
if not cleaned: if not cleaned:
raise ValueError("Please sanitize user input before sending it.") raise ValueError("Please sanitize user input before sending it.")
# Reopen the pool if its closed
# Only occurs when a login error is raised, after connection is successful
if not self.pool_status.pool_running:
# We want to reopen the connection pool,
# but we don't want the end user to wait while it opens.
# Raise syntax doesn't allow this, so we use a try/catch
# block.
try:
logger.error("Can't contact the Registry. Pool was not running.")
raise RegistryError("Can't contact the Registry. Pool was not running.")
except RegistryError as err:
raise err
finally:
# Code execution will halt after here.
# The end user will need to recall .send.
self.start_connection_pool()
counter = 0 # we'll try 3 times
while True:
try: try:
return self._send(command) return self._send(command)
except RegistryError as err: except RegistryError as err:
if counter < 3 and (err.should_retry() or err.is_transport_error()): if (
logger.info(f"Retrying transport error. Attempt #{counter+1} of 3.") err.is_transport_error()
counter += 1 or err.is_connection_error()
sleep((counter * 50) / 1000) # sleep 50 ms to 150 ms or err.is_session_error()
else: # don't try again or err.is_server_error()
raise err or err.should_retry()
):
def get_pool(self): message = f"{cmd_type} failed and will be retried"
"""Get the current pool instance""" logger.info(f"{message} Error: {err}")
return self._pool return self._retry(command)
def _create_pool(self, client, login, options):
"""Creates and returns new pool instance"""
logger.info("New pool was created")
return EPPConnectionPool(client, login, options)
def start_connection_pool(self, restart_pool_if_exists=True):
"""Starts a connection pool for the registry.
restart_pool_if_exists -> bool:
If an instance of the pool already exists,
then then that instance will be killed first.
It is generally recommended to keep this enabled.
"""
# Since we reuse the same creds for each pool, we can test on
# one socket, and if successful, then we know we can connect.
if not self._test_registry_connection_success():
logger.warning("start_connection_pool() -> Cannot contact the Registry")
self.pool_status.connection_success = False
else: else:
self.pool_status.connection_success = True raise err
# If this function is reinvoked, then ensure
# that we don't have duplicate data sitting around.
if self._pool is not None and restart_pool_if_exists:
logger.info("Connection pool restarting...")
self.kill_pool()
logger.info("Old pool killed")
self._pool = self._create_pool(self._client, self._login, self.pool_options)
self.pool_status.pool_running = True
self.pool_status.pool_hanging = False
logger.info("Connection pool started")
def kill_pool(self):
"""Kills the existing pool. Use this instead
of self._pool = None, as that doesn't clear
gevent instances."""
if self._pool is not None:
self._pool.kill_all_connections()
self._pool = None
self.pool_status.pool_running = False
return None
logger.info("kill_pool() was invoked but there was no pool to delete")
def _test_registry_connection_success(self):
"""Check that determines if our login
credentials are valid, and/or if the Registrar
can be contacted
"""
# This is closed in test_connection_success
socket = Socket(self._client, self._login)
can_login = False
# Something went wrong if this doesn't exist
if hasattr(socket, "test_connection_success"):
can_login = socket.test_connection_success()
return can_login
try: try:
@ -241,5 +168,4 @@ try:
CLIENT = EPPLibWrapper() CLIENT = EPPLibWrapper()
logger.info("registry client initialized") logger.info("registry client initialized")
except Exception: except Exception:
CLIENT = None # type: ignore logger.warning("Unable to configure epplib. Registrar cannot contact registry.")
logger.warning("Unable to configure epplib. Registrar cannot contact registry.", exc_info=True)

View file

@ -1,102 +0,0 @@
import logging
from time import sleep
try:
from epplib import commands
from epplib.client import Client
except ImportError:
pass
from .errors import LoginError
logger = logging.getLogger(__name__)
class Socket:
"""Context manager which establishes a TCP connection with registry."""
def __init__(self, client: Client, login: commands.Login) -> None:
"""Save the epplib client and login details."""
self.client = client
self.login = login
def __enter__(self):
"""Runs connect(), which opens a connection with EPPLib."""
self.connect()
def __exit__(self, *args, **kwargs):
"""Runs disconnect(), which closes a connection with EPPLib."""
self.disconnect()
def connect(self):
"""Use epplib to connect."""
logger.info("Opening socket on connection pool")
self.client.connect()
response = self.client.send(self.login)
if self.is_login_error(response.code):
self.client.close()
raise LoginError(response.msg)
return self.client
def disconnect(self):
"""Close the connection."""
logger.info("Closing socket on connection pool")
try:
self.client.send(commands.Logout())
self.client.close()
except Exception as err:
logger.warning("Connection to registry was not cleanly closed.")
logger.error(err)
def send(self, command):
"""Sends a command to the registry.
If the RegistryError code is >= 2000,
then this function raises a LoginError.
The calling function should handle this."""
response = self.client.send(command)
if self.is_login_error(response.code):
self.client.close()
raise LoginError(response.msg)
return response
def is_login_error(self, code):
"""Returns the result of code >= 2000 for RegistryError.
This indicates that something weird happened on the Registry,
and that we should return a LoginError."""
return code >= 2000
def test_connection_success(self):
"""Tests if a successful connection can be made with the registry.
Tries 3 times."""
# Something went wrong if this doesn't exist
if not hasattr(self.client, "connect"):
logger.warning("self.client does not have a connect attribute")
return False
counter = 0 # we'll try 3 times
while True:
try:
self.client.connect()
response = self.client.send(self.login)
except (LoginError, OSError) as err:
logger.error(err)
should_retry = True
if isinstance(err, LoginError):
should_retry = err.should_retry()
if should_retry and counter < 3:
counter += 1
sleep((counter * 50) / 1000) # sleep 50 ms to 150 ms
else: # don't try again
return False
else:
# If we encounter a login error, fail
if self.is_login_error(response.code):
logger.warning("A login error was found in test_connection_success")
return False
# Otherwise, just return true
return True
finally:
self.disconnect()

View file

@ -0,0 +1,257 @@
from unittest.mock import MagicMock, patch
from django.test import TestCase
from epplibwrapper.client import EPPLibWrapper
from epplibwrapper.errors import RegistryError, LoginError
from .common import less_console_noise
import logging
try:
from epplib.exceptions import TransportError
from epplib.responses import Result
except ImportError:
pass
logger = logging.getLogger(__name__)
class TestClient(TestCase):
"""Test the EPPlibwrapper client"""
def fake_result(self, code, msg):
"""Helper function to create a fake Result object"""
return Result(code=code, msg=msg, res_data=[], cl_tr_id="cl_tr_id", sv_tr_id="sv_tr_id")
@patch("epplibwrapper.client.Client")
def test_initialize_client_success(self, mock_client):
"""Test when the initialize_client is successful"""
with less_console_noise():
# Mock the Client instance and its methods
mock_connect = MagicMock()
# Create a mock Result instance
mock_result = MagicMock(spec=Result)
mock_result.code = 200
mock_result.msg = "Success"
mock_result.res_data = ["data1", "data2"]
mock_result.cl_tr_id = "client_id"
mock_result.sv_tr_id = "server_id"
mock_send = MagicMock(return_value=mock_result)
mock_client.return_value.connect = mock_connect
mock_client.return_value.send = mock_send
# Create EPPLibWrapper instance and initialize client
wrapper = EPPLibWrapper()
# Assert that connect method is called once
mock_connect.assert_called_once()
# Assert that _client is not None after initialization
self.assertIsNotNone(wrapper._client)
@patch("epplibwrapper.client.Client")
def test_initialize_client_transport_error(self, mock_client):
"""Test when the send(login) step of initialize_client raises a TransportError."""
with less_console_noise():
# Mock the Client instance and its methods
mock_connect = MagicMock()
mock_send = MagicMock(side_effect=TransportError("Transport error"))
mock_client.return_value.connect = mock_connect
mock_client.return_value.send = mock_send
with self.assertRaises(RegistryError):
# Create EPPLibWrapper instance and initialize client
# if functioning as expected, initial __init__ should except
# and log any Exception raised
wrapper = EPPLibWrapper()
# so call _initialize_client a second time directly to test
# the raised exception
wrapper._initialize_client()
@patch("epplibwrapper.client.Client")
def test_initialize_client_login_error(self, mock_client):
"""Test when the send(login) step of initialize_client returns (2400) comamnd failed code."""
with less_console_noise():
# Mock the Client instance and its methods
mock_connect = MagicMock()
# Create a mock Result instance
mock_result = MagicMock(spec=Result)
mock_result.code = 2400
mock_result.msg = "Login failed"
mock_result.res_data = ["data1", "data2"]
mock_result.cl_tr_id = "client_id"
mock_result.sv_tr_id = "server_id"
mock_send = MagicMock(return_value=mock_result)
mock_client.return_value.connect = mock_connect
mock_client.return_value.send = mock_send
with self.assertRaises(LoginError):
# Create EPPLibWrapper instance and initialize client
# if functioning as expected, initial __init__ should except
# and log any Exception raised
wrapper = EPPLibWrapper()
# so call _initialize_client a second time directly to test
# the raised exception
wrapper._initialize_client()
@patch("epplibwrapper.client.Client")
def test_initialize_client_unknown_exception(self, mock_client):
"""Test when the send(login) step of initialize_client raises an unexpected Exception."""
with less_console_noise():
# Mock the Client instance and its methods
mock_connect = MagicMock()
mock_send = MagicMock(side_effect=Exception("Unknown exception"))
mock_client.return_value.connect = mock_connect
mock_client.return_value.send = mock_send
with self.assertRaises(RegistryError):
# Create EPPLibWrapper instance and initialize client
# if functioning as expected, initial __init__ should except
# and log any Exception raised
wrapper = EPPLibWrapper()
# so call _initialize_client a second time directly to test
# the raised exception
wrapper._initialize_client()
@patch("epplibwrapper.client.Client")
def test_initialize_client_fails_recovers_with_send_command(self, mock_client):
"""Test when the initialize_client fails on the connect() step. And then a subsequent
call to send() should recover and re-initialize the client and properly return
the successful send command.
Flow:
Initialization step fails at app init
Send command fails (with 2400 code) prompting retry
Client closes and re-initializes, and command is sent successfully"""
with less_console_noise():
# Mock the Client instance and its methods
# close() should return successfully
mock_close = MagicMock()
mock_client.return_value.close = mock_close
# Create success and failure results
command_success_result = self.fake_result(1000, "Command completed successfully")
command_failure_result = self.fake_result(2400, "Command failed")
# side_effect for the connect() calls
# first connect() should raise an Exception
# subsequent connect() calls should return success
connect_call_count = 0
def connect_side_effect(*args, **kwargs):
nonlocal connect_call_count
connect_call_count += 1
if connect_call_count == 1:
raise Exception("Connection failed")
else:
return command_success_result
mock_connect = MagicMock(side_effect=connect_side_effect)
mock_client.return_value.connect = mock_connect
# side_effect for the send() calls
# first send will be the send("InfoDomainCommand") and should fail
# subsequend send() calls should return success
send_call_count = 0
def send_side_effect(*args, **kwargs):
nonlocal send_call_count
send_call_count += 1
if send_call_count == 1:
return command_failure_result
else:
return command_success_result
mock_send = MagicMock(side_effect=send_side_effect)
mock_client.return_value.send = mock_send
# Create EPPLibWrapper instance and call send command
wrapper = EPPLibWrapper()
wrapper.send("InfoDomainCommand", cleaned=True)
# two connect() calls should be made, the initial failed connect()
# and the successful connect() during retry()
self.assertEquals(mock_connect.call_count, 2)
# close() should only be called once, during retry()
mock_close.assert_called_once()
# send called 4 times: failed send("InfoDomainCommand"), passed send(logout),
# passed send(login), passed send("InfoDomainCommand")
self.assertEquals(mock_send.call_count, 4)
@patch("epplibwrapper.client.Client")
def test_send_command_failed_retries_and_fails_again(self, mock_client):
"""Test when the send("InfoDomainCommand) call fails with a 2400, prompting a retry
and the subsequent send("InfoDomainCommand) call also fails with a 2400, raise
a RegistryError
Flow:
Initialization succeeds
Send command fails (with 2400 code) prompting retry
Client closes and re-initializes, and command fails again with 2400"""
with less_console_noise():
# Mock the Client instance and its methods
# connect() and close() should succeed throughout
mock_connect = MagicMock()
mock_close = MagicMock()
# Create a mock Result instance
send_command_success_result = self.fake_result(1000, "Command completed successfully")
send_command_failure_result = self.fake_result(2400, "Command failed")
# side_effect for send command, passes for all other sends (login, logout), but
# fails for send("InfoDomainCommand")
def side_effect(*args, **kwargs):
if args[0] == "InfoDomainCommand":
return send_command_failure_result
else:
return send_command_success_result
mock_send = MagicMock(side_effect=side_effect)
mock_client.return_value.connect = mock_connect
mock_client.return_value.close = mock_close
mock_client.return_value.send = mock_send
with self.assertRaises(RegistryError):
# Create EPPLibWrapper instance and initialize client
wrapper = EPPLibWrapper()
# call send, which should throw a RegistryError (after retry)
wrapper.send("InfoDomainCommand", cleaned=True)
# connect() should be called twice, once during initialization, second time
# during retry
self.assertEquals(mock_connect.call_count, 2)
# close() is called once during retry
mock_close.assert_called_once()
# send() is called 5 times: send(login), send(command) fails, send(logout)
# send(login), send(command)
self.assertEquals(mock_send.call_count, 5)
@patch("epplibwrapper.client.Client")
def test_send_command_failure_prompts_successful_retry(self, mock_client):
"""Test when the send("InfoDomainCommand) call fails with a 2400, prompting a retry
and the subsequent send("InfoDomainCommand) call succeeds
Flow:
Initialization succeeds
Send command fails (with 2400 code) prompting retry
Client closes and re-initializes, and command succeeds"""
with less_console_noise():
# Mock the Client instance and its methods
# connect() and close() should succeed throughout
mock_connect = MagicMock()
mock_close = MagicMock()
# create success and failure result messages
send_command_success_result = self.fake_result(1000, "Command completed successfully")
send_command_failure_result = self.fake_result(2400, "Command failed")
# side_effect for send call, initial send(login) succeeds during initialization, next send(command)
# fails, subsequent sends (logout, login, command) all succeed
send_call_count = 0
def side_effect(*args, **kwargs):
nonlocal send_call_count
send_call_count += 1
if send_call_count == 2:
return send_command_failure_result
else:
return send_command_success_result
mock_send = MagicMock(side_effect=side_effect)
mock_client.return_value.connect = mock_connect
mock_client.return_value.close = mock_close
mock_client.return_value.send = mock_send
# Create EPPLibWrapper instance and initialize client
wrapper = EPPLibWrapper()
wrapper.send("InfoDomainCommand", cleaned=True)
# connect() is called twice, once during initialization of app, once during retry
self.assertEquals(mock_connect.call_count, 2)
# close() is called once, during retry
mock_close.assert_called_once()
# send() is called 5 times: send(login), send(command) fail, send(logout), send(login), send(command)
self.assertEquals(mock_send.call_count, 5)

View file

@ -1,262 +0,0 @@
import datetime
from pathlib import Path
from unittest.mock import MagicMock, patch
from dateutil.tz import tzlocal # type: ignore
from django.test import TestCase
from epplibwrapper.client import EPPLibWrapper
from epplibwrapper.errors import RegistryError
from epplibwrapper.socket import Socket
from epplibwrapper.utility.pool import EPPConnectionPool
from registrar.models.domain import registry
from contextlib import ExitStack
from .common import less_console_noise
import logging
try:
from epplib import commands
from epplib.client import Client
from epplib.exceptions import TransportError
from epplib.transport import SocketTransport
from epplib.models import common, info
except ImportError:
pass
logger = logging.getLogger(__name__)
class TestConnectionPool(TestCase):
"""Tests for our connection pooling behaviour"""
def setUp(self):
# Mimic the settings added to settings.py
self.pool_options = {
# Current pool size
"size": 1,
# Which errors the pool should look out for
"exc_classes": (TransportError,),
# Occasionally pings the registry to keep the connection alive.
# Value in seconds => (keepalive / size)
"keepalive": 60,
}
def fake_socket(self, login, client):
# Linter reasons
pw = "none"
# Create a fake client object
fake_client = Client(
SocketTransport(
"none",
cert_file="path/to/cert_file",
key_file="path/to/key_file",
password=pw,
)
)
return Socket(fake_client, MagicMock())
def patch_success(self):
return True
def fake_send(self, command, cleaned=None):
mock = MagicMock(
code=1000,
msg="Command completed successfully",
res_data=None,
cl_tr_id="xkw1uo#2023-10-17T15:29:09.559376",
sv_tr_id="5CcH4gxISuGkq8eqvr1UyQ==-35a",
extensions=[],
msg_q=None,
)
return mock
def fake_client(mock_client):
pw = "none"
client = Client(
SocketTransport(
"none",
cert_file="path/to/cert_file",
key_file="path/to/key_file",
password=pw,
)
)
return client
@patch.object(EPPLibWrapper, "_test_registry_connection_success", patch_success)
def test_pool_sends_data(self):
"""A .send is invoked on the pool successfully"""
expected_result = {
"cl_tr_id": None,
"code": 1000,
"extensions": [],
"msg": "Command completed successfully",
"msg_q": None,
"res_data": [
info.InfoDomainResultData(
roid="DF1340360-GOV",
statuses=[
common.Status(
state="serverTransferProhibited",
description=None,
lang="en",
),
common.Status(state="inactive", description=None, lang="en"),
],
cl_id="gov2023-ote",
cr_id="gov2023-ote",
cr_date=datetime.datetime(2023, 8, 15, 23, 56, 36, tzinfo=tzlocal()),
up_id="gov2023-ote",
up_date=datetime.datetime(2023, 8, 17, 2, 3, 19, tzinfo=tzlocal()),
tr_date=None,
name="test3.gov",
registrant="TuaWnx9hnm84GCSU",
admins=[],
nsset=None,
keyset=None,
ex_date=datetime.date(2024, 8, 15),
auth_info=info.DomainAuthInfo(pw="2fooBAR123fooBaz"),
)
],
"sv_tr_id": "wRRNVhKhQW2m6wsUHbo/lA==-29a",
}
# Mock a response from EPP
def fake_receive(command, cleaned=None):
location = Path(__file__).parent / "utility" / "infoDomain.xml"
xml = (location).read_bytes()
return xml
def do_nothing(command):
pass
# Mock what happens inside the "with"
with ExitStack() as stack:
stack.enter_context(patch.object(EPPConnectionPool, "_create_socket", self.fake_socket))
stack.enter_context(patch.object(Socket, "connect", self.fake_client))
stack.enter_context(patch.object(EPPConnectionPool, "kill_all_connections", do_nothing))
stack.enter_context(patch.object(SocketTransport, "send", self.fake_send))
stack.enter_context(patch.object(SocketTransport, "receive", fake_receive))
with less_console_noise():
# Restart the connection pool
registry.start_connection_pool()
# Pool should be running, and be the right size
self.assertEqual(registry.pool_status.connection_success, True)
self.assertEqual(registry.pool_status.pool_running, True)
# Send a command
result = registry.send(commands.InfoDomain(name="test.gov"), cleaned=True)
# Should this ever fail, it either means that the schema has changed,
# or the pool is broken.
# If the schema has changed: Update the associated infoDomain.xml file
self.assertEqual(result.__dict__, expected_result)
# The number of open pools should match the number of requested ones.
# If it is 0, then they failed to open
self.assertEqual(len(registry._pool.conn), self.pool_options["size"])
# Kill the connection pool
registry.kill_pool()
@patch.object(EPPLibWrapper, "_test_registry_connection_success", patch_success)
def test_pool_restarts_on_send(self):
"""A .send is invoked, but the pool isn't running.
The pool should restart."""
expected_result = {
"cl_tr_id": None,
"code": 1000,
"extensions": [],
"msg": "Command completed successfully",
"msg_q": None,
"res_data": [
info.InfoDomainResultData(
roid="DF1340360-GOV",
statuses=[
common.Status(
state="serverTransferProhibited",
description=None,
lang="en",
),
common.Status(state="inactive", description=None, lang="en"),
],
cl_id="gov2023-ote",
cr_id="gov2023-ote",
cr_date=datetime.datetime(2023, 8, 15, 23, 56, 36, tzinfo=tzlocal()),
up_id="gov2023-ote",
up_date=datetime.datetime(2023, 8, 17, 2, 3, 19, tzinfo=tzlocal()),
tr_date=None,
name="test3.gov",
registrant="TuaWnx9hnm84GCSU",
admins=[],
nsset=None,
keyset=None,
ex_date=datetime.date(2024, 8, 15),
auth_info=info.DomainAuthInfo(pw="2fooBAR123fooBaz"),
)
],
"sv_tr_id": "wRRNVhKhQW2m6wsUHbo/lA==-29a",
}
# Mock a response from EPP
def fake_receive(command, cleaned=None):
location = Path(__file__).parent / "utility" / "infoDomain.xml"
xml = (location).read_bytes()
return xml
def do_nothing(command):
pass
# Mock what happens inside the "with"
with ExitStack() as stack:
stack.enter_context(patch.object(EPPConnectionPool, "_create_socket", self.fake_socket))
stack.enter_context(patch.object(Socket, "connect", self.fake_client))
stack.enter_context(patch.object(EPPConnectionPool, "kill_all_connections", do_nothing))
stack.enter_context(patch.object(SocketTransport, "send", self.fake_send))
stack.enter_context(patch.object(SocketTransport, "receive", fake_receive))
with less_console_noise():
# Start the connection pool
registry.start_connection_pool()
# Kill the connection pool
registry.kill_pool()
self.assertEqual(registry.pool_status.pool_running, False)
# An exception should be raised as end user will be informed
# that they cannot connect to EPP
with self.assertRaises(RegistryError):
expected = "InfoDomain failed to execute due to a connection error."
result = registry.send(commands.InfoDomain(name="test.gov"), cleaned=True)
self.assertEqual(result, expected)
# A subsequent command should be successful, as the pool restarts
result = registry.send(commands.InfoDomain(name="test.gov"), cleaned=True)
# Should this ever fail, it either means that the schema has changed,
# or the pool is broken.
# If the schema has changed: Update the associated infoDomain.xml file
self.assertEqual(result.__dict__, expected_result)
# The number of open pools should match the number of requested ones.
# If it is 0, then they failed to open
self.assertEqual(len(registry._pool.conn), self.pool_options["size"])
# Kill the connection pool
registry.kill_pool()
@patch.object(EPPLibWrapper, "_test_registry_connection_success", patch_success)
def test_raises_connection_error(self):
"""A .send is invoked on the pool, but registry connection is lost
right as we send a command."""
with ExitStack() as stack:
stack.enter_context(patch.object(EPPConnectionPool, "_create_socket", self.fake_socket))
stack.enter_context(patch.object(Socket, "connect", self.fake_client))
with less_console_noise():
# Start the connection pool
registry.start_connection_pool()
# Pool should be running
self.assertEqual(registry.pool_status.connection_success, True)
self.assertEqual(registry.pool_status.pool_running, True)
# Try to send a command out - should fail
with self.assertRaises(RegistryError):
expected = "InfoDomain failed to execute due to a connection error."
result = registry.send(commands.InfoDomain(name="test.gov"), cleaned=True)
self.assertEqual(result, expected)

View file

@ -1,151 +0,0 @@
import logging
from typing import List
import gevent
from geventconnpool import ConnectionPool
from epplibwrapper.socket import Socket
from epplibwrapper.utility.pool_error import PoolError, PoolErrorCodes
try:
from epplib.commands import Hello
from epplib.exceptions import TransportError
except ImportError:
pass
from gevent.lock import BoundedSemaphore
from collections import deque
logger = logging.getLogger(__name__)
class EPPConnectionPool(ConnectionPool):
"""A connection pool for EPPLib.
Args:
client (Client): The client
login (commands.Login): Login creds
options (dict): Options for the ConnectionPool
base class
"""
def __init__(self, client, login, options: dict):
# For storing shared credentials
self._client = client
self._login = login
# Keep track of each greenlet
self.greenlets: List[gevent.Greenlet] = []
# Define optional pool settings.
# Kept in a dict so that the parent class,
# client.py, can maintain seperation/expandability
self.size = 1
if "size" in options:
self.size = options["size"]
self.exc_classes = tuple((TransportError,))
if "exc_classes" in options:
self.exc_classes = options["exc_classes"]
self.keepalive = None
if "keepalive" in options:
self.keepalive = options["keepalive"]
# Determines the period in which new
# gevent threads are spun up.
# This time period is in seconds. So for instance, .1 would be .1 seconds.
self.spawn_frequency = 0.1
if "spawn_frequency" in options:
self.spawn_frequency = options["spawn_frequency"]
self.conn: deque = deque()
self.lock = BoundedSemaphore(self.size)
self.populate_all_connections()
def _new_connection(self):
socket = self._create_socket(self._client, self._login)
try:
connection = socket.connect()
return connection
except Exception as err:
message = f"Failed to execute due to a registry error: {err}"
logger.error(message, exc_info=True)
# We want to raise a pool error rather than a LoginError here
# because if this occurs internally, we should handle this
# differently than we otherwise would for LoginError.
raise PoolError(code=PoolErrorCodes.NEW_CONNECTION_FAILED) from err
def _keepalive(self, c):
"""Sends a command to the server to keep the connection alive."""
try:
# Sends a ping to the registry via EPPLib
c.send(Hello())
except Exception as err:
message = "Failed to keep the connection alive."
logger.error(message, exc_info=True)
raise PoolError(code=PoolErrorCodes.KEEP_ALIVE_FAILED) from err
def _keepalive_periodic(self):
"""Overriding _keepalive_periodic from geventconnpool so that PoolErrors
are properly handled, as opposed to printing to stdout"""
delay = float(self.keepalive) / self.size
while 1:
try:
with self.get() as c:
self._keepalive(c)
except PoolError as err:
logger.error(err.message, exc_info=True)
except self.exc_classes:
# Nothing to do, the pool will generate a new connection later
pass
gevent.sleep(delay)
def _create_socket(self, client, login) -> Socket:
"""Creates and returns a socket instance"""
socket = Socket(client, login)
return socket
def get_connections(self):
"""Returns the connection queue"""
return self.conn
def kill_all_connections(self):
"""Kills all active connections in the pool."""
try:
if len(self.conn) > 0 or len(self.greenlets) > 0:
logger.info("Attempting to kill connections")
gevent.killall(self.greenlets)
self.greenlets.clear()
for connection in self.conn:
connection.disconnect()
self.conn.clear()
# Clear the semaphore
self.lock = BoundedSemaphore(self.size)
logger.info("Finished killing connections")
else:
logger.info("No connections to kill.")
except Exception as err:
logger.error("Could not kill all connections.")
raise PoolError(code=PoolErrorCodes.KILL_ALL_FAILED) from err
def populate_all_connections(self):
"""Generates the connection pool.
If any connections exist, kill them first.
Based off of the __init__ definition for geventconnpool.
"""
if len(self.conn) > 0 or len(self.greenlets) > 0:
self.kill_all_connections()
# Setup the lock
for i in range(self.size):
self.lock.acquire()
# Open multiple connections
for i in range(self.size):
self.greenlets.append(gevent.spawn_later(self.spawn_frequency * i, self._addOne))
# Open a "keepalive" thread if we want to ping open connections
if self.keepalive:
self.greenlets.append(gevent.spawn(self._keepalive_periodic))

View file

@ -1,46 +0,0 @@
from enum import IntEnum
class PoolErrorCodes(IntEnum):
"""Used in the PoolError class for
error mapping.
Overview of contact error codes:
- 2000 KILL_ALL_FAILED
- 2001 NEW_CONNECTION_FAILED
- 2002 KEEP_ALIVE_FAILED
"""
KILL_ALL_FAILED = 2000
NEW_CONNECTION_FAILED = 2001
KEEP_ALIVE_FAILED = 2002
class PoolError(Exception):
"""
Overview of contact error codes:
- 2000 KILL_ALL_FAILED
- 2001 NEW_CONNECTION_FAILED
- 2002 KEEP_ALIVE_FAILED
Note: These are separate from the error codes returned from EppLib
"""
# Used variables due to linter requirements
kill_failed = "Could not kill all connections. Are multiple pools running?"
conn_failed = "Failed to execute due to a registry error. See previous logs to determine the cause of the error."
alive_failed = "Failed to keep the connection alive. It is likely that the registry returned a LoginError."
_error_mapping = {
PoolErrorCodes.KILL_ALL_FAILED: kill_failed,
PoolErrorCodes.NEW_CONNECTION_FAILED: conn_failed,
PoolErrorCodes.KEEP_ALIVE_FAILED: alive_failed,
}
def __init__(self, *args, code=None, **kwargs):
super().__init__(*args, **kwargs)
self.code = code
if self.code in self._error_mapping:
self.message = self._error_mapping.get(self.code)
def __str__(self):
return f"{self.message}"

View file

@ -1,12 +0,0 @@
class PoolStatus:
"""A list of Booleans to keep track of Pool Status.
pool_running -> bool: Tracks if the pool itself is active or not.
connection_success -> bool: Tracks if connection is possible with the registry.
pool_hanging -> pool: Tracks if the pool has exceeded its timeout period.
"""
def __init__(self):
self.pool_running = False
self.connection_success = False
self.pool_hanging = False

View file

@ -188,15 +188,12 @@ WSGI_APPLICATION = "registrar.config.wsgi.application"
# https://docs.djangoproject.com/en/4.0/howto/static-files/ # https://docs.djangoproject.com/en/4.0/howto/static-files/
# Caching is disabled by default. CACHES = {
# For a low to medium traffic site, caching causes more "default": {
# problems than it solves. Should caching be desired, "BACKEND": "django.core.cache.backends.db.DatabaseCache",
# a reasonable start might be: "LOCATION": "cache_table",
# CACHES = { }
# "default": { }
# "BACKEND": "django.core.cache.backends.db.DatabaseCache",
# }
# }
# Absolute path to the directory where `collectstatic` # Absolute path to the directory where `collectstatic`
# will place static files for deployment. # will place static files for deployment.
@ -601,20 +598,6 @@ SECRET_REGISTRY_KEY = secret_registry_key
SECRET_REGISTRY_KEY_PASSPHRASE = secret_registry_key_passphrase SECRET_REGISTRY_KEY_PASSPHRASE = secret_registry_key_passphrase
SECRET_REGISTRY_HOSTNAME = secret_registry_hostname SECRET_REGISTRY_HOSTNAME = secret_registry_hostname
# Use this variable to set the size of our connection pool in client.py
# WARNING: Setting this value too high could cause frequent app crashes!
# Having too many connections open could cause the sandbox to timeout,
# as the spinup time could exceed the timeout time.
EPP_CONNECTION_POOL_SIZE = 1
# Determines the interval in which we ping open connections in seconds
# Calculated as POOL_KEEP_ALIVE / EPP_CONNECTION_POOL_SIZE
POOL_KEEP_ALIVE = 60
# Determines how long we try to keep a pool alive for,
# before restarting it.
POOL_TIMEOUT = 60
# endregion # endregion
# region: Security and Privacy----------------------------------------------### # region: Security and Privacy----------------------------------------------###

View file

@ -5,7 +5,6 @@ import phonenumber_field.modelfields
class Migration(migrations.Migration): class Migration(migrations.Migration):
dependencies = [ dependencies = [
("registrar", "0070_domainapplication_rejection_reason"), ("registrar", "0070_domainapplication_rejection_reason"),
] ]

View file

@ -0,0 +1,25 @@
# Generated by Django 4.2.10 on 2024-03-05 21:44
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
("registrar", "0071_alter_contact_first_name_alter_contact_last_name_and_more"),
]
operations = [
migrations.AlterField(
model_name="publiccontact",
name="fax",
field=models.CharField(
help_text="Contact's fax number (null ok). Must be in ITU.E164.2005 format.", null=True
),
),
migrations.AlterField(
model_name="publiccontact",
name="voice",
field=models.CharField(help_text="Contact's phone number. Must be in ITU.E164.2005 format"),
),
]

View file

@ -8,8 +8,6 @@ from registrar.utility.enums import DefaultEmail
from .utility.time_stamped_model import TimeStampedModel from .utility.time_stamped_model import TimeStampedModel
from phonenumber_field.modelfields import PhoneNumberField # type: ignore
def get_id(): def get_id():
"""Generate a 16 character registry ID with a low probability of collision.""" """Generate a 16 character registry ID with a low probability of collision."""
@ -71,8 +69,8 @@ class PublicContact(TimeStampedModel):
pc = models.CharField(null=False, help_text="Contact's postal code") pc = models.CharField(null=False, help_text="Contact's postal code")
cc = models.CharField(null=False, help_text="Contact's country code") cc = models.CharField(null=False, help_text="Contact's country code")
email = models.EmailField(null=False, help_text="Contact's email address") email = models.EmailField(null=False, help_text="Contact's email address")
voice = PhoneNumberField(null=False, help_text="Contact's phone number. Must be in ITU.E164.2005 format") voice = models.CharField(null=False, help_text="Contact's phone number. Must be in ITU.E164.2005 format")
fax = PhoneNumberField( fax = models.CharField(
null=True, null=True,
help_text="Contact's fax number (null ok). Must be in ITU.E164.2005 format.", help_text="Contact's fax number (null ok). Must be in ITU.E164.2005 format.",
) )

View file

@ -475,7 +475,6 @@ class AuditedAdminMockData:
def mock_user(): def mock_user():
"""A simple user.""" """A simple user."""
user_kwargs = dict( user_kwargs = dict(
id=4,
first_name="Jeff", first_name="Jeff",
last_name="Lebowski", last_name="Lebowski",
) )

View file

@ -1542,6 +1542,8 @@ class DomainInvitationAdminTest(TestCase):
def tearDown(self): def tearDown(self):
"""Delete all DomainInvitation objects""" """Delete all DomainInvitation objects"""
DomainInvitation.objects.all().delete() DomainInvitation.objects.all().delete()
User.objects.all().delete()
Contact.objects.all().delete()
def test_get_filters(self): def test_get_filters(self):
"""Ensures that our filters are displaying correctly""" """Ensures that our filters are displaying correctly"""

View file

@ -1,14 +1,14 @@
-i https://pypi.python.org/simple -i https://pypi.python.org/simple
annotated-types==0.6.0; python_version >= '3.8' annotated-types==0.6.0; python_version >= '3.8'
asgiref==3.7.2; python_version >= '3.7' asgiref==3.7.2; python_version >= '3.7'
boto3==1.34.37; python_version >= '3.8' boto3==1.34.54; python_version >= '3.8'
botocore==1.34.37; python_version >= '3.8' botocore==1.34.54; python_version >= '3.8'
cachetools==5.3.2; python_version >= '3.7' cachetools==5.3.3; python_version >= '3.7'
certifi==2024.2.2; python_version >= '3.6' certifi==2024.2.2; python_version >= '3.6'
cfenv==0.5.3 cfenv==0.5.3
cffi==1.16.0; platform_python_implementation != 'PyPy' cffi==1.16.0; platform_python_implementation != 'PyPy'
charset-normalizer==3.3.2; python_full_version >= '3.7.0' charset-normalizer==3.3.2; python_full_version >= '3.7.0'
cryptography==42.0.2; python_version >= '3.7' cryptography==42.0.5; python_version >= '3.7'
defusedxml==0.7.1; python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3, 3.4' defusedxml==0.7.1; python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3, 3.4'
dj-database-url==2.1.0 dj-database-url==2.1.0
dj-email-url==1.0.6 dj-email-url==1.0.6
@ -17,18 +17,17 @@ django-allow-cidr==0.7.1
django-auditlog==2.3.0; python_version >= '3.7' django-auditlog==2.3.0; python_version >= '3.7'
django-cache-url==3.4.5 django-cache-url==3.4.5
django-cors-headers==4.3.1; python_version >= '3.8' django-cors-headers==4.3.1; python_version >= '3.8'
django-csp==3.7 django-csp==3.8
django-fsm==2.8.1 django-fsm==2.8.1
django-login-required-middleware==0.9.0 django-login-required-middleware==0.9.0
django-phonenumber-field[phonenumberslite]==7.3.0; python_version >= '3.8' django-phonenumber-field[phonenumberslite]==7.3.0; python_version >= '3.8'
django-widget-tweaks==1.5.0; python_version >= '3.8' django-widget-tweaks==1.5.0; python_version >= '3.8'
environs[django]==10.3.0; python_version >= '3.8' environs[django]==10.3.0; python_version >= '3.8'
faker==23.1.0; python_version >= '3.8' faker==23.3.0; python_version >= '3.8'
fred-epplib@ git+https://github.com/cisagov/epplib.git@d56d183f1664f34c40ca9716a3a9a345f0ef561c fred-epplib@ git+https://github.com/cisagov/epplib.git@d56d183f1664f34c40ca9716a3a9a345f0ef561c
furl==2.1.3 furl==2.1.3
future==0.18.3; python_version >= '2.6' and python_version not in '3.0, 3.1, 3.2, 3.3' future==1.0.0; python_version >= '2.6' and python_version not in '3.0, 3.1, 3.2, 3.3'
gevent==23.9.1; python_version >= '3.8' gevent==24.2.1; python_version >= '3.8'
geventconnpool@ git+https://github.com/rasky/geventconnpool.git@1bbb93a714a331a069adf27265fe582d9ba7ecd4
greenlet==3.0.3; python_version >= '3.7' greenlet==3.0.3; python_version >= '3.7'
gunicorn==21.2.0; python_version >= '3.5' gunicorn==21.2.0; python_version >= '3.5'
idna==3.6; python_version >= '3.5' idna==3.6; python_version >= '3.5'
@ -36,27 +35,28 @@ jmespath==1.0.1; python_version >= '3.7'
lxml==5.1.0; python_version >= '3.6' lxml==5.1.0; python_version >= '3.6'
mako==1.3.2; python_version >= '3.8' mako==1.3.2; python_version >= '3.8'
markupsafe==2.1.5; python_version >= '3.7' markupsafe==2.1.5; python_version >= '3.7'
marshmallow==3.20.2; python_version >= '3.8' marshmallow==3.21.0; python_version >= '3.8'
oic==1.6.1; python_version ~= '3.7' oic==1.6.1; python_version ~= '3.7'
orderedmultidict==1.0.1 orderedmultidict==1.0.1
packaging==23.2; python_version >= '3.7' packaging==23.2; python_version >= '3.7'
phonenumberslite==8.13.29 phonenumberslite==8.13.31
psycopg2-binary==2.9.9; python_version >= '3.7' psycopg2-binary==2.9.9; python_version >= '3.7'
pycparser==2.21 pycparser==2.21
pycryptodomex==3.20.0; python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3, 3.4' pycryptodomex==3.20.0; python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3, 3.4'
pydantic==2.6.1; python_version >= '3.8' pydantic==2.6.3; python_version >= '3.8'
pydantic-core==2.16.2; python_version >= '3.8' pydantic-core==2.16.3; python_version >= '3.8'
pydantic-settings==2.1.0; python_version >= '3.8' pydantic-settings==2.2.1; python_version >= '3.8'
pyjwkest==1.4.2 pyjwkest==1.4.2
python-dateutil==2.8.2; python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3' python-dateutil==2.9.0.post0; python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3'
python-dotenv==1.0.1; python_version >= '3.8' python-dotenv==1.0.1; python_version >= '3.8'
requests==2.31.0; python_version >= '3.7' requests==2.31.0; python_version >= '3.7'
s3transfer==0.10.0; python_version >= '3.8' s3transfer==0.10.0; python_version >= '3.8'
setuptools==69.0.3; python_version >= '3.8' setuptools==69.1.1; python_version >= '3.8'
six==1.16.0; python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3' six==1.16.0; python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3'
sqlparse==0.4.4; python_version >= '3.5' sqlparse==0.4.4; python_version >= '3.5'
typing-extensions==4.9.0; python_version >= '3.8' tblib==3.0.0; python_version >= '3.8'
typing-extensions==4.10.0; python_version >= '3.8'
urllib3==2.0.7; python_version >= '3.7' urllib3==2.0.7; python_version >= '3.7'
whitenoise==6.6.0; python_version >= '3.8' whitenoise==6.6.0; python_version >= '3.8'
zope.event==5.0; python_version >= '3.7' zope.event==5.0; python_version >= '3.7'
zope.interface==6.1; python_version >= '3.7' zope.interface==6.2; python_version >= '3.7'

View file

@ -6,4 +6,4 @@ set -o pipefail
# Make sure that django's `collectstatic` has been run locally before pushing up to any environment, # Make sure that django's `collectstatic` has been run locally before pushing up to any environment,
# so that the styles and static assets to show up correctly on any environment. # so that the styles and static assets to show up correctly on any environment.
gunicorn --worker-class=gevent registrar.config.wsgi -t 60 gunicorn --workers=3 --worker-class=gevent registrar.config.wsgi -t 60