From e6e0c2c416857763d83fef8be032613e73fee0a0 Mon Sep 17 00:00:00 2001 From: Cameron Dixon Date: Fri, 6 Oct 2023 12:16:20 -0400 Subject: [PATCH 001/120] Content revision, add :emoji: names --- .github/ISSUE_TEMPLATE/issue-default.yml | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/.github/ISSUE_TEMPLATE/issue-default.yml b/.github/ISSUE_TEMPLATE/issue-default.yml index 27ec10415..2252845bf 100644 --- a/.github/ISSUE_TEMPLATE/issue-default.yml +++ b/.github/ISSUE_TEMPLATE/issue-default.yml @@ -6,13 +6,13 @@ body: id: title-help attributes: value: | - > Titles should be short, descriptive, and compelling. + > Titles should be short, descriptive, and compelling. Use sentence case. - type: textarea id: issue-description attributes: label: Issue description and context description: | - Describe the issue so that someone who wasn't present for its discovery can understand the problem and why it matters. Use full sentences, plain language, and good [formatting](https://docs.github.com/en/get-started/writing-on-github/getting-started-with-writing-and-formatting-on-github/basic-writing-and-formatting-syntax). Share desired outcomes or potential next steps. Images or links to other content/context (like documents or Slack discussions) are welcome. + Describe the issue so that someone who wasn't present for its discovery can understand why it matters. Use full sentences, plain language, and good [formatting](https://docs.github.com/en/get-started/writing-on-github/getting-started-with-writing-and-formatting-on-github/basic-writing-and-formatting-syntax). Screenshots and links to documents/discussions are welcome. validations: required: true - type: textarea @@ -20,13 +20,13 @@ body: attributes: label: Acceptance criteria description: "If known, share 1-3 statements that would need to be true for this issue to be considered resolved. Use a [task list](https://docs.github.com/en/get-started/writing-on-github/working-with-advanced-formatting/about-task-lists#creating-task-lists) if appropriate." - placeholder: "- [ ] The button does the thing." + placeholder: "- [ ]" - type: textarea id: links-to-other-issues attributes: label: Links to other issues description: | - Add the issue #number of other issues this relates to and how (e.g., 🚧 Blocks, ⛔️ Is blocked by, 🔄 Relates to). + "Add issue #numbers this relates to and how (e.g., 🚧 :construction: Blocks, ⛔️ :no_entry: Is blocked by, 🔄 :repeat: Relates to)." placeholder: 🔄 Relates to... - type: markdown id: note From af852125f87acfc1851ce100a2d620fc786e398d Mon Sep 17 00:00:00 2001 From: zandercymatics <141044360+zandercymatics@users.noreply.github.com> Date: Wed, 11 Oct 2023 12:58:00 -0600 Subject: [PATCH 002/120] Template --- src/Pipfile | 1 + src/Pipfile.lock | 1712 +++++++++++++++++------------ src/epplibwrapper/client.py | 15 +- src/epplibwrapper/socket.py | 23 +- src/epplibwrapper/utility/pool.py | 25 + src/requirements.txt | 92 +- 6 files changed, 1131 insertions(+), 737 deletions(-) create mode 100644 src/epplibwrapper/utility/pool.py diff --git a/src/Pipfile b/src/Pipfile index 6900b0bcf..5f84ac448 100644 --- a/src/Pipfile +++ b/src/Pipfile @@ -26,6 +26,7 @@ boto3 = "*" typing-extensions ='*' django-login-required-middleware = "*" fred-epplib = {git = "https://github.com/cisagov/epplib.git", ref = "master"} +gsocketpool = "*" [dev-packages] django-debug-toolbar = "*" diff --git a/src/Pipfile.lock b/src/Pipfile.lock index 3e7ae367d..e01528f1d 100644 --- a/src/Pipfile.lock +++ b/src/Pipfile.lock @@ -1,7 +1,7 @@ { "_meta": { "hash": { - "sha256": "1242c67b31261243a35128410d4a928fca3729ddac13b8c8e25adf31445c6328" + "sha256": "49cd54bd0c272b04889898edc62b2a314d9675409d862a93e257b3f79e09f84e" }, "pipfile-spec": 6, "requires": {}, @@ -14,6 +14,14 @@ ] }, "default": { + "annotated-types": { + "hashes": [ + "sha256:0641064de18ba7a25dee8f96403ebc39113d0cb953a01429249d5c7564666a43", + "sha256:563339e807e53ffd9c267e99fc6d9ea23eb8443c08f112651963e24e22f84a5d" + ], + "markers": "python_version >= '3.8'", + "version": "==0.6.0" + }, "asgiref": { "hashes": [ "sha256:89b2ef2247e3b562a16eef663bc0e2e703ec6468e2fa8a5cd61cd449786d4f6e", @@ -24,19 +32,20 @@ }, "boto3": { "hashes": [ - "sha256:30f8ab1cf89d5864a80ba2d5eb5316dbd2a63c9469877e0cffb522630438aa85", - "sha256:77e8fa7c257f9ed8bfe0c3ffc2ccc47b1cfa27058f99415b6003699d1202e0c0" + "sha256:0dfa2fc96ccafce4feb23044d6cba8b25075ad428a0c450d369d099c6a1059d2", + "sha256:148eeba0f1867b3db5b3e5ae2997d75a94d03fad46171374a0819168c36f7ed0" ], "index": "pypi", - "version": "==1.26.145" + "markers": "python_version >= '3.7'", + "version": "==1.28.62" }, "botocore": { "hashes": [ - "sha256:264a3f19ed280d80711b7e278be09acff7ed379a96432fdf179b4e6e3a687e6a", - "sha256:65e2a2b1cc70583225f87d6d63736215f93c6234721967bdab872270ba7a1f45" + "sha256:272b78ac65256b6294cb9cdb0ac484d447ad3a85642e33cb6a3b1b8afee15a4c", + "sha256:be792d806afc064694a2d0b9b25779f3ca0c1584b29a35ac32e67f0064ddb8b7" ], "markers": "python_version >= '3.7'", - "version": "==1.29.145" + "version": "==1.31.62" }, "cachetools": { "hashes": [ @@ -44,15 +53,16 @@ "sha256:dce83f2d9b4e1f732a8cd44af8e8fab2dbe46201467fc98b3ef8f269092bf62b" ], "index": "pypi", + "markers": "python_version >= '3.7'", "version": "==5.3.1" }, "certifi": { "hashes": [ - "sha256:0f0d56dc5a6ad56fd4ba36484d6cc34451e1c6548c61daad8c320169f91eddc7", - "sha256:c6c2e98f5c7869efca1f8916fed228dd91539f9f1b444c314c06eef02980c716" + "sha256:539cc1d13202e33ca466e88b2807e29f4c13049d6d87031a3c110744495cb082", + "sha256:92d6037539857d8206b8f6ae472e8b77db8058fec5937a1ef3f54304089edbb9" ], "markers": "python_version >= '3.6'", - "version": "==2023.5.7" + "version": "==2023.7.22" }, "cfenv": { "hashes": [ @@ -64,178 +74,186 @@ }, "cffi": { "hashes": [ - "sha256:00a9ed42e88df81ffae7a8ab6d9356b371399b91dbdf0c3cb1e84c03a13aceb5", - "sha256:03425bdae262c76aad70202debd780501fabeaca237cdfddc008987c0e0f59ef", - "sha256:04ed324bda3cda42b9b695d51bb7d54b680b9719cfab04227cdd1e04e5de3104", - "sha256:0e2642fe3142e4cc4af0799748233ad6da94c62a8bec3a6648bf8ee68b1c7426", - "sha256:173379135477dc8cac4bc58f45db08ab45d228b3363adb7af79436135d028405", - "sha256:198caafb44239b60e252492445da556afafc7d1e3ab7a1fb3f0584ef6d742375", - "sha256:1e74c6b51a9ed6589199c787bf5f9875612ca4a8a0785fb2d4a84429badaf22a", - "sha256:2012c72d854c2d03e45d06ae57f40d78e5770d252f195b93f581acf3ba44496e", - "sha256:21157295583fe8943475029ed5abdcf71eb3911894724e360acff1d61c1d54bc", - "sha256:2470043b93ff09bf8fb1d46d1cb756ce6132c54826661a32d4e4d132e1977adf", - "sha256:285d29981935eb726a4399badae8f0ffdff4f5050eaa6d0cfc3f64b857b77185", - "sha256:30d78fbc8ebf9c92c9b7823ee18eb92f2e6ef79b45ac84db507f52fbe3ec4497", - "sha256:320dab6e7cb2eacdf0e658569d2575c4dad258c0fcc794f46215e1e39f90f2c3", - "sha256:33ab79603146aace82c2427da5ca6e58f2b3f2fb5da893ceac0c42218a40be35", - "sha256:3548db281cd7d2561c9ad9984681c95f7b0e38881201e157833a2342c30d5e8c", - "sha256:3799aecf2e17cf585d977b780ce79ff0dc9b78d799fc694221ce814c2c19db83", - "sha256:39d39875251ca8f612b6f33e6b1195af86d1b3e60086068be9cc053aa4376e21", - "sha256:3b926aa83d1edb5aa5b427b4053dc420ec295a08e40911296b9eb1b6170f6cca", - "sha256:3bcde07039e586f91b45c88f8583ea7cf7a0770df3a1649627bf598332cb6984", - "sha256:3d08afd128ddaa624a48cf2b859afef385b720bb4b43df214f85616922e6a5ac", - "sha256:3eb6971dcff08619f8d91607cfc726518b6fa2a9eba42856be181c6d0d9515fd", - "sha256:40f4774f5a9d4f5e344f31a32b5096977b5d48560c5592e2f3d2c4374bd543ee", - "sha256:4289fc34b2f5316fbb762d75362931e351941fa95fa18789191b33fc4cf9504a", - "sha256:470c103ae716238bbe698d67ad020e1db9d9dba34fa5a899b5e21577e6d52ed2", - "sha256:4f2c9f67e9821cad2e5f480bc8d83b8742896f1242dba247911072d4fa94c192", - "sha256:50a74364d85fd319352182ef59c5c790484a336f6db772c1a9231f1c3ed0cbd7", - "sha256:54a2db7b78338edd780e7ef7f9f6c442500fb0d41a5a4ea24fff1c929d5af585", - "sha256:5635bd9cb9731e6d4a1132a498dd34f764034a8ce60cef4f5319c0541159392f", - "sha256:59c0b02d0a6c384d453fece7566d1c7e6b7bae4fc5874ef2ef46d56776d61c9e", - "sha256:5d598b938678ebf3c67377cdd45e09d431369c3b1a5b331058c338e201f12b27", - "sha256:5df2768244d19ab7f60546d0c7c63ce1581f7af8b5de3eb3004b9b6fc8a9f84b", - "sha256:5ef34d190326c3b1f822a5b7a45f6c4535e2f47ed06fec77d3d799c450b2651e", - "sha256:6975a3fac6bc83c4a65c9f9fcab9e47019a11d3d2cf7f3c0d03431bf145a941e", - "sha256:6c9a799e985904922a4d207a94eae35c78ebae90e128f0c4e521ce339396be9d", - "sha256:70df4e3b545a17496c9b3f41f5115e69a4f2e77e94e1d2a8e1070bc0c38c8a3c", - "sha256:7473e861101c9e72452f9bf8acb984947aa1661a7704553a9f6e4baa5ba64415", - "sha256:8102eaf27e1e448db915d08afa8b41d6c7ca7a04b7d73af6514df10a3e74bd82", - "sha256:87c450779d0914f2861b8526e035c5e6da0a3199d8f1add1a665e1cbc6fc6d02", - "sha256:8b7ee99e510d7b66cdb6c593f21c043c248537a32e0bedf02e01e9553a172314", - "sha256:91fc98adde3d7881af9b59ed0294046f3806221863722ba7d8d120c575314325", - "sha256:94411f22c3985acaec6f83c6df553f2dbe17b698cc7f8ae751ff2237d96b9e3c", - "sha256:98d85c6a2bef81588d9227dde12db8a7f47f639f4a17c9ae08e773aa9c697bf3", - "sha256:9ad5db27f9cabae298d151c85cf2bad1d359a1b9c686a275df03385758e2f914", - "sha256:a0b71b1b8fbf2b96e41c4d990244165e2c9be83d54962a9a1d118fd8657d2045", - "sha256:a0f100c8912c114ff53e1202d0078b425bee3649ae34d7b070e9697f93c5d52d", - "sha256:a591fe9e525846e4d154205572a029f653ada1a78b93697f3b5a8f1f2bc055b9", - "sha256:a5c84c68147988265e60416b57fc83425a78058853509c1b0629c180094904a5", - "sha256:a66d3508133af6e8548451b25058d5812812ec3798c886bf38ed24a98216fab2", - "sha256:a8c4917bd7ad33e8eb21e9a5bbba979b49d9a97acb3a803092cbc1133e20343c", - "sha256:b3bbeb01c2b273cca1e1e0c5df57f12dce9a4dd331b4fa1635b8bec26350bde3", - "sha256:cba9d6b9a7d64d4bd46167096fc9d2f835e25d7e4c121fb2ddfc6528fb0413b2", - "sha256:cc4d65aeeaa04136a12677d3dd0b1c0c94dc43abac5860ab33cceb42b801c1e8", - "sha256:ce4bcc037df4fc5e3d184794f27bdaab018943698f4ca31630bc7f84a7b69c6d", - "sha256:cec7d9412a9102bdc577382c3929b337320c4c4c4849f2c5cdd14d7368c5562d", - "sha256:d400bfb9a37b1351253cb402671cea7e89bdecc294e8016a707f6d1d8ac934f9", - "sha256:d61f4695e6c866a23a21acab0509af1cdfd2c013cf256bbf5b6b5e2695827162", - "sha256:db0fbb9c62743ce59a9ff687eb5f4afbe77e5e8403d6697f7446e5f609976f76", - "sha256:dd86c085fae2efd48ac91dd7ccffcfc0571387fe1193d33b6394db7ef31fe2a4", - "sha256:e00b098126fd45523dd056d2efba6c5a63b71ffe9f2bbe1a4fe1716e1d0c331e", - "sha256:e229a521186c75c8ad9490854fd8bbdd9a0c9aa3a524326b55be83b54d4e0ad9", - "sha256:e263d77ee3dd201c3a142934a086a4450861778baaeeb45db4591ef65550b0a6", - "sha256:ed9cb427ba5504c1dc15ede7d516b84757c3e3d7868ccc85121d9310d27eed0b", - "sha256:fa6693661a4c91757f4412306191b6dc88c1703f780c8234035eac011922bc01", - "sha256:fcd131dd944808b5bdb38e6f5b53013c5aa4f334c5cad0c72742f6eba4b73db0" + "sha256:0c9ef6ff37e974b73c25eecc13952c55bceed9112be2d9d938ded8e856138bcc", + "sha256:131fd094d1065b19540c3d72594260f118b231090295d8c34e19a7bbcf2e860a", + "sha256:1b8ebc27c014c59692bb2664c7d13ce7a6e9a629be20e54e7271fa696ff2b417", + "sha256:2c56b361916f390cd758a57f2e16233eb4f64bcbeee88a4881ea90fca14dc6ab", + "sha256:2d92b25dbf6cae33f65005baf472d2c245c050b1ce709cc4588cdcdd5495b520", + "sha256:31d13b0f99e0836b7ff893d37af07366ebc90b678b6664c955b54561fc36ef36", + "sha256:32c68ef735dbe5857c810328cb2481e24722a59a2003018885514d4c09af9743", + "sha256:3686dffb02459559c74dd3d81748269ffb0eb027c39a6fc99502de37d501faa8", + "sha256:582215a0e9adbe0e379761260553ba11c58943e4bbe9c36430c4ca6ac74b15ed", + "sha256:5b50bf3f55561dac5438f8e70bfcdfd74543fd60df5fa5f62d94e5867deca684", + "sha256:5bf44d66cdf9e893637896c7faa22298baebcd18d1ddb6d2626a6e39793a1d56", + "sha256:6602bc8dc6f3a9e02b6c22c4fc1e47aa50f8f8e6d3f78a5e16ac33ef5fefa324", + "sha256:673739cb539f8cdaa07d92d02efa93c9ccf87e345b9a0b556e3ecc666718468d", + "sha256:68678abf380b42ce21a5f2abde8efee05c114c2fdb2e9eef2efdb0257fba1235", + "sha256:68e7c44931cc171c54ccb702482e9fc723192e88d25a0e133edd7aff8fcd1f6e", + "sha256:6b3d6606d369fc1da4fd8c357d026317fbb9c9b75d36dc16e90e84c26854b088", + "sha256:748dcd1e3d3d7cd5443ef03ce8685043294ad6bd7c02a38d1bd367cfd968e000", + "sha256:7651c50c8c5ef7bdb41108b7b8c5a83013bfaa8a935590c5d74627c047a583c7", + "sha256:7b78010e7b97fef4bee1e896df8a4bbb6712b7f05b7ef630f9d1da00f6444d2e", + "sha256:7e61e3e4fa664a8588aa25c883eab612a188c725755afff6289454d6362b9673", + "sha256:80876338e19c951fdfed6198e70bc88f1c9758b94578d5a7c4c91a87af3cf31c", + "sha256:8895613bcc094d4a1b2dbe179d88d7fb4a15cee43c052e8885783fac397d91fe", + "sha256:88e2b3c14bdb32e440be531ade29d3c50a1a59cd4e51b1dd8b0865c54ea5d2e2", + "sha256:8f8e709127c6c77446a8c0a8c8bf3c8ee706a06cd44b1e827c3e6a2ee6b8c098", + "sha256:9cb4a35b3642fc5c005a6755a5d17c6c8b6bcb6981baf81cea8bfbc8903e8ba8", + "sha256:9f90389693731ff1f659e55c7d1640e2ec43ff725cc61b04b2f9c6d8d017df6a", + "sha256:a09582f178759ee8128d9270cd1344154fd473bb77d94ce0aeb2a93ebf0feaf0", + "sha256:a6a14b17d7e17fa0d207ac08642c8820f84f25ce17a442fd15e27ea18d67c59b", + "sha256:a72e8961a86d19bdb45851d8f1f08b041ea37d2bd8d4fd19903bc3083d80c896", + "sha256:abd808f9c129ba2beda4cfc53bde801e5bcf9d6e0f22f095e45327c038bfe68e", + "sha256:ac0f5edd2360eea2f1daa9e26a41db02dd4b0451b48f7c318e217ee092a213e9", + "sha256:b29ebffcf550f9da55bec9e02ad430c992a87e5f512cd63388abb76f1036d8d2", + "sha256:b2ca4e77f9f47c55c194982e10f058db063937845bb2b7a86c84a6cfe0aefa8b", + "sha256:b7be2d771cdba2942e13215c4e340bfd76398e9227ad10402a8767ab1865d2e6", + "sha256:b84834d0cf97e7d27dd5b7f3aca7b6e9263c56308ab9dc8aae9784abb774d404", + "sha256:b86851a328eedc692acf81fb05444bdf1891747c25af7529e39ddafaf68a4f3f", + "sha256:bcb3ef43e58665bbda2fb198698fcae6776483e0c4a631aa5647806c25e02cc0", + "sha256:c0f31130ebc2d37cdd8e44605fb5fa7ad59049298b3f745c74fa74c62fbfcfc4", + "sha256:c6a164aa47843fb1b01e941d385aab7215563bb8816d80ff3a363a9f8448a8dc", + "sha256:d8a9d3ebe49f084ad71f9269834ceccbf398253c9fac910c4fd7053ff1386936", + "sha256:db8e577c19c0fda0beb7e0d4e09e0ba74b1e4c092e0e40bfa12fe05b6f6d75ba", + "sha256:dc9b18bf40cc75f66f40a7379f6a9513244fe33c0e8aa72e2d56b0196a7ef872", + "sha256:e09f3ff613345df5e8c3667da1d918f9149bd623cd9070c983c013792a9a62eb", + "sha256:e4108df7fe9b707191e55f33efbcb2d81928e10cea45527879a4749cbe472614", + "sha256:e6024675e67af929088fda399b2094574609396b1decb609c55fa58b028a32a1", + "sha256:e70f54f1796669ef691ca07d046cd81a29cb4deb1e5f942003f401c0c4a2695d", + "sha256:e715596e683d2ce000574bae5d07bd522c781a822866c20495e52520564f0969", + "sha256:e760191dd42581e023a68b758769e2da259b5d52e3103c6060ddc02c9edb8d7b", + "sha256:ed86a35631f7bfbb28e108dd96773b9d5a6ce4811cf6ea468bb6a359b256b1e4", + "sha256:ee07e47c12890ef248766a6e55bd38ebfb2bb8edd4142d56db91b21ea68b7627", + "sha256:fa3a0128b152627161ce47201262d3140edb5a5c3da88d73a1b790a959126956", + "sha256:fcc8eb6d5902bb1cf6dc4f187ee3ea80a1eba0a89aba40a5cb20a5087d961357" ], - "version": "==1.15.1" + "markers": "python_version >= '3.8'", + "version": "==1.16.0" }, "charset-normalizer": { "hashes": [ - "sha256:04afa6387e2b282cf78ff3dbce20f0cc071c12dc8f685bd40960cc68644cfea6", - "sha256:04eefcee095f58eaabe6dc3cc2262f3bcd776d2c67005880894f447b3f2cb9c1", - "sha256:0be65ccf618c1e7ac9b849c315cc2e8a8751d9cfdaa43027d4f6624bd587ab7e", - "sha256:0c95f12b74681e9ae127728f7e5409cbbef9cd914d5896ef238cc779b8152373", - "sha256:0ca564606d2caafb0abe6d1b5311c2649e8071eb241b2d64e75a0d0065107e62", - "sha256:10c93628d7497c81686e8e5e557aafa78f230cd9e77dd0c40032ef90c18f2230", - "sha256:11d117e6c63e8f495412d37e7dc2e2fff09c34b2d09dbe2bee3c6229577818be", - "sha256:11d3bcb7be35e7b1bba2c23beedac81ee893ac9871d0ba79effc7fc01167db6c", - "sha256:12a2b561af122e3d94cdb97fe6fb2bb2b82cef0cdca131646fdb940a1eda04f0", - "sha256:12d1a39aa6b8c6f6248bb54550efcc1c38ce0d8096a146638fd4738e42284448", - "sha256:1435ae15108b1cb6fffbcea2af3d468683b7afed0169ad718451f8db5d1aff6f", - "sha256:1c60b9c202d00052183c9be85e5eaf18a4ada0a47d188a83c8f5c5b23252f649", - "sha256:1e8fcdd8f672a1c4fc8d0bd3a2b576b152d2a349782d1eb0f6b8e52e9954731d", - "sha256:20064ead0717cf9a73a6d1e779b23d149b53daf971169289ed2ed43a71e8d3b0", - "sha256:21fa558996782fc226b529fdd2ed7866c2c6ec91cee82735c98a197fae39f706", - "sha256:22908891a380d50738e1f978667536f6c6b526a2064156203d418f4856d6e86a", - "sha256:3160a0fd9754aab7d47f95a6b63ab355388d890163eb03b2d2b87ab0a30cfa59", - "sha256:322102cdf1ab682ecc7d9b1c5eed4ec59657a65e1c146a0da342b78f4112db23", - "sha256:34e0a2f9c370eb95597aae63bf85eb5e96826d81e3dcf88b8886012906f509b5", - "sha256:3573d376454d956553c356df45bb824262c397c6e26ce43e8203c4c540ee0acb", - "sha256:3747443b6a904001473370d7810aa19c3a180ccd52a7157aacc264a5ac79265e", - "sha256:38e812a197bf8e71a59fe55b757a84c1f946d0ac114acafaafaf21667a7e169e", - "sha256:3a06f32c9634a8705f4ca9946d667609f52cf130d5548881401f1eb2c39b1e2c", - "sha256:3a5fc78f9e3f501a1614a98f7c54d3969f3ad9bba8ba3d9b438c3bc5d047dd28", - "sha256:3d9098b479e78c85080c98e1e35ff40b4a31d8953102bb0fd7d1b6f8a2111a3d", - "sha256:3dc5b6a8ecfdc5748a7e429782598e4f17ef378e3e272eeb1340ea57c9109f41", - "sha256:4155b51ae05ed47199dc5b2a4e62abccb274cee6b01da5b895099b61b1982974", - "sha256:49919f8400b5e49e961f320c735388ee686a62327e773fa5b3ce6721f7e785ce", - "sha256:53d0a3fa5f8af98a1e261de6a3943ca631c526635eb5817a87a59d9a57ebf48f", - "sha256:5f008525e02908b20e04707a4f704cd286d94718f48bb33edddc7d7b584dddc1", - "sha256:628c985afb2c7d27a4800bfb609e03985aaecb42f955049957814e0491d4006d", - "sha256:65ed923f84a6844de5fd29726b888e58c62820e0769b76565480e1fdc3d062f8", - "sha256:6734e606355834f13445b6adc38b53c0fd45f1a56a9ba06c2058f86893ae8017", - "sha256:6baf0baf0d5d265fa7944feb9f7451cc316bfe30e8df1a61b1bb08577c554f31", - "sha256:6f4f4668e1831850ebcc2fd0b1cd11721947b6dc7c00bf1c6bd3c929ae14f2c7", - "sha256:6f5c2e7bc8a4bf7c426599765b1bd33217ec84023033672c1e9a8b35eaeaaaf8", - "sha256:6f6c7a8a57e9405cad7485f4c9d3172ae486cfef1344b5ddd8e5239582d7355e", - "sha256:7381c66e0561c5757ffe616af869b916c8b4e42b367ab29fedc98481d1e74e14", - "sha256:73dc03a6a7e30b7edc5b01b601e53e7fc924b04e1835e8e407c12c037e81adbd", - "sha256:74db0052d985cf37fa111828d0dd230776ac99c740e1a758ad99094be4f1803d", - "sha256:75f2568b4189dda1c567339b48cba4ac7384accb9c2a7ed655cd86b04055c795", - "sha256:78cacd03e79d009d95635e7d6ff12c21eb89b894c354bd2b2ed0b4763373693b", - "sha256:80d1543d58bd3d6c271b66abf454d437a438dff01c3e62fdbcd68f2a11310d4b", - "sha256:830d2948a5ec37c386d3170c483063798d7879037492540f10a475e3fd6f244b", - "sha256:891cf9b48776b5c61c700b55a598621fdb7b1e301a550365571e9624f270c203", - "sha256:8f25e17ab3039b05f762b0a55ae0b3632b2e073d9c8fc88e89aca31a6198e88f", - "sha256:9a3267620866c9d17b959a84dd0bd2d45719b817245e49371ead79ed4f710d19", - "sha256:a04f86f41a8916fe45ac5024ec477f41f886b3c435da2d4e3d2709b22ab02af1", - "sha256:aaf53a6cebad0eae578f062c7d462155eada9c172bd8c4d250b8c1d8eb7f916a", - "sha256:abc1185d79f47c0a7aaf7e2412a0eb2c03b724581139193d2d82b3ad8cbb00ac", - "sha256:ac0aa6cd53ab9a31d397f8303f92c42f534693528fafbdb997c82bae6e477ad9", - "sha256:ac3775e3311661d4adace3697a52ac0bab17edd166087d493b52d4f4f553f9f0", - "sha256:b06f0d3bf045158d2fb8837c5785fe9ff9b8c93358be64461a1089f5da983137", - "sha256:b116502087ce8a6b7a5f1814568ccbd0e9f6cfd99948aa59b0e241dc57cf739f", - "sha256:b82fab78e0b1329e183a65260581de4375f619167478dddab510c6c6fb04d9b6", - "sha256:bd7163182133c0c7701b25e604cf1611c0d87712e56e88e7ee5d72deab3e76b5", - "sha256:c36bcbc0d5174a80d6cccf43a0ecaca44e81d25be4b7f90f0ed7bcfbb5a00909", - "sha256:c3af8e0f07399d3176b179f2e2634c3ce9c1301379a6b8c9c9aeecd481da494f", - "sha256:c84132a54c750fda57729d1e2599bb598f5fa0344085dbde5003ba429a4798c0", - "sha256:cb7b2ab0188829593b9de646545175547a70d9a6e2b63bf2cd87a0a391599324", - "sha256:cca4def576f47a09a943666b8f829606bcb17e2bc2d5911a46c8f8da45f56755", - "sha256:cf6511efa4801b9b38dc5546d7547d5b5c6ef4b081c60b23e4d941d0eba9cbeb", - "sha256:d16fd5252f883eb074ca55cb622bc0bee49b979ae4e8639fff6ca3ff44f9f854", - "sha256:d2686f91611f9e17f4548dbf050e75b079bbc2a82be565832bc8ea9047b61c8c", - "sha256:d7fc3fca01da18fbabe4625d64bb612b533533ed10045a2ac3dd194bfa656b60", - "sha256:dd5653e67b149503c68c4018bf07e42eeed6b4e956b24c00ccdf93ac79cdff84", - "sha256:de5695a6f1d8340b12a5d6d4484290ee74d61e467c39ff03b39e30df62cf83a0", - "sha256:e0ac8959c929593fee38da1c2b64ee9778733cdf03c482c9ff1d508b6b593b2b", - "sha256:e1b25e3ad6c909f398df8921780d6a3d120d8c09466720226fc621605b6f92b1", - "sha256:e633940f28c1e913615fd624fcdd72fdba807bf53ea6925d6a588e84e1151531", - "sha256:e89df2958e5159b811af9ff0f92614dabf4ff617c03a4c1c6ff53bf1c399e0e1", - "sha256:ea9f9c6034ea2d93d9147818f17c2a0860d41b71c38b9ce4d55f21b6f9165a11", - "sha256:f645caaf0008bacf349875a974220f1f1da349c5dbe7c4ec93048cdc785a3326", - "sha256:f8303414c7b03f794347ad062c0516cee0e15f7a612abd0ce1e25caf6ceb47df", - "sha256:fca62a8301b605b954ad2e9c3666f9d97f63872aa4efcae5492baca2056b74ab" + "sha256:02673e456dc5ab13659f85196c534dc596d4ef260e4d86e856c3b2773ce09843", + "sha256:02af06682e3590ab952599fbadac535ede5d60d78848e555aa58d0c0abbde786", + "sha256:03680bb39035fbcffe828eae9c3f8afc0428c91d38e7d61aa992ef7a59fb120e", + "sha256:0570d21da019941634a531444364f2482e8db0b3425fcd5ac0c36565a64142c8", + "sha256:09c77f964f351a7369cc343911e0df63e762e42bac24cd7d18525961c81754f4", + "sha256:0d3d5b7db9ed8a2b11a774db2bbea7ba1884430a205dbd54a32d61d7c2a190fa", + "sha256:1063da2c85b95f2d1a430f1c33b55c9c17ffaf5e612e10aeaad641c55a9e2b9d", + "sha256:12ebea541c44fdc88ccb794a13fe861cc5e35d64ed689513a5c03d05b53b7c82", + "sha256:153e7b6e724761741e0974fc4dcd406d35ba70b92bfe3fedcb497226c93b9da7", + "sha256:15b26ddf78d57f1d143bdf32e820fd8935d36abe8a25eb9ec0b5a71c82eb3895", + "sha256:1872d01ac8c618a8da634e232f24793883d6e456a66593135aeafe3784b0848d", + "sha256:187d18082694a29005ba2944c882344b6748d5be69e3a89bf3cc9d878e548d5a", + "sha256:1b2919306936ac6efb3aed1fbf81039f7087ddadb3160882a57ee2ff74fd2382", + "sha256:232ac332403e37e4a03d209a3f92ed9071f7d3dbda70e2a5e9cff1c4ba9f0678", + "sha256:23e8565ab7ff33218530bc817922fae827420f143479b753104ab801145b1d5b", + "sha256:24817cb02cbef7cd499f7c9a2735286b4782bd47a5b3516a0e84c50eab44b98e", + "sha256:249c6470a2b60935bafd1d1d13cd613f8cd8388d53461c67397ee6a0f5dce741", + "sha256:24a91a981f185721542a0b7c92e9054b7ab4fea0508a795846bc5b0abf8118d4", + "sha256:2502dd2a736c879c0f0d3e2161e74d9907231e25d35794584b1ca5284e43f596", + "sha256:250c9eb0f4600361dd80d46112213dff2286231d92d3e52af1e5a6083d10cad9", + "sha256:278c296c6f96fa686d74eb449ea1697f3c03dc28b75f873b65b5201806346a69", + "sha256:2935ffc78db9645cb2086c2f8f4cfd23d9b73cc0dc80334bc30aac6f03f68f8c", + "sha256:2f4a0033ce9a76e391542c182f0d48d084855b5fcba5010f707c8e8c34663d77", + "sha256:30a85aed0b864ac88309b7d94be09f6046c834ef60762a8833b660139cfbad13", + "sha256:380c4bde80bce25c6e4f77b19386f5ec9db230df9f2f2ac1e5ad7af2caa70459", + "sha256:3ae38d325b512f63f8da31f826e6cb6c367336f95e418137286ba362925c877e", + "sha256:3b447982ad46348c02cb90d230b75ac34e9886273df3a93eec0539308a6296d7", + "sha256:3debd1150027933210c2fc321527c2299118aa929c2f5a0a80ab6953e3bd1908", + "sha256:4162918ef3098851fcd8a628bf9b6a98d10c380725df9e04caf5ca6dd48c847a", + "sha256:468d2a840567b13a590e67dd276c570f8de00ed767ecc611994c301d0f8c014f", + "sha256:4cc152c5dd831641e995764f9f0b6589519f6f5123258ccaca8c6d34572fefa8", + "sha256:542da1178c1c6af8873e143910e2269add130a299c9106eef2594e15dae5e482", + "sha256:557b21a44ceac6c6b9773bc65aa1b4cc3e248a5ad2f5b914b91579a32e22204d", + "sha256:5707a746c6083a3a74b46b3a631d78d129edab06195a92a8ece755aac25a3f3d", + "sha256:588245972aca710b5b68802c8cad9edaa98589b1b42ad2b53accd6910dad3545", + "sha256:5adf257bd58c1b8632046bbe43ee38c04e1038e9d37de9c57a94d6bd6ce5da34", + "sha256:619d1c96099be5823db34fe89e2582b336b5b074a7f47f819d6b3a57ff7bdb86", + "sha256:63563193aec44bce707e0c5ca64ff69fa72ed7cf34ce6e11d5127555756fd2f6", + "sha256:67b8cc9574bb518ec76dc8e705d4c39ae78bb96237cb533edac149352c1f39fe", + "sha256:6a685067d05e46641d5d1623d7c7fdf15a357546cbb2f71b0ebde91b175ffc3e", + "sha256:70f1d09c0d7748b73290b29219e854b3207aea922f839437870d8cc2168e31cc", + "sha256:750b446b2ffce1739e8578576092179160f6d26bd5e23eb1789c4d64d5af7dc7", + "sha256:7966951325782121e67c81299a031f4c115615e68046f79b85856b86ebffc4cd", + "sha256:7b8b8bf1189b3ba9b8de5c8db4d541b406611a71a955bbbd7385bbc45fcb786c", + "sha256:7f5d10bae5d78e4551b7be7a9b29643a95aded9d0f602aa2ba584f0388e7a557", + "sha256:805dfea4ca10411a5296bcc75638017215a93ffb584c9e344731eef0dcfb026a", + "sha256:81bf654678e575403736b85ba3a7867e31c2c30a69bc57fe88e3ace52fb17b89", + "sha256:82eb849f085624f6a607538ee7b83a6d8126df6d2f7d3b319cb837b289123078", + "sha256:85a32721ddde63c9df9ebb0d2045b9691d9750cb139c161c80e500d210f5e26e", + "sha256:86d1f65ac145e2c9ed71d8ffb1905e9bba3a91ae29ba55b4c46ae6fc31d7c0d4", + "sha256:86f63face3a527284f7bb8a9d4f78988e3c06823f7bea2bd6f0e0e9298ca0403", + "sha256:8eaf82f0eccd1505cf39a45a6bd0a8cf1c70dcfc30dba338207a969d91b965c0", + "sha256:93aa7eef6ee71c629b51ef873991d6911b906d7312c6e8e99790c0f33c576f89", + "sha256:96c2b49eb6a72c0e4991d62406e365d87067ca14c1a729a870d22354e6f68115", + "sha256:9cf3126b85822c4e53aa28c7ec9869b924d6fcfb76e77a45c44b83d91afd74f9", + "sha256:9fe359b2e3a7729010060fbca442ca225280c16e923b37db0e955ac2a2b72a05", + "sha256:a0ac5e7015a5920cfce654c06618ec40c33e12801711da6b4258af59a8eff00a", + "sha256:a3f93dab657839dfa61025056606600a11d0b696d79386f974e459a3fbc568ec", + "sha256:a4b71f4d1765639372a3b32d2638197f5cd5221b19531f9245fcc9ee62d38f56", + "sha256:aae32c93e0f64469f74ccc730a7cb21c7610af3a775157e50bbd38f816536b38", + "sha256:aaf7b34c5bc56b38c931a54f7952f1ff0ae77a2e82496583b247f7c969eb1479", + "sha256:abecce40dfebbfa6abf8e324e1860092eeca6f7375c8c4e655a8afb61af58f2c", + "sha256:abf0d9f45ea5fb95051c8bfe43cb40cda383772f7e5023a83cc481ca2604d74e", + "sha256:ac71b2977fb90c35d41c9453116e283fac47bb9096ad917b8819ca8b943abecd", + "sha256:ada214c6fa40f8d800e575de6b91a40d0548139e5dc457d2ebb61470abf50186", + "sha256:b09719a17a2301178fac4470d54b1680b18a5048b481cb8890e1ef820cb80455", + "sha256:b1121de0e9d6e6ca08289583d7491e7fcb18a439305b34a30b20d8215922d43c", + "sha256:b3b2316b25644b23b54a6f6401074cebcecd1244c0b8e80111c9a3f1c8e83d65", + "sha256:b3d9b48ee6e3967b7901c052b670c7dda6deb812c309439adaffdec55c6d7b78", + "sha256:b5bcf60a228acae568e9911f410f9d9e0d43197d030ae5799e20dca8df588287", + "sha256:b8f3307af845803fb0b060ab76cf6dd3a13adc15b6b451f54281d25911eb92df", + "sha256:c2af80fb58f0f24b3f3adcb9148e6203fa67dd3f61c4af146ecad033024dde43", + "sha256:c350354efb159b8767a6244c166f66e67506e06c8924ed74669b2c70bc8735b1", + "sha256:c5a74c359b2d47d26cdbbc7845e9662d6b08a1e915eb015d044729e92e7050b7", + "sha256:c71f16da1ed8949774ef79f4a0260d28b83b3a50c6576f8f4f0288d109777989", + "sha256:d47ecf253780c90ee181d4d871cd655a789da937454045b17b5798da9393901a", + "sha256:d7eff0f27edc5afa9e405f7165f85a6d782d308f3b6b9d96016c010597958e63", + "sha256:d97d85fa63f315a8bdaba2af9a6a686e0eceab77b3089af45133252618e70884", + "sha256:db756e48f9c5c607b5e33dd36b1d5872d0422e960145b08ab0ec7fd420e9d649", + "sha256:dc45229747b67ffc441b3de2f3ae5e62877a282ea828a5bdb67883c4ee4a8810", + "sha256:e0fc42822278451bc13a2e8626cf2218ba570f27856b536e00cfa53099724828", + "sha256:e39c7eb31e3f5b1f88caff88bcff1b7f8334975b46f6ac6e9fc725d829bc35d4", + "sha256:e46cd37076971c1040fc8c41273a8b3e2c624ce4f2be3f5dfcb7a430c1d3acc2", + "sha256:e5c1502d4ace69a179305abb3f0bb6141cbe4714bc9b31d427329a95acfc8bdd", + "sha256:edfe077ab09442d4ef3c52cb1f9dab89bff02f4524afc0acf2d46be17dc479f5", + "sha256:effe5406c9bd748a871dbcaf3ac69167c38d72db8c9baf3ff954c344f31c4cbe", + "sha256:f0d1e3732768fecb052d90d62b220af62ead5748ac51ef61e7b32c266cac9293", + "sha256:f5969baeaea61c97efa706b9b107dcba02784b1601c74ac84f2a532ea079403e", + "sha256:f8888e31e3a85943743f8fc15e71536bda1c81d5aa36d014a3c0c44481d7db6e", + "sha256:fc52b79d83a3fe3a360902d3f5d79073a993597d48114c29485e9431092905d8" ], "markers": "python_full_version >= '3.7.0'", - "version": "==3.1.0" + "version": "==3.3.0" }, "cryptography": { "hashes": [ - "sha256:059e348f9a3c1950937e1b5d7ba1f8e968508ab181e75fc32b879452f08356db", - "sha256:1a5472d40c8f8e91ff7a3d8ac6dfa363d8e3138b961529c996f3e2df0c7a411a", - "sha256:1a8e6c2de6fbbcc5e14fd27fb24414507cb3333198ea9ab1258d916f00bc3039", - "sha256:1fee5aacc7367487b4e22484d3c7e547992ed726d14864ee33c0176ae43b0d7c", - "sha256:5d092fdfedaec4cbbffbf98cddc915ba145313a6fdaab83c6e67f4e6c218e6f3", - "sha256:5f0ff6e18d13a3de56f609dd1fd11470918f770c6bd5d00d632076c727d35485", - "sha256:7bfc55a5eae8b86a287747053140ba221afc65eb06207bedf6e019b8934b477c", - "sha256:7fa01527046ca5facdf973eef2535a27fec4cb651e4daec4d043ef63f6ecd4ca", - "sha256:8dde71c4169ec5ccc1087bb7521d54251c016f126f922ab2dfe6649170a3b8c5", - "sha256:8f4ab7021127a9b4323537300a2acfb450124b2def3756f64dc3a3d2160ee4b5", - "sha256:948224d76c4b6457349d47c0c98657557f429b4e93057cf5a2f71d603e2fc3a3", - "sha256:9a6c7a3c87d595608a39980ebaa04d5a37f94024c9f24eb7d10262b92f739ddb", - "sha256:b46e37db3cc267b4dea1f56da7346c9727e1209aa98487179ee8ebed09d21e43", - "sha256:b4ceb5324b998ce2003bc17d519080b4ec8d5b7b70794cbd2836101406a9be31", - "sha256:cb33ccf15e89f7ed89b235cff9d49e2e62c6c981a6061c9c8bb47ed7951190bc", - "sha256:d198820aba55660b4d74f7b5fd1f17db3aa5eb3e6893b0a41b75e84e4f9e0e4b", - "sha256:d34579085401d3f49762d2f7d6634d6b6c2ae1242202e860f4d26b046e3a1006", - "sha256:eb8163f5e549a22888c18b0d53d6bb62a20510060a22fd5a995ec8a05268df8a", - "sha256:f73bff05db2a3e5974a6fd248af2566134d8981fd7ab012e5dd4ddb1d9a70699" + "sha256:004b6ccc95943f6a9ad3142cfabcc769d7ee38a3f60fb0dddbfb431f818c3a67", + "sha256:047c4603aeb4bbd8db2756e38f5b8bd7e94318c047cfe4efeb5d715e08b49311", + "sha256:0d9409894f495d465fe6fda92cb70e8323e9648af912d5b9141d616df40a87b8", + "sha256:23a25c09dfd0d9f28da2352503b23e086f8e78096b9fd585d1d14eca01613e13", + "sha256:2ed09183922d66c4ec5fdaa59b4d14e105c084dd0febd27452de8f6f74704143", + "sha256:35c00f637cd0b9d5b6c6bd11b6c3359194a8eba9c46d4e875a3660e3b400005f", + "sha256:37480760ae08065437e6573d14be973112c9e6dcaf5f11d00147ee74f37a3829", + "sha256:3b224890962a2d7b57cf5eeb16ccaafba6083f7b811829f00476309bce2fe0fd", + "sha256:5a0f09cefded00e648a127048119f77bc2b2ec61e736660b5789e638f43cc397", + "sha256:5b72205a360f3b6176485a333256b9bcd48700fc755fef51c8e7e67c4b63e3ac", + "sha256:7e53db173370dea832190870e975a1e09c86a879b613948f09eb49324218c14d", + "sha256:7febc3094125fc126a7f6fb1f420d0da639f3f32cb15c8ff0dc3997c4549f51a", + "sha256:80907d3faa55dc5434a16579952ac6da800935cd98d14dbd62f6f042c7f5e839", + "sha256:86defa8d248c3fa029da68ce61fe735432b047e32179883bdb1e79ed9bb8195e", + "sha256:8ac4f9ead4bbd0bc8ab2d318f97d85147167a488be0e08814a37eb2f439d5cf6", + "sha256:93530900d14c37a46ce3d6c9e6fd35dbe5f5601bf6b3a5c325c7bffc030344d9", + "sha256:9eeb77214afae972a00dee47382d2591abe77bdae166bda672fb1e24702a3860", + "sha256:b5f4dfe950ff0479f1f00eda09c18798d4f49b98f4e2006d644b3301682ebdca", + "sha256:c3391bd8e6de35f6f1140e50aaeb3e2b3d6a9012536ca23ab0d9c35ec18c8a91", + "sha256:c880eba5175f4307129784eca96f4e70b88e57aa3f680aeba3bab0e980b0f37d", + "sha256:cecfefa17042941f94ab54f769c8ce0fe14beff2694e9ac684176a2535bf9714", + "sha256:e40211b4923ba5a6dc9769eab704bdb3fbb58d56c5b336d30996c24fcf12aadb", + "sha256:efc8ad4e6fc4f1752ebfb58aefece8b4e3c4cae940b0994d43649bdfce8d0d4f" ], "markers": "python_version >= '3.7'", - "version": "==41.0.1" + "version": "==41.0.4" }, "defusedxml": { "hashes": [ @@ -247,10 +265,10 @@ }, "dj-database-url": { "hashes": [ - "sha256:9c9e5f7224f62635a787e9cc3c6762c9be2b19541a21e3c08fa573bd01609b4b", - "sha256:a35a9f0f43775ca6f90d819dc456233ef7bcc76b47377d5d908b75c7eb320624" + "sha256:04bc34b248d4c21aaa13e4ab419ae6575ef5f10f3df735ce7da97722caa356e0", + "sha256:f2042cefe1086e539c9da39fad5ad7f61173bf79665e69bf7e4de55fa88b135f" ], - "version": "==2.0.0" + "version": "==2.1.0" }, "dj-email-url": { "hashes": [ @@ -261,19 +279,20 @@ }, "django": { "hashes": [ - "sha256:066b6debb5ac335458d2a713ed995570536c8b59a580005acb0732378d5eb1ee", - "sha256:7efa6b1f781a6119a10ac94b4794ded90db8accbe7802281cd26f8664ffed59c" + "sha256:08f41f468b63335aea0d904c5729e0250300f6a1907bf293a65499496cdbc68f", + "sha256:a64d2487cdb00ad7461434320ccc38e60af9c404773a2f95ab0093b4453a3215" ], "index": "pypi", - "version": "==4.2.1" + "markers": "python_version >= '3.8'", + "version": "==4.2.6" }, "django-allow-cidr": { "hashes": [ - "sha256:24b71f70257e97bab9fdb5ad8342c96eeea1d45bc06a36332978574252219401", - "sha256:6709f4581dfd2a00476a134741a738a7f67714ec4f8596c55b22cf3b2ac5a12e" + "sha256:11126c5bb9df3a61ff9d97304856ba7e5b26d46c6d456709a6d9e28483bff47f", + "sha256:382c5d7a9807279e3e96e4f4892b59163a2b30128c596902bf5f80e133e1ccbb" ], "index": "pypi", - "version": "==0.6.0" + "version": "==0.7.1" }, "django-auditlog": { "hashes": [ @@ -281,6 +300,7 @@ "sha256:b9d3acebb64f3f2785157efe3f2f802e0929aafc579d85bbfb9827db4adab532" ], "index": "pypi", + "markers": "python_version >= '3.7'", "version": "==2.3.0" }, "django-cache-url": { @@ -318,19 +338,20 @@ "phonenumberslite" ], "hashes": [ - "sha256:4eaab35fe9a163046dc3a47188771385c56a788e0e11b7bbcc662e1e6b7b9104", - "sha256:63721dbdc7424cd594a08d80f550e790cf6e7c903cbc0fb4dd9d86baac8b8c51" + "sha256:16778f2717ea2aecc6178beb0d6bc431c78c6a8b0474e1fa8face040efeb6e9e", + "sha256:20c7c5c449e33eed5fd45ef8d3dc668faabaeff3277eddd1892b262d686ba381" ], - "index": "pypi", - "version": "==7.1.0" + "markers": "python_version >= '3.8'", + "version": "==7.2.0" }, "django-widget-tweaks": { "hashes": [ - "sha256:9bfc5c705684754a83cc81da328b39ad1b80f32bd0f4340e2a810cbab4b0c00e", - "sha256:fe6b17d5d595c63331f300917980db2afcf71f240ab9341b954aea8f45d25b9a" + "sha256:1c2180681ebb994e922c754804c7ffebbe1245014777ac47897a81f57cc629c7", + "sha256:a41b7b2f05bd44d673d11ebd6c09a96f1d013ee98121cb98c384fe84e33b881e" ], "index": "pypi", - "version": "==1.4.12" + "markers": "python_version >= '3.8'", + "version": "==1.5.0" }, "environs": { "extras": [ @@ -340,16 +361,16 @@ "sha256:1e549569a3de49c05f856f40bce86979e7d5ffbbc4398e7f338574c220189124", "sha256:a76307b36fbe856bdca7ee9161e6c466fd7fcffc297109a118c59b54e27e30c9" ], - "index": "pypi", + "markers": "python_version >= '3.6'", "version": "==9.5.0" }, "faker": { "hashes": [ - "sha256:a70de9ec7a14a02d278755a11134baa5a297bb82600f115022d0d07080a9e77a", - "sha256:dd15fa165ced55f668fbb0ad20ece98ab78ddacd58dc056950d66980ff61fa79" + "sha256:85468e16d4a9a8712bfdb98ba55aaf17c60658266a76958d099aee6a18c0a6c5", + "sha256:d75401c631a991b32d3595f26250f42c007cc32653ac3e522b626f3d80770571" ], - "index": "pypi", - "version": "==18.10.0" + "markers": "python_version >= '3.8'", + "version": "==19.9.0" }, "fred-epplib": { "git": "https://github.com/cisagov/epplib.git", @@ -369,13 +390,135 @@ "markers": "python_version >= '2.6' and python_version not in '3.0, 3.1, 3.2, 3.3'", "version": "==0.18.3" }, - "gunicorn": { + "gevent": { "hashes": [ - "sha256:9dcc4547dbb1cb284accfb15ab5667a0e5d1881cc443e0677b4882a4067a807e", - "sha256:e0a968b5ba15f8a328fdfd7ab1fcb5af4470c28aaf7e55df02a99bc13138e6e8" + "sha256:272cffdf535978d59c38ed837916dfd2b5d193be1e9e5dcc60a5f4d5025dd98a", + "sha256:2c7b5c9912378e5f5ccf180d1fdb1e83f42b71823483066eddbe10ef1a2fcaa2", + "sha256:36a549d632c14684bcbbd3014a6ce2666c5f2a500f34d58d32df6c9ea38b6535", + "sha256:4368f341a5f51611411ec3fc62426f52ac3d6d42eaee9ed0f9eebe715c80184e", + "sha256:43daf68496c03a35287b8b617f9f91e0e7c0d042aebcc060cadc3f049aadd653", + "sha256:455e5ee8103f722b503fa45dedb04f3ffdec978c1524647f8ba72b4f08490af1", + "sha256:45792c45d60f6ce3d19651d7fde0bc13e01b56bb4db60d3f32ab7d9ec467374c", + "sha256:4e24c2af9638d6c989caffc691a039d7c7022a31c0363da367c0d32ceb4a0648", + "sha256:52b4abf28e837f1865a9bdeef58ff6afd07d1d888b70b6804557e7908032e599", + "sha256:52e9f12cd1cda96603ce6b113d934f1aafb873e2c13182cf8e86d2c5c41982ea", + "sha256:5f3c781c84794926d853d6fb58554dc0dcc800ba25c41d42f6959c344b4db5a6", + "sha256:62d121344f7465e3739989ad6b91f53a6ca9110518231553fe5846dbe1b4518f", + "sha256:65883ac026731ac112184680d1f0f1e39fa6f4389fd1fc0bf46cc1388e2599f9", + "sha256:707904027d7130ff3e59ea387dddceedb133cc742b00b3ffe696d567147a9c9e", + "sha256:72c002235390d46f94938a96920d8856d4ffd9ddf62a303a0d7c118894097e34", + "sha256:7532c17bc6c1cbac265e751b95000961715adef35a25d2b0b1813aa7263fb397", + "sha256:78eebaf5e73ff91d34df48f4e35581ab4c84e22dd5338ef32714264063c57507", + "sha256:7c1abc6f25f475adc33e5fc2dbcc26a732608ac5375d0d306228738a9ae14d3b", + "sha256:7c28e38dcde327c217fdafb9d5d17d3e772f636f35df15ffae2d933a5587addd", + "sha256:7ccf0fd378257cb77d91c116e15c99e533374a8153632c48a3ecae7f7f4f09fe", + "sha256:921dda1c0b84e3d3b1778efa362d61ed29e2b215b90f81d498eb4d8eafcd0b7a", + "sha256:a2898b7048771917d85a1d548fd378e8a7b2ca963db8e17c6d90c76b495e0e2b", + "sha256:a3c5e9b1f766a7a64833334a18539a362fb563f6c4682f9634dea72cbe24f771", + "sha256:ada07076b380918829250201df1d016bdafb3acf352f35e5693b59dceee8dd2e", + "sha256:b101086f109168b23fa3586fccd1133494bdb97f86920a24dc0b23984dc30b69", + "sha256:bf456bd6b992eb0e1e869e2fd0caf817f0253e55ca7977fd0e72d0336a8c1c6a", + "sha256:bf7af500da05363e66f122896012acb6e101a552682f2352b618e541c941a011", + "sha256:c3e5d2fa532e4d3450595244de8ccf51f5721a05088813c1abd93ad274fe15e7", + "sha256:c84d34256c243b0a53d4335ef0bc76c735873986d478c53073861a92566a8d71", + "sha256:d163d59f1be5a4c4efcdd13c2177baaf24aadf721fdf2e1af9ee54a998d160f5", + "sha256:d57737860bfc332b9b5aa438963986afe90f49645f6e053140cfa0fa1bdae1ae", + "sha256:dbb22a9bbd6a13e925815ce70b940d1578dbe5d4013f20d23e8a11eddf8d14a7", + "sha256:dcb8612787a7f4626aa881ff15ff25439561a429f5b303048f0fca8a1c781c39", + "sha256:dd6c32ab977ecf7c7b8c2611ed95fa4aaebd69b74bf08f4b4960ad516861517d", + "sha256:de350fde10efa87ea60d742901e1053eb2127ebd8b59a7d3b90597eb4e586599", + "sha256:e1ead6863e596a8cc2a03e26a7a0981f84b6b3e956101135ff6d02df4d9a6b07", + "sha256:ed7a048d3e526a5c1d55c44cb3bc06cfdc1947d06d45006cc4cf60dedc628904", + "sha256:f632487c87866094546a74eefbca2c74c1d03638b715b6feb12e80120960185a", + "sha256:fae8d5b5b8fa2a8f63b39f5447168b02db10c888a3e387ed7af2bd1b8612e543", + "sha256:fde6402c5432b835fbb7698f1c7f2809c8d6b2bd9d047ac1f5a7c1d5aa569303" + ], + "markers": "python_version >= '3.8'", + "version": "==23.9.1" + }, + "greenlet": { + "hashes": [ + "sha256:02a807b2a58d5cdebb07050efe3d7deaf915468d112dfcf5e426d0564aa3aa4a", + "sha256:0b72b802496cccbd9b31acea72b6f87e7771ccfd7f7927437d592e5c92ed703c", + "sha256:0d3f83ffb18dc57243e0151331e3c383b05e5b6c5029ac29f754745c800f8ed9", + "sha256:10b5582744abd9858947d163843d323d0b67be9432db50f8bf83031032bc218d", + "sha256:123910c58234a8d40eaab595bc56a5ae49bdd90122dde5bdc012c20595a94c14", + "sha256:1482fba7fbed96ea7842b5a7fc11d61727e8be75a077e603e8ab49d24e234383", + "sha256:19834e3f91f485442adc1ee440171ec5d9a4840a1f7bd5ed97833544719ce10b", + "sha256:1d363666acc21d2c204dd8705c0e0457d7b2ee7a76cb16ffc099d6799744ac99", + "sha256:211ef8d174601b80e01436f4e6905aca341b15a566f35a10dd8d1e93f5dbb3b7", + "sha256:269d06fa0f9624455ce08ae0179430eea61085e3cf6457f05982b37fd2cefe17", + "sha256:2e7dcdfad252f2ca83c685b0fa9fba00e4d8f243b73839229d56ee3d9d219314", + "sha256:334ef6ed8337bd0b58bb0ae4f7f2dcc84c9f116e474bb4ec250a8bb9bd797a66", + "sha256:343675e0da2f3c69d3fb1e894ba0a1acf58f481f3b9372ce1eb465ef93cf6fed", + "sha256:37f60b3a42d8b5499be910d1267b24355c495064f271cfe74bf28b17b099133c", + "sha256:38ad562a104cd41e9d4644f46ea37167b93190c6d5e4048fcc4b80d34ecb278f", + "sha256:3c0d36f5adc6e6100aedbc976d7428a9f7194ea79911aa4bf471f44ee13a9464", + "sha256:3fd2b18432e7298fcbec3d39e1a0aa91ae9ea1c93356ec089421fabc3651572b", + "sha256:4a1a6244ff96343e9994e37e5b4839f09a0207d35ef6134dce5c20d260d0302c", + "sha256:4cd83fb8d8e17633ad534d9ac93719ef8937568d730ef07ac3a98cb520fd93e4", + "sha256:527cd90ba3d8d7ae7dceb06fda619895768a46a1b4e423bdb24c1969823b8362", + "sha256:56867a3b3cf26dc8a0beecdb4459c59f4c47cdd5424618c08515f682e1d46692", + "sha256:621fcb346141ae08cb95424ebfc5b014361621b8132c48e538e34c3c93ac7365", + "sha256:63acdc34c9cde42a6534518e32ce55c30f932b473c62c235a466469a710bfbf9", + "sha256:6512592cc49b2c6d9b19fbaa0312124cd4c4c8a90d28473f86f92685cc5fef8e", + "sha256:6672fdde0fd1a60b44fb1751a7779c6db487e42b0cc65e7caa6aa686874e79fb", + "sha256:6a5b2d4cdaf1c71057ff823a19d850ed5c6c2d3686cb71f73ae4d6382aaa7a06", + "sha256:6a68d670c8f89ff65c82b936275369e532772eebc027c3be68c6b87ad05ca695", + "sha256:6bb36985f606a7c49916eff74ab99399cdfd09241c375d5a820bb855dfb4af9f", + "sha256:73b2f1922a39d5d59cc0e597987300df3396b148a9bd10b76a058a2f2772fc04", + "sha256:7709fd7bb02b31908dc8fd35bfd0a29fc24681d5cc9ac1d64ad07f8d2b7db62f", + "sha256:8060b32d8586e912a7b7dac2d15b28dbbd63a174ab32f5bc6d107a1c4143f40b", + "sha256:80dcd3c938cbcac986c5c92779db8e8ce51a89a849c135172c88ecbdc8c056b7", + "sha256:813720bd57e193391dfe26f4871186cf460848b83df7e23e6bef698a7624b4c9", + "sha256:831d6f35037cf18ca5e80a737a27d822d87cd922521d18ed3dbc8a6967be50ce", + "sha256:871b0a8835f9e9d461b7fdaa1b57e3492dd45398e87324c047469ce2fc9f516c", + "sha256:952256c2bc5b4ee8df8dfc54fc4de330970bf5d79253c863fb5e6761f00dda35", + "sha256:96d9ea57292f636ec851a9bb961a5cc0f9976900e16e5d5647f19aa36ba6366b", + "sha256:9a812224a5fb17a538207e8cf8e86f517df2080c8ee0f8c1ed2bdaccd18f38f4", + "sha256:9adbd8ecf097e34ada8efde9b6fec4dd2a903b1e98037adf72d12993a1c80b51", + "sha256:9de687479faec7db5b198cc365bc34addd256b0028956501f4d4d5e9ca2e240a", + "sha256:a048293392d4e058298710a54dfaefcefdf49d287cd33fb1f7d63d55426e4355", + "sha256:aa15a2ec737cb609ed48902b45c5e4ff6044feb5dcdfcf6fa8482379190330d7", + "sha256:abe1ef3d780de56defd0c77c5ba95e152f4e4c4e12d7e11dd8447d338b85a625", + "sha256:ad6fb737e46b8bd63156b8f59ba6cdef46fe2b7db0c5804388a2d0519b8ddb99", + "sha256:b1660a15a446206c8545edc292ab5c48b91ff732f91b3d3b30d9a915d5ec4779", + "sha256:b505fcfc26f4148551826a96f7317e02c400665fa0883fe505d4fcaab1dabfdd", + "sha256:b822fab253ac0f330ee807e7485769e3ac85d5eef827ca224feaaefa462dc0d0", + "sha256:bdd696947cd695924aecb3870660b7545a19851f93b9d327ef8236bfc49be705", + "sha256:bdfaeecf8cc705d35d8e6de324bf58427d7eafb55f67050d8f28053a3d57118c", + "sha256:be557119bf467d37a8099d91fbf11b2de5eb1fd5fc5b91598407574848dc910f", + "sha256:c6b5ce7f40f0e2f8b88c28e6691ca6806814157ff05e794cdd161be928550f4c", + "sha256:c94e4e924d09b5a3e37b853fe5924a95eac058cb6f6fb437ebb588b7eda79870", + "sha256:cc3e2679ea13b4de79bdc44b25a0c4fcd5e94e21b8f290791744ac42d34a0353", + "sha256:d1e22c22f7826096ad503e9bb681b05b8c1f5a8138469b255eb91f26a76634f2", + "sha256:d5539f6da3418c3dc002739cb2bb8d169056aa66e0c83f6bacae0cd3ac26b423", + "sha256:d55db1db455c59b46f794346efce896e754b8942817f46a1bada2d29446e305a", + "sha256:e09dea87cc91aea5500262993cbd484b41edf8af74f976719dd83fe724644cd6", + "sha256:e52a712c38e5fb4fd68e00dc3caf00b60cb65634d50e32281a9d6431b33b4af1", + "sha256:e693e759e172fa1c2c90d35dea4acbdd1d609b6936115d3739148d5e4cd11947", + "sha256:ecf94aa539e97a8411b5ea52fc6ccd8371be9550c4041011a091eb8b3ca1d810", + "sha256:f351479a6914fd81a55c8e68963609f792d9b067fb8a60a042c585a621e0de4f", + "sha256:f47932c434a3c8d3c86d865443fadc1fbf574e9b11d6650b656e602b1797908a" + ], + "markers": "python_version < '3.11' and platform_python_implementation == 'CPython'", + "version": "==3.0.0" + }, + "gsocketpool": { + "hashes": [ + "sha256:f2e2749aceadce6b27ca52e2b0a64af99797746a8681e1a2963f72007c14cb14" ], "index": "pypi", - "version": "==20.1.0" + "version": "==0.1.6" + }, + "gunicorn": { + "hashes": [ + "sha256:3213aa5e8c24949e792bcacfc176fef362e7aac80b76c56f6b5122bf350722f0", + "sha256:88ec8bff1d634f98e61b9f65bc4bf3cd918a90806c6f5c48bc5603849ec81033" + ], + "index": "pypi", + "markers": "python_version >= '3.5'", + "version": "==21.2.0" }, "idna": { "hashes": [ @@ -395,86 +538,101 @@ }, "lxml": { "hashes": [ - "sha256:01d36c05f4afb8f7c20fd9ed5badca32a2029b93b1750f571ccc0b142531caf7", - "sha256:04876580c050a8c5341d706dd464ff04fd597095cc8c023252566a8826505726", - "sha256:05ca3f6abf5cf78fe053da9b1166e062ade3fa5d4f92b4ed688127ea7d7b1d03", - "sha256:090c6543d3696cbe15b4ac6e175e576bcc3f1ccfbba970061b7300b0c15a2140", - "sha256:0dc313ef231edf866912e9d8f5a042ddab56c752619e92dfd3a2c277e6a7299a", - "sha256:0f2b1e0d79180f344ff9f321327b005ca043a50ece8713de61d1cb383fb8ac05", - "sha256:13598ecfbd2e86ea7ae45ec28a2a54fb87ee9b9fdb0f6d343297d8e548392c03", - "sha256:16efd54337136e8cd72fb9485c368d91d77a47ee2d42b057564aae201257d419", - "sha256:1ab8f1f932e8f82355e75dda5413a57612c6ea448069d4fb2e217e9a4bed13d4", - "sha256:223f4232855ade399bd409331e6ca70fb5578efef22cf4069a6090acc0f53c0e", - "sha256:2455cfaeb7ac70338b3257f41e21f0724f4b5b0c0e7702da67ee6c3640835b67", - "sha256:2899456259589aa38bfb018c364d6ae7b53c5c22d8e27d0ec7609c2a1ff78b50", - "sha256:2a29ba94d065945944016b6b74e538bdb1751a1db6ffb80c9d3c2e40d6fa9894", - "sha256:2a87fa548561d2f4643c99cd13131acb607ddabb70682dcf1dff5f71f781a4bf", - "sha256:2e430cd2824f05f2d4f687701144556646bae8f249fd60aa1e4c768ba7018947", - "sha256:36c3c175d34652a35475a73762b545f4527aec044910a651d2bf50de9c3352b1", - "sha256:3818b8e2c4b5148567e1b09ce739006acfaa44ce3156f8cbbc11062994b8e8dd", - "sha256:3ab9fa9d6dc2a7f29d7affdf3edebf6ece6fb28a6d80b14c3b2fb9d39b9322c3", - "sha256:3efea981d956a6f7173b4659849f55081867cf897e719f57383698af6f618a92", - "sha256:4c8f293f14abc8fd3e8e01c5bd86e6ed0b6ef71936ded5bf10fe7a5efefbaca3", - "sha256:5344a43228767f53a9df6e5b253f8cdca7dfc7b7aeae52551958192f56d98457", - "sha256:58bfa3aa19ca4c0f28c5dde0ff56c520fbac6f0daf4fac66ed4c8d2fb7f22e74", - "sha256:5b4545b8a40478183ac06c073e81a5ce4cf01bf1734962577cf2bb569a5b3bbf", - "sha256:5f50a1c177e2fa3ee0667a5ab79fdc6b23086bc8b589d90b93b4bd17eb0e64d1", - "sha256:63da2ccc0857c311d764e7d3d90f429c252e83b52d1f8f1d1fe55be26827d1f4", - "sha256:6749649eecd6a9871cae297bffa4ee76f90b4504a2a2ab528d9ebe912b101975", - "sha256:6804daeb7ef69e7b36f76caddb85cccd63d0c56dedb47555d2fc969e2af6a1a5", - "sha256:689bb688a1db722485e4610a503e3e9210dcc20c520b45ac8f7533c837be76fe", - "sha256:699a9af7dffaf67deeae27b2112aa06b41c370d5e7633e0ee0aea2e0b6c211f7", - "sha256:6b418afe5df18233fc6b6093deb82a32895b6bb0b1155c2cdb05203f583053f1", - "sha256:76cf573e5a365e790396a5cc2b909812633409306c6531a6877c59061e42c4f2", - "sha256:7b515674acfdcadb0eb5d00d8a709868173acece5cb0be3dd165950cbfdf5409", - "sha256:7b770ed79542ed52c519119473898198761d78beb24b107acf3ad65deae61f1f", - "sha256:7d2278d59425777cfcb19735018d897ca8303abe67cc735f9f97177ceff8027f", - "sha256:7e91ee82f4199af8c43d8158024cbdff3d931df350252288f0d4ce656df7f3b5", - "sha256:821b7f59b99551c69c85a6039c65b75f5683bdc63270fec660f75da67469ca24", - "sha256:822068f85e12a6e292803e112ab876bc03ed1f03dddb80154c395f891ca6b31e", - "sha256:8340225bd5e7a701c0fa98284c849c9b9fc9238abf53a0ebd90900f25d39a4e4", - "sha256:85cabf64adec449132e55616e7ca3e1000ab449d1d0f9d7f83146ed5bdcb6d8a", - "sha256:880bbbcbe2fca64e2f4d8e04db47bcdf504936fa2b33933efd945e1b429bea8c", - "sha256:8d0b4612b66ff5d62d03bcaa043bb018f74dfea51184e53f067e6fdcba4bd8de", - "sha256:8e20cb5a47247e383cf4ff523205060991021233ebd6f924bca927fcf25cf86f", - "sha256:925073b2fe14ab9b87e73f9a5fde6ce6392da430f3004d8b72cc86f746f5163b", - "sha256:998c7c41910666d2976928c38ea96a70d1aa43be6fe502f21a651e17483a43c5", - "sha256:9b22c5c66f67ae00c0199f6055705bc3eb3fcb08d03d2ec4059a2b1b25ed48d7", - "sha256:9f102706d0ca011de571de32c3247c6476b55bb6bc65a20f682f000b07a4852a", - "sha256:a08cff61517ee26cb56f1e949cca38caabe9ea9fbb4b1e10a805dc39844b7d5c", - "sha256:a0a336d6d3e8b234a3aae3c674873d8f0e720b76bc1d9416866c41cd9500ffb9", - "sha256:a35f8b7fa99f90dd2f5dc5a9fa12332642f087a7641289ca6c40d6e1a2637d8e", - "sha256:a38486985ca49cfa574a507e7a2215c0c780fd1778bb6290c21193b7211702ab", - "sha256:a5da296eb617d18e497bcf0a5c528f5d3b18dadb3619fbdadf4ed2356ef8d941", - "sha256:a6e441a86553c310258aca15d1c05903aaf4965b23f3bc2d55f200804e005ee5", - "sha256:a82d05da00a58b8e4c0008edbc8a4b6ec5a4bc1e2ee0fb6ed157cf634ed7fa45", - "sha256:ab323679b8b3030000f2be63e22cdeea5b47ee0abd2d6a1dc0c8103ddaa56cd7", - "sha256:b1f42b6921d0e81b1bcb5e395bc091a70f41c4d4e55ba99c6da2b31626c44892", - "sha256:b23e19989c355ca854276178a0463951a653309fb8e57ce674497f2d9f208746", - "sha256:b264171e3143d842ded311b7dccd46ff9ef34247129ff5bf5066123c55c2431c", - "sha256:b26a29f0b7fc6f0897f043ca366142d2b609dc60756ee6e4e90b5f762c6adc53", - "sha256:b64d891da92e232c36976c80ed7ebb383e3f148489796d8d31a5b6a677825efe", - "sha256:b9cc34af337a97d470040f99ba4282f6e6bac88407d021688a5d585e44a23184", - "sha256:bc718cd47b765e790eecb74d044cc8d37d58562f6c314ee9484df26276d36a38", - "sha256:be7292c55101e22f2a3d4d8913944cbea71eea90792bf914add27454a13905df", - "sha256:c83203addf554215463b59f6399835201999b5e48019dc17f182ed5ad87205c9", - "sha256:c9ec3eaf616d67db0764b3bb983962b4f385a1f08304fd30c7283954e6a7869b", - "sha256:ca34efc80a29351897e18888c71c6aca4a359247c87e0b1c7ada14f0ab0c0fb2", - "sha256:ca989b91cf3a3ba28930a9fc1e9aeafc2a395448641df1f387a2d394638943b0", - "sha256:d02a5399126a53492415d4906ab0ad0375a5456cc05c3fc0fc4ca11771745cda", - "sha256:d17bc7c2ccf49c478c5bdd447594e82692c74222698cfc9b5daae7ae7e90743b", - "sha256:d5bf6545cd27aaa8a13033ce56354ed9e25ab0e4ac3b5392b763d8d04b08e0c5", - "sha256:d6b430a9938a5a5d85fc107d852262ddcd48602c120e3dbb02137c83d212b380", - "sha256:da248f93f0418a9e9d94b0080d7ebc407a9a5e6d0b57bb30db9b5cc28de1ad33", - "sha256:da4dd7c9c50c059aba52b3524f84d7de956f7fef88f0bafcf4ad7dde94a064e8", - "sha256:df0623dcf9668ad0445e0558a21211d4e9a149ea8f5666917c8eeec515f0a6d1", - "sha256:e5168986b90a8d1f2f9dc1b841467c74221bd752537b99761a93d2d981e04889", - "sha256:efa29c2fe6b4fdd32e8ef81c1528506895eca86e1d8c4657fda04c9b3786ddf9", - "sha256:f1496ea22ca2c830cbcbd473de8f114a320da308438ae65abad6bab7867fe38f", - "sha256:f49e52d174375a7def9915c9f06ec4e569d235ad428f70751765f48d5926678c" + "sha256:05186a0f1346ae12553d66df1cfce6f251589fea3ad3da4f3ef4e34b2d58c6a3", + "sha256:075b731ddd9e7f68ad24c635374211376aa05a281673ede86cbe1d1b3455279d", + "sha256:081d32421db5df44c41b7f08a334a090a545c54ba977e47fd7cc2deece78809a", + "sha256:0a3d3487f07c1d7f150894c238299934a2a074ef590b583103a45002035be120", + "sha256:0bfd0767c5c1de2551a120673b72e5d4b628737cb05414f03c3277bf9bed3305", + "sha256:0c0850c8b02c298d3c7006b23e98249515ac57430e16a166873fc47a5d549287", + "sha256:0e2cb47860da1f7e9a5256254b74ae331687b9672dfa780eed355c4c9c3dbd23", + "sha256:120fa9349a24c7043854c53cae8cec227e1f79195a7493e09e0c12e29f918e52", + "sha256:1247694b26342a7bf47c02e513d32225ededd18045264d40758abeb3c838a51f", + "sha256:141f1d1a9b663c679dc524af3ea1773e618907e96075262726c7612c02b149a4", + "sha256:14e019fd83b831b2e61baed40cab76222139926b1fb5ed0e79225bc0cae14584", + "sha256:1509dd12b773c02acd154582088820893109f6ca27ef7291b003d0e81666109f", + "sha256:17a753023436a18e27dd7769e798ce302963c236bc4114ceee5b25c18c52c693", + "sha256:1e224d5755dba2f4a9498e150c43792392ac9b5380aa1b845f98a1618c94eeef", + "sha256:1f447ea5429b54f9582d4b955f5f1985f278ce5cf169f72eea8afd9502973dd5", + "sha256:23eed6d7b1a3336ad92d8e39d4bfe09073c31bfe502f20ca5116b2a334f8ec02", + "sha256:25f32acefac14ef7bd53e4218fe93b804ef6f6b92ffdb4322bb6d49d94cad2bc", + "sha256:2c74524e179f2ad6d2a4f7caf70e2d96639c0954c943ad601a9e146c76408ed7", + "sha256:303bf1edce6ced16bf67a18a1cf8339d0db79577eec5d9a6d4a80f0fb10aa2da", + "sha256:3331bece23c9ee066e0fb3f96c61322b9e0f54d775fccefff4c38ca488de283a", + "sha256:3e9bdd30efde2b9ccfa9cb5768ba04fe71b018a25ea093379c857c9dad262c40", + "sha256:411007c0d88188d9f621b11d252cce90c4a2d1a49db6c068e3c16422f306eab8", + "sha256:42871176e7896d5d45138f6d28751053c711ed4d48d8e30b498da155af39aebd", + "sha256:46f409a2d60f634fe550f7133ed30ad5321ae2e6630f13657fb9479506b00601", + "sha256:48628bd53a426c9eb9bc066a923acaa0878d1e86129fd5359aee99285f4eed9c", + "sha256:48d6ed886b343d11493129e019da91d4039826794a3e3027321c56d9e71505be", + "sha256:4930be26af26ac545c3dffb662521d4e6268352866956672231887d18f0eaab2", + "sha256:4aec80cde9197340bc353d2768e2a75f5f60bacda2bab72ab1dc499589b3878c", + "sha256:4c28a9144688aef80d6ea666c809b4b0e50010a2aca784c97f5e6bf143d9f129", + "sha256:4d2d1edbca80b510443f51afd8496be95529db04a509bc8faee49c7b0fb6d2cc", + "sha256:4dd9a263e845a72eacb60d12401e37c616438ea2e5442885f65082c276dfb2b2", + "sha256:4f1026bc732b6a7f96369f7bfe1a4f2290fb34dce00d8644bc3036fb351a4ca1", + "sha256:4fb960a632a49f2f089d522f70496640fdf1218f1243889da3822e0a9f5f3ba7", + "sha256:50670615eaf97227d5dc60de2dc99fb134a7130d310d783314e7724bf163f75d", + "sha256:50baa9c1c47efcaef189f31e3d00d697c6d4afda5c3cde0302d063492ff9b477", + "sha256:53ace1c1fd5a74ef662f844a0413446c0629d151055340e9893da958a374f70d", + "sha256:5515edd2a6d1a5a70bfcdee23b42ec33425e405c5b351478ab7dc9347228f96e", + "sha256:56dc1f1ebccc656d1b3ed288f11e27172a01503fc016bcabdcbc0978b19352b7", + "sha256:578695735c5a3f51569810dfebd05dd6f888147a34f0f98d4bb27e92b76e05c2", + "sha256:57aba1bbdf450b726d58b2aea5fe47c7875f5afb2c4a23784ed78f19a0462574", + "sha256:57d6ba0ca2b0c462f339640d22882acc711de224d769edf29962b09f77129cbf", + "sha256:5c245b783db29c4e4fbbbfc9c5a78be496c9fea25517f90606aa1f6b2b3d5f7b", + "sha256:5c31c7462abdf8f2ac0577d9f05279727e698f97ecbb02f17939ea99ae8daa98", + "sha256:64f479d719dc9f4c813ad9bb6b28f8390360660b73b2e4beb4cb0ae7104f1c12", + "sha256:65299ea57d82fb91c7f019300d24050c4ddeb7c5a190e076b5f48a2b43d19c42", + "sha256:6689a3d7fd13dc687e9102a27e98ef33730ac4fe37795d5036d18b4d527abd35", + "sha256:690dafd0b187ed38583a648076865d8c229661ed20e48f2335d68e2cf7dc829d", + "sha256:6fc3c450eaa0b56f815c7b62f2b7fba7266c4779adcf1cece9e6deb1de7305ce", + "sha256:704f61ba8c1283c71b16135caf697557f5ecf3e74d9e453233e4771d68a1f42d", + "sha256:71c52db65e4b56b8ddc5bb89fb2e66c558ed9d1a74a45ceb7dcb20c191c3df2f", + "sha256:71d66ee82e7417828af6ecd7db817913cb0cf9d4e61aa0ac1fde0583d84358db", + "sha256:7d298a1bd60c067ea75d9f684f5f3992c9d6766fadbc0bcedd39750bf344c2f4", + "sha256:8b77946fd508cbf0fccd8e400a7f71d4ac0e1595812e66025bac475a8e811694", + "sha256:8d7e43bd40f65f7d97ad8ef5c9b1778943d02f04febef12def25f7583d19baac", + "sha256:8df133a2ea5e74eef5e8fc6f19b9e085f758768a16e9877a60aec455ed2609b2", + "sha256:8ed74706b26ad100433da4b9d807eae371efaa266ffc3e9191ea436087a9d6a7", + "sha256:92af161ecbdb2883c4593d5ed4815ea71b31fafd7fd05789b23100d081ecac96", + "sha256:97047f0d25cd4bcae81f9ec9dc290ca3e15927c192df17331b53bebe0e3ff96d", + "sha256:9719fe17307a9e814580af1f5c6e05ca593b12fb7e44fe62450a5384dbf61b4b", + "sha256:9767e79108424fb6c3edf8f81e6730666a50feb01a328f4a016464a5893f835a", + "sha256:9a92d3faef50658dd2c5470af249985782bf754c4e18e15afb67d3ab06233f13", + "sha256:9bb6ad405121241e99a86efff22d3ef469024ce22875a7ae045896ad23ba2340", + "sha256:9e28c51fa0ce5674be9f560c6761c1b441631901993f76700b1b30ca6c8378d6", + "sha256:aca086dc5f9ef98c512bac8efea4483eb84abbf926eaeedf7b91479feb092458", + "sha256:ae8b9c6deb1e634ba4f1930eb67ef6e6bf6a44b6eb5ad605642b2d6d5ed9ce3c", + "sha256:b0a545b46b526d418eb91754565ba5b63b1c0b12f9bd2f808c852d9b4b2f9b5c", + "sha256:b4e4bc18382088514ebde9328da057775055940a1f2e18f6ad2d78aa0f3ec5b9", + "sha256:b6420a005548ad52154c8ceab4a1290ff78d757f9e5cbc68f8c77089acd3c432", + "sha256:b86164d2cff4d3aaa1f04a14685cbc072efd0b4f99ca5708b2ad1b9b5988a991", + "sha256:bb3bb49c7a6ad9d981d734ef7c7193bc349ac338776a0360cc671eaee89bcf69", + "sha256:bef4e656f7d98aaa3486d2627e7d2df1157d7e88e7efd43a65aa5dd4714916cf", + "sha256:c0781a98ff5e6586926293e59480b64ddd46282953203c76ae15dbbbf302e8bb", + "sha256:c2006f5c8d28dee289f7020f721354362fa304acbaaf9745751ac4006650254b", + "sha256:c41bfca0bd3532d53d16fd34d20806d5c2b1ace22a2f2e4c0008570bf2c58833", + "sha256:cd47b4a0d41d2afa3e58e5bf1f62069255aa2fd6ff5ee41604418ca925911d76", + "sha256:cdb650fc86227eba20de1a29d4b2c1bfe139dc75a0669270033cb2ea3d391b85", + "sha256:cef2502e7e8a96fe5ad686d60b49e1ab03e438bd9123987994528febd569868e", + "sha256:d27be7405547d1f958b60837dc4c1007da90b8b23f54ba1f8b728c78fdb19d50", + "sha256:d37017287a7adb6ab77e1c5bee9bcf9660f90ff445042b790402a654d2ad81d8", + "sha256:d3ff32724f98fbbbfa9f49d82852b159e9784d6094983d9a8b7f2ddaebb063d4", + "sha256:d73d8ecf8ecf10a3bd007f2192725a34bd62898e8da27eb9d32a58084f93962b", + "sha256:dd708cf4ee4408cf46a48b108fb9427bfa00b9b85812a9262b5c668af2533ea5", + "sha256:e3cd95e10c2610c360154afdc2f1480aea394f4a4f1ea0a5eacce49640c9b190", + "sha256:e4da8ca0c0c0aea88fd46be8e44bd49716772358d648cce45fe387f7b92374a7", + "sha256:eadfbbbfb41b44034a4c757fd5d70baccd43296fb894dba0295606a7cf3124aa", + "sha256:ed667f49b11360951e201453fc3967344d0d0263aa415e1619e85ae7fd17b4e0", + "sha256:f3df3db1d336b9356dd3112eae5f5c2b8b377f3bc826848567f10bfddfee77e9", + "sha256:f6bdac493b949141b733c5345b6ba8f87a226029cbabc7e9e121a413e49441e0", + "sha256:fbf521479bcac1e25a663df882c46a641a9bff6b56dc8b0fafaebd2f66fb231b", + "sha256:fc9b106a1bf918db68619fdcd6d5ad4f972fdd19c01d19bdb6bf63f3589a9ec5", + "sha256:fcdd00edfd0a3001e0181eab3e63bd5c74ad3e67152c84f93f13769a40e073a7", + "sha256:fe4bda6bd4340caa6e5cf95e73f8fea5c4bfc55763dd42f1b50a94c1b4a2fbd4" ], "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3, 3.4'", - "version": "==4.9.2" + "version": "==4.9.3" }, "mako": { "hashes": [ @@ -486,75 +644,86 @@ }, "markupsafe": { "hashes": [ - "sha256:0576fe974b40a400449768941d5d0858cc624e3249dfd1e0c33674e5c7ca7aed", - "sha256:085fd3201e7b12809f9e6e9bc1e5c96a368c8523fad5afb02afe3c051ae4afcc", - "sha256:090376d812fb6ac5f171e5938e82e7f2d7adc2b629101cec0db8b267815c85e2", - "sha256:0b462104ba25f1ac006fdab8b6a01ebbfbce9ed37fd37fd4acd70c67c973e460", - "sha256:137678c63c977754abe9086a3ec011e8fd985ab90631145dfb9294ad09c102a7", - "sha256:1bea30e9bf331f3fef67e0a3877b2288593c98a21ccb2cf29b74c581a4eb3af0", - "sha256:22152d00bf4a9c7c83960521fc558f55a1adbc0631fbb00a9471e097b19d72e1", - "sha256:22731d79ed2eb25059ae3df1dfc9cb1546691cc41f4e3130fe6bfbc3ecbbecfa", - "sha256:2298c859cfc5463f1b64bd55cb3e602528db6fa0f3cfd568d3605c50678f8f03", - "sha256:28057e985dace2f478e042eaa15606c7efccb700797660629da387eb289b9323", - "sha256:2e7821bffe00aa6bd07a23913b7f4e01328c3d5cc0b40b36c0bd81d362faeb65", - "sha256:2ec4f2d48ae59bbb9d1f9d7efb9236ab81429a764dedca114f5fdabbc3788013", - "sha256:340bea174e9761308703ae988e982005aedf427de816d1afe98147668cc03036", - "sha256:40627dcf047dadb22cd25ea7ecfe9cbf3bbbad0482ee5920b582f3809c97654f", - "sha256:40dfd3fefbef579ee058f139733ac336312663c6706d1163b82b3003fb1925c4", - "sha256:4cf06cdc1dda95223e9d2d3c58d3b178aa5dacb35ee7e3bbac10e4e1faacb419", - "sha256:50c42830a633fa0cf9e7d27664637532791bfc31c731a87b202d2d8ac40c3ea2", - "sha256:55f44b440d491028addb3b88f72207d71eeebfb7b5dbf0643f7c023ae1fba619", - "sha256:608e7073dfa9e38a85d38474c082d4281f4ce276ac0010224eaba11e929dd53a", - "sha256:63ba06c9941e46fa389d389644e2d8225e0e3e5ebcc4ff1ea8506dce646f8c8a", - "sha256:65608c35bfb8a76763f37036547f7adfd09270fbdbf96608be2bead319728fcd", - "sha256:665a36ae6f8f20a4676b53224e33d456a6f5a72657d9c83c2aa00765072f31f7", - "sha256:6d6607f98fcf17e534162f0709aaad3ab7a96032723d8ac8750ffe17ae5a0666", - "sha256:7313ce6a199651c4ed9d7e4cfb4aa56fe923b1adf9af3b420ee14e6d9a73df65", - "sha256:7668b52e102d0ed87cb082380a7e2e1e78737ddecdde129acadb0eccc5423859", - "sha256:7df70907e00c970c60b9ef2938d894a9381f38e6b9db73c5be35e59d92e06625", - "sha256:7e007132af78ea9df29495dbf7b5824cb71648d7133cf7848a2a5dd00d36f9ff", - "sha256:835fb5e38fd89328e9c81067fd642b3593c33e1e17e2fdbf77f5676abb14a156", - "sha256:8bca7e26c1dd751236cfb0c6c72d4ad61d986e9a41bbf76cb445f69488b2a2bd", - "sha256:8db032bf0ce9022a8e41a22598eefc802314e81b879ae093f36ce9ddf39ab1ba", - "sha256:99625a92da8229df6d44335e6fcc558a5037dd0a760e11d84be2260e6f37002f", - "sha256:9cad97ab29dfc3f0249b483412c85c8ef4766d96cdf9dcf5a1e3caa3f3661cf1", - "sha256:a4abaec6ca3ad8660690236d11bfe28dfd707778e2442b45addd2f086d6ef094", - "sha256:a6e40afa7f45939ca356f348c8e23048e02cb109ced1eb8420961b2f40fb373a", - "sha256:a6f2fcca746e8d5910e18782f976489939d54a91f9411c32051b4aab2bd7c513", - "sha256:a806db027852538d2ad7555b203300173dd1b77ba116de92da9afbc3a3be3eed", - "sha256:abcabc8c2b26036d62d4c746381a6f7cf60aafcc653198ad678306986b09450d", - "sha256:b8526c6d437855442cdd3d87eede9c425c4445ea011ca38d937db299382e6fa3", - "sha256:bb06feb762bade6bf3c8b844462274db0c76acc95c52abe8dbed28ae3d44a147", - "sha256:c0a33bc9f02c2b17c3ea382f91b4db0e6cde90b63b296422a939886a7a80de1c", - "sha256:c4a549890a45f57f1ebf99c067a4ad0cb423a05544accaf2b065246827ed9603", - "sha256:ca244fa73f50a800cf8c3ebf7fd93149ec37f5cb9596aa8873ae2c1d23498601", - "sha256:cf877ab4ed6e302ec1d04952ca358b381a882fbd9d1b07cccbfd61783561f98a", - "sha256:d9d971ec1e79906046aa3ca266de79eac42f1dbf3612a05dc9368125952bd1a1", - "sha256:da25303d91526aac3672ee6d49a2f3db2d9502a4a60b55519feb1a4c7714e07d", - "sha256:e55e40ff0cc8cc5c07996915ad367fa47da6b3fc091fdadca7f5403239c5fec3", - "sha256:f03a532d7dee1bed20bc4884194a16160a2de9ffc6354b3878ec9682bb623c54", - "sha256:f1cd098434e83e656abf198f103a8207a8187c0fc110306691a2e94a78d0abb2", - "sha256:f2bfb563d0211ce16b63c7cb9395d2c682a23187f54c3d79bfec33e6705473c6", - "sha256:f8ffb705ffcf5ddd0e80b65ddf7bed7ee4f5a441ea7d3419e861a12eaf41af58" + "sha256:05fb21170423db021895e1ea1e1f3ab3adb85d1c2333cbc2310f2a26bc77272e", + "sha256:0a4e4a1aff6c7ac4cd55792abf96c915634c2b97e3cc1c7129578aa68ebd754e", + "sha256:10bbfe99883db80bdbaff2dcf681dfc6533a614f700da1287707e8a5d78a8431", + "sha256:134da1eca9ec0ae528110ccc9e48041e0828d79f24121a1a146161103c76e686", + "sha256:14ff806850827afd6b07a5f32bd917fb7f45b046ba40c57abdb636674a8b559c", + "sha256:1577735524cdad32f9f694208aa75e422adba74f1baee7551620e43a3141f559", + "sha256:1b40069d487e7edb2676d3fbdb2b0829ffa2cd63a2ec26c4938b2d34391b4ecc", + "sha256:1b8dd8c3fd14349433c79fa8abeb573a55fc0fdd769133baac1f5e07abf54aeb", + "sha256:1f67c7038d560d92149c060157d623c542173016c4babc0c1913cca0564b9939", + "sha256:282c2cb35b5b673bbcadb33a585408104df04f14b2d9b01d4c345a3b92861c2c", + "sha256:2c1b19b3aaacc6e57b7e25710ff571c24d6c3613a45e905b1fde04d691b98ee0", + "sha256:2ef12179d3a291be237280175b542c07a36e7f60718296278d8593d21ca937d4", + "sha256:338ae27d6b8745585f87218a3f23f1512dbf52c26c28e322dbe54bcede54ccb9", + "sha256:3c0fae6c3be832a0a0473ac912810b2877c8cb9d76ca48de1ed31e1c68386575", + "sha256:3fd4abcb888d15a94f32b75d8fd18ee162ca0c064f35b11134be77050296d6ba", + "sha256:42de32b22b6b804f42c5d98be4f7e5e977ecdd9ee9b660fda1a3edf03b11792d", + "sha256:47d4f1c5f80fc62fdd7777d0d40a2e9dda0a05883ab11374334f6c4de38adffd", + "sha256:504b320cd4b7eff6f968eddf81127112db685e81f7e36e75f9f84f0df46041c3", + "sha256:525808b8019e36eb524b8c68acdd63a37e75714eac50e988180b169d64480a00", + "sha256:56d9f2ecac662ca1611d183feb03a3fa4406469dafe241673d521dd5ae92a155", + "sha256:5bbe06f8eeafd38e5d0a4894ffec89378b6c6a625ff57e3028921f8ff59318ac", + "sha256:65c1a9bcdadc6c28eecee2c119465aebff8f7a584dd719facdd9e825ec61ab52", + "sha256:68e78619a61ecf91e76aa3e6e8e33fc4894a2bebe93410754bd28fce0a8a4f9f", + "sha256:69c0f17e9f5a7afdf2cc9fb2d1ce6aabdb3bafb7f38017c0b77862bcec2bbad8", + "sha256:6b2b56950d93e41f33b4223ead100ea0fe11f8e6ee5f641eb753ce4b77a7042b", + "sha256:715d3562f79d540f251b99ebd6d8baa547118974341db04f5ad06d5ea3eb8007", + "sha256:787003c0ddb00500e49a10f2844fac87aa6ce977b90b0feaaf9de23c22508b24", + "sha256:7ef3cb2ebbf91e330e3bb937efada0edd9003683db6b57bb108c4001f37a02ea", + "sha256:8023faf4e01efadfa183e863fefde0046de576c6f14659e8782065bcece22198", + "sha256:8758846a7e80910096950b67071243da3e5a20ed2546e6392603c096778d48e0", + "sha256:8afafd99945ead6e075b973fefa56379c5b5c53fd8937dad92c662da5d8fd5ee", + "sha256:8c41976a29d078bb235fea9b2ecd3da465df42a562910f9022f1a03107bd02be", + "sha256:8e254ae696c88d98da6555f5ace2279cf7cd5b3f52be2b5cf97feafe883b58d2", + "sha256:8f9293864fe09b8149f0cc42ce56e3f0e54de883a9de90cd427f191c346eb2e1", + "sha256:9402b03f1a1b4dc4c19845e5c749e3ab82d5078d16a2a4c2cd2df62d57bb0707", + "sha256:962f82a3086483f5e5f64dbad880d31038b698494799b097bc59c2edf392fce6", + "sha256:9aad3c1755095ce347e26488214ef77e0485a3c34a50c5a5e2471dff60b9dd9c", + "sha256:9dcdfd0eaf283af041973bff14a2e143b8bd64e069f4c383416ecd79a81aab58", + "sha256:aa57bd9cf8ae831a362185ee444e15a93ecb2e344c8e52e4d721ea3ab6ef1823", + "sha256:aa7bd130efab1c280bed0f45501b7c8795f9fdbeb02e965371bbef3523627779", + "sha256:ab4a0df41e7c16a1392727727e7998a467472d0ad65f3ad5e6e765015df08636", + "sha256:ad9e82fb8f09ade1c3e1b996a6337afac2b8b9e365f926f5a61aacc71adc5b3c", + "sha256:af598ed32d6ae86f1b747b82783958b1a4ab8f617b06fe68795c7f026abbdcad", + "sha256:b076b6226fb84157e3f7c971a47ff3a679d837cf338547532ab866c57930dbee", + "sha256:b7ff0f54cb4ff66dd38bebd335a38e2c22c41a8ee45aa608efc890ac3e3931bc", + "sha256:bfce63a9e7834b12b87c64d6b155fdd9b3b96191b6bd334bf37db7ff1fe457f2", + "sha256:c011a4149cfbcf9f03994ec2edffcb8b1dc2d2aede7ca243746df97a5d41ce48", + "sha256:c9c804664ebe8f83a211cace637506669e7890fec1b4195b505c214e50dd4eb7", + "sha256:ca379055a47383d02a5400cb0d110cef0a776fc644cda797db0c5696cfd7e18e", + "sha256:cb0932dc158471523c9637e807d9bfb93e06a95cbf010f1a38b98623b929ef2b", + "sha256:cd0f502fe016460680cd20aaa5a76d241d6f35a1c3350c474bac1273803893fa", + "sha256:ceb01949af7121f9fc39f7d27f91be8546f3fb112c608bc4029aef0bab86a2a5", + "sha256:d080e0a5eb2529460b30190fcfcc4199bd7f827663f858a226a81bc27beaa97e", + "sha256:dd15ff04ffd7e05ffcb7fe79f1b98041b8ea30ae9234aed2a9168b5797c3effb", + "sha256:df0be2b576a7abbf737b1575f048c23fb1d769f267ec4358296f31c2479db8f9", + "sha256:e09031c87a1e51556fdcb46e5bd4f59dfb743061cf93c4d6831bf894f125eb57", + "sha256:e4dd52d80b8c83fdce44e12478ad2e85c64ea965e75d66dbeafb0a3e77308fcc", + "sha256:f698de3fd0c4e6972b92290a45bd9b1536bffe8c6759c62471efaa8acb4c37bc", + "sha256:fec21693218efe39aa7f8599346e90c705afa52c5b31ae019b2e57e8f6542bb2", + "sha256:ffcc3f7c66b5f5b7931a5aa68fc9cecc51e685ef90282f4a82f0f5e9b704ad11" ], "markers": "python_version >= '3.7'", - "version": "==2.1.2" + "version": "==2.1.3" }, "marshmallow": { "hashes": [ - "sha256:90032c0fd650ce94b6ec6dc8dfeb0e3ff50c144586462c389b81a07205bedb78", - "sha256:93f0958568da045b0021ec6aeb7ac37c81bfcccbb9a0e7ed8559885070b3a19b" + "sha256:5d2371bbe42000f2b3fb5eaa065224df7d8f8597bc19a1bbfa5bfe7fba8da889", + "sha256:684939db93e80ad3561392f47be0230743131560a41c5110684c16e21ade0a5c" ], - "markers": "python_version >= '3.7'", - "version": "==3.19.0" + "markers": "python_version >= '3.8'", + "version": "==3.20.1" }, "oic": { "hashes": [ - "sha256:2de3b83f1299dda8ed0460baad8bb2d4c6ac8bfc08a220c768b7e6e754caf9e7", - "sha256:f82e087e0ffaba2194ebd24694721a25167d5d467fb06bf4f4da9f48d43e0cc6" + "sha256:385a1f64bb59519df1e23840530921bf416740240f505ea6d161e331d3d39fad", + "sha256:fcbf948a22e4d4df66f6bf57d327933f32a7b539640d9b42883457634360ba78" ], "index": "pypi", - "version": "==1.6.0" + "markers": "python_version ~= '3.7'", + "version": "==1.6.1" }, "orderedmultidict": { "hashes": [ @@ -565,86 +734,94 @@ }, "packaging": { "hashes": [ - "sha256:994793af429502c4ea2ebf6bf664629d07c1a9fe974af92966e4b8d2df7edc61", - "sha256:a392980d2b6cffa644431898be54b0045151319d1e7ec34f0cfed48767dd334f" + "sha256:048fb0e9405036518eaaf48a55953c750c11e1a1b68e0dd1a9d62ed0c092cfc5", + "sha256:8c491190033a9af7e1d931d0b5dacc2ef47509b34dd0de67ed209b5203fc88c7" ], "markers": "python_version >= '3.7'", - "version": "==23.1" + "version": "==23.2" }, "phonenumberslite": { "hashes": [ - "sha256:670a3e1ea775a9fad89d843d2d18a0e2bb0c3719e1a6bb2b96fb12f7ddd579a0", - "sha256:d1d23707dbde2b8b6940f80b181638a3e10334ff2f122c7165ce43e91c6639fe" + "sha256:8d1e5f2adfee2a634ccdb54b251dec32c5308fbca3d7f6ae6058f4adee4594a3", + "sha256:98684f21804c6df2e7d224e72d60defee20eddf9e144d57f24cbd9db0df450e0" ], - "version": "==8.13.13" + "version": "==8.13.22" }, "psycopg2-binary": { "hashes": [ - "sha256:02c0f3757a4300cf379eb49f543fb7ac527fb00144d39246ee40e1df684ab514", - "sha256:02c6e3cf3439e213e4ee930308dc122d6fb4d4bea9aef4a12535fbd605d1a2fe", - "sha256:0645376d399bfd64da57148694d78e1f431b1e1ee1054872a5713125681cf1be", - "sha256:0892ef645c2fabb0c75ec32d79f4252542d0caec1d5d949630e7d242ca4681a3", - "sha256:0d236c2825fa656a2d98bbb0e52370a2e852e5a0ec45fc4f402977313329174d", - "sha256:0e0f754d27fddcfd74006455b6e04e6705d6c31a612ec69ddc040a5468e44b4e", - "sha256:15e2ee79e7cf29582ef770de7dab3d286431b01c3bb598f8e05e09601b890081", - "sha256:1876843d8e31c89c399e31b97d4b9725a3575bb9c2af92038464231ec40f9edb", - "sha256:1f64dcfb8f6e0c014c7f55e51c9759f024f70ea572fbdef123f85318c297947c", - "sha256:2ab652e729ff4ad76d400df2624d223d6e265ef81bb8aa17fbd63607878ecbee", - "sha256:30637a20623e2a2eacc420059be11527f4458ef54352d870b8181a4c3020ae6b", - "sha256:34b9ccdf210cbbb1303c7c4db2905fa0319391bd5904d32689e6dd5c963d2ea8", - "sha256:38601cbbfe600362c43714482f43b7c110b20cb0f8172422c616b09b85a750c5", - "sha256:441cc2f8869a4f0f4bb408475e5ae0ee1f3b55b33f350406150277f7f35384fc", - "sha256:498807b927ca2510baea1b05cc91d7da4718a0f53cb766c154c417a39f1820a0", - "sha256:4ac30da8b4f57187dbf449294d23b808f8f53cad6b1fc3623fa8a6c11d176dd0", - "sha256:4c727b597c6444a16e9119386b59388f8a424223302d0c06c676ec8b4bc1f963", - "sha256:4d67fbdaf177da06374473ef6f7ed8cc0a9dc640b01abfe9e8a2ccb1b1402c1f", - "sha256:4dfb4be774c4436a4526d0c554af0cc2e02082c38303852a36f6456ece7b3503", - "sha256:4ea29fc3ad9d91162c52b578f211ff1c931d8a38e1f58e684c45aa470adf19e2", - "sha256:51537e3d299be0db9137b321dfb6a5022caaab275775680e0c3d281feefaca6b", - "sha256:61b047a0537bbc3afae10f134dc6393823882eb263088c271331602b672e52e9", - "sha256:6460c7a99fc939b849431f1e73e013d54aa54293f30f1109019c56a0b2b2ec2f", - "sha256:65bee1e49fa6f9cf327ce0e01c4c10f39165ee76d35c846ade7cb0ec6683e303", - "sha256:65c07febd1936d63bfde78948b76cd4c2a411572a44ac50719ead41947d0f26b", - "sha256:71f14375d6f73b62800530b581aed3ada394039877818b2d5f7fc77e3bb6894d", - "sha256:7a40c00dbe17c0af5bdd55aafd6ff6679f94a9be9513a4c7e071baf3d7d22a70", - "sha256:7e13a5a2c01151f1208d5207e42f33ba86d561b7a89fca67c700b9486a06d0e2", - "sha256:7f0438fa20fb6c7e202863e0d5ab02c246d35efb1d164e052f2f3bfe2b152bd0", - "sha256:8122cfc7cae0da9a3077216528b8bb3629c43b25053284cc868744bfe71eb141", - "sha256:8338a271cb71d8da40b023a35d9c1e919eba6cbd8fa20a54b748a332c355d896", - "sha256:84d2222e61f313c4848ff05353653bf5f5cf6ce34df540e4274516880d9c3763", - "sha256:8a6979cf527e2603d349a91060f428bcb135aea2be3201dff794813256c274f1", - "sha256:8a76e027f87753f9bd1ab5f7c9cb8c7628d1077ef927f5e2446477153a602f2c", - "sha256:964b4dfb7c1c1965ac4c1978b0f755cc4bd698e8aa2b7667c575fb5f04ebe06b", - "sha256:9972aad21f965599ed0106f65334230ce826e5ae69fda7cbd688d24fa922415e", - "sha256:a8c28fd40a4226b4a84bdf2d2b5b37d2c7bd49486b5adcc200e8c7ec991dfa7e", - "sha256:ae102a98c547ee2288637af07393dd33f440c25e5cd79556b04e3fca13325e5f", - "sha256:af335bac6b666cc6aea16f11d486c3b794029d9df029967f9938a4bed59b6a19", - "sha256:afe64e9b8ea66866a771996f6ff14447e8082ea26e675a295ad3bdbffdd72afb", - "sha256:b4b24f75d16a89cc6b4cdff0eb6a910a966ecd476d1e73f7ce5985ff1328e9a6", - "sha256:b6c8288bb8a84b47e07013bb4850f50538aa913d487579e1921724631d02ea1b", - "sha256:b83456c2d4979e08ff56180a76429263ea254c3f6552cd14ada95cff1dec9bb8", - "sha256:bfb13af3c5dd3a9588000910178de17010ebcccd37b4f9794b00595e3a8ddad3", - "sha256:c3dba7dab16709a33a847e5cd756767271697041fbe3fe97c215b1fc1f5c9848", - "sha256:c48d8f2db17f27d41fb0e2ecd703ea41984ee19362cbce52c097963b3a1b4365", - "sha256:c7e62ab8b332147a7593a385d4f368874d5fe4ad4e341770d4983442d89603e3", - "sha256:c83a74b68270028dc8ee74d38ecfaf9c90eed23c8959fca95bd703d25b82c88e", - "sha256:cacbdc5839bdff804dfebc058fe25684cae322987f7a38b0168bc1b2df703fb1", - "sha256:cf4499e0a83b7b7edcb8dabecbd8501d0d3a5ef66457200f77bde3d210d5debb", - "sha256:cfec476887aa231b8548ece2e06d28edc87c1397ebd83922299af2e051cf2827", - "sha256:d26e0342183c762de3276cca7a530d574d4e25121ca7d6e4a98e4f05cb8e4df7", - "sha256:d4e6036decf4b72d6425d5b29bbd3e8f0ff1059cda7ac7b96d6ac5ed34ffbacd", - "sha256:d57c3fd55d9058645d26ae37d76e61156a27722097229d32a9e73ed54819982a", - "sha256:dfa74c903a3c1f0d9b1c7e7b53ed2d929a4910e272add6700c38f365a6002820", - "sha256:e3ed340d2b858d6e6fb5083f87c09996506af483227735de6964a6100b4e6a54", - "sha256:e78e6e2a00c223e164c417628572a90093c031ed724492c763721c2e0bc2a8df", - "sha256:e9182eb20f41417ea1dd8e8f7888c4d7c6e805f8a7c98c1081778a3da2bee3e4", - "sha256:e99e34c82309dd78959ba3c1590975b5d3c862d6f279f843d47d26ff89d7d7e1", - "sha256:f6a88f384335bb27812293fdb11ac6aee2ca3f51d3c7820fe03de0a304ab6249", - "sha256:f81e65376e52f03422e1fb475c9514185669943798ed019ac50410fb4c4df232", - "sha256:ffe9dc0a884a8848075e576c1de0290d85a533a9f6e9c4e564f19adf8f6e54a7" + "sha256:03ef7df18daf2c4c07e2695e8cfd5ee7f748a1d54d802330985a78d2a5a6dca9", + "sha256:0a602ea5aff39bb9fac6308e9c9d82b9a35c2bf288e184a816002c9fae930b77", + "sha256:0c009475ee389757e6e34611d75f6e4f05f0cf5ebb76c6037508318e1a1e0d7e", + "sha256:0ef4854e82c09e84cc63084a9e4ccd6d9b154f1dbdd283efb92ecd0b5e2b8c84", + "sha256:1236ed0952fbd919c100bc839eaa4a39ebc397ed1c08a97fc45fee2a595aa1b3", + "sha256:143072318f793f53819048fdfe30c321890af0c3ec7cb1dfc9cc87aa88241de2", + "sha256:15208be1c50b99203fe88d15695f22a5bed95ab3f84354c494bcb1d08557df67", + "sha256:1873aade94b74715be2246321c8650cabf5a0d098a95bab81145ffffa4c13876", + "sha256:18d0ef97766055fec15b5de2c06dd8e7654705ce3e5e5eed3b6651a1d2a9a152", + "sha256:1ea665f8ce695bcc37a90ee52de7a7980be5161375d42a0b6c6abedbf0d81f0f", + "sha256:2293b001e319ab0d869d660a704942c9e2cce19745262a8aba2115ef41a0a42a", + "sha256:246b123cc54bb5361588acc54218c8c9fb73068bf227a4a531d8ed56fa3ca7d6", + "sha256:275ff571376626195ab95a746e6a04c7df8ea34638b99fc11160de91f2fef503", + "sha256:281309265596e388ef483250db3640e5f414168c5a67e9c665cafce9492eda2f", + "sha256:2d423c8d8a3c82d08fe8af900ad5b613ce3632a1249fd6a223941d0735fce493", + "sha256:2e5afae772c00980525f6d6ecf7cbca55676296b580c0e6abb407f15f3706996", + "sha256:30dcc86377618a4c8f3b72418df92e77be4254d8f89f14b8e8f57d6d43603c0f", + "sha256:31a34c508c003a4347d389a9e6fcc2307cc2150eb516462a7a17512130de109e", + "sha256:323ba25b92454adb36fa425dc5cf6f8f19f78948cbad2e7bc6cdf7b0d7982e59", + "sha256:34eccd14566f8fe14b2b95bb13b11572f7c7d5c36da61caf414d23b91fcc5d94", + "sha256:3a58c98a7e9c021f357348867f537017057c2ed7f77337fd914d0bedb35dace7", + "sha256:3f78fd71c4f43a13d342be74ebbc0666fe1f555b8837eb113cb7416856c79682", + "sha256:4154ad09dac630a0f13f37b583eae260c6aa885d67dfbccb5b02c33f31a6d420", + "sha256:420f9bbf47a02616e8554e825208cb947969451978dceb77f95ad09c37791dae", + "sha256:4686818798f9194d03c9129a4d9a702d9e113a89cb03bffe08c6cf799e053291", + "sha256:57fede879f08d23c85140a360c6a77709113efd1c993923c59fde17aa27599fe", + "sha256:60989127da422b74a04345096c10d416c2b41bd7bf2a380eb541059e4e999980", + "sha256:68fc1f1ba168724771e38bee37d940d2865cb0f562380a1fb1ffb428b75cb692", + "sha256:6e6f98446430fdf41bd36d4faa6cb409f5140c1c2cf58ce0bbdaf16af7d3f119", + "sha256:729177eaf0aefca0994ce4cffe96ad3c75e377c7b6f4efa59ebf003b6d398716", + "sha256:72dffbd8b4194858d0941062a9766f8297e8868e1dd07a7b36212aaa90f49472", + "sha256:75723c3c0fbbf34350b46a3199eb50638ab22a0228f93fb472ef4d9becc2382b", + "sha256:77853062a2c45be16fd6b8d6de2a99278ee1d985a7bd8b103e97e41c034006d2", + "sha256:78151aa3ec21dccd5cdef6c74c3e73386dcdfaf19bced944169697d7ac7482fc", + "sha256:7f01846810177d829c7692f1f5ada8096762d9172af1b1a28d4ab5b77c923c1c", + "sha256:804d99b24ad523a1fe18cc707bf741670332f7c7412e9d49cb5eab67e886b9b5", + "sha256:8359bf4791968c5a78c56103702000105501adb557f3cf772b2c207284273984", + "sha256:83791a65b51ad6ee6cf0845634859d69a038ea9b03d7b26e703f94c7e93dbcf9", + "sha256:8532fd6e6e2dc57bcb3bc90b079c60de896d2128c5d9d6f24a63875a95a088cf", + "sha256:876801744b0dee379e4e3c38b76fc89f88834bb15bf92ee07d94acd06ec890a0", + "sha256:8dbf6d1bc73f1d04ec1734bae3b4fb0ee3cb2a493d35ede9badbeb901fb40f6f", + "sha256:8f8544b092a29a6ddd72f3556a9fcf249ec412e10ad28be6a0c0d948924f2212", + "sha256:911dda9c487075abd54e644ccdf5e5c16773470a6a5d3826fda76699410066fb", + "sha256:977646e05232579d2e7b9c59e21dbe5261f403a88417f6a6512e70d3f8a046be", + "sha256:9dba73be7305b399924709b91682299794887cbbd88e38226ed9f6712eabee90", + "sha256:a148c5d507bb9b4f2030a2025c545fccb0e1ef317393eaba42e7eabd28eb6041", + "sha256:a6cdcc3ede532f4a4b96000b6362099591ab4a3e913d70bcbac2b56c872446f7", + "sha256:ac05fb791acf5e1a3e39402641827780fe44d27e72567a000412c648a85ba860", + "sha256:b58b4710c7f4161b5e9dcbe73bb7c62d65670a87df7bcce9e1faaad43e715245", + "sha256:b6356793b84728d9d50ead16ab43c187673831e9d4019013f1402c41b1db9b27", + "sha256:b76bedd166805480ab069612119ea636f5ab8f8771e640ae103e05a4aae3e417", + "sha256:bc7bb56d04601d443f24094e9e31ae6deec9ccb23581f75343feebaf30423359", + "sha256:c2470da5418b76232f02a2fcd2229537bb2d5a7096674ce61859c3229f2eb202", + "sha256:c332c8d69fb64979ebf76613c66b985414927a40f8defa16cf1bc028b7b0a7b0", + "sha256:c6af2a6d4b7ee9615cbb162b0738f6e1fd1f5c3eda7e5da17861eacf4c717ea7", + "sha256:c77e3d1862452565875eb31bdb45ac62502feabbd53429fdc39a1cc341d681ba", + "sha256:ca08decd2697fdea0aea364b370b1249d47336aec935f87b8bbfd7da5b2ee9c1", + "sha256:ca49a8119c6cbd77375ae303b0cfd8c11f011abbbd64601167ecca18a87e7cdd", + "sha256:cb16c65dcb648d0a43a2521f2f0a2300f40639f6f8c1ecbc662141e4e3e1ee07", + "sha256:d2997c458c690ec2bc6b0b7ecbafd02b029b7b4283078d3b32a852a7ce3ddd98", + "sha256:d3f82c171b4ccd83bbaf35aa05e44e690113bd4f3b7b6cc54d2219b132f3ae55", + "sha256:dc4926288b2a3e9fd7b50dc6a1909a13bbdadfc67d93f3374d984e56f885579d", + "sha256:ead20f7913a9c1e894aebe47cccf9dc834e1618b7aa96155d2091a626e59c972", + "sha256:ebdc36bea43063116f0486869652cb2ed7032dbc59fbcb4445c4862b5c1ecf7f", + "sha256:ed1184ab8f113e8d660ce49a56390ca181f2981066acc27cf637d5c1e10ce46e", + "sha256:ee825e70b1a209475622f7f7b776785bd68f34af6e7a46e2e42f27b659b5bc26", + "sha256:f7ae5d65ccfbebdfa761585228eb4d0df3a8b15cfb53bd953e713e09fbb12957", + "sha256:f7fc5a5acafb7d6ccca13bfa8c90f8c51f13d8fb87d95656d3950f0158d3ce53", + "sha256:f9b5571d33660d5009a8b3c25dc1db560206e2d2f89d3df1cb32d72c0d117d52" ], "index": "pypi", - "version": "==2.9.6" + "markers": "python_version >= '3.7'", + "version": "==2.9.9" }, "pycparser": { "hashes": [ @@ -655,83 +832,170 @@ }, "pycryptodomex": { "hashes": [ - "sha256:160a39a708c36fa0b168ab79386dede588e62aec06eb505add870739329aecc6", - "sha256:192306cf881fe3467dda0e174a4f47bb3a8bb24b90c9cdfbdc248eec5fc0578c", - "sha256:1949e09ea49b09c36d11a951b16ff2a05a0ffe969dda1846e4686ee342fe8646", - "sha256:215be2980a6b70704c10796dd7003eb4390e7be138ac6fb8344bf47e71a8d470", - "sha256:27072a494ce621cc7a9096bbf60ed66826bb94db24b49b7359509e7951033e74", - "sha256:2dc4eab20f4f04a2d00220fdc9258717b82d31913552e766d5f00282c031b70a", - "sha256:302a8f37c224e7b5d72017d462a2be058e28f7be627bdd854066e16722d0fc0c", - "sha256:3d9314ac785a5b75d5aaf924c5f21d6ca7e8df442e5cf4f0fefad4f6e284d422", - "sha256:3e3ecb5fe979e7c1bb0027e518340acf7ee60415d79295e5251d13c68dde576e", - "sha256:4d9379c684efea80fdab02a3eb0169372bca7db13f9332cb67483b8dc8b67c37", - "sha256:50308fcdbf8345e5ec224a5502b4215178bdb5e95456ead8ab1a69ffd94779cb", - "sha256:5594a125dae30d60e94f37797fc67ce3c744522de7992c7c360d02fdb34918f8", - "sha256:58fc0aceb9c961b9897facec9da24c6a94c5db04597ec832060f53d4d6a07196", - "sha256:6421d23d6a648e83ba2670a352bcd978542dad86829209f59d17a3f087f4afef", - "sha256:6875eb8666f68ddbd39097867325bd22771f595b4e2b0149739b5623c8bf899b", - "sha256:6ed3606832987018615f68e8ed716a7065c09a0fe94afd7c9ca1b6777f0ac6eb", - "sha256:71687eed47df7e965f6e0bf3cadef98f368d5221f0fb89d2132effe1a3e6a194", - "sha256:73d64b32d84cf48d9ec62106aa277dbe99ab5fbfd38c5100bc7bddd3beb569f7", - "sha256:75672205148bdea34669173366df005dbd52be05115e919551ee97171083423d", - "sha256:76f0a46bee539dae4b3dfe37216f678769349576b0080fdbe431d19a02da42ff", - "sha256:8ff129a5a0eb5ff16e45ca4fa70a6051da7f3de303c33b259063c19be0c43d35", - "sha256:ac614363a86cc53d8ba44b6c469831d1555947e69ab3276ae8d6edc219f570f7", - "sha256:ba95abd563b0d1b88401658665a260852a8e6c647026ee6a0a65589287681df8", - "sha256:bbdcce0a226d9205560a5936b05208c709b01d493ed8307792075dedfaaffa5f", - "sha256:bec6c80994d4e7a38312072f89458903b65ec99bed2d65aa4de96d997a53ea7a", - "sha256:c2953afebf282a444c51bf4effe751706b4d0d63d7ca2cc51db21f902aa5b84e", - "sha256:d35a8ffdc8b05e4b353ba281217c8437f02c57d7233363824e9d794cf753c419", - "sha256:d56c9ec41258fd3734db9f5e4d2faeabe48644ba9ca23b18e1839b3bdf093222", - "sha256:d84e105787f5e5d36ec6a581ff37a1048d12e638688074b2a00bcf402f9aa1c2", - "sha256:e00a4bacb83a2627e8210cb353a2e31f04befc1155db2976e5e239dd66482278", - "sha256:f237278836dda412a325e9340ba2e6a84cb0f56b9244781e5b61f10b3905de88", - "sha256:f9ab5ef0718f6a8716695dea16d83b671b22c45e9c0c78fd807c32c0192e54b5" + "sha256:09c9401dc06fb3d94cb1ec23b4ea067a25d1f4c6b7b118ff5631d0b5daaab3cc", + "sha256:0b2f1982c5bc311f0aab8c293524b861b485d76f7c9ab2c3ac9a25b6f7655975", + "sha256:136b284e9246b4ccf4f752d435c80f2c44fc2321c198505de1d43a95a3453b3c", + "sha256:1789d89f61f70a4cd5483d4dfa8df7032efab1118f8b9894faae03c967707865", + "sha256:2126bc54beccbede6eade00e647106b4f4c21e5201d2b0a73e9e816a01c50905", + "sha256:258c4233a3fe5a6341780306a36c6fb072ef38ce676a6d41eec3e591347919e8", + "sha256:263de9a96d2fcbc9f5bd3a279f14ea0d5f072adb68ebd324987576ec25da084d", + "sha256:50cb18d4dd87571006fd2447ccec85e6cec0136632a550aa29226ba075c80644", + "sha256:5b883e1439ab63af976656446fb4839d566bb096f15fc3c06b5a99cde4927188", + "sha256:5d73e9fa3fe830e7b6b42afc49d8329b07a049a47d12e0ef9225f2fd220f19b2", + "sha256:61056a1fd3254f6f863de94c233b30dd33bc02f8c935b2000269705f1eeeffa4", + "sha256:67c8eb79ab33d0fbcb56842992298ddb56eb6505a72369c20f60bc1d2b6fb002", + "sha256:6e45bb4635b3c4e0a00ca9df75ef6295838c85c2ac44ad882410cb631ed1eeaa", + "sha256:7cb51096a6a8d400724104db8a7e4f2206041a1f23e58924aa3d8d96bcb48338", + "sha256:800a2b05cfb83654df80266692f7092eeefe2a314fa7901dcefab255934faeec", + "sha256:8df69e41f7e7015a90b94d1096ec3d8e0182e73449487306709ec27379fff761", + "sha256:917033016ecc23c8933205585a0ab73e20020fdf671b7cd1be788a5c4039840b", + "sha256:a12144d785518f6491ad334c75ccdc6ad52ea49230b4237f319dbb7cef26f464", + "sha256:a3866d68e2fc345162b1b9b83ef80686acfe5cec0d134337f3b03950a0a8bf56", + "sha256:a588a1cb7781da9d5e1c84affd98c32aff9c89771eac8eaa659d2760666f7139", + "sha256:a77b79852175064c822b047fee7cf5a1f434f06ad075cc9986aa1c19a0c53eb0", + "sha256:af83a554b3f077564229865c45af0791be008ac6469ef0098152139e6bd4b5b6", + "sha256:b801216c48c0886742abf286a9a6b117e248ca144d8ceec1f931ce2dd0c9cb40", + "sha256:bfb040b5dda1dff1e197d2ef71927bd6b8bfcb9793bc4dfe0bb6df1e691eaacb", + "sha256:c01678aee8ac0c1a461cbc38ad496f953f9efcb1fa19f5637cbeba7544792a53", + "sha256:c74eb1f73f788facece7979ce91594dc177e1a9b5d5e3e64697dd58299e5cb4d", + "sha256:c9a68a2f7bd091ccea54ad3be3e9d65eded813e6d79fdf4cc3604e26cdd6384f", + "sha256:d4dd3b381ff5a5907a3eb98f5f6d32c64d319a840278ceea1dcfcc65063856f3", + "sha256:e8e5ecbd4da4157889fce8ba49da74764dd86c891410bfd6b24969fa46edda51", + "sha256:eb2fc0ec241bf5e5ef56c8fbec4a2634d631e4c4f616a59b567947a0f35ad83c", + "sha256:edbe083c299835de7e02c8aa0885cb904a75087d35e7bab75ebe5ed336e8c3e2", + "sha256:ff64fd720def623bf64d8776f8d0deada1cc1bf1ec3c1f9d6f5bb5bd098d034f" ], "index": "pypi", - "version": "==3.18.0" + "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3, 3.4'", + "version": "==3.19.0" }, "pydantic": { "hashes": [ - "sha256:052d8654cb65174d6f9490cc9b9a200083a82cf5c3c5d3985db765757eb3b375", - "sha256:0c6fafa0965b539d7aab0a673a046466d23b86e4b0e8019d25fd53f4df62c277", - "sha256:1243d28e9b05003a89d72e7915fdb26ffd1d39bdd39b00b7dbe4afae4b557f9d", - "sha256:12f7b0bf8553e310e530e9f3a2f5734c68699f42218bf3568ef49cd9b0e44df4", - "sha256:1410275520dfa70effadf4c21811d755e7ef9bb1f1d077a21958153a92c8d9ca", - "sha256:16f8c3e33af1e9bb16c7a91fc7d5fa9fe27298e9f299cff6cb744d89d573d62c", - "sha256:17aef11cc1b997f9d574b91909fed40761e13fac438d72b81f902226a69dac01", - "sha256:191ba419b605f897ede9892f6c56fb182f40a15d309ef0142212200a10af4c18", - "sha256:1952526ba40b220b912cdc43c1c32bcf4a58e3f192fa313ee665916b26befb68", - "sha256:1ced8375969673929809d7f36ad322934c35de4af3b5e5b09ec967c21f9f7887", - "sha256:2e4148e635994d57d834be1182a44bdb07dd867fa3c2d1b37002000646cc5459", - "sha256:34d327c81e68a1ecb52fe9c8d50c8a9b3e90d3c8ad991bfc8f953fb477d42fb4", - "sha256:35db5301b82e8661fa9c505c800d0990bc14e9f36f98932bb1d248c0ac5cada5", - "sha256:3e59417ba8a17265e632af99cc5f35ec309de5980c440c255ab1ca3ae96a3e0e", - "sha256:42aa0c4b5c3025483240a25b09f3c09a189481ddda2ea3a831a9d25f444e03c1", - "sha256:666bdf6066bf6dbc107b30d034615d2627e2121506c555f73f90b54a463d1f33", - "sha256:66a703d1983c675a6e0fed8953b0971c44dba48a929a2000a493c3772eb61a5a", - "sha256:6a82d6cda82258efca32b40040228ecf43a548671cb174a1e81477195ed3ed56", - "sha256:6f2e754d5566f050954727c77f094e01793bcb5725b663bf628fa6743a5a9108", - "sha256:7456eb22ed9aaa24ff3e7b4757da20d9e5ce2a81018c1b3ebd81a0b88a18f3b2", - "sha256:7b1f6cb446470b7ddf86c2e57cd119a24959af2b01e552f60705910663af09a4", - "sha256:7d5b8641c24886d764a74ec541d2fc2c7fb19f6da2a4001e6d580ba4a38f7878", - "sha256:84d80219c3f8d4cad44575e18404099c76851bc924ce5ab1c4c8bb5e2a2227d0", - "sha256:88f195f582851e8db960b4a94c3e3ad25692c1c1539e2552f3df7a9e972ef60e", - "sha256:93e6bcfccbd831894a6a434b0aeb1947f9e70b7468f274154d03d71fabb1d7c6", - "sha256:93e766b4a8226e0708ef243e843105bf124e21331694367f95f4e3b4a92bbb3f", - "sha256:ab523c31e22943713d80d8d342d23b6f6ac4b792a1e54064a8d0cf78fd64e800", - "sha256:bb14388ec45a7a0dc429e87def6396f9e73c8c77818c927b6a60706603d5f2ea", - "sha256:c0ab53b609c11dfc0c060d94335993cc2b95b2150e25583bec37a49b2d6c6c3f", - "sha256:c33b60054b2136aef8cf190cd4c52a3daa20b2263917c49adad20eaf381e823b", - "sha256:ceb6a23bf1ba4b837d0cfe378329ad3f351b5897c8d4914ce95b85fba96da5a1", - "sha256:d532bf00f381bd6bc62cabc7d1372096b75a33bc197a312b03f5838b4fb84edd", - "sha256:df7800cb1984d8f6e249351139667a8c50a379009271ee6236138a22a0c0f319", - "sha256:e82d4566fcd527eae8b244fa952d99f2ca3172b7e97add0b43e2d97ee77f81ab", - "sha256:f90c1e29f447557e9e26afb1c4dbf8768a10cc676e3781b6a577841ade126b85", - "sha256:f9613fadad06b4f3bc5db2653ce2f22e0de84a7c6c293909b48f6ed37b83c61f" + "sha256:94f336138093a5d7f426aac732dcfe7ab4eb4da243c88f891d65deb4a2556ee7", + "sha256:bc3ddf669d234f4220e6e1c4d96b061abe0998185a8d7855c0126782b7abc8c1" ], "markers": "python_version >= '3.7'", - "version": "==1.10.8" + "version": "==2.4.2" + }, + "pydantic-core": { + "hashes": [ + "sha256:042462d8d6ba707fd3ce9649e7bf268633a41018d6a998fb5fbacb7e928a183e", + "sha256:0523aeb76e03f753b58be33b26540880bac5aa54422e4462404c432230543f33", + "sha256:05560ab976012bf40f25d5225a58bfa649bb897b87192a36c6fef1ab132540d7", + "sha256:0675ba5d22de54d07bccde38997e780044dcfa9a71aac9fd7d4d7a1d2e3e65f7", + "sha256:073d4a470b195d2b2245d0343569aac7e979d3a0dcce6c7d2af6d8a920ad0bea", + "sha256:07ec6d7d929ae9c68f716195ce15e745b3e8fa122fc67698ac6498d802ed0fa4", + "sha256:0880e239827b4b5b3e2ce05e6b766a7414e5f5aedc4523be6b68cfbc7f61c5d0", + "sha256:0c27f38dc4fbf07b358b2bc90edf35e82d1703e22ff2efa4af4ad5de1b3833e7", + "sha256:0d8a8adef23d86d8eceed3e32e9cca8879c7481c183f84ed1a8edc7df073af94", + "sha256:0e2a35baa428181cb2270a15864ec6286822d3576f2ed0f4cd7f0c1708472aff", + "sha256:0f8682dbdd2f67f8e1edddcbffcc29f60a6182b4901c367fc8c1c40d30bb0a82", + "sha256:0fa467fd300a6f046bdb248d40cd015b21b7576c168a6bb20aa22e595c8ffcdd", + "sha256:128552af70a64660f21cb0eb4876cbdadf1a1f9d5de820fed6421fa8de07c893", + "sha256:1396e81b83516b9d5c9e26a924fa69164156c148c717131f54f586485ac3c15e", + "sha256:149b8a07712f45b332faee1a2258d8ef1fb4a36f88c0c17cb687f205c5dc6e7d", + "sha256:14ac492c686defc8e6133e3a2d9eaf5261b3df26b8ae97450c1647286750b901", + "sha256:14cfbb00959259e15d684505263d5a21732b31248a5dd4941f73a3be233865b9", + "sha256:14e09ff0b8fe6e46b93d36a878f6e4a3a98ba5303c76bb8e716f4878a3bee92c", + "sha256:154ea7c52e32dce13065dbb20a4a6f0cc012b4f667ac90d648d36b12007fa9f7", + "sha256:15d6bca84ffc966cc9976b09a18cf9543ed4d4ecbd97e7086f9ce9327ea48891", + "sha256:1d40f55222b233e98e3921df7811c27567f0e1a4411b93d4c5c0f4ce131bc42f", + "sha256:25bd966103890ccfa028841a8f30cebcf5875eeac8c4bde4fe221364c92f0c9a", + "sha256:2cf5bb4dd67f20f3bbc1209ef572a259027c49e5ff694fa56bed62959b41e1f9", + "sha256:2e0e2959ef5d5b8dc9ef21e1a305a21a36e254e6a34432d00c72a92fdc5ecda5", + "sha256:320f14bd4542a04ab23747ff2c8a778bde727158b606e2661349557f0770711e", + "sha256:3625578b6010c65964d177626fde80cf60d7f2e297d56b925cb5cdeda6e9925a", + "sha256:39215d809470f4c8d1881758575b2abfb80174a9e8daf8f33b1d4379357e417c", + "sha256:3f0ac9fb8608dbc6eaf17956bf623c9119b4db7dbb511650910a82e261e6600f", + "sha256:417243bf599ba1f1fef2bb8c543ceb918676954734e2dcb82bf162ae9d7bd514", + "sha256:420a692b547736a8d8703c39ea935ab5d8f0d2573f8f123b0a294e49a73f214b", + "sha256:443fed67d33aa85357464f297e3d26e570267d1af6fef1c21ca50921d2976302", + "sha256:48525933fea744a3e7464c19bfede85df4aba79ce90c60b94d8b6e1eddd67096", + "sha256:485a91abe3a07c3a8d1e082ba29254eea3e2bb13cbbd4351ea4e5a21912cc9b0", + "sha256:4a5be350f922430997f240d25f8219f93b0c81e15f7b30b868b2fddfc2d05f27", + "sha256:4d966c47f9dd73c2d32a809d2be529112d509321c5310ebf54076812e6ecd884", + "sha256:524ff0ca3baea164d6d93a32c58ac79eca9f6cf713586fdc0adb66a8cdeab96a", + "sha256:53df009d1e1ba40f696f8995683e067e3967101d4bb4ea6f667931b7d4a01357", + "sha256:5994985da903d0b8a08e4935c46ed8daf5be1cf217489e673910951dc533d430", + "sha256:5cabb9710f09d5d2e9e2748c3e3e20d991a4c5f96ed8f1132518f54ab2967221", + "sha256:5fdb39f67c779b183b0c853cd6b45f7db84b84e0571b3ef1c89cdb1dfc367325", + "sha256:600d04a7b342363058b9190d4e929a8e2e715c5682a70cc37d5ded1e0dd370b4", + "sha256:631cb7415225954fdcc2a024119101946793e5923f6c4d73a5914d27eb3d3a05", + "sha256:63974d168b6233b4ed6a0046296803cb13c56637a7b8106564ab575926572a55", + "sha256:64322bfa13e44c6c30c518729ef08fda6026b96d5c0be724b3c4ae4da939f875", + "sha256:655f8f4c8d6a5963c9a0687793da37b9b681d9ad06f29438a3b2326d4e6b7970", + "sha256:6835451b57c1b467b95ffb03a38bb75b52fb4dc2762bb1d9dbed8de31ea7d0fc", + "sha256:6db2eb9654a85ada248afa5a6db5ff1cf0f7b16043a6b070adc4a5be68c716d6", + "sha256:7c4d1894fe112b0864c1fa75dffa045720a194b227bed12f4be7f6045b25209f", + "sha256:7eb037106f5c6b3b0b864ad226b0b7ab58157124161d48e4b30c4a43fef8bc4b", + "sha256:8282bab177a9a3081fd3d0a0175a07a1e2bfb7fcbbd949519ea0980f8a07144d", + "sha256:82f55187a5bebae7d81d35b1e9aaea5e169d44819789837cdd4720d768c55d15", + "sha256:8572cadbf4cfa95fb4187775b5ade2eaa93511f07947b38f4cd67cf10783b118", + "sha256:8cdbbd92154db2fec4ec973d45c565e767ddc20aa6dbaf50142676484cbff8ee", + "sha256:8f6e6aed5818c264412ac0598b581a002a9f050cb2637a84979859e70197aa9e", + "sha256:92f675fefa977625105708492850bcbc1182bfc3e997f8eecb866d1927c98ae6", + "sha256:962ed72424bf1f72334e2f1e61b68f16c0e596f024ca7ac5daf229f7c26e4208", + "sha256:9badf8d45171d92387410b04639d73811b785b5161ecadabf056ea14d62d4ede", + "sha256:9c120c9ce3b163b985a3b966bb701114beb1da4b0468b9b236fc754783d85aa3", + "sha256:9f6f3e2598604956480f6c8aa24a3384dbf6509fe995d97f6ca6103bb8c2534e", + "sha256:a1254357f7e4c82e77c348dabf2d55f1d14d19d91ff025004775e70a6ef40ada", + "sha256:a1392e0638af203cee360495fd2cfdd6054711f2db5175b6e9c3c461b76f5175", + "sha256:a1c311fd06ab3b10805abb72109f01a134019739bd3286b8ae1bc2fc4e50c07a", + "sha256:a5cb87bdc2e5f620693148b5f8f842d293cae46c5f15a1b1bf7ceeed324a740c", + "sha256:a7a7902bf75779bc12ccfc508bfb7a4c47063f748ea3de87135d433a4cca7a2f", + "sha256:aad7bd686363d1ce4ee930ad39f14e1673248373f4a9d74d2b9554f06199fb58", + "sha256:aafdb89fdeb5fe165043896817eccd6434aee124d5ee9b354f92cd574ba5e78f", + "sha256:ae8a8843b11dc0b03b57b52793e391f0122e740de3df1474814c700d2622950a", + "sha256:b00bc4619f60c853556b35f83731bd817f989cba3e97dc792bb8c97941b8053a", + "sha256:b1f22a9ab44de5f082216270552aa54259db20189e68fc12484873d926426921", + "sha256:b3c01c2fb081fced3bbb3da78510693dc7121bb893a1f0f5f4b48013201f362e", + "sha256:b3dcd587b69bbf54fc04ca157c2323b8911033e827fffaecf0cafa5a892a0904", + "sha256:b4a6db486ac8e99ae696e09efc8b2b9fea67b63c8f88ba7a1a16c24a057a0776", + "sha256:bec7dd208a4182e99c5b6c501ce0b1f49de2802448d4056091f8e630b28e9a52", + "sha256:c0877239307b7e69d025b73774e88e86ce82f6ba6adf98f41069d5b0b78bd1bf", + "sha256:caa48fc31fc7243e50188197b5f0c4228956f97b954f76da157aae7f67269ae8", + "sha256:cfe1090245c078720d250d19cb05d67e21a9cd7c257698ef139bc41cf6c27b4f", + "sha256:d43002441932f9a9ea5d6f9efaa2e21458221a3a4b417a14027a1d530201ef1b", + "sha256:d64728ee14e667ba27c66314b7d880b8eeb050e58ffc5fec3b7a109f8cddbd63", + "sha256:d6495008733c7521a89422d7a68efa0a0122c99a5861f06020ef5b1f51f9ba7c", + "sha256:d8f1ebca515a03e5654f88411420fea6380fc841d1bea08effb28184e3d4899f", + "sha256:d99277877daf2efe074eae6338453a4ed54a2d93fb4678ddfe1209a0c93a2468", + "sha256:da01bec0a26befab4898ed83b362993c844b9a607a86add78604186297eb047e", + "sha256:db9a28c063c7c00844ae42a80203eb6d2d6bbb97070cfa00194dff40e6f545ab", + "sha256:dda81e5ec82485155a19d9624cfcca9be88a405e2857354e5b089c2a982144b2", + "sha256:e357571bb0efd65fd55f18db0a2fb0ed89d0bb1d41d906b138f088933ae618bb", + "sha256:e544246b859f17373bed915182ab841b80849ed9cf23f1f07b73b7c58baee5fb", + "sha256:e562617a45b5a9da5be4abe72b971d4f00bf8555eb29bb91ec2ef2be348cd132", + "sha256:e570ffeb2170e116a5b17e83f19911020ac79d19c96f320cbfa1fa96b470185b", + "sha256:e6f31a17acede6a8cd1ae2d123ce04d8cca74056c9d456075f4f6f85de055607", + "sha256:e9121b4009339b0f751955baf4543a0bfd6bc3f8188f8056b1a25a2d45099934", + "sha256:ebedb45b9feb7258fac0a268a3f6bec0a2ea4d9558f3d6f813f02ff3a6dc6698", + "sha256:ecaac27da855b8d73f92123e5f03612b04c5632fd0a476e469dfc47cd37d6b2e", + "sha256:ecdbde46235f3d560b18be0cb706c8e8ad1b965e5c13bbba7450c86064e96561", + "sha256:ed550ed05540c03f0e69e6d74ad58d026de61b9eaebebbaaf8873e585cbb18de", + "sha256:eeb3d3d6b399ffe55f9a04e09e635554012f1980696d6b0aca3e6cf42a17a03b", + "sha256:ef337945bbd76cce390d1b2496ccf9f90b1c1242a3a7bc242ca4a9fc5993427a", + "sha256:f1365e032a477c1430cfe0cf2856679529a2331426f8081172c4a74186f1d595", + "sha256:f23b55eb5464468f9e0e9a9935ce3ed2a870608d5f534025cd5536bca25b1402", + "sha256:f2e9072d71c1f6cfc79a36d4484c82823c560e6f5599c43c1ca6b5cdbd54f881", + "sha256:f323306d0556351735b54acbf82904fe30a27b6a7147153cbe6e19aaaa2aa429", + "sha256:f36a3489d9e28fe4b67be9992a23029c3cec0babc3bd9afb39f49844a8c721c5", + "sha256:f64f82cc3443149292b32387086d02a6c7fb39b8781563e0ca7b8d7d9cf72bd7", + "sha256:f6defd966ca3b187ec6c366604e9296f585021d922e666b99c47e78738b5666c", + "sha256:f7c2b8eb9fc872e68b46eeaf835e86bccc3a58ba57d0eedc109cbb14177be531", + "sha256:fa7db7558607afeccb33c0e4bf1c9a9a835e26599e76af6fe2fcea45904083a6", + "sha256:fcb83175cc4936a5425dde3356f079ae03c0802bbdf8ff82c035f8a54b333521" + ], + "markers": "python_version >= '3.7'", + "version": "==2.10.1" + }, + "pydantic-settings": { + "hashes": [ + "sha256:962dc3672495aad6ae96a4390fac7e593591e144625e5112d359f8f67fb75945", + "sha256:ddd907b066622bd67603b75e2ff791875540dc485b7307c4fffc015719da8625" + ], + "markers": "python_version >= '3.7'", + "version": "==2.0.3" }, "pyjwkest": { "hashes": [ @@ -762,23 +1026,24 @@ "sha256:942c5a758f98d790eaed1a29cb6eefc7ffb0d1cf7af05c3d2791656dbd6ad1e1" ], "index": "pypi", + "markers": "python_version >= '3.7'", "version": "==2.31.0" }, "s3transfer": { "hashes": [ - "sha256:3c0da2d074bf35d6870ef157158641178a4204a6e689e82546083e31e0311346", - "sha256:640bb492711f4c0c0905e1f62b6aaeb771881935ad27884852411f8e9cacbca9" + "sha256:10d6923c6359175f264811ef4bf6161a3156ce8e350e705396a7557d6293c33a", + "sha256:fd3889a66f5fe17299fe75b82eae6cf722554edca744ca5d5fe308b104883d2e" ], "markers": "python_version >= '3.7'", - "version": "==0.6.1" + "version": "==0.7.0" }, "setuptools": { "hashes": [ - "sha256:5df61bf30bb10c6f756eb19e7c9f3b473051f48db77fddbe06ff2ca307df9a6f", - "sha256:62642358adc77ffa87233bc4d2354c4b2682d214048f500964dbe760ccedf102" + "sha256:4ac1475276d2f1c48684874089fefcd83bd7162ddaafb81fac866ba0db282a87", + "sha256:b454a35605876da60632df1a60f736524eb73cc47bbc9f3f1ef1b644de74fd2a" ], - "markers": "python_version >= '3.7'", - "version": "==67.8.0" + "markers": "python_version >= '3.8'", + "version": "==68.2.2" }, "six": { "hashes": [ @@ -798,27 +1063,79 @@ }, "typing-extensions": { "hashes": [ - "sha256:88a4153d8505aabbb4e13aacb7c486c2b4a33ca3b3f807914a9b4c844c471c26", - "sha256:d91d5919357fe7f681a9f2b5b4cb2a5f1ef0a1e9f59c4d8ff0d3491e05c0ffd5" + "sha256:8f92fc8806f9a6b641eaa5318da32b44d401efaac0f6678c9bc448ba3605faa0", + "sha256:df8e4339e9cb77357558cbdbceca33c303714cf861d1eef15e1070055ae8b7ef" ], "index": "pypi", - "version": "==4.6.3" + "markers": "python_version >= '3.8'", + "version": "==4.8.0" }, "urllib3": { "hashes": [ - "sha256:8d36afa7616d8ab714608411b4a3b13e58f463aee519024578e062e141dce20f", - "sha256:8f135f6502756bde6b2a9b28989df5fbe87c9970cecaa69041edcce7f0589b14" + "sha256:7a7c7003b000adf9e7ca2a377c9688bbc54ed41b985789ed576570342a375cd2", + "sha256:b19e1a85d206b56d7df1d5e683df4a7725252a964e3993648dd0fb5a1c157564" ], - "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3, 3.4, 3.5'", - "version": "==1.26.16" + "markers": "python_version >= '3.7'", + "version": "==2.0.6" }, "whitenoise": { "hashes": [ - "sha256:599dc6ca57e48929dfeffb2e8e187879bfe2aed0d49ca419577005b7f2cc930b", - "sha256:a02d6660ad161ff17e3042653c8e3f5ecbb2a2481a006bde125b9efb9a30113a" + "sha256:8998f7370973447fac1e8ef6e8ded2c5209a7b1f67c1012866dbcd09681c3251", + "sha256:b1f9db9bf67dc183484d760b99f4080185633136a273a03f6436034a41064146" ], "index": "pypi", - "version": "==6.4.0" + "markers": "python_version >= '3.8'", + "version": "==6.6.0" + }, + "zope.event": { + "hashes": [ + "sha256:2832e95014f4db26c47a13fdaef84cef2f4df37e66b59d8f1f4a8f319a632c26", + "sha256:bac440d8d9891b4068e2b5a2c5e2c9765a9df762944bda6955f96bb9b91e67cd" + ], + "markers": "python_version >= '3.7'", + "version": "==5.0" + }, + "zope.interface": { + "hashes": [ + "sha256:0c8cf55261e15590065039696607f6c9c1aeda700ceee40c70478552d323b3ff", + "sha256:13b7d0f2a67eb83c385880489dbb80145e9d344427b4262c49fbf2581677c11c", + "sha256:1f294a15f7723fc0d3b40701ca9b446133ec713eafc1cc6afa7b3d98666ee1ac", + "sha256:239a4a08525c080ff833560171d23b249f7f4d17fcbf9316ef4159f44997616f", + "sha256:2f8d89721834524a813f37fa174bac074ec3d179858e4ad1b7efd4401f8ac45d", + "sha256:2fdc7ccbd6eb6b7df5353012fbed6c3c5d04ceaca0038f75e601060e95345309", + "sha256:34c15ca9248f2e095ef2e93af2d633358c5f048c49fbfddf5fdfc47d5e263736", + "sha256:387545206c56b0315fbadb0431d5129c797f92dc59e276b3ce82db07ac1c6179", + "sha256:43b576c34ef0c1f5a4981163b551a8781896f2a37f71b8655fd20b5af0386abb", + "sha256:57d0a8ce40ce440f96a2c77824ee94bf0d0925e6089df7366c2272ccefcb7941", + "sha256:5a804abc126b33824a44a7aa94f06cd211a18bbf31898ba04bd0924fbe9d282d", + "sha256:67be3ca75012c6e9b109860820a8b6c9a84bfb036fbd1076246b98e56951ca92", + "sha256:6af47f10cfc54c2ba2d825220f180cc1e2d4914d783d6fc0cd93d43d7bc1c78b", + "sha256:6dc998f6de015723196a904045e5a2217f3590b62ea31990672e31fbc5370b41", + "sha256:70d2cef1bf529bff41559be2de9d44d47b002f65e17f43c73ddefc92f32bf00f", + "sha256:7ebc4d34e7620c4f0da7bf162c81978fce0ea820e4fa1e8fc40ee763839805f3", + "sha256:964a7af27379ff4357dad1256d9f215047e70e93009e532d36dcb8909036033d", + "sha256:97806e9ca3651588c1baaebb8d0c5ee3db95430b612db354c199b57378312ee8", + "sha256:9b9bc671626281f6045ad61d93a60f52fd5e8209b1610972cf0ef1bbe6d808e3", + "sha256:9ffdaa5290422ac0f1688cb8adb1b94ca56cee3ad11f29f2ae301df8aecba7d1", + "sha256:a0da79117952a9a41253696ed3e8b560a425197d4e41634a23b1507efe3273f1", + "sha256:a41f87bb93b8048fe866fa9e3d0c51e27fe55149035dcf5f43da4b56732c0a40", + "sha256:aa6fd016e9644406d0a61313e50348c706e911dca29736a3266fc9e28ec4ca6d", + "sha256:ad54ed57bdfa3254d23ae04a4b1ce405954969c1b0550cc2d1d2990e8b439de1", + "sha256:b012d023b4fb59183909b45d7f97fb493ef7a46d2838a5e716e3155081894605", + "sha256:b51b64432eed4c0744241e9ce5c70dcfecac866dff720e746d0a9c82f371dfa7", + "sha256:bbe81def9cf3e46f16ce01d9bfd8bea595e06505e51b7baf45115c77352675fd", + "sha256:c9559138690e1bd4ea6cd0954d22d1e9251e8025ce9ede5d0af0ceae4a401e43", + "sha256:e30506bcb03de8983f78884807e4fd95d8db6e65b69257eea05d13d519b83ac0", + "sha256:e33e86fd65f369f10608b08729c8f1c92ec7e0e485964670b4d2633a4812d36b", + "sha256:e441e8b7d587af0414d25e8d05e27040d78581388eed4c54c30c0c91aad3a379", + "sha256:e8bb9c990ca9027b4214fa543fd4025818dc95f8b7abce79d61dc8a2112b561a", + "sha256:ef43ee91c193f827e49599e824385ec7c7f3cd152d74cb1dfe02cb135f264d83", + "sha256:ef467d86d3cfde8b39ea1b35090208b0447caaabd38405420830f7fd85fbdd56", + "sha256:f89b28772fc2562ed9ad871c865f5320ef761a7fcc188a935e21fe8b31a38ca9", + "sha256:fddbab55a2473f1d3b8833ec6b7ac31e8211b0aa608df5ab09ce07f3727326de" + ], + "markers": "python_version >= '3.7'", + "version": "==6.1" } }, "develop": { @@ -836,6 +1153,7 @@ "sha256:bdfc739baa03b880c2d15d0431b31c658ffc348e907fe197e54e0389dd59e11e" ], "index": "pypi", + "markers": "python_version >= '3.7'", "version": "==1.7.5" }, "beautifulsoup4": { @@ -848,50 +1166,49 @@ }, "black": { "hashes": [ - "sha256:064101748afa12ad2291c2b91c960be28b817c0c7eaa35bec09cc63aa56493c5", - "sha256:0945e13506be58bf7db93ee5853243eb368ace1c08a24c65ce108986eac65915", - "sha256:11c410f71b876f961d1de77b9699ad19f939094c3a677323f43d7a29855fe326", - "sha256:1c7b8d606e728a41ea1ccbd7264677e494e87cf630e399262ced92d4a8dac940", - "sha256:1d06691f1eb8de91cd1b322f21e3bfc9efe0c7ca1f0e1eb1db44ea367dff656b", - "sha256:3238f2aacf827d18d26db07524e44741233ae09a584273aa059066d644ca7b30", - "sha256:32daa9783106c28815d05b724238e30718f34155653d4d6e125dc7daec8e260c", - "sha256:35d1381d7a22cc5b2be2f72c7dfdae4072a3336060635718cc7e1ede24221d6c", - "sha256:3a150542a204124ed00683f0db1f5cf1c2aaaa9cc3495b7a3b5976fb136090ab", - "sha256:48f9d345675bb7fbc3dd85821b12487e1b9a75242028adad0333ce36ed2a6d27", - "sha256:50cb33cac881766a5cd9913e10ff75b1e8eb71babf4c7104f2e9c52da1fb7de2", - "sha256:562bd3a70495facf56814293149e51aa1be9931567474993c7942ff7d3533961", - "sha256:67de8d0c209eb5b330cce2469503de11bca4085880d62f1628bd9972cc3366b9", - "sha256:6b39abdfb402002b8a7d030ccc85cf5afff64ee90fa4c5aebc531e3ad0175ddb", - "sha256:6f3c333ea1dd6771b2d3777482429864f8e258899f6ff05826c3a4fcc5ce3f70", - "sha256:714290490c18fb0126baa0fca0a54ee795f7502b44177e1ce7624ba1c00f2331", - "sha256:7c3eb7cea23904399866c55826b31c1f55bbcd3890ce22ff70466b907b6775c2", - "sha256:92c543f6854c28a3c7f39f4d9b7694f9a6eb9d3c5e2ece488c327b6e7ea9b266", - "sha256:a6f6886c9869d4daae2d1715ce34a19bbc4b95006d20ed785ca00fa03cba312d", - "sha256:a8a968125d0a6a404842fa1bf0b349a568634f856aa08ffaff40ae0dfa52e7c6", - "sha256:c7ab5790333c448903c4b721b59c0d80b11fe5e9803d8703e84dcb8da56fec1b", - "sha256:e114420bf26b90d4b9daa597351337762b63039752bdf72bf361364c1aa05925", - "sha256:e198cf27888ad6f4ff331ca1c48ffc038848ea9f031a3b40ba36aced7e22f2c8", - "sha256:ec751418022185b0c1bb7d7736e6933d40bbb14c14a0abcf9123d1b159f98dd4", - "sha256:f0bd2f4a58d6666500542b26354978218a9babcdc972722f4bf90779524515f3" + "sha256:031e8c69f3d3b09e1aa471a926a1eeb0b9071f80b17689a655f7885ac9325a6f", + "sha256:13a2e4a93bb8ca74a749b6974925c27219bb3df4d42fc45e948a5d9feb5122b7", + "sha256:13ef033794029b85dfea8032c9d3b92b42b526f1ff4bf13b2182ce4e917f5100", + "sha256:14f04c990259576acd093871e7e9b14918eb28f1866f91968ff5524293f9c573", + "sha256:24b6b3ff5c6d9ea08a8888f6977eae858e1f340d7260cf56d70a49823236b62d", + "sha256:403397c033adbc45c2bd41747da1f7fc7eaa44efbee256b53842470d4ac5a70f", + "sha256:50254ebfa56aa46a9fdd5d651f9637485068a1adf42270148cd101cdf56e0ad9", + "sha256:538efb451cd50f43aba394e9ec7ad55a37598faae3348d723b59ea8e91616300", + "sha256:638619a559280de0c2aa4d76f504891c9860bb8fa214267358f0a20f27c12948", + "sha256:6a3b50e4b93f43b34a9d3ef00d9b6728b4a722c997c99ab09102fd5efdb88325", + "sha256:6ccd59584cc834b6d127628713e4b6b968e5f79572da66284532525a042549f9", + "sha256:75a2dc41b183d4872d3a500d2b9c9016e67ed95738a3624f4751a0cb4818fe71", + "sha256:7d30ec46de88091e4316b17ae58bbbfc12b2de05e069030f6b747dfc649ad186", + "sha256:8431445bf62d2a914b541da7ab3e2b4f3bc052d2ccbf157ebad18ea126efb91f", + "sha256:8fc1ddcf83f996247505db6b715294eba56ea9372e107fd54963c7553f2b6dfe", + "sha256:a732b82747235e0542c03bf352c126052c0fbc458d8a239a94701175b17d4855", + "sha256:adc3e4442eef57f99b5590b245a328aad19c99552e0bdc7f0b04db6656debd80", + "sha256:c46767e8df1b7beefb0899c4a95fb43058fa8500b6db144f4ff3ca38eb2f6393", + "sha256:c619f063c2d68f19b2d7270f4cf3192cb81c9ec5bc5ba02df91471d0b88c4c5c", + "sha256:cf3a4d00e4cdb6734b64bf23cd4341421e8953615cba6b3670453737a72ec204", + "sha256:cf99f3de8b3273a8317681d8194ea222f10e0133a24a7548c73ce44ea1679377", + "sha256:d6bc09188020c9ac2555a498949401ab35bb6bf76d4e0f8ee251694664df6301" ], "index": "pypi", - "version": "==23.3.0" + "markers": "python_version >= '3.8'", + "version": "==23.9.1" }, "blinker": { "hashes": [ - "sha256:4afd3de66ef3a9f8067559fb7a1cbe555c17dcbe15971b05d1b625c3e7abe213", - "sha256:c3d739772abb7bc2860abf5f2ec284223d9ad5c76da018234f6f50d6f31ab1f0" + "sha256:152090d27c1c5c722ee7e48504b02d76502811ce02e1523553b4cf8c8b3d3a8d", + "sha256:296320d6c28b006eb5e32d4712202dbcdcbf5dc482da298c2f44881c43884aaa" ], "markers": "python_version >= '3.7'", - "version": "==1.6.2" + "version": "==1.6.3" }, "boto3": { "hashes": [ - "sha256:30f8ab1cf89d5864a80ba2d5eb5316dbd2a63c9469877e0cffb522630438aa85", - "sha256:77e8fa7c257f9ed8bfe0c3ffc2ccc47b1cfa27058f99415b6003699d1202e0c0" + "sha256:0dfa2fc96ccafce4feb23044d6cba8b25075ad428a0c450d369d099c6a1059d2", + "sha256:148eeba0f1867b3db5b3e5ae2997d75a94d03fad46171374a0819168c36f7ed0" ], "index": "pypi", - "version": "==1.26.145" + "markers": "python_version >= '3.7'", + "version": "==1.28.62" }, "boto3-mocking": { "hashes": [ @@ -899,55 +1216,59 @@ "sha256:d0273366d3cb86c5bf3d6f31d1c6e40c11b714d49b5f2d5125234d0d59aa9378" ], "index": "pypi", + "markers": "python_version >= '3.6'", "version": "==0.1.1" }, "boto3-stubs": { "hashes": [ - "sha256:9413cb395c803d5b85e9ec7b16fba855a613ecd78b2e0011e2f6b62cf0b4fc1e", - "sha256:be2007f92138781288c7a22eba30b7d60742466fc28edd04637b31fabee854a5" + "sha256:22a08e27d2ede1849dd0d75e8501099240b34bd70adb606584a2af2e12f3a22b", + "sha256:f5ae08d2abae7709fff3e7cacea66c41cb43236527cfaf3975e506c6c67439a0" ], "index": "pypi", - "version": "==1.26.145" + "markers": "python_version >= '3.7'", + "version": "==1.28.62" }, "botocore": { "hashes": [ - "sha256:264a3f19ed280d80711b7e278be09acff7ed379a96432fdf179b4e6e3a687e6a", - "sha256:65e2a2b1cc70583225f87d6d63736215f93c6234721967bdab872270ba7a1f45" + "sha256:272b78ac65256b6294cb9cdb0ac484d447ad3a85642e33cb6a3b1b8afee15a4c", + "sha256:be792d806afc064694a2d0b9b25779f3ca0c1584b29a35ac32e67f0064ddb8b7" ], "markers": "python_version >= '3.7'", - "version": "==1.29.145" + "version": "==1.31.62" }, "botocore-stubs": { "hashes": [ - "sha256:80ffab72ad428d20cb1cf538ee55fcd94f7d81315b77d84fec99e218c3974e8b", - "sha256:928c58a434dd83bef956e3b5bb1e96278fff5eee9f8b8ab08d916cef1e9a2014" + "sha256:2ce555e5dff2e91fc22bd67106534bf3e0593b838d87f8a49d3b8e87fa83a440", + "sha256:d30217d8f6a0888616a44c83150490c5fbc899550ffe1896a2cd15a2205fd648" ], "markers": "python_version >= '3.7' and python_version < '4.0'", - "version": "==1.29.145" + "version": "==1.31.62" }, "click": { "hashes": [ - "sha256:7682dc8afb30297001674575ea00d1814d808d6a36af415a82bd481d37ba7b8e", - "sha256:bb4d8133cb15a609f44e8213d9b391b0809795062913b383c62be0ee95b1db48" + "sha256:ae74fb96c20a0277a1d615f1e4d73c8414f5a98db8b799a7931d1582f3390c28", + "sha256:ca9853ad459e787e2192211578cc907e7594e294c7ccc834310722b41b9ca6de" ], "markers": "python_version >= '3.7'", - "version": "==8.1.3" + "version": "==8.1.7" }, "django": { "hashes": [ - "sha256:066b6debb5ac335458d2a713ed995570536c8b59a580005acb0732378d5eb1ee", - "sha256:7efa6b1f781a6119a10ac94b4794ded90db8accbe7802281cd26f8664ffed59c" + "sha256:08f41f468b63335aea0d904c5729e0250300f6a1907bf293a65499496cdbc68f", + "sha256:a64d2487cdb00ad7461434320ccc38e60af9c404773a2f95ab0093b4453a3215" ], "index": "pypi", - "version": "==4.2.1" + "markers": "python_version >= '3.8'", + "version": "==4.2.6" }, "django-debug-toolbar": { "hashes": [ - "sha256:a0b532ef5d52544fd745d1dcfc0557fa75f6f0d1962a8298bd568427ef2fa436", - "sha256:f57882e335593cb8e74c2bda9f1116bbb9ca8fc0d81b50a75ace0f83de5173c7" + "sha256:af99128c06e8e794479e65ab62cc6c7d1e74e1c19beb44dcbf9bad7a9c017327", + "sha256:bc7fdaafafcdedefcc67a4a5ad9dac96efd6e41db15bc74d402a54a2ba4854dc" ], "index": "pypi", - "version": "==4.1.0" + "markers": "python_version >= '3.8'", + "version": "==4.2.0" }, "django-model2puml": { "hashes": [ @@ -958,35 +1279,37 @@ }, "django-stubs": { "hashes": [ - "sha256:66477bdba25407623f4079205e58f3c7265a4f0d8f7c9f540a6edc16f8883a5b", - "sha256:8c15d5f7b05926805cfb25f2bfbf3509c37792fbd8aec5aedea358b85d8bccd5" + "sha256:7d4a132c381519815e865c27a89eca41bcbd06056832507224816a43d75c601c", + "sha256:834b60fd81510cce6b56c1c6c28bec3c504a418bc90ff7d0063fabe8ab9a7868" ], "index": "pypi", - "version": "==4.2.1" + "markers": "python_version >= '3.8'", + "version": "==4.2.4" }, "django-stubs-ext": { "hashes": [ - "sha256:2696d6f7d8538341b060cffa9565c72ea797e866687e040b86d29cad8799e5fe", - "sha256:4b6b63e49f4ba30d93ec46f87507648c99c9de6911e651ad69db7084fd5b2f4e" + "sha256:c69d1cc46f1c4c3b7894b685a5022c29b2a36c7cfb52e23762eaf357ebfc2c98", + "sha256:fdacc65a14d2d4b97334b58ff178a5853ec8c8c76cec406e417916ad67536ce4" ], "markers": "python_version >= '3.8'", - "version": "==4.2.1" + "version": "==4.2.2" }, "django-webtest": { "hashes": [ - "sha256:c8c32041791cdae468e443097c432c67cf17cad339e1ab88b01a6c4841ee4c74", - "sha256:ef075e98b38fe3836dc533c2924d3e37c6bb3483008c40567115518a0303b1af" + "sha256:9597d26ced599bc5d4d9366bb451469fc9707b4779f79543cdf401ae6c5aeb35", + "sha256:e29baf8337e7fe7db41ce63ca6661f7b5c77fe56f506f48b305e09313f5475b4" ], "index": "pypi", - "version": "==1.9.10" + "version": "==1.9.11" }, "flake8": { "hashes": [ - "sha256:3833794e27ff64ea4e9cf5d410082a8b97ff1a06c16aa3d2027339cd0f1195c7", - "sha256:c61007e76655af75e6785a931f452915b371dc48f56efd765247c8fe68f2b181" + "sha256:d5b3857f07c030bdb5bf41c7f53799571d75c4491748a3adcd47de929e34cd23", + "sha256:ffdfce58ea94c6580c77888a86506937f9a1a227dfcd15f245d694ae20a6b6e5" ], "index": "pypi", - "version": "==6.0.0" + "markers": "python_full_version >= '3.8.1'", + "version": "==6.1.0" }, "gitdb": { "hashes": [ @@ -998,11 +1321,11 @@ }, "gitpython": { "hashes": [ - "sha256:8ce3bcf69adfdf7c7d503e78fd3b1c492af782d58893b650adb2ac8912ddd573", - "sha256:f04893614f6aa713a60cbbe1e6a97403ef633103cdd0ef5eb6efe0deb98dbe8d" + "sha256:5f4c4187de49616d710a77e98ddf17b4782060a1788df441846bddefbb89ab33", + "sha256:f9b9ddc0761c125d5780eab2d64be4873fc6817c2899cbcb34b02344bdc7bc54" ], "markers": "python_version >= '3.7'", - "version": "==3.1.31" + "version": "==3.1.37" }, "jmespath": { "hashes": [ @@ -1014,11 +1337,11 @@ }, "markdown-it-py": { "hashes": [ - "sha256:5a35f8d1870171d9acc47b99612dc146129b631baf04970128b568f190d0cc30", - "sha256:7c9a5e412688bc771c67432cbfebcdd686c93ce6484913dccf06cb5a0bea35a1" + "sha256:355216845c60bd96232cd8d8c40e8f9765cc86f46880e43a8fd22dc1a1a8cab1", + "sha256:e3f60a94fa066dc52ec76661e37c851cb232d92f9886b15cb560aaada2df8feb" ], - "markers": "python_version >= '3.7'", - "version": "==2.2.0" + "markers": "python_version >= '3.8'", + "version": "==3.0.0" }, "mccabe": { "hashes": [ @@ -1038,35 +1361,37 @@ }, "mypy": { "hashes": [ - "sha256:1c4c42c60a8103ead4c1c060ac3cdd3ff01e18fddce6f1016e08939647a0e703", - "sha256:44797d031a41516fcf5cbfa652265bb994e53e51994c1bd649ffcd0c3a7eccbf", - "sha256:473117e310febe632ddf10e745a355714e771ffe534f06db40702775056614c4", - "sha256:4c99c3ecf223cf2952638da9cd82793d8f3c0c5fa8b6ae2b2d9ed1e1ff51ba85", - "sha256:550a8b3a19bb6589679a7c3c31f64312e7ff482a816c96e0cecec9ad3a7564dd", - "sha256:658fe7b674769a0770d4b26cb4d6f005e88a442fe82446f020be8e5f5efb2fae", - "sha256:6e33bb8b2613614a33dff70565f4c803f889ebd2f859466e42b46e1df76018dd", - "sha256:6e42d29e324cdda61daaec2336c42512e59c7c375340bd202efa1fe0f7b8f8ca", - "sha256:74bc9b6e0e79808bf8678d7678b2ae3736ea72d56eede3820bd3849823e7f305", - "sha256:76ec771e2342f1b558c36d49900dfe81d140361dd0d2df6cd71b3db1be155409", - "sha256:7d23370d2a6b7a71dc65d1266f9a34e4cde9e8e21511322415db4b26f46f6b8c", - "sha256:87df44954c31d86df96c8bd6e80dfcd773473e877ac6176a8e29898bfb3501cb", - "sha256:8c5979d0deb27e0f4479bee18ea0f83732a893e81b78e62e2dda3e7e518c92ee", - "sha256:95d8d31a7713510685b05fbb18d6ac287a56c8f6554d88c19e73f724a445448a", - "sha256:a22435632710a4fcf8acf86cbd0d69f68ac389a3892cb23fbad176d1cddaf228", - "sha256:a8763e72d5d9574d45ce5881962bc8e9046bf7b375b0abf031f3e6811732a897", - "sha256:c1eb485cea53f4f5284e5baf92902cd0088b24984f4209e25981cc359d64448d", - "sha256:c5d2cc54175bab47011b09688b418db71403aefad07cbcd62d44010543fc143f", - "sha256:cbc07246253b9e3d7d74c9ff948cd0fd7a71afcc2b77c7f0a59c26e9395cb152", - "sha256:d0b6c62206e04061e27009481cb0ec966f7d6172b5b936f3ead3d74f29fe3dcf", - "sha256:ddae0f39ca146972ff6bb4399f3b2943884a774b8771ea0a8f50e971f5ea5ba8", - "sha256:e1f4d16e296f5135624b34e8fb741eb0eadedca90862405b1f1fde2040b9bd11", - "sha256:e86c2c6852f62f8f2b24cb7a613ebe8e0c7dc1402c61d36a609174f63e0ff017", - "sha256:ebc95f8386314272bbc817026f8ce8f4f0d2ef7ae44f947c4664efac9adec929", - "sha256:f9dca1e257d4cc129517779226753dbefb4f2266c4eaad610fc15c6a7e14283e", - "sha256:faff86aa10c1aa4a10e1a301de160f3d8fc8703b88c7e98de46b531ff1276a9a" + "sha256:091f53ff88cb093dcc33c29eee522c087a438df65eb92acd371161c1f4380ff0", + "sha256:1a69db3018b87b3e6e9dd28970f983ea6c933800c9edf8c503c3135b3274d5ad", + "sha256:24f3de8b9e7021cd794ad9dfbf2e9fe3f069ff5e28cb57af6f873ffec1cb0425", + "sha256:31eba8a7a71f0071f55227a8057468b8d2eb5bf578c8502c7f01abaec8141b2f", + "sha256:3c8835a07b8442da900db47ccfda76c92c69c3a575872a5b764332c4bacb5a0a", + "sha256:3df87094028e52766b0a59a3e46481bb98b27986ed6ded6a6cc35ecc75bb9182", + "sha256:49499cf1e464f533fc45be54d20a6351a312f96ae7892d8e9f1708140e27ce41", + "sha256:4c192445899c69f07874dabda7e931b0cc811ea055bf82c1ababf358b9b2a72c", + "sha256:4f3d27537abde1be6d5f2c96c29a454da333a2a271ae7d5bc7110e6d4b7beb3f", + "sha256:7469545380dddce5719e3656b80bdfbb217cfe8dbb1438532d6abc754b828fed", + "sha256:7807a2a61e636af9ca247ba8494031fb060a0a744b9fee7de3a54bed8a753323", + "sha256:856bad61ebc7d21dbc019b719e98303dc6256cec6dcc9ebb0b214b81d6901bd8", + "sha256:89513ddfda06b5c8ebd64f026d20a61ef264e89125dc82633f3c34eeb50e7d60", + "sha256:8e0db37ac4ebb2fee7702767dfc1b773c7365731c22787cb99f507285014fcaf", + "sha256:971104bcb180e4fed0d7bd85504c9036346ab44b7416c75dd93b5c8c6bb7e28f", + "sha256:9e1589ca150a51d9d00bb839bfeca2f7a04f32cd62fad87a847bc0818e15d7dc", + "sha256:9f8464ed410ada641c29f5de3e6716cbdd4f460b31cf755b2af52f2d5ea79ead", + "sha256:ab98b8f6fdf669711f3abe83a745f67f50e3cbaea3998b90e8608d2b459fd566", + "sha256:b19006055dde8a5425baa5f3b57a19fa79df621606540493e5e893500148c72f", + "sha256:c69051274762cccd13498b568ed2430f8d22baa4b179911ad0c1577d336ed849", + "sha256:d2dad072e01764823d4b2f06bc7365bb1d4b6c2f38c4d42fade3c8d45b0b4b67", + "sha256:dccd850a2e3863891871c9e16c54c742dba5470f5120ffed8152956e9e0a5e13", + "sha256:e28d7b221898c401494f3b77db3bac78a03ad0a0fff29a950317d87885c655d2", + "sha256:e4b7a99275a61aa22256bab5839c35fe8a6887781862471df82afb4b445daae6", + "sha256:eb7ff4007865833c470a601498ba30462b7374342580e2346bf7884557e40531", + "sha256:f8598307150b5722854f035d2e70a1ad9cc3c72d392c34fffd8c66d888c90f17", + "sha256:fea451a3125bf0bfe716e5d7ad4b92033c471e4b5b3e154c67525539d14dc15a" ], "index": "pypi", - "version": "==1.3.0" + "markers": "python_version >= '3.8'", + "version": "==1.6.0" }, "mypy-extensions": { "hashes": [ @@ -1086,19 +1411,19 @@ }, "packaging": { "hashes": [ - "sha256:994793af429502c4ea2ebf6bf664629d07c1a9fe974af92966e4b8d2df7edc61", - "sha256:a392980d2b6cffa644431898be54b0045151319d1e7ec34f0cfed48767dd334f" + "sha256:048fb0e9405036518eaaf48a55953c750c11e1a1b68e0dd1a9d62ed0c092cfc5", + "sha256:8c491190033a9af7e1d931d0b5dacc2ef47509b34dd0de67ed209b5203fc88c7" ], "markers": "python_version >= '3.7'", - "version": "==23.1" + "version": "==23.2" }, "pathspec": { "hashes": [ - "sha256:2798de800fa92780e33acca925945e9a19a133b715067cf165b8866c15a31687", - "sha256:d8af70af76652554bd134c22b3e8a1cc46ed7d91edcdd721ef1a0c51a84a5293" + "sha256:1d6ed233af05e679efb96b1851550ea95bbb64b7c490b0f5aa52996c11e92a20", + "sha256:e0d8d0ac2f12da61956eb2306b69f9469b42f4deb0f3cb6ed47b9cce9996ced3" ], "markers": "python_version >= '3.7'", - "version": "==0.11.1" + "version": "==0.11.2" }, "pbr": { "hashes": [ @@ -1110,35 +1435,35 @@ }, "platformdirs": { "hashes": [ - "sha256:412dae91f52a6f84830f39a8078cecd0e866cb72294a5c66808e74d5e88d251f", - "sha256:e2378146f1964972c03c085bb5662ae80b2b8c06226c54b2ff4aa9483e8a13a5" + "sha256:cf8ee52a3afdb965072dcc652433e0c7e3e40cf5ea1477cd4b3b1d2eb75495b3", + "sha256:e9d171d00af68be50e9202731309c4e658fd8bc76f55c11c7dd760d023bda68e" ], "markers": "python_version >= '3.7'", - "version": "==3.5.1" + "version": "==3.11.0" }, "pycodestyle": { "hashes": [ - "sha256:347187bdb476329d98f695c213d7295a846d1152ff4fe9bacb8a9590b8ee7053", - "sha256:8a4eaf0d0495c7395bdab3589ac2db602797d76207242c17d470186815706610" + "sha256:259bcc17857d8a8b3b4a2327324b79e5f020a13c16074670f9c8c8f872ea76d0", + "sha256:5d1013ba8dc7895b548be5afb05740ca82454fd899971563d2ef625d090326f8" ], - "markers": "python_version >= '3.6'", - "version": "==2.10.0" + "markers": "python_version >= '3.8'", + "version": "==2.11.0" }, "pyflakes": { "hashes": [ - "sha256:ec55bf7fe21fff7f1ad2f7da62363d749e2a470500eab1b555334b67aa1ef8cf", - "sha256:ec8b276a6b60bd80defed25add7e439881c19e64850afd9b346283d4165fd0fd" + "sha256:4132f6d49cb4dae6819e5379898f2b8cce3c5f23994194c24b77d5da2e36f774", + "sha256:a0aae034c444db0071aa077972ba4768d40c830d9539fd45bf4cd3f8f6992efc" ], - "markers": "python_version >= '3.6'", - "version": "==3.0.1" + "markers": "python_version >= '3.8'", + "version": "==3.1.0" }, "pygments": { "hashes": [ - "sha256:8ace4d3c1dd481894b2005f560ead0f9f19ee64fe983366be1a21e171d12775c", - "sha256:db2db3deb4b4179f399a09054b023b6a586b76499d36965813c71aa8ed7b5fd1" + "sha256:13fc09fa63bc8d8671a6d247e1eb303c4b343eaee81d861f3404db2935653692", + "sha256:1daff0494820c69bc8941e407aa20f577374ee88364ee10a98fdbe0aece96e29" ], "markers": "python_version >= '3.7'", - "version": "==2.15.1" + "version": "==2.16.1" }, "python-dateutil": { "hashes": [ @@ -1150,65 +1475,75 @@ }, "pyyaml": { "hashes": [ - "sha256:01b45c0191e6d66c470b6cf1b9531a771a83c1c4208272ead47a3ae4f2f603bf", - "sha256:0283c35a6a9fbf047493e3a0ce8d79ef5030852c51e9d911a27badfde0605293", - "sha256:055d937d65826939cb044fc8c9b08889e8c743fdc6a32b33e2390f66013e449b", - "sha256:07751360502caac1c067a8132d150cf3d61339af5691fe9e87803040dbc5db57", - "sha256:0b4624f379dab24d3725ffde76559cff63d9ec94e1736b556dacdfebe5ab6d4b", - "sha256:0ce82d761c532fe4ec3f87fc45688bdd3a4c1dc5e0b4a19814b9009a29baefd4", - "sha256:1e4747bc279b4f613a09eb64bba2ba602d8a6664c6ce6396a4d0cd413a50ce07", - "sha256:213c60cd50106436cc818accf5baa1aba61c0189ff610f64f4a3e8c6726218ba", - "sha256:231710d57adfd809ef5d34183b8ed1eeae3f76459c18fb4a0b373ad56bedcdd9", - "sha256:277a0ef2981ca40581a47093e9e2d13b3f1fbbeffae064c1d21bfceba2030287", - "sha256:2cd5df3de48857ed0544b34e2d40e9fac445930039f3cfe4bcc592a1f836d513", - "sha256:40527857252b61eacd1d9af500c3337ba8deb8fc298940291486c465c8b46ec0", - "sha256:432557aa2c09802be39460360ddffd48156e30721f5e8d917f01d31694216782", - "sha256:473f9edb243cb1935ab5a084eb238d842fb8f404ed2193a915d1784b5a6b5fc0", - "sha256:48c346915c114f5fdb3ead70312bd042a953a8ce5c7106d5bfb1a5254e47da92", - "sha256:50602afada6d6cbfad699b0c7bb50d5ccffa7e46a3d738092afddc1f9758427f", - "sha256:68fb519c14306fec9720a2a5b45bc9f0c8d1b9c72adf45c37baedfcd949c35a2", - "sha256:77f396e6ef4c73fdc33a9157446466f1cff553d979bd00ecb64385760c6babdc", - "sha256:81957921f441d50af23654aa6c5e5eaf9b06aba7f0a19c18a538dc7ef291c5a1", - "sha256:819b3830a1543db06c4d4b865e70ded25be52a2e0631ccd2f6a47a2822f2fd7c", - "sha256:897b80890765f037df3403d22bab41627ca8811ae55e9a722fd0392850ec4d86", - "sha256:98c4d36e99714e55cfbaaee6dd5badbc9a1ec339ebfc3b1f52e293aee6bb71a4", - "sha256:9df7ed3b3d2e0ecfe09e14741b857df43adb5a3ddadc919a2d94fbdf78fea53c", - "sha256:9fa600030013c4de8165339db93d182b9431076eb98eb40ee068700c9c813e34", - "sha256:a80a78046a72361de73f8f395f1f1e49f956c6be882eed58505a15f3e430962b", - "sha256:afa17f5bc4d1b10afd4466fd3a44dc0e245382deca5b3c353d8b757f9e3ecb8d", - "sha256:b3d267842bf12586ba6c734f89d1f5b871df0273157918b0ccefa29deb05c21c", - "sha256:b5b9eccad747aabaaffbc6064800670f0c297e52c12754eb1d976c57e4f74dcb", - "sha256:bfaef573a63ba8923503d27530362590ff4f576c626d86a9fed95822a8255fd7", - "sha256:c5687b8d43cf58545ade1fe3e055f70eac7a5a1a0bf42824308d868289a95737", - "sha256:cba8c411ef271aa037d7357a2bc8f9ee8b58b9965831d9e51baf703280dc73d3", - "sha256:d15a181d1ecd0d4270dc32edb46f7cb7733c7c508857278d3d378d14d606db2d", - "sha256:d4b0ba9512519522b118090257be113b9468d804b19d63c71dbcf4a48fa32358", - "sha256:d4db7c7aef085872ef65a8fd7d6d09a14ae91f691dec3e87ee5ee0539d516f53", - "sha256:d4eccecf9adf6fbcc6861a38015c2a64f38b9d94838ac1810a9023a0609e1b78", - "sha256:d67d839ede4ed1b28a4e8909735fc992a923cdb84e618544973d7dfc71540803", - "sha256:daf496c58a8c52083df09b80c860005194014c3698698d1a57cbcfa182142a3a", - "sha256:dbad0e9d368bb989f4515da330b88a057617d16b6a8245084f1b05400f24609f", - "sha256:e61ceaab6f49fb8bdfaa0f92c4b57bcfbea54c09277b1b4f7ac376bfb7a7c174", - "sha256:f84fbc98b019fef2ee9a1cb3ce93e3187a6df0b2538a651bfb890254ba9f90b5" + "sha256:04ac92ad1925b2cff1db0cfebffb6ffc43457495c9b3c39d3fcae417d7125dc5", + "sha256:062582fca9fabdd2c8b54a3ef1c978d786e0f6b3a1510e0ac93ef59e0ddae2bc", + "sha256:0d3304d8c0adc42be59c5f8a4d9e3d7379e6955ad754aa9d6ab7a398b59dd1df", + "sha256:1635fd110e8d85d55237ab316b5b011de701ea0f29d07611174a1b42f1444741", + "sha256:184c5108a2aca3c5b3d3bf9395d50893a7ab82a38004c8f61c258d4428e80206", + "sha256:18aeb1bf9a78867dc38b259769503436b7c72f7a1f1f4c93ff9a17de54319b27", + "sha256:1d4c7e777c441b20e32f52bd377e0c409713e8bb1386e1099c2415f26e479595", + "sha256:1e2722cc9fbb45d9b87631ac70924c11d3a401b2d7f410cc0e3bbf249f2dca62", + "sha256:1fe35611261b29bd1de0070f0b2f47cb6ff71fa6595c077e42bd0c419fa27b98", + "sha256:28c119d996beec18c05208a8bd78cbe4007878c6dd15091efb73a30e90539696", + "sha256:326c013efe8048858a6d312ddd31d56e468118ad4cdeda36c719bf5bb6192290", + "sha256:40df9b996c2b73138957fe23a16a4f0ba614f4c0efce1e9406a184b6d07fa3a9", + "sha256:42f8152b8dbc4fe7d96729ec2b99c7097d656dc1213a3229ca5383f973a5ed6d", + "sha256:49a183be227561de579b4a36efbb21b3eab9651dd81b1858589f796549873dd6", + "sha256:4fb147e7a67ef577a588a0e2c17b6db51dda102c71de36f8549b6816a96e1867", + "sha256:50550eb667afee136e9a77d6dc71ae76a44df8b3e51e41b77f6de2932bfe0f47", + "sha256:510c9deebc5c0225e8c96813043e62b680ba2f9c50a08d3724c7f28a747d1486", + "sha256:5773183b6446b2c99bb77e77595dd486303b4faab2b086e7b17bc6bef28865f6", + "sha256:596106435fa6ad000c2991a98fa58eeb8656ef2325d7e158344fb33864ed87e3", + "sha256:6965a7bc3cf88e5a1c3bd2e0b5c22f8d677dc88a455344035f03399034eb3007", + "sha256:69b023b2b4daa7548bcfbd4aa3da05b3a74b772db9e23b982788168117739938", + "sha256:6c22bec3fbe2524cde73d7ada88f6566758a8f7227bfbf93a408a9d86bcc12a0", + "sha256:704219a11b772aea0d8ecd7058d0082713c3562b4e271b849ad7dc4a5c90c13c", + "sha256:7e07cbde391ba96ab58e532ff4803f79c4129397514e1413a7dc761ccd755735", + "sha256:81e0b275a9ecc9c0c0c07b4b90ba548307583c125f54d5b6946cfee6360c733d", + "sha256:855fb52b0dc35af121542a76b9a84f8d1cd886ea97c84703eaa6d88e37a2ad28", + "sha256:8d4e9c88387b0f5c7d5f281e55304de64cf7f9c0021a3525bd3b1c542da3b0e4", + "sha256:9046c58c4395dff28dd494285c82ba00b546adfc7ef001486fbf0324bc174fba", + "sha256:9eb6caa9a297fc2c2fb8862bc5370d0303ddba53ba97e71f08023b6cd73d16a8", + "sha256:a0cd17c15d3bb3fa06978b4e8958dcdc6e0174ccea823003a106c7d4d7899ac5", + "sha256:afd7e57eddb1a54f0f1a974bc4391af8bcce0b444685d936840f125cf046d5bd", + "sha256:b1275ad35a5d18c62a7220633c913e1b42d44b46ee12554e5fd39c70a243d6a3", + "sha256:b786eecbdf8499b9ca1d697215862083bd6d2a99965554781d0d8d1ad31e13a0", + "sha256:ba336e390cd8e4d1739f42dfe9bb83a3cc2e80f567d8805e11b46f4a943f5515", + "sha256:baa90d3f661d43131ca170712d903e6295d1f7a0f595074f151c0aed377c9b9c", + "sha256:bc1bf2925a1ecd43da378f4db9e4f799775d6367bdb94671027b73b393a7c42c", + "sha256:bd4af7373a854424dabd882decdc5579653d7868b8fb26dc7d0e99f823aa5924", + "sha256:bf07ee2fef7014951eeb99f56f39c9bb4af143d8aa3c21b1677805985307da34", + "sha256:bfdf460b1736c775f2ba9f6a92bca30bc2095067b8a9d77876d1fad6cc3b4a43", + "sha256:c8098ddcc2a85b61647b2590f825f3db38891662cfc2fc776415143f599bb859", + "sha256:d2b04aac4d386b172d5b9692e2d2da8de7bfb6c387fa4f801fbf6fb2e6ba4673", + "sha256:d483d2cdf104e7c9fa60c544d92981f12ad66a457afae824d146093b8c294c54", + "sha256:d858aa552c999bc8a8d57426ed01e40bef403cd8ccdd0fc5f6f04a00414cac2a", + "sha256:e7d73685e87afe9f3b36c799222440d6cf362062f78be1013661b00c5c6f678b", + "sha256:f003ed9ad21d6a4713f0a9b5a7a0a79e08dd0f221aff4525a2be4c346ee60aab", + "sha256:f22ac1c3cac4dbc50079e965eba2c1058622631e526bd9afd45fedd49ba781fa", + "sha256:faca3bdcf85b2fc05d06ff3fbc1f83e1391b3e724afa3feba7d13eeab355484c", + "sha256:fca0e3a251908a499833aa292323f32437106001d436eca0e6e7833256674585", + "sha256:fd1592b3fdf65fff2ad0004b5e363300ef59ced41c2e6b3a99d4089fa8c5435d", + "sha256:fd66fc5d0da6d9815ba2cebeb4205f95818ff4b79c3ebe268e75d961704af52f" ], "markers": "python_version >= '3.6'", - "version": "==6.0" + "version": "==6.0.1" }, "rich": { "hashes": [ - "sha256:76f6b65ea7e5c5d924ba80e322231d7cb5b5981aa60bfc1e694f1bc097fe6fe1", - "sha256:d204aadb50b936bf6b1a695385429d192bc1fdaf3e8b907e8e26f4c4e4b5bf75" + "sha256:2b38e2fe9ca72c9a00170a1a2d20c63c790d0e10ef1fe35eba76e1e7b1d7d245", + "sha256:5c14d22737e6d5084ef4771b62d5d4363165b403455a30a1c8ca39dc7b644bef" ], "markers": "python_full_version >= '3.7.0'", - "version": "==13.4.1" + "version": "==13.6.0" }, "s3transfer": { "hashes": [ - "sha256:3c0da2d074bf35d6870ef157158641178a4204a6e689e82546083e31e0311346", - "sha256:640bb492711f4c0c0905e1f62b6aaeb771881935ad27884852411f8e9cacbca9" + "sha256:10d6923c6359175f264811ef4bf6161a3156ce8e350e705396a7557d6293c33a", + "sha256:fd3889a66f5fe17299fe75b82eae6cf722554edca744ca5d5fe308b104883d2e" ], "markers": "python_version >= '3.7'", - "version": "==0.6.1" + "version": "==0.7.0" }, "six": { "hashes": [ @@ -1220,19 +1555,19 @@ }, "smmap": { "hashes": [ - "sha256:2aba19d6a040e78d8b09de5c57e96207b09ed71d8e55ce0959eeee6c8e190d94", - "sha256:c840e62059cd3be204b0c9c9f74be2c09d5648eddd4580d9314c3ecde0b30936" + "sha256:dceeb6c0028fdb6734471eb07c0cd2aae706ccaecab45965ee83f11c8d3b1f62", + "sha256:e6d8668fa5f93e706934a62d7b4db19c8d9eb8cf2adbb75ef1b675aa332b69da" ], - "markers": "python_version >= '3.6'", - "version": "==5.0.0" + "markers": "python_version >= '3.7'", + "version": "==5.0.1" }, "soupsieve": { "hashes": [ - "sha256:1c1bfee6819544a3447586c889157365a27e10d88cde3ad3da0cf0ddf646feb8", - "sha256:89d12b2d5dfcd2c9e8c22326da9d9aa9cb3dfab0a83a024f05704076ee8d35ea" + "sha256:5663d5a7b3bfaeee0bc4372e7fc48f9cff4940b3eec54a6451cc5299f1097690", + "sha256:eaa337ff55a1579b6549dc679565eac1e3d000563bcb1c8ab0d0fefbc0c2cdc7" ], - "markers": "python_version >= '3.7'", - "version": "==2.4.1" + "markers": "python_version >= '3.8'", + "version": "==2.5" }, "sqlparse": { "hashes": [ @@ -1260,72 +1595,67 @@ }, "types-awscrt": { "hashes": [ - "sha256:50fe7610aa40550a23d79d6167b2b8536281f038f42846eda0e520d6e3e01787", - "sha256:763d8d543f145d51cd16ea407d079608b126941d24525e16cb1de31d949ff563" + "sha256:477a14565909312fe1de70d0b301548e83c038f436b8a1d7c83729e87cdd0b85", + "sha256:d8c379420ba75b1e43687d12b0b772a5bb17f352859a2bef6aa8f0abde123f55" ], "markers": "python_version >= '3.7' and python_version < '4.0'", - "version": "==0.16.19" + "version": "==0.19.2" }, "types-cachetools": { "hashes": [ - "sha256:67fa46d51a650896770aee0ba80f0e61dc4a7d1373198eec1bc0622263eaa256", - "sha256:c0c5fa00199017d974c935bf043c467d5204e4f835141e489b48765b5ac1d960" + "sha256:595f0342d246c8ba534f5a762cf4c2f60ecb61e8002b8b2277fd5cf791d4e851", + "sha256:f7f8a25bfe306f2e6bc2ad0a2f949d9e72f2d91036d509c36d3810bf728bc6e1" ], "index": "pypi", - "version": "==5.3.0.5" + "version": "==5.3.0.6" }, "types-pytz": { "hashes": [ - "sha256:4fc2a7fbbc315f0b6630e0b899fd6c743705abe1094d007b0e612d10da15e0f3", - "sha256:ecdc70d543aaf3616a7e48631543a884f74205f284cefd6649ddf44c6a820aac" + "sha256:1999a123a3dc0e39a2ef6d19f3f8584211de9e6a77fe7a0259f04a524e90a5cf", + "sha256:cc23d0192cd49c8f6bba44ee0c81e4586a8f30204970fc0894d209a6b08dab9a" ], - "version": "==2023.3.0.0" + "version": "==2023.3.1.1" }, "types-pyyaml": { "hashes": [ - "sha256:662fa444963eff9b68120d70cda1af5a5f2aa57900003c2006d7626450eaae5f", - "sha256:ebab3d0700b946553724ae6ca636ea932c1b0868701d4af121630e78d695fc97" + "sha256:334373d392fde0fdf95af5c3f1661885fa10c52167b14593eb856289e1855062", + "sha256:c05bc6c158facb0676674b7f11fe3960db4f389718e19e62bd2b84d6205cfd24" ], - "version": "==6.0.12.10" + "version": "==6.0.12.12" }, "types-requests": { "hashes": [ - "sha256:3de667cffa123ce698591de0ad7db034a5317457a596eb0b4944e5a9d9e8d1ac", - "sha256:afb06ef8f25ba83d59a1d424bd7a5a939082f94b94e90ab5e6116bd2559deaa3" + "sha256:39894cbca3fb3d032ed8bdd02275b4273471aa5668564617cc1734b0a65ffdf8", + "sha256:e1b325c687b3494a2f528ab06e411d7092cc546cc9245c000bacc2fca5ae96d4" ], "index": "pypi", - "version": "==2.31.0.1" + "markers": "python_version >= '3.7'", + "version": "==2.31.0.8" }, "types-s3transfer": { "hashes": [ - "sha256:6d1ac1dedac750d570428362acdf60fdd4f277b0788855c3894d3226756b2bfb", - "sha256:75ac1d7143d58c1e6af467cfd4a96c67ee058a3adf7c249d9309999e1f5f41e4" + "sha256:aca0f2486d0a3a5037cd5b8f3e20a4522a29579a8dd183281ff0aa1c4e2c8aa7", + "sha256:ae9ed9273465d9f43da8b96307383da410c6b59c3b2464c88d20b578768e97c6" ], "markers": "python_version >= '3.7' and python_version < '4.0'", - "version": "==0.6.1" - }, - "types-urllib3": { - "hashes": [ - "sha256:3300538c9dc11dad32eae4827ac313f5d986b8b21494801f1bf97a1ac6c03ae5", - "sha256:5dbd1d2bef14efee43f5318b5d36d805a489f6600252bb53626d4bfafd95e27c" - ], - "version": "==1.26.25.13" + "version": "==0.7.0" }, "typing-extensions": { "hashes": [ - "sha256:88a4153d8505aabbb4e13aacb7c486c2b4a33ca3b3f807914a9b4c844c471c26", - "sha256:d91d5919357fe7f681a9f2b5b4cb2a5f1ef0a1e9f59c4d8ff0d3491e05c0ffd5" + "sha256:8f92fc8806f9a6b641eaa5318da32b44d401efaac0f6678c9bc448ba3605faa0", + "sha256:df8e4339e9cb77357558cbdbceca33c303714cf861d1eef15e1070055ae8b7ef" ], "index": "pypi", - "version": "==4.6.3" + "markers": "python_version >= '3.8'", + "version": "==4.8.0" }, "urllib3": { "hashes": [ - "sha256:8d36afa7616d8ab714608411b4a3b13e58f463aee519024578e062e141dce20f", - "sha256:8f135f6502756bde6b2a9b28989df5fbe87c9970cecaa69041edcce7f0589b14" + "sha256:7a7c7003b000adf9e7ca2a377c9688bbc54ed41b985789ed576570342a375cd2", + "sha256:b19e1a85d206b56d7df1d5e683df4a7725252a964e3993648dd0fb5a1c157564" ], - "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3, 3.4, 3.5'", - "version": "==1.26.16" + "markers": "python_version >= '3.7'", + "version": "==2.0.6" }, "waitress": { "hashes": [ diff --git a/src/epplibwrapper/client.py b/src/epplibwrapper/client.py index 0234ef6c6..a9ea30ecb 100644 --- a/src/epplibwrapper/client.py +++ b/src/epplibwrapper/client.py @@ -3,6 +3,8 @@ import logging from time import sleep +from epplibwrapper.utility.pool import EppConnectionPool + try: from epplib.client import Client from epplib import commands @@ -63,13 +65,22 @@ class EPPLibWrapper: # prepare a context manager which will connect and login when invoked # (it will also logout and disconnect when the context manager exits) self._connect = Socket(self._client, self._login) + options = { + # Pool size + "size": 10, + # Which errors the pool should look out for + "exc_classes": (LoginError, RegistryError,), + # Should we ping the connection on occassion to keep it alive? + "keep_alive": None, + } + self._pool = EppConnectionPool(client=self._client, login=self._login, options=options) def _send(self, command): """Helper function used by `send`.""" try: cmd_type = command.__class__.__name__ - with self._connect as wire: - response = wire.send(command) + with self._pool.get() as connection: + response = connection.send(command) except (ValueError, ParsingError) as err: message = "%s failed to execute due to some syntax error." logger.warning(message, cmd_type, exc_info=True) diff --git a/src/epplibwrapper/socket.py b/src/epplibwrapper/socket.py index 5c9acce79..703ac9538 100644 --- a/src/epplibwrapper/socket.py +++ b/src/epplibwrapper/socket.py @@ -20,6 +20,14 @@ class Socket: 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.""" self.client.connect() response = self.client.send(self.login) @@ -27,11 +35,22 @@ class Socket: self.client.close() raise LoginError(response.msg) return self.client - - def __exit__(self, *args, **kwargs): + + def disconnect(self): """Close the connection.""" try: self.client.send(commands.Logout()) self.client.close() except Exception: logger.warning("Connection to registry was not cleanly closed.") + + def send(self, command): + logger.debug(f"command is this: {command}") + response = self.client.send(command) + # TODO - add some validation + """ + if response.code >= 2000: + self.client.close() + raise LoginError(response.msg) + """ + return response \ No newline at end of file diff --git a/src/epplibwrapper/utility/pool.py b/src/epplibwrapper/utility/pool.py new file mode 100644 index 000000000..6773bc312 --- /dev/null +++ b/src/epplibwrapper/utility/pool.py @@ -0,0 +1,25 @@ +from geventconnpool import ConnectionPool +from epplibwrapper.socket import Socket + +class EppConnectionPool(ConnectionPool): + def __init__(self, client, login, options): + # For storing shared credentials + self._client = client + self._login = login + super().__init__(**options) + + def _new_connection(self): + socket = self.create_socket(self._client, self._login) + try: + connection = socket.connect() + return connection + except Exception as err: + raise err + + def _keepalive(self, connection): + pass + + def create_socket(self, client, login) -> Socket: + """Creates and returns a socket instance""" + socket = Socket(client, login) + return socket \ No newline at end of file diff --git a/src/requirements.txt b/src/requirements.txt index ae6ed90df..8617b6472 100644 --- a/src/requirements.txt +++ b/src/requirements.txt @@ -1,53 +1,61 @@ -i https://pypi.python.org/simple -asgiref==3.7.2 ; python_version >= '3.7' -boto3==1.26.145 -botocore==1.29.145 ; python_version >= '3.7' -cachetools==5.3.1 -certifi==2023.7.22 ; python_version >= '3.6' +annotated-types==0.6.0; python_version >= '3.8' +asgiref==3.7.2; python_version >= '3.7' +boto3==1.28.62; python_version >= '3.7' +botocore==1.31.62; python_version >= '3.7' +cachetools==5.3.1; python_version >= '3.7' +certifi==2023.7.22; python_version >= '3.6' cfenv==0.5.3 -cffi==1.15.1 -charset-normalizer==3.1.0 ; python_full_version >= '3.7.0' -cryptography==41.0.4 ; 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' -dj-database-url==2.0.0 +cffi==1.16.0; python_version >= '3.8' +charset-normalizer==3.3.0; python_full_version >= '3.7.0' +cryptography==41.0.4; 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' +dj-database-url==2.1.0 dj-email-url==1.0.6 -django==4.2.3 -django-allow-cidr==0.6.0 -django-auditlog==2.3.0 +django==4.2.6; python_version >= '3.8' +django-allow-cidr==0.7.1 +django-auditlog==2.3.0; python_version >= '3.7' django-cache-url==3.4.4 django-csp==3.7 django-fsm==2.8.1 django-login-required-middleware==0.9.0 -django-phonenumber-field[phonenumberslite]==7.1.0 -django-widget-tweaks==1.4.12 -environs[django]==9.5.0 -faker==18.10.0 -git+https://github.com/cisagov/epplib.git@d56d183f1664f34c40ca9716a3a9a345f0ef561c#egg=fred-epplib +django-phonenumber-field[phonenumberslite]==7.2.0; python_version >= '3.8' +django-widget-tweaks==1.5.0; python_version >= '3.8' +environs[django]==9.5.0; python_version >= '3.6' +faker==19.9.0; python_version >= '3.8' +fred-epplib@ git+https://github.com/cisagov/epplib.git@d56d183f1664f34c40ca9716a3a9a345f0ef561c 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' -gunicorn==20.1.0 -idna==3.4 ; python_version >= '3.5' -jmespath==1.0.1 ; python_version >= '3.7' -lxml==4.9.2 ; python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3, 3.4' -mako==1.2.4 ; python_version >= '3.7' -markupsafe==2.1.2 ; python_version >= '3.7' -marshmallow==3.19.0 ; python_version >= '3.7' -oic==1.6.0 +future==0.18.3; 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' +greenlet==3.0.0; python_version < '3.11' and platform_python_implementation == 'CPython' +gsocketpool==0.1.6 +gunicorn==21.2.0; python_version >= '3.5' +idna==3.4; python_version >= '3.5' +jmespath==1.0.1; python_version >= '3.7' +lxml==4.9.3; python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3, 3.4' +mako==1.2.4; python_version >= '3.7' +markupsafe==2.1.3; python_version >= '3.7' +marshmallow==3.20.1; python_version >= '3.8' +oic==1.6.1; python_version ~= '3.7' orderedmultidict==1.0.1 -packaging==23.1 ; python_version >= '3.7' -phonenumberslite==8.13.13 -psycopg2-binary==2.9.6 +packaging==23.2; python_version >= '3.7' +phonenumberslite==8.13.22 +psycopg2-binary==2.9.9; python_version >= '3.7' pycparser==2.21 -pycryptodomex==3.18.0 -pydantic==1.10.8 ; python_version >= '3.7' +pycryptodomex==3.19.0; python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3, 3.4' +pydantic==2.4.2; python_version >= '3.7' +pydantic-core==2.10.1; python_version >= '3.7' +pydantic-settings==2.0.3; python_version >= '3.7' 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-dotenv==1.0.0 ; python_version >= '3.8' -requests==2.31.0 -s3transfer==0.6.1 ; python_version >= '3.7' -setuptools==67.8.0 ; python_version >= '3.7' -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' -typing-extensions==4.6.3 -urllib3==1.26.17 ; python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3, 3.4, 3.5' -whitenoise==6.4.0 +python-dateutil==2.8.2; python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3' +python-dotenv==1.0.0; python_version >= '3.8' +requests==2.31.0; python_version >= '3.7' +s3transfer==0.7.0; python_version >= '3.7' +setuptools==68.2.2; 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' +sqlparse==0.4.4; python_version >= '3.5' +typing-extensions==4.8.0; python_version >= '3.8' +urllib3==2.0.6; python_version >= '3.7' +whitenoise==6.6.0; python_version >= '3.8' +zope.event==5.0; python_version >= '3.7' +zope.interface==6.1; python_version >= '3.7' From f22d72da4de9b67dcc91f097d42e37d95a34f783 Mon Sep 17 00:00:00 2001 From: zandercymatics <141044360+zandercymatics@users.noreply.github.com> Date: Wed, 11 Oct 2023 13:48:37 -0600 Subject: [PATCH 003/120] Imports --- src/Pipfile | 2 +- src/Pipfile.lock | 19 ++++++++----------- src/epplibwrapper/client.py | 2 +- src/requirements.txt | 4 ++-- 4 files changed, 12 insertions(+), 15 deletions(-) diff --git a/src/Pipfile b/src/Pipfile index 5f84ac448..8864248c1 100644 --- a/src/Pipfile +++ b/src/Pipfile @@ -26,7 +26,7 @@ boto3 = "*" typing-extensions ='*' django-login-required-middleware = "*" fred-epplib = {git = "https://github.com/cisagov/epplib.git", ref = "master"} -gsocketpool = "*" +geventconnpool = {git = "https://github.com/zandercymatics/geventconnpool.git", ref = "master"} [dev-packages] django-debug-toolbar = "*" diff --git a/src/Pipfile.lock b/src/Pipfile.lock index e01528f1d..6124b7096 100644 --- a/src/Pipfile.lock +++ b/src/Pipfile.lock @@ -1,7 +1,7 @@ { "_meta": { "hash": { - "sha256": "49cd54bd0c272b04889898edc62b2a314d9675409d862a93e257b3f79e09f84e" + "sha256": "f9d7900daf9ca6d77fc9fe29c79b7620040cdaa50a34401bb2f84cef0862189e" }, "pipfile-spec": 6, "requires": {}, @@ -366,11 +366,11 @@ }, "faker": { "hashes": [ - "sha256:85468e16d4a9a8712bfdb98ba55aaf17c60658266a76958d099aee6a18c0a6c5", - "sha256:d75401c631a991b32d3595f26250f42c007cc32653ac3e522b626f3d80770571" + "sha256:63da90512d0cb3acdb71bd833bb3071cb8a196020d08b8567a01d232954f1820", + "sha256:f321e657ed61616fbfe14dbb9ccc6b2e8282652bbcfcb503c1bd0231ff834df6" ], "markers": "python_version >= '3.8'", - "version": "==19.9.0" + "version": "==19.10.0" }, "fred-epplib": { "git": "https://github.com/cisagov/epplib.git", @@ -436,6 +436,10 @@ "markers": "python_version >= '3.8'", "version": "==23.9.1" }, + "geventconnpool": { + "git": "https://github.com/zandercymatics/geventconnpool.git", + "ref": "e4f349117875fa70b1034b89c3bc7caafac6a9b4" + }, "greenlet": { "hashes": [ "sha256:02a807b2a58d5cdebb07050efe3d7deaf915468d112dfcf5e426d0564aa3aa4a", @@ -504,13 +508,6 @@ "markers": "python_version < '3.11' and platform_python_implementation == 'CPython'", "version": "==3.0.0" }, - "gsocketpool": { - "hashes": [ - "sha256:f2e2749aceadce6b27ca52e2b0a64af99797746a8681e1a2963f72007c14cb14" - ], - "index": "pypi", - "version": "==0.1.6" - }, "gunicorn": { "hashes": [ "sha256:3213aa5e8c24949e792bcacfc176fef362e7aac80b76c56f6b5122bf350722f0", diff --git a/src/epplibwrapper/client.py b/src/epplibwrapper/client.py index a9ea30ecb..fc5455f84 100644 --- a/src/epplibwrapper/client.py +++ b/src/epplibwrapper/client.py @@ -71,7 +71,7 @@ class EPPLibWrapper: # Which errors the pool should look out for "exc_classes": (LoginError, RegistryError,), # Should we ping the connection on occassion to keep it alive? - "keep_alive": None, + "keepalive": None, } self._pool = EppConnectionPool(client=self._client, login=self._login, options=options) diff --git a/src/requirements.txt b/src/requirements.txt index 8617b6472..2c76aca02 100644 --- a/src/requirements.txt +++ b/src/requirements.txt @@ -22,13 +22,13 @@ django-login-required-middleware==0.9.0 django-phonenumber-field[phonenumberslite]==7.2.0; python_version >= '3.8' django-widget-tweaks==1.5.0; python_version >= '3.8' environs[django]==9.5.0; python_version >= '3.6' -faker==19.9.0; python_version >= '3.8' +faker==19.10.0; python_version >= '3.8' fred-epplib@ git+https://github.com/cisagov/epplib.git@d56d183f1664f34c40ca9716a3a9a345f0ef561c 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' gevent==23.9.1; python_version >= '3.8' +geventconnpool@ git+https://github.com/zandercymatics/geventconnpool.git@e4f349117875fa70b1034b89c3bc7caafac6a9b4 greenlet==3.0.0; python_version < '3.11' and platform_python_implementation == 'CPython' -gsocketpool==0.1.6 gunicorn==21.2.0; python_version >= '3.5' idna==3.4; python_version >= '3.5' jmespath==1.0.1; python_version >= '3.7' From a02d27aaecfc9126f55813064468dcbe86a843ca Mon Sep 17 00:00:00 2001 From: zandercymatics <141044360+zandercymatics@users.noreply.github.com> Date: Wed, 11 Oct 2023 14:31:13 -0600 Subject: [PATCH 004/120] More specific error returns --- src/epplibwrapper/client.py | 5 +---- src/epplibwrapper/utility/pool.py | 11 +++++++++-- 2 files changed, 10 insertions(+), 6 deletions(-) diff --git a/src/epplibwrapper/client.py b/src/epplibwrapper/client.py index fc5455f84..bd1740ec9 100644 --- a/src/epplibwrapper/client.py +++ b/src/epplibwrapper/client.py @@ -62,15 +62,12 @@ class EPPLibWrapper: password=settings.SECRET_REGISTRY_KEY_PASSPHRASE, ) ) - # prepare a context manager which will connect and login when invoked - # (it will also logout and disconnect when the context manager exits) - self._connect = Socket(self._client, self._login) options = { # Pool size "size": 10, # Which errors the pool should look out for "exc_classes": (LoginError, RegistryError,), - # Should we ping the connection on occassion to keep it alive? + # Should we ping the connection on occasion to keep it alive? "keepalive": None, } self._pool = EppConnectionPool(client=self._client, login=self._login, options=options) diff --git a/src/epplibwrapper/utility/pool.py b/src/epplibwrapper/utility/pool.py index 6773bc312..6d5ab5d58 100644 --- a/src/epplibwrapper/utility/pool.py +++ b/src/epplibwrapper/utility/pool.py @@ -1,6 +1,11 @@ +import logging from geventconnpool import ConnectionPool +from epplibwrapper import RegistryError +from epplibwrapper.errors import LoginError from epplibwrapper.socket import Socket +logger = logging.getLogger(__name__) + class EppConnectionPool(ConnectionPool): def __init__(self, client, login, options): # For storing shared credentials @@ -13,8 +18,10 @@ class EppConnectionPool(ConnectionPool): try: connection = socket.connect() return connection - except Exception as err: - raise err + except LoginError as err: + message = "_new_connection failed to execute due to a registry login error." + logger.warning(message, exc_info=True) + raise RegistryError(message) from err def _keepalive(self, connection): pass From 3a28a3a3625f526b7872d25aec3f237a17e2716a Mon Sep 17 00:00:00 2001 From: zandercymatics <141044360+zandercymatics@users.noreply.github.com> Date: Thu, 12 Oct 2023 10:16:22 -0600 Subject: [PATCH 005/120] Add settings for pooling, keep_alive --- src/epplibwrapper/client.py | 7 +++---- src/epplibwrapper/utility/pool.py | 20 +++++++++++++++----- src/registrar/config/settings.py | 10 ++++++++++ 3 files changed, 28 insertions(+), 9 deletions(-) diff --git a/src/epplibwrapper/client.py b/src/epplibwrapper/client.py index bd1740ec9..8cdb85fad 100644 --- a/src/epplibwrapper/client.py +++ b/src/epplibwrapper/client.py @@ -17,7 +17,6 @@ from django.conf import settings from .cert import Cert, Key from .errors import LoginError, RegistryError -from .socket import Socket logger = logging.getLogger(__name__) @@ -64,11 +63,11 @@ class EPPLibWrapper: ) options = { # Pool size - "size": 10, + "size": settings.EPP_CONNECTION_POOL_SIZE, # Which errors the pool should look out for "exc_classes": (LoginError, RegistryError,), - # Should we ping the connection on occasion to keep it alive? - "keepalive": None, + # Occasionally pings the registry to keep the connection alive + "keepalive": settings.POOL_KEEP_ALIVE, } self._pool = EppConnectionPool(client=self._client, login=self._login, options=options) diff --git a/src/epplibwrapper/utility/pool.py b/src/epplibwrapper/utility/pool.py index 6d5ab5d58..2ad9f82c2 100644 --- a/src/epplibwrapper/utility/pool.py +++ b/src/epplibwrapper/utility/pool.py @@ -1,9 +1,13 @@ import logging from geventconnpool import ConnectionPool -from epplibwrapper import RegistryError -from epplibwrapper.errors import LoginError +from epplibwrapper.errors import RegistryError, LoginError from epplibwrapper.socket import Socket +try: + from epplib.commands import Hello +except ImportError: + pass + logger = logging.getLogger(__name__) class EppConnectionPool(ConnectionPool): @@ -20,11 +24,17 @@ class EppConnectionPool(ConnectionPool): return connection except LoginError as err: message = "_new_connection failed to execute due to a registry login error." - logger.warning(message, exc_info=True) + logger.error(message, exc_info=True) raise RegistryError(message) from err - def _keepalive(self, connection): - pass + def _keepalive(self, c): + """Sends a command to the server to keep the connection alive.""" + try: + # Sends a ping to EPPLib + c.send(Hello()) + except Exception as err: + logger.error("Failed to keep the connection alive.", exc_info=True) + raise RegistryError("Failed to keep the connection alive.") from err def create_socket(self, client, login) -> Socket: """Creates and returns a socket instance""" diff --git a/src/registrar/config/settings.py b/src/registrar/config/settings.py index ceb215a4d..ee3b496bf 100644 --- a/src/registrar/config/settings.py +++ b/src/registrar/config/settings.py @@ -534,6 +534,16 @@ SECRET_REGISTRY_KEY = secret_registry_key SECRET_REGISTRY_KEY_PASSPHRASE = secret_registry_key_passphrase SECRET_REGISTRY_HOSTNAME = secret_registry_hostname +# endregion +# region: Registry Connection Pool----------------------------------------------------------### + +# 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! +EPP_CONNECTION_POOL_SIZE = 10 + +# Determines if we should ping open connections +POOL_KEEP_ALIVE = True + # endregion # region: Security and Privacy----------------------------------------------### From 542554959ea0089f65b512c1c95d2ed03bde19d4 Mon Sep 17 00:00:00 2001 From: zandercymatics <141044360+zandercymatics@users.noreply.github.com> Date: Thu, 12 Oct 2023 10:21:56 -0600 Subject: [PATCH 006/120] Run black linter --- src/epplibwrapper/client.py | 11 ++++++++--- src/epplibwrapper/socket.py | 6 +++--- src/epplibwrapper/utility/pool.py | 5 +++-- 3 files changed, 14 insertions(+), 8 deletions(-) diff --git a/src/epplibwrapper/client.py b/src/epplibwrapper/client.py index 8cdb85fad..66d3d696c 100644 --- a/src/epplibwrapper/client.py +++ b/src/epplibwrapper/client.py @@ -65,11 +65,16 @@ class EPPLibWrapper: # Pool size "size": settings.EPP_CONNECTION_POOL_SIZE, # Which errors the pool should look out for - "exc_classes": (LoginError, RegistryError,), - # Occasionally pings the registry to keep the connection alive + "exc_classes": ( + LoginError, + RegistryError, + ), + # Occasionally pings the registry to keep the connection alive "keepalive": settings.POOL_KEEP_ALIVE, } - self._pool = EppConnectionPool(client=self._client, login=self._login, options=options) + self._pool = EppConnectionPool( + client=self._client, login=self._login, options=options + ) def _send(self, command): """Helper function used by `send`.""" diff --git a/src/epplibwrapper/socket.py b/src/epplibwrapper/socket.py index 703ac9538..d66589495 100644 --- a/src/epplibwrapper/socket.py +++ b/src/epplibwrapper/socket.py @@ -35,7 +35,7 @@ class Socket: self.client.close() raise LoginError(response.msg) return self.client - + def disconnect(self): """Close the connection.""" try: @@ -43,7 +43,7 @@ class Socket: self.client.close() except Exception: logger.warning("Connection to registry was not cleanly closed.") - + def send(self, command): logger.debug(f"command is this: {command}") response = self.client.send(command) @@ -53,4 +53,4 @@ class Socket: self.client.close() raise LoginError(response.msg) """ - return response \ No newline at end of file + return response diff --git a/src/epplibwrapper/utility/pool.py b/src/epplibwrapper/utility/pool.py index 2ad9f82c2..6682f3bf6 100644 --- a/src/epplibwrapper/utility/pool.py +++ b/src/epplibwrapper/utility/pool.py @@ -10,6 +10,7 @@ except ImportError: logger = logging.getLogger(__name__) + class EppConnectionPool(ConnectionPool): def __init__(self, client, login, options): # For storing shared credentials @@ -35,8 +36,8 @@ class EppConnectionPool(ConnectionPool): except Exception as err: logger.error("Failed to keep the connection alive.", exc_info=True) raise RegistryError("Failed to keep the connection alive.") from err - + def create_socket(self, client, login) -> Socket: """Creates and returns a socket instance""" socket = Socket(client, login) - return socket \ No newline at end of file + return socket From 94a85aeea1a75f8f60ab3c475df2239057458eb5 Mon Sep 17 00:00:00 2001 From: zandercymatics <141044360+zandercymatics@users.noreply.github.com> Date: Thu, 12 Oct 2023 10:27:23 -0600 Subject: [PATCH 007/120] Linter changes --- src/registrar/config/settings.py | 3 --- 1 file changed, 3 deletions(-) diff --git a/src/registrar/config/settings.py b/src/registrar/config/settings.py index ee3b496bf..e153e32d5 100644 --- a/src/registrar/config/settings.py +++ b/src/registrar/config/settings.py @@ -534,9 +534,6 @@ SECRET_REGISTRY_KEY = secret_registry_key SECRET_REGISTRY_KEY_PASSPHRASE = secret_registry_key_passphrase SECRET_REGISTRY_HOSTNAME = secret_registry_hostname -# endregion -# region: Registry Connection Pool----------------------------------------------------------### - # 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! EPP_CONNECTION_POOL_SIZE = 10 From aeb3c73d3d649e3a116f9a5cd6761d440d71998a Mon Sep 17 00:00:00 2001 From: zandercymatics <141044360+zandercymatics@users.noreply.github.com> Date: Thu, 12 Oct 2023 10:35:06 -0600 Subject: [PATCH 008/120] Increase alive interval --- src/registrar/config/settings.py | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/src/registrar/config/settings.py b/src/registrar/config/settings.py index e153e32d5..0097c1f84 100644 --- a/src/registrar/config/settings.py +++ b/src/registrar/config/settings.py @@ -538,8 +538,9 @@ SECRET_REGISTRY_HOSTNAME = secret_registry_hostname # WARNING: Setting this value too high could cause frequent app crashes! EPP_CONNECTION_POOL_SIZE = 10 -# Determines if we should ping open connections -POOL_KEEP_ALIVE = True +# Determines the interval in which we ping open connections in seconds +# Calculated as POOL_KEEP_ALIVE / EPP_CONNECTION_POOL_SIZE +POOL_KEEP_ALIVE = 600 # endregion # region: Security and Privacy----------------------------------------------### From a82f3c5ce963dbca01a1a1151902e7fd05981fb1 Mon Sep 17 00:00:00 2001 From: zandercymatics <141044360+zandercymatics@users.noreply.github.com> Date: Thu, 12 Oct 2023 10:48:48 -0600 Subject: [PATCH 009/120] Disable keep_alive --- src/registrar/config/settings.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/registrar/config/settings.py b/src/registrar/config/settings.py index 0097c1f84..cbde49ba2 100644 --- a/src/registrar/config/settings.py +++ b/src/registrar/config/settings.py @@ -540,7 +540,7 @@ EPP_CONNECTION_POOL_SIZE = 10 # Determines the interval in which we ping open connections in seconds # Calculated as POOL_KEEP_ALIVE / EPP_CONNECTION_POOL_SIZE -POOL_KEEP_ALIVE = 600 +POOL_KEEP_ALIVE = None # endregion # region: Security and Privacy----------------------------------------------### From c4d2950ac957f675a7f853df7763c3e306922d6c Mon Sep 17 00:00:00 2001 From: zandercymatics <141044360+zandercymatics@users.noreply.github.com> Date: Thu, 12 Oct 2023 12:07:05 -0600 Subject: [PATCH 010/120] Fix pool locally, cleanup --- src/epplibwrapper/client.py | 28 +++++++++++++++++----------- src/epplibwrapper/socket.py | 5 +---- 2 files changed, 18 insertions(+), 15 deletions(-) diff --git a/src/epplibwrapper/client.py b/src/epplibwrapper/client.py index 66d3d696c..f43ee41e4 100644 --- a/src/epplibwrapper/client.py +++ b/src/epplibwrapper/client.py @@ -3,8 +3,6 @@ import logging from time import sleep -from epplibwrapper.utility.pool import EppConnectionPool - try: from epplib.client import Client from epplib import commands @@ -17,6 +15,7 @@ from django.conf import settings from .cert import Cert, Key from .errors import LoginError, RegistryError +from .utility.pool import EppConnectionPool logger = logging.getLogger(__name__) @@ -78,25 +77,32 @@ class EPPLibWrapper: def _send(self, command): """Helper function used by `send`.""" + cmd_type = command.__class__.__name__ try: - cmd_type = command.__class__.__name__ + # We won't have an EPP connection locally, + # shortcut this and raise an err + # TODO - implement a timeout in _pool.get() + if settings.DEBUG: + raise LoginError with self._pool.get() as connection: response = connection.send(command) except (ValueError, ParsingError) as err: - message = "%s failed to execute due to some syntax error." - logger.warning(message, cmd_type, exc_info=True) + message = f"{cmd_type} failed to execute due to some syntax error." + logger.warning(message, exc_info=True) raise RegistryError(message) from err except TransportError as err: - message = "%s failed to execute due to a connection error." - logger.warning(message, cmd_type, exc_info=True) + message = f"{cmd_type} failed to execute due to a connection error." + logger.warning(message, exc_info=True) raise RegistryError(message) from err except LoginError as err: - message = "%s failed to execute due to a registry login error." - logger.warning(message, cmd_type, exc_info=True) + # For linter + text = "failed to execute due to a registry login error." + message = f"{cmd_type} {text}" + logger.warning(message, exc_info=True) raise RegistryError(message) from err except Exception as err: - message = "%s failed to execute due to an unknown error." % err - logger.warning(message, cmd_type, exc_info=True) + message = f"{cmd_type} failed to execute due to an unknown error." + logger.warning(message, exc_info=True) raise RegistryError(message) from err else: if response.code >= 2000: diff --git a/src/epplibwrapper/socket.py b/src/epplibwrapper/socket.py index d66589495..33db76a73 100644 --- a/src/epplibwrapper/socket.py +++ b/src/epplibwrapper/socket.py @@ -45,12 +45,9 @@ class Socket: logger.warning("Connection to registry was not cleanly closed.") def send(self, command): - logger.debug(f"command is this: {command}") response = self.client.send(command) - # TODO - add some validation - """ if response.code >= 2000: self.client.close() raise LoginError(response.msg) - """ + return response From 003db40e58d73a205d027fd8674b89a716370708 Mon Sep 17 00:00:00 2001 From: Rachid Mrad Date: Thu, 12 Oct 2023 15:32:10 -0400 Subject: [PATCH 011/120] use shortened names for orgs in the choicefield which satisfies the admin requirement, replace short name with a long name in the user facing app in the form, summary page, manage app page --- src/registrar/admin.py | 4 +- src/registrar/forms/application_wizard.py | 3 +- ...napplication_organization_type_and_more.py | 52 +++++++++++++++++ src/registrar/models/domain_application.py | 56 ++++++++++++++----- src/registrar/models/domain_information.py | 1 + .../templates/application_review.html | 9 ++- .../templates/application_status.html | 6 +- src/registrar/templatetags/custom_filters.py | 15 +++++ src/registrar/tests/test_admin.py | 17 ++++++ src/registrar/tests/test_views.py | 28 +++++++++- 10 files changed, 171 insertions(+), 20 deletions(-) create mode 100644 src/registrar/migrations/0038_alter_domainapplication_organization_type_and_more.py diff --git a/src/registrar/admin.py b/src/registrar/admin.py index 174500f28..aef56e0b3 100644 --- a/src/registrar/admin.py +++ b/src/registrar/admin.py @@ -219,9 +219,9 @@ class MyUserAdmin(BaseUserAdmin): # (which should in theory be the ONLY group) def group(self, obj): if obj.groups.filter(name="full_access_group").exists(): - return "Full access" + return "full_access_group" elif obj.groups.filter(name="cisa_analysts_group").exists(): - return "Analyst" + return "cisa_analysts_group" return "" def get_list_display(self, request): diff --git a/src/registrar/forms/application_wizard.py b/src/registrar/forms/application_wizard.py index 516683247..2fd78cdd8 100644 --- a/src/registrar/forms/application_wizard.py +++ b/src/registrar/forms/application_wizard.py @@ -153,7 +153,8 @@ class RegistrarFormSet(forms.BaseFormSet): class OrganizationTypeForm(RegistrarForm): organization_type = forms.ChoiceField( - choices=DomainApplication.OrganizationChoices.choices, + # use the long names in the application form + choices=DomainApplication.OrganizationChoicesVerbose.choices, widget=forms.RadioSelect, error_messages={"required": "Select the type of organization you represent."}, ) diff --git a/src/registrar/migrations/0038_alter_domainapplication_organization_type_and_more.py b/src/registrar/migrations/0038_alter_domainapplication_organization_type_and_more.py new file mode 100644 index 000000000..bdbca82d8 --- /dev/null +++ b/src/registrar/migrations/0038_alter_domainapplication_organization_type_and_more.py @@ -0,0 +1,52 @@ +# Generated by Django 4.2.1 on 2023-10-12 19:30 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + dependencies = [ + ("registrar", "0037_create_groups_v01"), + ] + + operations = [ + migrations.AlterField( + model_name="domainapplication", + name="organization_type", + field=models.CharField( + blank=True, + choices=[ + ("federal", "Federal"), + ("interstate", "Interstate"), + ("state_or_territory", "State or territory"), + ("tribal", "Tribal"), + ("county", "County"), + ("city", "City"), + ("special_district", "Special district"), + ("school_district", "School district"), + ], + help_text="Type of organization", + max_length=255, + null=True, + ), + ), + migrations.AlterField( + model_name="domaininformation", + name="organization_type", + field=models.CharField( + blank=True, + choices=[ + ("federal", "Federal"), + ("interstate", "Interstate"), + ("state_or_territory", "State or territory"), + ("tribal", "Tribal"), + ("county", "County"), + ("city", "City"), + ("special_district", "Special district"), + ("school_district", "School district"), + ], + help_text="Type of Organization", + max_length=255, + null=True, + ), + ), + ] diff --git a/src/registrar/models/domain_application.py b/src/registrar/models/domain_application.py index 68429d381..a4752aa88 100644 --- a/src/registrar/models/domain_application.py +++ b/src/registrar/models/domain_application.py @@ -105,28 +105,57 @@ class DomainApplication(TimeStampedModel): ARMED_FORCES_AP = "AP", "Armed Forces Pacific (AP)" class OrganizationChoices(models.TextChoices): + + """ + Primary organization choices: + For use in django admin + Keys need to match OrganizationChoicesVerbose + """ + + FEDERAL = "federal", "Federal" + INTERSTATE = "interstate", "Interstate" + STATE_OR_TERRITORY = "state_or_territory", "State or territory" + TRIBAL = "tribal", "Tribal" + COUNTY = "county", "County" + CITY = "city", "City" + SPECIAL_DISTRICT = "special_district", "Special district" + SCHOOL_DISTRICT = "school_district", "School district" + + class OrganizationChoicesVerbose(models.TextChoices): + + """ + Secondary organization choices + For use in the application form and on the templates + Keys need to match OrganizationChoices + """ + FEDERAL = ( "federal", - "Federal: an agency of the U.S. government's executive, legislative, " - "or judicial branches", + "Federal: an agency of the U.S. government's executive, " + "legislative, or judicial branches", ) INTERSTATE = "interstate", "Interstate: an organization of two or more states" - STATE_OR_TERRITORY = "state_or_territory", ( - "State or territory: one of the 50 U.S. states, the District of " - "Columbia, American Samoa, Guam, Northern Mariana Islands, " - "Puerto Rico, or the U.S. Virgin Islands" + STATE_OR_TERRITORY = ( + "state_or_territory", + "State or territory: one of the 50 U.S. states, the District of Columbia, " + "American Samoa, Guam, Northern Mariana Islands, Puerto Rico, or the U.S. " + "Virgin Islands", ) - TRIBAL = "tribal", ( - "Tribal: a tribal government recognized by the federal or " - "a state government" + TRIBAL = ( + "tribal", + "Tribal: a tribal government recognized by the federal or a state " + "government", ) COUNTY = "county", "County: a county, parish, or borough" CITY = "city", "City: a city, town, township, village, etc." - SPECIAL_DISTRICT = "special_district", ( - "Special district: an independent organization within a single state" + SPECIAL_DISTRICT = ( + "special_district", + "Special district: an independent organization within a single state", ) - SCHOOL_DISTRICT = "school_district", ( - "School district: a school district that is not part of a local government" + SCHOOL_DISTRICT = ( + "school_district", + "School district: a school district that is not part of a local " + "government", ) class BranchChoices(models.TextChoices): @@ -297,6 +326,7 @@ class DomainApplication(TimeStampedModel): # ##### data fields from the initial form ##### organization_type = models.CharField( max_length=255, + # use the short names in Django admin choices=OrganizationChoices.choices, null=True, blank=True, diff --git a/src/registrar/models/domain_information.py b/src/registrar/models/domain_information.py index 3b93aff48..d2bc5c53d 100644 --- a/src/registrar/models/domain_information.py +++ b/src/registrar/models/domain_information.py @@ -21,6 +21,7 @@ class DomainInformation(TimeStampedModel): StateTerritoryChoices = DomainApplication.StateTerritoryChoices + # use the short names in Django admin OrganizationChoices = DomainApplication.OrganizationChoices BranchChoices = DomainApplication.BranchChoices diff --git a/src/registrar/templates/application_review.html b/src/registrar/templates/application_review.html index be81303b8..6a4dcbffd 100644 --- a/src/registrar/templates/application_review.html +++ b/src/registrar/templates/application_review.html @@ -1,5 +1,6 @@ {% extends 'application_form.html' %} {% load static url_helpers %} +{% load custom_filters %} {% block form_required_fields_help_text %} {# there are no required fields on this page so don't show this #} @@ -26,7 +27,13 @@
{{ form_titles|get_item:step }}
{% if step == Step.ORGANIZATION_TYPE %} - {{ application.get_organization_type_display|default:"Incomplete" }} + {% if application.organization_type is not None %} + {% with long_org_type=application.organization_type|get_organization_long_name %} + {{ long_org_type }} + {% endwith %} + {% else %} + Incomplete + {% endif %} {% endif %} {% if step == Step.TRIBAL_GOVERNMENT %} {{ application.tribe_name|default:"Incomplete" }} diff --git a/src/registrar/templates/application_status.html b/src/registrar/templates/application_status.html index a68c07c8a..79d0f7ff9 100644 --- a/src/registrar/templates/application_status.html +++ b/src/registrar/templates/application_status.html @@ -1,5 +1,7 @@ {% extends 'base.html' %} +{% load custom_filters %} + {% block title %}Domain request status | {{ domainapplication.requested_domain.name }} | {% endblock %} {% load static url_helpers %} @@ -50,7 +52,9 @@

Summary of your domain request

{% with heading_level='h3' %} - {% include "includes/summary_item.html" with title='Type of organization' value=domainapplication.get_organization_type_display heading_level=heading_level %} + {% with long_org_type=domainapplication.organization_type|get_organization_long_name %} + {% include "includes/summary_item.html" with title='Type of organization' value=long_org_type heading_level=heading_level %} + {% endwith %} {% if domainapplication.tribe_name %} {% include "includes/summary_item.html" with title='Tribal government' value=domainapplication.tribe_name heading_level=heading_level %} diff --git a/src/registrar/templatetags/custom_filters.py b/src/registrar/templatetags/custom_filters.py index 3614db18e..e90c3166d 100644 --- a/src/registrar/templatetags/custom_filters.py +++ b/src/registrar/templatetags/custom_filters.py @@ -1,5 +1,6 @@ from django import template import re +from registrar.models.domain_application import DomainApplication register = template.Library() @@ -48,3 +49,17 @@ def contains_checkbox(html_list): if re.search(r']*type="checkbox"', html_string): return True return False + + +@register.filter +def get_organization_long_name(organization_type): + organization_choices_dict = {} + + for name, value in DomainApplication.OrganizationChoicesVerbose.choices: + organization_choices_dict[name] = value + + long_form_type = organization_choices_dict[organization_type] + if long_form_type is not None: + return long_form_type + + return "Error" diff --git a/src/registrar/tests/test_admin.py b/src/registrar/tests/test_admin.py index 51ace34f7..b5827d3e9 100644 --- a/src/registrar/tests/test_admin.py +++ b/src/registrar/tests/test_admin.py @@ -300,6 +300,23 @@ class TestDomainApplicationAdmin(MockEppLib): self.superuser = create_superuser() self.staffuser = create_user() + def test_short_org_name_in_applications_list(self): + """ + Make sure the short name is displaying in admin on the list page + """ + self.client.force_login(self.superuser) + completed_application() + response = self.client.get("/admin/registrar/domainapplication/") + # There are 3 template references to Federal (3) plus one reference in the table + # for our actual application + self.assertContains(response, "Federal", count=4) + # This may be a bit more robust + self.assertContains( + response, 'Federal', count=1 + ) + # Now let's make sure the long description does not exist + self.assertNotContains(response, "Federal: an agency of the U.S. government") + @boto3_mocking.patching def test_save_model_sends_submitted_email(self): # make sure there is no user with this email diff --git a/src/registrar/tests/test_views.py b/src/registrar/tests/test_views.py index 2194b42db..32a22916e 100644 --- a/src/registrar/tests/test_views.py +++ b/src/registrar/tests/test_views.py @@ -141,9 +141,12 @@ class DomainApplicationTests(TestWithUser, WebTest): @boto3_mocking.patching def test_application_form_submission(self): - """Can fill out the entire form and submit. + """ + Can fill out the entire form and submit. As we add additional form pages, we need to include them here to make this test work. + + This test also looks for the long organization name on the summary page. """ num_pages_tested = 0 # elections, type_of_work, tribal_government, no_other_contacts @@ -427,7 +430,8 @@ class DomainApplicationTests(TestWithUser, WebTest): review_form = review_page.form # Review page contains all the previously entered data - self.assertContains(review_page, "Federal") + # Let's make sure the long org name is displayed + self.assertContains(review_page, "Federal: an agency of the U.S. government") self.assertContains(review_page, "Executive") self.assertContains(review_page, "Testorg") self.assertContains(review_page, "address 1") @@ -1065,6 +1069,26 @@ class DomainApplicationTests(TestWithUser, WebTest): # page = self.app.get(url) # self.assertNotContains(page, "VALUE") + def test_long_org_name_in_application(self): + """ + Make sure the long name is displaying in the application form, + org step + """ + request = self.app.get(reverse("application:")).follow() + self.assertContains(request, "Federal: an agency of the U.S. government") + + def test_long_org_name_in_application_manage(self): + """ + Make sure the long name is displaying in the application summary + page (manage your application) + """ + completed_application(status=DomainApplication.SUBMITTED, user=self.user) + home_page = self.app.get("/") + self.assertContains(home_page, "city.gov") + # click the "Edit" link + detail_page = home_page.click("Manage") + self.assertContains(detail_page, "Federal: an agency of the U.S. government") + class TestWithDomainPermissions(TestWithUser): def setUp(self): From 7b13dd4b3a68e973e0d6a204a3ff3398dff47737 Mon Sep 17 00:00:00 2001 From: Rachid Mrad Date: Thu, 12 Oct 2023 16:14:51 -0400 Subject: [PATCH 012/120] trigger PR pipeline --- src/registrar/models/domain_application.py | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/src/registrar/models/domain_application.py b/src/registrar/models/domain_application.py index a4752aa88..4da32ad18 100644 --- a/src/registrar/models/domain_application.py +++ b/src/registrar/models/domain_application.py @@ -107,9 +107,9 @@ class DomainApplication(TimeStampedModel): class OrganizationChoices(models.TextChoices): """ - Primary organization choices: - For use in django admin - Keys need to match OrganizationChoicesVerbose + Primary organization choices: + For use in django admin + Keys need to match OrganizationChoicesVerbose """ FEDERAL = "federal", "Federal" @@ -124,9 +124,9 @@ class DomainApplication(TimeStampedModel): class OrganizationChoicesVerbose(models.TextChoices): """ - Secondary organization choices - For use in the application form and on the templates - Keys need to match OrganizationChoices + Secondary organization choices + For use in the application form and on the templates + Keys need to match OrganizationChoices """ FEDERAL = ( From dfec8c200e61da5560dd77a915ee2443e6960dd5 Mon Sep 17 00:00:00 2001 From: zandercymatics <141044360+zandercymatics@users.noreply.github.com> Date: Thu, 12 Oct 2023 14:38:49 -0600 Subject: [PATCH 013/120] Check if registry connection can be made --- src/epplibwrapper/client.py | 33 +++++++++++++++++++++++++-------- src/epplibwrapper/socket.py | 32 +++++++++++++++++++++++++++++--- 2 files changed, 54 insertions(+), 11 deletions(-) diff --git a/src/epplibwrapper/client.py b/src/epplibwrapper/client.py index f43ee41e4..1bbda80e7 100644 --- a/src/epplibwrapper/client.py +++ b/src/epplibwrapper/client.py @@ -15,6 +15,7 @@ from django.conf import settings from .cert import Cert, Key from .errors import LoginError, RegistryError +from .socket import Socket from .utility.pool import EppConnectionPool logger = logging.getLogger(__name__) @@ -41,7 +42,6 @@ class EPPLibWrapper: def __init__(self) -> None: """Initialize settings which will be used for all connections.""" - # prepare (but do not send) a Login command self._login = commands.Login( cl_id=settings.SECRET_REGISTRY_CL_ID, @@ -51,6 +51,7 @@ class EPPLibWrapper: "urn:ietf:params:xml:ns:contact-1.0", ], ) + # establish a client object with a TCP socket transport self._client = Client( SocketTransport( @@ -71,19 +72,23 @@ class EPPLibWrapper: # Occasionally pings the registry to keep the connection alive "keepalive": settings.POOL_KEEP_ALIVE, } - self._pool = EppConnectionPool( - client=self._client, login=self._login, options=options - ) + + self._pool = None + if not settings.DEBUG or self._test_registry_connection_success(): + self._pool = EppConnectionPool( + client=self._client, login=self._login, options=options + ) + else: + logger.warning("Cannot contact the Registry") + # TODO - signal that the app may need to restart? def _send(self, command): """Helper function used by `send`.""" cmd_type = command.__class__.__name__ try: - # We won't have an EPP connection locally, - # shortcut this and raise an err - # TODO - implement a timeout in _pool.get() - if settings.DEBUG: + if self._pool is None: raise LoginError + # TODO - add a timeout with self._pool.get() as connection: response = connection.send(command) except (ValueError, ParsingError) as err: @@ -127,6 +132,18 @@ class EPPLibWrapper: else: # don't try again raise err + def _test_registry_connection_success(self): + """Check that determines if our login + credentials are valid, and/or if the Registrar + can be contacted + """ + socket = Socket(self._login, self._client) + 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: # Initialize epplib diff --git a/src/epplibwrapper/socket.py b/src/epplibwrapper/socket.py index 33db76a73..d25d823f1 100644 --- a/src/epplibwrapper/socket.py +++ b/src/epplibwrapper/socket.py @@ -1,7 +1,9 @@ import logging +from time import sleep try: from epplib import commands + from epplib.client import Client except ImportError: pass @@ -14,7 +16,7 @@ logger = logging.getLogger(__name__) class Socket: """Context manager which establishes a TCP connection with registry.""" - def __init__(self, client, login) -> None: + def __init__(self, client: commands.Login, login: Client) -> None: """Save the epplib client and login details.""" self.client = client self.login = login @@ -27,15 +29,39 @@ class Socket: """Runs disconnect(), which closes a connection with EPPLib.""" self.disconnect() - def connect(self): + def connect(self, pass_response_only=False): """Use epplib to connect.""" self.client.connect() response = self.client.send(self.login) - if response.code >= 2000: + if self.is_login_error(response.code): self.client.close() raise LoginError(response.msg) return self.client + def is_login_error(self, code): + return code >= 2000 + + def test_connection_success(self): + """Tests if a successful connection can be made with the registry""" + # Something went wrong if this doesn't exist + if not hasattr(self.client, "connect"): + return False + + counter = 0 # we'll try 3 times + while True: + try: + self.client.connect() + response = self.client.send(self.login) + except LoginError as err: + if err.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: + self.disconnect() + return not self.is_login_error(response.code) + def disconnect(self): """Close the connection.""" try: From fa6ed6f31842490b666b04606a994b661a27c7b4 Mon Sep 17 00:00:00 2001 From: zandercymatics <141044360+zandercymatics@users.noreply.github.com> Date: Thu, 12 Oct 2023 14:55:01 -0600 Subject: [PATCH 014/120] Test fix for security scanner --- src/epplibwrapper/__init__.py | 3 +++ src/epplibwrapper/client.py | 2 ++ src/epplibwrapper/socket.py | 3 ++- 3 files changed, 7 insertions(+), 1 deletion(-) diff --git a/src/epplibwrapper/__init__.py b/src/epplibwrapper/__init__.py index dd6664a3a..5b3bdc55c 100644 --- a/src/epplibwrapper/__init__.py +++ b/src/epplibwrapper/__init__.py @@ -45,6 +45,9 @@ except NameError: # Attn: these imports should NOT be at the top of the file try: from .client import CLIENT, commands +except ImportError: + pass +try: from .errors import RegistryError, ErrorCode from epplib.models import common, info from epplib.responses import extensions diff --git a/src/epplibwrapper/client.py b/src/epplibwrapper/client.py index 1bbda80e7..3426dd486 100644 --- a/src/epplibwrapper/client.py +++ b/src/epplibwrapper/client.py @@ -74,6 +74,8 @@ class EPPLibWrapper: } self._pool = None + # 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 settings.DEBUG or self._test_registry_connection_success(): self._pool = EppConnectionPool( client=self._client, login=self._login, options=options diff --git a/src/epplibwrapper/socket.py b/src/epplibwrapper/socket.py index d25d823f1..c6ffe20bf 100644 --- a/src/epplibwrapper/socket.py +++ b/src/epplibwrapper/socket.py @@ -42,7 +42,8 @@ class Socket: return code >= 2000 def test_connection_success(self): - """Tests if a successful connection can be made with the registry""" + """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"): return False From a05d02bbfa56232e73ec2c716d75f338ac204a56 Mon Sep 17 00:00:00 2001 From: zandercymatics <141044360+zandercymatics@users.noreply.github.com> Date: Fri, 13 Oct 2023 11:24:58 -0600 Subject: [PATCH 015/120] Expand exceptions --- src/epplibwrapper/__init__.py | 3 - src/epplibwrapper/client.py | 95 +++++++++++++++++++----- src/epplibwrapper/errors.py | 3 + src/epplibwrapper/utility/pool.py | 50 ++++++++++++- src/epplibwrapper/utility/pool_status.py | 6 ++ 5 files changed, 133 insertions(+), 24 deletions(-) create mode 100644 src/epplibwrapper/utility/pool_status.py diff --git a/src/epplibwrapper/__init__.py b/src/epplibwrapper/__init__.py index 5b3bdc55c..dd6664a3a 100644 --- a/src/epplibwrapper/__init__.py +++ b/src/epplibwrapper/__init__.py @@ -45,9 +45,6 @@ except NameError: # Attn: these imports should NOT be at the top of the file try: from .client import CLIENT, commands -except ImportError: - pass -try: from .errors import RegistryError, ErrorCode from epplib.models import common, info from epplib.responses import extensions diff --git a/src/epplibwrapper/client.py b/src/epplibwrapper/client.py index 3426dd486..e38665e01 100644 --- a/src/epplibwrapper/client.py +++ b/src/epplibwrapper/client.py @@ -3,6 +3,8 @@ import logging from time import sleep +from epplibwrapper.utility.pool_status import PoolStatus + try: from epplib.client import Client from epplib import commands @@ -16,7 +18,7 @@ from django.conf import settings from .cert import Cert, Key from .errors import LoginError, RegistryError from .socket import Socket -from .utility.pool import EppConnectionPool +from .utility.pool import EPPConnectionPool logger = logging.getLogger(__name__) @@ -32,7 +34,6 @@ except Exception: exc_info=True, ) - class EPPLibWrapper: """ A wrapper over epplib's client. @@ -61,35 +62,33 @@ class EPPLibWrapper: password=settings.SECRET_REGISTRY_KEY_PASSPHRASE, ) ) - options = { + + self.pool_options = { # Pool size "size": settings.EPP_CONNECTION_POOL_SIZE, # Which errors the pool should look out for "exc_classes": ( - LoginError, - RegistryError, + TransportError, ), - # Occasionally pings the registry to keep the connection alive + # Occasionally pings the registry to keep the connection alive. + # Value in seconds => (keepalive / size) "keepalive": settings.POOL_KEEP_ALIVE, } self._pool = None - # 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 settings.DEBUG or self._test_registry_connection_success(): - self._pool = EppConnectionPool( - client=self._client, login=self._login, options=options - ) - else: - logger.warning("Cannot contact the Registry") - # TODO - signal that the app may need to restart? + # Tracks the status of the pool + self.pool_status = PoolStatus() + + self.start_connection_pool() def _send(self, command): """Helper function used by `send`.""" cmd_type = command.__class__.__name__ try: - if self._pool is None: - raise LoginError + if not self.pool_status.connection_success: + raise LoginError( + "Couldn't connect to the registry after three attempts" + ) # TODO - add a timeout with self._pool.get() as connection: response = connection.send(command) @@ -122,6 +121,21 @@ class EPPLibWrapper: # try to prevent use of this method without appropriate safeguards if not cleaned: raise ValueError("Please sanitize user input before sending it.") + + # Reopen the pool if its closed + 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: + raise RegistryError( + "Can't contact the Registry. Please try again later" + ) + except RegistryError as err: + raise err + finally: + self.start_connection_pool() counter = 0 # we'll try 3 times while True: @@ -134,6 +148,53 @@ class EPPLibWrapper: else: # don't try again raise err + 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 settings.DEBUG or self._test_registry_connection_success(): + logger.warning("Cannot contact the Registry") + self.pool_status.connection_success = False + # Q: Should err be raised instead? + # Q2: Since we try to connect 3 times, + # this indicates that the Registry isn't responsive. + # What should we do in this case? + return + else: + self.pool_status.connection_success = True + + # 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() + + self._pool = EPPConnectionPool( + client=self._client, login=self._login, options=self.pool_options + ) + self.pool_status.pool_running = True + + 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 + 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 diff --git a/src/epplibwrapper/errors.py b/src/epplibwrapper/errors.py index d34ed5e91..91c8721d8 100644 --- a/src/epplibwrapper/errors.py +++ b/src/epplibwrapper/errors.py @@ -79,6 +79,9 @@ class RegistryError(Exception): def is_client_error(self): return self.code is not None and (self.code >= 2000 and self.code <= 2308) + + def is_not_retryable(self): + pass class LoginError(RegistryError): diff --git a/src/epplibwrapper/utility/pool.py b/src/epplibwrapper/utility/pool.py index 6682f3bf6..3784f3fbc 100644 --- a/src/epplibwrapper/utility/pool.py +++ b/src/epplibwrapper/utility/pool.py @@ -1,4 +1,6 @@ +from collections import deque import logging +import gevent from geventconnpool import ConnectionPool from epplibwrapper.errors import RegistryError, LoginError from epplibwrapper.socket import Socket @@ -11,15 +13,23 @@ except ImportError: logger = logging.getLogger(__name__) -class EppConnectionPool(ConnectionPool): - def __init__(self, client, login, options): +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 super().__init__(**options) def _new_connection(self): - socket = self.create_socket(self._client, self._login) + socket = self._create_socket(self._client, self._login) try: connection = socket.connect() return connection @@ -37,7 +47,39 @@ class EppConnectionPool(ConnectionPool): logger.error("Failed to keep the connection alive.", exc_info=True) raise RegistryError("Failed to keep the connection alive.") from err - def create_socket(self, client, login) -> Socket: + 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: + gevent.killall(self.conn) + self.conn.clear() + # Clear the semaphore + for i in range(self.lock.counter): + self.lock.release() + # TODO - connection pool err + except Exception as err: + logger.error( + "Could not kill all connections." + ) + raise err + + def repopulate_all_connections(self): + """Regenerates the connection pool. + If any connections exist, kill them first. + """ + if len(self.conn) > 0: + self.kill_all_connections() + for i in range(self.size): + self.lock.acquire() + for i in range(self.size): + gevent.spawn_later(self.SPAWN_FREQUENCY*i, self._addOne) + + diff --git a/src/epplibwrapper/utility/pool_status.py b/src/epplibwrapper/utility/pool_status.py new file mode 100644 index 000000000..82c032941 --- /dev/null +++ b/src/epplibwrapper/utility/pool_status.py @@ -0,0 +1,6 @@ +class PoolStatus: + """A list of Booleans to keep track of Pool Status""" + def __init__(self): + self.pool_running = False + self.connection_success = False + self.pool_hanging = False From 4cdf9794df7dd754a4c653071425215cada48fc7 Mon Sep 17 00:00:00 2001 From: zandercymatics <141044360+zandercymatics@users.noreply.github.com> Date: Fri, 13 Oct 2023 11:35:36 -0600 Subject: [PATCH 016/120] Test pool size of 1 --- src/registrar/config/settings.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/registrar/config/settings.py b/src/registrar/config/settings.py index cbde49ba2..e10c5fc13 100644 --- a/src/registrar/config/settings.py +++ b/src/registrar/config/settings.py @@ -536,7 +536,7 @@ 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! -EPP_CONNECTION_POOL_SIZE = 10 +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 From f6d287cf219e724a420413f361ea45745fb01ee3 Mon Sep 17 00:00:00 2001 From: zandercymatics <141044360+zandercymatics@users.noreply.github.com> Date: Fri, 13 Oct 2023 11:45:02 -0600 Subject: [PATCH 017/120] Setting tinkernig --- src/registrar/config/settings.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/registrar/config/settings.py b/src/registrar/config/settings.py index e10c5fc13..9ec3d0a5c 100644 --- a/src/registrar/config/settings.py +++ b/src/registrar/config/settings.py @@ -536,11 +536,11 @@ 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! -EPP_CONNECTION_POOL_SIZE = 1 +EPP_CONNECTION_POOL_SIZE = 3 # Determines the interval in which we ping open connections in seconds # Calculated as POOL_KEEP_ALIVE / EPP_CONNECTION_POOL_SIZE -POOL_KEEP_ALIVE = None +POOL_KEEP_ALIVE = 60 # Ping every 20 seconds # endregion # region: Security and Privacy----------------------------------------------### From c165fd8401f606dfac40ba0af070e0d8edebfef3 Mon Sep 17 00:00:00 2001 From: zandercymatics <141044360+zandercymatics@users.noreply.github.com> Date: Fri, 13 Oct 2023 11:56:31 -0600 Subject: [PATCH 018/120] Update settings.py --- src/registrar/config/settings.py | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/src/registrar/config/settings.py b/src/registrar/config/settings.py index 9ec3d0a5c..fe7ff57c4 100644 --- a/src/registrar/config/settings.py +++ b/src/registrar/config/settings.py @@ -534,9 +534,15 @@ SECRET_REGISTRY_KEY = secret_registry_key SECRET_REGISTRY_KEY_PASSPHRASE = secret_registry_key_passphrase SECRET_REGISTRY_HOSTNAME = secret_registry_hostname +# Question for reviewers: For one client, the performance difference +# between a pool of size 1 vs a pool of size 10 isn't noticeable. +# The main performance increase comes from an open connection. +# We would need to do load testing to determine the ideal number, +# my recommendation now would be 3 as it is a good balance between +# overhead vs capacity. # 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! -EPP_CONNECTION_POOL_SIZE = 3 +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 From 284b7ceadfbfecd4912605c525f967589fca68ce Mon Sep 17 00:00:00 2001 From: zandercymatics <141044360+zandercymatics@users.noreply.github.com> Date: Fri, 13 Oct 2023 13:13:51 -0600 Subject: [PATCH 019/120] Update piplock --- src/Pipfile | 2 +- src/Pipfile.lock | 54 ++++++++++++++++++++++---------------------- src/requirements.txt | 6 ++--- 3 files changed, 31 insertions(+), 31 deletions(-) diff --git a/src/Pipfile b/src/Pipfile index 8864248c1..377252e05 100644 --- a/src/Pipfile +++ b/src/Pipfile @@ -26,7 +26,7 @@ boto3 = "*" typing-extensions ='*' django-login-required-middleware = "*" fred-epplib = {git = "https://github.com/cisagov/epplib.git", ref = "master"} -geventconnpool = {git = "https://github.com/zandercymatics/geventconnpool.git", ref = "master"} +geventconnpool = {git = "https://github.com/rasky/geventconnpool.git", ref = "1bbb93a714a331a069adf27265fe582d9ba7ecd4"} [dev-packages] django-debug-toolbar = "*" diff --git a/src/Pipfile.lock b/src/Pipfile.lock index 6124b7096..c8ca68d5c 100644 --- a/src/Pipfile.lock +++ b/src/Pipfile.lock @@ -1,7 +1,7 @@ { "_meta": { "hash": { - "sha256": "f9d7900daf9ca6d77fc9fe29c79b7620040cdaa50a34401bb2f84cef0862189e" + "sha256": "a3a996c98e72cee37bc89a3b95fab6ae4b396d5663eb4fe66a80684154bc90e0" }, "pipfile-spec": 6, "requires": {}, @@ -32,20 +32,20 @@ }, "boto3": { "hashes": [ - "sha256:0dfa2fc96ccafce4feb23044d6cba8b25075ad428a0c450d369d099c6a1059d2", - "sha256:148eeba0f1867b3db5b3e5ae2997d75a94d03fad46171374a0819168c36f7ed0" + "sha256:65d052ec13197460586ee385aa2d6bba0e7378d2d2c7f3e93c044c43ae1ca782", + "sha256:94218aba2feb5b404b665b8d76c172dc654f79b4c5fa0e9e92459c098da87bf4" ], "index": "pypi", "markers": "python_version >= '3.7'", - "version": "==1.28.62" + "version": "==1.28.63" }, "botocore": { "hashes": [ - "sha256:272b78ac65256b6294cb9cdb0ac484d447ad3a85642e33cb6a3b1b8afee15a4c", - "sha256:be792d806afc064694a2d0b9b25779f3ca0c1584b29a35ac32e67f0064ddb8b7" + "sha256:6e582c811ea74f25bdb490ac372b2645de4a60286b42ddd8c69f3b6df82b6b12", + "sha256:cb9db5db5af865b1fc2e1405b967db5d78dd0f4d84e5dc1974e082733c1034b7" ], "markers": "python_version >= '3.7'", - "version": "==1.31.62" + "version": "==1.31.63" }, "cachetools": { "hashes": [ @@ -437,8 +437,8 @@ "version": "==23.9.1" }, "geventconnpool": { - "git": "https://github.com/zandercymatics/geventconnpool.git", - "ref": "e4f349117875fa70b1034b89c3bc7caafac6a9b4" + "git": "https://github.com/rasky/geventconnpool.git", + "ref": "1bbb93a714a331a069adf27265fe582d9ba7ecd4" }, "greenlet": { "hashes": [ @@ -1200,12 +1200,12 @@ }, "boto3": { "hashes": [ - "sha256:0dfa2fc96ccafce4feb23044d6cba8b25075ad428a0c450d369d099c6a1059d2", - "sha256:148eeba0f1867b3db5b3e5ae2997d75a94d03fad46171374a0819168c36f7ed0" + "sha256:65d052ec13197460586ee385aa2d6bba0e7378d2d2c7f3e93c044c43ae1ca782", + "sha256:94218aba2feb5b404b665b8d76c172dc654f79b4c5fa0e9e92459c098da87bf4" ], "index": "pypi", "markers": "python_version >= '3.7'", - "version": "==1.28.62" + "version": "==1.28.63" }, "boto3-mocking": { "hashes": [ @@ -1218,28 +1218,28 @@ }, "boto3-stubs": { "hashes": [ - "sha256:22a08e27d2ede1849dd0d75e8501099240b34bd70adb606584a2af2e12f3a22b", - "sha256:f5ae08d2abae7709fff3e7cacea66c41cb43236527cfaf3975e506c6c67439a0" + "sha256:bd1becb0f8781d0a3022261a41d33f757a121117bf84ea6476b4761cb9e3cfd5", + "sha256:ecf4fb2b5b71be52cfc970ee059fe17439ed1904d0395508f5545c380d4d951d" ], "index": "pypi", "markers": "python_version >= '3.7'", - "version": "==1.28.62" + "version": "==1.28.63" }, "botocore": { "hashes": [ - "sha256:272b78ac65256b6294cb9cdb0ac484d447ad3a85642e33cb6a3b1b8afee15a4c", - "sha256:be792d806afc064694a2d0b9b25779f3ca0c1584b29a35ac32e67f0064ddb8b7" + "sha256:6e582c811ea74f25bdb490ac372b2645de4a60286b42ddd8c69f3b6df82b6b12", + "sha256:cb9db5db5af865b1fc2e1405b967db5d78dd0f4d84e5dc1974e082733c1034b7" ], "markers": "python_version >= '3.7'", - "version": "==1.31.62" + "version": "==1.31.63" }, "botocore-stubs": { "hashes": [ - "sha256:2ce555e5dff2e91fc22bd67106534bf3e0593b838d87f8a49d3b8e87fa83a440", - "sha256:d30217d8f6a0888616a44c83150490c5fbc899550ffe1896a2cd15a2205fd648" + "sha256:873715a5c21d0f4593628393c78e47cf94e53a43e40863a9ef5f165fcdcf900f", + "sha256:e92b5bd8d2667e557ea25025b396613c9bcb33d18b1971f98ebc24fa54caf495" ], "markers": "python_version >= '3.7' and python_version < '4.0'", - "version": "==1.31.62" + "version": "==1.31.63" }, "click": { "hashes": [ @@ -1440,11 +1440,11 @@ }, "pycodestyle": { "hashes": [ - "sha256:259bcc17857d8a8b3b4a2327324b79e5f020a13c16074670f9c8c8f872ea76d0", - "sha256:5d1013ba8dc7895b548be5afb05740ca82454fd899971563d2ef625d090326f8" + "sha256:41ba0e7afc9752dfb53ced5489e89f8186be00e599e712660695b7a75ff2663f", + "sha256:44fe31000b2d866f2e41841b18528a505fbd7fef9017b04eff4e2648a0fadc67" ], "markers": "python_version >= '3.8'", - "version": "==2.11.0" + "version": "==2.11.1" }, "pyflakes": { "hashes": [ @@ -1622,12 +1622,12 @@ }, "types-requests": { "hashes": [ - "sha256:39894cbca3fb3d032ed8bdd02275b4273471aa5668564617cc1734b0a65ffdf8", - "sha256:e1b325c687b3494a2f528ab06e411d7092cc546cc9245c000bacc2fca5ae96d4" + "sha256:140e323da742a0cd0ff0a5a83669da9ffcebfaeb855d367186b2ec3985ba2742", + "sha256:3bb11188795cc3aa39f9635032044ee771009370fb31c3a06ae952b267b6fcd7" ], "index": "pypi", "markers": "python_version >= '3.7'", - "version": "==2.31.0.8" + "version": "==2.31.0.9" }, "types-s3transfer": { "hashes": [ diff --git a/src/requirements.txt b/src/requirements.txt index 2c76aca02..0e3d41d87 100644 --- a/src/requirements.txt +++ b/src/requirements.txt @@ -1,8 +1,8 @@ -i https://pypi.python.org/simple annotated-types==0.6.0; python_version >= '3.8' asgiref==3.7.2; python_version >= '3.7' -boto3==1.28.62; python_version >= '3.7' -botocore==1.31.62; python_version >= '3.7' +boto3==1.28.63; python_version >= '3.7' +botocore==1.31.63; python_version >= '3.7' cachetools==5.3.1; python_version >= '3.7' certifi==2023.7.22; python_version >= '3.6' cfenv==0.5.3 @@ -27,7 +27,7 @@ fred-epplib@ git+https://github.com/cisagov/epplib.git@d56d183f1664f34c40ca9716a 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' gevent==23.9.1; python_version >= '3.8' -geventconnpool@ git+https://github.com/zandercymatics/geventconnpool.git@e4f349117875fa70b1034b89c3bc7caafac6a9b4 +geventconnpool@ git+https://github.com/rasky/geventconnpool.git@1bbb93a714a331a069adf27265fe582d9ba7ecd4 greenlet==3.0.0; python_version < '3.11' and platform_python_implementation == 'CPython' gunicorn==21.2.0; python_version >= '3.5' idna==3.4; python_version >= '3.5' From 6709ac9ddb91259f060657970086c05c50cde98e Mon Sep 17 00:00:00 2001 From: Rachid Mrad Date: Fri, 13 Oct 2023 16:20:38 -0400 Subject: [PATCH 020/120] trick the sandbox by deleting a conflicting migration --- .../migrations/0035_alter_user_options.py | 42 ++++++++++++++- ...napplication_organization_type_and_more.py | 52 ------------------- 2 files changed, 41 insertions(+), 53 deletions(-) delete mode 100644 src/registrar/migrations/0038_alter_domainapplication_organization_type_and_more.py diff --git a/src/registrar/migrations/0035_alter_user_options.py b/src/registrar/migrations/0035_alter_user_options.py index 7ed81cdf5..b76f07c5d 100644 --- a/src/registrar/migrations/0035_alter_user_options.py +++ b/src/registrar/migrations/0035_alter_user_options.py @@ -1,6 +1,6 @@ # Generated by Django 4.2.1 on 2023-09-27 18:53 -from django.db import migrations +from django.db import migrations, models class Migration(migrations.Migration): @@ -18,4 +18,44 @@ class Migration(migrations.Migration): ] }, ), + migrations.AlterField( + model_name="domainapplication", + name="organization_type", + field=models.CharField( + blank=True, + choices=[ + ("federal", "Federal"), + ("interstate", "Interstate"), + ("state_or_territory", "State or territory"), + ("tribal", "Tribal"), + ("county", "County"), + ("city", "City"), + ("special_district", "Special district"), + ("school_district", "School district"), + ], + help_text="Type of organization", + max_length=255, + null=True, + ), + ), + migrations.AlterField( + model_name="domaininformation", + name="organization_type", + field=models.CharField( + blank=True, + choices=[ + ("federal", "Federal"), + ("interstate", "Interstate"), + ("state_or_territory", "State or territory"), + ("tribal", "Tribal"), + ("county", "County"), + ("city", "City"), + ("special_district", "Special district"), + ("school_district", "School district"), + ], + help_text="Type of Organization", + max_length=255, + null=True, + ), + ), ] diff --git a/src/registrar/migrations/0038_alter_domainapplication_organization_type_and_more.py b/src/registrar/migrations/0038_alter_domainapplication_organization_type_and_more.py deleted file mode 100644 index bdbca82d8..000000000 --- a/src/registrar/migrations/0038_alter_domainapplication_organization_type_and_more.py +++ /dev/null @@ -1,52 +0,0 @@ -# Generated by Django 4.2.1 on 2023-10-12 19:30 - -from django.db import migrations, models - - -class Migration(migrations.Migration): - dependencies = [ - ("registrar", "0037_create_groups_v01"), - ] - - operations = [ - migrations.AlterField( - model_name="domainapplication", - name="organization_type", - field=models.CharField( - blank=True, - choices=[ - ("federal", "Federal"), - ("interstate", "Interstate"), - ("state_or_territory", "State or territory"), - ("tribal", "Tribal"), - ("county", "County"), - ("city", "City"), - ("special_district", "Special district"), - ("school_district", "School district"), - ], - help_text="Type of organization", - max_length=255, - null=True, - ), - ), - migrations.AlterField( - model_name="domaininformation", - name="organization_type", - field=models.CharField( - blank=True, - choices=[ - ("federal", "Federal"), - ("interstate", "Interstate"), - ("state_or_territory", "State or territory"), - ("tribal", "Tribal"), - ("county", "County"), - ("city", "City"), - ("special_district", "Special district"), - ("school_district", "School district"), - ], - help_text="Type of Organization", - max_length=255, - null=True, - ), - ), - ] From a2574124ffaae7eccaf3d94bb3e67aefdffe2ec7 Mon Sep 17 00:00:00 2001 From: Rachid Mrad Date: Fri, 13 Oct 2023 16:27:23 -0400 Subject: [PATCH 021/120] return migrations to pervious state --- .../migrations/0035_alter_user_options.py | 42 +-------------- ...napplication_organization_type_and_more.py | 52 +++++++++++++++++++ 2 files changed, 53 insertions(+), 41 deletions(-) create mode 100644 src/registrar/migrations/0038_alter_domainapplication_organization_type_and_more.py diff --git a/src/registrar/migrations/0035_alter_user_options.py b/src/registrar/migrations/0035_alter_user_options.py index b76f07c5d..7ed81cdf5 100644 --- a/src/registrar/migrations/0035_alter_user_options.py +++ b/src/registrar/migrations/0035_alter_user_options.py @@ -1,6 +1,6 @@ # Generated by Django 4.2.1 on 2023-09-27 18:53 -from django.db import migrations, models +from django.db import migrations class Migration(migrations.Migration): @@ -18,44 +18,4 @@ class Migration(migrations.Migration): ] }, ), - migrations.AlterField( - model_name="domainapplication", - name="organization_type", - field=models.CharField( - blank=True, - choices=[ - ("federal", "Federal"), - ("interstate", "Interstate"), - ("state_or_territory", "State or territory"), - ("tribal", "Tribal"), - ("county", "County"), - ("city", "City"), - ("special_district", "Special district"), - ("school_district", "School district"), - ], - help_text="Type of organization", - max_length=255, - null=True, - ), - ), - migrations.AlterField( - model_name="domaininformation", - name="organization_type", - field=models.CharField( - blank=True, - choices=[ - ("federal", "Federal"), - ("interstate", "Interstate"), - ("state_or_territory", "State or territory"), - ("tribal", "Tribal"), - ("county", "County"), - ("city", "City"), - ("special_district", "Special district"), - ("school_district", "School district"), - ], - help_text="Type of Organization", - max_length=255, - null=True, - ), - ), ] diff --git a/src/registrar/migrations/0038_alter_domainapplication_organization_type_and_more.py b/src/registrar/migrations/0038_alter_domainapplication_organization_type_and_more.py new file mode 100644 index 000000000..a06ea0451 --- /dev/null +++ b/src/registrar/migrations/0038_alter_domainapplication_organization_type_and_more.py @@ -0,0 +1,52 @@ +# Generated by Django 4.2.1 on 2023-10-13 20:26 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + dependencies = [ + ("registrar", "0037_create_groups_v01"), + ] + + operations = [ + migrations.AlterField( + model_name="domainapplication", + name="organization_type", + field=models.CharField( + blank=True, + choices=[ + ("federal", "Federal"), + ("interstate", "Interstate"), + ("state_or_territory", "State or territory"), + ("tribal", "Tribal"), + ("county", "County"), + ("city", "City"), + ("special_district", "Special district"), + ("school_district", "School district"), + ], + help_text="Type of organization", + max_length=255, + null=True, + ), + ), + migrations.AlterField( + model_name="domaininformation", + name="organization_type", + field=models.CharField( + blank=True, + choices=[ + ("federal", "Federal"), + ("interstate", "Interstate"), + ("state_or_territory", "State or territory"), + ("tribal", "Tribal"), + ("county", "County"), + ("city", "City"), + ("special_district", "Special district"), + ("school_district", "School district"), + ], + help_text="Type of Organization", + max_length=255, + null=True, + ), + ), + ] From 5e787eeed324f8789c8a1e3e9dcf0694d13d588a Mon Sep 17 00:00:00 2001 From: Rachid Mrad Date: Fri, 13 Oct 2023 16:30:29 -0400 Subject: [PATCH 022/120] merge diverging migrations --- src/registrar/migrations/0039_merge_20231013_2029.py | 12 ++++++++++++ 1 file changed, 12 insertions(+) create mode 100644 src/registrar/migrations/0039_merge_20231013_2029.py diff --git a/src/registrar/migrations/0039_merge_20231013_2029.py b/src/registrar/migrations/0039_merge_20231013_2029.py new file mode 100644 index 000000000..aed231bdc --- /dev/null +++ b/src/registrar/migrations/0039_merge_20231013_2029.py @@ -0,0 +1,12 @@ +# Generated by Django 4.2.1 on 2023-10-13 20:29 + +from django.db import migrations + + +class Migration(migrations.Migration): + dependencies = [ + ("registrar", "0038_alter_domainapplication_organization_type_and_more"), + ("registrar", "0038_create_groups_v02"), + ] + + operations = [] From 372ead1121372d885b19fc56fce641f1cb6b7541 Mon Sep 17 00:00:00 2001 From: Rachid Mrad Date: Fri, 13 Oct 2023 17:05:33 -0400 Subject: [PATCH 023/120] refactor org literal mapping to dict, tweak error handing, lint --- src/registrar/models/domain_application.py | 12 ++++++------ src/registrar/templatetags/custom_filters.py | 17 ++++++++++------- 2 files changed, 16 insertions(+), 13 deletions(-) diff --git a/src/registrar/models/domain_application.py b/src/registrar/models/domain_application.py index 4da32ad18..a4752aa88 100644 --- a/src/registrar/models/domain_application.py +++ b/src/registrar/models/domain_application.py @@ -107,9 +107,9 @@ class DomainApplication(TimeStampedModel): class OrganizationChoices(models.TextChoices): """ - Primary organization choices: - For use in django admin - Keys need to match OrganizationChoicesVerbose + Primary organization choices: + For use in django admin + Keys need to match OrganizationChoicesVerbose """ FEDERAL = "federal", "Federal" @@ -124,9 +124,9 @@ class DomainApplication(TimeStampedModel): class OrganizationChoicesVerbose(models.TextChoices): """ - Secondary organization choices - For use in the application form and on the templates - Keys need to match OrganizationChoices + Secondary organization choices + For use in the application form and on the templates + Keys need to match OrganizationChoices """ FEDERAL = ( diff --git a/src/registrar/templatetags/custom_filters.py b/src/registrar/templatetags/custom_filters.py index e90c3166d..158b7269e 100644 --- a/src/registrar/templatetags/custom_filters.py +++ b/src/registrar/templatetags/custom_filters.py @@ -1,8 +1,10 @@ +import logging from django import template import re from registrar.models.domain_application import DomainApplication register = template.Library() +logger = logging.getLogger(__name__) @register.filter(name="extract_value") @@ -53,13 +55,14 @@ def contains_checkbox(html_list): @register.filter def get_organization_long_name(organization_type): - organization_choices_dict = {} - - for name, value in DomainApplication.OrganizationChoicesVerbose.choices: - organization_choices_dict[name] = value + # https://gist.github.com/OmenApps/3eef60ba4204f3d1842d9d7477efcce1#file-django_choices-txt-L28 + organization_choices_dict = dict( + DomainApplication.OrganizationChoicesVerbose.choices + ) long_form_type = organization_choices_dict[organization_type] - if long_form_type is not None: - return long_form_type + if long_form_type is None: + logger.error("Organization type error, triggered by a template's custom filter") + return "Error" - return "Error" + return long_form_type From a9e4d340994d1b66d6d96a027540210df86e61aa Mon Sep 17 00:00:00 2001 From: zandercymatics <141044360+zandercymatics@users.noreply.github.com> Date: Mon, 16 Oct 2023 13:08:28 -0600 Subject: [PATCH 024/120] Tests / pool restart on freeze --- src/epplibwrapper/client.py | 16 +++- src/epplibwrapper/tests/__init__.py | 0 src/epplibwrapper/tests/test_pool.py | 132 +++++++++++++++++++++++++++ src/registrar/config/settings.py | 4 + 4 files changed, 150 insertions(+), 2 deletions(-) create mode 100644 src/epplibwrapper/tests/__init__.py create mode 100644 src/epplibwrapper/tests/test_pool.py diff --git a/src/epplibwrapper/client.py b/src/epplibwrapper/client.py index e38665e01..b0d79c300 100644 --- a/src/epplibwrapper/client.py +++ b/src/epplibwrapper/client.py @@ -3,6 +3,8 @@ import logging from time import sleep +from gevent import Timeout + from epplibwrapper.utility.pool_status import PoolStatus try: @@ -84,14 +86,22 @@ class EPPLibWrapper: def _send(self, command): """Helper function used by `send`.""" cmd_type = command.__class__.__name__ + # Start a timeout to check if the pool is hanging + timeout = Timeout(settings.POOL_TIMEOUT) + timeout.start() try: if not self.pool_status.connection_success: raise LoginError( "Couldn't connect to the registry after three attempts" ) - # TODO - add a timeout with self._pool.get() as connection: response = connection.send(command) + except Timeout as t: + if t is timeout: + # Flag that the pool is frozen, + # then restart the pool. + self.pool_status.pool_hanging = True + self.start_connection_pool() except (ValueError, ParsingError) as err: message = f"{cmd_type} failed to execute due to some syntax error." logger.warning(message, exc_info=True) @@ -115,6 +125,8 @@ class EPPLibWrapper: raise RegistryError(response.msg, code=response.code) else: return response + finally: + timeout.close() def send(self, command, *, cleaned=False): """Login, send the command, then close the connection. Tries 3 times.""" @@ -155,7 +167,6 @@ class EPPLibWrapper: 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 settings.DEBUG or self._test_registry_connection_success(): @@ -179,6 +190,7 @@ class EPPLibWrapper: client=self._client, login=self._login, options=self.pool_options ) self.pool_status.pool_running = True + self.pool_status.pool_hanging = False logger.info("Connection pool started") diff --git a/src/epplibwrapper/tests/__init__.py b/src/epplibwrapper/tests/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/src/epplibwrapper/tests/test_pool.py b/src/epplibwrapper/tests/test_pool.py new file mode 100644 index 000000000..8752c9eab --- /dev/null +++ b/src/epplibwrapper/tests/test_pool.py @@ -0,0 +1,132 @@ +from unittest import skip +from unittest.mock import patch +from django.conf import settings + +from django.test import Client +from epplibwrapper.client import EPPLibWrapper +from registrar.tests.common import MockEppLib + +import logging + +try: + from epplib.client import Client + from epplib import commands + from epplib.exceptions import TransportError + from epplib.transport import SocketTransport +except ImportError: + pass + +logger = logging.getLogger(__name__) + + +@patch("djangooidc.views.CLIENT", autospec=True) +class TestConnectionPool(MockEppLib): + """Tests for our connection pooling behaviour""" + def setUp(self): + """ + Background: + Given the registrant is logged in + And the registrant is the admin on a domain + """ + super().setUp() + 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 tearDown(self): + super().tearDown() + + def user_info(*args): + return { + "sub": "TEST", + "email": "test@example.com", + "first_name": "Testy", + "last_name": "Tester", + "phone": "814564000", + } + + def test_pool_created_successfully(self, mock_client): + # setup + session = self.client.session + session["state"] = "TEST" # nosec B105 + session.save() + # mock + mock_client.callback.side_effect = self.user_info + + client = EPPLibWrapper() + pool = client._pool + + # These are defined outside of the pool, + # so we can reimplement how this is being done + # in client.py. They should remain unchanged, + # and if they aren't, something went wrong. + expected_login = commands.Login( + cl_id='nothing', + password='nothing', + obj_uris=[ + 'urn:ietf:params:xml:ns:domain-1.0', + 'urn:ietf:params:xml:ns:contact-1.0' + ], + new_pw=None, + version='1.0', + lang='en', + ext_uris=[] + ) + + # Key/cert will generate a new file everytime. + # This should never be null, so we can check for that. + try: + expected_client = Client( + SocketTransport( + settings.SECRET_REGISTRY_HOSTNAME, + cert_file=pool._client.transport.cert_file, + key_file=pool._client.transport.key_file, + password=settings.SECRET_REGISTRY_KEY_PASSPHRASE, + ) + ).__dict__ + except Exception as err: + self.fail(err) + + # We don't care about checking if the objects are both of + # the same reference, we only care about data parity, so + # we do a dict conversion. + actual_client = pool._client.__dict__ + actual_client["transport"] = actual_client["transport"].__dict__ + expected_client["transport"] = expected_client["transport"].__dict__ + + # Ensure that we're getting the credentials we expect + self.assertEqual(pool._login, expected_login) + self.assertEqual(actual_client, expected_client) + + # Check that options are set correctly + self.assertEqual(pool.size, self.pool_options["size"]) + self.assertEqual(pool.keepalive, self.pool_options["keepalive"]) + self.assertEqual(pool.exc_classes, self.pool_options["exc_classes"]) + + # Check that it is running + self.assertEqual(client.pool_status.connection_success, True) + self.assertEqual(client.pool_status.pool_running, True) + + @skip("not implemented yet") + def test_pool_timesout(self): + """The pool timesout and restarts""" + raise + + @skip("not implemented yet") + def test_multiple_users_send_data(self): + """Multiple users send data concurrently""" + raise + + @skip("not implemented yet") + def test_pool_sends_data(self): + """A .send is invoked on the pool""" + raise + diff --git a/src/registrar/config/settings.py b/src/registrar/config/settings.py index fe7ff57c4..9d03dbc89 100644 --- a/src/registrar/config/settings.py +++ b/src/registrar/config/settings.py @@ -548,6 +548,10 @@ EPP_CONNECTION_POOL_SIZE = 1 # Calculated as POOL_KEEP_ALIVE / EPP_CONNECTION_POOL_SIZE POOL_KEEP_ALIVE = 60 # Ping every 20 seconds +# Determines how long we try to keep a pool alive for, +# before restarting it. +POOL_TIMEOUT = 60 + # endregion # region: Security and Privacy----------------------------------------------### From 7bb6bb9e7d9a20cc1669007a215a92c3e9c9774b Mon Sep 17 00:00:00 2001 From: zandercymatics <141044360+zandercymatics@users.noreply.github.com> Date: Mon, 16 Oct 2023 13:33:11 -0600 Subject: [PATCH 025/120] Update pipfile --- src/Pipfile | 1 + src/Pipfile.lock | 3 ++- src/epplibwrapper/client.py | 25 +++++++++----------- src/epplibwrapper/errors.py | 2 +- src/epplibwrapper/tests/test_pool.py | 30 +++++++++++------------- src/epplibwrapper/utility/pool.py | 15 +++++------- src/epplibwrapper/utility/pool_status.py | 1 + 7 files changed, 36 insertions(+), 41 deletions(-) diff --git a/src/Pipfile b/src/Pipfile index 377252e05..43b919c08 100644 --- a/src/Pipfile +++ b/src/Pipfile @@ -25,6 +25,7 @@ django-phonenumber-field = {extras = ["phonenumberslite"], version = "*"} boto3 = "*" typing-extensions ='*' django-login-required-middleware = "*" +gevent = "*" fred-epplib = {git = "https://github.com/cisagov/epplib.git", ref = "master"} geventconnpool = {git = "https://github.com/rasky/geventconnpool.git", ref = "1bbb93a714a331a069adf27265fe582d9ba7ecd4"} diff --git a/src/Pipfile.lock b/src/Pipfile.lock index c8ca68d5c..a7314de46 100644 --- a/src/Pipfile.lock +++ b/src/Pipfile.lock @@ -1,7 +1,7 @@ { "_meta": { "hash": { - "sha256": "a3a996c98e72cee37bc89a3b95fab6ae4b396d5663eb4fe66a80684154bc90e0" + "sha256": "67b51a57b7d9d7d70d1eeca3515e169cd614d575a7213f31251f9dde43e1f748" }, "pipfile-spec": 6, "requires": {}, @@ -433,6 +433,7 @@ "sha256:fae8d5b5b8fa2a8f63b39f5447168b02db10c888a3e387ed7af2bd1b8612e543", "sha256:fde6402c5432b835fbb7698f1c7f2809c8d6b2bd9d047ac1f5a7c1d5aa569303" ], + "index": "pypi", "markers": "python_version >= '3.8'", "version": "==23.9.1" }, diff --git a/src/epplibwrapper/client.py b/src/epplibwrapper/client.py index b0d79c300..50c3c3a3e 100644 --- a/src/epplibwrapper/client.py +++ b/src/epplibwrapper/client.py @@ -36,6 +36,7 @@ except Exception: exc_info=True, ) + class EPPLibWrapper: """ A wrapper over epplib's client. @@ -69,9 +70,7 @@ class EPPLibWrapper: # Pool size "size": settings.EPP_CONNECTION_POOL_SIZE, # Which errors the pool should look out for - "exc_classes": ( - TransportError, - ), + "exc_classes": (TransportError,), # Occasionally pings the registry to keep the connection alive. # Value in seconds => (keepalive / size) "keepalive": settings.POOL_KEEP_ALIVE, @@ -133,7 +132,7 @@ class EPPLibWrapper: # try to prevent use of this method without appropriate safeguards if not cleaned: raise ValueError("Please sanitize user input before sending it.") - + # Reopen the pool if its closed if not self.pool_status.pool_running: # We want to reopen the connection pool, @@ -160,12 +159,12 @@ class EPPLibWrapper: else: # don't try again raise err - def start_connection_pool(self, restart_pool_if_exists = True): - """Starts a connection pool for the registry. + def start_connection_pool(self, restart_pool_if_exists=True): + """Starts a connection pool for the registry. - restart_pool_if_exists -> bool: + restart_pool_if_exists -> bool: If an instance of the pool already exists, - then then that instance will be killed first. + 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. @@ -179,13 +178,13 @@ class EPPLibWrapper: return else: self.pool_status.connection_success = True - + # 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() - + self._pool = EPPConnectionPool( client=self._client, login=self._login, options=self.pool_options ) @@ -193,7 +192,7 @@ class EPPLibWrapper: 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 @@ -203,9 +202,7 @@ class EPPLibWrapper: self._pool = None self.pool_status.pool_running = False return - logger.info( - "kill_pool() was invoked but there was no pool to delete" - ) + 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 diff --git a/src/epplibwrapper/errors.py b/src/epplibwrapper/errors.py index 91c8721d8..884b453fa 100644 --- a/src/epplibwrapper/errors.py +++ b/src/epplibwrapper/errors.py @@ -79,7 +79,7 @@ class RegistryError(Exception): def is_client_error(self): return self.code is not None and (self.code >= 2000 and self.code <= 2308) - + def is_not_retryable(self): pass diff --git a/src/epplibwrapper/tests/test_pool.py b/src/epplibwrapper/tests/test_pool.py index 8752c9eab..f77607d56 100644 --- a/src/epplibwrapper/tests/test_pool.py +++ b/src/epplibwrapper/tests/test_pool.py @@ -22,6 +22,7 @@ logger = logging.getLogger(__name__) @patch("djangooidc.views.CLIENT", autospec=True) class TestConnectionPool(MockEppLib): """Tests for our connection pooling behaviour""" + def setUp(self): """ Background: @@ -33,9 +34,7 @@ class TestConnectionPool(MockEppLib): # Current pool size "size": 1, # Which errors the pool should look out for - "exc_classes": ( - TransportError, - ), + "exc_classes": (TransportError,), # Occasionally pings the registry to keep the connection alive. # Value in seconds => (keepalive / size) "keepalive": 60, @@ -43,7 +42,7 @@ class TestConnectionPool(MockEppLib): def tearDown(self): super().tearDown() - + def user_info(*args): return { "sub": "TEST", @@ -69,16 +68,16 @@ class TestConnectionPool(MockEppLib): # in client.py. They should remain unchanged, # and if they aren't, something went wrong. expected_login = commands.Login( - cl_id='nothing', - password='nothing', + cl_id="nothing", + password="nothing", obj_uris=[ - 'urn:ietf:params:xml:ns:domain-1.0', - 'urn:ietf:params:xml:ns:contact-1.0' + "urn:ietf:params:xml:ns:domain-1.0", + "urn:ietf:params:xml:ns:contact-1.0", ], new_pw=None, - version='1.0', - lang='en', - ext_uris=[] + version="1.0", + lang="en", + ext_uris=[], ) # Key/cert will generate a new file everytime. @@ -94,7 +93,7 @@ class TestConnectionPool(MockEppLib): ).__dict__ except Exception as err: self.fail(err) - + # We don't care about checking if the objects are both of # the same reference, we only care about data parity, so # we do a dict conversion. @@ -118,15 +117,14 @@ class TestConnectionPool(MockEppLib): @skip("not implemented yet") def test_pool_timesout(self): """The pool timesout and restarts""" - raise - + raise + @skip("not implemented yet") def test_multiple_users_send_data(self): """Multiple users send data concurrently""" raise - + @skip("not implemented yet") def test_pool_sends_data(self): """A .send is invoked on the pool""" raise - diff --git a/src/epplibwrapper/utility/pool.py b/src/epplibwrapper/utility/pool.py index 3784f3fbc..00bf0ffd0 100644 --- a/src/epplibwrapper/utility/pool.py +++ b/src/epplibwrapper/utility/pool.py @@ -22,6 +22,7 @@ class EPPConnectionPool(ConnectionPool): options (dict): Options for the ConnectionPool base class """ + def __init__(self, client, login, options: dict): # For storing shared credentials self._client = client @@ -51,11 +52,11 @@ class EPPConnectionPool(ConnectionPool): """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: @@ -66,11 +67,9 @@ class EPPConnectionPool(ConnectionPool): self.lock.release() # TODO - connection pool err except Exception as err: - logger.error( - "Could not kill all connections." - ) + logger.error("Could not kill all connections.") raise err - + def repopulate_all_connections(self): """Regenerates the connection pool. If any connections exist, kill them first. @@ -80,6 +79,4 @@ class EPPConnectionPool(ConnectionPool): for i in range(self.size): self.lock.acquire() for i in range(self.size): - gevent.spawn_later(self.SPAWN_FREQUENCY*i, self._addOne) - - + gevent.spawn_later(self.SPAWN_FREQUENCY * i, self._addOne) diff --git a/src/epplibwrapper/utility/pool_status.py b/src/epplibwrapper/utility/pool_status.py index 82c032941..214bf8ac1 100644 --- a/src/epplibwrapper/utility/pool_status.py +++ b/src/epplibwrapper/utility/pool_status.py @@ -1,5 +1,6 @@ class PoolStatus: """A list of Booleans to keep track of Pool Status""" + def __init__(self): self.pool_running = False self.connection_success = False From 8db3f66c42841562b72bb9970d05178639e9ea06 Mon Sep 17 00:00:00 2001 From: zandercymatics <141044360+zandercymatics@users.noreply.github.com> Date: Mon, 16 Oct 2023 13:44:00 -0600 Subject: [PATCH 026/120] Test fix for scanner --- src/epplibwrapper/utility/pool.py | 1 - src/registrar/models/domain.py | 22 +++++++++++++--------- 2 files changed, 13 insertions(+), 10 deletions(-) diff --git a/src/epplibwrapper/utility/pool.py b/src/epplibwrapper/utility/pool.py index 00bf0ffd0..df4ff993d 100644 --- a/src/epplibwrapper/utility/pool.py +++ b/src/epplibwrapper/utility/pool.py @@ -1,4 +1,3 @@ -from collections import deque import logging import gevent from geventconnpool import ConnectionPool diff --git a/src/registrar/models/domain.py b/src/registrar/models/domain.py index d6dd5e287..898836fc4 100644 --- a/src/registrar/models/domain.py +++ b/src/registrar/models/domain.py @@ -8,15 +8,19 @@ from django_fsm import FSMField, transition, TransitionNotAllowed # type: ignor from django.db import models from typing import Any -from epplibwrapper import ( - CLIENT as registry, - commands, - common as epp, - extensions, - info as eppInfo, - RegistryError, - ErrorCode, -) +try: + from epplibwrapper import ( + CLIENT as registry, + commands, + common as epp, + extensions, + info as eppInfo, + RegistryError, + ErrorCode, + ) +except ImportError: + pass + from registrar.utility.errors import ( ActionNotAllowed, From 0896401169db553a5da4b365116a3c7cbfbf92ff Mon Sep 17 00:00:00 2001 From: zandercymatics <141044360+zandercymatics@users.noreply.github.com> Date: Mon, 16 Oct 2023 13:51:13 -0600 Subject: [PATCH 027/120] Another import test --- src/epplibwrapper/__init__.py | 1 + src/registrar/models/domain.py | 23 +++++++++++------------ 2 files changed, 12 insertions(+), 12 deletions(-) diff --git a/src/epplibwrapper/__init__.py b/src/epplibwrapper/__init__.py index dd6664a3a..7defcca31 100644 --- a/src/epplibwrapper/__init__.py +++ b/src/epplibwrapper/__init__.py @@ -44,6 +44,7 @@ except NameError: # Attn: these imports should NOT be at the top of the file try: + from .utility.pool import EPPConnectionPool from .client import CLIENT, commands from .errors import RegistryError, ErrorCode from epplib.models import common, info diff --git a/src/registrar/models/domain.py b/src/registrar/models/domain.py index 898836fc4..06b9dce60 100644 --- a/src/registrar/models/domain.py +++ b/src/registrar/models/domain.py @@ -8,18 +8,17 @@ from django_fsm import FSMField, transition, TransitionNotAllowed # type: ignor from django.db import models from typing import Any -try: - from epplibwrapper import ( - CLIENT as registry, - commands, - common as epp, - extensions, - info as eppInfo, - RegistryError, - ErrorCode, - ) -except ImportError: - pass + +from epplibwrapper import ( + + CLIENT as registry, + commands, + common as epp, + extensions, + info as eppInfo, + RegistryError, + ErrorCode, +) from registrar.utility.errors import ( From 3aac1e38df2643123f65a83f7eee4bbabd8012e8 Mon Sep 17 00:00:00 2001 From: zandercymatics <141044360+zandercymatics@users.noreply.github.com> Date: Mon, 16 Oct 2023 13:54:20 -0600 Subject: [PATCH 028/120] Log error --- src/epplibwrapper/__init__.py | 1 - src/registrar/models/domain.py | 29 +++++++++++++++++------------ 2 files changed, 17 insertions(+), 13 deletions(-) diff --git a/src/epplibwrapper/__init__.py b/src/epplibwrapper/__init__.py index 7defcca31..dd6664a3a 100644 --- a/src/epplibwrapper/__init__.py +++ b/src/epplibwrapper/__init__.py @@ -44,7 +44,6 @@ except NameError: # Attn: these imports should NOT be at the top of the file try: - from .utility.pool import EPPConnectionPool from .client import CLIENT, commands from .errors import RegistryError, ErrorCode from epplib.models import common, info diff --git a/src/registrar/models/domain.py b/src/registrar/models/domain.py index 06b9dce60..319be9fae 100644 --- a/src/registrar/models/domain.py +++ b/src/registrar/models/domain.py @@ -9,17 +9,6 @@ from django_fsm import FSMField, transition, TransitionNotAllowed # type: ignor from django.db import models from typing import Any -from epplibwrapper import ( - - CLIENT as registry, - commands, - common as epp, - extensions, - info as eppInfo, - RegistryError, - ErrorCode, -) - from registrar.utility.errors import ( ActionNotAllowed, @@ -35,8 +24,24 @@ from .utility.domain_helper import DomainHelper from .utility.time_stamped_model import TimeStampedModel from .public_contact import PublicContact - logger = logging.getLogger(__name__) +try: + from epplibwrapper import ( + CLIENT as registry, + commands, + common as epp, + extensions, + info as eppInfo, + RegistryError, + ErrorCode, + ) +except ImportError as err: + logger.error("An import error occured....") + logger.error(err) + raise err + + + class Domain(TimeStampedModel, DomainHelper): From d7c7fb9d238181e56102276a0366b5e88aa6d5ac Mon Sep 17 00:00:00 2001 From: zandercymatics <141044360+zandercymatics@users.noreply.github.com> Date: Tue, 17 Oct 2023 11:21:18 -0600 Subject: [PATCH 029/120] Tests / code cleanup --- src/epplibwrapper/client.py | 66 +++++----- src/epplibwrapper/socket.py | 4 +- src/epplibwrapper/tests/test_pool.py | 158 +++++++++++++----------- src/epplibwrapper/utility/pool.py | 17 +-- src/epplibwrapper/utility/pool_error.py | 44 +++++++ src/registrar/models/domain.py | 29 ++--- 6 files changed, 188 insertions(+), 130 deletions(-) create mode 100644 src/epplibwrapper/utility/pool_error.py diff --git a/src/epplibwrapper/client.py b/src/epplibwrapper/client.py index 50c3c3a3e..920dc284d 100644 --- a/src/epplibwrapper/client.py +++ b/src/epplibwrapper/client.py @@ -44,7 +44,7 @@ class EPPLibWrapper: ATTN: This should not be used directly. Use `Domain` from domain.py. """ - def __init__(self) -> None: + def __init__(self, start_connection_pool=True) -> None: """Initialize settings which will be used for all connections.""" # prepare (but do not send) a Login command self._login = commands.Login( @@ -80,7 +80,8 @@ class EPPLibWrapper: # Tracks the status of the pool self.pool_status = PoolStatus() - self.start_connection_pool() + if start_connection_pool: + self.start_connection_pool() def _send(self, command): """Helper function used by `send`.""" @@ -103,21 +104,21 @@ class EPPLibWrapper: self.start_connection_pool() except (ValueError, ParsingError) as err: message = f"{cmd_type} failed to execute due to some syntax error." - logger.warning(message, exc_info=True) + logger.error(message, exc_info=True) raise RegistryError(message) from err except TransportError as err: message = f"{cmd_type} failed to execute due to a connection error." - logger.warning(message, exc_info=True) + logger.error(message, exc_info=True) raise RegistryError(message) from err except LoginError as err: # For linter text = "failed to execute due to a registry login error." message = f"{cmd_type} {text}" - logger.warning(message, exc_info=True) + logger.error(message, exc_info=True) raise RegistryError(message) from err except Exception as err: message = f"{cmd_type} failed to execute due to an unknown error." - logger.warning(message, exc_info=True) + logger.error(message, exc_info=True) raise RegistryError(message) from err else: if response.code >= 2000: @@ -134,15 +135,15 @@ class EPPLibWrapper: 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: - raise RegistryError( - "Can't contact the Registry. Please try again later" - ) + 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: @@ -159,39 +160,46 @@ class EPPLibWrapper: else: # don't try again raise err - def start_connection_pool(self, restart_pool_if_exists=True): + def start_connection_pool( + self, restart_pool_if_exists=True, try_start_if_invalid=False + ): """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.""" + It is generally recommended to keep this enabled. + + try_start_if_invalid -> bool: + Designed for use in test cases, if we can't connect + to the registry, ignore that and try to connect anyway + It is generally recommended to keep this disabled. + """ # 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 settings.DEBUG or self._test_registry_connection_success(): + if ( + not try_start_if_invalid + and settings.DEBUG + or not self._test_registry_connection_success() + ): logger.warning("Cannot contact the Registry") self.pool_status.connection_success = False - # Q: Should err be raised instead? - # Q2: Since we try to connect 3 times, - # this indicates that the Registry isn't responsive. - # What should we do in this case? - return else: self.pool_status.connection_success = True - # 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() + # 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() - self._pool = EPPConnectionPool( - client=self._client, login=self._login, options=self.pool_options - ) - self.pool_status.pool_running = True - self.pool_status.pool_hanging = False + self._pool = EPPConnectionPool( + client=self._client, login=self._login, options=self.pool_options + ) + self.pool_status.pool_running = True + self.pool_status.pool_hanging = False - logger.info("Connection pool started") + logger.info("Connection pool started") def kill_pool(self): """Kills the existing pool. Use this instead @@ -220,7 +228,7 @@ class EPPLibWrapper: try: # Initialize epplib CLIENT = EPPLibWrapper() - logger.debug("registry client initialized") + logger.info("registry client initialized") except Exception: CLIENT = None # type: ignore logger.warning( diff --git a/src/epplibwrapper/socket.py b/src/epplibwrapper/socket.py index c6ffe20bf..19ad2bd0b 100644 --- a/src/epplibwrapper/socket.py +++ b/src/epplibwrapper/socket.py @@ -16,7 +16,7 @@ logger = logging.getLogger(__name__) class Socket: """Context manager which establishes a TCP connection with registry.""" - def __init__(self, client: commands.Login, login: Client) -> None: + def __init__(self, client: Client, login: commands.Login) -> None: """Save the epplib client and login details.""" self.client = client self.login = login @@ -29,7 +29,7 @@ class Socket: """Runs disconnect(), which closes a connection with EPPLib.""" self.disconnect() - def connect(self, pass_response_only=False): + def connect(self): """Use epplib to connect.""" self.client.connect() response = self.client.send(self.login) diff --git a/src/epplibwrapper/tests/test_pool.py b/src/epplibwrapper/tests/test_pool.py index f77607d56..fedd9f7a4 100644 --- a/src/epplibwrapper/tests/test_pool.py +++ b/src/epplibwrapper/tests/test_pool.py @@ -1,10 +1,14 @@ from unittest import skip -from unittest.mock import patch +from unittest.mock import MagicMock, patch from django.conf import settings from django.test import Client +from django.test import TestCase from epplibwrapper.client import EPPLibWrapper +from epplibwrapper.utility.pool import EPPConnectionPool +from registrar.models.domain import Domain from registrar.tests.common import MockEppLib +from registrar.models.domain import registry import logging @@ -12,24 +16,17 @@ try: from epplib.client import Client from epplib import commands from epplib.exceptions import TransportError - from epplib.transport import SocketTransport + from epplib.responses import base except ImportError: pass logger = logging.getLogger(__name__) -@patch("djangooidc.views.CLIENT", autospec=True) -class TestConnectionPool(MockEppLib): +class TestConnectionPool(TestCase): """Tests for our connection pooling behaviour""" def setUp(self): - """ - Background: - Given the registrant is logged in - And the registrant is the admin on a domain - """ - super().setUp() self.pool_options = { # Current pool size "size": 1, @@ -40,10 +37,45 @@ class TestConnectionPool(MockEppLib): "keepalive": 60, } - def tearDown(self): - super().tearDown() + # Mock a successful connection + self.mock_connect_patch = patch("epplib.client.Client.connect") + self.mocked_connect_function = self.mock_connect_patch.start() + self.mocked_connect_function.side_effect = self.mock_connect - def user_info(*args): + # Mock the send behaviour + self.mock_send_patch = patch("epplib.client.Client.send") + self.mocked_send_function = self.mock_send_patch.start() + self.mocked_send_function.side_effect = self.mock_send + + # Mock the pool object + self.mockSendPatch = patch("registrar.models.domain.registry._pool") + self.mockedSendFunction = self.mockSendPatch.start() + self.mockedSendFunction.side_effect = self.fake_pool + + def tearDown(self): + self.mock_send_patch.stop() + self.mock_connect_patch.stop() + self.mockSendPatch.stop() + + def mock_connect(self, _request): + return None + + def mock_send(self, _request): + if isinstance(_request, commands.Login): + response = 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 response + return None + + def user_info(self, *args): return { "sub": "TEST", "email": "test@example.com", @@ -52,67 +84,19 @@ class TestConnectionPool(MockEppLib): "phone": "814564000", } - def test_pool_created_successfully(self, mock_client): - # setup - session = self.client.session - session["state"] = "TEST" # nosec B105 - session.save() - # mock + @patch("djangooidc.views.CLIENT", autospec=True) + def fake_pool(self, mock_client): + # mock client mock_client.callback.side_effect = self.user_info + # Create a mock transport object + mock_login = MagicMock() + mock_login.cert_file = "path/to/cert_file" + mock_login.key_file = "path/to/key_file" - client = EPPLibWrapper() - pool = client._pool - - # These are defined outside of the pool, - # so we can reimplement how this is being done - # in client.py. They should remain unchanged, - # and if they aren't, something went wrong. - expected_login = commands.Login( - cl_id="nothing", - password="nothing", - obj_uris=[ - "urn:ietf:params:xml:ns:domain-1.0", - "urn:ietf:params:xml:ns:contact-1.0", - ], - new_pw=None, - version="1.0", - lang="en", - ext_uris=[], + pool = EPPConnectionPool( + client=mock_client, login=mock_login, options=self.pool_options ) - - # Key/cert will generate a new file everytime. - # This should never be null, so we can check for that. - try: - expected_client = Client( - SocketTransport( - settings.SECRET_REGISTRY_HOSTNAME, - cert_file=pool._client.transport.cert_file, - key_file=pool._client.transport.key_file, - password=settings.SECRET_REGISTRY_KEY_PASSPHRASE, - ) - ).__dict__ - except Exception as err: - self.fail(err) - - # We don't care about checking if the objects are both of - # the same reference, we only care about data parity, so - # we do a dict conversion. - actual_client = pool._client.__dict__ - actual_client["transport"] = actual_client["transport"].__dict__ - expected_client["transport"] = expected_client["transport"].__dict__ - - # Ensure that we're getting the credentials we expect - self.assertEqual(pool._login, expected_login) - self.assertEqual(actual_client, expected_client) - - # Check that options are set correctly - self.assertEqual(pool.size, self.pool_options["size"]) - self.assertEqual(pool.keepalive, self.pool_options["keepalive"]) - self.assertEqual(pool.exc_classes, self.pool_options["exc_classes"]) - - # Check that it is running - self.assertEqual(client.pool_status.connection_success, True) - self.assertEqual(client.pool_status.pool_running, True) + return pool @skip("not implemented yet") def test_pool_timesout(self): @@ -124,7 +108,33 @@ class TestConnectionPool(MockEppLib): """Multiple users send data concurrently""" raise - @skip("not implemented yet") + def test_pool_tries_create_invalid(self): + """A .send is invoked on the pool, but the pool + shouldn't be running.""" + # Fake data for the _pool object + domain, _ = Domain.objects.get_or_create(name="freeman.gov") + + # Trigger the getter - should fail + expected_contact = domain.security_contact + self.assertEqual(registry.pool_status.pool_running, False) + self.assertEqual(registry.pool_status.connection_success, False) + self.assertEqual(len(registry._pool.conn), 0) + def test_pool_sends_data(self): - """A .send is invoked on the pool""" - raise + """A .send is invoked on the pool successfully""" + # Fake data for the _pool object + domain, _ = Domain.objects.get_or_create(name="freeman.gov") + + # The connection pool will fail to start, start it manually + # so that our mocks can take over + registry.start_connection_pool(try_start_if_invalid=True) + + # Pretend that we've connected + registry.pool_status.pool_running = True + registry.pool_status.connection_success = True + + # Trigger the getter - should succeed + expected_contact = domain.security_contact + self.assertEqual(registry.pool_status.pool_running, True) + self.assertEqual(registry.pool_status.connection_success, True) + self.assertEqual(len(registry._pool.conn), self.pool_options["size"]) diff --git a/src/epplibwrapper/utility/pool.py b/src/epplibwrapper/utility/pool.py index df4ff993d..6d51e539f 100644 --- a/src/epplibwrapper/utility/pool.py +++ b/src/epplibwrapper/utility/pool.py @@ -1,8 +1,8 @@ import logging import gevent from geventconnpool import ConnectionPool -from epplibwrapper.errors import RegistryError, LoginError from epplibwrapper.socket import Socket +from epplibwrapper.utility.pool_error import PoolError, PoolErrorCodes try: from epplib.commands import Hello @@ -33,10 +33,13 @@ class EPPConnectionPool(ConnectionPool): try: connection = socket.connect() return connection - except LoginError as err: - message = "_new_connection failed to execute due to a registry login error." + except Exception as err: + message = f"Failed to execute due to a registry login error: {err}" logger.error(message, exc_info=True) - raise RegistryError(message) from err + # 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.""" @@ -44,8 +47,9 @@ class EPPConnectionPool(ConnectionPool): # Sends a ping to EPPLib c.send(Hello()) except Exception as err: - logger.error("Failed to keep the connection alive.", exc_info=True) - raise RegistryError("Failed to keep the connection alive.") from err + message = "Failed to keep the connection alive." + logger.error(message, exc_info=True) + raise PoolError(code=PoolErrorCodes.KEEP_ALIVE_FAILED) from err def _create_socket(self, client, login) -> Socket: """Creates and returns a socket instance""" @@ -64,7 +68,6 @@ class EPPConnectionPool(ConnectionPool): # Clear the semaphore for i in range(self.lock.counter): self.lock.release() - # TODO - connection pool err except Exception as err: logger.error("Could not kill all connections.") raise err diff --git a/src/epplibwrapper/utility/pool_error.py b/src/epplibwrapper/utility/pool_error.py new file mode 100644 index 000000000..0bcd36a53 --- /dev/null +++ b/src/epplibwrapper/utility/pool_error.py @@ -0,0 +1,44 @@ +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 + """ + + # For linter + kill_failed = "Could not kill all connections." + conn_failed = "Failed to execute due to a registry login error." + alive_failed = "Failed to keep the connection alive." + _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}" diff --git a/src/registrar/models/domain.py b/src/registrar/models/domain.py index 319be9fae..07d92f757 100644 --- a/src/registrar/models/domain.py +++ b/src/registrar/models/domain.py @@ -16,32 +16,25 @@ from registrar.utility.errors import ( NameserverErrorCodes as nsErrorCodes, ) -from registrar.models.utility.contact_error import ContactError, ContactErrorCodes +from epplibwrapper import ( + CLIENT as registry, + commands, + common as epp, + extensions, + info as eppInfo, + RegistryError, + ErrorCode, +) +from registrar.models.utility.contact_error import ContactError, ContactErrorCodes from .utility.domain_field import DomainField from .utility.domain_helper import DomainHelper from .utility.time_stamped_model import TimeStampedModel from .public_contact import PublicContact + logger = logging.getLogger(__name__) -try: - from epplibwrapper import ( - CLIENT as registry, - commands, - common as epp, - extensions, - info as eppInfo, - RegistryError, - ErrorCode, - ) -except ImportError as err: - logger.error("An import error occured....") - logger.error(err) - raise err - - - class Domain(TimeStampedModel, DomainHelper): From b6921b3f4ce3156b03b471918165d634a4960d66 Mon Sep 17 00:00:00 2001 From: zandercymatics <141044360+zandercymatics@users.noreply.github.com> Date: Tue, 17 Oct 2023 11:26:47 -0600 Subject: [PATCH 030/120] Linter --- src/epplibwrapper/tests/test_pool.py | 5 ----- src/registrar/config/settings.py | 2 +- 2 files changed, 1 insertion(+), 6 deletions(-) diff --git a/src/epplibwrapper/tests/test_pool.py b/src/epplibwrapper/tests/test_pool.py index fedd9f7a4..6573a64f5 100644 --- a/src/epplibwrapper/tests/test_pool.py +++ b/src/epplibwrapper/tests/test_pool.py @@ -2,21 +2,16 @@ from unittest import skip from unittest.mock import MagicMock, patch from django.conf import settings -from django.test import Client from django.test import TestCase -from epplibwrapper.client import EPPLibWrapper from epplibwrapper.utility.pool import EPPConnectionPool from registrar.models.domain import Domain -from registrar.tests.common import MockEppLib from registrar.models.domain import registry import logging try: - from epplib.client import Client from epplib import commands from epplib.exceptions import TransportError - from epplib.responses import base except ImportError: pass diff --git a/src/registrar/config/settings.py b/src/registrar/config/settings.py index 9d03dbc89..fd3893617 100644 --- a/src/registrar/config/settings.py +++ b/src/registrar/config/settings.py @@ -546,7 +546,7 @@ 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 # Ping every 20 seconds +POOL_KEEP_ALIVE = 60 # Determines how long we try to keep a pool alive for, # before restarting it. From 0dde277c864d308075042dd74a740b86884c191c Mon Sep 17 00:00:00 2001 From: zandercymatics <141044360+zandercymatics@users.noreply.github.com> Date: Tue, 17 Oct 2023 12:40:15 -0600 Subject: [PATCH 031/120] Test changes --- src/epplibwrapper/client.py | 18 +++++++++++++++--- src/epplibwrapper/tests/test_pool.py | 17 +++++++++++++---- 2 files changed, 28 insertions(+), 7 deletions(-) diff --git a/src/epplibwrapper/client.py b/src/epplibwrapper/client.py index 920dc284d..680a6661c 100644 --- a/src/epplibwrapper/client.py +++ b/src/epplibwrapper/client.py @@ -121,6 +121,7 @@ class EPPLibWrapper: logger.error(message, exc_info=True) raise RegistryError(message) from err else: + print(f"test thing {response}") if response.code >= 2000: raise RegistryError(response.msg, code=response.code) else: @@ -160,6 +161,16 @@ class EPPLibWrapper: else: # don't try again raise err + def get_pool(self): + """Get the current pool instance""" + return self._pool + + def _create_pool(self, client, login, options): + """Creates and returns new pool instance""" + return EPPConnectionPool( + client, login, options + ) + def start_connection_pool( self, restart_pool_if_exists=True, try_start_if_invalid=False ): @@ -193,9 +204,10 @@ class EPPLibWrapper: logger.info("Connection pool restarting...") self.kill_pool() - self._pool = EPPConnectionPool( - client=self._client, login=self._login, options=self.pool_options - ) + self._pool = self._create_pool( + self._client, self._login, self.pool_options + ) + self.pool_status.pool_running = True self.pool_status.pool_hanging = False diff --git a/src/epplibwrapper/tests/test_pool.py b/src/epplibwrapper/tests/test_pool.py index 6573a64f5..e62135221 100644 --- a/src/epplibwrapper/tests/test_pool.py +++ b/src/epplibwrapper/tests/test_pool.py @@ -3,9 +3,11 @@ from unittest.mock import MagicMock, patch from django.conf import settings from django.test import TestCase +from epplibwrapper.client import EPPLibWrapper from epplibwrapper.utility.pool import EPPConnectionPool from registrar.models.domain import Domain from registrar.models.domain import registry +from contextlib import ExitStack import logging @@ -120,16 +122,23 @@ class TestConnectionPool(TestCase): # Fake data for the _pool object domain, _ = Domain.objects.get_or_create(name="freeman.gov") + def start_fake_connection(self): + registry.pool_status.pool_running = True + registry.pool_status.connection_success = True + registry._pool = registry.get_pool() + # The connection pool will fail to start, start it manually # so that our mocks can take over - registry.start_connection_pool(try_start_if_invalid=True) - + with ExitStack() as stack: + stack.enter_context(patch.object(EPPLibWrapper, "get_pool", self.fake_pool)) + stack.enter_context(patch.object(EPPLibWrapper, "start_connection_pool", start_fake_connection)) + expected_contact = domain.security_contact + # Pretend that we've connected registry.pool_status.pool_running = True registry.pool_status.connection_success = True # Trigger the getter - should succeed - expected_contact = domain.security_contact self.assertEqual(registry.pool_status.pool_running, True) self.assertEqual(registry.pool_status.connection_success, True) - self.assertEqual(len(registry._pool.conn), self.pool_options["size"]) + self.assertEqual(len(registry.get_pool().conn), self.pool_options["size"]) From faa35613e23ff6f95cf06a355140b685fe18d7ca Mon Sep 17 00:00:00 2001 From: zandercymatics <141044360+zandercymatics@users.noreply.github.com> Date: Tue, 17 Oct 2023 12:58:03 -0600 Subject: [PATCH 032/120] test --- src/epplibwrapper/__init__.py | 5 +++++ src/epplibwrapper/client.py | 5 +++-- src/epplibwrapper/utility/pool.py | 2 +- 3 files changed, 9 insertions(+), 3 deletions(-) diff --git a/src/epplibwrapper/__init__.py b/src/epplibwrapper/__init__.py index dd6664a3a..b8fb12bcb 100644 --- a/src/epplibwrapper/__init__.py +++ b/src/epplibwrapper/__init__.py @@ -44,6 +44,8 @@ except NameError: # Attn: these imports should NOT be at the top of the file try: + from epplibwrapper.socket import Socket + from epplibwrapper.utility.pool_error import PoolError, PoolErrorCodes from .client import CLIENT, commands from .errors import RegistryError, ErrorCode from epplib.models import common, info @@ -61,4 +63,7 @@ __all__ = [ "info", "ErrorCode", "RegistryError", + "Socket", + "PoolError", + "PoolErrorCodes" ] diff --git a/src/epplibwrapper/client.py b/src/epplibwrapper/client.py index 680a6661c..4f393aa0c 100644 --- a/src/epplibwrapper/client.py +++ b/src/epplibwrapper/client.py @@ -81,7 +81,8 @@ class EPPLibWrapper: self.pool_status = PoolStatus() if start_connection_pool: - self.start_connection_pool() + pass + #self.start_connection_pool() def _send(self, command): """Helper function used by `send`.""" @@ -207,7 +208,7 @@ class EPPLibWrapper: self._pool = self._create_pool( self._client, self._login, self.pool_options ) - + self.pool_status.pool_running = True self.pool_status.pool_hanging = False diff --git a/src/epplibwrapper/utility/pool.py b/src/epplibwrapper/utility/pool.py index 6d51e539f..7a58f7efe 100644 --- a/src/epplibwrapper/utility/pool.py +++ b/src/epplibwrapper/utility/pool.py @@ -3,12 +3,12 @@ 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 except ImportError: pass + logger = logging.getLogger(__name__) From fc5436039d89972daea989b6a661cee4217a898d Mon Sep 17 00:00:00 2001 From: zandercymatics <141044360+zandercymatics@users.noreply.github.com> Date: Tue, 17 Oct 2023 13:06:05 -0600 Subject: [PATCH 033/120] Stuff --- src/epplibwrapper/client.py | 3 +-- src/registrar/config/settings.py | 1 + 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/src/epplibwrapper/client.py b/src/epplibwrapper/client.py index 4f393aa0c..812760330 100644 --- a/src/epplibwrapper/client.py +++ b/src/epplibwrapper/client.py @@ -81,8 +81,7 @@ class EPPLibWrapper: self.pool_status = PoolStatus() if start_connection_pool: - pass - #self.start_connection_pool() + self.start_connection_pool() def _send(self, command): """Helper function used by `send`.""" diff --git a/src/registrar/config/settings.py b/src/registrar/config/settings.py index fd3893617..6c7f32de6 100644 --- a/src/registrar/config/settings.py +++ b/src/registrar/config/settings.py @@ -78,6 +78,7 @@ DEBUG = env_debug # Installing them here makes them available for execution. # Do not access INSTALLED_APPS directly. Use `django.apps.apps` instead. INSTALLED_APPS = [ + "epplibwrapper", # let's be sure to install our own application! # it needs to be listed before django.contrib.admin # otherwise Django would find the default template From d38c33e4d95ca5cb50ecfa8873c1254f0c3dd4db Mon Sep 17 00:00:00 2001 From: zandercymatics <141044360+zandercymatics@users.noreply.github.com> Date: Tue, 17 Oct 2023 13:20:11 -0600 Subject: [PATCH 034/120] Test script change --- .github/actions/django-security-check/entrypoint.sh | 2 +- src/registrar/config/settings.py | 1 - 2 files changed, 1 insertion(+), 2 deletions(-) diff --git a/.github/actions/django-security-check/entrypoint.sh b/.github/actions/django-security-check/entrypoint.sh index 69f8246d7..8e2ca26ab 100755 --- a/.github/actions/django-security-check/entrypoint.sh +++ b/.github/actions/django-security-check/entrypoint.sh @@ -14,7 +14,7 @@ if [[ "$ENV_TYPE" == "pipenv" ]]; then cd $REQS pip3 install pipenv PIPENV_IGNORE_VIRTUALENVS=1 pipenv install - cd $MANAGE_PATH && PIPENV_IGNORE_VIRTUALENVS=1 pipenv run python3 manage.py check --deploy --fail-level ${FAIL} ${ARGS} &> output.txt + cd $MANAGE_PATH && PIPENV_IGNORE_VIRTUALENVS=1 pipenv run python manage.py check --deploy --fail-level ${FAIL} ${ARGS} &> output.txt EXIT_CODE=$? fi if [[ "$ENV_TYPE" == "venv" ]]; then diff --git a/src/registrar/config/settings.py b/src/registrar/config/settings.py index 6c7f32de6..fd3893617 100644 --- a/src/registrar/config/settings.py +++ b/src/registrar/config/settings.py @@ -78,7 +78,6 @@ DEBUG = env_debug # Installing them here makes them available for execution. # Do not access INSTALLED_APPS directly. Use `django.apps.apps` instead. INSTALLED_APPS = [ - "epplibwrapper", # let's be sure to install our own application! # it needs to be listed before django.contrib.admin # otherwise Django would find the default template From 939ff4b06396f830a62cbaa061f0f9466a5cbe75 Mon Sep 17 00:00:00 2001 From: zandercymatics <141044360+zandercymatics@users.noreply.github.com> Date: Tue, 17 Oct 2023 13:34:24 -0600 Subject: [PATCH 035/120] Import fix test --- .github/actions/django-security-check/entrypoint.sh | 2 +- src/registrar/models/__init__.py | 6 +++++- 2 files changed, 6 insertions(+), 2 deletions(-) diff --git a/.github/actions/django-security-check/entrypoint.sh b/.github/actions/django-security-check/entrypoint.sh index 8e2ca26ab..69f8246d7 100755 --- a/.github/actions/django-security-check/entrypoint.sh +++ b/.github/actions/django-security-check/entrypoint.sh @@ -14,7 +14,7 @@ if [[ "$ENV_TYPE" == "pipenv" ]]; then cd $REQS pip3 install pipenv PIPENV_IGNORE_VIRTUALENVS=1 pipenv install - cd $MANAGE_PATH && PIPENV_IGNORE_VIRTUALENVS=1 pipenv run python manage.py check --deploy --fail-level ${FAIL} ${ARGS} &> output.txt + cd $MANAGE_PATH && PIPENV_IGNORE_VIRTUALENVS=1 pipenv run python3 manage.py check --deploy --fail-level ${FAIL} ${ARGS} &> output.txt EXIT_CODE=$? fi if [[ "$ENV_TYPE" == "venv" ]]; then diff --git a/src/registrar/models/__init__.py b/src/registrar/models/__init__.py index f287c401c..0091955b0 100644 --- a/src/registrar/models/__init__.py +++ b/src/registrar/models/__init__.py @@ -1,7 +1,11 @@ from auditlog.registry import auditlog # type: ignore from .contact import Contact -from .domain_application import DomainApplication +try: + from .domain_application import DomainApplication +except ImportError as err: + print(err.with_traceback()) + pass from .domain_information import DomainInformation from .domain import Domain from .draft_domain import DraftDomain From ae01ffd9105d4033f8e22bc683e185d0e1e98c2a Mon Sep 17 00:00:00 2001 From: zandercymatics <141044360+zandercymatics@users.noreply.github.com> Date: Tue, 17 Oct 2023 13:36:03 -0600 Subject: [PATCH 036/120] Update __init__.py --- src/registrar/models/__init__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/registrar/models/__init__.py b/src/registrar/models/__init__.py index 0091955b0..6285b9f09 100644 --- a/src/registrar/models/__init__.py +++ b/src/registrar/models/__init__.py @@ -4,7 +4,7 @@ from .contact import Contact try: from .domain_application import DomainApplication except ImportError as err: - print(err.with_traceback()) + print(err) pass from .domain_information import DomainInformation from .domain import Domain From f1589c8480b0a54f50eeb394c9702449fdeaad91 Mon Sep 17 00:00:00 2001 From: zandercymatics <141044360+zandercymatics@users.noreply.github.com> Date: Tue, 17 Oct 2023 13:41:55 -0600 Subject: [PATCH 037/120] Update __init__.py --- src/registrar/models/__init__.py | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/src/registrar/models/__init__.py b/src/registrar/models/__init__.py index 6285b9f09..5ff1f38e2 100644 --- a/src/registrar/models/__init__.py +++ b/src/registrar/models/__init__.py @@ -1,10 +1,11 @@ from auditlog.registry import auditlog # type: ignore - +import traceback from .contact import Contact try: from .domain_application import DomainApplication except ImportError as err: - print(err) + print("Error traceback is...") + print(traceback.format_exc()) pass from .domain_information import DomainInformation from .domain import Domain From d50de8516ea08f6cd5a84bde48cc06e897c323ee Mon Sep 17 00:00:00 2001 From: zandercymatics <141044360+zandercymatics@users.noreply.github.com> Date: Tue, 17 Oct 2023 13:44:05 -0600 Subject: [PATCH 038/120] Change import order --- src/registrar/models/__init__.py | 12 +++--------- 1 file changed, 3 insertions(+), 9 deletions(-) diff --git a/src/registrar/models/__init__.py b/src/registrar/models/__init__.py index 5ff1f38e2..63f0bbb54 100644 --- a/src/registrar/models/__init__.py +++ b/src/registrar/models/__init__.py @@ -1,14 +1,8 @@ from auditlog.registry import auditlog # type: ignore -import traceback from .contact import Contact -try: - from .domain_application import DomainApplication -except ImportError as err: - print("Error traceback is...") - print(traceback.format_exc()) - pass -from .domain_information import DomainInformation from .domain import Domain +from .domain_application import DomainApplication +from .domain_information import DomainInformation from .draft_domain import DraftDomain from .host_ip import HostIP from .host import Host @@ -23,9 +17,9 @@ from .transition_domain import TransitionDomain __all__ = [ "Contact", + "Domain", "DomainApplication", "DomainInformation", - "Domain", "DraftDomain", "DomainInvitation", "HostIP", From d943b69b3d353a8a54eb8abc2f4b032f88a9067f Mon Sep 17 00:00:00 2001 From: zandercymatics <141044360+zandercymatics@users.noreply.github.com> Date: Tue, 17 Oct 2023 14:49:42 -0600 Subject: [PATCH 039/120] Mocked sockets --- src/epplibwrapper/client.py | 6 ++-- src/epplibwrapper/tests/test_pool.py | 50 ++++++++++++++++++---------- src/registrar/models/__init__.py | 4 +-- 3 files changed, 37 insertions(+), 23 deletions(-) diff --git a/src/epplibwrapper/client.py b/src/epplibwrapper/client.py index 812760330..a465593a0 100644 --- a/src/epplibwrapper/client.py +++ b/src/epplibwrapper/client.py @@ -168,7 +168,7 @@ class EPPLibWrapper: def _create_pool(self, client, login, options): """Creates and returns new pool instance""" return EPPConnectionPool( - client, login, options + client, login, options ) def start_connection_pool( @@ -190,8 +190,8 @@ class EPPLibWrapper: # one socket, and if successful, then we know we can connect. if ( not try_start_if_invalid - and settings.DEBUG - or not self._test_registry_connection_success() + and (settings.DEBUG + or not self._test_registry_connection_success()) ): logger.warning("Cannot contact the Registry") self.pool_status.connection_success = False diff --git a/src/epplibwrapper/tests/test_pool.py b/src/epplibwrapper/tests/test_pool.py index e62135221..d088697ec 100644 --- a/src/epplibwrapper/tests/test_pool.py +++ b/src/epplibwrapper/tests/test_pool.py @@ -4,6 +4,7 @@ from django.conf import settings from django.test import TestCase from epplibwrapper.client import EPPLibWrapper +from epplibwrapper.socket import Socket from epplibwrapper.utility.pool import EPPConnectionPool from registrar.models.domain import Domain from registrar.models.domain import registry @@ -121,24 +122,37 @@ class TestConnectionPool(TestCase): """A .send is invoked on the pool successfully""" # Fake data for the _pool object domain, _ = Domain.objects.get_or_create(name="freeman.gov") - - def start_fake_connection(self): - registry.pool_status.pool_running = True - registry.pool_status.connection_success = True - registry._pool = registry.get_pool() - # The connection pool will fail to start, start it manually - # so that our mocks can take over + def fake_send(self): + return 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, + ) + with ExitStack() as stack: - stack.enter_context(patch.object(EPPLibWrapper, "get_pool", self.fake_pool)) - stack.enter_context(patch.object(EPPLibWrapper, "start_connection_pool", start_fake_connection)) - expected_contact = domain.security_contact - - # Pretend that we've connected - registry.pool_status.pool_running = True - registry.pool_status.connection_success = True + stack.enter_context(patch.object(Socket, "connect", None)) + stack.enter_context(patch.object(Socket, "send", fake_send)) + stack.enter_context(patch.object(Socket, "_create_socket", Socket())) + #stack.enter_context(patch.object(EPPLibWrapper, "get_pool", self.fake_pool)) + pool = EPPLibWrapper(False) + # The connection pool will fail to start, start it manually + # so that our mocks can take over + pool.start_connection_pool(try_start_if_invalid=True) + print(f"this is pool {pool._pool.__dict__}") + # Pool should be running, and be the right size + self.assertEqual(pool.pool_status.pool_running, True) + self.assertEqual(pool.pool_status.connection_success, True) + pool.send(commands.InfoDomain(name="test.gov"), cleaned=True) + self.assertEqual(len(pool._pool.conn), self.pool_options["size"]) + + #pool.send() + + # Trigger the getter - should succeed + #expected_contact = domain.security_contact + - # Trigger the getter - should succeed - self.assertEqual(registry.pool_status.pool_running, True) - self.assertEqual(registry.pool_status.connection_success, True) - self.assertEqual(len(registry.get_pool().conn), self.pool_options["size"]) diff --git a/src/registrar/models/__init__.py b/src/registrar/models/__init__.py index 63f0bbb54..1d28c9e89 100644 --- a/src/registrar/models/__init__.py +++ b/src/registrar/models/__init__.py @@ -1,8 +1,8 @@ from auditlog.registry import auditlog # type: ignore from .contact import Contact -from .domain import Domain from .domain_application import DomainApplication from .domain_information import DomainInformation +from .domain import Domain from .draft_domain import DraftDomain from .host_ip import HostIP from .host import Host @@ -17,9 +17,9 @@ from .transition_domain import TransitionDomain __all__ = [ "Contact", - "Domain", "DomainApplication", "DomainInformation", + "Domain", "DraftDomain", "DomainInvitation", "HostIP", From 2f4425d9144266f642b3f5ee13edd20e54f322dc Mon Sep 17 00:00:00 2001 From: zandercymatics <141044360+zandercymatics@users.noreply.github.com> Date: Tue, 17 Oct 2023 14:53:57 -0600 Subject: [PATCH 040/120] Fix bad logic --- src/epplibwrapper/socket.py | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/src/epplibwrapper/socket.py b/src/epplibwrapper/socket.py index 19ad2bd0b..716fef18a 100644 --- a/src/epplibwrapper/socket.py +++ b/src/epplibwrapper/socket.py @@ -61,7 +61,13 @@ class Socket: return False else: self.disconnect() - return not self.is_login_error(response.code) + + # If we encounter a login error, fail + if self.is_login_error(response.code): + return False + + # otherwise, just return true + return True def disconnect(self): """Close the connection.""" From bbd3feaeec6ecc611d84e3d9f45eb8494e0baf15 Mon Sep 17 00:00:00 2001 From: Rachid Mrad Date: Tue, 17 Oct 2023 17:17:02 -0400 Subject: [PATCH 041/120] Axe all Key data components except in model --- src/registrar/config/urls.py | 5 - src/registrar/forms/__init__.py | 2 - src/registrar/forms/common.py | 15 +- src/registrar/forms/domain.py | 43 ----- src/registrar/models/domain.py | 9 +- src/registrar/templates/domain_dnssec.html | 28 +--- src/registrar/templates/domain_dsdata.html | 32 +--- src/registrar/templates/domain_keydata.html | 110 ------------- src/registrar/templates/domain_sidebar.html | 11 +- src/registrar/tests/common.py | 15 -- src/registrar/tests/test_models_domain.py | 73 -------- src/registrar/tests/test_views.py | 118 +------------ src/registrar/views/__init__.py | 1 - src/registrar/views/domain.py | 174 ++------------------ 14 files changed, 28 insertions(+), 608 deletions(-) delete mode 100644 src/registrar/templates/domain_keydata.html diff --git a/src/registrar/config/urls.py b/src/registrar/config/urls.py index bd2215620..c00d1c589 100644 --- a/src/registrar/config/urls.py +++ b/src/registrar/config/urls.py @@ -100,11 +100,6 @@ urlpatterns = [ views.DomainDsDataView.as_view(), name="domain-dns-dnssec-dsdata", ), - path( - "domain//dns/dnssec/keydata", - views.DomainKeyDataView.as_view(), - name="domain-dns-dnssec-keydata", - ), path( "domain//your-contact-information", views.DomainYourContactInformationView.as_view(), diff --git a/src/registrar/forms/__init__.py b/src/registrar/forms/__init__.py index 7d2baf646..c3aa89fed 100644 --- a/src/registrar/forms/__init__.py +++ b/src/registrar/forms/__init__.py @@ -8,6 +8,4 @@ from .domain import ( DomainDnssecForm, DomainDsdataFormset, DomainDsdataForm, - DomainKeydataFormset, - DomainKeydataForm, ) diff --git a/src/registrar/forms/common.py b/src/registrar/forms/common.py index 159113488..585d5ed3e 100644 --- a/src/registrar/forms/common.py +++ b/src/registrar/forms/common.py @@ -1,6 +1,6 @@ # common.py # -# ALGORITHM_CHOICES are options for alg attribute in DS Data and Key Data +# ALGORITHM_CHOICES are options for alg attribute in DS Data # reference: # https://www.iana.org/assignments/dns-sec-alg-numbers/dns-sec-alg-numbers.xhtml ALGORITHM_CHOICES = [ @@ -24,15 +24,4 @@ DIGEST_TYPE_CHOICES = [ (0, "(0) Reserved"), (1, "(1) SHA-256"), ] -# PROTOCOL_CHOICES are options for protocol attribute in Key Data -# reference: https://datatracker.ietf.org/doc/html/rfc4034#section-2.1.2 -PROTOCOL_CHOICES = [ - (3, "(3) DNSSEC"), -] -# FLAG_CHOICES are options for flags attribute in Key Data -# reference: https://datatracker.ietf.org/doc/html/rfc4034#section-2.1.1 -FLAG_CHOICES = [ - (0, "(0)"), - (256, "(256) ZSK"), - (257, "(257) KSK"), -] + diff --git a/src/registrar/forms/domain.py b/src/registrar/forms/domain.py index 8abc7e14a..6bbade5ef 100644 --- a/src/registrar/forms/domain.py +++ b/src/registrar/forms/domain.py @@ -10,8 +10,6 @@ from ..models import Contact, DomainInformation from .common import ( ALGORITHM_CHOICES, DIGEST_TYPE_CHOICES, - FLAG_CHOICES, - PROTOCOL_CHOICES, ) @@ -188,44 +186,3 @@ DomainDsdataFormset = formset_factory( extra=0, can_delete=True, ) - - -class DomainKeydataForm(forms.Form): - """Form for adding or editing DNSSEC Key Data to a domain.""" - - flag = forms.TypedChoiceField( - required=True, - label="Flag", - coerce=int, - choices=FLAG_CHOICES, - error_messages={"required": ("Flag is required.")}, - ) - - protocol = forms.TypedChoiceField( - required=True, - label="Protocol", - coerce=int, - choices=PROTOCOL_CHOICES, - error_messages={"required": ("Protocol is required.")}, - ) - - algorithm = forms.TypedChoiceField( - required=True, - label="Algorithm", - coerce=int, - choices=[(None, "--Select--")] + ALGORITHM_CHOICES, # type: ignore - error_messages={"required": ("Algorithm is required.")}, - ) - - pub_key = forms.CharField( - required=True, - label="Pub key", - error_messages={"required": ("Pub key is required.")}, - ) - - -DomainKeydataFormset = formset_factory( - DomainKeydataForm, - extra=0, - can_delete=True, -) diff --git a/src/registrar/models/domain.py b/src/registrar/models/domain.py index bab993b04..1aad4fef4 100644 --- a/src/registrar/models/domain.py +++ b/src/registrar/models/domain.py @@ -486,12 +486,11 @@ class Domain(TimeStampedModel, DomainHelper): addExtension: dict remExtension: dict - addExtension includes all dsData or keyData to be added - remExtension includes all dsData or keyData to be removed + addExtension includes all dsData to be added + remExtension includes all dsData to be removed - method operates on dsData OR keyData, never a mix of the two; - operates based on which is present in _dnssecdata; - if neither is present, addExtension will be empty dict, and + method operates on dsData; + if dsData is not present, addExtension will be empty dict, and remExtension will be all existing dnssecdata to be deleted """ diff --git a/src/registrar/templates/domain_dnssec.html b/src/registrar/templates/domain_dnssec.html index 5eedb2184..c4a19470e 100644 --- a/src/registrar/templates/domain_dnssec.html +++ b/src/registrar/templates/domain_dnssec.html @@ -7,14 +7,14 @@

DNSSEC

-

DNSSEC, or DNS Security Extensions, is additional security layer to protect your domain. Enabling DNSSEC ensures that when someone visits your domain, they can be certain that it's connecting to the correct server, preventing potential hijacking or tampering with your domain's records.

+

DNSSEC, or DNS Security Extensions, is additional security layer to protect your website. Enabling DNSSEC ensures that when someone visits your domain, they can be certain that it's connecting to the correct server, preventing potential hijacking or tampering with your domain's records.

{% csrf_token %} {% if has_dnssec_records %}
- In order to fully disable DNSSEC on your domain, you will need to work with your DNS provider to remove your DNSSEC-related records from your zone. + In order to fully disable DNSSEC on your domain, you will need to work with your DNS provider to remove your DNSSEC-related records from your zone.
Disable DNSSEC - {% elif dnssec_enabled %} -
-

Add DS Records

-

In order to enable DNSSEC and add Delegation Signer (DS) records, you must first configure it with your DNS hosting service. Your configuration will determine whether you need to add DS Data or Key Data. Contact your DNS hosting provider if you are unsure which record type to add.

-

- Add DS Data - Add Key Data - -

-
{% else %}
- It is strongly recommended that you only enable DNSSEC if you know how to set it up properly at your hosting service. If you make a mistake, it could cause your domain name to stop working. + It is strongly recommended that you only enable DNSSEC if you know how to set it up properly at your hosting service. If you make a mistake, it could cause your domain name to stop working.
- + Enable DNSSEC
{% endif %}
diff --git a/src/registrar/templates/domain_dsdata.html b/src/registrar/templates/domain_dsdata.html index ca4dce783..ac38bd87f 100644 --- a/src/registrar/templates/domain_dsdata.html +++ b/src/registrar/templates/domain_dsdata.html @@ -8,41 +8,17 @@ {% include "includes/form_errors.html" with form=form %} {% endfor %} - {% if domain.dnssecdata is None and not dnssec_ds_confirmed %} + {% if domain.dnssecdata is None %}
- You have no DS Data added. Enable DNSSEC by adding DS Data or return to the DNSSEC page and click 'enable.' + You have no DS Data added. Enable DNSSEC by adding DS Data.
{% endif %}

DS Data

- {% if domain.dnssecdata is not None and domain.dnssecdata.keyData is not None %} -
-
-

Warning, you cannot add DS Data

-

- You cannot add DS Data because you have already added Key Data. Delete your Key Data records in order to add DS Data. -

-
-
- {% elif not dnssec_ds_confirmed %} -

In order to enable DNSSEC, you must first configure it with your DNS hosting service.

-

Enter the values given by your DNS provider for DS Data.

-

Required fields are marked with an asterisk (*).

-
- {% csrf_token %} - -
- {% else %} +

In order to enable DNSSEC, you must first configure it with your DNS hosting service.

Enter the values given by your DNS provider for DS Data.

{% include "includes/required_fields.html" %} @@ -119,5 +95,5 @@ >Cancel - {% endif %} + {% endblock %} {# domain_content #} diff --git a/src/registrar/templates/domain_keydata.html b/src/registrar/templates/domain_keydata.html deleted file mode 100644 index 167d86370..000000000 --- a/src/registrar/templates/domain_keydata.html +++ /dev/null @@ -1,110 +0,0 @@ -{% extends "domain_base.html" %} -{% load static field_helpers url_helpers %} - -{% block title %}Key Data | {{ domain.name }} | {% endblock %} - -{% block domain_content %} - {% for form in formset %} - {% include "includes/form_errors.html" with form=form %} - {% endfor %} - -

Key Data

- - {% if domain.dnssecdata is not None and domain.dnssecdata.dsData is not None %} -
-
-

Warning, you cannot add Key Data

-

- You cannot add Key Data because you have already added DS Data. Delete your DS Data records in order to add Key Data. -

-
-
- {% elif not dnssec_key_confirmed %} -

In order to enable DNSSEC and add DS records, you must first configure it with your DNS hosting service. Your configuration will determine whether you need to add DS Data or Key Data. Contact your DNS hosting provider if you are unsure which record type to add.

-
- {% csrf_token %} - -
- {% else %} - -

Enter the values given by your DNS provider for DS Key Data.

- {% include "includes/required_fields.html" %} - -
- {% csrf_token %} - {{ formset.management_form }} - - {% for form in formset %} -
- - DS Data record {{forloop.counter}} - -

DS Data record {{forloop.counter}}

- -
-
- {% with attr_required=True add_group_class="usa-form-group--unstyled-error" %} - {% input_with_errors form.flag %} - {% endwith %} -
-
- {% with attr_required=True add_group_class="usa-form-group--unstyled-error" %} - {% input_with_errors form.protocol %} - {% endwith %} -
-
- {% with attr_required=True add_group_class="usa-form-group--unstyled-error" %} - {% input_with_errors form.algorithm %} - {% endwith %} -
-
- -
-
- {% with attr_required=True add_group_class="usa-form-group--unstyled-error" %} - {% input_with_errors form.pub_key %} - {% endwith %} -
-
- -
-
- -
-
- -
- {% endfor %} - - - - -
- -
- -
- {% endif %} -{% endblock %} {# domain_content #} diff --git a/src/registrar/templates/domain_sidebar.html b/src/registrar/templates/domain_sidebar.html index 1acd87eeb..fda3b322d 100644 --- a/src/registrar/templates/domain_sidebar.html +++ b/src/registrar/templates/domain_sidebar.html @@ -34,7 +34,7 @@ > DNSSEC - {% if domain.dnssecdata is not None or request.path|startswith:url and request.path|endswith:'data' %} + {% if domain.dnssecdata is not None or request.path|startswith:url and request.path|endswith:'dsdata' %}
  • {% url 'domain-dns-dnssec-dsdata' pk=domain.id as url %} @@ -44,15 +44,6 @@ DS Data
  • - -
  • - {% url 'domain-dns-dnssec-keydata' pk=domain.id as url %} - - DS Key Data - -
{% endif %} diff --git a/src/registrar/tests/common.py b/src/registrar/tests/common.py index 7ae107006..239e50ae8 100644 --- a/src/registrar/tests/common.py +++ b/src/registrar/tests/common.py @@ -732,12 +732,6 @@ class MockEppLib(TestCase): "digestType": 1, "digest": "ec0bdd990b39feead889f0ba613db4adecb4adec", } - keyDataDict = { - "flags": 257, - "protocol": 3, - "alg": 1, - "pubKey": "AQPJ////4Q==", - } dnssecExtensionWithDsData = extensions.DNSSECExtension( **{ "dsData": [ @@ -753,11 +747,6 @@ class MockEppLib(TestCase): ], # type: ignore } ) - dnssecExtensionWithKeyData = extensions.DNSSECExtension( - **{ - "keyData": [common.DNSSECKeyData(**keyDataDict)], # type: ignore - } - ) dnssecExtensionRemovingDsData = extensions.DNSSECExtension() infoDomainHasIP = fakedEppObject( @@ -851,10 +840,6 @@ class MockEppLib(TestCase): self.mockDataInfoDomain, self.dnssecExtensionWithMultDsData, ), - "dnssec-keydata.gov": ( - self.mockDataInfoDomain, - self.dnssecExtensionWithKeyData, - ), "dnssec-none.gov": (self.mockDataInfoDomain, None), "my-nameserver.gov": ( self.infoDomainTwoHosts diff --git a/src/registrar/tests/test_models_domain.py b/src/registrar/tests/test_models_domain.py index ef3084f9c..e612d7b22 100644 --- a/src/registrar/tests/test_models_domain.py +++ b/src/registrar/tests/test_models_domain.py @@ -1798,79 +1798,6 @@ class TestRegistrantDNSSEC(MockEppLib): patcher.stop() - def test_user_adds_dnssec_keydata(self): - """ - Scenario: Registrant adds DNSSEC key data. - Verify that both the setter and getter are functioning properly - - This test verifies: - 1 - setter calls UpdateDomain command - 2 - setter adds the UpdateDNSSECExtension extension to the command - 3 - setter causes the getter to call info domain on next get from cache - 4 - getter properly parses dnssecdata from InfoDomain response and sets to cache - - """ - - # need to use a separate patcher and side_effect for this test, as - # response from InfoDomain must be different for different iterations - # of the same command - def side_effect(_request, cleaned): - if isinstance(_request, commands.InfoDomain): - if mocked_send.call_count == 1: - return MagicMock(res_data=[self.mockDataInfoDomain]) - else: - return MagicMock( - res_data=[self.mockDataInfoDomain], - extensions=[self.dnssecExtensionWithKeyData], - ) - else: - return MagicMock(res_data=[self.mockDataInfoHosts]) - - patcher = patch("registrar.models.domain.registry.send") - mocked_send = patcher.start() - mocked_send.side_effect = side_effect - - domain, _ = Domain.objects.get_or_create(name="dnssec-keydata.gov") - - domain.dnssecdata = self.dnssecExtensionWithKeyData - # get the DNS SEC extension added to the UpdateDomain command - # and verify that it is properly sent - # args[0] is the _request sent to registry - args, _ = mocked_send.call_args - # assert that the extension matches - self.assertEquals( - args[0].extensions[0], - self.createUpdateExtension(self.dnssecExtensionWithKeyData), - ) - # test that the dnssecdata getter is functioning properly - dnssecdata_get = domain.dnssecdata - mocked_send.assert_has_calls( - [ - call( - commands.UpdateDomain( - name="dnssec-keydata.gov", - nsset=None, - keyset=None, - registrant=None, - auth_info=None, - ), - cleaned=True, - ), - call( - commands.InfoDomain( - name="dnssec-keydata.gov", - ), - cleaned=True, - ), - ] - ) - - self.assertEquals( - dnssecdata_get.keyData, self.dnssecExtensionWithKeyData.keyData - ) - - patcher.stop() - def test_update_is_unsuccessful(self): """ Scenario: An update to the dns data is unsuccessful diff --git a/src/registrar/tests/test_views.py b/src/registrar/tests/test_views.py index 06fddfde7..e8946c52c 100644 --- a/src/registrar/tests/test_views.py +++ b/src/registrar/tests/test_views.py @@ -1075,7 +1075,6 @@ class TestWithDomainPermissions(TestWithUser): self.domain_multdsdata, _ = Domain.objects.get_or_create( name="dnssec-multdsdata.gov" ) - self.domain_keydata, _ = Domain.objects.get_or_create(name="dnssec-keydata.gov") # We could simply use domain (igorville) but this will be more readable in tests # that inherit this setUp self.domain_dnssec_none, _ = Domain.objects.get_or_create( @@ -1090,9 +1089,6 @@ class TestWithDomainPermissions(TestWithUser): DomainInformation.objects.get_or_create( creator=self.user, domain=self.domain_multdsdata ) - DomainInformation.objects.get_or_create( - creator=self.user, domain=self.domain_keydata - ) DomainInformation.objects.get_or_create( creator=self.user, domain=self.domain_dnssec_none ) @@ -1107,9 +1103,6 @@ class TestWithDomainPermissions(TestWithUser): domain=self.domain_multdsdata, role=UserDomainRole.Roles.ADMIN, ) - UserDomainRole.objects.get_or_create( - user=self.user, domain=self.domain_keydata, role=UserDomainRole.Roles.ADMIN - ) UserDomainRole.objects.get_or_create( user=self.user, domain=self.domain_dnssec_none, @@ -1561,38 +1554,13 @@ class TestDomainDNSSEC(TestDomainOverview): def test_dnssec_page_refreshes_enable_button(self): """DNSSEC overview page loads when domain has no DNSSEC data - and shows a 'Enable DNSSEC' button. When button is clicked the template - updates. When user navigates away then comes back to the page, the - 'Enable DNSSEC' button is shown again.""" - # home_page = self.app.get("/") - + and shows a 'Enable DNSSEC' button.""" + page = self.client.get( reverse("domain-dns-dnssec", kwargs={"pk": self.domain.id}) ) self.assertContains(page, "Enable DNSSEC") - # Prepare the data for the POST request - post_data = { - "enable_dnssec": "Enable DNSSEC", - } - updated_page = self.client.post( - reverse("domain-dns-dnssec", kwargs={"pk": self.domain.id}), - post_data, - follow=True, - ) - - self.assertEqual(updated_page.status_code, 200) - - self.assertContains(updated_page, "Add DS Data") - self.assertContains(updated_page, "Add Key Data") - - self.app.get("/") - - back_to_page = self.client.get( - reverse("domain-dns-dnssec", kwargs={"pk": self.domain.id}) - ) - self.assertContains(back_to_page, "Enable DNSSEC") - def test_dnssec_page_loads_with_data_in_domain(self): """DNSSEC overview page loads when domain has DNSSEC data and the template contains a button to disable DNSSEC.""" @@ -1637,44 +1605,6 @@ class TestDomainDNSSEC(TestDomainOverview): ) self.assertContains(page, "DS Data record 1") - def test_ds_form_loads_with_key_data(self): - """DNSSEC Add DS Data page loads when there is - domain DNSSEC KEY data and shows an alert""" - - page = self.client.get( - reverse("domain-dns-dnssec-dsdata", kwargs={"pk": self.domain_keydata.id}) - ) - self.assertContains(page, "Warning, you cannot add DS Data") - - def test_key_form_loads_with_no_domain_data(self): - """DNSSEC Add Key Data page loads when there is no - domain DNSSEC data and shows a button to Add DS Key record""" - - page = self.client.get( - reverse( - "domain-dns-dnssec-keydata", kwargs={"pk": self.domain_dnssec_none.id} - ) - ) - self.assertContains(page, "Add DS Key record") - - def test_key_form_loads_with_key_data(self): - """DNSSEC Add Key Data page loads when there is - domain DNSSEC Key data and shows the data""" - - page = self.client.get( - reverse("domain-dns-dnssec-keydata", kwargs={"pk": self.domain_keydata.id}) - ) - self.assertContains(page, "DS Data record 1") - - def test_key_form_loads_with_ds_data(self): - """DNSSEC Add Key Data page loads when there is - domain DNSSEC DS data and shows an alert""" - - page = self.client.get( - reverse("domain-dns-dnssec-keydata", kwargs={"pk": self.domain_dsdata.id}) - ) - self.assertContains(page, "Warning, you cannot add Key Data") - def test_ds_data_form_submits(self): """DS Data form submits successfully @@ -1719,50 +1649,6 @@ class TestDomainDNSSEC(TestDomainOverview): # the field. self.assertContains(result, "Key tag is required", count=2, status_code=200) - def test_key_data_form_submits(self): - """Key Data form submits successfully - - Uses self.app WebTest because we need to interact with forms. - """ - add_data_page = self.app.get( - reverse("domain-dns-dnssec-keydata", kwargs={"pk": self.domain_keydata.id}) - ) - session_id = self.app.cookies[settings.SESSION_COOKIE_NAME] - self.app.set_cookie(settings.SESSION_COOKIE_NAME, session_id) - with less_console_noise(): # swallow log warning message - result = add_data_page.forms[0].submit() - # form submission was a post, response should be a redirect - self.assertEqual(result.status_code, 302) - self.assertEqual( - result["Location"], - reverse("domain-dns-dnssec-keydata", kwargs={"pk": self.domain_keydata.id}), - ) - self.app.set_cookie(settings.SESSION_COOKIE_NAME, session_id) - page = result.follow() - self.assertContains( - page, "The Key Data records for this domain have been updated." - ) - - def test_key_data_form_invalid(self): - """Key Data form errors with invalid data - - Uses self.app WebTest because we need to interact with forms. - """ - add_data_page = self.app.get( - reverse("domain-dns-dnssec-keydata", kwargs={"pk": self.domain_keydata.id}) - ) - session_id = self.app.cookies[settings.SESSION_COOKIE_NAME] - self.app.set_cookie(settings.SESSION_COOKIE_NAME, session_id) - # first two nameservers are required, so if we empty one out we should - # get a form error - add_data_page.forms[0]["form-0-pub_key"] = "" - with less_console_noise(): # swallow logged warning message - result = add_data_page.forms[0].submit() - # form submission was a post with an error, response should be a 200 - # error text appears twice, once at the top of the page, once around - # the field. - self.assertContains(result, "Pub key is required", count=2, status_code=200) - class TestApplicationStatus(TestWithUser, WebTest): def setUp(self): diff --git a/src/registrar/views/__init__.py b/src/registrar/views/__init__.py index 5fd81df8c..c1400d7c0 100644 --- a/src/registrar/views/__init__.py +++ b/src/registrar/views/__init__.py @@ -7,7 +7,6 @@ from .domain import ( DomainNameserversView, DomainDNSSECView, DomainDsDataView, - DomainKeyDataView, DomainYourContactInformationView, DomainSecurityEmailView, DomainUsersView, diff --git a/src/registrar/views/domain.py b/src/registrar/views/domain.py index 36b7a9445..1e0505353 100644 --- a/src/registrar/views/domain.py +++ b/src/registrar/views/domain.py @@ -33,8 +33,6 @@ from ..forms import ( DomainDnssecForm, DomainDsdataFormset, DomainDsdataForm, - DomainKeydataFormset, - DomainKeydataForm, ) from epplibwrapper import ( @@ -280,12 +278,6 @@ class DomainDNSSECView(DomainPermissionView, FormMixin): errmsg = "Error removing existing DNSSEC record(s)." logger.error(errmsg + ": " + err) messages.error(self.request, errmsg) - request.session["dnssec_ds_confirmed"] = False - request.session["dnssec_key_confirmed"] = False - elif "enable_dnssec" in request.POST: - request.session["dnssec_enabled"] = True - request.session["dnssec_ds_confirmed"] = False - request.session["dnssec_key_confirmed"] = False return self.form_valid(form) @@ -303,24 +295,17 @@ class DomainDsDataView(DomainPermissionView, FormMixin): dnssecdata: extensions.DNSSECExtension = domain.dnssecdata initial_data = [] - if dnssecdata is not None: - if dnssecdata.keyData is not None: - # TODO: Throw an error - # Note: This is moot if we're - # removing key data - pass - - if dnssecdata.dsData is not None: - # Add existing nameservers as initial data - initial_data.extend( - { - "key_tag": record.keyTag, - "algorithm": record.alg, - "digest_type": record.digestType, - "digest": record.digest, - } - for record in dnssecdata.dsData - ) + if dnssecdata is not None and dnssecdata.dsData is not None: + # Add existing nameservers as initial data + initial_data.extend( + { + "key_tag": record.keyTag, + "algorithm": record.alg, + "digest_type": record.digestType, + "digest": record.digest, + } + for record in dnssecdata.dsData + ) # Ensure at least 1 record, filled or empty while len(initial_data) == 0: @@ -338,18 +323,6 @@ class DomainDsDataView(DomainPermissionView, FormMixin): # use "formset" instead of "form" for the key context["formset"] = context.pop("form") - # set the dnssec_ds_confirmed flag in the context for this view - # based either on the existence of DS Data in the domain, - # or on the flag stored in the session - domain = self.get_object() - dnssecdata: extensions.DNSSECExtension = domain.dnssecdata - - if dnssecdata is not None and dnssecdata.dsData is not None: - self.request.session["dnssec_ds_confirmed"] = True - - context["dnssec_ds_confirmed"] = self.request.session.get( - "dnssec_ds_confirmed", False - ) return context def post(self, request, *args, **kwargs): @@ -357,11 +330,6 @@ class DomainDsDataView(DomainPermissionView, FormMixin): self.object = self.get_object() formset = self.get_form() - if "confirm-ds" in request.POST: - request.session["dnssec_ds_confirmed"] = True - request.session["dnssec_key_confirmed"] = False - return super().form_valid(formset) - if "btn-cancel-click" in request.POST: return redirect("/", {"formset": formset}, RequestContext(request)) @@ -411,126 +379,6 @@ class DomainDsDataView(DomainPermissionView, FormMixin): return super().form_valid(formset) -class DomainKeyDataView(DomainPermissionView, FormMixin): - """Domain DNSSEC key data editing view.""" - - template_name = "domain_keydata.html" - form_class = DomainKeydataFormset - form = DomainKeydataForm - - def get_initial(self): - """The initial value for the form (which is a formset here).""" - domain = self.get_object() - dnssecdata: extensions.DNSSECExtension = domain.dnssecdata - initial_data = [] - - if dnssecdata is not None: - if dnssecdata.dsData is not None: - # TODO: Throw an error? - # Note: this is moot if we're - # removing Key data - pass - - if dnssecdata.keyData is not None: - # Add existing keydata as initial data - initial_data.extend( - { - "flag": record.flags, - "protocol": record.protocol, - "algorithm": record.alg, - "pub_key": record.pubKey, - } - for record in dnssecdata.keyData - ) - - # Ensure at least 1 record, filled or empty - while len(initial_data) == 0: - initial_data.append({}) - - return initial_data - - def get_success_url(self): - """Redirect to the Key Data page for the domain.""" - return reverse("domain-dns-dnssec-keydata", kwargs={"pk": self.object.pk}) - - def get_context_data(self, **kwargs): - """Adjust context from FormMixin for formsets.""" - context = super().get_context_data(**kwargs) - # use "formset" instead of "form" for the key - context["formset"] = context.pop("form") - - # set the dnssec_key_confirmed flag in the context for this view - # based either on the existence of Key Data in the domain, - # or on the flag stored in the session - domain = self.get_object() - dnssecdata: extensions.DNSSECExtension = domain.dnssecdata - - if dnssecdata is not None and dnssecdata.keyData is not None: - self.request.session["dnssec_key_confirmed"] = True - - context["dnssec_key_confirmed"] = self.request.session.get( - "dnssec_key_confirmed", False - ) - return context - - def post(self, request, *args, **kwargs): - """Formset submission posts to this view.""" - self.object = self.get_object() - formset = self.get_form() - - if "confirm-key" in request.POST: - request.session["dnssec_key_confirmed"] = True - request.session["dnssec_ds_confirmed"] = False - self.object.save() - return super().form_valid(formset) - - if "btn-cancel-click" in request.POST: - return redirect("/", {"formset": formset}, RequestContext(request)) - - if formset.is_valid(): - return self.form_valid(formset) - else: - return self.form_invalid(formset) - - def form_valid(self, formset): - """The formset is valid, perform something with it.""" - - # Set the nameservers from the formset - dnssecdata = extensions.DNSSECExtension() - - for form in formset: - try: - # if 'delete' not in form.cleaned_data - # or form.cleaned_data['delete'] == False: - keyrecord = { - "flags": int(form.cleaned_data["flag"]), - "protocol": int(form.cleaned_data["protocol"]), - "alg": int(form.cleaned_data["algorithm"]), - "pubKey": form.cleaned_data["pub_key"], - } - if dnssecdata.keyData is None: - dnssecdata.keyData = [] - dnssecdata.keyData.append(common.DNSSECKeyData(**keyrecord)) - except KeyError: - # no server information in this field, skip it - pass - domain = self.get_object() - try: - domain.dnssecdata = dnssecdata - except RegistryError as err: - errmsg = "Error updating DNSSEC data in the registry." - logger.error(errmsg) - logger.error(err) - messages.error(self.request, errmsg) - return self.form_invalid(formset) - else: - messages.success( - self.request, "The Key Data records for this domain have been updated." - ) - # superclass has the redirect - return super().form_valid(formset) - - class DomainYourContactInformationView(DomainPermissionView, FormMixin): """Domain your contact information editing view.""" From b0262f6d34fcce4428d642a2819bcf0c51c7bb62 Mon Sep 17 00:00:00 2001 From: David Kennedy Date: Tue, 17 Oct 2023 17:23:47 -0400 Subject: [PATCH 042/120] removed keyData from domain model for dnssec --- src/registrar/models/domain.py | 31 +------------------------------ 1 file changed, 1 insertion(+), 30 deletions(-) diff --git a/src/registrar/models/domain.py b/src/registrar/models/domain.py index 1aad4fef4..aea3deecd 100644 --- a/src/registrar/models/domain.py +++ b/src/registrar/models/domain.py @@ -523,32 +523,9 @@ class Domain(TimeStampedModel, DomainHelper): else: addDnssecdata["dsData"] = None - elif _dnssecdata and _dnssecdata.keyData is not None: - # initialize addDnssecdata and remDnssecdata for keyData - addDnssecdata["keyData"] = _dnssecdata.keyData - - if oldDnssecdata and len(oldDnssecdata.keyData) > 0: - # if existing keyData not in new keyData, mark for removal - keyDataForRemoval = [ - keyData - for keyData in oldDnssecdata.keyData - if keyData not in _dnssecdata.keyData - ] - if len(keyDataForRemoval) > 0: - remDnssecdata["keyData"] = keyDataForRemoval - - # if new keyData not in existing keyData, mark for add - keyDataForAdd = [ - keyData - for keyData in _dnssecdata.keyData - if keyData not in oldDnssecdata.keyData - ] - if len(keyDataForAdd) > 0: - addDnssecdata["keyData"] = keyDataForAdd else: - # there are no new dsData or keyData, remove all + # there are no new dsData, remove all dsData from existing remDnssecdata["dsData"] = getattr(oldDnssecdata, "dsData", None) - remDnssecdata["keyData"] = getattr(oldDnssecdata, "keyData", None) return addDnssecdata, remDnssecdata @@ -558,12 +535,10 @@ class Domain(TimeStampedModel, DomainHelper): addParams = { "maxSigLife": _addDnssecdata.get("maxSigLife", None), "dsData": _addDnssecdata.get("dsData", None), - "keyData": _addDnssecdata.get("keyData", None), } remParams = { "maxSigLife": _remDnssecdata.get("maxSigLife", None), "remDsData": _remDnssecdata.get("dsData", None), - "remKeyData": _remDnssecdata.get("keyData", None), } addRequest = commands.UpdateDomain(name=self.name) addExtension = commands.UpdateDomainDNSSECExtension(**addParams) @@ -575,15 +550,11 @@ class Domain(TimeStampedModel, DomainHelper): if ( "dsData" in _addDnssecdata and _addDnssecdata["dsData"] is not None - or "keyData" in _addDnssecdata - and _addDnssecdata["keyData"] is not None ): registry.send(addRequest, cleaned=True) if ( "dsData" in _remDnssecdata and _remDnssecdata["dsData"] is not None - or "keyData" in _remDnssecdata - and _remDnssecdata["keyData"] is not None ): registry.send(remRequest, cleaned=True) except RegistryError as e: From 6ef6044459c7369ea40dfbdf43b8bc78db6ce01c Mon Sep 17 00:00:00 2001 From: Rachid Mrad Date: Tue, 17 Oct 2023 17:31:05 -0400 Subject: [PATCH 043/120] lint --- src/registrar/forms/common.py | 1 - src/registrar/models/domain.py | 10 ++-------- src/registrar/tests/test_views.py | 2 +- 3 files changed, 3 insertions(+), 10 deletions(-) diff --git a/src/registrar/forms/common.py b/src/registrar/forms/common.py index 585d5ed3e..a27545a84 100644 --- a/src/registrar/forms/common.py +++ b/src/registrar/forms/common.py @@ -24,4 +24,3 @@ DIGEST_TYPE_CHOICES = [ (0, "(0) Reserved"), (1, "(1) SHA-256"), ] - diff --git a/src/registrar/models/domain.py b/src/registrar/models/domain.py index aea3deecd..6d911c975 100644 --- a/src/registrar/models/domain.py +++ b/src/registrar/models/domain.py @@ -547,15 +547,9 @@ class Domain(TimeStampedModel, DomainHelper): remExtension = commands.UpdateDomainDNSSECExtension(**remParams) remRequest.add_extension(remExtension) try: - if ( - "dsData" in _addDnssecdata - and _addDnssecdata["dsData"] is not None - ): + if "dsData" in _addDnssecdata and _addDnssecdata["dsData"] is not None: registry.send(addRequest, cleaned=True) - if ( - "dsData" in _remDnssecdata - and _remDnssecdata["dsData"] is not None - ): + if "dsData" in _remDnssecdata and _remDnssecdata["dsData"] is not None: registry.send(remRequest, cleaned=True) except RegistryError as e: logger.error( diff --git a/src/registrar/tests/test_views.py b/src/registrar/tests/test_views.py index e8946c52c..2a1cd8fed 100644 --- a/src/registrar/tests/test_views.py +++ b/src/registrar/tests/test_views.py @@ -1555,7 +1555,7 @@ class TestDomainDNSSEC(TestDomainOverview): def test_dnssec_page_refreshes_enable_button(self): """DNSSEC overview page loads when domain has no DNSSEC data and shows a 'Enable DNSSEC' button.""" - + page = self.client.get( reverse("domain-dns-dnssec", kwargs={"pk": self.domain.id}) ) From 1ea318bdbcc01b2a41b18666474bb53475f40c8b Mon Sep 17 00:00:00 2001 From: Rachid Mrad Date: Tue, 17 Oct 2023 17:47:34 -0400 Subject: [PATCH 044/120] swap out the order of the alerts so that the form error alerts are below the informational one --- src/registrar/templates/domain_dsdata.html | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/src/registrar/templates/domain_dsdata.html b/src/registrar/templates/domain_dsdata.html index ac38bd87f..5aa1a9969 100644 --- a/src/registrar/templates/domain_dsdata.html +++ b/src/registrar/templates/domain_dsdata.html @@ -4,10 +4,6 @@ {% block title %}DS Data | {{ domain.name }} | {% endblock %} {% block domain_content %} - {% for form in formset %} - {% include "includes/form_errors.html" with form=form %} - {% endfor %} - {% if domain.dnssecdata is None %}
@@ -16,6 +12,10 @@
{% endif %} + {% for form in formset %} + {% include "includes/form_errors.html" with form=form %} + {% endfor %} +

DS Data

In order to enable DNSSEC, you must first configure it with your DNS hosting service.

From 1502e7a693a4f4c51428082a084dd0f58796dc21 Mon Sep 17 00:00:00 2001 From: zandercymatics <141044360+zandercymatics@users.noreply.github.com> Date: Tue, 17 Oct 2023 16:00:21 -0600 Subject: [PATCH 045/120] Update tests --- src/epplibwrapper/client.py | 2 +- src/epplibwrapper/socket.py | 6 +++++- src/epplibwrapper/tests/test_pool.py | 15 +++++---------- 3 files changed, 11 insertions(+), 12 deletions(-) diff --git a/src/epplibwrapper/client.py b/src/epplibwrapper/client.py index a465593a0..bdb00b856 100644 --- a/src/epplibwrapper/client.py +++ b/src/epplibwrapper/client.py @@ -189,7 +189,7 @@ class EPPLibWrapper: # 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 try_start_if_invalid + try_start_if_invalid and (settings.DEBUG or not self._test_registry_connection_success()) ): diff --git a/src/epplibwrapper/socket.py b/src/epplibwrapper/socket.py index 716fef18a..186b98322 100644 --- a/src/epplibwrapper/socket.py +++ b/src/epplibwrapper/socket.py @@ -46,6 +46,7 @@ class Socket: 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 method") return False counter = 0 # we'll try 3 times @@ -58,12 +59,15 @@ class Socket: counter += 1 sleep((counter * 50) / 1000) # sleep 50 ms to 150 ms else: # don't try again + logger.warning("LoginError raised and should not retry or has been retried 3 times already") + logger.warning(f"should retry? {err.should_retry()}") return False else: self.disconnect() - + # If we encounter a login error, fail if self.is_login_error(response.code): + logger.warning("was login error") return False # otherwise, just return true diff --git a/src/epplibwrapper/tests/test_pool.py b/src/epplibwrapper/tests/test_pool.py index d088697ec..3005edb29 100644 --- a/src/epplibwrapper/tests/test_pool.py +++ b/src/epplibwrapper/tests/test_pool.py @@ -137,18 +137,13 @@ class TestConnectionPool(TestCase): with ExitStack() as stack: stack.enter_context(patch.object(Socket, "connect", None)) stack.enter_context(patch.object(Socket, "send", fake_send)) - stack.enter_context(patch.object(Socket, "_create_socket", Socket())) + stack.enter_context(patch.object(EPPLibWrapper, "_create_pool", self.fake_pool)) #stack.enter_context(patch.object(EPPLibWrapper, "get_pool", self.fake_pool)) - pool = EPPLibWrapper(False) - # The connection pool will fail to start, start it manually - # so that our mocks can take over - pool.start_connection_pool(try_start_if_invalid=True) - print(f"this is pool {pool._pool.__dict__}") # Pool should be running, and be the right size - self.assertEqual(pool.pool_status.pool_running, True) - self.assertEqual(pool.pool_status.connection_success, True) - pool.send(commands.InfoDomain(name="test.gov"), cleaned=True) - self.assertEqual(len(pool._pool.conn), self.pool_options["size"]) + self.assertEqual(registry.pool_status.pool_running, True) + self.assertEqual(registry.pool_status.connection_success, True) + registry.send(commands.InfoDomain(name="test.gov"), cleaned=True) + self.assertEqual(len(registry._pool.conn), self.pool_options["size"]) #pool.send() From 34294782c3a887f3ef1b5f99d45202bac4a59365 Mon Sep 17 00:00:00 2001 From: Rebecca Hsieh Date: Tue, 17 Oct 2023 15:24:38 -0700 Subject: [PATCH 046/120] Update Staff permissions for contacts, websites, addresses and domain information and application --- src/registrar/admin.py | 26 ++++++++++--- .../migrations/0040_create_groups_v03.py | 37 +++++++++++++++++++ src/registrar/models/user_group.py | 7 +++- 3 files changed, 63 insertions(+), 7 deletions(-) create mode 100644 src/registrar/migrations/0040_create_groups_v03.py diff --git a/src/registrar/admin.py b/src/registrar/admin.py index 8d0ed8c2e..0533929f6 100644 --- a/src/registrar/admin.py +++ b/src/registrar/admin.py @@ -294,6 +294,26 @@ class ContactAdmin(ListHeaderAdmin): contact.admin_order_field = "first_name" # type: ignore + # Read only that we'll leverage for CISA Analysts + analyst_readonly_fields = [ + "user", + ] + + def get_readonly_fields(self, request, obj=None): + """Set the read-only state on form elements. + We have 1 conditions that determine which fields are read-only: + admin user permissions. + """ + + readonly_fields = list(self.readonly_fields) + + if request.user.has_perm("registrar.full_access_permission"): + return readonly_fields + # Return restrictive Read-only fields for analysts and + # users who might not belong to groups + readonly_fields.extend([field for field in self.analyst_readonly_fields]) + return readonly_fields # Read-only fields for analysts + class WebsiteAdmin(ListHeaderAdmin): """Custom website admin class.""" @@ -420,9 +440,6 @@ class DomainInformationAdmin(ListHeaderAdmin): "creator", "type_of_work", "more_organization_information", - "address_line1", - "address_line2", - "zipcode", "domain", "submitter", "no_other_contacts_rationale", @@ -557,9 +574,6 @@ class DomainApplicationAdmin(ListHeaderAdmin): analyst_readonly_fields = [ "creator", "about_your_organization", - "address_line1", - "address_line2", - "zipcode", "requested_domain", "alternative_domains", "purpose", diff --git a/src/registrar/migrations/0040_create_groups_v03.py b/src/registrar/migrations/0040_create_groups_v03.py new file mode 100644 index 000000000..6885b9dfc --- /dev/null +++ b/src/registrar/migrations/0040_create_groups_v03.py @@ -0,0 +1,37 @@ +# This migration creates the create_full_access_group and create_cisa_analyst_group groups +# It is dependent on 0035 (which populates ContentType and Permissions) +# If permissions on the groups need changing, edit CISA_ANALYST_GROUP_PERMISSIONS +# in the user_group model then: +# [NOT RECOMMENDED] +# step 1: docker-compose exec app ./manage.py migrate --fake registrar 0035_contenttypes_permissions +# step 2: docker-compose exec app ./manage.py migrate registrar 0036_create_groups +# step 3: fake run the latest migration in the migrations list +# [RECOMMENDED] +# Alternatively: +# step 1: duplicate the migration that loads data +# step 2: docker-compose exec app ./manage.py migrate + +from django.db import migrations +from registrar.models import UserGroup +from typing import Any + + +# For linting: RunPython expects a function reference, +# so let's give it one +def create_groups(apps, schema_editor) -> Any: + UserGroup.create_cisa_analyst_group(apps, schema_editor) + UserGroup.create_full_access_group(apps, schema_editor) + + +class Migration(migrations.Migration): + dependencies = [ + ("registrar", "0039_alter_transitiondomain_status"), + ] + + operations = [ + migrations.RunPython( + create_groups, + reverse_code=migrations.RunPython.noop, + atomic=True, + ), + ] \ No newline at end of file diff --git a/src/registrar/models/user_group.py b/src/registrar/models/user_group.py index 5cdb1f2ec..568741786 100644 --- a/src/registrar/models/user_group.py +++ b/src/registrar/models/user_group.py @@ -24,7 +24,7 @@ class UserGroup(Group): { "app_label": "registrar", "model": "contact", - "permissions": ["view_contact"], + "permissions": ["change_contact"], }, { "app_label": "registrar", @@ -56,6 +56,11 @@ class UserGroup(Group): "model": "domaininvitation", "permissions": ["add_domaininvitation", "view_domaininvitation"], }, + { + "app_label": "registrar", + "model": "website", + "permissions": ["change_website"], + }, ] # Avoid error: You can't execute queries until the end From a87ebfc287fef3f886133471a4b6bb8f37adf822 Mon Sep 17 00:00:00 2001 From: Rebecca Hsieh Date: Tue, 17 Oct 2023 15:40:11 -0700 Subject: [PATCH 047/120] Fix linter errors and tests --- src/registrar/migrations/0040_create_groups_v03.py | 2 +- src/registrar/tests/test_admin.py | 3 --- src/registrar/tests/test_migrations.py | 3 ++- 3 files changed, 3 insertions(+), 5 deletions(-) diff --git a/src/registrar/migrations/0040_create_groups_v03.py b/src/registrar/migrations/0040_create_groups_v03.py index 6885b9dfc..cad2cadc5 100644 --- a/src/registrar/migrations/0040_create_groups_v03.py +++ b/src/registrar/migrations/0040_create_groups_v03.py @@ -34,4 +34,4 @@ class Migration(migrations.Migration): reverse_code=migrations.RunPython.noop, atomic=True, ), - ] \ No newline at end of file + ] diff --git a/src/registrar/tests/test_admin.py b/src/registrar/tests/test_admin.py index 51ace34f7..7dbc8ff38 100644 --- a/src/registrar/tests/test_admin.py +++ b/src/registrar/tests/test_admin.py @@ -620,9 +620,6 @@ class TestDomainApplicationAdmin(MockEppLib): expected_fields = [ "creator", "about_your_organization", - "address_line1", - "address_line2", - "zipcode", "requested_domain", "alternative_domains", "purpose", diff --git a/src/registrar/tests/test_migrations.py b/src/registrar/tests/test_migrations.py index 95e5853ff..165ef6f71 100644 --- a/src/registrar/tests/test_migrations.py +++ b/src/registrar/tests/test_migrations.py @@ -36,7 +36,7 @@ class TestGroups(TestCase): # Define the expected permission codenames expected_permissions = [ "view_logentry", - "view_contact", + "change_contact", "view_domain", "change_domainapplication", "change_domaininformation", @@ -45,6 +45,7 @@ class TestGroups(TestCase): "change_draftdomain", "analyst_access_permission", "change_user", + "change_website", ] # Get the codenames of actual permissions associated with the group From 5dd76bfeefb8a8945b5d9f2703f80039ff6496bb Mon Sep 17 00:00:00 2001 From: CocoByte Date: Wed, 18 Oct 2023 11:06:06 -0600 Subject: [PATCH 048/120] Add logic for transition domains in user-login handler Signed-off-by: CocoByte --- src/registrar/models/user.py | 18 ++++++++++++++++++ 1 file changed, 18 insertions(+) diff --git a/src/registrar/models/user.py b/src/registrar/models/user.py index acf59cb68..5d73866a4 100644 --- a/src/registrar/models/user.py +++ b/src/registrar/models/user.py @@ -4,6 +4,8 @@ from django.contrib.auth.models import AbstractUser from django.db import models from .domain_invitation import DomainInvitation +from registrar.models import TransitionDomain +from registrar.models import DomainInformation from phonenumber_field.modelfields import PhoneNumberField # type: ignore @@ -81,6 +83,22 @@ class User(AbstractUser): logger.warn( "Failed to retrieve invitation %s", invitation, exc_info=True ) + + transition_domain_exists = TransitionDomain.objects.filter( + username=self.email + ).exists() + if transition_domain_exists: + # Looks like the user logged in with the same e-mail as + # a corresponding transition domain. Create a Domain + # Information object. + # TODO: Do we need to check for existing Domain Info objects? + # TODO: Should we add a Domain to the DomainInfo object? + # NOTE that adding a user role for this user + # as admin for this domain is already done + # in the incitation.retrieve() method. So + # we don't need to do that here. + new_domain_info = DomainInformation(creator=self) + new_domain_info.save() class Meta: permissions = [ From 1ca7c51fbb5a5a4aa069384fafc74bbe74c99883 Mon Sep 17 00:00:00 2001 From: zandercymatics <141044360+zandercymatics@users.noreply.github.com> Date: Wed, 18 Oct 2023 11:56:28 -0600 Subject: [PATCH 049/120] Hotfix --- src/epplibwrapper/client.py | 7 ++--- src/epplibwrapper/tests/test_pool.py | 39 ++++++++++++++++++------- src/epplibwrapper/utility/pool.py | 2 +- src/epplibwrapper/utility/pool_error.py | 2 +- 4 files changed, 34 insertions(+), 16 deletions(-) diff --git a/src/epplibwrapper/client.py b/src/epplibwrapper/client.py index bdb00b856..3cecb4995 100644 --- a/src/epplibwrapper/client.py +++ b/src/epplibwrapper/client.py @@ -172,7 +172,7 @@ class EPPLibWrapper: ) def start_connection_pool( - self, restart_pool_if_exists=True, try_start_if_invalid=False + self, restart_pool_if_exists=True ): """Starts a connection pool for the registry. @@ -189,9 +189,8 @@ class EPPLibWrapper: # 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 ( - try_start_if_invalid - and (settings.DEBUG - or not self._test_registry_connection_success()) + settings.DEBUG + or not self._test_registry_connection_success() ): logger.warning("Cannot contact the Registry") self.pool_status.connection_success = False diff --git a/src/epplibwrapper/tests/test_pool.py b/src/epplibwrapper/tests/test_pool.py index 3005edb29..895eaa7e8 100644 --- a/src/epplibwrapper/tests/test_pool.py +++ b/src/epplibwrapper/tests/test_pool.py @@ -14,6 +14,7 @@ import logging try: from epplib import commands + from epplib.client import Client from epplib.exceptions import TransportError except ImportError: pass @@ -35,6 +36,15 @@ class TestConnectionPool(TestCase): "keepalive": 60, } + #self.start_mocks() + + def tearDown(self): + #self.mock_send_patch.stop() + #self.mock_connect_patch.stop() + #self.mockSendPatch.stop() + pass + + def start_mocks(self): # Mock a successful connection self.mock_connect_patch = patch("epplib.client.Client.connect") self.mocked_connect_function = self.mock_connect_patch.start() @@ -50,11 +60,6 @@ class TestConnectionPool(TestCase): self.mockedSendFunction = self.mockSendPatch.start() self.mockedSendFunction.side_effect = self.fake_pool - def tearDown(self): - self.mock_send_patch.stop() - self.mock_connect_patch.stop() - self.mockSendPatch.stop() - def mock_connect(self, _request): return None @@ -95,6 +100,22 @@ class TestConnectionPool(TestCase): client=mock_client, login=mock_login, options=self.pool_options ) return pool + + @patch("djangooidc.views.CLIENT", autospec=True) + def fake_socket(self, mock_client): + # mock client + mock_client.callback.side_effect = self.user_info + # Create a mock transport object + mock_login = MagicMock() + mock_login.transport.cert_file = "path/to/cert_file" + mock_login.transport.key_file = "path/to/key_file" + return Socket(mock_client, mock_login) + + def test(self, client, login): + mock = MagicMock() + mock.response.code = 1000 + mock().return_value = 1000 + return MagicMock() @skip("not implemented yet") def test_pool_timesout(self): @@ -124,7 +145,7 @@ class TestConnectionPool(TestCase): domain, _ = Domain.objects.get_or_create(name="freeman.gov") def fake_send(self): - return MagicMock( + mock = MagicMock( code=1000, msg="Command completed successfully", res_data=None, @@ -133,12 +154,10 @@ class TestConnectionPool(TestCase): extensions=[], msg_q=None, ) + return mock with ExitStack() as stack: - stack.enter_context(patch.object(Socket, "connect", None)) - stack.enter_context(patch.object(Socket, "send", fake_send)) - stack.enter_context(patch.object(EPPLibWrapper, "_create_pool", self.fake_pool)) - #stack.enter_context(patch.object(EPPLibWrapper, "get_pool", self.fake_pool)) + stack.enter_context(patch.object(EPPConnectionPool, "_create_socket", self.fake_socket)) # Pool should be running, and be the right size self.assertEqual(registry.pool_status.pool_running, True) self.assertEqual(registry.pool_status.connection_success, True) diff --git a/src/epplibwrapper/utility/pool.py b/src/epplibwrapper/utility/pool.py index 7a58f7efe..3b8eb240c 100644 --- a/src/epplibwrapper/utility/pool.py +++ b/src/epplibwrapper/utility/pool.py @@ -34,7 +34,7 @@ class EPPConnectionPool(ConnectionPool): connection = socket.connect() return connection except Exception as err: - message = f"Failed to execute due to a registry login error: {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 diff --git a/src/epplibwrapper/utility/pool_error.py b/src/epplibwrapper/utility/pool_error.py index 0bcd36a53..70312f32e 100644 --- a/src/epplibwrapper/utility/pool_error.py +++ b/src/epplibwrapper/utility/pool_error.py @@ -26,7 +26,7 @@ class PoolError(Exception): # For linter kill_failed = "Could not kill all connections." - conn_failed = "Failed to execute due to a registry login error." + conn_failed = "Failed to execute due to a registry error." alive_failed = "Failed to keep the connection alive." _error_mapping = { PoolErrorCodes.KILL_ALL_FAILED: kill_failed, From 44b3e78aed1ee0743a4630ad44ca1a94bebb5b21 Mon Sep 17 00:00:00 2001 From: CocoByte Date: Wed, 18 Oct 2023 13:28:49 -0600 Subject: [PATCH 050/120] updated user login logic for one-to-many transition domains relationship, and added some error handling --- src/registrar/models/user.py | 89 ++++++++++++++++++++++++++++-------- 1 file changed, 69 insertions(+), 20 deletions(-) diff --git a/src/registrar/models/user.py b/src/registrar/models/user.py index 5d73866a4..afc16bb68 100644 --- a/src/registrar/models/user.py +++ b/src/registrar/models/user.py @@ -6,6 +6,7 @@ from django.db import models from .domain_invitation import DomainInvitation from registrar.models import TransitionDomain from registrar.models import DomainInformation +from registrar.models import Domain from phonenumber_field.modelfields import PhoneNumberField # type: ignore @@ -64,12 +65,9 @@ class User(AbstractUser): def is_restricted(self): return self.status == self.RESTRICTED - def first_login(self): - """Callback when the user is authenticated for the very first time. - - When a user first arrives on the site, we need to retrieve any domain - invitations that match their email address. - """ + def check_domain_invitations_on_login(self): + """When a user first arrives on the site, we need to retrieve any domain + invitations that match their email address.""" for invitation in DomainInvitation.objects.filter( email=self.email, status=DomainInvitation.INVITED ): @@ -83,22 +81,73 @@ class User(AbstractUser): logger.warn( "Failed to retrieve invitation %s", invitation, exc_info=True ) - - transition_domain_exists = TransitionDomain.objects.filter( + + def check_transition_domains_on_login(self): + """When a user first arrives on the site, we need to check + if they are logging in with the same e-mail as a + transition domain and update our database accordingly.""" + + for transition_domain in TransitionDomain.objects.filter( username=self.email - ).exists() - if transition_domain_exists: + ): # Looks like the user logged in with the same e-mail as - # a corresponding transition domain. Create a Domain - # Information object. - # TODO: Do we need to check for existing Domain Info objects? - # TODO: Should we add a Domain to the DomainInfo object? - # NOTE that adding a user role for this user - # as admin for this domain is already done - # in the incitation.retrieve() method. So - # we don't need to do that here. - new_domain_info = DomainInformation(creator=self) - new_domain_info.save() + # one or more corresponding transition domains. + # Create corresponding DomainInformation objects. + + # NOTE: adding an ADMIN user role for this user + # for each domain should already be done + # in the invitation.retrieve() method. + # However, if the migration scripts for transition + # domain objects were not executed correctly, + # there could be transition domains without + # any corresponding Domain & DomainInvitation objects, + # which means the invitation.retrieve() method might + # not execute. + # Check that there is a corresponding domain object + # for this transition domain. If not, we have an error + # with our data and migrations need to be run again. + # TODO: how should we handle this? + + # Get the domain that corresponds with this transition domain + domain_exists = Domain.objects.filter(name=transition_domain.name).exists() + if not domain_exists: + logger.warn("""There are transition domains without + corresponding domain objects! + Please run migration scripts for transition domains + (See data_migration.md)""") + # TODO: throw exception?? + break + domain = Domain.objects.get(name=transition_domain.name) + + # Create a domain information object, if one doesn't + # already exist + domain_info_exists = DomainInformation.objects.filter( + creator=self, + domain=domain + ).exists() + if not domain_info_exists: + # TODO: Should we add a Domain to the DomainInfo object? + new_domain_info = DomainInformation( + creator=self, + domain=domain) + new_domain_info.save() + + def first_login(self): + """Callback when the user is authenticated for the very first time. + + When a user first arrives on the site, we need to retrieve any domain + invitations that match their email address. + + We also need to check if they are logging in with the same e-mail + as a transition domain and update our database accordingly. + """ + + # PART 1: DOMAIN INVITATIONS + self.check_domain_invitations_on_login() + + # PART 2: TRANSITION DOMAINS + self.check_transition_domains_on_login() + class Meta: permissions = [ From 9c6a6ef12a5d44516c129c464e5e4e594f4b445a Mon Sep 17 00:00:00 2001 From: zandercymatics <141044360+zandercymatics@users.noreply.github.com> Date: Wed, 18 Oct 2023 14:02:14 -0600 Subject: [PATCH 051/120] Fix test cases --- .../decisions/0023-use-geventconnpool.md | 2 +- src/epplibwrapper/client.py | 6 +- src/epplibwrapper/tests/test_pool.py | 228 +++++++++--------- .../tests/utility/infoDomain.xml | 33 +++ 4 files changed, 151 insertions(+), 118 deletions(-) create mode 100644 src/epplibwrapper/tests/utility/infoDomain.xml diff --git a/docs/architecture/decisions/0023-use-geventconnpool.md b/docs/architecture/decisions/0023-use-geventconnpool.md index c24318b4f..9288f3e86 100644 --- a/docs/architecture/decisions/0023-use-geventconnpool.md +++ b/docs/architecture/decisions/0023-use-geventconnpool.md @@ -4,7 +4,7 @@ Date: 2023-13-10 ## Status -In Review +Accepted ## Context diff --git a/src/epplibwrapper/client.py b/src/epplibwrapper/client.py index 3cecb4995..5381f7ce1 100644 --- a/src/epplibwrapper/client.py +++ b/src/epplibwrapper/client.py @@ -121,7 +121,6 @@ class EPPLibWrapper: logger.error(message, exc_info=True) raise RegistryError(message) from err else: - print(f"test thing {response}") if response.code >= 2000: raise RegistryError(response.msg, code=response.code) else: @@ -188,10 +187,7 @@ class EPPLibWrapper: """ # 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 ( - settings.DEBUG - or not self._test_registry_connection_success() - ): + if not self._test_registry_connection_success(): logger.warning("Cannot contact the Registry") self.pool_status.connection_success = False else: diff --git a/src/epplibwrapper/tests/test_pool.py b/src/epplibwrapper/tests/test_pool.py index 895eaa7e8..74a2b687d 100644 --- a/src/epplibwrapper/tests/test_pool.py +++ b/src/epplibwrapper/tests/test_pool.py @@ -1,5 +1,8 @@ +import datetime +from pathlib import Path from unittest import skip from unittest.mock import MagicMock, patch +from dateutil.tz import tzlocal from django.conf import settings from django.test import TestCase @@ -16,6 +19,8 @@ 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 @@ -35,116 +40,24 @@ class TestConnectionPool(TestCase): # Value in seconds => (keepalive / size) "keepalive": 60, } - - #self.start_mocks() - - def tearDown(self): - #self.mock_send_patch.stop() - #self.mock_connect_patch.stop() - #self.mockSendPatch.stop() - pass - def start_mocks(self): - # Mock a successful connection - self.mock_connect_patch = patch("epplib.client.Client.connect") - self.mocked_connect_function = self.mock_connect_patch.start() - self.mocked_connect_function.side_effect = self.mock_connect - - # Mock the send behaviour - self.mock_send_patch = patch("epplib.client.Client.send") - self.mocked_send_function = self.mock_send_patch.start() - self.mocked_send_function.side_effect = self.mock_send - - # Mock the pool object - self.mockSendPatch = patch("registrar.models.domain.registry._pool") - self.mockedSendFunction = self.mockSendPatch.start() - self.mockedSendFunction.side_effect = self.fake_pool - - def mock_connect(self, _request): - return None - - def mock_send(self, _request): - if isinstance(_request, commands.Login): - response = 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, + def fake_socket(self, login, client): + # Create a fake client object + fake_client = Client( + SocketTransport( + "none", + cert_file="path/to/cert_file", + key_file="path/to/key_file", + password="none", ) - - return response - return None - - def user_info(self, *args): - return { - "sub": "TEST", - "email": "test@example.com", - "first_name": "Testy", - "last_name": "Tester", - "phone": "814564000", - } - - @patch("djangooidc.views.CLIENT", autospec=True) - def fake_pool(self, mock_client): - # mock client - mock_client.callback.side_effect = self.user_info - # Create a mock transport object - mock_login = MagicMock() - mock_login.cert_file = "path/to/cert_file" - mock_login.key_file = "path/to/key_file" - - pool = EPPConnectionPool( - client=mock_client, login=mock_login, options=self.pool_options ) - return pool + + return Socket(fake_client, MagicMock()) + + def patch_success(self): + return True - @patch("djangooidc.views.CLIENT", autospec=True) - def fake_socket(self, mock_client): - # mock client - mock_client.callback.side_effect = self.user_info - # Create a mock transport object - mock_login = MagicMock() - mock_login.transport.cert_file = "path/to/cert_file" - mock_login.transport.key_file = "path/to/key_file" - return Socket(mock_client, mock_login) - - def test(self, client, login): - mock = MagicMock() - mock.response.code = 1000 - mock().return_value = 1000 - return MagicMock() - - @skip("not implemented yet") - def test_pool_timesout(self): - """The pool timesout and restarts""" - raise - - @skip("not implemented yet") - def test_multiple_users_send_data(self): - """Multiple users send data concurrently""" - raise - - def test_pool_tries_create_invalid(self): - """A .send is invoked on the pool, but the pool - shouldn't be running.""" - # Fake data for the _pool object - domain, _ = Domain.objects.get_or_create(name="freeman.gov") - - # Trigger the getter - should fail - expected_contact = domain.security_contact - self.assertEqual(registry.pool_status.pool_running, False) - self.assertEqual(registry.pool_status.connection_success, False) - self.assertEqual(len(registry._pool.conn), 0) - - def test_pool_sends_data(self): - """A .send is invoked on the pool successfully""" - # Fake data for the _pool object - domain, _ = Domain.objects.get_or_create(name="freeman.gov") - - def fake_send(self): + def fake_send(self, command, cleaned=None): mock = MagicMock( code=1000, msg="Command completed successfully", @@ -156,17 +69,108 @@ class TestConnectionPool(TestCase): ) return mock + @patch.object(EPPLibWrapper, "_test_registry_connection_success", patch_success) + def test_pool_sends_data(self): + """A .send is invoked on the pool successfully""" + self.maxDiff = None + 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' + } + + def fake_client(mock_client): + client = Client( + SocketTransport( + "none", + cert_file="path/to/cert_file", + key_file="path/to/key_file", + password="none", + ) + ) + return client + + # Mock a response from EPP + def fake_receive(command, cleaned=None): + location= Path(__file__).parent / "utility" / "infoDomain.xml" + xml = (location).read_bytes() + return xml + # 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", fake_client)) + stack.enter_context(patch.object(SocketTransport, "send", self.fake_send)) + stack.enter_context(patch.object(SocketTransport, "receive", fake_receive)) + # Restart the connection pool, since it starts on app startup + registry.start_connection_pool() # Pool should be running, and be the right size - self.assertEqual(registry.pool_status.pool_running, True) self.assertEqual(registry.pool_status.connection_success, True) - registry.send(commands.InfoDomain(name="test.gov"), cleaned=True) + self.assertEqual(registry.pool_status.pool_running, True) + + # Send a command + result = registry.send(commands.InfoDomain(name="test.gov"), cleaned=True) + + 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"]) + + @patch.object(EPPLibWrapper, "_test_registry_connection_success", patch_success) + def test_raises_transport_error(self): + """A .send is invoked on the pool, but registry connection is lost + right as we send a command.""" + # Fake data for the _pool object + def fake_client(self): + client = Client( + SocketTransport( + "none", + cert_file="path/to/cert_file", + key_file="path/to/key_file", + password="none", + ) + ) + return client - #pool.send() - - # Trigger the getter - should succeed - #expected_contact = domain.security_contact + with ExitStack() as stack: + stack.enter_context(patch.object(EPPConnectionPool, "_create_socket", self.fake_socket)) + stack.enter_context(patch.object(Socket, "connect", fake_client)) + # Restart the connection pool, since it starts on app startup + 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(TransportError): + registry.send(commands.InfoDomain(name="test.gov"), cleaned=True) diff --git a/src/epplibwrapper/tests/utility/infoDomain.xml b/src/epplibwrapper/tests/utility/infoDomain.xml new file mode 100644 index 000000000..541812577 --- /dev/null +++ b/src/epplibwrapper/tests/utility/infoDomain.xml @@ -0,0 +1,33 @@ + + + + + + Command completed successfully + + + + test3.gov + DF1340360-GOV + + + TuaWnx9hnm84GCSU + CONT2 + CONT3 + gov2023-ote + gov2023-ote + 2023-08-15T23:56:36Z + gov2023-ote + 2023-08-17T02:03:19Z + 2024-08-15T23:56:36Z + + 2fooBAR123fooBaz + + + + + wRRNVhKhQW2m6wsUHbo/lA==-29a + + + + From ffcfb9818b4b450b0426dae47d601103a0845dce Mon Sep 17 00:00:00 2001 From: zandercymatics <141044360+zandercymatics@users.noreply.github.com> Date: Wed, 18 Oct 2023 14:15:40 -0600 Subject: [PATCH 052/120] Test fix for scanner --- src/epplibwrapper/tests/test_pool.py | 8 ++++++-- src/registrar/models/domain.py | 21 ++++++++++++--------- 2 files changed, 18 insertions(+), 11 deletions(-) diff --git a/src/epplibwrapper/tests/test_pool.py b/src/epplibwrapper/tests/test_pool.py index 74a2b687d..b1912b9cf 100644 --- a/src/epplibwrapper/tests/test_pool.py +++ b/src/epplibwrapper/tests/test_pool.py @@ -68,11 +68,10 @@ class TestConnectionPool(TestCase): msg_q=None, ) return mock - + @patch.object(EPPLibWrapper, "_test_registry_connection_success", patch_success) def test_pool_sends_data(self): """A .send is invoked on the pool successfully""" - self.maxDiff = None expected_result = { 'cl_tr_id': None, 'code': 1000, @@ -124,6 +123,7 @@ class TestConnectionPool(TestCase): location= Path(__file__).parent / "utility" / "infoDomain.xml" xml = (location).read_bytes() return xml + # Mock what happens inside the "with" with ExitStack() as stack: stack.enter_context(patch.object(EPPConnectionPool, "_create_socket", self.fake_socket)) @@ -139,7 +139,11 @@ class TestConnectionPool(TestCase): # 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"]) diff --git a/src/registrar/models/domain.py b/src/registrar/models/domain.py index f66221338..68f0600ec 100644 --- a/src/registrar/models/domain.py +++ b/src/registrar/models/domain.py @@ -17,15 +17,18 @@ from registrar.utility.errors import ( NameserverErrorCodes as nsErrorCodes, ) -from epplibwrapper import ( - CLIENT as registry, - commands, - common as epp, - extensions, - info as eppInfo, - RegistryError, - ErrorCode, -) +try: + from epplibwrapper import ( + CLIENT as registry, + commands, + common as epp, + extensions, + info as eppInfo, + RegistryError, + ErrorCode, + ) +except Exception: + pass from registrar.models.utility.contact_error import ContactError, ContactErrorCodes From f672d15b002f6fa6d603fe16b08e791f967e55b3 Mon Sep 17 00:00:00 2001 From: zandercymatics <141044360+zandercymatics@users.noreply.github.com> Date: Wed, 18 Oct 2023 14:34:18 -0600 Subject: [PATCH 053/120] Update domain.py --- src/registrar/models/domain.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/registrar/models/domain.py b/src/registrar/models/domain.py index 8da56eb1b..b03b98ad1 100644 --- a/src/registrar/models/domain.py +++ b/src/registrar/models/domain.py @@ -27,7 +27,8 @@ try: RegistryError, ErrorCode, ) -except Exception: +except Exception as err: + print(f"err is {err}") pass from registrar.models.utility.contact_error import ContactError, ContactErrorCodes From a10ddccd0e903eaa52d7d0304ffbb8dcc2bff6cc Mon Sep 17 00:00:00 2001 From: zandercymatics <141044360+zandercymatics@users.noreply.github.com> Date: Wed, 18 Oct 2023 14:54:06 -0600 Subject: [PATCH 054/120] Test --- src/epplibwrapper/client.py | 3 ++- src/registrar/models/domain.py | 24 ++++++++++-------------- 2 files changed, 12 insertions(+), 15 deletions(-) diff --git a/src/epplibwrapper/client.py b/src/epplibwrapper/client.py index 5381f7ce1..b83f481b1 100644 --- a/src/epplibwrapper/client.py +++ b/src/epplibwrapper/client.py @@ -234,7 +234,8 @@ class EPPLibWrapper: try: # Initialize epplib - CLIENT = EPPLibWrapper() + #CLIENT = EPPLibWrapper() + CLIENT = None logger.info("registry client initialized") except Exception: CLIENT = None # type: ignore diff --git a/src/registrar/models/domain.py b/src/registrar/models/domain.py index b03b98ad1..94bdae0a1 100644 --- a/src/registrar/models/domain.py +++ b/src/registrar/models/domain.py @@ -16,20 +16,16 @@ from registrar.utility.errors import ( NameserverError, NameserverErrorCodes as nsErrorCodes, ) - -try: - from epplibwrapper import ( - CLIENT as registry, - commands, - common as epp, - extensions, - info as eppInfo, - RegistryError, - ErrorCode, - ) -except Exception as err: - print(f"err is {err}") - pass + +from epplibwrapper import ( + CLIENT as registry, + commands, + common as epp, + extensions, + info as eppInfo, + RegistryError, + ErrorCode, +) from registrar.models.utility.contact_error import ContactError, ContactErrorCodes From 599b22662d86f9ce90bb7935a4a0ff8dc9917646 Mon Sep 17 00:00:00 2001 From: zandercymatics <141044360+zandercymatics@users.noreply.github.com> Date: Wed, 18 Oct 2023 14:59:15 -0600 Subject: [PATCH 055/120] Defix the fix --- src/epplibwrapper/__init__.py | 2 -- src/epplibwrapper/client.py | 3 +-- 2 files changed, 1 insertion(+), 4 deletions(-) diff --git a/src/epplibwrapper/__init__.py b/src/epplibwrapper/__init__.py index 81add8e79..d0138d73c 100644 --- a/src/epplibwrapper/__init__.py +++ b/src/epplibwrapper/__init__.py @@ -44,8 +44,6 @@ except NameError: # Attn: these imports should NOT be at the top of the file try: - from epplibwrapper.socket import Socket - from epplibwrapper.utility.pool_error import PoolError, PoolErrorCodes from .client import CLIENT, commands from .errors import RegistryError, ErrorCode, CANNOT_CONTACT_REGISTRY, GENERIC_ERROR from epplib.models import common, info diff --git a/src/epplibwrapper/client.py b/src/epplibwrapper/client.py index b83f481b1..5381f7ce1 100644 --- a/src/epplibwrapper/client.py +++ b/src/epplibwrapper/client.py @@ -234,8 +234,7 @@ class EPPLibWrapper: try: # Initialize epplib - #CLIENT = EPPLibWrapper() - CLIENT = None + CLIENT = EPPLibWrapper() logger.info("registry client initialized") except Exception: CLIENT = None # type: ignore From 730319744f611a28a413ff067e1d472f14348c60 Mon Sep 17 00:00:00 2001 From: zandercymatics <141044360+zandercymatics@users.noreply.github.com> Date: Wed, 18 Oct 2023 15:26:11 -0600 Subject: [PATCH 056/120] Fix pipfile --- src/Pipfile | 1 + 1 file changed, 1 insertion(+) diff --git a/src/Pipfile b/src/Pipfile index 43b919c08..33f2c0954 100644 --- a/src/Pipfile +++ b/src/Pipfile @@ -25,6 +25,7 @@ django-phonenumber-field = {extras = ["phonenumberslite"], version = "*"} boto3 = "*" typing-extensions ='*' django-login-required-middleware = "*" +greenlet = "*" gevent = "*" fred-epplib = {git = "https://github.com/cisagov/epplib.git", ref = "master"} geventconnpool = {git = "https://github.com/rasky/geventconnpool.git", ref = "1bbb93a714a331a069adf27265fe582d9ba7ecd4"} From c2cc19ee1a21e1d2a8f4af5f90a09756fa5e1eef Mon Sep 17 00:00:00 2001 From: zandercymatics <141044360+zandercymatics@users.noreply.github.com> Date: Wed, 18 Oct 2023 15:28:51 -0600 Subject: [PATCH 057/120] Test --- src/Pipfile.lock | 195 +++++++++++++++--------------- src/epplibwrapper/utility/pool.py | 1 + src/requirements.txt | 14 +-- 3 files changed, 104 insertions(+), 106 deletions(-) diff --git a/src/Pipfile.lock b/src/Pipfile.lock index a7314de46..a773db219 100644 --- a/src/Pipfile.lock +++ b/src/Pipfile.lock @@ -1,7 +1,7 @@ { "_meta": { "hash": { - "sha256": "67b51a57b7d9d7d70d1eeca3515e169cd614d575a7213f31251f9dde43e1f748" + "sha256": "423c746438717fb7d281dfac02d3950e6e5033c6190f4adf0c63ccdf9433fae5" }, "pipfile-spec": 6, "requires": {}, @@ -32,20 +32,20 @@ }, "boto3": { "hashes": [ - "sha256:65d052ec13197460586ee385aa2d6bba0e7378d2d2c7f3e93c044c43ae1ca782", - "sha256:94218aba2feb5b404b665b8d76c172dc654f79b4c5fa0e9e92459c098da87bf4" + "sha256:38658585791f47cca3fc6aad03838de0136778b533e8c71c6a9590aedc60fbde", + "sha256:a8228522c7db33694c0746dec8b48c05473671626359dd62ab6829eb7871eddc" ], "index": "pypi", "markers": "python_version >= '3.7'", - "version": "==1.28.63" + "version": "==1.28.66" }, "botocore": { "hashes": [ - "sha256:6e582c811ea74f25bdb490ac372b2645de4a60286b42ddd8c69f3b6df82b6b12", - "sha256:cb9db5db5af865b1fc2e1405b967db5d78dd0f4d84e5dc1974e082733c1034b7" + "sha256:70e94a5f9bd46b26b63a41fb441ad35f2ae8862ad9d90765b6fa31ccc02c0a19", + "sha256:8d161a97a25eb381721b4b7251d5126ef4ec57e452114250b3e51ba5e4ff45a4" ], "markers": "python_version >= '3.7'", - "version": "==1.31.63" + "version": "==1.31.66" }, "cachetools": { "hashes": [ @@ -366,11 +366,11 @@ }, "faker": { "hashes": [ - "sha256:63da90512d0cb3acdb71bd833bb3071cb8a196020d08b8567a01d232954f1820", - "sha256:f321e657ed61616fbfe14dbb9ccc6b2e8282652bbcfcb503c1bd0231ff834df6" + "sha256:a62a3fd3bfa3122d4f57dfa26a1cc37d76751a76c8ddd63cf9d24078c57913a4", + "sha256:e28090068293c5a83e7f4d636417d45fae1031ca8a8136cc2415549ebc2111e2" ], "markers": "python_version >= '3.8'", - "version": "==19.10.0" + "version": "==19.11.0" }, "fred-epplib": { "git": "https://github.com/cisagov/epplib.git", @@ -439,7 +439,7 @@ }, "geventconnpool": { "git": "https://github.com/rasky/geventconnpool.git", - "ref": "1bbb93a714a331a069adf27265fe582d9ba7ecd4" + "ref": null }, "greenlet": { "hashes": [ @@ -506,7 +506,8 @@ "sha256:f351479a6914fd81a55c8e68963609f792d9b067fb8a60a042c585a621e0de4f", "sha256:f47932c434a3c8d3c86d865443fadc1fbf574e9b11d6650b656e602b1797908a" ], - "markers": "python_version < '3.11' and platform_python_implementation == 'CPython'", + "index": "pypi", + "markers": "python_version >= '3.7'", "version": "==3.0.0" }, "gunicorn": { @@ -740,10 +741,10 @@ }, "phonenumberslite": { "hashes": [ - "sha256:8d1e5f2adfee2a634ccdb54b251dec32c5308fbca3d7f6ae6058f4adee4594a3", - "sha256:98684f21804c6df2e7d224e72d60defee20eddf9e144d57f24cbd9db0df450e0" + "sha256:7c719e35ef551a895459382e9faf592f52647312dd90b543b06460aa0e1c49c4", + "sha256:cf6cf56c889c6787ec6b30b5791693f6dd678f633358f4aeea1fddf98d4cadcb" ], - "version": "==8.13.22" + "version": "==8.13.23" }, "psycopg2-binary": { "hashes": [ @@ -1070,11 +1071,11 @@ }, "urllib3": { "hashes": [ - "sha256:7a7c7003b000adf9e7ca2a377c9688bbc54ed41b985789ed576570342a375cd2", - "sha256:b19e1a85d206b56d7df1d5e683df4a7725252a964e3993648dd0fb5a1c157564" + "sha256:c97dfde1f7bd43a71c8d2a58e369e9b2bf692d1334ea9f9cae55add7d0dd0f84", + "sha256:fdb6d215c776278489906c2f8916e6e7d4f5a9b602ccbcfdf7f016fc8da0596e" ], "markers": "python_version >= '3.7'", - "version": "==2.0.6" + "version": "==2.0.7" }, "whitenoise": { "hashes": [ @@ -1164,32 +1165,28 @@ }, "black": { "hashes": [ - "sha256:031e8c69f3d3b09e1aa471a926a1eeb0b9071f80b17689a655f7885ac9325a6f", - "sha256:13a2e4a93bb8ca74a749b6974925c27219bb3df4d42fc45e948a5d9feb5122b7", - "sha256:13ef033794029b85dfea8032c9d3b92b42b526f1ff4bf13b2182ce4e917f5100", - "sha256:14f04c990259576acd093871e7e9b14918eb28f1866f91968ff5524293f9c573", - "sha256:24b6b3ff5c6d9ea08a8888f6977eae858e1f340d7260cf56d70a49823236b62d", - "sha256:403397c033adbc45c2bd41747da1f7fc7eaa44efbee256b53842470d4ac5a70f", - "sha256:50254ebfa56aa46a9fdd5d651f9637485068a1adf42270148cd101cdf56e0ad9", - "sha256:538efb451cd50f43aba394e9ec7ad55a37598faae3348d723b59ea8e91616300", - "sha256:638619a559280de0c2aa4d76f504891c9860bb8fa214267358f0a20f27c12948", - "sha256:6a3b50e4b93f43b34a9d3ef00d9b6728b4a722c997c99ab09102fd5efdb88325", - "sha256:6ccd59584cc834b6d127628713e4b6b968e5f79572da66284532525a042549f9", - "sha256:75a2dc41b183d4872d3a500d2b9c9016e67ed95738a3624f4751a0cb4818fe71", - "sha256:7d30ec46de88091e4316b17ae58bbbfc12b2de05e069030f6b747dfc649ad186", - "sha256:8431445bf62d2a914b541da7ab3e2b4f3bc052d2ccbf157ebad18ea126efb91f", - "sha256:8fc1ddcf83f996247505db6b715294eba56ea9372e107fd54963c7553f2b6dfe", - "sha256:a732b82747235e0542c03bf352c126052c0fbc458d8a239a94701175b17d4855", - "sha256:adc3e4442eef57f99b5590b245a328aad19c99552e0bdc7f0b04db6656debd80", - "sha256:c46767e8df1b7beefb0899c4a95fb43058fa8500b6db144f4ff3ca38eb2f6393", - "sha256:c619f063c2d68f19b2d7270f4cf3192cb81c9ec5bc5ba02df91471d0b88c4c5c", - "sha256:cf3a4d00e4cdb6734b64bf23cd4341421e8953615cba6b3670453737a72ec204", - "sha256:cf99f3de8b3273a8317681d8194ea222f10e0133a24a7548c73ce44ea1679377", - "sha256:d6bc09188020c9ac2555a498949401ab35bb6bf76d4e0f8ee251694664df6301" + "sha256:0e232f24a337fed7a82c1185ae46c56c4a6167fb0fe37411b43e876892c76699", + "sha256:30b78ac9b54cf87bcb9910ee3d499d2bc893afd52495066c49d9ee6b21eee06e", + "sha256:31946ec6f9c54ed7ba431c38bc81d758970dd734b96b8e8c2b17a367d7908171", + "sha256:31b9f87b277a68d0e99d2905edae08807c007973eaa609da5f0c62def6b7c0bd", + "sha256:47c4510f70ec2e8f9135ba490811c071419c115e46f143e4dce2ac45afdcf4c9", + "sha256:481167c60cd3e6b1cb8ef2aac0f76165843a374346aeeaa9d86765fe0dd0318b", + "sha256:6901631b937acbee93c75537e74f69463adaf34379a04eef32425b88aca88a23", + "sha256:76baba9281e5e5b230c9b7f83a96daf67a95e919c2dfc240d9e6295eab7b9204", + "sha256:7fb5fc36bb65160df21498d5a3dd330af8b6401be3f25af60c6ebfe23753f747", + "sha256:960c21555be135c4b37b7018d63d6248bdae8514e5c55b71e994ad37407f45b8", + "sha256:a3c2ddb35f71976a4cfeca558848c2f2f89abc86b06e8dd89b5a65c1e6c0f22a", + "sha256:c870bee76ad5f7a5ea7bd01dc646028d05568d33b0b09b7ecfc8ec0da3f3f39c", + "sha256:d3d9129ce05b0829730323bdcb00f928a448a124af5acf90aa94d9aba6969604", + "sha256:db451a3363b1e765c172c3fd86213a4ce63fb8524c938ebd82919bf2a6e28c6a", + "sha256:e223b731a0e025f8ef427dd79d8cd69c167da807f5710add30cdf131f13dd62e", + "sha256:f20ff03f3fdd2fd4460b4f631663813e57dc277e37fb216463f3b907aa5a9bdd", + "sha256:f74892b4b836e5162aa0452393112a574dac85e13902c57dfbaaf388e4eda37c", + "sha256:f8dc7d50d94063cdfd13c82368afd8588bac4ce360e4224ac399e769d6704e98" ], "index": "pypi", "markers": "python_version >= '3.8'", - "version": "==23.9.1" + "version": "==23.10.0" }, "blinker": { "hashes": [ @@ -1201,12 +1198,12 @@ }, "boto3": { "hashes": [ - "sha256:65d052ec13197460586ee385aa2d6bba0e7378d2d2c7f3e93c044c43ae1ca782", - "sha256:94218aba2feb5b404b665b8d76c172dc654f79b4c5fa0e9e92459c098da87bf4" + "sha256:38658585791f47cca3fc6aad03838de0136778b533e8c71c6a9590aedc60fbde", + "sha256:a8228522c7db33694c0746dec8b48c05473671626359dd62ab6829eb7871eddc" ], "index": "pypi", "markers": "python_version >= '3.7'", - "version": "==1.28.63" + "version": "==1.28.66" }, "boto3-mocking": { "hashes": [ @@ -1219,28 +1216,28 @@ }, "boto3-stubs": { "hashes": [ - "sha256:bd1becb0f8781d0a3022261a41d33f757a121117bf84ea6476b4761cb9e3cfd5", - "sha256:ecf4fb2b5b71be52cfc970ee059fe17439ed1904d0395508f5545c380d4d951d" + "sha256:06e696ce8529f899a2ba388d6604ca8ed8ba367dd53898e27b4ce49e8b3fd2aa", + "sha256:b85689c50a6768bb0fcb85e06394d7898b330b82f34cec26c36d912e6a41280d" ], "index": "pypi", "markers": "python_version >= '3.7'", - "version": "==1.28.63" + "version": "==1.28.66" }, "botocore": { "hashes": [ - "sha256:6e582c811ea74f25bdb490ac372b2645de4a60286b42ddd8c69f3b6df82b6b12", - "sha256:cb9db5db5af865b1fc2e1405b967db5d78dd0f4d84e5dc1974e082733c1034b7" + "sha256:70e94a5f9bd46b26b63a41fb441ad35f2ae8862ad9d90765b6fa31ccc02c0a19", + "sha256:8d161a97a25eb381721b4b7251d5126ef4ec57e452114250b3e51ba5e4ff45a4" ], "markers": "python_version >= '3.7'", - "version": "==1.31.63" + "version": "==1.31.66" }, "botocore-stubs": { "hashes": [ - "sha256:873715a5c21d0f4593628393c78e47cf94e53a43e40863a9ef5f165fcdcf900f", - "sha256:e92b5bd8d2667e557ea25025b396613c9bcb33d18b1971f98ebc24fa54caf495" + "sha256:5ea5f4af18ee654cf510d69b3bc7c1ed3236b50fcd4e3eb98c11d28033ff05c3", + "sha256:b65fa3ff36e8a70518a143b5025559918a68d7a20b85c88f8a1f067f6620a205" ], "markers": "python_version >= '3.7' and python_version < '4.0'", - "version": "==1.31.63" + "version": "==1.31.66" }, "click": { "hashes": [ @@ -1277,20 +1274,20 @@ }, "django-stubs": { "hashes": [ - "sha256:7d4a132c381519815e865c27a89eca41bcbd06056832507224816a43d75c601c", - "sha256:834b60fd81510cce6b56c1c6c28bec3c504a418bc90ff7d0063fabe8ab9a7868" + "sha256:5a23cf622f1426a0b0c48bd6e2ef709a66275d72073baf6fdf5ac36fc4cce736", + "sha256:706b2456bd0e56c468dfd8f27b0e7dde001c5c7cd3010d67fcbda9d95467e050" ], "index": "pypi", "markers": "python_version >= '3.8'", - "version": "==4.2.4" + "version": "==4.2.5" }, "django-stubs-ext": { "hashes": [ - "sha256:c69d1cc46f1c4c3b7894b685a5022c29b2a36c7cfb52e23762eaf357ebfc2c98", - "sha256:fdacc65a14d2d4b97334b58ff178a5853ec8c8c76cec406e417916ad67536ce4" + "sha256:8c4d1fb5f68419b3b2474c659681a189803e27d6a5e5abf5aa0da57601b58633", + "sha256:921cd7ae4614e74c234bc0fe86ee75537d163addfe1fc6f134bf03e29d86c01e" ], "markers": "python_version >= '3.8'", - "version": "==4.2.2" + "version": "==4.2.5" }, "django-webtest": { "hashes": [ @@ -1319,11 +1316,11 @@ }, "gitpython": { "hashes": [ - "sha256:5f4c4187de49616d710a77e98ddf17b4782060a1788df441846bddefbb89ab33", - "sha256:f9b9ddc0761c125d5780eab2d64be4873fc6817c2899cbcb34b02344bdc7bc54" + "sha256:22b126e9ffb671fdd0c129796343a02bf67bf2994b35449ffc9321aa755e18a4", + "sha256:cf14627d5a8049ffbf49915732e5eddbe8134c3bdb9d476e6182b676fc573f8a" ], "markers": "python_version >= '3.7'", - "version": "==3.1.37" + "version": "==3.1.40" }, "jmespath": { "hashes": [ @@ -1359,37 +1356,37 @@ }, "mypy": { "hashes": [ - "sha256:091f53ff88cb093dcc33c29eee522c087a438df65eb92acd371161c1f4380ff0", - "sha256:1a69db3018b87b3e6e9dd28970f983ea6c933800c9edf8c503c3135b3274d5ad", - "sha256:24f3de8b9e7021cd794ad9dfbf2e9fe3f069ff5e28cb57af6f873ffec1cb0425", - "sha256:31eba8a7a71f0071f55227a8057468b8d2eb5bf578c8502c7f01abaec8141b2f", - "sha256:3c8835a07b8442da900db47ccfda76c92c69c3a575872a5b764332c4bacb5a0a", - "sha256:3df87094028e52766b0a59a3e46481bb98b27986ed6ded6a6cc35ecc75bb9182", - "sha256:49499cf1e464f533fc45be54d20a6351a312f96ae7892d8e9f1708140e27ce41", - "sha256:4c192445899c69f07874dabda7e931b0cc811ea055bf82c1ababf358b9b2a72c", - "sha256:4f3d27537abde1be6d5f2c96c29a454da333a2a271ae7d5bc7110e6d4b7beb3f", - "sha256:7469545380dddce5719e3656b80bdfbb217cfe8dbb1438532d6abc754b828fed", - "sha256:7807a2a61e636af9ca247ba8494031fb060a0a744b9fee7de3a54bed8a753323", - "sha256:856bad61ebc7d21dbc019b719e98303dc6256cec6dcc9ebb0b214b81d6901bd8", - "sha256:89513ddfda06b5c8ebd64f026d20a61ef264e89125dc82633f3c34eeb50e7d60", - "sha256:8e0db37ac4ebb2fee7702767dfc1b773c7365731c22787cb99f507285014fcaf", - "sha256:971104bcb180e4fed0d7bd85504c9036346ab44b7416c75dd93b5c8c6bb7e28f", - "sha256:9e1589ca150a51d9d00bb839bfeca2f7a04f32cd62fad87a847bc0818e15d7dc", - "sha256:9f8464ed410ada641c29f5de3e6716cbdd4f460b31cf755b2af52f2d5ea79ead", - "sha256:ab98b8f6fdf669711f3abe83a745f67f50e3cbaea3998b90e8608d2b459fd566", - "sha256:b19006055dde8a5425baa5f3b57a19fa79df621606540493e5e893500148c72f", - "sha256:c69051274762cccd13498b568ed2430f8d22baa4b179911ad0c1577d336ed849", - "sha256:d2dad072e01764823d4b2f06bc7365bb1d4b6c2f38c4d42fade3c8d45b0b4b67", - "sha256:dccd850a2e3863891871c9e16c54c742dba5470f5120ffed8152956e9e0a5e13", - "sha256:e28d7b221898c401494f3b77db3bac78a03ad0a0fff29a950317d87885c655d2", - "sha256:e4b7a99275a61aa22256bab5839c35fe8a6887781862471df82afb4b445daae6", - "sha256:eb7ff4007865833c470a601498ba30462b7374342580e2346bf7884557e40531", - "sha256:f8598307150b5722854f035d2e70a1ad9cc3c72d392c34fffd8c66d888c90f17", - "sha256:fea451a3125bf0bfe716e5d7ad4b92033c471e4b5b3e154c67525539d14dc15a" + "sha256:19f905bcfd9e167159b3d63ecd8cb5e696151c3e59a1742e79bc3bcb540c42c7", + "sha256:21a1ad938fee7d2d96ca666c77b7c494c3c5bd88dff792220e1afbebb2925b5e", + "sha256:40b1844d2e8b232ed92e50a4bd11c48d2daa351f9deee6c194b83bf03e418b0c", + "sha256:41697773aa0bf53ff917aa077e2cde7aa50254f28750f9b88884acea38a16169", + "sha256:49ae115da099dcc0922a7a895c1eec82c1518109ea5c162ed50e3b3594c71208", + "sha256:4c46b51de523817a0045b150ed11b56f9fff55f12b9edd0f3ed35b15a2809de0", + "sha256:4cbe68ef919c28ea561165206a2dcb68591c50f3bcf777932323bc208d949cf1", + "sha256:4d01c00d09a0be62a4ca3f933e315455bde83f37f892ba4b08ce92f3cf44bcc1", + "sha256:59a0d7d24dfb26729e0a068639a6ce3500e31d6655df8557156c51c1cb874ce7", + "sha256:68351911e85145f582b5aa6cd9ad666c8958bcae897a1bfda8f4940472463c45", + "sha256:7274b0c57737bd3476d2229c6389b2ec9eefeb090bbaf77777e9d6b1b5a9d143", + "sha256:81af8adaa5e3099469e7623436881eff6b3b06db5ef75e6f5b6d4871263547e5", + "sha256:82e469518d3e9a321912955cc702d418773a2fd1e91c651280a1bda10622f02f", + "sha256:8b27958f8c76bed8edaa63da0739d76e4e9ad4ed325c814f9b3851425582a3cd", + "sha256:8c223fa57cb154c7eab5156856c231c3f5eace1e0bed9b32a24696b7ba3c3245", + "sha256:8f57e6b6927a49550da3d122f0cb983d400f843a8a82e65b3b380d3d7259468f", + "sha256:925cd6a3b7b55dfba252b7c4561892311c5358c6b5a601847015a1ad4eb7d332", + "sha256:a43ef1c8ddfdb9575691720b6352761f3f53d85f1b57d7745701041053deff30", + "sha256:a8032e00ce71c3ceb93eeba63963b864bf635a18f6c0c12da6c13c450eedb183", + "sha256:b96ae2c1279d1065413965c607712006205a9ac541895004a1e0d4f281f2ff9f", + "sha256:bb8ccb4724f7d8601938571bf3f24da0da791fe2db7be3d9e79849cb64e0ae85", + "sha256:bbaf4662e498c8c2e352da5f5bca5ab29d378895fa2d980630656178bd607c46", + "sha256:cfd13d47b29ed3bbaafaff7d8b21e90d827631afda134836962011acb5904b71", + "sha256:d4473c22cc296425bbbce7e9429588e76e05bc7342da359d6520b6427bf76660", + "sha256:d8fbb68711905f8912e5af474ca8b78d077447d8f3918997fecbf26943ff3cbb", + "sha256:e5012e5cc2ac628177eaac0e83d622b2dd499e28253d4107a08ecc59ede3fc2c", + "sha256:eb4f18589d196a4cbe5290b435d135dee96567e07c2b2d43b5c4621b6501531a" ], "index": "pypi", "markers": "python_version >= '3.8'", - "version": "==1.6.0" + "version": "==1.6.1" }, "mypy-extensions": { "hashes": [ @@ -1593,11 +1590,11 @@ }, "types-awscrt": { "hashes": [ - "sha256:477a14565909312fe1de70d0b301548e83c038f436b8a1d7c83729e87cdd0b85", - "sha256:d8c379420ba75b1e43687d12b0b772a5bb17f352859a2bef6aa8f0abde123f55" + "sha256:7b55f5a12ccd4407bc8f1e35c69bb40c931f8513ce1ad81a4527fce3989003fd", + "sha256:9a21caac4287c113dd52665707785c45bb1d3242b7a2b8aeb57c49e9e749a330" ], "markers": "python_version >= '3.7' and python_version < '4.0'", - "version": "==0.19.2" + "version": "==0.19.3" }, "types-cachetools": { "hashes": [ @@ -1623,12 +1620,12 @@ }, "types-requests": { "hashes": [ - "sha256:140e323da742a0cd0ff0a5a83669da9ffcebfaeb855d367186b2ec3985ba2742", - "sha256:3bb11188795cc3aa39f9635032044ee771009370fb31c3a06ae952b267b6fcd7" + "sha256:b32b9a86beffa876c0c3ac99a4cd3b8b51e973fb8e3bd4e0a6bb32c7efad80fc", + "sha256:dc5852a76f1eaf60eafa81a2e50aefa3d1f015c34cf0cba130930866b1b22a92" ], "index": "pypi", "markers": "python_version >= '3.7'", - "version": "==2.31.0.9" + "version": "==2.31.0.10" }, "types-s3transfer": { "hashes": [ @@ -1649,11 +1646,11 @@ }, "urllib3": { "hashes": [ - "sha256:7a7c7003b000adf9e7ca2a377c9688bbc54ed41b985789ed576570342a375cd2", - "sha256:b19e1a85d206b56d7df1d5e683df4a7725252a964e3993648dd0fb5a1c157564" + "sha256:c97dfde1f7bd43a71c8d2a58e369e9b2bf692d1334ea9f9cae55add7d0dd0f84", + "sha256:fdb6d215c776278489906c2f8916e6e7d4f5a9b602ccbcfdf7f016fc8da0596e" ], "markers": "python_version >= '3.7'", - "version": "==2.0.6" + "version": "==2.0.7" }, "waitress": { "hashes": [ diff --git a/src/epplibwrapper/utility/pool.py b/src/epplibwrapper/utility/pool.py index 3b8eb240c..2fff8e170 100644 --- a/src/epplibwrapper/utility/pool.py +++ b/src/epplibwrapper/utility/pool.py @@ -1,4 +1,5 @@ import logging +import greenlet import gevent from geventconnpool import ConnectionPool from epplibwrapper.socket import Socket diff --git a/src/requirements.txt b/src/requirements.txt index 0e3d41d87..109262b07 100644 --- a/src/requirements.txt +++ b/src/requirements.txt @@ -1,8 +1,8 @@ -i https://pypi.python.org/simple annotated-types==0.6.0; python_version >= '3.8' asgiref==3.7.2; python_version >= '3.7' -boto3==1.28.63; python_version >= '3.7' -botocore==1.31.63; python_version >= '3.7' +boto3==1.28.66; python_version >= '3.7' +botocore==1.31.66; python_version >= '3.7' cachetools==5.3.1; python_version >= '3.7' certifi==2023.7.22; python_version >= '3.6' cfenv==0.5.3 @@ -22,13 +22,13 @@ django-login-required-middleware==0.9.0 django-phonenumber-field[phonenumberslite]==7.2.0; python_version >= '3.8' django-widget-tweaks==1.5.0; python_version >= '3.8' environs[django]==9.5.0; python_version >= '3.6' -faker==19.10.0; python_version >= '3.8' +faker==19.11.0; python_version >= '3.8' fred-epplib@ git+https://github.com/cisagov/epplib.git@d56d183f1664f34c40ca9716a3a9a345f0ef561c 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' gevent==23.9.1; python_version >= '3.8' -geventconnpool@ git+https://github.com/rasky/geventconnpool.git@1bbb93a714a331a069adf27265fe582d9ba7ecd4 -greenlet==3.0.0; python_version < '3.11' and platform_python_implementation == 'CPython' +geventconnpool@ git+https://github.com/rasky/geventconnpool.git +greenlet==3.0.0; python_version >= '3.7' gunicorn==21.2.0; python_version >= '3.5' idna==3.4; python_version >= '3.5' jmespath==1.0.1; python_version >= '3.7' @@ -39,7 +39,7 @@ marshmallow==3.20.1; python_version >= '3.8' oic==1.6.1; python_version ~= '3.7' orderedmultidict==1.0.1 packaging==23.2; python_version >= '3.7' -phonenumberslite==8.13.22 +phonenumberslite==8.13.23 psycopg2-binary==2.9.9; python_version >= '3.7' pycparser==2.21 pycryptodomex==3.19.0; python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3, 3.4' @@ -55,7 +55,7 @@ setuptools==68.2.2; 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' sqlparse==0.4.4; python_version >= '3.5' typing-extensions==4.8.0; python_version >= '3.8' -urllib3==2.0.6; python_version >= '3.7' +urllib3==2.0.7; python_version >= '3.7' whitenoise==6.6.0; python_version >= '3.8' zope.event==5.0; python_version >= '3.7' zope.interface==6.1; python_version >= '3.7' From 9a12e29c5a4e0913d679ee8402e5fa009149df29 Mon Sep 17 00:00:00 2001 From: zandercymatics <141044360+zandercymatics@users.noreply.github.com> Date: Wed, 18 Oct 2023 15:41:38 -0600 Subject: [PATCH 058/120] Linter --- src/epplibwrapper/client.py | 14 +--- src/epplibwrapper/socket.py | 4 +- src/epplibwrapper/tests/test_pool.py | 115 ++++++++++++++------------- src/epplibwrapper/utility/pool.py | 2 +- src/registrar/config/settings.py | 2 +- src/registrar/models/domain.py | 2 +- 6 files changed, 66 insertions(+), 73 deletions(-) diff --git a/src/epplibwrapper/client.py b/src/epplibwrapper/client.py index 5381f7ce1..01998b955 100644 --- a/src/epplibwrapper/client.py +++ b/src/epplibwrapper/client.py @@ -163,16 +163,12 @@ class EPPLibWrapper: def get_pool(self): """Get the current pool instance""" return self._pool - + def _create_pool(self, client, login, options): """Creates and returns new pool instance""" - return EPPConnectionPool( - client, login, options - ) + return EPPConnectionPool(client, login, options) - def start_connection_pool( - self, restart_pool_if_exists=True - ): + def start_connection_pool(self, restart_pool_if_exists=True): """Starts a connection pool for the registry. restart_pool_if_exists -> bool: @@ -199,9 +195,7 @@ class EPPLibWrapper: logger.info("Connection pool restarting...") self.kill_pool() - self._pool = self._create_pool( - self._client, self._login, self.pool_options - ) + self._pool = self._create_pool(self._client, self._login, self.pool_options) self.pool_status.pool_running = True self.pool_status.pool_hanging = False diff --git a/src/epplibwrapper/socket.py b/src/epplibwrapper/socket.py index 186b98322..00cad80af 100644 --- a/src/epplibwrapper/socket.py +++ b/src/epplibwrapper/socket.py @@ -59,8 +59,6 @@ class Socket: counter += 1 sleep((counter * 50) / 1000) # sleep 50 ms to 150 ms else: # don't try again - logger.warning("LoginError raised and should not retry or has been retried 3 times already") - logger.warning(f"should retry? {err.should_retry()}") return False else: self.disconnect() @@ -69,7 +67,7 @@ class Socket: if self.is_login_error(response.code): logger.warning("was login error") return False - + # otherwise, just return true return True diff --git a/src/epplibwrapper/tests/test_pool.py b/src/epplibwrapper/tests/test_pool.py index b1912b9cf..077b059ea 100644 --- a/src/epplibwrapper/tests/test_pool.py +++ b/src/epplibwrapper/tests/test_pool.py @@ -1,15 +1,11 @@ import datetime from pathlib import Path -from unittest import skip from unittest.mock import MagicMock, patch from dateutil.tz import tzlocal -from django.conf import settings - from django.test import TestCase from epplibwrapper.client import EPPLibWrapper from epplibwrapper.socket import Socket from epplibwrapper.utility.pool import EPPConnectionPool -from registrar.models.domain import Domain from registrar.models.domain import registry from contextlib import ExitStack @@ -40,7 +36,7 @@ class TestConnectionPool(TestCase): # Value in seconds => (keepalive / size) "keepalive": 60, } - + def fake_socket(self, login, client): # Create a fake client object fake_client = Client( @@ -56,55 +52,57 @@ class TestConnectionPool(TestCase): 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 - + 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 + @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' + "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", } def fake_client(mock_client): @@ -117,16 +115,18 @@ class TestConnectionPool(TestCase): ) ) return client - + # Mock a response from EPP def fake_receive(command, cleaned=None): - location= Path(__file__).parent / "utility" / "infoDomain.xml" + location = Path(__file__).parent / "utility" / "infoDomain.xml" xml = (location).read_bytes() return xml # 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(EPPConnectionPool, "_create_socket", self.fake_socket) + ) stack.enter_context(patch.object(Socket, "connect", fake_client)) stack.enter_context(patch.object(SocketTransport, "send", self.fake_send)) stack.enter_context(patch.object(SocketTransport, "receive", fake_receive)) @@ -141,17 +141,18 @@ class TestConnectionPool(TestCase): # 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 + # 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"]) - + @patch.object(EPPLibWrapper, "_test_registry_connection_success", patch_success) def test_raises_transport_error(self): """A .send is invoked on the pool, but registry connection is lost right as we send a command.""" + # Fake data for the _pool object def fake_client(self): client = Client( @@ -165,7 +166,9 @@ class TestConnectionPool(TestCase): return client with ExitStack() as stack: - stack.enter_context(patch.object(EPPConnectionPool, "_create_socket", self.fake_socket)) + stack.enter_context( + patch.object(EPPConnectionPool, "_create_socket", self.fake_socket) + ) stack.enter_context(patch.object(Socket, "connect", fake_client)) # Restart the connection pool, since it starts on app startup registry.start_connection_pool() @@ -176,5 +179,3 @@ class TestConnectionPool(TestCase): # Try to send a command out - should fail with self.assertRaises(TransportError): registry.send(commands.InfoDomain(name="test.gov"), cleaned=True) - - diff --git a/src/epplibwrapper/utility/pool.py b/src/epplibwrapper/utility/pool.py index 2fff8e170..ba7edec91 100644 --- a/src/epplibwrapper/utility/pool.py +++ b/src/epplibwrapper/utility/pool.py @@ -1,9 +1,9 @@ import logging -import greenlet 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 except ImportError: diff --git a/src/registrar/config/settings.py b/src/registrar/config/settings.py index d4b0af408..2e88154ba 100644 --- a/src/registrar/config/settings.py +++ b/src/registrar/config/settings.py @@ -546,7 +546,7 @@ 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 +POOL_KEEP_ALIVE = 60 # Determines how long we try to keep a pool alive for, # before restarting it. diff --git a/src/registrar/models/domain.py b/src/registrar/models/domain.py index 94bdae0a1..2bfdd58c5 100644 --- a/src/registrar/models/domain.py +++ b/src/registrar/models/domain.py @@ -16,7 +16,7 @@ from registrar.utility.errors import ( NameserverError, NameserverErrorCodes as nsErrorCodes, ) - + from epplibwrapper import ( CLIENT as registry, commands, From 02a9c98a570f0c1d58a5d47f53e947990065a081 Mon Sep 17 00:00:00 2001 From: Rachid Mrad Date: Wed, 18 Oct 2023 17:49:49 -0400 Subject: [PATCH 059/120] fix org in domain table and write a unit test for it --- src/registrar/admin.py | 2 +- src/registrar/tests/test_admin.py | 25 ++++++++++++++++++++++++- 2 files changed, 25 insertions(+), 2 deletions(-) diff --git a/src/registrar/admin.py b/src/registrar/admin.py index 88f0de869..417d23708 100644 --- a/src/registrar/admin.py +++ b/src/registrar/admin.py @@ -721,7 +721,7 @@ class DomainAdmin(ListHeaderAdmin): ] def organization_type(self, obj): - return obj.domain_info.organization_type + return obj.domain_info.get_organization_type_display() organization_type.admin_order_field = ( # type: ignore "domain_info__organization_type" diff --git a/src/registrar/tests/test_admin.py b/src/registrar/tests/test_admin.py index b5827d3e9..805e97171 100644 --- a/src/registrar/tests/test_admin.py +++ b/src/registrar/tests/test_admin.py @@ -52,6 +52,26 @@ class TestDomainAdmin(MockEppLib): self.factory = RequestFactory() super().setUp() + def test_short_org_name_in_domains_list(self): + """ + Make sure the short name is displaying in admin on the list page + """ + self.client.force_login(self.superuser) + application = completed_application(status=DomainApplication.IN_REVIEW) + application.approve() + + response = self.client.get("/admin/registrar/domain/") + + # There are 3 template references to Federal (3) plus one reference in the table + # for our actual application + self.assertContains(response, "Federal", count=4) + # This may be a bit more robust + self.assertContains( + response, 'Federal', count=1 + ) + # Now let's make sure the long description does not exist + self.assertNotContains(response, "Federal: an agency of the U.S. government") + @skip("Why did this test stop working, and is is a good test") def test_place_and_remove_hold(self): domain = create_ready_domain() @@ -243,8 +263,11 @@ class TestDomainAdmin(MockEppLib): raise def tearDown(self): - User.objects.all().delete() super().tearDown() + Domain.objects.all().delete() + DomainInformation.objects.all().delete() + DomainApplication.objects.all().delete() + User.objects.all().delete() class TestDomainApplicationAdminForm(TestCase): From ceb2e5ec66c8d8a32f224d170fb2238d40af82d2 Mon Sep 17 00:00:00 2001 From: zandercymatics <141044360+zandercymatics@users.noreply.github.com> Date: Wed, 18 Oct 2023 16:03:03 -0600 Subject: [PATCH 060/120] Remove unused code --- src/epplibwrapper/client.py | 3 +-- src/epplibwrapper/errors.py | 3 --- 2 files changed, 1 insertion(+), 5 deletions(-) diff --git a/src/epplibwrapper/client.py b/src/epplibwrapper/client.py index 01998b955..6fafb2fd6 100644 --- a/src/epplibwrapper/client.py +++ b/src/epplibwrapper/client.py @@ -1,10 +1,9 @@ """Provide a wrapper around epplib to handle authentication and errors.""" import logging + from time import sleep - from gevent import Timeout - from epplibwrapper.utility.pool_status import PoolStatus try: diff --git a/src/epplibwrapper/errors.py b/src/epplibwrapper/errors.py index 0223ec0ae..dba5f328c 100644 --- a/src/epplibwrapper/errors.py +++ b/src/epplibwrapper/errors.py @@ -83,9 +83,6 @@ class RegistryError(Exception): def is_client_error(self): return self.code is not None and (self.code >= 2000 and self.code <= 2308) - def is_not_retryable(self): - pass - class LoginError(RegistryError): pass From d01bebec4110dc247fd3a882a86cdd07c9a9af4f Mon Sep 17 00:00:00 2001 From: Rachid Mrad Date: Wed, 18 Oct 2023 18:03:22 -0400 Subject: [PATCH 061/120] merge conflicting migrations --- src/registrar/migrations/0040_merge_20231018_2203.py | 12 ++++++++++++ 1 file changed, 12 insertions(+) create mode 100644 src/registrar/migrations/0040_merge_20231018_2203.py diff --git a/src/registrar/migrations/0040_merge_20231018_2203.py b/src/registrar/migrations/0040_merge_20231018_2203.py new file mode 100644 index 000000000..fad679098 --- /dev/null +++ b/src/registrar/migrations/0040_merge_20231018_2203.py @@ -0,0 +1,12 @@ +# Generated by Django 4.2.1 on 2023-10-18 22:03 + +from django.db import migrations + + +class Migration(migrations.Migration): + dependencies = [ + ("registrar", "0039_alter_transitiondomain_status"), + ("registrar", "0039_merge_20231013_2029"), + ] + + operations = [] From 825d07ba7c6b755750072f73a70a8127f7382551 Mon Sep 17 00:00:00 2001 From: zandercymatics <141044360+zandercymatics@users.noreply.github.com> Date: Thu, 19 Oct 2023 07:59:09 -0600 Subject: [PATCH 062/120] Debug client bug --- src/epplibwrapper/client.py | 58 +++++++++++++++++++------ src/epplibwrapper/errors.py | 2 + src/epplibwrapper/socket.py | 9 ++-- src/epplibwrapper/utility/pool_error.py | 5 +++ 4 files changed, 58 insertions(+), 16 deletions(-) diff --git a/src/epplibwrapper/client.py b/src/epplibwrapper/client.py index 6fafb2fd6..f068bfad4 100644 --- a/src/epplibwrapper/client.py +++ b/src/epplibwrapper/client.py @@ -4,14 +4,18 @@ import logging from time import sleep from gevent import Timeout +from epplibwrapper.utility.pool_error import PoolError, PoolErrorCodes from epplibwrapper.utility.pool_status import PoolStatus +logger = logging.getLogger(__name__) + try: from epplib.client import Client from epplib import commands from epplib.exceptions import TransportError, ParsingError from epplib.transport import SocketTransport except ImportError: + logger.warning("There was an import error {}") pass from django.conf import settings @@ -21,7 +25,7 @@ from .errors import LoginError, RegistryError from .socket import Socket from .utility.pool import EPPConnectionPool -logger = logging.getLogger(__name__) + try: # Write cert and key to disk @@ -55,15 +59,11 @@ class EPPLibWrapper: ], ) + # TODO - if client is none, send signal up and set it + # back to this # establish a client object with a TCP socket transport - self._client = Client( - SocketTransport( - settings.SECRET_REGISTRY_HOSTNAME, - cert_file=CERT.filename, - key_file=KEY.filename, - password=settings.SECRET_REGISTRY_KEY_PASSPHRASE, - ) - ) + self._client = self._get_default_client() + logger.warning(f"client is this {self._client}") self.pool_options = { # Pool size @@ -82,6 +82,16 @@ class EPPLibWrapper: if start_connection_pool: self.start_connection_pool() + def _get_default_client(self): + return Client( + SocketTransport( + settings.SECRET_REGISTRY_HOSTNAME, + cert_file=CERT.filename, + key_file=KEY.filename, + password=settings.SECRET_REGISTRY_KEY_PASSPHRASE, + ) + ) + def _send(self, command): """Helper function used by `send`.""" cmd_type = command.__class__.__name__ @@ -217,12 +227,34 @@ class EPPLibWrapper: credentials are valid, and/or if the Registrar can be contacted """ - socket = Socket(self._login, self._client) - can_login = False + socket = self._create_default_socket() + can_login = True # Something went wrong if this doesn't exist - if hasattr(socket, "test_connection_success"): + if not hasattr(socket, "test_connection_success"): + return can_login + + try: can_login = socket.test_connection_success() - return can_login + except PoolError as err: + logger.error(err) + # If the client isn't the right type, + # recreate it. + if err.code == PoolErrorCodes.INVALID_CLIENT_TYPE: + # Try to recreate the socket + self._client = self._get_default_client() + socket = self._create_default_socket() + + # Test it again + can_login = socket.test_connection_success() + return can_login + else: + return can_login + + def _create_default_socket(self): + """Creates a default socket. + Uses self._login and self._client + """ + return Socket(self._login, self._client) try: diff --git a/src/epplibwrapper/errors.py b/src/epplibwrapper/errors.py index dba5f328c..96188750c 100644 --- a/src/epplibwrapper/errors.py +++ b/src/epplibwrapper/errors.py @@ -86,3 +86,5 @@ class RegistryError(Exception): class LoginError(RegistryError): pass + + diff --git a/src/epplibwrapper/socket.py b/src/epplibwrapper/socket.py index 00cad80af..63fab9743 100644 --- a/src/epplibwrapper/socket.py +++ b/src/epplibwrapper/socket.py @@ -1,13 +1,15 @@ import logging from time import sleep +from epplibwrapper.utility.pool_error import PoolError, PoolErrorCodes + try: from epplib import commands from epplib.client import Client except ImportError: pass -from .errors import LoginError +from .errors import LoginError, SocketError logger = logging.getLogger(__name__) @@ -46,8 +48,9 @@ class Socket: 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 method") - return False + message = "self.client does not have a connect method" + logger.warning(message) + raise PoolError(code=PoolErrorCodes.INVALID_CLIENT_TYPE) counter = 0 # we'll try 3 times while True: diff --git a/src/epplibwrapper/utility/pool_error.py b/src/epplibwrapper/utility/pool_error.py index 70312f32e..2febcaaa0 100644 --- a/src/epplibwrapper/utility/pool_error.py +++ b/src/epplibwrapper/utility/pool_error.py @@ -9,11 +9,13 @@ class PoolErrorCodes(IntEnum): - 2000 KILL_ALL_FAILED - 2001 NEW_CONNECTION_FAILED - 2002 KEEP_ALIVE_FAILED + - 2003 INVALID_CLIENT_TYPE """ KILL_ALL_FAILED = 2000 NEW_CONNECTION_FAILED = 2001 KEEP_ALIVE_FAILED = 2002 + INVALID_CLIENT_TYPE = 2003 class PoolError(Exception): @@ -22,16 +24,19 @@ class PoolError(Exception): - 2000 KILL_ALL_FAILED - 2001 NEW_CONNECTION_FAILED - 2002 KEEP_ALIVE_FAILED + - 2003 INVALID_CLIENT_TYPE """ # For linter kill_failed = "Could not kill all connections." conn_failed = "Failed to execute due to a registry error." alive_failed = "Failed to keep the connection alive." + invalid_client = "Invalid client type." _error_mapping = { PoolErrorCodes.KILL_ALL_FAILED: kill_failed, PoolErrorCodes.NEW_CONNECTION_FAILED: conn_failed, PoolErrorCodes.KEEP_ALIVE_FAILED: alive_failed, + PoolErrorCodes.INVALID_CLIENT_TYPE: invalid_client } def __init__(self, *args, code=None, **kwargs): From d4633aeef2816376e99ad1845ea166c6101be164 Mon Sep 17 00:00:00 2001 From: zandercymatics <141044360+zandercymatics@users.noreply.github.com> Date: Thu, 19 Oct 2023 08:05:45 -0600 Subject: [PATCH 063/120] Update test_pool.py --- src/epplibwrapper/tests/test_pool.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/src/epplibwrapper/tests/test_pool.py b/src/epplibwrapper/tests/test_pool.py index 077b059ea..d7b4d4aad 100644 --- a/src/epplibwrapper/tests/test_pool.py +++ b/src/epplibwrapper/tests/test_pool.py @@ -170,8 +170,7 @@ class TestConnectionPool(TestCase): patch.object(EPPConnectionPool, "_create_socket", self.fake_socket) ) stack.enter_context(patch.object(Socket, "connect", fake_client)) - # Restart the connection pool, since it starts on app startup - registry.start_connection_pool() + # Pool should be running self.assertEqual(registry.pool_status.connection_success, True) self.assertEqual(registry.pool_status.pool_running, True) From 47afb0339faa72d0f4fafb86be1d85c6f4a104e4 Mon Sep 17 00:00:00 2001 From: zandercymatics <141044360+zandercymatics@users.noreply.github.com> Date: Thu, 19 Oct 2023 08:50:53 -0600 Subject: [PATCH 064/120] Update broken piplock --- src/Pipfile.lock | 2 +- src/requirements.txt | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/src/Pipfile.lock b/src/Pipfile.lock index a773db219..9d7daf597 100644 --- a/src/Pipfile.lock +++ b/src/Pipfile.lock @@ -439,7 +439,7 @@ }, "geventconnpool": { "git": "https://github.com/rasky/geventconnpool.git", - "ref": null + "ref": "1bbb93a714a331a069adf27265fe582d9ba7ecd4" }, "greenlet": { "hashes": [ diff --git a/src/requirements.txt b/src/requirements.txt index 109262b07..ff289ea63 100644 --- a/src/requirements.txt +++ b/src/requirements.txt @@ -27,7 +27,7 @@ fred-epplib@ git+https://github.com/cisagov/epplib.git@d56d183f1664f34c40ca9716a 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' gevent==23.9.1; python_version >= '3.8' -geventconnpool@ git+https://github.com/rasky/geventconnpool.git +geventconnpool@ git+https://github.com/rasky/geventconnpool.git@1bbb93a714a331a069adf27265fe582d9ba7ecd4 greenlet==3.0.0; python_version >= '3.7' gunicorn==21.2.0; python_version >= '3.5' idna==3.4; python_version >= '3.5' From 6262a4cf39dcae754cd74a0d45d17bb4d58384d0 Mon Sep 17 00:00:00 2001 From: zandercymatics <141044360+zandercymatics@users.noreply.github.com> Date: Thu, 19 Oct 2023 09:00:56 -0600 Subject: [PATCH 065/120] Revert "Debug client bug" This reverts commit 825d07ba7c6b755750072f73a70a8127f7382551. --- src/epplibwrapper/client.py | 58 ++++++------------------- src/epplibwrapper/errors.py | 2 - src/epplibwrapper/socket.py | 9 ++-- src/epplibwrapper/utility/pool_error.py | 5 --- 4 files changed, 16 insertions(+), 58 deletions(-) diff --git a/src/epplibwrapper/client.py b/src/epplibwrapper/client.py index f068bfad4..6fafb2fd6 100644 --- a/src/epplibwrapper/client.py +++ b/src/epplibwrapper/client.py @@ -4,18 +4,14 @@ import logging from time import sleep from gevent import Timeout -from epplibwrapper.utility.pool_error import PoolError, PoolErrorCodes from epplibwrapper.utility.pool_status import PoolStatus -logger = logging.getLogger(__name__) - try: from epplib.client import Client from epplib import commands from epplib.exceptions import TransportError, ParsingError from epplib.transport import SocketTransport except ImportError: - logger.warning("There was an import error {}") pass from django.conf import settings @@ -25,7 +21,7 @@ from .errors import LoginError, RegistryError from .socket import Socket from .utility.pool import EPPConnectionPool - +logger = logging.getLogger(__name__) try: # Write cert and key to disk @@ -59,11 +55,15 @@ class EPPLibWrapper: ], ) - # TODO - if client is none, send signal up and set it - # back to this # establish a client object with a TCP socket transport - self._client = self._get_default_client() - logger.warning(f"client is this {self._client}") + self._client = Client( + SocketTransport( + settings.SECRET_REGISTRY_HOSTNAME, + cert_file=CERT.filename, + key_file=KEY.filename, + password=settings.SECRET_REGISTRY_KEY_PASSPHRASE, + ) + ) self.pool_options = { # Pool size @@ -82,16 +82,6 @@ class EPPLibWrapper: if start_connection_pool: self.start_connection_pool() - def _get_default_client(self): - return Client( - SocketTransport( - settings.SECRET_REGISTRY_HOSTNAME, - cert_file=CERT.filename, - key_file=KEY.filename, - password=settings.SECRET_REGISTRY_KEY_PASSPHRASE, - ) - ) - def _send(self, command): """Helper function used by `send`.""" cmd_type = command.__class__.__name__ @@ -227,34 +217,12 @@ class EPPLibWrapper: credentials are valid, and/or if the Registrar can be contacted """ - socket = self._create_default_socket() - can_login = True + socket = Socket(self._login, self._client) + can_login = False # Something went wrong if this doesn't exist - if not hasattr(socket, "test_connection_success"): - return can_login - - try: + if hasattr(socket, "test_connection_success"): can_login = socket.test_connection_success() - except PoolError as err: - logger.error(err) - # If the client isn't the right type, - # recreate it. - if err.code == PoolErrorCodes.INVALID_CLIENT_TYPE: - # Try to recreate the socket - self._client = self._get_default_client() - socket = self._create_default_socket() - - # Test it again - can_login = socket.test_connection_success() - return can_login - else: - return can_login - - def _create_default_socket(self): - """Creates a default socket. - Uses self._login and self._client - """ - return Socket(self._login, self._client) + return can_login try: diff --git a/src/epplibwrapper/errors.py b/src/epplibwrapper/errors.py index 96188750c..dba5f328c 100644 --- a/src/epplibwrapper/errors.py +++ b/src/epplibwrapper/errors.py @@ -86,5 +86,3 @@ class RegistryError(Exception): class LoginError(RegistryError): pass - - diff --git a/src/epplibwrapper/socket.py b/src/epplibwrapper/socket.py index 63fab9743..00cad80af 100644 --- a/src/epplibwrapper/socket.py +++ b/src/epplibwrapper/socket.py @@ -1,15 +1,13 @@ import logging from time import sleep -from epplibwrapper.utility.pool_error import PoolError, PoolErrorCodes - try: from epplib import commands from epplib.client import Client except ImportError: pass -from .errors import LoginError, SocketError +from .errors import LoginError logger = logging.getLogger(__name__) @@ -48,9 +46,8 @@ class Socket: Tries 3 times""" # Something went wrong if this doesn't exist if not hasattr(self.client, "connect"): - message = "self.client does not have a connect method" - logger.warning(message) - raise PoolError(code=PoolErrorCodes.INVALID_CLIENT_TYPE) + logger.warning("self.client does not have a connect method") + return False counter = 0 # we'll try 3 times while True: diff --git a/src/epplibwrapper/utility/pool_error.py b/src/epplibwrapper/utility/pool_error.py index 2febcaaa0..70312f32e 100644 --- a/src/epplibwrapper/utility/pool_error.py +++ b/src/epplibwrapper/utility/pool_error.py @@ -9,13 +9,11 @@ class PoolErrorCodes(IntEnum): - 2000 KILL_ALL_FAILED - 2001 NEW_CONNECTION_FAILED - 2002 KEEP_ALIVE_FAILED - - 2003 INVALID_CLIENT_TYPE """ KILL_ALL_FAILED = 2000 NEW_CONNECTION_FAILED = 2001 KEEP_ALIVE_FAILED = 2002 - INVALID_CLIENT_TYPE = 2003 class PoolError(Exception): @@ -24,19 +22,16 @@ class PoolError(Exception): - 2000 KILL_ALL_FAILED - 2001 NEW_CONNECTION_FAILED - 2002 KEEP_ALIVE_FAILED - - 2003 INVALID_CLIENT_TYPE """ # For linter kill_failed = "Could not kill all connections." conn_failed = "Failed to execute due to a registry error." alive_failed = "Failed to keep the connection alive." - invalid_client = "Invalid client type." _error_mapping = { PoolErrorCodes.KILL_ALL_FAILED: kill_failed, PoolErrorCodes.NEW_CONNECTION_FAILED: conn_failed, PoolErrorCodes.KEEP_ALIVE_FAILED: alive_failed, - PoolErrorCodes.INVALID_CLIENT_TYPE: invalid_client } def __init__(self, *args, code=None, **kwargs): From 352a895d8d3d488dd855659ae4fcbf046e65c10d Mon Sep 17 00:00:00 2001 From: zandercymatics <141044360+zandercymatics@users.noreply.github.com> Date: Thu, 19 Oct 2023 09:07:35 -0600 Subject: [PATCH 066/120] Fix socket bug --- src/epplibwrapper/client.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/epplibwrapper/client.py b/src/epplibwrapper/client.py index 6fafb2fd6..4a65e63ea 100644 --- a/src/epplibwrapper/client.py +++ b/src/epplibwrapper/client.py @@ -217,7 +217,7 @@ class EPPLibWrapper: credentials are valid, and/or if the Registrar can be contacted """ - socket = Socket(self._login, self._client) + socket = Socket(self._client, self._login) can_login = False # Something went wrong if this doesn't exist if hasattr(socket, "test_connection_success"): From b663ac7b713d9d78f6c31cdee64fb5acee78fc88 Mon Sep 17 00:00:00 2001 From: zandercymatics <141044360+zandercymatics@users.noreply.github.com> Date: Thu, 19 Oct 2023 09:28:11 -0600 Subject: [PATCH 067/120] Fix edge case on localhost --- src/epplibwrapper/client.py | 3 ++- src/epplibwrapper/socket.py | 4 ++++ src/epplibwrapper/tests/test_pool.py | 2 +- 3 files changed, 7 insertions(+), 2 deletions(-) diff --git a/src/epplibwrapper/client.py b/src/epplibwrapper/client.py index 4a65e63ea..77e152d0e 100644 --- a/src/epplibwrapper/client.py +++ b/src/epplibwrapper/client.py @@ -229,8 +229,9 @@ try: # Initialize epplib CLIENT = EPPLibWrapper() logger.info("registry client initialized") -except Exception: +except Exception as err: CLIENT = None # type: ignore logger.warning( "Unable to configure epplib. Registrar cannot contact registry.", exc_info=True ) + logger.warning(err) diff --git a/src/epplibwrapper/socket.py b/src/epplibwrapper/socket.py index 00cad80af..8329e36cf 100644 --- a/src/epplibwrapper/socket.py +++ b/src/epplibwrapper/socket.py @@ -60,6 +60,10 @@ class Socket: sleep((counter * 50) / 1000) # sleep 50 ms to 150 ms else: # don't try again return False + # Occurs when an invalid creds are passed in - such as on localhost + except OSError as err: + logger.error(err) + return False else: self.disconnect() diff --git a/src/epplibwrapper/tests/test_pool.py b/src/epplibwrapper/tests/test_pool.py index d7b4d4aad..f82d5ee6a 100644 --- a/src/epplibwrapper/tests/test_pool.py +++ b/src/epplibwrapper/tests/test_pool.py @@ -170,7 +170,7 @@ class TestConnectionPool(TestCase): patch.object(EPPConnectionPool, "_create_socket", self.fake_socket) ) stack.enter_context(patch.object(Socket, "connect", fake_client)) - + # Pool should be running self.assertEqual(registry.pool_status.connection_success, True) self.assertEqual(registry.pool_status.pool_running, True) From e91ee079d097c7c371a0a8a37ded97a2914fe4b3 Mon Sep 17 00:00:00 2001 From: zandercymatics <141044360+zandercymatics@users.noreply.github.com> Date: Thu, 19 Oct 2023 09:40:40 -0600 Subject: [PATCH 068/120] Linter + fix test --- src/epplibwrapper/tests/test_pool.py | 13 +++++++++---- 1 file changed, 9 insertions(+), 4 deletions(-) diff --git a/src/epplibwrapper/tests/test_pool.py b/src/epplibwrapper/tests/test_pool.py index f82d5ee6a..edcd48981 100644 --- a/src/epplibwrapper/tests/test_pool.py +++ b/src/epplibwrapper/tests/test_pool.py @@ -4,6 +4,7 @@ from unittest.mock import MagicMock, patch from dateutil.tz import tzlocal 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 @@ -38,13 +39,15 @@ class TestConnectionPool(TestCase): } 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="none", + password=pw, ) ) @@ -149,7 +152,7 @@ class TestConnectionPool(TestCase): self.assertEqual(len(registry._pool.conn), self.pool_options["size"]) @patch.object(EPPLibWrapper, "_test_registry_connection_success", patch_success) - def test_raises_transport_error(self): + def test_raises_connection_error(self): """A .send is invoked on the pool, but registry connection is lost right as we send a command.""" @@ -176,5 +179,7 @@ class TestConnectionPool(TestCase): self.assertEqual(registry.pool_status.pool_running, True) # Try to send a command out - should fail - with self.assertRaises(TransportError): - registry.send(commands.InfoDomain(name="test.gov"), cleaned=True) + with self.assertRaises(RegistryError): + expected_message = "InfoDomain failed to execute due to a connection error." + result = registry.send(commands.InfoDomain(name="test.gov"), cleaned=True) + self.assertEqual(result, expected_message) From 29dc3111aefe4e4e64bda8100fa34eced38b2cf3 Mon Sep 17 00:00:00 2001 From: zandercymatics <141044360+zandercymatics@users.noreply.github.com> Date: Thu, 19 Oct 2023 09:59:31 -0600 Subject: [PATCH 069/120] Linter pt. 2 --- src/epplibwrapper/tests/test_pool.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/epplibwrapper/tests/test_pool.py b/src/epplibwrapper/tests/test_pool.py index edcd48981..a3ac66e54 100644 --- a/src/epplibwrapper/tests/test_pool.py +++ b/src/epplibwrapper/tests/test_pool.py @@ -109,12 +109,13 @@ class TestConnectionPool(TestCase): } def fake_client(mock_client): + pw = "none" client = Client( SocketTransport( "none", cert_file="path/to/cert_file", key_file="path/to/key_file", - password="none", + password=pw, ) ) return client From aabe4df12a4b56e8b75493c440c287876ea5b651 Mon Sep 17 00:00:00 2001 From: zandercymatics <141044360+zandercymatics@users.noreply.github.com> Date: Thu, 19 Oct 2023 10:11:23 -0600 Subject: [PATCH 070/120] Linter pt. 3 --- src/epplibwrapper/tests/test_pool.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/epplibwrapper/tests/test_pool.py b/src/epplibwrapper/tests/test_pool.py index a3ac66e54..9fb546bd1 100644 --- a/src/epplibwrapper/tests/test_pool.py +++ b/src/epplibwrapper/tests/test_pool.py @@ -159,12 +159,13 @@ class TestConnectionPool(TestCase): # Fake data for the _pool object def fake_client(self): + pw = "none" client = Client( SocketTransport( "none", cert_file="path/to/cert_file", key_file="path/to/key_file", - password="none", + password=pw, ) ) return client From a744b9407fa21e9a014614877bd387185efe6b7f Mon Sep 17 00:00:00 2001 From: zandercymatics <141044360+zandercymatics@users.noreply.github.com> Date: Thu, 19 Oct 2023 10:20:20 -0600 Subject: [PATCH 071/120] Lint pt. many --- src/epplibwrapper/tests/test_pool.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/epplibwrapper/tests/test_pool.py b/src/epplibwrapper/tests/test_pool.py index 9fb546bd1..24b8b7b31 100644 --- a/src/epplibwrapper/tests/test_pool.py +++ b/src/epplibwrapper/tests/test_pool.py @@ -1,7 +1,7 @@ import datetime from pathlib import Path from unittest.mock import MagicMock, patch -from dateutil.tz import tzlocal +from dateutil.tz import tzlocal # type: ignore from django.test import TestCase from epplibwrapper.client import EPPLibWrapper from epplibwrapper.errors import RegistryError From b8dfe0b8f053520ea2f0d1fefdb2d34c3231a8eb Mon Sep 17 00:00:00 2001 From: zandercymatics <141044360+zandercymatics@users.noreply.github.com> Date: Thu, 19 Oct 2023 10:25:51 -0600 Subject: [PATCH 072/120] Update test_pool.py --- src/epplibwrapper/tests/test_pool.py | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/src/epplibwrapper/tests/test_pool.py b/src/epplibwrapper/tests/test_pool.py index 24b8b7b31..35e5d9378 100644 --- a/src/epplibwrapper/tests/test_pool.py +++ b/src/epplibwrapper/tests/test_pool.py @@ -1,7 +1,7 @@ import datetime from pathlib import Path from unittest.mock import MagicMock, patch -from dateutil.tz import tzlocal # type: ignore +from dateutil.tz import tzlocal # type: ignore from django.test import TestCase from epplibwrapper.client import EPPLibWrapper from epplibwrapper.errors import RegistryError @@ -182,6 +182,8 @@ class TestConnectionPool(TestCase): # Try to send a command out - should fail with self.assertRaises(RegistryError): - expected_message = "InfoDomain failed to execute due to a connection error." - result = registry.send(commands.InfoDomain(name="test.gov"), cleaned=True) - self.assertEqual(result, expected_message) + expected = "InfoDomain failed to execute due to a connection error." + result = registry.send( + commands.InfoDomain(name="test.gov"), cleaned=True + ) + self.assertEqual(result, expected) From e7d73d2254224fd293705ed092d798ff006dd630 Mon Sep 17 00:00:00 2001 From: zandercymatics <141044360+zandercymatics@users.noreply.github.com> Date: Thu, 19 Oct 2023 11:52:32 -0600 Subject: [PATCH 073/120] Update test_models_domain.py --- src/registrar/tests/test_models_domain.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/registrar/tests/test_models_domain.py b/src/registrar/tests/test_models_domain.py index ef3084f9c..f0522b36d 100644 --- a/src/registrar/tests/test_models_domain.py +++ b/src/registrar/tests/test_models_domain.py @@ -873,7 +873,7 @@ class TestRegistrantContacts(MockEppLib): contact_id="regContact", contact_type=PublicContact.ContactTypeChoices.REGISTRANT, ) - + # test commit - will remove self.assertEqual( self.domain_contact.registrant_contact.email, expected_contact.email ) From 055942fe3aed798d56b35248bf776ed4cc1b3081 Mon Sep 17 00:00:00 2001 From: zandercymatics <141044360+zandercymatics@users.noreply.github.com> Date: Thu, 19 Oct 2023 15:59:28 -0600 Subject: [PATCH 074/120] Fix kill_pool() Kill pool was not killing instances correctly. This fixes that, and adds an additional test case --- src/epplibwrapper/client.py | 23 +++-- src/epplibwrapper/socket.py | 43 ++++++---- src/epplibwrapper/tests/test_pool.py | 124 +++++++++++++++++++++------ src/epplibwrapper/utility/pool.py | 64 ++++++++++++-- 4 files changed, 189 insertions(+), 65 deletions(-) diff --git a/src/epplibwrapper/client.py b/src/epplibwrapper/client.py index 77e152d0e..b6359d494 100644 --- a/src/epplibwrapper/client.py +++ b/src/epplibwrapper/client.py @@ -68,7 +68,9 @@ class EPPLibWrapper: self.pool_options = { # Pool size "size": settings.EPP_CONNECTION_POOL_SIZE, - # Which errors the pool should look out for + # Which errors the pool should look out for. + # Avoid changing this unless necessary, + # it can and will break things. "exc_classes": (TransportError,), # Occasionally pings the registry to keep the connection alive. # Value in seconds => (keepalive / size) @@ -76,6 +78,7 @@ class EPPLibWrapper: } self._pool = None + # Tracks the status of the pool self.pool_status = PoolStatus() @@ -85,9 +88,11 @@ class EPPLibWrapper: def _send(self, command): """Helper function used by `send`.""" cmd_type = command.__class__.__name__ + # Start a timeout to check if the pool is hanging timeout = Timeout(settings.POOL_TIMEOUT) timeout.start() + try: if not self.pool_status.connection_success: raise LoginError( @@ -96,6 +101,9 @@ class EPPLibWrapper: with self._pool.get() as connection: response = connection.send(command) except Timeout as t: + # 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. @@ -125,6 +133,7 @@ class EPPLibWrapper: else: return response finally: + # Close the timeout no matter what happens timeout.close() def send(self, command, *, cleaned=False): @@ -174,11 +183,6 @@ class EPPLibWrapper: If an instance of the pool already exists, then then that instance will be killed first. It is generally recommended to keep this enabled. - - try_start_if_invalid -> bool: - Designed for use in test cases, if we can't connect - to the registry, ignore that and try to connect anyway - It is generally recommended to keep this disabled. """ # Since we reuse the same creds for each pool, we can test on # one socket, and if successful, then we know we can connect. @@ -209,7 +213,7 @@ class EPPLibWrapper: self._pool.kill_all_connections() self._pool = None self.pool_status.pool_running = False - return + return None logger.info("kill_pool() was invoked but there was no pool to delete") def _test_registry_connection_success(self): @@ -219,9 +223,11 @@ class EPPLibWrapper: """ 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 @@ -229,9 +235,8 @@ try: # Initialize epplib CLIENT = EPPLibWrapper() logger.info("registry client initialized") -except Exception as err: +except Exception: CLIENT = None # type: ignore logger.warning( "Unable to configure epplib. Registrar cannot contact registry.", exc_info=True ) - logger.warning(err) diff --git a/src/epplibwrapper/socket.py b/src/epplibwrapper/socket.py index 8329e36cf..c44d07910 100644 --- a/src/epplibwrapper/socket.py +++ b/src/epplibwrapper/socket.py @@ -38,15 +38,36 @@ class Socket: raise LoginError(response.msg) return self.client + def disconnect(self): + """Close the connection.""" + try: + self.client.send(commands.Logout()) + self.client.close() + except Exception: + logger.warning("Connection to registry was not cleanly closed.") + + def send(self, command): + """Sends a command to the registry. + If the response 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""" return code >= 2000 def test_connection_success(self): """Tests if a successful connection can be made with the registry. - Tries 3 times""" + 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 method") + logger.warning("self.client does not have a connect attribute") return False counter = 0 # we'll try 3 times @@ -72,21 +93,5 @@ class Socket: logger.warning("was login error") return False - # otherwise, just return true + # Otherwise, just return true return True - - def disconnect(self): - """Close the connection.""" - try: - self.client.send(commands.Logout()) - self.client.close() - except Exception: - logger.warning("Connection to registry was not cleanly closed.") - - def send(self, command): - response = self.client.send(command) - if response.code >= 2000: - self.client.close() - raise LoginError(response.msg) - - return response diff --git a/src/epplibwrapper/tests/test_pool.py b/src/epplibwrapper/tests/test_pool.py index 35e5d9378..3a431ef1e 100644 --- a/src/epplibwrapper/tests/test_pool.py +++ b/src/epplibwrapper/tests/test_pool.py @@ -68,6 +68,18 @@ class TestConnectionPool(TestCase): ) 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""" @@ -108,18 +120,6 @@ class TestConnectionPool(TestCase): "sv_tr_id": "wRRNVhKhQW2m6wsUHbo/lA==-29a", } - 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 - # Mock a response from EPP def fake_receive(command, cleaned=None): location = Path(__file__).parent / "utility" / "infoDomain.xml" @@ -131,10 +131,10 @@ class TestConnectionPool(TestCase): stack.enter_context( patch.object(EPPConnectionPool, "_create_socket", self.fake_socket) ) - stack.enter_context(patch.object(Socket, "connect", fake_client)) + stack.enter_context(patch.object(Socket, "connect", self.fake_client)) stack.enter_context(patch.object(SocketTransport, "send", self.fake_send)) stack.enter_context(patch.object(SocketTransport, "receive", fake_receive)) - # Restart the connection pool, since it starts on app startup + # 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) @@ -152,29 +152,97 @@ class TestConnectionPool(TestCase): # If it is 0, then they failed to open self.assertEqual(len(registry._pool.conn), self.pool_options["size"]) + @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 + + # 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(SocketTransport, "send", self.fake_send)) + stack.enter_context(patch.object(SocketTransport, "receive", fake_receive)) + # Kill the connection pool + registry.kill_pool() + + self.assertEqual(registry.pool_status.connection_success, False) + 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"]) + @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.""" - # Fake data for the _pool object - def fake_client(self): - pw = "none" - client = Client( - SocketTransport( - "none", - cert_file="path/to/cert_file", - key_file="path/to/key_file", - password=pw, - ) - ) - return client - with ExitStack() as stack: stack.enter_context( patch.object(EPPConnectionPool, "_create_socket", self.fake_socket) ) - stack.enter_context(patch.object(Socket, "connect", fake_client)) + stack.enter_context(patch.object(Socket, "connect", self.fake_client)) # Pool should be running self.assertEqual(registry.pool_status.connection_success, True) diff --git a/src/epplibwrapper/utility/pool.py b/src/epplibwrapper/utility/pool.py index ba7edec91..322779285 100644 --- a/src/epplibwrapper/utility/pool.py +++ b/src/epplibwrapper/utility/pool.py @@ -6,9 +6,12 @@ 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__) @@ -27,7 +30,34 @@ class EPPConnectionPool(ConnectionPool): # For storing shared credentials self._client = client self._login = login - super().__init__(**options) + # Keep track of each greenlet + self.greenlets = [] + + # Define optional pool settings. + # Kept in a dict so that the parent class, + # client.py, can maintain seperation/expanadability + 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 + self.spawn_frequency = 0.1 + if "spawn_frequency" in options: + self.spawn_frequency = options["spawn_frequency"] + + self.conn = deque() + self.lock = BoundedSemaphore(self.size) + + self.populate_all_connections() def _new_connection(self): socket = self._create_socket(self._client, self._login) @@ -64,22 +94,38 @@ class EPPConnectionPool(ConnectionPool): def kill_all_connections(self): """Kills all active connections in the pool.""" try: - gevent.killall(self.conn) - self.conn.clear() - # Clear the semaphore - for i in range(self.lock.counter): - self.lock.release() + if len(self.conn) > 0: + gevent.killall(self.greenlets) + + self.greenlets.clear() + self.conn.clear() + + # Clear the semaphore + self.lock = BoundedSemaphore(self.size) + else: + logger.info("No connections to kill.") except Exception as err: logger.error("Could not kill all connections.") raise err - def repopulate_all_connections(self): - """Regenerates the connection pool. + 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: 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): - gevent.spawn_later(self.SPAWN_FREQUENCY * i, self._addOne) + 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)) From 02baff3f79197103b526385a9ca6e5e583829b4d Mon Sep 17 00:00:00 2001 From: zandercymatics <141044360+zandercymatics@users.noreply.github.com> Date: Thu, 19 Oct 2023 16:00:31 -0600 Subject: [PATCH 075/120] Add spacing --- src/epplibwrapper/utility/pool.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/epplibwrapper/utility/pool.py b/src/epplibwrapper/utility/pool.py index 322779285..01edb25e8 100644 --- a/src/epplibwrapper/utility/pool.py +++ b/src/epplibwrapper/utility/pool.py @@ -30,12 +30,13 @@ class EPPConnectionPool(ConnectionPool): # For storing shared credentials self._client = client self._login = login + # Keep track of each greenlet self.greenlets = [] # Define optional pool settings. # Kept in a dict so that the parent class, - # client.py, can maintain seperation/expanadability + # client.py, can maintain seperation/expandability self.size = 1 if "size" in options: self.size = options["size"] From 7998508881509982e3c00d2562b814c34ca7b6d9 Mon Sep 17 00:00:00 2001 From: CocoByte Date: Thu, 19 Oct 2023 17:16:09 -0600 Subject: [PATCH 076/120] Added unit tests Signed-off-by: CocoByte --- src/registrar/models/user.py | 56 ++++++++++++++++++++++-------- src/registrar/tests/test_models.py | 46 +++++++++++++++++++++++- 2 files changed, 86 insertions(+), 16 deletions(-) diff --git a/src/registrar/models/user.py b/src/registrar/models/user.py index afc16bb68..378b6b24e 100644 --- a/src/registrar/models/user.py +++ b/src/registrar/models/user.py @@ -4,9 +4,9 @@ from django.contrib.auth.models import AbstractUser from django.db import models from .domain_invitation import DomainInvitation -from registrar.models import TransitionDomain -from registrar.models import DomainInformation -from registrar.models import Domain +from .transition_domain import TransitionDomain +from .domain_information import DomainInformation +from .domain import Domain from phonenumber_field.modelfields import PhoneNumberField # type: ignore @@ -82,6 +82,26 @@ class User(AbstractUser): "Failed to retrieve invitation %s", invitation, exc_info=True ) + def create_domain_and_invite(self, transition_domain: TransitionDomain): + print("creating DOMAIN") + new_domain = Domain( + name=transition_domain.domain_name, state=transition_domain.status + ) + new_domain.save() + # check that a domain invitation doesn't already + # exist for this e-mail / Domain pair + domain_email_already_in_domain_invites = DomainInvitation.objects.filter( + email=transition_domain.username.lower(), domain=new_domain + ).exists() + if not domain_email_already_in_domain_invites: + + print("creating INVITATION") + # Create new domain invitation + new_domain_invitation = DomainInvitation( + email=transition_domain.username.lower(), domain=new_domain + ) + new_domain_invitation.save() + def check_transition_domains_on_login(self): """When a user first arrives on the site, we need to check if they are logging in with the same e-mail as a @@ -106,27 +126,26 @@ class User(AbstractUser): # Check that there is a corresponding domain object # for this transition domain. If not, we have an error # with our data and migrations need to be run again. - # TODO: how should we handle this? # Get the domain that corresponds with this transition domain - domain_exists = Domain.objects.filter(name=transition_domain.name).exists() + domain_exists = Domain.objects.filter(name=transition_domain.domain_name).exists() if not domain_exists: logger.warn("""There are transition domains without corresponding domain objects! Please run migration scripts for transition domains (See data_migration.md)""") - # TODO: throw exception?? - break - domain = Domain.objects.get(name=transition_domain.name) + # No need to throw an exception...just create a domain + # and domain invite, then proceed as normal + self.create_domain_and_invite(transition_domain) + + domain = Domain.objects.get(name=transition_domain.domain_name) # Create a domain information object, if one doesn't # already exist domain_info_exists = DomainInformation.objects.filter( - creator=self, domain=domain ).exists() if not domain_info_exists: - # TODO: Should we add a Domain to the DomainInfo object? new_domain_info = DomainInformation( creator=self, domain=domain) @@ -139,14 +158,21 @@ class User(AbstractUser): invitations that match their email address. We also need to check if they are logging in with the same e-mail - as a transition domain and update our database accordingly. + as a transition domain and update our domainInfo objects accordingly. """ - # PART 1: DOMAIN INVITATIONS - self.check_domain_invitations_on_login() - - # PART 2: TRANSITION DOMAINS + # PART 1: TRANSITION DOMAINS + # + # NOTE: THIS MUST RUN FIRST + # (If we have an issue where transition domains were + # not fully converted into Domain and DomainInvitation + # objects, this method will fill in the gaps. + # This will ensure the Domain Invitations method + # runs correctly (no missing invites)) self.check_transition_domains_on_login() + + # PART 2: DOMAIN INVITATIONS + self.check_domain_invitations_on_login() class Meta: diff --git a/src/registrar/tests/test_models.py b/src/registrar/tests/test_models.py index 2c6f78ef5..ffbd0fc55 100644 --- a/src/registrar/tests/test_models.py +++ b/src/registrar/tests/test_models.py @@ -14,7 +14,8 @@ from registrar.models import ( UserDomainRole, ) -import boto3_mocking # type: ignore +import boto3_mocking +from registrar.models.transition_domain import TransitionDomain # type: ignore from .common import MockSESClient, less_console_noise, completed_application from django_fsm import TransitionNotAllowed @@ -612,3 +613,46 @@ class TestInvitations(TestCase): """A new user's first_login callback retrieves their invitations.""" self.user.first_login() self.assertTrue(UserDomainRole.objects.get(user=self.user, domain=self.domain)) + +class TestUser(TestCase): + """For now, just test actions that + occur on user login.""" + + def setUp(self): + self.email = "mayor@igorville.gov" + self.domain_name = "igorvilleInTransition.gov" + self.user, _ = User.objects.get_or_create(email=self.email) + + # clean out the roles each time + UserDomainRole.objects.all().delete() + + TransitionDomain.objects.get_or_create(username="mayor@igorville.gov", + domain_name=self.domain_name) + + def tearDown(self): + super().tearDown() + Domain.objects.all().delete() + DomainInvitation.objects.all().delete() + DomainInformation.objects.all().delete() + TransitionDomain.objects.all().delete() + User.objects.all().delete() + + def test_check_transition_domains_on_login(self): + """A new user's first_login callback checks transition domains. + Makes DomainInformation object.""" + self.domain, _ = Domain.objects.get_or_create(name=self.domain_name) + + self.user.first_login() + self.assertTrue(DomainInformation.objects.get(domain=self.domain)) + + def test_check_transition_domains_without_domains_on_login(self): + """A new user's first_login callback checks transition domains. + This test makes sure that in the event a domain does not exist + for a given transition domain, both a domain and domain invitation + are created.""" + self.user.first_login() + self.assertTrue(Domain.objects.get(name=self.domain_name)) + + domain = Domain.objects.get(name=self.domain_name) + self.assertTrue(DomainInvitation.objects.get(email=self.email, domain=domain)) + self.assertTrue(DomainInformation.objects.get(domain=domain)) \ No newline at end of file From 257bc0f0ba2def600a45eb94be0d8486781ca3a8 Mon Sep 17 00:00:00 2001 From: Rebecca Hsieh Date: Thu, 19 Oct 2023 17:36:22 -0700 Subject: [PATCH 077/120] Add tests --- src/registrar/tests/test_admin.py | 36 +++++++++++++++++++++++++++++++ 1 file changed, 36 insertions(+) diff --git a/src/registrar/tests/test_admin.py b/src/registrar/tests/test_admin.py index 7dbc8ff38..4737909fe 100644 --- a/src/registrar/tests/test_admin.py +++ b/src/registrar/tests/test_admin.py @@ -11,6 +11,7 @@ from registrar.admin import ( ListHeaderAdmin, MyUserAdmin, AuditedAdmin, + ContactAdmin, ) from registrar.models import ( Domain, @@ -1310,3 +1311,38 @@ class DomainSessionVariableTest(TestCase): {"_edit_domain": "true"}, follow=True, ) + + +class ContactAdminTest(TestCase): + def setUp(self): + self.site = AdminSite() + self.factory = RequestFactory() + self.client = Client(HTTP_HOST="localhost:8080") + self.admin = ContactAdmin(model=get_user_model(), admin_site=None) + self.superuser = create_superuser() + self.staffuser = create_user() + + def test_readonly_when_restricted_staffuser(self): + request = self.factory.get("/") + request.user = self.staffuser + + readonly_fields = self.admin.get_readonly_fields(request) + + expected_fields = [ + "user", + ] + + self.assertEqual(readonly_fields, expected_fields) + + def test_readonly_when_restricted_superuser(self): + request = self.factory.get("/") + request.user = self.superuser + + readonly_fields = self.admin.get_readonly_fields(request) + + expected_fields = [] + + self.assertEqual(readonly_fields, expected_fields) + + def tearDown(self): + User.objects.all().delete() From bc14129f953720ee90c0ada655a60c2c6ca1e10a Mon Sep 17 00:00:00 2001 From: CocoByte Date: Thu, 19 Oct 2023 22:45:16 -0600 Subject: [PATCH 078/120] mostly linted Signed-off-by: CocoByte --- src/registrar/models/user.py | 44 ++++++++++++++---------------- src/registrar/tests/test_models.py | 14 ++++++---- 2 files changed, 29 insertions(+), 29 deletions(-) diff --git a/src/registrar/models/user.py b/src/registrar/models/user.py index 378b6b24e..e2fff98b8 100644 --- a/src/registrar/models/user.py +++ b/src/registrar/models/user.py @@ -83,22 +83,21 @@ class User(AbstractUser): ) def create_domain_and_invite(self, transition_domain: TransitionDomain): - print("creating DOMAIN") - new_domain = Domain( - name=transition_domain.domain_name, state=transition_domain.status - ) + transition_domain_name = transition_domain.domain_name + transition_domain_status = transition_domain.status + transition_domain_email = transition_domain.username + + new_domain = Domain(name=transition_domain_name, state=transition_domain_status) new_domain.save() # check that a domain invitation doesn't already # exist for this e-mail / Domain pair domain_email_already_in_domain_invites = DomainInvitation.objects.filter( - email=transition_domain.username.lower(), domain=new_domain + email=transition_domain_email.lower(), domain=new_domain ).exists() if not domain_email_already_in_domain_invites: - - print("creating INVITATION") # Create new domain invitation new_domain_invitation = DomainInvitation( - email=transition_domain.username.lower(), domain=new_domain + email=transition_domain_email.lower(), domain=new_domain ) new_domain_invitation.save() @@ -107,18 +106,16 @@ class User(AbstractUser): if they are logging in with the same e-mail as a transition domain and update our database accordingly.""" - for transition_domain in TransitionDomain.objects.filter( - username=self.email - ): + for transition_domain in TransitionDomain.objects.filter(username=self.email): # Looks like the user logged in with the same e-mail as - # one or more corresponding transition domains. + # one or more corresponding transition domains. # Create corresponding DomainInformation objects. # NOTE: adding an ADMIN user role for this user # for each domain should already be done - # in the invitation.retrieve() method. + # in the invitation.retrieve() method. # However, if the migration scripts for transition - # domain objects were not executed correctly, + # domain objects were not executed correctly, # there could be transition domains without # any corresponding Domain & DomainInvitation objects, # which means the invitation.retrieve() method might @@ -128,12 +125,16 @@ class User(AbstractUser): # with our data and migrations need to be run again. # Get the domain that corresponds with this transition domain - domain_exists = Domain.objects.filter(name=transition_domain.domain_name).exists() + domain_exists = Domain.objects.filter( + name=transition_domain.domain_name + ).exists() if not domain_exists: - logger.warn("""There are transition domains without - corresponding domain objects! + logger.warn( + """There are transition domains without + corresponding domain objects! Please run migration scripts for transition domains - (See data_migration.md)""") + (See data_migration.md)""" + ) # No need to throw an exception...just create a domain # and domain invite, then proceed as normal self.create_domain_and_invite(transition_domain) @@ -146,9 +147,7 @@ class User(AbstractUser): domain=domain ).exists() if not domain_info_exists: - new_domain_info = DomainInformation( - creator=self, - domain=domain) + new_domain_info = DomainInformation(creator=self, domain=domain) new_domain_info.save() def first_login(self): @@ -162,7 +161,7 @@ class User(AbstractUser): """ # PART 1: TRANSITION DOMAINS - # + # # NOTE: THIS MUST RUN FIRST # (If we have an issue where transition domains were # not fully converted into Domain and DomainInvitation @@ -173,7 +172,6 @@ class User(AbstractUser): # PART 2: DOMAIN INVITATIONS self.check_domain_invitations_on_login() - class Meta: permissions = [ diff --git a/src/registrar/tests/test_models.py b/src/registrar/tests/test_models.py index ffbd0fc55..f1a1724c5 100644 --- a/src/registrar/tests/test_models.py +++ b/src/registrar/tests/test_models.py @@ -614,8 +614,9 @@ class TestInvitations(TestCase): self.user.first_login() self.assertTrue(UserDomainRole.objects.get(user=self.user, domain=self.domain)) + class TestUser(TestCase): - """For now, just test actions that + """For now, just test actions that occur on user login.""" def setUp(self): @@ -626,8 +627,9 @@ class TestUser(TestCase): # clean out the roles each time UserDomainRole.objects.all().delete() - TransitionDomain.objects.get_or_create(username="mayor@igorville.gov", - domain_name=self.domain_name) + TransitionDomain.objects.get_or_create( + username="mayor@igorville.gov", domain_name=self.domain_name + ) def tearDown(self): super().tearDown() @@ -644,10 +646,10 @@ class TestUser(TestCase): self.user.first_login() self.assertTrue(DomainInformation.objects.get(domain=self.domain)) - + def test_check_transition_domains_without_domains_on_login(self): """A new user's first_login callback checks transition domains. - This test makes sure that in the event a domain does not exist + This test makes sure that in the event a domain does not exist for a given transition domain, both a domain and domain invitation are created.""" self.user.first_login() @@ -655,4 +657,4 @@ class TestUser(TestCase): domain = Domain.objects.get(name=self.domain_name) self.assertTrue(DomainInvitation.objects.get(email=self.email, domain=domain)) - self.assertTrue(DomainInformation.objects.get(domain=domain)) \ No newline at end of file + self.assertTrue(DomainInformation.objects.get(domain=domain)) From 1a3c095e0bcfbcc6f4ec588ea08113bed535d994 Mon Sep 17 00:00:00 2001 From: CocoByte Date: Thu, 19 Oct 2023 23:41:19 -0600 Subject: [PATCH 079/120] linted Signed-off-by: CocoByte --- src/registrar/models/user.py | 28 ++++++++++++++++------------ 1 file changed, 16 insertions(+), 12 deletions(-) diff --git a/src/registrar/models/user.py b/src/registrar/models/user.py index e2fff98b8..7207a5a61 100644 --- a/src/registrar/models/user.py +++ b/src/registrar/models/user.py @@ -87,19 +87,23 @@ class User(AbstractUser): transition_domain_status = transition_domain.status transition_domain_email = transition_domain.username - new_domain = Domain(name=transition_domain_name, state=transition_domain_status) - new_domain.save() - # check that a domain invitation doesn't already - # exist for this e-mail / Domain pair - domain_email_already_in_domain_invites = DomainInvitation.objects.filter( - email=transition_domain_email.lower(), domain=new_domain - ).exists() - if not domain_email_already_in_domain_invites: - # Create new domain invitation - new_domain_invitation = DomainInvitation( - email=transition_domain_email.lower(), domain=new_domain + # type safety check. name should never be none + if transition_domain_name is not None: + new_domain = Domain( + name=transition_domain_name, state=transition_domain_status ) - new_domain_invitation.save() + new_domain.save() + # check that a domain invitation doesn't already + # exist for this e-mail / Domain pair + domain_email_already_in_domain_invites = DomainInvitation.objects.filter( + email=transition_domain_email.lower(), domain=new_domain + ).exists() + if not domain_email_already_in_domain_invites: + # Create new domain invitation + new_domain_invitation = DomainInvitation( + email=transition_domain_email.lower(), domain=new_domain + ) + new_domain_invitation.save() def check_transition_domains_on_login(self): """When a user first arrives on the site, we need to check From 15ecaa83e70f0c40f8a5a06dc0f506a54967cbdc Mon Sep 17 00:00:00 2001 From: Cameron Dixon Date: Fri, 20 Oct 2023 06:33:11 -0400 Subject: [PATCH 080/120] Separate additional context from the description --- .github/ISSUE_TEMPLATE/issue-default.yml | 12 +++++++++--- 1 file changed, 9 insertions(+), 3 deletions(-) diff --git a/.github/ISSUE_TEMPLATE/issue-default.yml b/.github/ISSUE_TEMPLATE/issue-default.yml index 2252845bf..701742f72 100644 --- a/.github/ISSUE_TEMPLATE/issue-default.yml +++ b/.github/ISSUE_TEMPLATE/issue-default.yml @@ -10,9 +10,9 @@ body: - type: textarea id: issue-description attributes: - label: Issue description and context + label: Issue description description: | - Describe the issue so that someone who wasn't present for its discovery can understand why it matters. Use full sentences, plain language, and good [formatting](https://docs.github.com/en/get-started/writing-on-github/getting-started-with-writing-and-formatting-on-github/basic-writing-and-formatting-syntax). Screenshots and links to documents/discussions are welcome. + Describe the issue so that someone who wasn't present for its discovery can understand why it matters. Use full sentences, plain language, and good [formatting](https://docs.github.com/en/get-started/writing-on-github/getting-started-with-writing-and-formatting-on-github/basic-writing-and-formatting-syntax). validations: required: true - type: textarea @@ -21,6 +21,11 @@ body: label: Acceptance criteria description: "If known, share 1-3 statements that would need to be true for this issue to be considered resolved. Use a [task list](https://docs.github.com/en/get-started/writing-on-github/working-with-advanced-formatting/about-task-lists#creating-task-lists) if appropriate." placeholder: "- [ ]" + - type: textarea + id: additional-context + attributes: + label: Additional context + description: "Share any other thoughts, like how this might be implemented or fixed. Screenshots and links to documents/discussions are welcome." - type: textarea id: links-to-other-issues attributes: @@ -32,4 +37,5 @@ body: id: note attributes: value: | - > We may edit this issue's text to document our understanding and clarify the product work. + > We may edit the text in this issue to document our understanding and clarify the product work. + From 0b087a81517c4e2a66e14844de18adeb2441ad41 Mon Sep 17 00:00:00 2001 From: Cameron Dixon Date: Fri, 20 Oct 2023 06:38:34 -0400 Subject: [PATCH 081/120] fix spacing --- .github/ISSUE_TEMPLATE/issue-default.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/ISSUE_TEMPLATE/issue-default.yml b/.github/ISSUE_TEMPLATE/issue-default.yml index 701742f72..26384ceda 100644 --- a/.github/ISSUE_TEMPLATE/issue-default.yml +++ b/.github/ISSUE_TEMPLATE/issue-default.yml @@ -24,8 +24,8 @@ body: - type: textarea id: additional-context attributes: - label: Additional context - description: "Share any other thoughts, like how this might be implemented or fixed. Screenshots and links to documents/discussions are welcome." + label: Additional context + description: "Share any other thoughts, like how this might be implemented or fixed. Screenshots and links to documents/discussions are welcome." - type: textarea id: links-to-other-issues attributes: From 37d6bc9de9b18ccd18e8755c2bfe6ca4b8ffbd48 Mon Sep 17 00:00:00 2001 From: zandercymatics <141044360+zandercymatics@users.noreply.github.com> Date: Fri, 20 Oct 2023 08:48:07 -0600 Subject: [PATCH 082/120] Linting --- src/epplibwrapper/utility/pool.py | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/src/epplibwrapper/utility/pool.py b/src/epplibwrapper/utility/pool.py index 01edb25e8..99d5326ab 100644 --- a/src/epplibwrapper/utility/pool.py +++ b/src/epplibwrapper/utility/pool.py @@ -1,4 +1,5 @@ import logging +from typing import List import gevent from geventconnpool import ConnectionPool from epplibwrapper.socket import Socket @@ -32,7 +33,7 @@ class EPPConnectionPool(ConnectionPool): self._login = login # Keep track of each greenlet - self.greenlets = [] + self.greenlets: List[gevent.Greenlet] = [] # Define optional pool settings. # Kept in a dict so that the parent class, @@ -55,7 +56,7 @@ class EPPConnectionPool(ConnectionPool): if "spawn_frequency" in options: self.spawn_frequency = options["spawn_frequency"] - self.conn = deque() + self.conn: deque = deque() self.lock = BoundedSemaphore(self.size) self.populate_all_connections() From 34e0ce955ab54384b2404fac4ef4adafc493f49d Mon Sep 17 00:00:00 2001 From: Rachid Mrad Date: Fri, 20 Oct 2023 11:51:11 -0400 Subject: [PATCH 083/120] clean up a bad comment --- src/registrar/templatetags/custom_filters.py | 2 -- 1 file changed, 2 deletions(-) diff --git a/src/registrar/templatetags/custom_filters.py b/src/registrar/templatetags/custom_filters.py index 158b7269e..14e2c9e3e 100644 --- a/src/registrar/templatetags/custom_filters.py +++ b/src/registrar/templatetags/custom_filters.py @@ -55,11 +55,9 @@ def contains_checkbox(html_list): @register.filter def get_organization_long_name(organization_type): - # https://gist.github.com/OmenApps/3eef60ba4204f3d1842d9d7477efcce1#file-django_choices-txt-L28 organization_choices_dict = dict( DomainApplication.OrganizationChoicesVerbose.choices ) - long_form_type = organization_choices_dict[organization_type] if long_form_type is None: logger.error("Organization type error, triggered by a template's custom filter") From 3cfdacccfc3bc5aaf7aa71eb97da959b2cf1fc44 Mon Sep 17 00:00:00 2001 From: Neil Martinsen-Burrell Date: Fri, 20 Oct 2023 12:00:17 -0500 Subject: [PATCH 084/120] Change User management to Domain managers --- src/registrar/templates/domain_detail.html | 2 +- src/registrar/templates/domain_sidebar.html | 2 +- src/registrar/templates/domain_users.html | 17 +++++++++++++++-- src/registrar/tests/test_views.py | 2 +- src/registrar/views/domain.py | 2 +- 5 files changed, 19 insertions(+), 6 deletions(-) diff --git a/src/registrar/templates/domain_detail.html b/src/registrar/templates/domain_detail.html index e0d672093..4ddbd673a 100644 --- a/src/registrar/templates/domain_detail.html +++ b/src/registrar/templates/domain_detail.html @@ -52,7 +52,7 @@ {% include "includes/summary_item.html" with title='Security email' value='None provided' edit_link=url %} {% endif %} {% url 'domain-users' pk=domain.id as url %} - {% include "includes/summary_item.html" with title='User management' users='true' list=True value=domain.permissions.all edit_link=url %} + {% include "includes/summary_item.html" with title='Domain managers' users='true' list=True value=domain.permissions.all edit_link=url %}
{% endblock %} {# domain_content #} diff --git a/src/registrar/templates/domain_sidebar.html b/src/registrar/templates/domain_sidebar.html index 1acd87eeb..ac45ad04c 100644 --- a/src/registrar/templates/domain_sidebar.html +++ b/src/registrar/templates/domain_sidebar.html @@ -100,7 +100,7 @@ - User management + Domain managers diff --git a/src/registrar/templates/domain_users.html b/src/registrar/templates/domain_users.html index 22b9d18d1..5cb7acffd 100644 --- a/src/registrar/templates/domain_users.html +++ b/src/registrar/templates/domain_users.html @@ -1,10 +1,23 @@ {% extends "domain_base.html" %} {% load static %} -{% block title %}User management | {{ domain.name }} | {% endblock %} +{% block title %}Domain managers | {{ domain.name }} | {% endblock %} {% block domain_content %} -

User management

+

Domain managers

+ +

+ Domain managers can update all information related to a domain within the + .gov registrar, including contact details, authorizing official, security + email, and DNS name servers. +

+ +
    +
  • There is no limit to the number of domain managers you can add.
  • +
  • After adding a domain manager, an email invitation will be sent to that user with + instructions on how to set up an account.
  • +
  • To remove a domain manager, contact us for assistance. +
{% if domain.permissions %}
diff --git a/src/registrar/tests/test_views.py b/src/registrar/tests/test_views.py index 0e8f895af..8ad855433 100644 --- a/src/registrar/tests/test_views.py +++ b/src/registrar/tests/test_views.py @@ -1204,7 +1204,7 @@ class TestDomainUserManagement(TestDomainOverview): response = self.client.get( reverse("domain-users", kwargs={"pk": self.domain.id}) ) - self.assertContains(response, "User management") + self.assertContains(response, "Domain managers") def test_domain_user_management_add_link(self): """Button to get to user add page works.""" diff --git a/src/registrar/views/domain.py b/src/registrar/views/domain.py index aa71a7551..d9b671a65 100644 --- a/src/registrar/views/domain.py +++ b/src/registrar/views/domain.py @@ -656,7 +656,7 @@ class DomainSecurityEmailView(DomainFormBaseView): class DomainUsersView(DomainBaseView): - """User management page in the domain details.""" + """Domain managers page in the domain details.""" template_name = "domain_users.html" From 8e753796ec166e1dc467a4db2b6429623602425a Mon Sep 17 00:00:00 2001 From: Rebecca Hsieh Date: Fri, 20 Oct 2023 10:10:14 -0700 Subject: [PATCH 085/120] Update migration to match main --- .../migrations/0041_create_groups_v03.py | 37 +++++++++++++++++++ 1 file changed, 37 insertions(+) create mode 100644 src/registrar/migrations/0041_create_groups_v03.py diff --git a/src/registrar/migrations/0041_create_groups_v03.py b/src/registrar/migrations/0041_create_groups_v03.py new file mode 100644 index 000000000..4ddbb651a --- /dev/null +++ b/src/registrar/migrations/0041_create_groups_v03.py @@ -0,0 +1,37 @@ +# This migration creates the create_full_access_group and create_cisa_analyst_group groups +# It is dependent on 0035 (which populates ContentType and Permissions) +# If permissions on the groups need changing, edit CISA_ANALYST_GROUP_PERMISSIONS +# in the user_group model then: +# [NOT RECOMMENDED] +# step 1: docker-compose exec app ./manage.py migrate --fake registrar 0035_contenttypes_permissions +# step 2: docker-compose exec app ./manage.py migrate registrar 0036_create_groups +# step 3: fake run the latest migration in the migrations list +# [RECOMMENDED] +# Alternatively: +# step 1: duplicate the migration that loads data +# step 2: docker-compose exec app ./manage.py migrate + +from django.db import migrations +from registrar.models import UserGroup +from typing import Any + + +# For linting: RunPython expects a function reference, +# so let's give it one +def create_groups(apps, schema_editor) -> Any: + UserGroup.create_cisa_analyst_group(apps, schema_editor) + UserGroup.create_full_access_group(apps, schema_editor) + + +class Migration(migrations.Migration): + dependencies = [ + ("registrar", "0040_alter_user_domainrole_role"), + ] + + operations = [ + migrations.RunPython( + create_groups, + reverse_code=migrations.RunPython.noop, + atomic=True, + ), + ] From 070631b2160200850be76cd932af996aed677de6 Mon Sep 17 00:00:00 2001 From: Rebecca Hsieh Date: Fri, 20 Oct 2023 10:19:57 -0700 Subject: [PATCH 086/120] Fix naming for migration --- src/registrar/migrations/0041_create_groups_v03.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/registrar/migrations/0041_create_groups_v03.py b/src/registrar/migrations/0041_create_groups_v03.py index 4ddbb651a..d6ec5e433 100644 --- a/src/registrar/migrations/0041_create_groups_v03.py +++ b/src/registrar/migrations/0041_create_groups_v03.py @@ -25,7 +25,7 @@ def create_groups(apps, schema_editor) -> Any: class Migration(migrations.Migration): dependencies = [ - ("registrar", "0040_alter_user_domainrole_role"), + ("registrar", "0040_alter_userdomainrole_role"), ] operations = [ From a0911b46f84e7b16df9c0d38de591799a99c91e0 Mon Sep 17 00:00:00 2001 From: Neil Martinsen-Burrell Date: Fri, 20 Oct 2023 13:28:36 -0500 Subject: [PATCH 087/120] load public_site_url helper --- src/registrar/templates/domain_users.html | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/registrar/templates/domain_users.html b/src/registrar/templates/domain_users.html index 5cb7acffd..f66eef5a6 100644 --- a/src/registrar/templates/domain_users.html +++ b/src/registrar/templates/domain_users.html @@ -1,5 +1,5 @@ {% extends "domain_base.html" %} -{% load static %} +{% load static url_helpers %} {% block title %}Domain managers | {{ domain.name }} | {% endblock %} From 7f153f77ed70147c39e021ab02d85498c1cb4bf2 Mon Sep 17 00:00:00 2001 From: Neil Martinsen-Burrell Date: Fri, 20 Oct 2023 13:46:51 -0500 Subject: [PATCH 088/120] Review feedback: rename tests to match page name --- src/registrar/tests/test_views.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/registrar/tests/test_views.py b/src/registrar/tests/test_views.py index 8ad855433..1262347a1 100644 --- a/src/registrar/tests/test_views.py +++ b/src/registrar/tests/test_views.py @@ -1199,14 +1199,14 @@ class TestDomainOverview(TestWithDomainPermissions, WebTest): self.assertEqual(response.status_code, 403) -class TestDomainUserManagement(TestDomainOverview): - def test_domain_user_management(self): +class TestDomainManagers(TestDomainOverview): + def test_domain_managers(self): response = self.client.get( reverse("domain-users", kwargs={"pk": self.domain.id}) ) self.assertContains(response, "Domain managers") - def test_domain_user_management_add_link(self): + def test_domain_managers_add_link(self): """Button to get to user add page works.""" management_page = self.app.get( reverse("domain-users", kwargs={"pk": self.domain.id}) From 020f2614d0e7c727d0261a4f9bf3da0c22fbff3e Mon Sep 17 00:00:00 2001 From: Cameron Dixon Date: Fri, 20 Oct 2023 15:46:51 -0400 Subject: [PATCH 089/120] Update issue-default.yml --- .github/ISSUE_TEMPLATE/issue-default.yml | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/.github/ISSUE_TEMPLATE/issue-default.yml b/.github/ISSUE_TEMPLATE/issue-default.yml index 26384ceda..3a34b2943 100644 --- a/.github/ISSUE_TEMPLATE/issue-default.yml +++ b/.github/ISSUE_TEMPLATE/issue-default.yml @@ -21,9 +21,9 @@ body: label: Acceptance criteria description: "If known, share 1-3 statements that would need to be true for this issue to be considered resolved. Use a [task list](https://docs.github.com/en/get-started/writing-on-github/working-with-advanced-formatting/about-task-lists#creating-task-lists) if appropriate." placeholder: "- [ ]" - - type: textarea - id: additional-context - attributes: + - type: textarea + id: additional-context + attributes: label: Additional context description: "Share any other thoughts, like how this might be implemented or fixed. Screenshots and links to documents/discussions are welcome." - type: textarea From 95735598dc0dd60305c5a10074c3882b008780e9 Mon Sep 17 00:00:00 2001 From: Rachid Mrad Date: Fri, 20 Oct 2023 16:32:41 -0400 Subject: [PATCH 090/120] template revision, modal revision on dnssec, added modal on delete all on ds form --- src/registrar/assets/js/get-gov.js | 28 ++++ src/registrar/assets/sass/_theme/_forms.scss | 4 + src/registrar/templates/domain_dnssec.html | 25 ++- src/registrar/templates/domain_dsdata.html | 160 ++++++++++++------- src/registrar/templates/includes/modal.html | 71 ++++++-- src/registrar/views/domain.py | 39 ++++- 6 files changed, 240 insertions(+), 87 deletions(-) diff --git a/src/registrar/assets/js/get-gov.js b/src/registrar/assets/js/get-gov.js index c21060382..1813deea0 100644 --- a/src/registrar/assets/js/get-gov.js +++ b/src/registrar/assets/js/get-gov.js @@ -383,3 +383,31 @@ function prepareDeleteButtons() { } })(); + +/** + * An IIFE that triggers a modal on the DS Data Form under certain conditions + * + */ +(function triggerModalOnDsDataForm() { + let saveButon = document.querySelector("#save-ds-data"); + + // if (saveButon) + // saveButon.addEventListener('click', triggerModalIfModalTriggerExists); + + // function triggerModalIfModalTriggerExists(e){ + if (saveButon) { + let i = 0; + var tryToTriggerModal = setInterval(function() { + i++; + if (i > 100) { + clearInterval(tryToTriggerModal); + } + let modalTrigger = document.querySelector("#ds-toggle-dnssec-alert"); + if (modalTrigger) { + modalTrigger.click() + clearInterval(tryToTriggerModal); + } + }, 50); + + } +})(); diff --git a/src/registrar/assets/sass/_theme/_forms.scss b/src/registrar/assets/sass/_theme/_forms.scss index ed118bb94..38b42c3d0 100644 --- a/src/registrar/assets/sass/_theme/_forms.scss +++ b/src/registrar/assets/sass/_theme/_forms.scss @@ -4,6 +4,10 @@ margin-top: units(3); } +.usa-form .usa-button.margin-top-1 { + margin-top: units(1); +} + .usa-form--extra-large { max-width: none; } diff --git a/src/registrar/templates/domain_dnssec.html b/src/registrar/templates/domain_dnssec.html index c4a19470e..a851400ae 100644 --- a/src/registrar/templates/domain_dnssec.html +++ b/src/registrar/templates/domain_dnssec.html @@ -7,19 +7,32 @@

DNSSEC

-

DNSSEC, or DNS Security Extensions, is additional security layer to protect your website. Enabling DNSSEC ensures that when someone visits your domain, they can be certain that it's connecting to the correct server, preventing potential hijacking or tampering with your domain's records.

+

DNSSEC, or DNS Security Extensions, is additional security layer to protect your website. Enabling DNSSEC ensures that when someone visits your website, they can be certain that it’s connecting to the correct server, preventing potential hijacking or tampering with your domain's records.

{% csrf_token %} {% if has_dnssec_records %} -
-
- In order to fully disable DNSSEC on your domain, you will need to work with your DNS provider to remove your DNSSEC-related records from your zone. +
+
+

+

A note on disabling DNSSEC

+
    +
  • After clicking disable DNSSEC here, you will need to wait to disable DNSSEC at your DNS hosting provider after the expiration of the TTL (“time to live”) on your host’s records, which is usually between 1 hour and a day
  • +
  • If you disable DNSSEC at your DNS hosting provider before the expiration of the TTL, you may cause your domain to appear offline
  • +
  • Ask your DNS host for more information on their TTL expiry time
  • +
+

DNSSEC is enabled on your domain

Disable DNSSEC - {% include 'includes/modal.html' with modal_heading="Are you sure you want to continue?" modal_description="Your DNSSEC records will be deleted from the registry." modal_button=modal_button|safe %} + {% include 'includes/modal.html' with modal_heading="Are you sure you want to continue?" modal_button=modal_button|safe %}
{% endblock %} {# domain_content #} diff --git a/src/registrar/templates/domain_dsdata.html b/src/registrar/templates/domain_dsdata.html index 5aa1a9969..b387d34b6 100644 --- a/src/registrar/templates/domain_dsdata.html +++ b/src/registrar/templates/domain_dsdata.html @@ -21,79 +21,117 @@

In order to enable DNSSEC, you must first configure it with your DNS hosting service.

Enter the values given by your DNS provider for DS Data.

- {% include "includes/required_fields.html" %} - - {% csrf_token %} - {{ formset.management_form }} + {% if not dnssec_ds_confirmed %} +

Required fields are marked with an asterisk (*).

+ + {% csrf_token %} + + + {% else %} + {% include "includes/required_fields.html" %} - {% for form in formset %} -
+
+ {% csrf_token %} + {{ formset.management_form }} - DS Data record {{forloop.counter}} + {% for form in formset %} +
-

DS Data record {{forloop.counter}}

+ DS Data record {{forloop.counter}} -
-
- {% with attr_required=True add_group_class="usa-form-group--unstyled-error" %} - {% input_with_errors form.key_tag %} - {% endwith %} +

DS Data record {{forloop.counter}}

+ +
+
+ {% with attr_required=True add_group_class="usa-form-group--unstyled-error" %} + {% input_with_errors form.key_tag %} + {% endwith %} +
+
+ {% with attr_required=True add_group_class="usa-form-group--unstyled-error" %} + {% input_with_errors form.algorithm %} + {% endwith %} +
+
+ {% with attr_required=True add_group_class="usa-form-group--unstyled-error" %} + {% input_with_errors form.digest_type %} + {% endwith %} +
-
- {% with attr_required=True add_group_class="usa-form-group--unstyled-error" %} - {% input_with_errors form.algorithm %} - {% endwith %} + +
+
+ {% with attr_required=True add_group_class="usa-form-group--unstyled-error" %} + {% input_with_errors form.digest %} + {% endwith %} +
-
- {% with attr_required=True add_group_class="usa-form-group--unstyled-error" %} - {% input_with_errors form.digest_type %} - {% endwith %} + +
+
+ +
-
-
-
- {% with attr_required=True add_group_class="usa-form-group--unstyled-error" %} - {% input_with_errors form.digest %} - {% endwith %} -
-
+
+ {% endfor %} -
-
- -
-
+ -
- {% endfor %} + + - +
+ +
+ {% endif %} - - - -
- -
+ {% if trigger_modal %} + + {% endif %} + {# Use data-force-action to take esc out of the equation and pass cancel_button_resets_ds_form to effectuate a reset in the view #} +
+ {% include 'includes/modal.html' with cancel_button_resets_ds_form=True modal_heading="Are you sure you want to continue?" modal_description="some good text" modal_button=modal_button|safe %} +
{% endblock %} {# domain_content #} diff --git a/src/registrar/templates/includes/modal.html b/src/registrar/templates/includes/modal.html index 996759576..1e4bc78dc 100644 --- a/src/registrar/templates/includes/modal.html +++ b/src/registrar/templates/includes/modal.html @@ -1,3 +1,5 @@ +{% load static form_helpers url_helpers %} +
- + {% comment %} The cancel button the DS form actually triggers a context change in the view, + in addition to being a close modal hook {% endcomment %} + {% if cancel_button_resets_ds_form %} +
+ {% csrf_token %} + +
+ {% else %} + + {% endif %}
diff --git a/src/registrar/views/domain.py b/src/registrar/views/domain.py index 1e0505353..275079ffb 100644 --- a/src/registrar/views/domain.py +++ b/src/registrar/views/domain.py @@ -14,6 +14,7 @@ from django.shortcuts import redirect from django.template import RequestContext from django.urls import reverse from django.views.generic.edit import FormMixin +from django.shortcuts import render from registrar.models import ( Domain, @@ -252,7 +253,7 @@ class DomainDNSSECView(DomainPermissionView, FormMixin): # Create HTML for the modal button modal_button = ( '' ) @@ -322,6 +323,19 @@ class DomainDsDataView(DomainPermissionView, FormMixin): context = super().get_context_data(**kwargs) # use "formset" instead of "form" for the key context["formset"] = context.pop("form") + + # Create HTML for the modal button + modal_button = ( + '' + ) + + # context to back out of a broken form on all fields delete + context["modal_button"] = modal_button + context["dnssec_ds_confirmed"] = self.request.session.get( + "dnssec_ds_confirmed", False + ) return context @@ -329,16 +343,35 @@ class DomainDsDataView(DomainPermissionView, FormMixin): """Formset submission posts to this view.""" self.object = self.get_object() formset = self.get_form() + override = False + # switch to form in template + if "confirm-ds" in request.POST: + request.session["dnssec_ds_confirmed"] = True + return super().form_valid(formset) + + # This is called but the form cancel button, but also by the modal's X and cacel button if "btn-cancel-click" in request.POST: - return redirect("/", {"formset": formset}, RequestContext(request)) + return render(request, 'domain_dsdata.html', {'formset': formset}) + + if "disable-override-click" in request.POST: + override = True + # we are deleteing all data, + # switch out of form + request.session["dnssec_ds_confirmed"] = False + + if len(formset) == 0 and formset.initial != [{}] and override == False: + # trigger the modal + context = self.get_context_data(**kwargs) + context["trigger_modal"] = True + return self.render_to_response(context) if formset.is_valid(): return self.form_valid(formset) else: return self.form_invalid(formset) - def form_valid(self, formset): + def form_valid(self, formset, **kwargs): """The formset is valid, perform something with it.""" # Set the dnssecdata from the formset From ffbae457bf28e4a6cd676f4f7d3ddb2e571d96e6 Mon Sep 17 00:00:00 2001 From: Rachid Mrad Date: Fri, 20 Oct 2023 16:41:47 -0400 Subject: [PATCH 091/120] revise disable dnssec copy on dnssec page --- src/registrar/templates/domain_dnssec.html | 11 ++++++----- 1 file changed, 6 insertions(+), 5 deletions(-) diff --git a/src/registrar/templates/domain_dnssec.html b/src/registrar/templates/domain_dnssec.html index a851400ae..8e19e8360 100644 --- a/src/registrar/templates/domain_dnssec.html +++ b/src/registrar/templates/domain_dnssec.html @@ -7,7 +7,7 @@

DNSSEC

-

DNSSEC, or DNS Security Extensions, is additional security layer to protect your website. Enabling DNSSEC ensures that when someone visits your website, they can be certain that it’s connecting to the correct server, preventing potential hijacking or tampering with your domain's records.

+

DNSSEC, or DNS Security Extensions, is additional security layer to protect your website. Enabling DNSSEC ensures that when someone visits your website, they can be certain that it’s connecting to the correct server, preventing potential hijacking or tampering with your domain's records.

{% csrf_token %} @@ -21,12 +21,13 @@

-

A note on disabling DNSSEC

+

To fully disable DNSSEC

    -
  • After clicking disable DNSSEC here, you will need to wait to disable DNSSEC at your DNS hosting provider after the expiration of the TTL (“time to live”) on your host’s records, which is usually between 1 hour and a day
  • -
  • If you disable DNSSEC at your DNS hosting provider before the expiration of the TTL, you may cause your domain to appear offline
  • -
  • Ask your DNS host for more information on their TTL expiry time
  • +
  • Click “Disable DNSSEC” below.
  • +
  • Wait until the Time to Live (TTL) expires on your DNSSEC records managed by your DNS hosting provider. This is often less than 24 hours, but confirm with your provider.
  • +
  • After the TTL expiration, disable DNSSEC at your DNS hosting provider.
+

Warning: If you disable DNSSEC at your DNS hosting provider before TTL expiration, this may cause your domain to appear offline.

DNSSEC is enabled on your domain

From 0d12c17aff932f80ad04c7a4541d7e18074c4a45 Mon Sep 17 00:00:00 2001 From: Rachid Mrad Date: Fri, 20 Oct 2023 17:02:21 -0400 Subject: [PATCH 092/120] Add Vicky to fixtures --- src/registrar/fixtures_users.py | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/src/registrar/fixtures_users.py b/src/registrar/fixtures_users.py index dfe51785b..f03fb025d 100644 --- a/src/registrar/fixtures_users.py +++ b/src/registrar/fixtures_users.py @@ -86,6 +86,12 @@ class UserFixture: "first_name": "Kristina", "last_name": "Yin", }, + { + "username": "ac49d7c1-368a-4e6b-8f1d-60250e20a16f", + "first_name": "Vicky", + "last_name": "Chin", + "email": "szu.chin@associates.cisa.dhs.gov", + }, ] STAFF = [ @@ -150,6 +156,12 @@ class UserFixture: "last_name": "Yin-Analyst", "email": "kristina.yin+1@gsa.gov", }, + { + "username": "8f42302e-b83a-4c9e-8764-fc19e2cea576", + "first_name": "Vickster-Analyst", + "last_name": "Chin-Analyst", + "email": "szu.chin@ecstech.com", + }, ] def load_users(cls, users, group_name): From b8b12bad6ea2f1330ef840cdf1318795d7876fae Mon Sep 17 00:00:00 2001 From: Rachid Mrad Date: Fri, 20 Oct 2023 17:09:50 -0400 Subject: [PATCH 093/120] Clean up migrations --- ...napplication_organization_type_and_more.py | 52 ------------------- .../migrations/0039_merge_20231013_2029.py | 12 ----- .../migrations/0040_merge_20231018_2203.py | 12 ----- 3 files changed, 76 deletions(-) delete mode 100644 src/registrar/migrations/0038_alter_domainapplication_organization_type_and_more.py delete mode 100644 src/registrar/migrations/0039_merge_20231013_2029.py delete mode 100644 src/registrar/migrations/0040_merge_20231018_2203.py diff --git a/src/registrar/migrations/0038_alter_domainapplication_organization_type_and_more.py b/src/registrar/migrations/0038_alter_domainapplication_organization_type_and_more.py deleted file mode 100644 index a06ea0451..000000000 --- a/src/registrar/migrations/0038_alter_domainapplication_organization_type_and_more.py +++ /dev/null @@ -1,52 +0,0 @@ -# Generated by Django 4.2.1 on 2023-10-13 20:26 - -from django.db import migrations, models - - -class Migration(migrations.Migration): - dependencies = [ - ("registrar", "0037_create_groups_v01"), - ] - - operations = [ - migrations.AlterField( - model_name="domainapplication", - name="organization_type", - field=models.CharField( - blank=True, - choices=[ - ("federal", "Federal"), - ("interstate", "Interstate"), - ("state_or_territory", "State or territory"), - ("tribal", "Tribal"), - ("county", "County"), - ("city", "City"), - ("special_district", "Special district"), - ("school_district", "School district"), - ], - help_text="Type of organization", - max_length=255, - null=True, - ), - ), - migrations.AlterField( - model_name="domaininformation", - name="organization_type", - field=models.CharField( - blank=True, - choices=[ - ("federal", "Federal"), - ("interstate", "Interstate"), - ("state_or_territory", "State or territory"), - ("tribal", "Tribal"), - ("county", "County"), - ("city", "City"), - ("special_district", "Special district"), - ("school_district", "School district"), - ], - help_text="Type of Organization", - max_length=255, - null=True, - ), - ), - ] diff --git a/src/registrar/migrations/0039_merge_20231013_2029.py b/src/registrar/migrations/0039_merge_20231013_2029.py deleted file mode 100644 index aed231bdc..000000000 --- a/src/registrar/migrations/0039_merge_20231013_2029.py +++ /dev/null @@ -1,12 +0,0 @@ -# Generated by Django 4.2.1 on 2023-10-13 20:29 - -from django.db import migrations - - -class Migration(migrations.Migration): - dependencies = [ - ("registrar", "0038_alter_domainapplication_organization_type_and_more"), - ("registrar", "0038_create_groups_v02"), - ] - - operations = [] diff --git a/src/registrar/migrations/0040_merge_20231018_2203.py b/src/registrar/migrations/0040_merge_20231018_2203.py deleted file mode 100644 index fad679098..000000000 --- a/src/registrar/migrations/0040_merge_20231018_2203.py +++ /dev/null @@ -1,12 +0,0 @@ -# Generated by Django 4.2.1 on 2023-10-18 22:03 - -from django.db import migrations - - -class Migration(migrations.Migration): - dependencies = [ - ("registrar", "0039_alter_transitiondomain_status"), - ("registrar", "0039_merge_20231013_2029"), - ] - - operations = [] From d5b21435ddea84afd8fb821c065a3606a37f1726 Mon Sep 17 00:00:00 2001 From: Rachid Mrad Date: Fri, 20 Oct 2023 17:11:00 -0400 Subject: [PATCH 094/120] re-create org names migration --- ...napplication_organization_type_and_more.py | 52 +++++++++++++++++++ 1 file changed, 52 insertions(+) create mode 100644 src/registrar/migrations/0041_alter_domainapplication_organization_type_and_more.py diff --git a/src/registrar/migrations/0041_alter_domainapplication_organization_type_and_more.py b/src/registrar/migrations/0041_alter_domainapplication_organization_type_and_more.py new file mode 100644 index 000000000..07cfe0e77 --- /dev/null +++ b/src/registrar/migrations/0041_alter_domainapplication_organization_type_and_more.py @@ -0,0 +1,52 @@ +# Generated by Django 4.2.1 on 2023-10-20 21:10 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + dependencies = [ + ("registrar", "0040_alter_userdomainrole_role"), + ] + + operations = [ + migrations.AlterField( + model_name="domainapplication", + name="organization_type", + field=models.CharField( + blank=True, + choices=[ + ("federal", "Federal"), + ("interstate", "Interstate"), + ("state_or_territory", "State or territory"), + ("tribal", "Tribal"), + ("county", "County"), + ("city", "City"), + ("special_district", "Special district"), + ("school_district", "School district"), + ], + help_text="Type of organization", + max_length=255, + null=True, + ), + ), + migrations.AlterField( + model_name="domaininformation", + name="organization_type", + field=models.CharField( + blank=True, + choices=[ + ("federal", "Federal"), + ("interstate", "Interstate"), + ("state_or_territory", "State or territory"), + ("tribal", "Tribal"), + ("county", "County"), + ("city", "City"), + ("special_district", "Special district"), + ("school_district", "School district"), + ], + help_text="Type of Organization", + max_length=255, + null=True, + ), + ), + ] From fdd2aaf7adeec762896dea85ecc5783b9afcdcc4 Mon Sep 17 00:00:00 2001 From: Cameron Dixon Date: Fri, 20 Oct 2023 17:11:09 -0400 Subject: [PATCH 095/120] Replace :emojicode: with sq brackets, extend href --- .github/ISSUE_TEMPLATE/issue-default.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/ISSUE_TEMPLATE/issue-default.yml b/.github/ISSUE_TEMPLATE/issue-default.yml index 3a34b2943..47fb2c226 100644 --- a/.github/ISSUE_TEMPLATE/issue-default.yml +++ b/.github/ISSUE_TEMPLATE/issue-default.yml @@ -12,7 +12,7 @@ body: attributes: label: Issue description description: | - Describe the issue so that someone who wasn't present for its discovery can understand why it matters. Use full sentences, plain language, and good [formatting](https://docs.github.com/en/get-started/writing-on-github/getting-started-with-writing-and-formatting-on-github/basic-writing-and-formatting-syntax). + Describe the issue so that someone who wasn't present for its discovery can understand why it matters. Use full sentences, plain language, and [good formatting](https://docs.github.com/en/get-started/writing-on-github/getting-started-with-writing-and-formatting-on-github/basic-writing-and-formatting-syntax). validations: required: true - type: textarea @@ -31,7 +31,7 @@ body: attributes: label: Links to other issues description: | - "Add issue #numbers this relates to and how (e.g., 🚧 :construction: Blocks, ⛔️ :no_entry: Is blocked by, 🔄 :repeat: Relates to)." + "Add issue #numbers this relates to and how (e.g., 🚧 [construction] Blocks, ⛔️ [no_entry] Is blocked by, 🔄 [arrows_counterclockwise] Relates to)." placeholder: 🔄 Relates to... - type: markdown id: note From 62b6514b0732d3e6713dbc296cd2adb14dce01d1 Mon Sep 17 00:00:00 2001 From: Rebecca Hsieh Date: Fri, 20 Oct 2023 14:50:02 -0700 Subject: [PATCH 096/120] Remove 0041 so I can match main --- .../migrations/0041_create_groups_v03.py | 37 ------------------- 1 file changed, 37 deletions(-) delete mode 100644 src/registrar/migrations/0041_create_groups_v03.py diff --git a/src/registrar/migrations/0041_create_groups_v03.py b/src/registrar/migrations/0041_create_groups_v03.py deleted file mode 100644 index d6ec5e433..000000000 --- a/src/registrar/migrations/0041_create_groups_v03.py +++ /dev/null @@ -1,37 +0,0 @@ -# This migration creates the create_full_access_group and create_cisa_analyst_group groups -# It is dependent on 0035 (which populates ContentType and Permissions) -# If permissions on the groups need changing, edit CISA_ANALYST_GROUP_PERMISSIONS -# in the user_group model then: -# [NOT RECOMMENDED] -# step 1: docker-compose exec app ./manage.py migrate --fake registrar 0035_contenttypes_permissions -# step 2: docker-compose exec app ./manage.py migrate registrar 0036_create_groups -# step 3: fake run the latest migration in the migrations list -# [RECOMMENDED] -# Alternatively: -# step 1: duplicate the migration that loads data -# step 2: docker-compose exec app ./manage.py migrate - -from django.db import migrations -from registrar.models import UserGroup -from typing import Any - - -# For linting: RunPython expects a function reference, -# so let's give it one -def create_groups(apps, schema_editor) -> Any: - UserGroup.create_cisa_analyst_group(apps, schema_editor) - UserGroup.create_full_access_group(apps, schema_editor) - - -class Migration(migrations.Migration): - dependencies = [ - ("registrar", "0040_alter_userdomainrole_role"), - ] - - operations = [ - migrations.RunPython( - create_groups, - reverse_code=migrations.RunPython.noop, - atomic=True, - ), - ] From 797d896e0a2362f20c221e10dd45e9e041c42ffd Mon Sep 17 00:00:00 2001 From: Rebecca Hsieh Date: Fri, 20 Oct 2023 14:51:48 -0700 Subject: [PATCH 097/120] Now updating correct migration after new main additions --- .../migrations/0042_create_groups_v03.py | 37 +++++++++++++++++++ 1 file changed, 37 insertions(+) create mode 100644 src/registrar/migrations/0042_create_groups_v03.py diff --git a/src/registrar/migrations/0042_create_groups_v03.py b/src/registrar/migrations/0042_create_groups_v03.py new file mode 100644 index 000000000..e7039294b --- /dev/null +++ b/src/registrar/migrations/0042_create_groups_v03.py @@ -0,0 +1,37 @@ +# This migration creates the create_full_access_group and create_cisa_analyst_group groups +# It is dependent on 0035 (which populates ContentType and Permissions) +# If permissions on the groups need changing, edit CISA_ANALYST_GROUP_PERMISSIONS +# in the user_group model then: +# [NOT RECOMMENDED] +# step 1: docker-compose exec app ./manage.py migrate --fake registrar 0035_contenttypes_permissions +# step 2: docker-compose exec app ./manage.py migrate registrar 0036_create_groups +# step 3: fake run the latest migration in the migrations list +# [RECOMMENDED] +# Alternatively: +# step 1: duplicate the migration that loads data +# step 2: docker-compose exec app ./manage.py migrate + +from django.db import migrations +from registrar.models import UserGroup +from typing import Any + + +# For linting: RunPython expects a function reference, +# so let's give it one +def create_groups(apps, schema_editor) -> Any: + UserGroup.create_cisa_analyst_group(apps, schema_editor) + UserGroup.create_full_access_group(apps, schema_editor) + + +class Migration(migrations.Migration): + dependencies = [ + ("registrar", "0041_alter_domainapplication_organization_type_and_more"), + ] + + operations = [ + migrations.RunPython( + create_groups, + reverse_code=migrations.RunPython.noop, + atomic=True, + ), + ] \ No newline at end of file From a25a38f94832036d8189d693b53b120032ebf7af Mon Sep 17 00:00:00 2001 From: Rebecca Hsieh Date: Fri, 20 Oct 2023 14:57:04 -0700 Subject: [PATCH 098/120] Linter wins as always --- src/registrar/migrations/0042_create_groups_v03.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/registrar/migrations/0042_create_groups_v03.py b/src/registrar/migrations/0042_create_groups_v03.py index e7039294b..01b7985bf 100644 --- a/src/registrar/migrations/0042_create_groups_v03.py +++ b/src/registrar/migrations/0042_create_groups_v03.py @@ -34,4 +34,4 @@ class Migration(migrations.Migration): reverse_code=migrations.RunPython.noop, atomic=True, ), - ] \ No newline at end of file + ] From 0827ea77eaf8bae5301d92c7a14b4a6519ce6fb9 Mon Sep 17 00:00:00 2001 From: zandercymatics <141044360+zandercymatics@users.noreply.github.com> Date: Mon, 23 Oct 2023 07:35:36 -0600 Subject: [PATCH 099/120] Remove test commit --- src/registrar/tests/test_models_domain.py | 1 - 1 file changed, 1 deletion(-) diff --git a/src/registrar/tests/test_models_domain.py b/src/registrar/tests/test_models_domain.py index f0522b36d..3024aeaba 100644 --- a/src/registrar/tests/test_models_domain.py +++ b/src/registrar/tests/test_models_domain.py @@ -873,7 +873,6 @@ class TestRegistrantContacts(MockEppLib): contact_id="regContact", contact_type=PublicContact.ContactTypeChoices.REGISTRANT, ) - # test commit - will remove self.assertEqual( self.domain_contact.registrant_contact.email, expected_contact.email ) From 2cd02240085300a861ef2e117a0c69b4587719c1 Mon Sep 17 00:00:00 2001 From: zandercymatics <141044360+zandercymatics@users.noreply.github.com> Date: Mon, 23 Oct 2023 07:38:37 -0600 Subject: [PATCH 100/120] Remove old comment --- src/registrar/config/settings.py | 6 ------ 1 file changed, 6 deletions(-) diff --git a/src/registrar/config/settings.py b/src/registrar/config/settings.py index 2e88154ba..5506bbcaf 100644 --- a/src/registrar/config/settings.py +++ b/src/registrar/config/settings.py @@ -534,12 +534,6 @@ SECRET_REGISTRY_KEY = secret_registry_key SECRET_REGISTRY_KEY_PASSPHRASE = secret_registry_key_passphrase SECRET_REGISTRY_HOSTNAME = secret_registry_hostname -# Question for reviewers: For one client, the performance difference -# between a pool of size 1 vs a pool of size 10 isn't noticeable. -# The main performance increase comes from an open connection. -# We would need to do load testing to determine the ideal number, -# my recommendation now would be 3 as it is a good balance between -# overhead vs capacity. # 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! EPP_CONNECTION_POOL_SIZE = 1 From fb5cc4e5c670235c3f94d9702b6cafaac20e57d1 Mon Sep 17 00:00:00 2001 From: Neil Martinsen-Burrell Date: Mon, 23 Oct 2023 10:32:40 -0500 Subject: [PATCH 101/120] Allow connections from manage.get.gov --- src/registrar/config/settings.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/registrar/config/settings.py b/src/registrar/config/settings.py index 7b96af5ee..59f00fe61 100644 --- a/src/registrar/config/settings.py +++ b/src/registrar/config/settings.py @@ -581,7 +581,7 @@ ALLOWED_HOSTS = [ "getgov-bl.app.cloud.gov", "getgov-rjm.app.cloud.gov", "getgov-dk.app.cloud.gov", - "get.gov", + "manage.get.gov", ] # Extend ALLOWED_HOSTS. From 5319f58191bb0c9a0bdd97f5e2cbe8ff395ffeeb Mon Sep 17 00:00:00 2001 From: Neil Martinsen-Burrell Date: Mon, 23 Oct 2023 10:57:58 -0500 Subject: [PATCH 102/120] Use manage.get.gov as the BASE_URL --- ops/manifests/manifest-stable.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/ops/manifests/manifest-stable.yaml b/ops/manifests/manifest-stable.yaml index bc5e933f6..6295fa63b 100644 --- a/ops/manifests/manifest-stable.yaml +++ b/ops/manifests/manifest-stable.yaml @@ -18,7 +18,7 @@ applications: # Tell Django where to find its configuration DJANGO_SETTINGS_MODULE: registrar.config.settings # Tell Django where it is being hosted - DJANGO_BASE_URL: https://getgov-stable.app.cloud.gov + DJANGO_BASE_URL: https://manage.get.gov # Tell Django how much stuff to log DJANGO_LOG_LEVEL: INFO # default public site location From 8bf2432b15c3e7a960077ed1869ddcecb21e7fce Mon Sep 17 00:00:00 2001 From: Rachid Mrad Date: Mon, 23 Oct 2023 16:43:45 -0400 Subject: [PATCH 103/120] remove the dnssec confirmed session --- src/registrar/templates/domain_dsdata.html | 6 +++--- src/registrar/views/domain.py | 17 +++-------------- 2 files changed, 6 insertions(+), 17 deletions(-) diff --git a/src/registrar/templates/domain_dsdata.html b/src/registrar/templates/domain_dsdata.html index b387d34b6..45b72a9a2 100644 --- a/src/registrar/templates/domain_dsdata.html +++ b/src/registrar/templates/domain_dsdata.html @@ -22,7 +22,7 @@

Enter the values given by your DNS provider for DS Data.

- {% if not dnssec_ds_confirmed %} + {% comment %} {% if not dnssec_ds_confirmed %}

Required fields are marked with an asterisk (Add new record - {% else %} + {% else %} {% endcomment %} {% include "includes/required_fields.html" %}

@@ -111,7 +111,7 @@ >Cancel
- {% endif %} + {% comment %} {% endif %} {% endcomment %} {% if trigger_modal %} Date: Mon, 23 Oct 2023 18:15:48 -0400 Subject: [PATCH 104/120] working wip --- src/registrar/templates/domain_dsdata.html | 6 ++-- src/registrar/templates/includes/modal.html | 2 +- src/registrar/views/domain.py | 35 ++++++++++++++++++--- 3 files changed, 35 insertions(+), 8 deletions(-) diff --git a/src/registrar/templates/domain_dsdata.html b/src/registrar/templates/domain_dsdata.html index 45b72a9a2..afe230376 100644 --- a/src/registrar/templates/domain_dsdata.html +++ b/src/registrar/templates/domain_dsdata.html @@ -100,12 +100,12 @@ class="usa-button" >Save - + ' + ) + + # context to back out of a broken form on all fields delete + context["modal_button"] = modal_button return self.render_to_response(context) - if formset.is_valid(): + if formset.is_valid() or override: return self.form_valid(formset) else: return self.form_invalid(formset) @@ -363,6 +387,7 @@ class DomainDsDataView(DomainPermissionView, FormMixin): def form_valid(self, formset, **kwargs): """The formset is valid, perform something with it.""" + logger.info("form_valid is called") # Set the dnssecdata from the formset dnssecdata = extensions.DNSSECExtension() @@ -386,7 +411,9 @@ class DomainDsDataView(DomainPermissionView, FormMixin): pass domain = self.get_object() try: + logger.debug("attempting to set dnssecdata") domain.dnssecdata = dnssecdata + logger.debug("successfully set the dnssecdata") except RegistryError as err: errmsg = "Error updating DNSSEC data in the registry." logger.error(errmsg) From d50628f14d0fde24fd90bfc865078dec59d3d513 Mon Sep 17 00:00:00 2001 From: David Kennedy Date: Mon, 23 Oct 2023 18:32:22 -0400 Subject: [PATCH 105/120] cleaned up working code for review --- src/registrar/templates/domain_dsdata.html | 139 +++++++++------------ src/registrar/views/domain.py | 33 +---- 2 files changed, 67 insertions(+), 105 deletions(-) diff --git a/src/registrar/templates/domain_dsdata.html b/src/registrar/templates/domain_dsdata.html index afe230376..832361f6e 100644 --- a/src/registrar/templates/domain_dsdata.html +++ b/src/registrar/templates/domain_dsdata.html @@ -22,96 +22,79 @@

Enter the values given by your DNS provider for DS Data.

- {% comment %} {% if not dnssec_ds_confirmed %} -

Required fields are marked with an asterisk (*).

- - {% csrf_token %} - - - {% else %} {% endcomment %} - {% include "includes/required_fields.html" %} + {% include "includes/required_fields.html" %} -
- {% csrf_token %} - {{ formset.management_form }} + + {% csrf_token %} + {{ formset.management_form }} - {% for form in formset %} -
+ {% for form in formset %} +
- DS Data record {{forloop.counter}} + DS Data record {{forloop.counter}} -

DS Data record {{forloop.counter}}

+

DS Data record {{forloop.counter}}

-
-
- {% with attr_required=True add_group_class="usa-form-group--unstyled-error" %} - {% input_with_errors form.key_tag %} - {% endwith %} -
-
- {% with attr_required=True add_group_class="usa-form-group--unstyled-error" %} - {% input_with_errors form.algorithm %} - {% endwith %} -
-
- {% with attr_required=True add_group_class="usa-form-group--unstyled-error" %} - {% input_with_errors form.digest_type %} - {% endwith %} -
+
+
+ {% with attr_required=True add_group_class="usa-form-group--unstyled-error" %} + {% input_with_errors form.key_tag %} + {% endwith %}
- -
-
- {% with attr_required=True add_group_class="usa-form-group--unstyled-error" %} - {% input_with_errors form.digest %} - {% endwith %} -
+
+ {% with attr_required=True add_group_class="usa-form-group--unstyled-error" %} + {% input_with_errors form.algorithm %} + {% endwith %}
- -
-
- -
+
+ {% with attr_required=True add_group_class="usa-form-group--unstyled-error" %} + {% input_with_errors form.digest_type %} + {% endwith %}
+
-
- {% endfor %} +
+
+ {% with attr_required=True add_group_class="usa-form-group--unstyled-error" %} + {% input_with_errors form.digest %} + {% endwith %} +
+
- +
+
+ +
+
- - - - - {% comment %} {% endif %} {% endcomment %} + + + + + + {% if trigger_modal %}
Disable DNSSEC' - ) - - # context to back out of a broken form on all fields delete - context["modal_button"] = modal_button return context @@ -342,30 +332,22 @@ class DomainDsDataView(DomainPermissionView, FormMixin): self.object = self.get_object() formset = self.get_form() override = False - - # log the context and the formset - logger.info("==================FORMSET================") - logger.info(formset) - logger.info("=======>>>>>>CONTEXT<<<<<<===========") - logger.info(self.get_context_data(**kwargs)) - logger.info("========^^^^^^^CONTEXT^^^^^^^^========") - # This is called by the form cancel button, and also by the modal's X and cancel buttons if "btn-cancel-click" in request.POST: - logger.info(">>>>>>>clicked cancel") url = self.get_success_url() return HttpResponseRedirect(url) - #return reverse("domain-dns-dnssec-dsdata", kwargs={"pk": self.object.pk}) - # return redirect("domain-dns-dnssec-dsdata", kwargs={"pk": self.object.pk}) + # This is called by the Disable DNSSEC modal to override if "disable-override-click" in request.POST: - logger.info(">>>>>>>>clicked disable") override = True - if len(formset) == 0 and formset.initial != [{}] and override == False: + # This is called when all DNSSEC data has been deleted and the + # Save button is pressed + if len(formset) == 0 and formset.initial == [{}] and override == False: # trigger the modal - logger.info(">>>>>>>>clicked save") + # get context data from super() rather than self + # to preserve the context["form"] context = super().get_context_data(form=formset) context["trigger_modal"] = True # Create HTML for the modal button @@ -387,7 +369,6 @@ class DomainDsDataView(DomainPermissionView, FormMixin): def form_valid(self, formset, **kwargs): """The formset is valid, perform something with it.""" - logger.info("form_valid is called") # Set the dnssecdata from the formset dnssecdata = extensions.DNSSECExtension() @@ -411,9 +392,7 @@ class DomainDsDataView(DomainPermissionView, FormMixin): pass domain = self.get_object() try: - logger.debug("attempting to set dnssecdata") domain.dnssecdata = dnssecdata - logger.debug("successfully set the dnssecdata") except RegistryError as err: errmsg = "Error updating DNSSEC data in the registry." logger.error(errmsg) From 6400fa5f4b4e68bda803db44bc8c6c58c6a2334f Mon Sep 17 00:00:00 2001 From: David Kennedy Date: Mon, 23 Oct 2023 20:58:09 -0400 Subject: [PATCH 106/120] fixed logic on save with no dnssec data records; fixed link on dns page --- src/registrar/templates/domain_dns.html | 2 +- src/registrar/views/domain.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/src/registrar/templates/domain_dns.html b/src/registrar/templates/domain_dns.html index b16c1cb8b..45529a19e 100644 --- a/src/registrar/templates/domain_dns.html +++ b/src/registrar/templates/domain_dns.html @@ -14,7 +14,7 @@ {% url 'domain-dns-nameservers' pk=domain.id as url %}

DNS name servers

- {% url 'domain-dnssec' pk=domain.id as url %} + {% url 'domain-dns-dnssec' pk=domain.id as url %}

DNSSEC

{% endblock %} {# domain_content #} diff --git a/src/registrar/views/domain.py b/src/registrar/views/domain.py index 1058c537a..90e915a60 100644 --- a/src/registrar/views/domain.py +++ b/src/registrar/views/domain.py @@ -382,7 +382,7 @@ class DomainDsDataView(DomainFormBaseView): # This is called when all DNSSEC data has been deleted and the # Save button is pressed - if len(formset) == 0 and formset.initial == [{}] and override == False: + if len(formset) == 0 and formset.initial != [{}] and override == False: # trigger the modal # get context data from super() rather than self # to preserve the context["form"] From d35475148115e8ae653a3952f540b856f60ed1f7 Mon Sep 17 00:00:00 2001 From: David Kennedy Date: Mon, 23 Oct 2023 21:16:12 -0400 Subject: [PATCH 107/120] fixed tests and updated code formatting --- src/registrar/tests/test_views.py | 2 +- src/registrar/views/domain.py | 11 +++++------ 2 files changed, 6 insertions(+), 7 deletions(-) diff --git a/src/registrar/tests/test_views.py b/src/registrar/tests/test_views.py index 0fba81fc1..6614cf8e8 100644 --- a/src/registrar/tests/test_views.py +++ b/src/registrar/tests/test_views.py @@ -1125,7 +1125,7 @@ class TestWithDomainPermissions(TestWithUser): UserDomainRole.objects.get_or_create( user=self.user, domain=self.domain_multdsdata, - role=UserDomainRole.Roles.ADMIN, + role=UserDomainRole.Roles.MANAGER, ) UserDomainRole.objects.get_or_create( user=self.user, diff --git a/src/registrar/views/domain.py b/src/registrar/views/domain.py index 90e915a60..2dcefae39 100644 --- a/src/registrar/views/domain.py +++ b/src/registrar/views/domain.py @@ -12,10 +12,8 @@ from django.contrib.messages.views import SuccessMessageMixin from django.db import IntegrityError from django.http import HttpResponseRedirect from django.shortcuts import redirect -from django.template import RequestContext from django.urls import reverse from django.views.generic.edit import FormMixin -from django.shortcuts import render from registrar.models import ( Domain, @@ -371,18 +369,19 @@ class DomainDsDataView(DomainFormBaseView): formset = self.get_form() override = False - # This is called by the form cancel button, and also by the modal's X and cancel buttons + # This is called by the form cancel button, + # and also by the modal's X and cancel buttons if "btn-cancel-click" in request.POST: url = self.get_success_url() return HttpResponseRedirect(url) - + # This is called by the Disable DNSSEC modal to override if "disable-override-click" in request.POST: override = True - + # This is called when all DNSSEC data has been deleted and the # Save button is pressed - if len(formset) == 0 and formset.initial != [{}] and override == False: + if len(formset) == 0 and formset.initial != [{}] and override is False: # trigger the modal # get context data from super() rather than self # to preserve the context["form"] From 65b2053a4fc2f1917d0301d9c5aa707411414034 Mon Sep 17 00:00:00 2001 From: dave-kennedy-ecs <111779554+dave-kennedy-ecs@users.noreply.github.com> Date: Mon, 23 Oct 2023 21:18:37 -0400 Subject: [PATCH 108/120] Update src/registrar/templates/domain_sidebar.html updated logic in domain sidebar for code readability Co-authored-by: zandercymatics <141044360+zandercymatics@users.noreply.github.com> --- src/registrar/templates/domain_sidebar.html | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/registrar/templates/domain_sidebar.html b/src/registrar/templates/domain_sidebar.html index 65c5254e9..c365638c1 100644 --- a/src/registrar/templates/domain_sidebar.html +++ b/src/registrar/templates/domain_sidebar.html @@ -34,7 +34,7 @@ > DNSSEC - {% if domain.dnssecdata is not None or request.path|startswith:url and request.path|endswith:'dsdata' %} + {% if domain.dnssecdata is not None or (request.path|startswith:url and request.path|endswith:'dsdata') %}
  • {% url 'domain-dns-dnssec-dsdata' pk=domain.id as url %} From ce6fc5a9ac2d98e735d85b5d15f95de959b11f4e Mon Sep 17 00:00:00 2001 From: David Kennedy Date: Mon, 23 Oct 2023 21:29:26 -0400 Subject: [PATCH 109/120] fixed problem with sidebar --- src/registrar/templates/domain_sidebar.html | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/registrar/templates/domain_sidebar.html b/src/registrar/templates/domain_sidebar.html index c365638c1..65c5254e9 100644 --- a/src/registrar/templates/domain_sidebar.html +++ b/src/registrar/templates/domain_sidebar.html @@ -34,7 +34,7 @@ > DNSSEC - {% if domain.dnssecdata is not None or (request.path|startswith:url and request.path|endswith:'dsdata') %} + {% if domain.dnssecdata is not None or request.path|startswith:url and request.path|endswith:'dsdata' %}
    • {% url 'domain-dns-dnssec-dsdata' pk=domain.id as url %} From 2f40d52254a53e4ea1cc70fe23f19ae73e222c1a Mon Sep 17 00:00:00 2001 From: Rachid Mrad Date: Tue, 24 Oct 2023 12:03:40 -0400 Subject: [PATCH 110/120] edit modal text --- src/registrar/templates/domain_dsdata.html | 2 +- src/registrar/views/domain.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/src/registrar/templates/domain_dsdata.html b/src/registrar/templates/domain_dsdata.html index 832361f6e..f961250a1 100644 --- a/src/registrar/templates/domain_dsdata.html +++ b/src/registrar/templates/domain_dsdata.html @@ -114,7 +114,7 @@ aria-describedby="Your DNSSEC records will be deleted from the registry." data-force-action > - {% include 'includes/modal.html' with cancel_button_resets_ds_form=True modal_heading="Are you sure you want to continue?" modal_description="some good text" modal_button=modal_button|safe %} + {% include 'includes/modal.html' with cancel_button_resets_ds_form=True modal_heading="Caution: Deleting all DS records will disable DNSSEC on your domain" modal_description="To avoid causing your domain to appear offline you should wait to delete your DS records at your DNS host until the Time to Live (TTL) expires. This is often less than 24 hours, but confirm with your provider." modal_button=modal_button|safe %} {% endblock %} {# domain_content #} diff --git a/src/registrar/views/domain.py b/src/registrar/views/domain.py index 2dcefae39..3f6e6df72 100644 --- a/src/registrar/views/domain.py +++ b/src/registrar/views/domain.py @@ -391,7 +391,7 @@ class DomainDsDataView(DomainFormBaseView): modal_button = ( '' + 'name="disable-override-click">Delete all records' ) # context to back out of a broken form on all fields delete From 548735cb078cd7af144ba1c9a73b20eb6b2bd758 Mon Sep 17 00:00:00 2001 From: Rachid Mrad Date: Tue, 24 Oct 2023 12:06:30 -0400 Subject: [PATCH 111/120] edit modal text again --- src/registrar/templates/domain_dsdata.html | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/registrar/templates/domain_dsdata.html b/src/registrar/templates/domain_dsdata.html index f961250a1..bdf4deb46 100644 --- a/src/registrar/templates/domain_dsdata.html +++ b/src/registrar/templates/domain_dsdata.html @@ -114,7 +114,7 @@ aria-describedby="Your DNSSEC records will be deleted from the registry." data-force-action > - {% include 'includes/modal.html' with cancel_button_resets_ds_form=True modal_heading="Caution: Deleting all DS records will disable DNSSEC on your domain" modal_description="To avoid causing your domain to appear offline you should wait to delete your DS records at your DNS host until the Time to Live (TTL) expires. This is often less than 24 hours, but confirm with your provider." modal_button=modal_button|safe %} + {% include 'includes/modal.html' with cancel_button_resets_ds_form=True modal_heading="Warning: You are about to delete all DS records on your domain" modal_description="To fully disable DNSSEC: In addition to deleting your DS records here you’ll also need to delete the DS records at your DNS host. To avoid causing your domain to appear offline you should wait to delete your DS records at your DNS host until the Time to Live (TTL) expires. This is often less than 24 hours, but confirm with your provider." modal_button=modal_button|safe %} {% endblock %} {# domain_content #} From 15f656b77a751ea942a9d17044f3c417a9a12a99 Mon Sep 17 00:00:00 2001 From: Rachid Mrad Date: Tue, 24 Oct 2023 12:19:48 -0400 Subject: [PATCH 112/120] edit modal text on dnssec page --- src/registrar/templates/domain_dnssec.html | 2 +- src/registrar/views/domain.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/src/registrar/templates/domain_dnssec.html b/src/registrar/templates/domain_dnssec.html index 8e19e8360..691ba79b2 100644 --- a/src/registrar/templates/domain_dnssec.html +++ b/src/registrar/templates/domain_dnssec.html @@ -56,7 +56,7 @@ aria-labelledby="Are you sure you want to continue?" aria-describedby="Your DNSSEC records will be deleted from the registry." > - {% include 'includes/modal.html' with modal_heading="Are you sure you want to continue?" modal_button=modal_button|safe %} + {% include 'includes/modal.html' with modal_heading="Are you sure you want to disable DNSSEC?" modal_button=modal_button|safe %} {% endblock %} {# domain_content #} diff --git a/src/registrar/views/domain.py b/src/registrar/views/domain.py index 3f6e6df72..b51b931dc 100644 --- a/src/registrar/views/domain.py +++ b/src/registrar/views/domain.py @@ -292,7 +292,7 @@ class DomainDNSSECView(DomainFormBaseView): modal_button = ( '' + 'name="disable_dnssec">Confirm' ) context["modal_button"] = modal_button From 736fe46ec3afc6a0dab176cfebf5fee7776f0e2c Mon Sep 17 00:00:00 2001 From: zandercymatics <141044360+zandercymatics@users.noreply.github.com> Date: Tue, 24 Oct 2023 13:12:36 -0600 Subject: [PATCH 113/120] Update domain.py --- src/registrar/models/domain.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/registrar/models/domain.py b/src/registrar/models/domain.py index 2bfdd58c5..457f3305c 100644 --- a/src/registrar/models/domain.py +++ b/src/registrar/models/domain.py @@ -59,7 +59,7 @@ class Domain(TimeStampedModel, DomainHelper): G) Activation is controlled by the registry. It will happen automatically when the domain meets the required checks. """ - + # test comment for pushing to sandbox - will remove def __init__(self, *args, **kwargs): self._cache = {} super(Domain, self).__init__(*args, **kwargs) From e78e95df0dc80de1f6d241f1fc53ad5ad3a095af Mon Sep 17 00:00:00 2001 From: Rachid Mrad Date: Tue, 24 Oct 2023 16:29:54 -0400 Subject: [PATCH 114/120] fix outline btn style (remove underline) when anchor emlement) --- src/registrar/assets/sass/_theme/_buttons.scss | 17 ++++------------- 1 file changed, 4 insertions(+), 13 deletions(-) diff --git a/src/registrar/assets/sass/_theme/_buttons.scss b/src/registrar/assets/sass/_theme/_buttons.scss index 0857ec603..cb2117fb9 100644 --- a/src/registrar/assets/sass/_theme/_buttons.scss +++ b/src/registrar/assets/sass/_theme/_buttons.scss @@ -22,8 +22,11 @@ a.breadcrumb__back { } } -a.usa-button:not(.usa-button--unstyled, .usa-button--outline) { +a.usa-button { text-decoration: none; +} + +a.usa-button:not(.usa-button--unstyled, .usa-button--outline) { color: color('white'); } @@ -111,15 +114,3 @@ a.usa-button--unstyled:visited { margin-left: units(2); } } - - -// WARNING: crazy hack ahead: -// Cancel button(s) on the DNSSEC form pages -// We want to position the cancel button on the -// dnssec forms next to the submit button -// This button's markup is in its own form -.btn-cancel { - position: relative; - top: -39.2px; - left: 88px; -} From 76be3c6b7296ecb53024d267b52949e4903f449c Mon Sep 17 00:00:00 2001 From: Rachid Mrad Date: Tue, 24 Oct 2023 17:36:27 -0400 Subject: [PATCH 115/120] test_ds_data_form_modal --- src/registrar/assets/js/get-gov.js | 33 +++++++++++++++--------------- src/registrar/tests/test_views.py | 16 +++++++++++++++ 2 files changed, 32 insertions(+), 17 deletions(-) diff --git a/src/registrar/assets/js/get-gov.js b/src/registrar/assets/js/get-gov.js index 1813deea0..851de8fcf 100644 --- a/src/registrar/assets/js/get-gov.js +++ b/src/registrar/assets/js/get-gov.js @@ -390,24 +390,23 @@ function prepareDeleteButtons() { */ (function triggerModalOnDsDataForm() { let saveButon = document.querySelector("#save-ds-data"); - - // if (saveButon) - // saveButon.addEventListener('click', triggerModalIfModalTriggerExists); - // function triggerModalIfModalTriggerExists(e){ + // The view context will cause a hitherto hidden modal trigger to + // show up. On save, we'll test for that modal trigger appearing. We'll + // run that test once every 100 ms for 5 secs, which should balance performance + // while accounting for network or lag issues. if (saveButon) { - let i = 0; - var tryToTriggerModal = setInterval(function() { - i++; - if (i > 100) { - clearInterval(tryToTriggerModal); - } - let modalTrigger = document.querySelector("#ds-toggle-dnssec-alert"); - if (modalTrigger) { - modalTrigger.click() - clearInterval(tryToTriggerModal); - } - }, 50); - + let i = 0; + var tryToTriggerModal = setInterval(function() { + i++; + if (i > 100) { + clearInterval(tryToTriggerModal); + } + let modalTrigger = document.querySelector("#ds-toggle-dnssec-alert"); + if (modalTrigger) { + modalTrigger.click() + clearInterval(tryToTriggerModal); + } + }, 50); } })(); diff --git a/src/registrar/tests/test_views.py b/src/registrar/tests/test_views.py index 6614cf8e8..6bd59facb 100644 --- a/src/registrar/tests/test_views.py +++ b/src/registrar/tests/test_views.py @@ -1701,6 +1701,22 @@ class TestDomainDNSSEC(TestDomainOverview): ) self.assertContains(page, "DS Data record 1") + def test_ds_data_form_modal(self): + """When user clicks on save, a modal pops up.""" + add_data_page = self.app.get( + reverse("domain-dns-dnssec-dsdata", kwargs={"pk": self.domain_dsdata.id}) + ) + self.assertNotContains(add_data_page, "Trigger Disable DNSSEC Modal") + # Simulate a delete all data + form_data = {} + response = self.client.post( + reverse("domain-dns-dnssec-dsdata", kwargs={"pk": self.domain_dsdata.id}), + data=form_data, + ) + self.assertEqual(response.status_code, 200) # Adjust status code as needed + # Now check to see whether the JS trigger for the modal is present on the page + self.assertContains(response, "Trigger Disable DNSSEC Modal") + def test_ds_data_form_submits(self): """DS Data form submits successfully From a87519e61524d64533bdf34a2ea6bd9e204cc0f3 Mon Sep 17 00:00:00 2001 From: zandercymatics <141044360+zandercymatics@users.noreply.github.com> Date: Wed, 25 Oct 2023 08:29:39 -0600 Subject: [PATCH 116/120] Cleanup, add comment --- src/registrar/config/settings.py | 1 + src/registrar/models/domain.py | 2 +- src/registrar/tests/test_models_domain.py | 1 + 3 files changed, 3 insertions(+), 1 deletion(-) diff --git a/src/registrar/config/settings.py b/src/registrar/config/settings.py index 5506bbcaf..e4c4ae1f8 100644 --- a/src/registrar/config/settings.py +++ b/src/registrar/config/settings.py @@ -540,6 +540,7 @@ 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 +# WARNING: Setting this value too high could cause frequent app crashes! POOL_KEEP_ALIVE = 60 # Determines how long we try to keep a pool alive for, diff --git a/src/registrar/models/domain.py b/src/registrar/models/domain.py index 457f3305c..2bfdd58c5 100644 --- a/src/registrar/models/domain.py +++ b/src/registrar/models/domain.py @@ -59,7 +59,7 @@ class Domain(TimeStampedModel, DomainHelper): G) Activation is controlled by the registry. It will happen automatically when the domain meets the required checks. """ - # test comment for pushing to sandbox - will remove + def __init__(self, *args, **kwargs): self._cache = {} super(Domain, self).__init__(*args, **kwargs) diff --git a/src/registrar/tests/test_models_domain.py b/src/registrar/tests/test_models_domain.py index 3024aeaba..ef3084f9c 100644 --- a/src/registrar/tests/test_models_domain.py +++ b/src/registrar/tests/test_models_domain.py @@ -873,6 +873,7 @@ class TestRegistrantContacts(MockEppLib): contact_id="regContact", contact_type=PublicContact.ContactTypeChoices.REGISTRANT, ) + self.assertEqual( self.domain_contact.registrant_contact.email, expected_contact.email ) From 7436a8a4632c605f7c972a8161fbc208a1bc1dcf Mon Sep 17 00:00:00 2001 From: Rachid Mrad Date: Wed, 25 Oct 2023 11:22:04 -0400 Subject: [PATCH 117/120] add coment on test_ds_data_form_modal --- src/registrar/tests/test_views.py | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/src/registrar/tests/test_views.py b/src/registrar/tests/test_views.py index 6bd59facb..bff64eb33 100644 --- a/src/registrar/tests/test_views.py +++ b/src/registrar/tests/test_views.py @@ -1706,6 +1706,10 @@ class TestDomainDNSSEC(TestDomainOverview): add_data_page = self.app.get( reverse("domain-dns-dnssec-dsdata", kwargs={"pk": self.domain_dsdata.id}) ) + # Assert that a hidden trigger for the modal does not exist. + # This hidden trigger will pop on the page when certain condition are met: + # 1) Initial form contained DS data, 2) All data is deleted and form is + # submitted. self.assertNotContains(add_data_page, "Trigger Disable DNSSEC Modal") # Simulate a delete all data form_data = {} From 91c68f91f1768c60e1cfc3bf021cfff56d11f5b5 Mon Sep 17 00:00:00 2001 From: zandercymatics <141044360+zandercymatics@users.noreply.github.com> Date: Wed, 25 Oct 2023 11:35:41 -0600 Subject: [PATCH 118/120] Add PR suggestions --- src/epplibwrapper/client.py | 12 +++++++----- src/epplibwrapper/socket.py | 8 +++++--- src/epplibwrapper/tests/test_pool.py | 1 + src/epplibwrapper/utility/pool.py | 7 ++++--- src/epplibwrapper/utility/pool_error.py | 16 ++++++++++++---- src/epplibwrapper/utility/pool_status.py | 7 ++++++- src/registrar/config/settings.py | 3 ++- 7 files changed, 37 insertions(+), 17 deletions(-) diff --git a/src/epplibwrapper/client.py b/src/epplibwrapper/client.py index b6359d494..e4b7a5d53 100644 --- a/src/epplibwrapper/client.py +++ b/src/epplibwrapper/client.py @@ -111,21 +111,21 @@ class EPPLibWrapper: self.start_connection_pool() except (ValueError, ParsingError) as err: message = f"{cmd_type} failed to execute due to some syntax error." - logger.error(message, exc_info=True) + logger.error(f"{message} Error: {err}", exc_info=True) raise RegistryError(message) from err except TransportError as err: message = f"{cmd_type} failed to execute due to a connection error." - logger.error(message, exc_info=True) + logger.error(f"{message} Error: {err}", exc_info=True) raise RegistryError(message) from err except LoginError as err: - # For linter + # For linter due to it not liking this line length text = "failed to execute due to a registry login error." message = f"{cmd_type} {text}" - logger.error(message, exc_info=True) + logger.error(f"{message} Error: {err}", exc_info=True) raise RegistryError(message) from err except Exception as err: message = f"{cmd_type} failed to execute due to an unknown error." - logger.error(message, exc_info=True) + logger.error(f"{message} Error: {err}", exc_info=True) raise RegistryError(message) from err else: if response.code >= 2000: @@ -155,6 +155,8 @@ class EPPLibWrapper: 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 diff --git a/src/epplibwrapper/socket.py b/src/epplibwrapper/socket.py index c44d07910..6040f6682 100644 --- a/src/epplibwrapper/socket.py +++ b/src/epplibwrapper/socket.py @@ -48,7 +48,7 @@ class Socket: def send(self, command): """Sends a command to the registry. - If the response code is >= 2000, + If the RegistryError code is >= 2000, then this function raises a LoginError. The calling function should handle this.""" response = self.client.send(command) @@ -59,7 +59,9 @@ class Socket: return response def is_login_error(self, code): - """Returns the result of code >= 2000""" + """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): @@ -90,7 +92,7 @@ class Socket: # If we encounter a login error, fail if self.is_login_error(response.code): - logger.warning("was login error") + logger.warning("A login error was found in test_connection_success") return False # Otherwise, just return true diff --git a/src/epplibwrapper/tests/test_pool.py b/src/epplibwrapper/tests/test_pool.py index 3a431ef1e..4e919ba76 100644 --- a/src/epplibwrapper/tests/test_pool.py +++ b/src/epplibwrapper/tests/test_pool.py @@ -28,6 +28,7 @@ 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, diff --git a/src/epplibwrapper/utility/pool.py b/src/epplibwrapper/utility/pool.py index 99d5326ab..8979c9744 100644 --- a/src/epplibwrapper/utility/pool.py +++ b/src/epplibwrapper/utility/pool.py @@ -51,7 +51,8 @@ class EPPConnectionPool(ConnectionPool): self.keepalive = options["keepalive"] # Determines the period in which new - # gevent threads are spun up + # 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"] @@ -77,7 +78,7 @@ class EPPConnectionPool(ConnectionPool): def _keepalive(self, c): """Sends a command to the server to keep the connection alive.""" try: - # Sends a ping to EPPLib + # Sends a ping to the registry via EPPLib c.send(Hello()) except Exception as err: message = "Failed to keep the connection alive." @@ -108,7 +109,7 @@ class EPPConnectionPool(ConnectionPool): logger.info("No connections to kill.") except Exception as err: logger.error("Could not kill all connections.") - raise err + raise PoolError(code=PoolErrorCodes.KILL_ALL_FAILED) from err def populate_all_connections(self): """Generates the connection pool. diff --git a/src/epplibwrapper/utility/pool_error.py b/src/epplibwrapper/utility/pool_error.py index 70312f32e..16aa0e08d 100644 --- a/src/epplibwrapper/utility/pool_error.py +++ b/src/epplibwrapper/utility/pool_error.py @@ -22,12 +22,20 @@ class PoolError(Exception): - 2000 KILL_ALL_FAILED - 2001 NEW_CONNECTION_FAILED - 2002 KEEP_ALIVE_FAILED + + Note: These are separate from the error codes returned from EppLib """ - # For linter - kill_failed = "Could not kill all connections." - conn_failed = "Failed to execute due to a registry error." - alive_failed = "Failed to keep the connection alive." + # 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, diff --git a/src/epplibwrapper/utility/pool_status.py b/src/epplibwrapper/utility/pool_status.py index 214bf8ac1..64ebbe5eb 100644 --- a/src/epplibwrapper/utility/pool_status.py +++ b/src/epplibwrapper/utility/pool_status.py @@ -1,5 +1,10 @@ class PoolStatus: - """A list of Booleans to keep track of Pool Status""" + """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 diff --git a/src/registrar/config/settings.py b/src/registrar/config/settings.py index e4c4ae1f8..385f2a1e3 100644 --- a/src/registrar/config/settings.py +++ b/src/registrar/config/settings.py @@ -536,11 +536,12 @@ 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 -# WARNING: Setting this value too high could cause frequent app crashes! POOL_KEEP_ALIVE = 60 # Determines how long we try to keep a pool alive for, From 90b12a6c48a339d95fda5f399f9ab6085ce544d9 Mon Sep 17 00:00:00 2001 From: David Kennedy Date: Wed, 25 Oct 2023 21:35:17 -0400 Subject: [PATCH 119/120] generic error handling for RegistryErrors as in other views --- src/registrar/views/domain.py | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/src/registrar/views/domain.py b/src/registrar/views/domain.py index b51b931dc..a2f2c1198 100644 --- a/src/registrar/views/domain.py +++ b/src/registrar/views/domain.py @@ -430,10 +430,12 @@ class DomainDsDataView(DomainFormBaseView): try: self.object.dnssecdata = dnssecdata except RegistryError as err: - errmsg = "Error updating DNSSEC data in the registry." - logger.error(errmsg) - logger.error(err) - messages.error(self.request, errmsg) + if err.is_connection_error(): + messages.error(self.request, CANNOT_CONTACT_REGISTRY) + logger.error(f"Registry connection error: {err}") + else: + messages.error(self.request, GENERIC_ERROR) + logger.error(f"Registry error: {err}") return self.form_invalid(formset) else: messages.success( From 745f2bbdf9efaf0777a425ae2ff148cdf5f1c5f6 Mon Sep 17 00:00:00 2001 From: zandercymatics <141044360+zandercymatics@users.noreply.github.com> Date: Thu, 26 Oct 2023 07:51:31 -0600 Subject: [PATCH 120/120] Linter --- src/epplibwrapper/utility/pool.py | 2 +- src/epplibwrapper/utility/pool_error.py | 8 ++++---- src/epplibwrapper/utility/pool_status.py | 2 +- 3 files changed, 6 insertions(+), 6 deletions(-) diff --git a/src/epplibwrapper/utility/pool.py b/src/epplibwrapper/utility/pool.py index 8979c9744..36771252b 100644 --- a/src/epplibwrapper/utility/pool.py +++ b/src/epplibwrapper/utility/pool.py @@ -51,7 +51,7 @@ class EPPConnectionPool(ConnectionPool): self.keepalive = options["keepalive"] # Determines the period in which new - # gevent threads are spun up. + # 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: diff --git a/src/epplibwrapper/utility/pool_error.py b/src/epplibwrapper/utility/pool_error.py index 16aa0e08d..821962774 100644 --- a/src/epplibwrapper/utility/pool_error.py +++ b/src/epplibwrapper/utility/pool_error.py @@ -22,19 +22,19 @@ class PoolError(Exception): - 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." + "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." + "Failed to keep the connection alive. " + "It is likely that the registry returned a LoginError." ) _error_mapping = { PoolErrorCodes.KILL_ALL_FAILED: kill_failed, diff --git a/src/epplibwrapper/utility/pool_status.py b/src/epplibwrapper/utility/pool_status.py index 64ebbe5eb..3a0ae750f 100644 --- a/src/epplibwrapper/utility/pool_status.py +++ b/src/epplibwrapper/utility/pool_status.py @@ -1,6 +1,6 @@ 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.