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 001/128] 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 002/128] 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 003/128] 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 004/128] 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 005/128] 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 006/128] 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 007/128] 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 008/128] 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 009/128] 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 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 010/128] 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 011/128] 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 012/128] 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 013/128] 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 014/128] 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 015/128] 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 016/128] 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 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 017/128] 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 018/128] 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 019/128] 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 020/128] 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 021/128] 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 022/128] 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 023/128] 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 024/128] 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 025/128] 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 026/128] 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 027/128] 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 028/128] 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 029/128] 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 030/128] 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 031/128] 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 032/128] 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 033/128] 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 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 034/128] 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 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 035/128] 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 16164f1f05d0db168701da60fb3765458eb6a9b6 Mon Sep 17 00:00:00 2001 From: David Kennedy Date: Wed, 18 Oct 2023 15:01:15 -0400 Subject: [PATCH 036/128] work in progress --- src/registrar/forms/domain.py | 3 ++- src/registrar/models/domain.py | 5 +++- .../templates/domain_nameservers.html | 27 ++++++++++++------- src/registrar/views/domain.py | 8 ++++-- 4 files changed, 30 insertions(+), 13 deletions(-) diff --git a/src/registrar/forms/domain.py b/src/registrar/forms/domain.py index 8abc7e14a..d14ed41ba 100644 --- a/src/registrar/forms/domain.py +++ b/src/registrar/forms/domain.py @@ -25,7 +25,8 @@ class DomainNameserverForm(forms.Form): """Form for changing nameservers.""" server = forms.CharField(label="Name server", strip=True) - # when adding IPs to this form ensure they are stripped as well + + ip = forms.CharField(label="IP address", strip=True, required=False) NameserverFormset = formset_factory( diff --git a/src/registrar/models/domain.py b/src/registrar/models/domain.py index bab993b04..80bb1ab30 100644 --- a/src/registrar/models/domain.py +++ b/src/registrar/models/domain.py @@ -322,6 +322,7 @@ class Domain(TimeStampedModel, DomainHelper): ) elif ip is not None and ip != []: for addr in ip: + logger.info(f"ip address {addr}") if not self._valid_ip_addr(addr): raise NameserverError( code=nsErrorCodes.INVALID_IP, nameserver=nameserver, ip=ip @@ -334,7 +335,9 @@ class Domain(TimeStampedModel, DomainHelper): returns: isValid (boolean)-True for valid ip address""" try: + logger.info(f"in valid_ip_addr: {ipToTest}") ip = ipaddress.ip_address(ipToTest) + logger.info(ip.version) return ip.version == 6 or ip.version == 4 except ValueError: @@ -602,7 +605,7 @@ class Domain(TimeStampedModel, DomainHelper): if len(hosts) > 13: raise NameserverError(code=nsErrorCodes.TOO_MANY_HOSTS) - if self.state not in [self.State.DNS_NEEDED, self.State.READY]: + if self.state not in [self.State.DNS_NEEDED, self.State.READY, self.State.UNKNOWN]: raise ActionNotAllowed("Nameservers can not be " "set in the current state") logger.info("Setting nameservers") diff --git a/src/registrar/templates/domain_nameservers.html b/src/registrar/templates/domain_nameservers.html index 596eec524..37fa1eb85 100644 --- a/src/registrar/templates/domain_nameservers.html +++ b/src/registrar/templates/domain_nameservers.html @@ -22,15 +22,24 @@ {% for form in formset %}
- {% with sublabel_text="Example: ns"|concat:forloop.counter|concat:".example.com" %} - {% if forloop.counter <= 2 %} - {% with attr_required=True %} - {% input_with_errors form.server %} - {% endwith %} - {% else %} - {% input_with_errors form.server %} - {% endif %} - {% endwith %} +
+
+ {% with sublabel_text="Example: ns"|concat:forloop.counter|concat:".example.com" %} + {% if forloop.counter <= 2 %} + {% with attr_required=True %} + {% input_with_errors form.server %} + {% endwith %} + {% else %} + {% input_with_errors form.server %} + {% endif %} + {% endwith %} +
+
+ {% with sublabel_text="Example: ns"|concat:forloop.counter|concat:".example.com" %} + {% input_with_errors form.ip %} + {% endwith %} +
+
{% endfor %} diff --git a/src/registrar/views/domain.py b/src/registrar/views/domain.py index 36b7a9445..dbf65fe6b 100644 --- a/src/registrar/views/domain.py +++ b/src/registrar/views/domain.py @@ -173,7 +173,7 @@ class DomainNameserversView(DomainPermissionView, FormMixin): if nameservers is not None: # Add existing nameservers as initial data - initial_data.extend({"server": name} for name, *ip in nameservers) + initial_data.extend({"server": name, "ip": ip} for name, ip in nameservers) # Ensure at least 3 fields, filled or empty while len(initial_data) < 2: @@ -198,6 +198,7 @@ class DomainNameserversView(DomainPermissionView, FormMixin): for i, form in enumerate(formset): form.fields["server"].label += f" {i+1}" + form.fields["ip"].label += f" {i+1}" if i < 2: form.fields["server"].required = True else: @@ -221,7 +222,10 @@ class DomainNameserversView(DomainPermissionView, FormMixin): nameservers = [] for form in formset: try: - as_tuple = (form.cleaned_data["server"],) + as_tuple = ( + form.cleaned_data["server"], + [form.cleaned_data["ip"]] + ) nameservers.append(as_tuple) except KeyError: # no server information in this field, skip it From 51307d3b12bd1c8c27aeb5600e768d4573135c55 Mon Sep 17 00:00:00 2001 From: Rachid Mrad Date: Wed, 18 Oct 2023 15:36:41 -0400 Subject: [PATCH 037/128] some error validation, light refactor of ip checking in model --- src/registrar/forms/domain.py | 9 ++++- src/registrar/models/domain.py | 6 +-- .../templates/domain_nameservers.html | 6 +-- src/registrar/views/domain.py | 37 ++++++++++++++++--- 4 files changed, 45 insertions(+), 13 deletions(-) diff --git a/src/registrar/forms/domain.py b/src/registrar/forms/domain.py index d14ed41ba..b93950df0 100644 --- a/src/registrar/forms/domain.py +++ b/src/registrar/forms/domain.py @@ -26,7 +26,14 @@ class DomainNameserverForm(forms.Form): server = forms.CharField(label="Name server", strip=True) - ip = forms.CharField(label="IP address", strip=True, required=False) + ip = forms.CharField( + label="IP address", + strip=True, + required=False, + validators=[ + # TODO in progress + ], + ) NameserverFormset = formset_factory( diff --git a/src/registrar/models/domain.py b/src/registrar/models/domain.py index 80bb1ab30..0b9139277 100644 --- a/src/registrar/models/domain.py +++ b/src/registrar/models/domain.py @@ -313,14 +313,14 @@ class Domain(TimeStampedModel, DomainHelper): NameserverError (if exception hit) Returns: None""" - if self.isSubdomain(nameserver) and (ip is None or ip == []): + if self.isSubdomain(nameserver) and (ip is None or ip == [] or ip != []): raise NameserverError(code=nsErrorCodes.MISSING_IP, nameserver=nameserver) - elif not self.isSubdomain(nameserver) and (ip is not None and ip != []): + elif not self.isSubdomain(nameserver) and (ip is not None and ip != [] and ip != ['']): raise NameserverError( code=nsErrorCodes.GLUE_RECORD_NOT_ALLOWED, nameserver=nameserver, ip=ip ) - elif ip is not None and ip != []: + elif ip is not None and ip != [] and ip != ['']: for addr in ip: logger.info(f"ip address {addr}") if not self._valid_ip_addr(addr): diff --git a/src/registrar/templates/domain_nameservers.html b/src/registrar/templates/domain_nameservers.html index 37fa1eb85..fffd4b8c0 100644 --- a/src/registrar/templates/domain_nameservers.html +++ b/src/registrar/templates/domain_nameservers.html @@ -16,7 +16,7 @@ {% include "includes/required_fields.html" %} -
+ {% csrf_token %} {{ formset.management_form }} @@ -26,7 +26,7 @@
{% with sublabel_text="Example: ns"|concat:forloop.counter|concat:".example.com" %} {% if forloop.counter <= 2 %} - {% with attr_required=True %} + {% with attr_required=True add_group_class="usa-form-group--unstyled-error" %} {% input_with_errors form.server %} {% endwith %} {% else %} @@ -35,7 +35,7 @@ {% endwith %}
- {% with sublabel_text="Example: ns"|concat:forloop.counter|concat:".example.com" %} + {% with sublabel_text="Example: ns"|concat:forloop.counter|concat:".example.com" add_group_class="usa-form-group--unstyled-error" %} {% input_with_errors form.ip %} {% endwith %}
diff --git a/src/registrar/views/domain.py b/src/registrar/views/domain.py index dbf65fe6b..f8e144f7f 100644 --- a/src/registrar/views/domain.py +++ b/src/registrar/views/domain.py @@ -23,6 +23,7 @@ from registrar.models import ( UserDomainRole, ) from registrar.models.public_contact import PublicContact +from registrar.utility.errors import NameserverError from ..forms import ( ContactForm, @@ -222,20 +223,44 @@ class DomainNameserversView(DomainPermissionView, FormMixin): nameservers = [] for form in formset: try: + ip_string = form.cleaned_data["ip"] + # Split the string into a list using a comma as the delimiter + ip_list = ip_string.split(',') + # Remove any leading or trailing whitespace from each IP in the list + # this will return [''] if no ips have been entered, which is taken + # into account in the model in checkHostIPCombo + ip_list = [ip.strip() for ip in ip_list] + as_tuple = ( form.cleaned_data["server"], - [form.cleaned_data["ip"]] + ip_list, ) nameservers.append(as_tuple) except KeyError: # no server information in this field, skip it pass domain = self.get_object() - domain.nameservers = nameservers - - messages.success( - self.request, "The name servers for this domain have been updated." - ) + + try: + domain.nameservers = nameservers + except NameserverError as Err: + # TODO: move into literal + messages.error(self.request, 'Whoops, Nameservers Error') + # messages.error(self.request, GENERIC_ERROR) + logger.error(f"Nameservers error: {Err}") + # TODO: registry is not throwing an error when no connection + # TODO: merge 1103 and use literals + except RegistryError as Err: + 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}") + else: + messages.success( + self.request, "The name servers for this domain have been updated." + ) # superclass has the redirect return super().form_valid(formset) From 5e316adfd494329a1b593ed04d7c596d1024f6b8 Mon Sep 17 00:00:00 2001 From: Rachid Mrad Date: Wed, 18 Oct 2023 15:57:20 -0400 Subject: [PATCH 038/128] fix typo in check for isSubdomain --- 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 0b9139277..c4c38774d 100644 --- a/src/registrar/models/domain.py +++ b/src/registrar/models/domain.py @@ -313,7 +313,7 @@ class Domain(TimeStampedModel, DomainHelper): NameserverError (if exception hit) Returns: None""" - if self.isSubdomain(nameserver) and (ip is None or ip == [] or ip != []): + if self.isSubdomain(nameserver) and (ip is None or ip == [] or ip == ['']): raise NameserverError(code=nsErrorCodes.MISSING_IP, nameserver=nameserver) elif not self.isSubdomain(nameserver) and (ip is not None and ip != [] and ip != ['']): 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 039/128] 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 040/128] 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 1a01fdc98d162407cce4106338f254dd2f3d51d4 Mon Sep 17 00:00:00 2001 From: David Kennedy Date: Wed, 18 Oct 2023 16:30:54 -0400 Subject: [PATCH 041/128] merge --- src/registrar/views/domain.py | 14 +------------- 1 file changed, 1 insertion(+), 13 deletions(-) diff --git a/src/registrar/views/domain.py b/src/registrar/views/domain.py index 81ab32b80..d743d9d9e 100644 --- a/src/registrar/views/domain.py +++ b/src/registrar/views/domain.py @@ -23,11 +23,8 @@ from registrar.models import ( UserDomainRole, ) from registrar.models.public_contact import PublicContact -<<<<<<< HEAD from registrar.utility.errors import NameserverError -======= from registrar.models.utility.contact_error import ContactError ->>>>>>> main from ..forms import ( ContactForm, @@ -284,11 +281,9 @@ class DomainNameserversView(DomainFormBaseView): except KeyError: # no server information in this field, skip it pass -<<<<<<< HEAD - domain = self.get_object() try: - domain.nameservers = nameservers + self.object.nameservers = nameservers except NameserverError as Err: # TODO: move into literal messages.error(self.request, 'Whoops, Nameservers Error') @@ -307,13 +302,6 @@ class DomainNameserversView(DomainFormBaseView): messages.success( self.request, "The name servers for this domain have been updated." ) -======= - self.object.nameservers = nameservers - - messages.success( - self.request, "The name servers for this domain have been updated." - ) ->>>>>>> main # superclass has the redirect return super().form_valid(formset) 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 042/128] 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 043/128] 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 044/128] 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 045/128] 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 046/128] 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 047/128] 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 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 048/128] 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 8dba1234c1ba8d918eeea16fac5ad1543adab045 Mon Sep 17 00:00:00 2001 From: Rachid Mrad Date: Wed, 18 Oct 2023 18:54:01 -0400 Subject: [PATCH 049/128] wip on validation in form --- src/registrar/forms/domain.py | 32 +++++++++++++++++++++++++------- 1 file changed, 25 insertions(+), 7 deletions(-) diff --git a/src/registrar/forms/domain.py b/src/registrar/forms/domain.py index b93950df0..416d265ef 100644 --- a/src/registrar/forms/domain.py +++ b/src/registrar/forms/domain.py @@ -5,8 +5,9 @@ from django.core.validators import MinValueValidator, MaxValueValidator, RegexVa from django.forms import formset_factory from phonenumber_field.widgets import RegionalPhoneNumberWidget +from registrar.utility.errors import NameserverError -from ..models import Contact, DomainInformation +from ..models import Contact, DomainInformation, Domain from .common import ( ALGORITHM_CHOICES, DIGEST_TYPE_CHOICES, @@ -21,18 +22,35 @@ class DomainAddUserForm(forms.Form): email = forms.EmailField(label="Email") +class IPAddressField(forms.CharField): + # def __init__(self, server_value, *args, **kwargs): + # self.server_value = server_value + # super().__init__(*args, **kwargs) + + def validate(self, value): + super().validate(value) # Run the default CharField validation + + ip_list = [ip.strip() for ip in value.split(",")] # Split IPs and remove whitespace + + # TODO: pass hostname from view? + hostname = "" + + # Call the IP validation method from Domain + try: + Domain.checkHostIPCombo(hostname, ip_list) + except NameserverError as e: + raise forms.ValidationError(str(e)) + + class DomainNameserverForm(forms.Form): """Form for changing nameservers.""" server = forms.CharField(label="Name server", strip=True) - ip = forms.CharField( - label="IP address", - strip=True, + ip = IPAddressField( + label="IP address", + strip=True, required=False, - validators=[ - # TODO in progress - ], ) From b4596d3bcd31e966b0b2d6b7048b6a10344bd7e1 Mon Sep 17 00:00:00 2001 From: Rachid Mrad Date: Thu, 19 Oct 2023 02:55:02 -0400 Subject: [PATCH 050/128] wip making view communicate data with form --- src/registrar/forms/domain.py | 77 +++++++++++++++++++++++++++++------ src/registrar/views/domain.py | 16 ++++++-- 2 files changed, 77 insertions(+), 16 deletions(-) diff --git a/src/registrar/forms/domain.py b/src/registrar/forms/domain.py index 416d265ef..fe6a879e8 100644 --- a/src/registrar/forms/domain.py +++ b/src/registrar/forms/domain.py @@ -23,23 +23,25 @@ class DomainAddUserForm(forms.Form): class IPAddressField(forms.CharField): - # def __init__(self, server_value, *args, **kwargs): - # self.server_value = server_value - # super().__init__(*args, **kwargs) + + - def validate(self, value): + def validate(self, value): super().validate(value) # Run the default CharField validation - ip_list = [ip.strip() for ip in value.split(",")] # Split IPs and remove whitespace + # ip_list = [ip.strip() for ip in value.split(",")] # Split IPs and remove whitespace - # TODO: pass hostname from view? - hostname = "" + # # TODO: pass hostname from view? - # Call the IP validation method from Domain - try: - Domain.checkHostIPCombo(hostname, ip_list) - except NameserverError as e: - raise forms.ValidationError(str(e)) + # hostname = self.form.cleaned_data.get("server", "") + + # print(f"hostname {hostname}") + + # # Call the IP validation method from Domain + # try: + # Domain.checkHostIPCombo(hostname, ip_list) + # except NameserverError as e: + # raise forms.ValidationError(str(e)) class DomainNameserverForm(forms.Form): @@ -51,7 +53,58 @@ class DomainNameserverForm(forms.Form): label="IP address", strip=True, required=False, + # validators=[ + # django.core.validators.validate_ipv46_address + # ], ) + + def __init__(self, *args, **kwargs): + # Access the context object passed to the form + print(f"domain domain domain {kwargs}") + self.domain = kwargs.pop('rachid', None) + print(f"domain domain domain {kwargs}") + super().__init__(*args, **kwargs) + + # def __init__(self, request, *args, **kwargs): + # # Pass the request object to the form during initialization + # self.request = request + # super().__init__(*args, **kwargs) + + def clean(self): + cleaned_data = super().clean() + server = cleaned_data.get('server', '') + ip = cleaned_data.get('ip', '') + + # make sure there's a nameserver if an ip is passed + if ip: + ip_list = [ip.strip() for ip in ip.split(",")] + if not server: + # If 'server' is empty, disallow 'ip' input + raise forms.ValidationError("Name server must be provided to enter IP address.") + try: + Domain.checkHostIPCombo(server, ip_list) + except NameserverError as e: + raise forms.ValidationError(str(e)) + + # if there's a nameserver, validate nameserver/ip combo + domain, _ = Domain.objects.get_or_create(name="magazine-claim.gov") + + # Access session data from the request object + # session_data = self.request.session.get('nameservers_form_domain', None) + + print(f"domain domain domain {self.domain}") + + if server: + if ip: + ip_list = [ip.strip() for ip in ip.split(",")] + else: + ip_list = [""] + try: + Domain.checkHostIPCombo(domain, server, ip_list) + except NameserverError as e: + raise forms.ValidationError(str(e)) + + return cleaned_data NameserverFormset = formset_factory( diff --git a/src/registrar/views/domain.py b/src/registrar/views/domain.py index d743d9d9e..8c6fa5910 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.forms import BaseFormSet from registrar.models import ( Domain, @@ -213,12 +214,18 @@ class DomainDNSView(DomainBaseView): template_name = "domain_dns.html" -class DomainNameserversView(DomainFormBaseView): +class DomainNameserversView(DomainFormBaseView, BaseFormSet): """Domain nameserver editing view.""" template_name = "domain_nameservers.html" form_class = NameserverFormset - + + def get_formset_kwargs(self, index): + kwargs = super().get_formset_kwargs(index) + # kwargs['domain'] = self.object # Pass the context data + kwargs.update({"domain", self.object}) + return kwargs + def get_initial(self): """The initial value for the form (which is a formset here).""" nameservers = self.object.nameservers @@ -247,8 +254,7 @@ class DomainNameserversView(DomainFormBaseView): def get_form(self, **kwargs): """Override the labels and required fields every time we get a formset.""" - formset = super().get_form(**kwargs) - + formset = super().get_form(**kwargs) for i, form in enumerate(formset): form.fields["server"].label += f" {i+1}" form.fields["ip"].label += f" {i+1}" @@ -261,6 +267,8 @@ class DomainNameserversView(DomainFormBaseView): def form_valid(self, formset): """The formset is valid, perform something with it.""" + self.request.session['nameservers_form_domain'] = self.object + # Set the nameservers from the formset nameservers = [] for form in formset: 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 051/128] 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 052/128] 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 053/128] 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 054/128] 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 055/128] 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 056/128] 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 057/128] 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 058/128] 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 059/128] 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 060/128] 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 061/128] 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 d48312f74b4f51e8e9e57170c7f0e9084e1550d5 Mon Sep 17 00:00:00 2001 From: Rachid Mrad Date: Thu, 19 Oct 2023 13:24:35 -0400 Subject: [PATCH 062/128] wip passing domain from view to form --- src/registrar/forms/domain.py | 26 ++++++++++---------------- src/registrar/views/domain.py | 8 +++++++- 2 files changed, 17 insertions(+), 17 deletions(-) diff --git a/src/registrar/forms/domain.py b/src/registrar/forms/domain.py index fe6a879e8..dbee3fa4d 100644 --- a/src/registrar/forms/domain.py +++ b/src/registrar/forms/domain.py @@ -60,9 +60,8 @@ class DomainNameserverForm(forms.Form): def __init__(self, *args, **kwargs): # Access the context object passed to the form - print(f"domain domain domain {kwargs}") - self.domain = kwargs.pop('rachid', None) - print(f"domain domain domain {kwargs}") + print(f"kwargs in __init__ {kwargs}") + self.domain = kwargs.pop('domain', None) super().__init__(*args, **kwargs) # def __init__(self, request, *args, **kwargs): @@ -78,27 +77,20 @@ class DomainNameserverForm(forms.Form): # make sure there's a nameserver if an ip is passed if ip: ip_list = [ip.strip() for ip in ip.split(",")] - if not server: + if len(server) < len(ip_list): # If 'server' is empty, disallow 'ip' input raise forms.ValidationError("Name server must be provided to enter IP address.") - try: - Domain.checkHostIPCombo(server, ip_list) - except NameserverError as e: - raise forms.ValidationError(str(e)) - # if there's a nameserver, validate nameserver/ip combo - domain, _ = Domain.objects.get_or_create(name="magazine-claim.gov") + # if there's a nameserver and an ip, validate nameserver/ip combo + domain, _ = Domain.objects.get_or_create(name="realize-shake-too.gov") # Access session data from the request object # session_data = self.request.session.get('nameservers_form_domain', None) - print(f"domain domain domain {self.domain}") + print(f"domain in clean: {self.domain}") - if server: - if ip: - ip_list = [ip.strip() for ip in ip.split(",")] - else: - ip_list = [""] + if server and ip: + ip_list = [ip.strip() for ip in ip.split(",")] try: Domain.checkHostIPCombo(domain, server, ip_list) except NameserverError as e: @@ -110,6 +102,8 @@ class DomainNameserverForm(forms.Form): NameserverFormset = formset_factory( DomainNameserverForm, extra=1, + max_num=13, + validate_max=True, ) diff --git a/src/registrar/views/domain.py b/src/registrar/views/domain.py index 8c6fa5910..f82326a67 100644 --- a/src/registrar/views/domain.py +++ b/src/registrar/views/domain.py @@ -219,11 +219,13 @@ class DomainNameserversView(DomainFormBaseView, BaseFormSet): template_name = "domain_nameservers.html" form_class = NameserverFormset + model = Domain def get_formset_kwargs(self, index): kwargs = super().get_formset_kwargs(index) # kwargs['domain'] = self.object # Pass the context data kwargs.update({"domain", self.object}) + print(f"kwargs in get_formset_kwargs {kwargs}") return kwargs def get_initial(self): @@ -254,8 +256,12 @@ class DomainNameserversView(DomainFormBaseView, BaseFormSet): def get_form(self, **kwargs): """Override the labels and required fields every time we get a formset.""" - formset = super().get_form(**kwargs) + # kwargs.update({"domain", self.object}) + + formset = super().get_form(**kwargs) + for i, form in enumerate(formset): + # form = self.get_form(self, **kwargs) form.fields["server"].label += f" {i+1}" form.fields["ip"].label += f" {i+1}" if i < 2: 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 063/128] 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 150e89d2ee47e8287b1d2b51076eda07c6a2b94a Mon Sep 17 00:00:00 2001 From: David Kennedy Date: Thu, 19 Oct 2023 16:42:36 -0400 Subject: [PATCH 064/128] added form validation --- src/registrar/forms/domain.py | 28 ++++++------------- src/registrar/models/domain.py | 24 ++++++++++------ .../templates/domain_nameservers.html | 1 + src/registrar/views/domain.py | 24 ++++++++++------ 4 files changed, 40 insertions(+), 37 deletions(-) diff --git a/src/registrar/forms/domain.py b/src/registrar/forms/domain.py index dbee3fa4d..1a85cb373 100644 --- a/src/registrar/forms/domain.py +++ b/src/registrar/forms/domain.py @@ -47,6 +47,8 @@ class IPAddressField(forms.CharField): class DomainNameserverForm(forms.Form): """Form for changing nameservers.""" + domain = forms.CharField(widget=forms.HiddenInput, required=False) + server = forms.CharField(label="Name server", strip=True) ip = IPAddressField( @@ -58,21 +60,12 @@ class DomainNameserverForm(forms.Form): # ], ) - def __init__(self, *args, **kwargs): - # Access the context object passed to the form - print(f"kwargs in __init__ {kwargs}") - self.domain = kwargs.pop('domain', None) - super().__init__(*args, **kwargs) - - # def __init__(self, request, *args, **kwargs): - # # Pass the request object to the form during initialization - # self.request = request - # super().__init__(*args, **kwargs) - def clean(self): cleaned_data = super().clean() server = cleaned_data.get('server', '') ip = cleaned_data.get('ip', '') + domain = cleaned_data.get('domain', '') + print(f"clean is called on {domain} {server}") # make sure there's a nameserver if an ip is passed if ip: @@ -82,15 +75,12 @@ class DomainNameserverForm(forms.Form): raise forms.ValidationError("Name server must be provided to enter IP address.") # if there's a nameserver and an ip, validate nameserver/ip combo - domain, _ = Domain.objects.get_or_create(name="realize-shake-too.gov") - # Access session data from the request object - # session_data = self.request.session.get('nameservers_form_domain', None) - - print(f"domain in clean: {self.domain}") - - if server and ip: - ip_list = [ip.strip() for ip in ip.split(",")] + if server: + if ip: + ip_list = [ip.strip() for ip in ip.split(",")] + else: + ip_list = [''] try: Domain.checkHostIPCombo(domain, server, ip_list) except NameserverError as e: diff --git a/src/registrar/models/domain.py b/src/registrar/models/domain.py index efbb42bfb..978e442e3 100644 --- a/src/registrar/models/domain.py +++ b/src/registrar/models/domain.py @@ -291,14 +291,16 @@ class Domain(TimeStampedModel, DomainHelper): newDict[tup[0]] = tup[1] return newDict - def isSubdomain(self, nameserver: str): + @classmethod + def isSubdomain(cls, name: str, nameserver: str): """Returns boolean if the domain name is found in the argument passed""" subdomain_pattern = r"([\w-]+\.)*" - full_pattern = subdomain_pattern + self.name + full_pattern = subdomain_pattern + name regex = re.compile(full_pattern) return bool(regex.match(nameserver)) - def checkHostIPCombo(self, nameserver: str, ip: list[str]): + @classmethod + def checkHostIPCombo(cls, name: str, nameserver: str, ip: list[str]): """Checks the parameters past for a valid combination raises error if: - nameserver is a subdomain but is missing ip @@ -312,23 +314,27 @@ class Domain(TimeStampedModel, DomainHelper): NameserverError (if exception hit) Returns: None""" - if self.isSubdomain(nameserver) and (ip is None or ip == [] or ip == ['']): + logger.info("checkHostIPCombo is called on %s, %s", name, nameserver) + if cls.isSubdomain(name, nameserver) and (ip is None or ip == [] or ip == ['']): + raise NameserverError(code=nsErrorCodes.MISSING_IP, nameserver=nameserver) - elif not self.isSubdomain(nameserver) and (ip is not None and ip != [] and ip != ['']): + elif not cls.isSubdomain(name, nameserver) and (ip is not None and ip != [] and ip != ['']): raise NameserverError( code=nsErrorCodes.GLUE_RECORD_NOT_ALLOWED, nameserver=nameserver, ip=ip ) elif ip is not None and ip != [] and ip != ['']: for addr in ip: logger.info(f"ip address {addr}") - if not self._valid_ip_addr(addr): + if not cls._valid_ip_addr(addr): raise NameserverError( code=nsErrorCodes.INVALID_IP, nameserver=nameserver, ip=ip ) + logger.info("got no errors") return None - def _valid_ip_addr(self, ipToTest: str): + @classmethod + def _valid_ip_addr(cls, ipToTest: str): """returns boolean if valid ip address string We currently only accept v4 or v6 ips returns: @@ -383,7 +389,7 @@ class Domain(TimeStampedModel, DomainHelper): if newHostDict[prevHost] is not None and set( newHostDict[prevHost] ) != set(addrs): - self.checkHostIPCombo(nameserver=prevHost, ip=newHostDict[prevHost]) + self.__class__.checkHostIPCombo(name=self.name, nameserver=prevHost, ip=newHostDict[prevHost]) updated_values.append((prevHost, newHostDict[prevHost])) new_values = { @@ -393,7 +399,7 @@ class Domain(TimeStampedModel, DomainHelper): } for nameserver, ip in new_values.items(): - self.checkHostIPCombo(nameserver=nameserver, ip=ip) + self.__class__.checkHostIPCombo(name=self.name, nameserver=nameserver, ip=ip) return (deleted_values, updated_values, new_values, previousHostDict) diff --git a/src/registrar/templates/domain_nameservers.html b/src/registrar/templates/domain_nameservers.html index fffd4b8c0..aa53719ea 100644 --- a/src/registrar/templates/domain_nameservers.html +++ b/src/registrar/templates/domain_nameservers.html @@ -24,6 +24,7 @@
+ {{ form.domain }} {% with sublabel_text="Example: ns"|concat:forloop.counter|concat:".example.com" %} {% if forloop.counter <= 2 %} {% with attr_required=True add_group_class="usa-form-group--unstyled-error" %} diff --git a/src/registrar/views/domain.py b/src/registrar/views/domain.py index f82326a67..06361a3b3 100644 --- a/src/registrar/views/domain.py +++ b/src/registrar/views/domain.py @@ -221,13 +221,6 @@ class DomainNameserversView(DomainFormBaseView, BaseFormSet): form_class = NameserverFormset model = Domain - def get_formset_kwargs(self, index): - kwargs = super().get_formset_kwargs(index) - # kwargs['domain'] = self.object # Pass the context data - kwargs.update({"domain", self.object}) - print(f"kwargs in get_formset_kwargs {kwargs}") - return kwargs - def get_initial(self): """The initial value for the form (which is a formset here).""" nameservers = self.object.nameservers @@ -257,7 +250,6 @@ class DomainNameserversView(DomainFormBaseView, BaseFormSet): def get_form(self, **kwargs): """Override the labels and required fields every time we get a formset.""" # kwargs.update({"domain", self.object}) - formset = super().get_form(**kwargs) for i, form in enumerate(formset): @@ -268,8 +260,22 @@ class DomainNameserversView(DomainFormBaseView, BaseFormSet): form.fields["server"].required = True else: form.fields["server"].required = False + form.fields["domain"].initial = self.object.name + print(f"domain in get_form {self.object.name}") return formset + + def post(self, request, *args, **kwargs): + """Form submission posts to this view. + This post method harmonizes using DomainBaseView and FormMixin + """ + self._get_domain(request) + form = self.get_form() + if form.is_valid(): + return self.form_valid(form) + else: + return self.form_invalid(form) + def form_valid(self, formset): """The formset is valid, perform something with it.""" @@ -286,7 +292,7 @@ class DomainNameserversView(DomainFormBaseView, BaseFormSet): # this will return [''] if no ips have been entered, which is taken # into account in the model in checkHostIPCombo ip_list = [ip.strip() for ip in ip_list] - + as_tuple = ( form.cleaned_data["server"], ip_list, From 43b6d1e380dc1b93145b6e6aef7d8c809ed61cae Mon Sep 17 00:00:00 2001 From: David Kennedy Date: Thu, 19 Oct 2023 17:26:26 -0400 Subject: [PATCH 065/128] form level error handling at the field level --- src/registrar/forms/domain.py | 16 ++++++++++++---- src/registrar/models/domain.py | 3 --- 2 files changed, 12 insertions(+), 7 deletions(-) diff --git a/src/registrar/forms/domain.py b/src/registrar/forms/domain.py index 1a85cb373..303c43690 100644 --- a/src/registrar/forms/domain.py +++ b/src/registrar/forms/domain.py @@ -5,7 +5,10 @@ from django.core.validators import MinValueValidator, MaxValueValidator, RegexVa from django.forms import formset_factory from phonenumber_field.widgets import RegionalPhoneNumberWidget -from registrar.utility.errors import NameserverError +from registrar.utility.errors import ( + NameserverError, + NameserverErrorCodes as nsErrorCodes +) from ..models import Contact, DomainInformation, Domain from .common import ( @@ -70,9 +73,9 @@ class DomainNameserverForm(forms.Form): # make sure there's a nameserver if an ip is passed if ip: ip_list = [ip.strip() for ip in ip.split(",")] - if len(server) < len(ip_list): + if not server and len(ip_list) > 0: # If 'server' is empty, disallow 'ip' input - raise forms.ValidationError("Name server must be provided to enter IP address.") + self.add_error('server', "Nameserver must be provided to enter IP address.") # if there's a nameserver and an ip, validate nameserver/ip combo @@ -84,7 +87,12 @@ class DomainNameserverForm(forms.Form): try: Domain.checkHostIPCombo(domain, server, ip_list) except NameserverError as e: - raise forms.ValidationError(str(e)) + if e.code == nsErrorCodes.GLUE_RECORD_NOT_ALLOWED: + self.add_error('server', "Name server address does not match domain name") + elif e.code == nsErrorCodes.MISSING_IP: + self.add_error('ip', "Subdomains require an IP address") + else: + self.add_error('ip', str(e)) return cleaned_data diff --git a/src/registrar/models/domain.py b/src/registrar/models/domain.py index 978e442e3..933ae63be 100644 --- a/src/registrar/models/domain.py +++ b/src/registrar/models/domain.py @@ -314,7 +314,6 @@ class Domain(TimeStampedModel, DomainHelper): NameserverError (if exception hit) Returns: None""" - logger.info("checkHostIPCombo is called on %s, %s", name, nameserver) if cls.isSubdomain(name, nameserver) and (ip is None or ip == [] or ip == ['']): raise NameserverError(code=nsErrorCodes.MISSING_IP, nameserver=nameserver) @@ -330,7 +329,6 @@ class Domain(TimeStampedModel, DomainHelper): raise NameserverError( code=nsErrorCodes.INVALID_IP, nameserver=nameserver, ip=ip ) - logger.info("got no errors") return None @classmethod @@ -340,7 +338,6 @@ class Domain(TimeStampedModel, DomainHelper): returns: isValid (boolean)-True for valid ip address""" try: - logger.info(f"in valid_ip_addr: {ipToTest}") ip = ipaddress.ip_address(ipToTest) logger.info(ip.version) return ip.version == 6 or ip.version == 4 From 37ada699a0d30ff19c5ab75eb957f0470fb0dd68 Mon Sep 17 00:00:00 2001 From: Rachid Mrad Date: Thu, 19 Oct 2023 17:52:41 -0400 Subject: [PATCH 066/128] cancel button, template tweaks --- src/registrar/forms/domain.py | 4 +-- .../templates/domain_nameservers.html | 25 ++++++++++++++++--- .../templates/includes/form_messages.html | 2 +- src/registrar/views/domain.py | 15 ++++++----- 4 files changed, 33 insertions(+), 13 deletions(-) diff --git a/src/registrar/forms/domain.py b/src/registrar/forms/domain.py index 303c43690..a8a285e99 100644 --- a/src/registrar/forms/domain.py +++ b/src/registrar/forms/domain.py @@ -55,7 +55,7 @@ class DomainNameserverForm(forms.Form): server = forms.CharField(label="Name server", strip=True) ip = IPAddressField( - label="IP address", + label="IP Address (IPv4 or IPv6)", strip=True, required=False, # validators=[ @@ -75,7 +75,7 @@ class DomainNameserverForm(forms.Form): ip_list = [ip.strip() for ip in ip.split(",")] if not server and len(ip_list) > 0: # If 'server' is empty, disallow 'ip' input - self.add_error('server', "Nameserver must be provided to enter IP address.") + self.add_error('server', "Name server must be provided to enter IP address.") # if there's a nameserver and an ip, validate nameserver/ip combo diff --git a/src/registrar/templates/domain_nameservers.html b/src/registrar/templates/domain_nameservers.html index aa53719ea..e4323615a 100644 --- a/src/registrar/templates/domain_nameservers.html +++ b/src/registrar/templates/domain_nameservers.html @@ -11,8 +11,16 @@

DNS name servers

-

Before your domain can be used we'll need information about your domain - name servers.

+

Before your domain can be used we’ll need information about your domain name servers. Name server records indicate which DNS server is authoritative for your domain.

+ +

Add a name server record by entering the address (e.g., ns1.nameserver.com) in the name server fields below. You may add up to 13 name servers.

+ +
+
+

Add an IP address only when your name server's address includes your domain name (e.g., if your domain name is "example.gov" and your name server is "ns1.example.gov,” then an IP address is required.) To add multiple IP addresses, separate them with commas.

+

This step is uncommon unless you self-host your DNS or use custom addresses for your nameserver.

+
+
{% include "includes/required_fields.html" %} @@ -36,7 +44,7 @@ {% endwith %}
- {% with sublabel_text="Example: ns"|concat:forloop.counter|concat:".example.com" add_group_class="usa-form-group--unstyled-error" %} + {% with sublabel_text="Ex: 86.124.49.54 or 2001:db8::1234:5678" %} {% input_with_errors form.ip %} {% endwith %}
@@ -44,7 +52,7 @@
{% endfor %} - +
+ +
{% endblock %} {# domain_content #} diff --git a/src/registrar/templates/includes/form_messages.html b/src/registrar/templates/includes/form_messages.html index c7b704f67..e2888e4ee 100644 --- a/src/registrar/templates/includes/form_messages.html +++ b/src/registrar/templates/includes/form_messages.html @@ -1,6 +1,6 @@ {% if messages %} {% for message in messages %} -
+
{{ message }}
diff --git a/src/registrar/views/domain.py b/src/registrar/views/domain.py index 06361a3b3..23306bda9 100644 --- a/src/registrar/views/domain.py +++ b/src/registrar/views/domain.py @@ -255,7 +255,6 @@ class DomainNameserversView(DomainFormBaseView, BaseFormSet): for i, form in enumerate(formset): # form = self.get_form(self, **kwargs) form.fields["server"].label += f" {i+1}" - form.fields["ip"].label += f" {i+1}" if i < 2: form.fields["server"].required = True else: @@ -270,11 +269,15 @@ class DomainNameserversView(DomainFormBaseView, BaseFormSet): This post method harmonizes using DomainBaseView and FormMixin """ self._get_domain(request) - form = self.get_form() - if form.is_valid(): - return self.form_valid(form) + formset = self.get_form() + + 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(form) + return self.form_invalid(formset) def form_valid(self, formset): """The formset is valid, perform something with it.""" @@ -320,7 +323,7 @@ class DomainNameserversView(DomainFormBaseView, BaseFormSet): logger.error(f"Registry error: {Err}") else: messages.success( - self.request, "The name servers for this domain have been updated." + self.request, "The name servers for this domain have been updated. Keep in mind that DNS changes may take some time to propagate across the internet. It can take anywhere from a few minutes to 48 hours for your changes to take place." ) # superclass has the redirect 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 067/128] 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 068/128] 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 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 069/128] 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 4d604b68b8cbd6d3c70c5d845001a5dc6ca7a4e9 Mon Sep 17 00:00:00 2001 From: David Kennedy Date: Fri, 20 Oct 2023 14:09:41 -0400 Subject: [PATCH 070/128] fixed formatting of error message in form --- src/registrar/templates/domain_nameservers.html | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/registrar/templates/domain_nameservers.html b/src/registrar/templates/domain_nameservers.html index e4323615a..4baf5d389 100644 --- a/src/registrar/templates/domain_nameservers.html +++ b/src/registrar/templates/domain_nameservers.html @@ -44,7 +44,7 @@ {% endwith %}
- {% with sublabel_text="Ex: 86.124.49.54 or 2001:db8::1234:5678" %} + {% with sublabel_text="Ex: 86.124.49.54 or 2001:db8::1234:5678" add_group_class="usa-form-group--unstyled-error" %} {% input_with_errors form.ip %} {% endwith %}
From ff44ee2f1ef132b8f6a7ba4b85968ee4445aaeda Mon Sep 17 00:00:00 2001 From: Erin <121973038+erinysong@users.noreply.github.com> Date: Fri, 20 Oct 2023 12:28:47 -0700 Subject: [PATCH 071/128] Fix logic of availability API --- src/api/tests/test_available.py | 47 ++++++++++--------- src/api/views.py | 14 +++--- src/registrar/models/utility/domain_helper.py | 4 +- src/registrar/templates/domain_detail.html | 2 +- src/registrar/tests/common.py | 8 ++-- 5 files changed, 39 insertions(+), 36 deletions(-) diff --git a/src/api/tests/test_available.py b/src/api/tests/test_available.py index 9eab17bf7..52d0d962c 100644 --- a/src/api/tests/test_available.py +++ b/src/api/tests/test_available.py @@ -5,7 +5,7 @@ import json from django.contrib.auth import get_user_model from django.test import RequestFactory -from ..views import available, in_domains +from ..views import available, check_domain_available from .common import less_console_noise from registrar.tests.common import MockEppLib from unittest.mock import call @@ -37,10 +37,10 @@ class AvailableViewTest(MockEppLib): response_object = json.loads(response.content) self.assertIn("available", response_object) - def test_in_domains_makes_calls_(self): + def test_domain_available_makes_calls_(self): """Domain searches successfully make correct mock EPP calls""" - gsa_available = in_domains("gsa.gov") - igorville_available = in_domains("igorvilleremixed.gov") + gsa_available = check_domain_available("gsa.gov") + igorville_available = check_domain_available("igorville.gov") """Domain searches successfully make mock EPP calls""" self.mockedSendFunction.assert_has_calls( @@ -53,29 +53,32 @@ class AvailableViewTest(MockEppLib): ), call( commands.CheckDomain( - ["igorvilleremixed.gov"], + ["igorville.gov"], ), cleaned=True, ), ] ) """Domain searches return correct availability results""" - self.assertTrue(gsa_available) - self.assertFalse(igorville_available) + self.assertFalse(gsa_available) + self.assertTrue(igorville_available) - def test_in_domains_capitalized(self): + def test_domain_available_capitalized(self): """Domain searches work without case sensitivity""" - self.assertTrue(in_domains("gsa.gov")) - # input is lowercased so GSA.GOV should be found - self.assertTrue(in_domains("GSA.gov")) + self.assertFalse(check_domain_available("gsa.gov")) + self.assertTrue(check_domain_available("igorville.gov")) + # input is lowercased so GSA.GOV should also not be available + self.assertFalse(check_domain_available("GSA.gov")) + # input is lowercased so IGORVILLE.GOV should also not be available + self.assertFalse(check_domain_available("IGORVILLE.gov")) - def test_in_domains_dotgov(self): + def test_domain_available_dotgov(self): """Domain searches work without trailing .gov""" - self.assertTrue(in_domains("gsa")) + self.assertFalse(check_domain_available("gsa")) # input is lowercased so GSA.GOV should be found - self.assertTrue(in_domains("GSA")) - # This domain should not have been registered - self.assertFalse(in_domains("igorvilleremixed")) + self.assertFalse(check_domain_available("GSA")) + # This domain should be available to register + self.assertTrue(check_domain_available("igorville")) def test_not_available_domain(self): """gsa.gov is not available""" @@ -85,17 +88,17 @@ class AvailableViewTest(MockEppLib): self.assertFalse(json.loads(response.content)["available"]) def test_available_domain(self): - """igorvilleremixed.gov is still available""" - request = self.factory.get(API_BASE_PATH + "igorvilleremixed.gov") + """igorville.gov is still available""" + request = self.factory.get(API_BASE_PATH + "igorville.gov") request.user = self.user - response = available(request, domain="igorvilleremixed.gov") + response = available(request, domain="igorville.gov") self.assertTrue(json.loads(response.content)["available"]) def test_available_domain_dotgov(self): - """igorvilleremixed.gov is still available even without the .gov suffix""" - request = self.factory.get(API_BASE_PATH + "igorvilleremixed") + """igorville.gov is still available even without the .gov suffix""" + request = self.factory.get(API_BASE_PATH + "igorville") request.user = self.user - response = available(request, domain="igorvilleremixed") + response = available(request, domain="igorville") self.assertTrue(json.loads(response.content)["available"]) def test_error_handling(self): diff --git a/src/api/views.py b/src/api/views.py index e8b8431de..87607a71a 100644 --- a/src/api/views.py +++ b/src/api/views.py @@ -50,8 +50,8 @@ def _domains(): return domains -def in_domains(domain): - """Return true if the given domain is in the domains list. +def check_domain_available(domain): + """Return true if the given domain is available. The given domain is lowercased to match against the domains list. If the given domain doesn't end with .gov, ".gov" is added when looking for @@ -83,11 +83,11 @@ def available(request, domain=""): {"available": False, "message": DOMAIN_API_MESSAGES["invalid"]} ) # a domain is available if it is NOT in the list of current domains - if in_domains(domain): - return JsonResponse( - {"available": False, "message": DOMAIN_API_MESSAGES["unavailable"]} - ) - else: + if check_domain_available(domain): return JsonResponse( {"available": True, "message": DOMAIN_API_MESSAGES["success"]} ) + else: + return JsonResponse( + {"available": False, "message": DOMAIN_API_MESSAGES["unavailable"]} + ) diff --git a/src/registrar/models/utility/domain_helper.py b/src/registrar/models/utility/domain_helper.py index 8f5737915..1a77e44b1 100644 --- a/src/registrar/models/utility/domain_helper.py +++ b/src/registrar/models/utility/domain_helper.py @@ -1,6 +1,6 @@ import re -from api.views import in_domains +from api.views import check_domain_available from registrar.utility import errors @@ -44,7 +44,7 @@ class DomainHelper: raise errors.ExtraDotsError() if not DomainHelper.string_could_be_domain(domain + ".gov"): raise ValueError() - if in_domains(domain): + if not check_domain_available(domain): raise errors.DomainUnavailableError() return domain diff --git a/src/registrar/templates/domain_detail.html b/src/registrar/templates/domain_detail.html index e0d672093..bb137a91f 100644 --- a/src/registrar/templates/domain_detail.html +++ b/src/registrar/templates/domain_detail.html @@ -29,7 +29,7 @@ {% url 'domain-dns-nameservers' pk=domain.id as url %} {% if domain.nameservers|length > 0 %} - {% include "includes/summary_item.html" with title='DNS name servers' value=domain.nameservers list='true' edit_link=url %} + {% include "includes/summary_item.html" with title='DNS name servers' value=domain.nameservers list='true' edit_link=url only %} {% else %}

DNS name servers

No DNS name servers have been added yet. Before your domain can be used we’ll need information about your domain name servers.

diff --git a/src/registrar/tests/common.py b/src/registrar/tests/common.py index 8cd5fd6ba..a2370a20d 100644 --- a/src/registrar/tests/common.py +++ b/src/registrar/tests/common.py @@ -834,11 +834,11 @@ class MockEppLib(TestCase): def mockCheckDomainCommand(self, _request, cleaned): if "gsa.gov" in getattr(_request, "names", None): - return self._mockDomainName("gsa.gov", True) + return self._mockDomainName("gsa.gov", False) elif "GSA.gov" in getattr(_request, "names", None): - return self._mockDomainName("GSA.gov", True) - elif "igorvilleremixed.gov" in getattr(_request, "names", None): - return self._mockDomainName("igorvilleremixed.gov", False) + return self._mockDomainName("GSA.gov", False) + elif "igorville.gov" in getattr(_request, "names", None): + return self._mockDomainName("igorvilleremixed.gov", True) elif "errordomain.gov" in getattr(_request, "names", None): raise RegistryError("Registry cannot find domain availability.") else: From 18447c14864a72d72181097b0bea00537935544d Mon Sep 17 00:00:00 2001 From: Rebecca Hsieh Date: Fri, 20 Oct 2023 14:42:33 -0700 Subject: [PATCH 072/128] Formatting for nameservers --- src/registrar/models/domain.py | 38 ++++++++++++++++++++++ src/registrar/templates/domain_detail.html | 4 +-- 2 files changed, 40 insertions(+), 2 deletions(-) diff --git a/src/registrar/models/domain.py b/src/registrar/models/domain.py index c7d786426..ab0ad4b7b 100644 --- a/src/registrar/models/domain.py +++ b/src/registrar/models/domain.py @@ -2,6 +2,7 @@ from itertools import zip_longest import logging import ipaddress import re +import string from datetime import date from string import digits from typing import Optional @@ -228,6 +229,43 @@ class Domain(TimeStampedModel, DomainHelper): """ raise NotImplementedError() + @Cache + def format_nameservers(self): + """ + Formatting nameservers for display for this domain. + + Hosts are provided as a list of tuples, e.g. + + [("ns1.example.com",), ("ns1.example.gov", ["0.0.0.0"])] + + We want to display as: + ns1.example.gov + ns1.example.gov (0.0.0.0) + + Subordinate hosts (something.your-domain.gov) MUST have IP addresses, + while non-subordinate hosts MUST NOT. + + + """ + try: + hosts = self._get_property("hosts") + except Exception as err: + # Do not raise error when missing nameservers + # this is a standard occurence when a domain + # is first created + logger.info("Domain is missing nameservers %s" % err) + return [] + + hostList = [] + for host in hosts: + host_info = host["name"] + if len(host["addrs"]) > 0: + converter = string.maketrans("[]", "()") + host_info.append(host["addrs"].translate(converter, "'")) + hostList.append(host_info) + + return hostList + @Cache def nameservers(self) -> list[tuple[str, list]]: """ diff --git a/src/registrar/templates/domain_detail.html b/src/registrar/templates/domain_detail.html index e0d672093..c5394e433 100644 --- a/src/registrar/templates/domain_detail.html +++ b/src/registrar/templates/domain_detail.html @@ -28,8 +28,8 @@
{% url 'domain-dns-nameservers' pk=domain.id as url %} - {% if domain.nameservers|length > 0 %} - {% include "includes/summary_item.html" with title='DNS name servers' value=domain.nameservers list='true' edit_link=url %} + {% if domain.format_nameservers|length > 0 %} + {% include "includes/summary_item.html" with title='DNS name servers' value=domain.format_nameservers list='true' edit_link=url %} {% else %}

DNS name servers

No DNS name servers have been added yet. Before your domain can be used we’ll need information about your domain name servers.

From 9c7bbd31ec0e588cbd55407f1a5a64da42f33632 Mon Sep 17 00:00:00 2001 From: Erin <121973038+erinysong@users.noreply.github.com> Date: Fri, 20 Oct 2023 15:03:07 -0700 Subject: [PATCH 073/128] Make login not required to access available API --- src/api/views.py | 16 +++++++++++----- 1 file changed, 11 insertions(+), 5 deletions(-) diff --git a/src/api/views.py b/src/api/views.py index 87607a71a..a80aff643 100644 --- a/src/api/views.py +++ b/src/api/views.py @@ -5,6 +5,8 @@ from django.http import JsonResponse import requests +from login_required import login_not_required + from cachetools.func import ttl_cache @@ -58,14 +60,18 @@ def check_domain_available(domain): a match. """ Domain = apps.get_model("registrar.Domain") - if domain.endswith(".gov"): - return Domain.available(domain) - else: - # domain search string doesn't end with .gov, add it on here - return Domain.available(domain + ".gov") + try: + if domain.endswith(".gov"): + return Domain.available(domain) + else: + # domain search string doesn't end with .gov, add it on here + return Domain.available(domain + ".gov") + except: + return False @require_http_methods(["GET"]) +@login_not_required def available(request, domain=""): """Is a given domain available or not. From c75891ee9077043807b676fa8ccc6712bfa9559f Mon Sep 17 00:00:00 2001 From: Erin <121973038+erinysong@users.noreply.github.com> Date: Fri, 20 Oct 2023 15:57:27 -0700 Subject: [PATCH 074/128] Modify tests for availability API to return false on error --- src/api/tests/test_available.py | 7 +++---- src/api/views.py | 19 +++++++++++-------- src/registrar/tests/test_url_auth.py | 1 + 3 files changed, 15 insertions(+), 12 deletions(-) diff --git a/src/api/tests/test_available.py b/src/api/tests/test_available.py index 52d0d962c..c7253fa35 100644 --- a/src/api/tests/test_available.py +++ b/src/api/tests/test_available.py @@ -108,10 +108,9 @@ class AvailableViewTest(MockEppLib): request.user = self.user response = available(request, domain=bad_string) self.assertFalse(json.loads(response.content)["available"]) - # domain set to raise error successfully raises error - with self.assertRaises(RegistryError): - error_domain_available = available(request, "errordomain.gov") - self.assertFalse(json.loads(error_domain_available.content)["available"]) + # domain set to raise error returns false for availability + error_domain_available = available(request, "errordomain.gov") + self.assertFalse(json.loads(error_domain_available.content)["available"]) class AvailableAPITest(MockEppLib): diff --git a/src/api/views.py b/src/api/views.py index a80aff643..94a5ea9f9 100644 --- a/src/api/views.py +++ b/src/api/views.py @@ -89,11 +89,14 @@ def available(request, domain=""): {"available": False, "message": DOMAIN_API_MESSAGES["invalid"]} ) # a domain is available if it is NOT in the list of current domains - if check_domain_available(domain): - return JsonResponse( - {"available": True, "message": DOMAIN_API_MESSAGES["success"]} - ) - else: - return JsonResponse( - {"available": False, "message": DOMAIN_API_MESSAGES["unavailable"]} - ) + try: + if check_domain_available(domain): + return JsonResponse( + {"available": True, "message": DOMAIN_API_MESSAGES["success"]} + ) + else: + return JsonResponse( + {"available": False, "message": DOMAIN_API_MESSAGES["unavailable"]} + ) + except: + raise RegistryError("Registry cannot find domain availability.") diff --git a/src/registrar/tests/test_url_auth.py b/src/registrar/tests/test_url_auth.py index 17ad2c329..2a04e8030 100644 --- a/src/registrar/tests/test_url_auth.py +++ b/src/registrar/tests/test_url_auth.py @@ -116,6 +116,7 @@ class TestURLAuth(TestCase): "/openid/callback", "/openid/callback/login/", "/openid/callback/logout/", + "/api/v1/available" ] def assertURLIsProtectedByAuth(self, url): From 15b16583cd8a6e894dbfa069d829dc7677d9fffa Mon Sep 17 00:00:00 2001 From: Erin <121973038+erinysong@users.noreply.github.com> Date: Fri, 20 Oct 2023 16:10:15 -0700 Subject: [PATCH 075/128] Add mocked responses for test_forms cases --- src/api/views.py | 5 ++++- src/registrar/tests/common.py | 2 ++ 2 files changed, 6 insertions(+), 1 deletion(-) diff --git a/src/api/views.py b/src/api/views.py index 94a5ea9f9..158280b11 100644 --- a/src/api/views.py +++ b/src/api/views.py @@ -25,6 +25,7 @@ DOMAIN_API_MESSAGES = { "invalid": "Enter a domain using only letters," " numbers, or hyphens (though we don't recommend using hyphens).", "success": "That domain is available!", + "error": "Error finding domain availability." } @@ -99,4 +100,6 @@ def available(request, domain=""): {"available": False, "message": DOMAIN_API_MESSAGES["unavailable"]} ) except: - raise RegistryError("Registry cannot find domain availability.") + return JsonResponse( + {"available": False, "message": DOMAIN_API_MESSAGES["error"]} + ) diff --git a/src/registrar/tests/common.py b/src/registrar/tests/common.py index a2370a20d..53d15a91c 100644 --- a/src/registrar/tests/common.py +++ b/src/registrar/tests/common.py @@ -839,6 +839,8 @@ class MockEppLib(TestCase): return self._mockDomainName("GSA.gov", False) elif "igorville.gov" in getattr(_request, "names", None): return self._mockDomainName("igorvilleremixed.gov", True) + elif "top-level-agency.gov" in getattr(_request, "names", None): + return self._mockDomainName("top-level-agency.gov", True) elif "errordomain.gov" in getattr(_request, "names", None): raise RegistryError("Registry cannot find domain availability.") else: From a30ed4725ad528d9789234a438a61b55b91e1cff Mon Sep 17 00:00:00 2001 From: Erin <121973038+erinysong@users.noreply.github.com> Date: Fri, 20 Oct 2023 16:15:03 -0700 Subject: [PATCH 076/128] Fix linting errors --- src/api/tests/test_available.py | 1 - src/api/views.py | 10 +++++----- src/registrar/tests/test_url_auth.py | 2 +- 3 files changed, 6 insertions(+), 7 deletions(-) diff --git a/src/api/tests/test_available.py b/src/api/tests/test_available.py index c7253fa35..524fd689a 100644 --- a/src/api/tests/test_available.py +++ b/src/api/tests/test_available.py @@ -12,7 +12,6 @@ from unittest.mock import call from epplibwrapper import ( commands, - RegistryError, ) API_BASE_PATH = "/api/v1/available/" diff --git a/src/api/views.py b/src/api/views.py index 158280b11..694bea349 100644 --- a/src/api/views.py +++ b/src/api/views.py @@ -25,7 +25,7 @@ DOMAIN_API_MESSAGES = { "invalid": "Enter a domain using only letters," " numbers, or hyphens (though we don't recommend using hyphens).", "success": "That domain is available!", - "error": "Error finding domain availability." + "error": "Error finding domain availability.", } @@ -67,7 +67,7 @@ def check_domain_available(domain): else: # domain search string doesn't end with .gov, add it on here return Domain.available(domain + ".gov") - except: + except Exception: return False @@ -99,7 +99,7 @@ def available(request, domain=""): return JsonResponse( {"available": False, "message": DOMAIN_API_MESSAGES["unavailable"]} ) - except: + except Exception: return JsonResponse( - {"available": False, "message": DOMAIN_API_MESSAGES["error"]} - ) + {"available": False, "message": DOMAIN_API_MESSAGES["error"]} + ) diff --git a/src/registrar/tests/test_url_auth.py b/src/registrar/tests/test_url_auth.py index 2a04e8030..32fc78ca1 100644 --- a/src/registrar/tests/test_url_auth.py +++ b/src/registrar/tests/test_url_auth.py @@ -116,7 +116,7 @@ class TestURLAuth(TestCase): "/openid/callback", "/openid/callback/login/", "/openid/callback/logout/", - "/api/v1/available" + "/api/v1/available", ] def assertURLIsProtectedByAuth(self, url): From 3f068bc209f5c7d1f80a17cd6e900ecd901fc1a3 Mon Sep 17 00:00:00 2001 From: David Kennedy Date: Fri, 20 Oct 2023 20:11:44 -0400 Subject: [PATCH 077/128] fixed some issues with nameserver getter and setter, as well as view get_initial --- src/registrar/models/domain.py | 4 ++-- src/registrar/views/domain.py | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/src/registrar/models/domain.py b/src/registrar/models/domain.py index 933ae63be..d4c634f21 100644 --- a/src/registrar/models/domain.py +++ b/src/registrar/models/domain.py @@ -1384,7 +1384,7 @@ class Domain(TimeStampedModel, DomainHelper): @transition( field="state", - source=[State.DNS_NEEDED], + source=[State.DNS_NEEDED, State.READY], target=State.READY, # conditions=[dns_not_needed] ) @@ -1549,7 +1549,7 @@ class Domain(TimeStampedModel, DomainHelper): data = registry.send(req, cleaned=True).res_data[0] host = { "name": name, - "addrs": getattr(data, "addrs", ...), + "addrs": [item.addr for item in getattr(data, "addrs", [])], "cr_date": getattr(data, "cr_date", ...), "statuses": getattr(data, "statuses", ...), "tr_date": getattr(data, "tr_date", ...), diff --git a/src/registrar/views/domain.py b/src/registrar/views/domain.py index 23306bda9..4d977891d 100644 --- a/src/registrar/views/domain.py +++ b/src/registrar/views/domain.py @@ -228,7 +228,7 @@ class DomainNameserversView(DomainFormBaseView, BaseFormSet): if nameservers is not None: # Add existing nameservers as initial data - initial_data.extend({"server": name, "ip": ip} for name, ip in nameservers) + initial_data.extend({"server": name, "ip": ','.join(ip)} for name, ip in nameservers) # Ensure at least 3 fields, filled or empty while len(initial_data) < 2: 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 078/128] 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 079/128] 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 5c9fbf483eb4b3064ed4574952d66af4413539e6 Mon Sep 17 00:00:00 2001 From: Erin <121973038+erinysong@users.noreply.github.com> Date: Mon, 23 Oct 2023 09:50:35 -0700 Subject: [PATCH 080/128] Add url to ignore_urls list --- src/registrar/templates/domain_detail.html | 2 +- src/registrar/tests/test_url_auth.py | 4 ++-- src/registrar/tests/test_views.py | 3 +++ 3 files changed, 6 insertions(+), 3 deletions(-) diff --git a/src/registrar/templates/domain_detail.html b/src/registrar/templates/domain_detail.html index bb137a91f..e0d672093 100644 --- a/src/registrar/templates/domain_detail.html +++ b/src/registrar/templates/domain_detail.html @@ -29,7 +29,7 @@ {% url 'domain-dns-nameservers' pk=domain.id as url %} {% if domain.nameservers|length > 0 %} - {% include "includes/summary_item.html" with title='DNS name servers' value=domain.nameservers list='true' edit_link=url only %} + {% include "includes/summary_item.html" with title='DNS name servers' value=domain.nameservers list='true' edit_link=url %} {% else %}

DNS name servers

No DNS name servers have been added yet. Before your domain can be used we’ll need information about your domain name servers.

diff --git a/src/registrar/tests/test_url_auth.py b/src/registrar/tests/test_url_auth.py index 32fc78ca1..19af605a5 100644 --- a/src/registrar/tests/test_url_auth.py +++ b/src/registrar/tests/test_url_auth.py @@ -110,13 +110,13 @@ class TestURLAuth(TestCase): # Note that the trailing slash is wobbly depending on how the URL was defined. IGNORE_URLS = [ # These are the OIDC auth endpoints that always need - # to be public. + # to be public. Use the exact URLs that will be tested. "/openid/login/", "/openid/logout/", "/openid/callback", "/openid/callback/login/", "/openid/callback/logout/", - "/api/v1/available", + "/api/v1/available/whitehouse.gov", ] def assertURLIsProtectedByAuth(self, url): diff --git a/src/registrar/tests/test_views.py b/src/registrar/tests/test_views.py index 7cc616889..7250a9d8c 100644 --- a/src/registrar/tests/test_views.py +++ b/src/registrar/tests/test_views.py @@ -291,6 +291,9 @@ class DomainApplicationTests(TestWithUser, WebTest): dotgov_result = dotgov_form.submit() # validate that data from this step are being saved application = DomainApplication.objects.get() # there's only one + print("dotgov result: ", dotgov_form) + print("application: ", application) + print("application requested domain: ", application.requested_domain) self.assertEqual(application.requested_domain.name, "city.gov") self.assertEqual( application.alternative_domains.filter(website="city1.gov").count(), 1 From 6d3f5bf5ef9d4c529feba90313608020e5df834b Mon Sep 17 00:00:00 2001 From: Rachid Mrad Date: Mon, 23 Oct 2023 13:08:59 -0400 Subject: [PATCH 081/128] Add a delete button on forms on Nameservers, refactor JS and DS/Key data for DRY code --- src/registrar/assets/js/get-gov.js | 89 +++++++------------ src/registrar/assets/sass/_theme/_forms.scss | 4 + src/registrar/templates/domain_dsdata.html | 4 +- src/registrar/templates/domain_keydata.html | 4 +- .../templates/domain_nameservers.html | 19 ++-- 5 files changed, 55 insertions(+), 65 deletions(-) diff --git a/src/registrar/assets/js/get-gov.js b/src/registrar/assets/js/get-gov.js index c21060382..aff3384eb 100644 --- a/src/registrar/assets/js/get-gov.js +++ b/src/registrar/assets/js/get-gov.js @@ -231,39 +231,11 @@ function handleValidationClick(e) { /** - * An IIFE that attaches a click handler for our dynamic nameservers form - * - * Only does something on a single page, but it should be fast enough to run - * it everywhere. + * Prepare the namerservers and DS data forms delete buttons + * We will call this on the forms init, and also every time we add a form + * */ -(function prepareNameserverForms() { - let serverForm = document.querySelectorAll(".server-form"); - let container = document.querySelector("#form-container"); - let addButton = document.querySelector("#add-nameserver-form"); - let totalForms = document.querySelector("#id_form-TOTAL_FORMS"); - - let formNum = serverForm.length-1; - if (addButton) - addButton.addEventListener('click', addForm); - - function addForm(e){ - let newForm = serverForm[2].cloneNode(true); - let formNumberRegex = RegExp(`form-(\\d){1}-`,'g'); - let formLabelRegex = RegExp(`Name server (\\d){1}`, 'g'); - let formExampleRegex = RegExp(`ns(\\d){1}`, 'g'); - - formNum++; - newForm.innerHTML = newForm.innerHTML.replace(formNumberRegex, `form-${formNum}-`); - newForm.innerHTML = newForm.innerHTML.replace(formLabelRegex, `Name server ${formNum+1}`); - newForm.innerHTML = newForm.innerHTML.replace(formExampleRegex, `ns${formNum+1}`); - container.insertBefore(newForm, addButton); - newForm.querySelector("input").value = ""; - - totalForms.setAttribute('value', `${formNum+1}`); - } -})(); - -function prepareDeleteButtons() { +function prepareDeleteButtons(formLabel) { let deleteButtons = document.querySelectorAll(".delete-record"); let totalForms = document.querySelector("#id_form-TOTAL_FORMS"); @@ -273,13 +245,13 @@ function prepareDeleteButtons() { }); function removeForm(e){ - let formToRemove = e.target.closest(".ds-record"); + let formToRemove = e.target.closest(".repeatable-form"); formToRemove.remove(); - let forms = document.querySelectorAll(".ds-record"); + let forms = document.querySelectorAll(".repeatable-form"); totalForms.setAttribute('value', `${forms.length}`); let formNumberRegex = RegExp(`form-(\\d){1}-`, 'g'); - let formLabelRegex = RegExp(`DS Data record (\\d){1}`, 'g'); + let formLabelRegex = RegExp(`${formLabel} (\\d){1}`, 'g'); forms.forEach((form, index) => { // Iterate over child nodes of the current element @@ -294,8 +266,9 @@ function prepareDeleteButtons() { }); }); - Array.from(form.querySelectorAll('h2, legend')).forEach((node) => { - node.textContent = node.textContent.replace(formLabelRegex, `DS Data record ${index + 1}`); + // h2 and legend for DS form, label for nameservers + Array.from(form.querySelectorAll('h2, legend, label')).forEach((node) => { + node.textContent = node.textContent.replace(formLabelRegex, `${formLabel} ${index + 1}`); }); }); @@ -303,39 +276,44 @@ function prepareDeleteButtons() { } /** - * An IIFE that attaches a click handler for our dynamic DNSSEC forms + * An IIFE that attaches a click handler for our dynamic formsets * + * Only does something on a few pages, but it should be fast enough to run + * it everywhere. */ -(function prepareDNSSECForms() { - let serverForm = document.querySelectorAll(".ds-record"); +(function prepareFormsetsForms() { + let repeatableForm = document.querySelectorAll(".repeatable-form"); let container = document.querySelector("#form-container"); - let addButton = document.querySelector("#add-ds-form"); + let addButton = document.querySelector("#add-form"); let totalForms = document.querySelector("#id_form-TOTAL_FORMS"); + let cloneIndex = 0; + let formLabel = ''; + if (document.title.includes("DNS name servers |")) { + cloneIndex = 2; + formLabel = "Name server"; + } else if ((document.title.includes("DS Data |")) || (document.title.includes("Key Data |"))) { + formLabel = "DS Data record"; + } // Attach click event listener on the delete buttons of the existing forms - prepareDeleteButtons(); + prepareDeleteButtons(formLabel); - // Attack click event listener on the add button if (addButton) addButton.addEventListener('click', addForm); - /* - * Add a formset to the end of the form. - * For each element in the added formset, name the elements with the prefix, - * form-{#}-{element_name} where # is the index of the formset and element_name - * is the element's name. - * Additionally, update the form element's metadata, including totalForms' value. - */ function addForm(e){ - let forms = document.querySelectorAll(".ds-record"); + let forms = document.querySelectorAll(".repeatable-form"); let formNum = forms.length; - let newForm = serverForm[0].cloneNode(true); + let newForm = repeatableForm[cloneIndex].cloneNode(true); let formNumberRegex = RegExp(`form-(\\d){1}-`,'g'); - let formLabelRegex = RegExp(`DS Data record (\\d){1}`, 'g'); + let formLabelRegex = RegExp(`${formLabel} (\\d){1}`, 'g'); + // For the eample on Nameservers + let formExampleRegex = RegExp(`ns(\\d){1}`, 'g'); formNum++; newForm.innerHTML = newForm.innerHTML.replace(formNumberRegex, `form-${formNum-1}-`); - newForm.innerHTML = newForm.innerHTML.replace(formLabelRegex, `DS Data record ${formNum}`); + newForm.innerHTML = newForm.innerHTML.replace(formLabelRegex, `${formLabel} ${formNum}`); + newForm.innerHTML = newForm.innerHTML.replace(formExampleRegex, `ns${formNum}`); container.insertBefore(newForm, addButton); let inputs = newForm.querySelectorAll("input"); @@ -379,7 +357,6 @@ function prepareDeleteButtons() { totalForms.setAttribute('value', `${formNum}`); // Attach click event listener on the delete buttons of the new form - prepareDeleteButtons(); + prepareDeleteButtons(formLabel); } - })(); diff --git a/src/registrar/assets/sass/_theme/_forms.scss b/src/registrar/assets/sass/_theme/_forms.scss index ed118bb94..529fb8245 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-bottom-075 { + margin-bottom: units(1.5); +} + .usa-form--extra-large { max-width: none; } diff --git a/src/registrar/templates/domain_dsdata.html b/src/registrar/templates/domain_dsdata.html index ca4dce783..086d602e7 100644 --- a/src/registrar/templates/domain_dsdata.html +++ b/src/registrar/templates/domain_dsdata.html @@ -52,7 +52,7 @@ {{ formset.management_form }} {% for form in formset %} -
+
DS Data record {{forloop.counter}} @@ -97,7 +97,7 @@
{% endfor %} - + {% endif %} +
{% endfor %} - - + {% comment %} Work around USWDS' button margins to add some spacing between the submit and the 'add more' + This solution still works when we remove the 'add more' at 13 forms {% endcomment %} +
+ +
From 5ae8ad6a5ffba512eacd84bd964199fab5a12ab2 Mon Sep 17 00:00:00 2001 From: Erin <121973038+erinysong@users.noreply.github.com> Date: Mon, 23 Oct 2023 13:03:35 -0700 Subject: [PATCH 083/128] Inherit MockEppLib to DomainApplicationTests --- src/registrar/tests/test_views.py | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/src/registrar/tests/test_views.py b/src/registrar/tests/test_views.py index 7250a9d8c..ae8386c5c 100644 --- a/src/registrar/tests/test_views.py +++ b/src/registrar/tests/test_views.py @@ -118,7 +118,7 @@ class LoggedInTests(TestWithUser): self.assertEqual(response.status_code, 403) -class DomainApplicationTests(TestWithUser, WebTest): +class DomainApplicationTests(TestWithUser, WebTest, MockEppLib): """Webtests for domain application to test filling and submitting.""" @@ -291,9 +291,6 @@ class DomainApplicationTests(TestWithUser, WebTest): dotgov_result = dotgov_form.submit() # validate that data from this step are being saved application = DomainApplication.objects.get() # there's only one - print("dotgov result: ", dotgov_form) - print("application: ", application) - print("application requested domain: ", application.requested_domain) self.assertEqual(application.requested_domain.name, "city.gov") self.assertEqual( application.alternative_domains.filter(website="city1.gov").count(), 1 From 67433d45024daa493d957f0826e3c3544783739b Mon Sep 17 00:00:00 2001 From: Erin <121973038+erinysong@users.noreply.github.com> Date: Mon, 23 Oct 2023 14:00:55 -0700 Subject: [PATCH 084/128] Add city.gov test cases to Check Domain mocks --- src/registrar/tests/common.py | 4 ++++ src/registrar/tests/test_views.py | 5 ++++- 2 files changed, 8 insertions(+), 1 deletion(-) diff --git a/src/registrar/tests/common.py b/src/registrar/tests/common.py index 53d15a91c..596b551e4 100644 --- a/src/registrar/tests/common.py +++ b/src/registrar/tests/common.py @@ -841,6 +841,10 @@ class MockEppLib(TestCase): return self._mockDomainName("igorvilleremixed.gov", True) elif "top-level-agency.gov" in getattr(_request, "names", None): return self._mockDomainName("top-level-agency.gov", True) + elif "city.gov" in getattr(_request, "names", None): + return self._mockDomainName("city.gov", True) + elif "city1.gov" in getattr(_request, "names", None): + return self._mockDomainName("city1.gov", True) elif "errordomain.gov" in getattr(_request, "names", None): raise RegistryError("Registry cannot find domain availability.") else: diff --git a/src/registrar/tests/test_views.py b/src/registrar/tests/test_views.py index ae8386c5c..b89fb519e 100644 --- a/src/registrar/tests/test_views.py +++ b/src/registrar/tests/test_views.py @@ -118,7 +118,7 @@ class LoggedInTests(TestWithUser): self.assertEqual(response.status_code, 403) -class DomainApplicationTests(TestWithUser, WebTest, MockEppLib): +class DomainApplicationTests(TestWithUser, WebTest): """Webtests for domain application to test filling and submitting.""" @@ -291,6 +291,9 @@ class DomainApplicationTests(TestWithUser, WebTest, MockEppLib): dotgov_result = dotgov_form.submit() # validate that data from this step are being saved application = DomainApplication.objects.get() # there's only one + # print("application: ", application) + print("application requested domains ", application.requested_domain) + print("application alternative domains ", application.alternative_domains) self.assertEqual(application.requested_domain.name, "city.gov") self.assertEqual( application.alternative_domains.filter(website="city1.gov").count(), 1 From 421603df2c47bbefb3a96da252af1645e7058f25 Mon Sep 17 00:00:00 2001 From: Erin <121973038+erinysong@users.noreply.github.com> Date: Mon, 23 Oct 2023 14:04:15 -0700 Subject: [PATCH 085/128] Remove test print statements --- src/registrar/tests/test_views.py | 3 --- 1 file changed, 3 deletions(-) diff --git a/src/registrar/tests/test_views.py b/src/registrar/tests/test_views.py index b89fb519e..7cc616889 100644 --- a/src/registrar/tests/test_views.py +++ b/src/registrar/tests/test_views.py @@ -291,9 +291,6 @@ class DomainApplicationTests(TestWithUser, WebTest): dotgov_result = dotgov_form.submit() # validate that data from this step are being saved application = DomainApplication.objects.get() # there's only one - # print("application: ", application) - print("application requested domains ", application.requested_domain) - print("application alternative domains ", application.alternative_domains) self.assertEqual(application.requested_domain.name, "city.gov") self.assertEqual( application.alternative_domains.filter(website="city1.gov").count(), 1 From ea567192d043ab995ca5ed0e0bcbca4211c31e95 Mon Sep 17 00:00:00 2001 From: Rebecca Hsieh Date: Mon, 23 Oct 2023 14:10:43 -0700 Subject: [PATCH 086/128] Convert bracket to parenthesis --- src/registrar/models/domain.py | 6 +----- 1 file changed, 1 insertion(+), 5 deletions(-) diff --git a/src/registrar/models/domain.py b/src/registrar/models/domain.py index ab0ad4b7b..c97bf1318 100644 --- a/src/registrar/models/domain.py +++ b/src/registrar/models/domain.py @@ -2,7 +2,6 @@ from itertools import zip_longest import logging import ipaddress import re -import string from datetime import date from string import digits from typing import Optional @@ -260,10 +259,8 @@ class Domain(TimeStampedModel, DomainHelper): for host in hosts: host_info = host["name"] if len(host["addrs"]) > 0: - converter = string.maketrans("[]", "()") - host_info.append(host["addrs"].translate(converter, "'")) + hostList.append(tuple(host["addrs"])) hostList.append(host_info) - return hostList @Cache @@ -291,7 +288,6 @@ class Domain(TimeStampedModel, DomainHelper): hostList = [] for host in hosts: hostList.append((host["name"], host["addrs"])) - return hostList def _create_host(self, host, addrs): From 9c939d4a84280f19821d9f907dbd5c3396d8a74d Mon Sep 17 00:00:00 2001 From: Erin <121973038+erinysong@users.noreply.github.com> Date: Mon, 23 Oct 2023 15:20:34 -0700 Subject: [PATCH 087/128] Reformat DNS name servers --- src/registrar/models/domain.py | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/src/registrar/models/domain.py b/src/registrar/models/domain.py index c97bf1318..ce5198756 100644 --- a/src/registrar/models/domain.py +++ b/src/registrar/models/domain.py @@ -257,10 +257,8 @@ class Domain(TimeStampedModel, DomainHelper): hostList = [] for host in hosts: - host_info = host["name"] - if len(host["addrs"]) > 0: - hostList.append(tuple(host["addrs"])) - hostList.append(host_info) + host_info_str = f'{host["name"]} {host["addrs"] if len(host["addrs"]) > 0 else ""}' + hostList.append(host_info_str) return hostList @Cache From 0dbcb749a6c754a9fe724dcd36a6e2496473b8d5 Mon Sep 17 00:00:00 2001 From: David Kennedy Date: Mon, 23 Oct 2023 21:47:23 -0400 Subject: [PATCH 088/128] fixed cancel button on nameservers --- .../templates/domain_nameservers.html | 18 ++++++++---------- src/registrar/views/domain.py | 4 +++- 2 files changed, 11 insertions(+), 11 deletions(-) diff --git a/src/registrar/templates/domain_nameservers.html b/src/registrar/templates/domain_nameservers.html index f03570218..eebba921d 100644 --- a/src/registrar/templates/domain_nameservers.html +++ b/src/registrar/templates/domain_nameservers.html @@ -76,16 +76,14 @@ class="usa-button" >Save + + - -
- -
{% endblock %} {# domain_content #} diff --git a/src/registrar/views/domain.py b/src/registrar/views/domain.py index 4d977891d..36ac74f94 100644 --- a/src/registrar/views/domain.py +++ b/src/registrar/views/domain.py @@ -10,6 +10,7 @@ import logging from django.contrib import messages 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 @@ -272,7 +273,8 @@ class DomainNameserversView(DomainFormBaseView, BaseFormSet): formset = self.get_form() if "btn-cancel-click" in request.POST: - return redirect("/", {"formset": formset}, RequestContext(request)) + url = self.get_success_url() + return HttpResponseRedirect(url) if formset.is_valid(): return self.form_valid(formset) From 293eb40cffb31a1749d843919afe5adeb5b320fb Mon Sep 17 00:00:00 2001 From: David Kennedy Date: Mon, 23 Oct 2023 22:10:03 -0400 Subject: [PATCH 089/128] removed some logging; fixed some existing unit tests --- src/registrar/models/domain.py | 2 -- src/registrar/tests/common.py | 4 ++-- src/registrar/tests/test_models_domain.py | 2 +- 3 files changed, 3 insertions(+), 5 deletions(-) diff --git a/src/registrar/models/domain.py b/src/registrar/models/domain.py index 9c0195345..eda24fbaa 100644 --- a/src/registrar/models/domain.py +++ b/src/registrar/models/domain.py @@ -324,7 +324,6 @@ class Domain(TimeStampedModel, DomainHelper): ) elif ip is not None and ip != [] and ip != ['']: for addr in ip: - logger.info(f"ip address {addr}") if not cls._valid_ip_addr(addr): raise NameserverError( code=nsErrorCodes.INVALID_IP, nameserver=nameserver, ip=ip @@ -339,7 +338,6 @@ class Domain(TimeStampedModel, DomainHelper): isValid (boolean)-True for valid ip address""" try: ip = ipaddress.ip_address(ipToTest) - logger.info(ip.version) return ip.version == 6 or ip.version == 4 except ValueError: diff --git a/src/registrar/tests/common.py b/src/registrar/tests/common.py index 8cd5fd6ba..43554241c 100644 --- a/src/registrar/tests/common.py +++ b/src/registrar/tests/common.py @@ -755,7 +755,7 @@ class MockEppLib(TestCase): mockDataInfoHosts = fakedEppObject( "lastPw", cr_date=datetime.datetime(2023, 8, 25, 19, 45, 35), - addrs=["1.2.3.4", "2.3.4.5"], + addrs=[common.Ip(addr="1.2.3.4"), common.Ip(addr="2.3.4.5")], ) mockDataHostChange = fakedEppObject( @@ -810,7 +810,7 @@ class MockEppLib(TestCase): "ns2.nameserverwithip.gov", "ns3.nameserverwithip.gov", ], - addrs=["1.2.3.4", "2.3.4.5"], + addrs=[common.Ip(addr="1.2.3.4"), common.Ip(addr="2.3.4.5")], ) infoDomainCheckHostIPCombo = fakedEppObject( diff --git a/src/registrar/tests/test_models_domain.py b/src/registrar/tests/test_models_domain.py index 5759df1be..29bf58a2a 100644 --- a/src/registrar/tests/test_models_domain.py +++ b/src/registrar/tests/test_models_domain.py @@ -107,7 +107,7 @@ class TestDomainCache(MockEppLib): } expectedHostsDict = { "name": self.mockDataInfoDomain.hosts[0], - "addrs": self.mockDataInfoHosts.addrs, + "addrs": [item.addr for item in self.mockDataInfoHosts.addrs], "cr_date": self.mockDataInfoHosts.cr_date, } From 2bf7847654a02bba7ad313965eae6aba946b353e Mon Sep 17 00:00:00 2001 From: David Kennedy Date: Tue, 24 Oct 2023 08:15:04 -0400 Subject: [PATCH 090/128] consolidation of nameserver view error messages --- src/registrar/forms/domain.py | 12 +++++++++--- src/registrar/utility/errors.py | 12 ++++++++---- src/registrar/views/domain.py | 4 ++-- 3 files changed, 19 insertions(+), 9 deletions(-) diff --git a/src/registrar/forms/domain.py b/src/registrar/forms/domain.py index a8a285e99..3c70ab41e 100644 --- a/src/registrar/forms/domain.py +++ b/src/registrar/forms/domain.py @@ -75,7 +75,7 @@ class DomainNameserverForm(forms.Form): ip_list = [ip.strip() for ip in ip.split(",")] if not server and len(ip_list) > 0: # If 'server' is empty, disallow 'ip' input - self.add_error('server', "Name server must be provided to enter IP address.") + self.add_error('server', NameserverError(code=nsErrorCodes.MISSING_HOST)) # if there's a nameserver and an ip, validate nameserver/ip combo @@ -88,9 +88,15 @@ class DomainNameserverForm(forms.Form): Domain.checkHostIPCombo(domain, server, ip_list) except NameserverError as e: if e.code == nsErrorCodes.GLUE_RECORD_NOT_ALLOWED: - self.add_error('server', "Name server address does not match domain name") + self.add_error( + 'server', + NameserverError(code=nsErrorCodes.GLUE_RECORD_NOT_ALLOWED) + ) elif e.code == nsErrorCodes.MISSING_IP: - self.add_error('ip', "Subdomains require an IP address") + self.add_error( + 'ip', + NameserverError(code=nsErrorCodes.MISSING_IP) + ) else: self.add_error('ip', str(e)) diff --git a/src/registrar/utility/errors.py b/src/registrar/utility/errors.py index f7bc743d6..c4bbc86c9 100644 --- a/src/registrar/utility/errors.py +++ b/src/registrar/utility/errors.py @@ -36,6 +36,7 @@ class NameserverErrorCodes(IntEnum): INVALID_IP = 3 TOO_MANY_HOSTS = 4 UNABLE_TO_UPDATE_DOMAIN = 5 + MISSING_HOST = 6 class NameserverError(Exception): @@ -45,10 +46,10 @@ class NameserverError(Exception): """ _error_mapping = { - NameserverErrorCodes.MISSING_IP: "Nameserver {} needs to have an " - "IP address because it is a subdomain", - NameserverErrorCodes.GLUE_RECORD_NOT_ALLOWED: "Nameserver {} cannot be linked " - "because it is not a subdomain", + NameserverErrorCodes.MISSING_IP: "Subdomains require an IP address", + NameserverErrorCodes.GLUE_RECORD_NOT_ALLOWED: ( + "Name server address does not match domain name" + ), NameserverErrorCodes.INVALID_IP: "Nameserver {} has an invalid IP address: {}", NameserverErrorCodes.TOO_MANY_HOSTS: ( "Too many hosts provided, you may not have more than 13 nameservers." @@ -57,6 +58,9 @@ class NameserverError(Exception): "Unable to update domain, changes were not applied." "Check logs as a Registry Error is the likely cause" ), + NameserverErrorCodes.MISSING_HOST: ( + "Name server must be provided to enter IP address." + ), } def __init__(self, *args, code=None, nameserver=None, ip=None, **kwargs): diff --git a/src/registrar/views/domain.py b/src/registrar/views/domain.py index 639b83dfe..7583b062e 100644 --- a/src/registrar/views/domain.py +++ b/src/registrar/views/domain.py @@ -318,10 +318,10 @@ class DomainNameserversView(DomainFormBaseView, BaseFormSet): # TODO: merge 1103 and use literals except RegistryError as Err: if Err.is_connection_error(): - messages.error(self.request, 'CANNOT_CONTACT_REGISTRY') + messages.error(self.request, CANNOT_CONTACT_REGISTRY) logger.error(f"Registry connection error: {Err}") else: - messages.error(self.request, 'GENERIC_ERROR') + messages.error(self.request, GENERIC_ERROR) logger.error(f"Registry error: {Err}") else: messages.success( From a7ba33b52364107ac983a244906b5b8b1dd103f1 Mon Sep 17 00:00:00 2001 From: David Kennedy Date: Tue, 24 Oct 2023 08:25:18 -0400 Subject: [PATCH 091/128] fixed broken tests --- src/registrar/tests/test_nameserver_error.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/src/registrar/tests/test_nameserver_error.py b/src/registrar/tests/test_nameserver_error.py index c64717eb5..1e87d48e1 100644 --- a/src/registrar/tests/test_nameserver_error.py +++ b/src/registrar/tests/test_nameserver_error.py @@ -11,8 +11,7 @@ class TestNameserverError(TestCase): """Test NameserverError when no ip address is passed""" nameserver = "nameserver val" expected = ( - f"Nameserver {nameserver} needs to have an " - "IP address because it is a subdomain" + "Subdomains require an IP address" ) nsException = NameserverError( From d532449b3d6c2adf6dc3fd36a60637108c9291b3 Mon Sep 17 00:00:00 2001 From: David Kennedy Date: Tue, 24 Oct 2023 09:37:43 -0400 Subject: [PATCH 092/128] cleaned up test cases and formatting --- src/registrar/forms/domain.py | 50 +++++++------------- src/registrar/models/domain.py | 23 ++++++--- src/registrar/tests/test_nameserver_error.py | 4 +- src/registrar/views/domain.py | 39 ++++++++------- 4 files changed, 55 insertions(+), 61 deletions(-) diff --git a/src/registrar/forms/domain.py b/src/registrar/forms/domain.py index 3c70ab41e..516e0abd2 100644 --- a/src/registrar/forms/domain.py +++ b/src/registrar/forms/domain.py @@ -7,7 +7,7 @@ from django.forms import formset_factory from phonenumber_field.widgets import RegionalPhoneNumberWidget from registrar.utility.errors import ( NameserverError, - NameserverErrorCodes as nsErrorCodes + NameserverErrorCodes as nsErrorCodes, ) from ..models import Contact, DomainInformation, Domain @@ -26,26 +26,9 @@ class DomainAddUserForm(forms.Form): class IPAddressField(forms.CharField): - - - - def validate(self, value): + def validate(self, value): super().validate(value) # Run the default CharField validation - # ip_list = [ip.strip() for ip in value.split(",")] # Split IPs and remove whitespace - - # # TODO: pass hostname from view? - - # hostname = self.form.cleaned_data.get("server", "") - - # print(f"hostname {hostname}") - - # # Call the IP validation method from Domain - # try: - # Domain.checkHostIPCombo(hostname, ip_list) - # except NameserverError as e: - # raise forms.ValidationError(str(e)) - class DomainNameserverForm(forms.Form): """Form for changing nameservers.""" @@ -62,12 +45,12 @@ class DomainNameserverForm(forms.Form): # django.core.validators.validate_ipv46_address # ], ) - + def clean(self): cleaned_data = super().clean() - server = cleaned_data.get('server', '') - ip = cleaned_data.get('ip', '') - domain = cleaned_data.get('domain', '') + server = cleaned_data.get("server", "") + ip = cleaned_data.get("ip", "") + domain = cleaned_data.get("domain", "") print(f"clean is called on {domain} {server}") # make sure there's a nameserver if an ip is passed @@ -75,30 +58,29 @@ class DomainNameserverForm(forms.Form): ip_list = [ip.strip() for ip in ip.split(",")] if not server and len(ip_list) > 0: # If 'server' is empty, disallow 'ip' input - self.add_error('server', NameserverError(code=nsErrorCodes.MISSING_HOST)) - + self.add_error( + "server", NameserverError(code=nsErrorCodes.MISSING_HOST) + ) + # if there's a nameserver and an ip, validate nameserver/ip combo - + if server: if ip: ip_list = [ip.strip() for ip in ip.split(",")] else: - ip_list = [''] + ip_list = [""] try: Domain.checkHostIPCombo(domain, server, ip_list) except NameserverError as e: if e.code == nsErrorCodes.GLUE_RECORD_NOT_ALLOWED: self.add_error( - 'server', - NameserverError(code=nsErrorCodes.GLUE_RECORD_NOT_ALLOWED) + "server", + NameserverError(code=nsErrorCodes.GLUE_RECORD_NOT_ALLOWED), ) elif e.code == nsErrorCodes.MISSING_IP: - self.add_error( - 'ip', - NameserverError(code=nsErrorCodes.MISSING_IP) - ) + self.add_error("ip", NameserverError(code=nsErrorCodes.MISSING_IP)) else: - self.add_error('ip', str(e)) + self.add_error("ip", str(e)) return cleaned_data diff --git a/src/registrar/models/domain.py b/src/registrar/models/domain.py index eda24fbaa..d82e0b20b 100644 --- a/src/registrar/models/domain.py +++ b/src/registrar/models/domain.py @@ -314,15 +314,16 @@ class Domain(TimeStampedModel, DomainHelper): NameserverError (if exception hit) Returns: None""" - if cls.isSubdomain(name, nameserver) and (ip is None or ip == [] or ip == ['']): - + if cls.isSubdomain(name, nameserver) and (ip is None or ip == [] or ip == [""]): raise NameserverError(code=nsErrorCodes.MISSING_IP, nameserver=nameserver) - elif not cls.isSubdomain(name, nameserver) and (ip is not None and ip != [] and ip != ['']): + elif not cls.isSubdomain(name, nameserver) and ( + ip is not None and ip != [] and ip != [""] + ): raise NameserverError( code=nsErrorCodes.GLUE_RECORD_NOT_ALLOWED, nameserver=nameserver, ip=ip ) - elif ip is not None and ip != [] and ip != ['']: + elif ip is not None and ip != [] and ip != [""]: for addr in ip: if not cls._valid_ip_addr(addr): raise NameserverError( @@ -384,7 +385,9 @@ class Domain(TimeStampedModel, DomainHelper): if newHostDict[prevHost] is not None and set( newHostDict[prevHost] ) != set(addrs): - self.__class__.checkHostIPCombo(name=self.name, nameserver=prevHost, ip=newHostDict[prevHost]) + self.__class__.checkHostIPCombo( + name=self.name, nameserver=prevHost, ip=newHostDict[prevHost] + ) updated_values.append((prevHost, newHostDict[prevHost])) new_values = { @@ -394,7 +397,9 @@ class Domain(TimeStampedModel, DomainHelper): } for nameserver, ip in new_values.items(): - self.__class__.checkHostIPCombo(name=self.name, nameserver=nameserver, ip=ip) + self.__class__.checkHostIPCombo( + name=self.name, nameserver=nameserver, ip=ip + ) return (deleted_values, updated_values, new_values, previousHostDict) @@ -605,7 +610,11 @@ class Domain(TimeStampedModel, DomainHelper): if len(hosts) > 13: raise NameserverError(code=nsErrorCodes.TOO_MANY_HOSTS) - if self.state not in [self.State.DNS_NEEDED, self.State.READY, self.State.UNKNOWN]: + if self.state not in [ + self.State.DNS_NEEDED, + self.State.READY, + self.State.UNKNOWN, + ]: raise ActionNotAllowed("Nameservers can not be " "set in the current state") logger.info("Setting nameservers") diff --git a/src/registrar/tests/test_nameserver_error.py b/src/registrar/tests/test_nameserver_error.py index 1e87d48e1..218a48231 100644 --- a/src/registrar/tests/test_nameserver_error.py +++ b/src/registrar/tests/test_nameserver_error.py @@ -10,9 +10,7 @@ class TestNameserverError(TestCase): def test_with_no_ip(self): """Test NameserverError when no ip address is passed""" nameserver = "nameserver val" - expected = ( - "Subdomains require an IP address" - ) + expected = "Subdomains require an IP address" nsException = NameserverError( code=nsErrorCodes.MISSING_IP, nameserver=nameserver diff --git a/src/registrar/views/domain.py b/src/registrar/views/domain.py index 7583b062e..01c31e9bc 100644 --- a/src/registrar/views/domain.py +++ b/src/registrar/views/domain.py @@ -15,7 +15,6 @@ from django.shortcuts import redirect from django.template import RequestContext from django.urls import reverse from django.views.generic.edit import FormMixin -from django.forms import BaseFormSet from registrar.models import ( Domain, @@ -215,13 +214,13 @@ class DomainDNSView(DomainBaseView): template_name = "domain_dns.html" -class DomainNameserversView(DomainFormBaseView, BaseFormSet): +class DomainNameserversView(DomainFormBaseView): """Domain nameserver editing view.""" template_name = "domain_nameservers.html" form_class = NameserverFormset model = Domain - + def get_initial(self): """The initial value for the form (which is a formset here).""" nameservers = self.object.nameservers @@ -229,7 +228,9 @@ class DomainNameserversView(DomainFormBaseView, BaseFormSet): if nameservers is not None: # Add existing nameservers as initial data - initial_data.extend({"server": name, "ip": ','.join(ip)} for name, ip in nameservers) + initial_data.extend( + {"server": name, "ip": ",".join(ip)} for name, ip in nameservers + ) # Ensure at least 3 fields, filled or empty while len(initial_data) < 2: @@ -251,8 +252,8 @@ class DomainNameserversView(DomainFormBaseView, BaseFormSet): def get_form(self, **kwargs): """Override the labels and required fields every time we get a formset.""" # kwargs.update({"domain", self.object}) - formset = super().get_form(**kwargs) - + formset = super().get_form(**kwargs) + for i, form in enumerate(formset): # form = self.get_form(self, **kwargs) form.fields["server"].label += f" {i+1}" @@ -263,7 +264,7 @@ class DomainNameserversView(DomainFormBaseView, BaseFormSet): form.fields["domain"].initial = self.object.name print(f"domain in get_form {self.object.name}") return formset - + def post(self, request, *args, **kwargs): """Form submission posts to this view. @@ -271,33 +272,33 @@ class DomainNameserversView(DomainFormBaseView, BaseFormSet): """ self._get_domain(request) formset = self.get_form() - + if "btn-cancel-click" in request.POST: url = self.get_success_url() return HttpResponseRedirect(url) - + 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.""" - self.request.session['nameservers_form_domain'] = self.object - + self.request.session["nameservers_form_domain"] = self.object + # Set the nameservers from the formset nameservers = [] for form in formset: try: ip_string = form.cleaned_data["ip"] # Split the string into a list using a comma as the delimiter - ip_list = ip_string.split(',') + ip_list = ip_string.split(",") # Remove any leading or trailing whitespace from each IP in the list # this will return [''] if no ips have been entered, which is taken # into account in the model in checkHostIPCombo ip_list = [ip.strip() for ip in ip_list] - + as_tuple = ( form.cleaned_data["server"], ip_list, @@ -306,12 +307,12 @@ class DomainNameserversView(DomainFormBaseView, BaseFormSet): except KeyError: # no server information in this field, skip it pass - + try: self.object.nameservers = nameservers except NameserverError as Err: # TODO: move into literal - messages.error(self.request, 'Whoops, Nameservers Error') + messages.error(self.request, "Whoops, Nameservers Error") # messages.error(self.request, GENERIC_ERROR) logger.error(f"Nameservers error: {Err}") # TODO: registry is not throwing an error when no connection @@ -325,7 +326,11 @@ class DomainNameserversView(DomainFormBaseView, BaseFormSet): logger.error(f"Registry error: {Err}") else: messages.success( - self.request, "The name servers for this domain have been updated. Keep in mind that DNS changes may take some time to propagate across the internet. It can take anywhere from a few minutes to 48 hours for your changes to take place." + self.request, + "The name servers for this domain have been updated. " + "Keep in mind that DNS changes may take some time to " + "propagate across the internet. It can take anywhere " + "from a few minutes to 48 hours for your changes to take place.", ) # superclass has the redirect From 182d1a61c0347caba0ffc31f48c24721876f7c7d Mon Sep 17 00:00:00 2001 From: David Kennedy Date: Tue, 24 Oct 2023 13:02:02 -0400 Subject: [PATCH 093/128] more consolidation of error messages and their text --- src/epplibwrapper/__init__.py | 4 +-- src/epplibwrapper/errors.py | 3 -- src/registrar/forms/domain.py | 15 ++++++-- src/registrar/utility/errors.py | 37 +++++++++++++++++++ src/registrar/views/domain.py | 63 ++++++++++++++++++++++++++------- 5 files changed, 101 insertions(+), 21 deletions(-) diff --git a/src/epplibwrapper/__init__.py b/src/epplibwrapper/__init__.py index d0138d73c..dd6664a3a 100644 --- a/src/epplibwrapper/__init__.py +++ b/src/epplibwrapper/__init__.py @@ -45,7 +45,7 @@ except NameError: # Attn: these imports should NOT be at the top of the file try: from .client import CLIENT, commands - from .errors import RegistryError, ErrorCode, CANNOT_CONTACT_REGISTRY, GENERIC_ERROR + from .errors import RegistryError, ErrorCode from epplib.models import common, info from epplib.responses import extensions from epplib import responses @@ -61,6 +61,4 @@ __all__ = [ "info", "ErrorCode", "RegistryError", - "CANNOT_CONTACT_REGISTRY", - "GENERIC_ERROR", ] diff --git a/src/epplibwrapper/errors.py b/src/epplibwrapper/errors.py index dba5f328c..d34ed5e91 100644 --- a/src/epplibwrapper/errors.py +++ b/src/epplibwrapper/errors.py @@ -1,8 +1,5 @@ from enum import IntEnum -CANNOT_CONTACT_REGISTRY = "Update failed. Cannot contact the registry." -GENERIC_ERROR = "Value entered was wrong." - class ErrorCode(IntEnum): """ diff --git a/src/registrar/forms/domain.py b/src/registrar/forms/domain.py index 516e0abd2..93d42e53d 100644 --- a/src/registrar/forms/domain.py +++ b/src/registrar/forms/domain.py @@ -75,10 +75,21 @@ class DomainNameserverForm(forms.Form): if e.code == nsErrorCodes.GLUE_RECORD_NOT_ALLOWED: self.add_error( "server", - NameserverError(code=nsErrorCodes.GLUE_RECORD_NOT_ALLOWED), + NameserverError( + code=nsErrorCodes.GLUE_RECORD_NOT_ALLOWED, + nameserver=domain, + ip=ip_list + ), ) elif e.code == nsErrorCodes.MISSING_IP: - self.add_error("ip", NameserverError(code=nsErrorCodes.MISSING_IP)) + self.add_error( + "ip", + NameserverError( + code=nsErrorCodes.MISSING_IP, + nameserver=domain, + ip=ip_list, + ) + ) else: self.add_error("ip", str(e)) diff --git a/src/registrar/utility/errors.py b/src/registrar/utility/errors.py index c4bbc86c9..1f658b341 100644 --- a/src/registrar/utility/errors.py +++ b/src/registrar/utility/errors.py @@ -20,6 +20,41 @@ class ActionNotAllowed(Exception): pass +class GenericErrorCodes(IntEnum): + """Used across the registrar for + error mapping. + Overview of generic error codes: + - 1 GENERIC_ERROR a generic value error + - 2 CANNOT_CONTACT_REGISTRY a connection error w registry + """ + + GENERIC_ERROR = 1 + CANNOT_CONTACT_REGISTRY = 2 + + +class GenericError(Exception): + """ + GenericError class used to raise exceptions across + the registrar + """ + + _error_mapping = { + GenericErrorCodes.CANNOT_CONTACT_REGISTRY: "Update failed. Cannot contact the registry.", + GenericErrorCodes.GENERIC_ERROR: ( + "Value entered was wrong." + ), + } + + 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}" + + class NameserverErrorCodes(IntEnum): """Used in the NameserverError class for error mapping. @@ -29,6 +64,8 @@ class NameserverErrorCodes(IntEnum): value but is not a subdomain - 3 INVALID_IP invalid ip address format or invalid version - 4 TOO_MANY_HOSTS more than the max allowed host values + - 5 UNABLE_TO_UPDATE_DOMAIN unable to update the domain + - 6 MISSING_HOST host is missing for a nameserver """ MISSING_IP = 1 diff --git a/src/registrar/views/domain.py b/src/registrar/views/domain.py index 01c31e9bc..b0f00f03a 100644 --- a/src/registrar/views/domain.py +++ b/src/registrar/views/domain.py @@ -24,7 +24,12 @@ from registrar.models import ( UserDomainRole, ) from registrar.models.public_contact import PublicContact -from registrar.utility.errors import NameserverError +from registrar.utility.errors import ( + GenericError, + GenericErrorCodes, + NameserverError, + NameserverErrorCodes as nsErrorCodes, +) from registrar.models.utility.contact_error import ContactError from ..forms import ( @@ -44,8 +49,6 @@ from epplibwrapper import ( common, extensions, RegistryError, - CANNOT_CONTACT_REGISTRY, - GENERIC_ERROR, ) from ..utility.email import send_templated_email, EmailSendingError @@ -311,18 +314,32 @@ class DomainNameserversView(DomainFormBaseView): try: self.object.nameservers = nameservers except NameserverError as Err: - # TODO: move into literal - messages.error(self.request, "Whoops, Nameservers Error") - # messages.error(self.request, GENERIC_ERROR) + # NamserverErrors *should* be caught in form; if reached here, + # there was an uncaught error in submission (through EPP) + messages.error( + self.request, + NameserverError( + code=nsErrorCodes.UNABLE_TO_UPDATE_DOMAIN + ) + ) logger.error(f"Nameservers error: {Err}") # TODO: registry is not throwing an error when no connection - # TODO: merge 1103 and use literals except RegistryError as Err: if Err.is_connection_error(): - messages.error(self.request, CANNOT_CONTACT_REGISTRY) + messages.error( + self.request, + GenericError( + code=GenericErrorCodes.CANNOT_CONTACT_REGISTRY + ) + ) logger.error(f"Registry connection error: {Err}") else: - messages.error(self.request, GENERIC_ERROR) + messages.error( + self.request, + GenericError( + code=GenericErrorCodes.GENERIC_ERROR + ) + ) logger.error(f"Registry error: {Err}") else: messages.success( @@ -688,7 +705,12 @@ class DomainSecurityEmailView(DomainFormBaseView): # If no default is created for security_contact, # then we cannot connect to the registry. if contact is None: - messages.error(self.request, CANNOT_CONTACT_REGISTRY) + messages.error( + self.request, + GenericError( + code=GenericErrorCodes.CANNOT_CONTACT_REGISTRY + ) + ) return redirect(self.get_success_url()) contact.email = new_email @@ -697,13 +719,28 @@ class DomainSecurityEmailView(DomainFormBaseView): contact.save() except RegistryError as Err: if Err.is_connection_error(): - messages.error(self.request, CANNOT_CONTACT_REGISTRY) + messages.error( + self.request, + GenericError( + code=GenericErrorCodes.CANNOT_CONTACT_REGISTRY + ) + ) logger.error(f"Registry connection error: {Err}") else: - messages.error(self.request, GENERIC_ERROR) + messages.error( + self.request, + GenericError( + code=GenericErrorCodes.GENERIC_ERROR + ) + ) logger.error(f"Registry error: {Err}") except ContactError as Err: - messages.error(self.request, GENERIC_ERROR) + messages.error( + self.request, + GenericError( + code=GenericErrorCodes.GENERIC_ERROR + ) + ) logger.error(f"Generic registry error: {Err}") else: messages.success( From 497e81f6fa50e04155c64afa7623917e6e4011a9 Mon Sep 17 00:00:00 2001 From: David Kennedy Date: Tue, 24 Oct 2023 15:07:12 -0400 Subject: [PATCH 094/128] formatting for linting --- src/registrar/forms/domain.py | 4 ++-- src/registrar/utility/errors.py | 6 +++--- src/registrar/views/domain.py | 32 +++++++------------------------- 3 files changed, 12 insertions(+), 30 deletions(-) diff --git a/src/registrar/forms/domain.py b/src/registrar/forms/domain.py index 93d42e53d..b9aaee566 100644 --- a/src/registrar/forms/domain.py +++ b/src/registrar/forms/domain.py @@ -78,7 +78,7 @@ class DomainNameserverForm(forms.Form): NameserverError( code=nsErrorCodes.GLUE_RECORD_NOT_ALLOWED, nameserver=domain, - ip=ip_list + ip=ip_list, ), ) elif e.code == nsErrorCodes.MISSING_IP: @@ -88,7 +88,7 @@ class DomainNameserverForm(forms.Form): code=nsErrorCodes.MISSING_IP, nameserver=domain, ip=ip_list, - ) + ), ) else: self.add_error("ip", str(e)) diff --git a/src/registrar/utility/errors.py b/src/registrar/utility/errors.py index 1f658b341..41caed837 100644 --- a/src/registrar/utility/errors.py +++ b/src/registrar/utility/errors.py @@ -39,10 +39,10 @@ class GenericError(Exception): """ _error_mapping = { - GenericErrorCodes.CANNOT_CONTACT_REGISTRY: "Update failed. Cannot contact the registry.", - GenericErrorCodes.GENERIC_ERROR: ( - "Value entered was wrong." + GenericErrorCodes.CANNOT_CONTACT_REGISTRY: ( + "Update failed. Cannot contact the registry." ), + GenericErrorCodes.GENERIC_ERROR: ("Value entered was wrong."), } def __init__(self, *args, code=None, **kwargs): diff --git a/src/registrar/views/domain.py b/src/registrar/views/domain.py index b0f00f03a..a2ca6d8cb 100644 --- a/src/registrar/views/domain.py +++ b/src/registrar/views/domain.py @@ -317,10 +317,7 @@ class DomainNameserversView(DomainFormBaseView): # NamserverErrors *should* be caught in form; if reached here, # there was an uncaught error in submission (through EPP) messages.error( - self.request, - NameserverError( - code=nsErrorCodes.UNABLE_TO_UPDATE_DOMAIN - ) + self.request, NameserverError(code=nsErrorCodes.UNABLE_TO_UPDATE_DOMAIN) ) logger.error(f"Nameservers error: {Err}") # TODO: registry is not throwing an error when no connection @@ -328,17 +325,12 @@ class DomainNameserversView(DomainFormBaseView): if Err.is_connection_error(): messages.error( self.request, - GenericError( - code=GenericErrorCodes.CANNOT_CONTACT_REGISTRY - ) + GenericError(code=GenericErrorCodes.CANNOT_CONTACT_REGISTRY), ) logger.error(f"Registry connection error: {Err}") else: messages.error( - self.request, - GenericError( - code=GenericErrorCodes.GENERIC_ERROR - ) + self.request, GenericError(code=GenericErrorCodes.GENERIC_ERROR) ) logger.error(f"Registry error: {Err}") else: @@ -707,9 +699,7 @@ class DomainSecurityEmailView(DomainFormBaseView): if contact is None: messages.error( self.request, - GenericError( - code=GenericErrorCodes.CANNOT_CONTACT_REGISTRY - ) + GenericError(code=GenericErrorCodes.CANNOT_CONTACT_REGISTRY), ) return redirect(self.get_success_url()) @@ -721,25 +711,17 @@ class DomainSecurityEmailView(DomainFormBaseView): if Err.is_connection_error(): messages.error( self.request, - GenericError( - code=GenericErrorCodes.CANNOT_CONTACT_REGISTRY - ) + GenericError(code=GenericErrorCodes.CANNOT_CONTACT_REGISTRY), ) logger.error(f"Registry connection error: {Err}") else: messages.error( - self.request, - GenericError( - code=GenericErrorCodes.GENERIC_ERROR - ) + self.request, GenericError(code=GenericErrorCodes.GENERIC_ERROR) ) logger.error(f"Registry error: {Err}") except ContactError as Err: messages.error( - self.request, - GenericError( - code=GenericErrorCodes.GENERIC_ERROR - ) + self.request, GenericError(code=GenericErrorCodes.GENERIC_ERROR) ) logger.error(f"Generic registry error: {Err}") else: 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 095/128] 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 46f03cd4e42afa070026ec877748be1c3e0fc0e3 Mon Sep 17 00:00:00 2001 From: Rachid Mrad Date: Tue, 24 Oct 2023 15:58:25 -0400 Subject: [PATCH 096/128] tweak error msg on invalid ip --- src/registrar/utility/errors.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/registrar/utility/errors.py b/src/registrar/utility/errors.py index 41caed837..5e28bc728 100644 --- a/src/registrar/utility/errors.py +++ b/src/registrar/utility/errors.py @@ -87,7 +87,7 @@ class NameserverError(Exception): NameserverErrorCodes.GLUE_RECORD_NOT_ALLOWED: ( "Name server address does not match domain name" ), - NameserverErrorCodes.INVALID_IP: "Nameserver {} has an invalid IP address: {}", + NameserverErrorCodes.INVALID_IP: "{}: Enter an IP address in the required format.", NameserverErrorCodes.TOO_MANY_HOSTS: ( "Too many hosts provided, you may not have more than 13 nameservers." ), @@ -106,7 +106,7 @@ class NameserverError(Exception): if self.code in self._error_mapping: self.message = self._error_mapping.get(self.code) if nameserver is not None and ip is not None: - self.message = self.message.format(str(nameserver), str(ip)) + self.message = self.message.format(str(nameserver)) elif nameserver is not None: self.message = self.message.format(str(nameserver)) elif ip is not None: From fbbde984090e05888e1c70f56c174c8dce9ad0a2 Mon Sep 17 00:00:00 2001 From: Erin <121973038+erinysong@users.noreply.github.com> Date: Tue, 24 Oct 2023 13:44:49 -0700 Subject: [PATCH 097/128] Format hosts to name (IP addresses) --- src/registrar/models/domain.py | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/src/registrar/models/domain.py b/src/registrar/models/domain.py index ce5198756..38afa28ee 100644 --- a/src/registrar/models/domain.py +++ b/src/registrar/models/domain.py @@ -257,7 +257,11 @@ class Domain(TimeStampedModel, DomainHelper): hostList = [] for host in hosts: - host_info_str = f'{host["name"]} {host["addrs"] if len(host["addrs"]) > 0 else ""}' + # can remove str conversion when EPP.IP address display changes from IP object -> IP address + host_addrs_stringified = [str(addr) for addr in host["addrs"]] + host_addrs_str = "" + host_addrs_str = f'({", ".join(obj for obj in host_addrs_stringified)})' + host_info_str = f'{host["name"]} {host_addrs_str if len(host["addrs"]) > 0 else ""}' hostList.append(host_info_str) return hostList From 61d6dda59f91ef42994fc263964fceed39af86cc Mon Sep 17 00:00:00 2001 From: Erin <121973038+erinysong@users.noreply.github.com> Date: Tue, 24 Oct 2023 13:46:01 -0700 Subject: [PATCH 098/128] Clean up unused string initialization --- src/registrar/models/domain.py | 1 - 1 file changed, 1 deletion(-) diff --git a/src/registrar/models/domain.py b/src/registrar/models/domain.py index 38afa28ee..0a78d4ccf 100644 --- a/src/registrar/models/domain.py +++ b/src/registrar/models/domain.py @@ -259,7 +259,6 @@ class Domain(TimeStampedModel, DomainHelper): for host in hosts: # can remove str conversion when EPP.IP address display changes from IP object -> IP address host_addrs_stringified = [str(addr) for addr in host["addrs"]] - host_addrs_str = "" host_addrs_str = f'({", ".join(obj for obj in host_addrs_stringified)})' host_info_str = f'{host["name"]} {host_addrs_str if len(host["addrs"]) > 0 else ""}' hostList.append(host_info_str) From 6c26010fe535271e2e12dfee8eb5e33e69b8427f Mon Sep 17 00:00:00 2001 From: Erin <121973038+erinysong@users.noreply.github.com> Date: Tue, 24 Oct 2023 14:43:29 -0700 Subject: [PATCH 099/128] Resolve lint errors --- src/registrar/models/domain.py | 6 ++++-- src/registrar/tests/test_views.py | 2 ++ 2 files changed, 6 insertions(+), 2 deletions(-) diff --git a/src/registrar/models/domain.py b/src/registrar/models/domain.py index 0a78d4ccf..9060ef90d 100644 --- a/src/registrar/models/domain.py +++ b/src/registrar/models/domain.py @@ -257,10 +257,12 @@ class Domain(TimeStampedModel, DomainHelper): hostList = [] for host in hosts: - # can remove str conversion when EPP.IP address display changes from IP object -> IP address + # can remove str conversion when EPP.IP address display + # changes from IP object -> IP address host_addrs_stringified = [str(addr) for addr in host["addrs"]] host_addrs_str = f'({", ".join(obj for obj in host_addrs_stringified)})' - host_info_str = f'{host["name"]} {host_addrs_str if len(host["addrs"]) > 0 else ""}' + host_info_str = f'{host["name"]} \ + {host_addrs_str if len(host["addrs"]) > 0 else ""}' hostList.append(host_info_str) return hostList diff --git a/src/registrar/tests/test_views.py b/src/registrar/tests/test_views.py index 7cc616889..7b46b530e 100644 --- a/src/registrar/tests/test_views.py +++ b/src/registrar/tests/test_views.py @@ -1392,6 +1392,8 @@ class TestDomainNameservers(TestDomainOverview): page = result.follow() self.assertContains(page, "The name servers for this domain have been updated") + # changed nameserver is reflected on domain overview page + @skip("Broken by adding registry connection fix in ticket 848") def test_domain_nameservers_form_invalid(self): """Can change domain's nameservers. From 5267a1af53bfdfbae828f39ca27696bd2dcdbf44 Mon Sep 17 00:00:00 2001 From: Rebecca Hsieh Date: Tue, 24 Oct 2023 15:47:24 -0700 Subject: [PATCH 100/128] Add test cases for displaying just nameserver and displaying nameserver AND ip --- src/registrar/tests/common.py | 40 +++++++++++++++++++++- src/registrar/tests/test_views.py | 56 +++++++++++++++++++++++++++++++ 2 files changed, 95 insertions(+), 1 deletion(-) diff --git a/src/registrar/tests/common.py b/src/registrar/tests/common.py index 8cd5fd6ba..463530e85 100644 --- a/src/registrar/tests/common.py +++ b/src/registrar/tests/common.py @@ -738,6 +738,7 @@ class MockEppLib(TestCase): "ns1.cats-are-superior3.com", ], ) + infoDomainNoHost = fakedEppObject( "my-nameserver.gov", cr_date=datetime.datetime(2023, 5, 25, 19, 45, 35), @@ -804,7 +805,20 @@ class MockEppLib(TestCase): infoDomainHasIP = fakedEppObject( "nameserverwithip.gov", cr_date=datetime.datetime(2023, 5, 25, 19, 45, 35), - contacts=[], + contacts=[ + common.DomainContact( + contact="securityContact", + type=PublicContact.ContactTypeChoices.SECURITY, + ), + common.DomainContact( + contact="technicalContact", + type=PublicContact.ContactTypeChoices.TECHNICAL, + ), + common.DomainContact( + contact="adminContact", + type=PublicContact.ContactTypeChoices.ADMINISTRATIVE, + ), + ], hosts=[ "ns1.nameserverwithip.gov", "ns2.nameserverwithip.gov", @@ -813,6 +827,29 @@ class MockEppLib(TestCase): addrs=["1.2.3.4", "2.3.4.5"], ) + justNameserver = fakedEppObject( + "justnameserver.com", + cr_date=datetime.datetime(2023, 5, 25, 19, 45, 35), + contacts=[ + common.DomainContact( + contact="securityContact", + type=PublicContact.ContactTypeChoices.SECURITY, + ), + common.DomainContact( + contact="technicalContact", + type=PublicContact.ContactTypeChoices.TECHNICAL, + ), + common.DomainContact( + contact="adminContact", + type=PublicContact.ContactTypeChoices.ADMINISTRATIVE, + ), + ], + hosts=[ + "ns1.justnameserver.com", + "ns2.justnameserver.com", + ], + ) + infoDomainCheckHostIPCombo = fakedEppObject( "nameserversubdomain.gov", cr_date=datetime.datetime(2023, 5, 25, 19, 45, 35), @@ -931,6 +968,7 @@ class MockEppLib(TestCase): "threenameserversDomain.gov": (self.infoDomainThreeHosts, None), "defaultsecurity.gov": (self.InfoDomainWithDefaultSecurityContact, None), "defaulttechnical.gov": (self.InfoDomainWithDefaultTechnicalContact, None), + "justnameserver.com": (self.justNameserver, None), } # Retrieve the corresponding values from the dictionary diff --git a/src/registrar/tests/test_views.py b/src/registrar/tests/test_views.py index 7b46b530e..81afe1182 100644 --- a/src/registrar/tests/test_views.py +++ b/src/registrar/tests/test_views.py @@ -1071,6 +1071,13 @@ class TestWithDomainPermissions(TestWithUser): def setUp(self): super().setUp() self.domain, _ = Domain.objects.get_or_create(name="igorville.gov") + self.domain_with_ip, _ = Domain.objects.get_or_create( + name="nameserverwithip.gov" + ) + self.domain_just_nameserver, _ = Domain.objects.get_or_create( + name="justnameserver.com" + ) + self.domain_dsdata, _ = Domain.objects.get_or_create(name="dnssec-dsdata.gov") self.domain_multdsdata, _ = Domain.objects.get_or_create( name="dnssec-multdsdata.gov" @@ -1081,9 +1088,11 @@ class TestWithDomainPermissions(TestWithUser): self.domain_dnssec_none, _ = Domain.objects.get_or_create( name="dnssec-none.gov" ) + self.domain_information, _ = DomainInformation.objects.get_or_create( creator=self.user, domain=self.domain ) + DomainInformation.objects.get_or_create( creator=self.user, domain=self.domain_dsdata ) @@ -1096,9 +1105,17 @@ class TestWithDomainPermissions(TestWithUser): DomainInformation.objects.get_or_create( creator=self.user, domain=self.domain_dnssec_none ) + DomainInformation.objects.get_or_create( + creator=self.user, domain=self.domain_with_ip + ) + DomainInformation.objects.get_or_create( + creator=self.user, domain=self.domain_just_nameserver + ) + self.role, _ = UserDomainRole.objects.get_or_create( user=self.user, domain=self.domain, role=UserDomainRole.Roles.MANAGER ) + UserDomainRole.objects.get_or_create( user=self.user, domain=self.domain_dsdata, role=UserDomainRole.Roles.MANAGER ) @@ -1117,6 +1134,16 @@ class TestWithDomainPermissions(TestWithUser): domain=self.domain_dnssec_none, role=UserDomainRole.Roles.MANAGER, ) + UserDomainRole.objects.get_or_create( + user=self.user, + domain=self.domain_with_ip, + role=UserDomainRole.Roles.MANAGER, + ) + UserDomainRole.objects.get_or_create( + user=self.user, + domain=self.domain_just_nameserver, + role=UserDomainRole.Roles.MANAGER, + ) def tearDown(self): try: @@ -1200,6 +1227,35 @@ class TestDomainOverview(TestWithDomainPermissions, WebTest): response = self.client.get(reverse("domain", kwargs={"pk": self.domain.id})) self.assertEqual(response.status_code, 403) + def test_domain_see_just_nameserver(self): + home_page = self.app.get("/") + self.assertContains(home_page, "justnameserver.com") + + # View nameserver on Domain Overview page + detail_page = self.app.get( + reverse("domain", kwargs={"pk": self.domain_just_nameserver.id}) + ) + + self.assertContains(detail_page, "justnameserver.com") + self.assertContains(detail_page, "ns1.justnameserver.com") + self.assertContains(detail_page, "ns2.justnameserver.com") + + def test_domain_see_nameserver_and_ip(self): + home_page = self.app.get("/") + self.assertContains(home_page, "nameserverwithip.gov") + + # View nameserver on Domain Overview page + detail_page = self.app.get( + reverse("domain", kwargs={"pk": self.domain_with_ip.id}) + ) + + self.assertContains(detail_page, "nameserverwithip.gov") + + self.assertContains(detail_page, "ns1.nameserverwithip.gov") + self.assertContains(detail_page, "ns2.nameserverwithip.gov") + self.assertContains(detail_page, "ns3.nameserverwithip.gov") + self.assertContains(detail_page, "(1.2.3.4, 2.3.4.5)") + class TestDomainUserManagement(TestDomainOverview): def test_domain_user_management(self): From 448cef084578e137210e6c681390d5b846a30ebb Mon Sep 17 00:00:00 2001 From: Rebecca Hsieh Date: Tue, 24 Oct 2023 17:40:57 -0700 Subject: [PATCH 101/128] Remove a comment --- src/registrar/tests/test_views.py | 2 -- 1 file changed, 2 deletions(-) diff --git a/src/registrar/tests/test_views.py b/src/registrar/tests/test_views.py index 81afe1182..4b9ac1dcb 100644 --- a/src/registrar/tests/test_views.py +++ b/src/registrar/tests/test_views.py @@ -1448,8 +1448,6 @@ class TestDomainNameservers(TestDomainOverview): page = result.follow() self.assertContains(page, "The name servers for this domain have been updated") - # changed nameserver is reflected on domain overview page - @skip("Broken by adding registry connection fix in ticket 848") def test_domain_nameservers_form_invalid(self): """Can change domain's nameservers. 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 102/128] 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 517390cc55a391618d1560f7dc7ed5a0690fad25 Mon Sep 17 00:00:00 2001 From: David Kennedy Date: Wed, 25 Oct 2023 13:00:25 -0400 Subject: [PATCH 103/128] fixed bugs in ip list handling, reformatted clean function in forms, removed custom field type for ip address in nameserverform --- src/registrar/forms/domain.py | 90 +++++++++----------- src/registrar/models/domain.py | 10 +-- src/registrar/tests/test_nameserver_error.py | 2 +- src/registrar/utility/errors.py | 4 +- src/registrar/views/domain.py | 8 +- 5 files changed, 56 insertions(+), 58 deletions(-) diff --git a/src/registrar/forms/domain.py b/src/registrar/forms/domain.py index b9aaee566..3b65c979b 100644 --- a/src/registrar/forms/domain.py +++ b/src/registrar/forms/domain.py @@ -37,64 +37,58 @@ class DomainNameserverForm(forms.Form): server = forms.CharField(label="Name server", strip=True) - ip = IPAddressField( - label="IP Address (IPv4 or IPv6)", - strip=True, - required=False, - # validators=[ - # django.core.validators.validate_ipv46_address - # ], - ) + ip = forms.CharField(label="IP Address (IPv4 or IPv6)", strip=True, required=False) def clean(self): + # clean is called from clean_forms, which is called from is_valid + # after clean_fields. it is used to determine form level errors. + # is_valid is typically called from view during a post cleaned_data = super().clean() + self.clean_empty_strings(cleaned_data) server = cleaned_data.get("server", "") - ip = cleaned_data.get("ip", "") + ip = cleaned_data.get("ip", None) domain = cleaned_data.get("domain", "") - print(f"clean is called on {domain} {server}") - # make sure there's a nameserver if an ip is passed - if ip: - ip_list = [ip.strip() for ip in ip.split(",")] - if not server and len(ip_list) > 0: - # If 'server' is empty, disallow 'ip' input - self.add_error( - "server", NameserverError(code=nsErrorCodes.MISSING_HOST) - ) + ip_list = self.extract_ip_list(ip) - # if there's a nameserver and an ip, validate nameserver/ip combo - - if server: - if ip: - ip_list = [ip.strip() for ip in ip.split(",")] - else: - ip_list = [""] - try: - Domain.checkHostIPCombo(domain, server, ip_list) - except NameserverError as e: - if e.code == nsErrorCodes.GLUE_RECORD_NOT_ALLOWED: - self.add_error( - "server", - NameserverError( - code=nsErrorCodes.GLUE_RECORD_NOT_ALLOWED, - nameserver=domain, - ip=ip_list, - ), - ) - elif e.code == nsErrorCodes.MISSING_IP: - self.add_error( - "ip", - NameserverError( - code=nsErrorCodes.MISSING_IP, - nameserver=domain, - ip=ip_list, - ), - ) - else: - self.add_error("ip", str(e)) + if ip and not server and ip_list: + self.add_error("server", NameserverError(code=nsErrorCodes.MISSING_HOST)) + elif server: + self.validate_nameserver_ip_combo(domain, server, ip_list) return cleaned_data + def clean_empty_strings(self, cleaned_data): + ip = cleaned_data.get("ip", "") + if ip and len(ip.strip()) == 0: + cleaned_data["ip"] = None + + def extract_ip_list(self, ip): + return [ip.strip() for ip in ip.split(",")] if ip else [] + + def validate_nameserver_ip_combo(self, domain, server, ip_list): + try: + Domain.checkHostIPCombo(domain, server, ip_list) + except NameserverError as e: + if e.code == nsErrorCodes.GLUE_RECORD_NOT_ALLOWED: + self.add_error( + "server", + NameserverError( + code=nsErrorCodes.GLUE_RECORD_NOT_ALLOWED, + nameserver=domain, + ip=ip_list, + ), + ) + elif e.code == nsErrorCodes.MISSING_IP: + self.add_error( + "ip", + NameserverError( + code=nsErrorCodes.MISSING_IP, nameserver=domain, ip=ip_list + ), + ) + else: + self.add_error("ip", str(e)) + NameserverFormset = formset_factory( DomainNameserverForm, diff --git a/src/registrar/models/domain.py b/src/registrar/models/domain.py index d82e0b20b..451838395 100644 --- a/src/registrar/models/domain.py +++ b/src/registrar/models/domain.py @@ -260,7 +260,7 @@ class Domain(TimeStampedModel, DomainHelper): """Creates the host object in the registry doesn't add the created host to the domain returns ErrorCode (int)""" - if addrs is not None: + if addrs is not None and addrs != []: addresses = [epp.Ip(addr=addr) for addr in addrs] request = commands.CreateHost(name=host, addrs=addresses) else: @@ -314,16 +314,14 @@ class Domain(TimeStampedModel, DomainHelper): NameserverError (if exception hit) Returns: None""" - if cls.isSubdomain(name, nameserver) and (ip is None or ip == [] or ip == [""]): + if cls.isSubdomain(name, nameserver) and (ip is None or ip == []): raise NameserverError(code=nsErrorCodes.MISSING_IP, nameserver=nameserver) - elif not cls.isSubdomain(name, nameserver) and ( - ip is not None and ip != [] and ip != [""] - ): + elif not cls.isSubdomain(name, nameserver) and (ip is not None and ip != []): raise NameserverError( code=nsErrorCodes.GLUE_RECORD_NOT_ALLOWED, nameserver=nameserver, ip=ip ) - elif ip is not None and ip != [] and ip != [""]: + elif ip is not None and ip != []: for addr in ip: if not cls._valid_ip_addr(addr): raise NameserverError( diff --git a/src/registrar/tests/test_nameserver_error.py b/src/registrar/tests/test_nameserver_error.py index 218a48231..0657b6599 100644 --- a/src/registrar/tests/test_nameserver_error.py +++ b/src/registrar/tests/test_nameserver_error.py @@ -35,7 +35,7 @@ class TestNameserverError(TestCase): ip = "ip val" nameserver = "nameserver val" - expected = f"Nameserver {nameserver} has an invalid IP address: {ip}" + expected = f"{nameserver}: Enter an IP address in the required format." nsException = NameserverError( code=nsErrorCodes.INVALID_IP, nameserver=nameserver, ip=ip ) diff --git a/src/registrar/utility/errors.py b/src/registrar/utility/errors.py index 5e28bc728..64c86ee01 100644 --- a/src/registrar/utility/errors.py +++ b/src/registrar/utility/errors.py @@ -87,7 +87,9 @@ class NameserverError(Exception): NameserverErrorCodes.GLUE_RECORD_NOT_ALLOWED: ( "Name server address does not match domain name" ), - NameserverErrorCodes.INVALID_IP: "{}: Enter an IP address in the required format.", + NameserverErrorCodes.INVALID_IP: ( + "{}: Enter an IP address in the required format." + ), NameserverErrorCodes.TOO_MANY_HOSTS: ( "Too many hosts provided, you may not have more than 13 nameservers." ), diff --git a/src/registrar/views/domain.py b/src/registrar/views/domain.py index a2ca6d8cb..da501b273 100644 --- a/src/registrar/views/domain.py +++ b/src/registrar/views/domain.py @@ -295,8 +295,12 @@ class DomainNameserversView(DomainFormBaseView): for form in formset: try: ip_string = form.cleaned_data["ip"] - # Split the string into a list using a comma as the delimiter - ip_list = ip_string.split(",") + # ip_string will be None or a string of IP addresses + # comma-separated + ip_list = [] + if ip_string: + # Split the string into a list using a comma as the delimiter + ip_list = ip_string.split(",") # Remove any leading or trailing whitespace from each IP in the list # this will return [''] if no ips have been entered, which is taken # into account in the model in checkHostIPCombo 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 104/128] 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 04168419e7f4cfdf1531bd1ed48b7a766c624c3b Mon Sep 17 00:00:00 2001 From: David Kennedy Date: Wed, 25 Oct 2023 20:23:35 -0400 Subject: [PATCH 105/128] unit tests for nameserver views --- src/registrar/tests/test_views.py | 185 ++++++++++++++++++++++++++++-- 1 file changed, 178 insertions(+), 7 deletions(-) diff --git a/src/registrar/tests/test_views.py b/src/registrar/tests/test_views.py index 1c215ec05..59a4ab700 100644 --- a/src/registrar/tests/test_views.py +++ b/src/registrar/tests/test_views.py @@ -10,6 +10,10 @@ from .common import MockEppLib, completed_application # type: ignore from django_webtest import WebTest # type: ignore import boto3_mocking # type: ignore +from registrar.utility.errors import ( + NameserverError, + NameserverErrorCodes, +) from registrar.models import ( DomainApplication, @@ -1393,20 +1397,165 @@ class TestDomainNameservers(TestDomainOverview): ) self.assertContains(page, "DNS name servers") - @skip("Broken by adding registry connection fix in ticket 848") - def test_domain_nameservers_form(self): + def test_domain_nameservers_form_submit_one_nameserver(self): """Can change domain's nameservers. Uses self.app WebTest because we need to interact with forms. """ + # initial nameservers page has one server with two ips nameservers_page = self.app.get( reverse("domain-dns-nameservers", kwargs={"pk": self.domain.id}) ) session_id = self.app.cookies[settings.SESSION_COOKIE_NAME] self.app.set_cookie(settings.SESSION_COOKIE_NAME, session_id) + # attempt to submit the form with only one nameserver, should error + # regarding required fields with less_console_noise(): # swallow log warning message result = nameservers_page.form.submit() - # form submission was a post, response should be a redirect + # 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 required field. form requires a minimum of 2 name servers + self.assertContains(result, "This field is required.", count=2, status_code=200) + + def test_domain_nameservers_form_submit_subdomain_missing_ip(self): + """Can change domain's nameservers. + + Uses self.app WebTest because we need to interact with forms. + """ + # initial nameservers page has one server with two ips + nameservers_page = self.app.get( + reverse("domain-dns-nameservers", kwargs={"pk": self.domain.id}) + ) + session_id = self.app.cookies[settings.SESSION_COOKIE_NAME] + self.app.set_cookie(settings.SESSION_COOKIE_NAME, session_id) + # attempt to submit the form without two hosts, both subdomains, + # only one has ips + nameservers_page.form["form-1-server"] = "ns2.igorville.gov" + with less_console_noise(): # swallow log warning message + result = nameservers_page.form.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 required field. subdomain missing an ip + self.assertContains( + result, + str(NameserverError(code=NameserverErrorCodes.MISSING_IP)), + count=2, + status_code=200 + ) + + def test_domain_nameservers_form_submit_missing_host(self): + """Can change domain's nameservers. + + Uses self.app WebTest because we need to interact with forms. + """ + # initial nameservers page has one server with two ips + nameservers_page = self.app.get( + reverse("domain-dns-nameservers", kwargs={"pk": self.domain.id}) + ) + session_id = self.app.cookies[settings.SESSION_COOKIE_NAME] + self.app.set_cookie(settings.SESSION_COOKIE_NAME, session_id) + # attempt to submit the form without two hosts, both subdomains, + # only one has ips + nameservers_page.form["form-1-ip"] = "127.0.0.1" + with less_console_noise(): # swallow log warning message + result = nameservers_page.form.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 required field. nameserver has ip but missing host + self.assertContains( + result, + str(NameserverError(code=NameserverErrorCodes.MISSING_HOST)), + count=2, + status_code=200 + ) + + def test_domain_nameservers_form_submit_glue_record_not_allowed(self): + """Can change domain's nameservers. + + Uses self.app WebTest because we need to interact with forms. + """ + nameserver1 = "ns1.igorville.gov" + nameserver2 = "ns2.igorville.com" + valid_ip = "127.0.0.1" + # initial nameservers page has one server with two ips + nameservers_page = self.app.get( + reverse("domain-dns-nameservers", kwargs={"pk": self.domain.id}) + ) + session_id = self.app.cookies[settings.SESSION_COOKIE_NAME] + self.app.set_cookie(settings.SESSION_COOKIE_NAME, session_id) + # attempt to submit the form without two hosts, both subdomains, + # only one has ips + nameservers_page.form["form-0-server"] = nameserver1 + nameservers_page.form["form-1-server"] = nameserver2 + nameservers_page.form["form-1-ip"] = valid_ip + with less_console_noise(): # swallow log warning message + result = nameservers_page.form.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 required field. nameserver has ip but missing host + self.assertContains( + result, + str(NameserverError( + code=NameserverErrorCodes.GLUE_RECORD_NOT_ALLOWED + )), + count=2, + status_code=200 + ) + + def test_domain_nameservers_form_submit_invalid_ip(self): + """Can change domain's nameservers. + + Uses self.app WebTest because we need to interact with forms. + """ + nameserver = "ns2.igorville.gov" + invalid_ip = "123" + # initial nameservers page has one server with two ips + nameservers_page = self.app.get( + reverse("domain-dns-nameservers", kwargs={"pk": self.domain.id}) + ) + session_id = self.app.cookies[settings.SESSION_COOKIE_NAME] + self.app.set_cookie(settings.SESSION_COOKIE_NAME, session_id) + # attempt to submit the form without two hosts, both subdomains, + # only one has ips + nameservers_page.form["form-1-server"] = nameserver + nameservers_page.form["form-1-ip"] = invalid_ip + with less_console_noise(): # swallow log warning message + result = nameservers_page.form.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 required field. nameserver has ip but missing host + self.assertContains( + result, + str(NameserverError( + code=NameserverErrorCodes.INVALID_IP, + nameserver=nameserver + )), + count=2, + status_code=200 + ) + + def test_domain_nameservers_form_submits_successfully(self): + """Can change domain's nameservers. + + Uses self.app WebTest because we need to interact with forms. + """ + nameserver1 = "ns1.igorville.gov" + nameserver2 = "ns2.igorville.gov" + invalid_ip = "127.0.0.1" + # initial nameservers page has one server with two ips + nameservers_page = self.app.get( + reverse("domain-dns-nameservers", kwargs={"pk": self.domain.id}) + ) + session_id = self.app.cookies[settings.SESSION_COOKIE_NAME] + self.app.set_cookie(settings.SESSION_COOKIE_NAME, session_id) + # attempt to submit the form without two hosts, both subdomains, + # only one has ips + nameservers_page.form["form-0-server"] = nameserver1 + nameservers_page.form["form-1-server"] = nameserver2 + nameservers_page.form["form-1-ip"] = invalid_ip + with less_console_noise(): # swallow log warning message + result = nameservers_page.form.submit() + # form submission was a successful post, response should be a 302 self.assertEqual(result.status_code, 302) self.assertEqual( result["Location"], @@ -1416,7 +1565,29 @@ class TestDomainNameservers(TestDomainOverview): page = result.follow() self.assertContains(page, "The name servers for this domain have been updated") - @skip("Broken by adding registry connection fix in ticket 848") + # self.assertContains(result, "This field is required.", count=2, status_code=200) + # add a second name server, which is a subdomain, but don't + # submit the required ip + # nameservers_page.form["form-1-server"] = "ns1.igorville.gov" + # with less_console_noise(): # swallow log warning message + # result = nameservers_page.form.submit() + # # form submission was a post with an error, response should be a 200 + # self.assertContains( + # result, + # str(NameserverError(code=NameserverErrorCodes.MISSING_IP)), + # count=2, + # status_code=200 + # ) + # form submission was a post, response should be a redirect + # self.assertEqual(result.status_code, 302) + # self.assertEqual( + # result["Location"], + # reverse("domain-dns-nameservers", kwargs={"pk": self.domain.id}), + # ) + # self.app.set_cookie(settings.SESSION_COOKIE_NAME, session_id) + # page = result.follow() + # self.assertContains(page, "The name servers for this domain have been updated") + def test_domain_nameservers_form_invalid(self): """Can change domain's nameservers. @@ -1433,9 +1604,9 @@ class TestDomainNameservers(TestDomainOverview): with less_console_noise(): # swallow logged warning message result = nameservers_page.form.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, "This field is required", count=2, status_code=200) + # error text appears four times, twice at the top of the page, + # once around each required field. + self.assertContains(result, "This field is required", count=4, status_code=200) class TestDomainAuthorizingOfficial(TestDomainOverview): From 62f3367c26ca7911c08b312626daea7f878997c7 Mon Sep 17 00:00:00 2001 From: David Kennedy Date: Wed, 25 Oct 2023 20:44:21 -0400 Subject: [PATCH 106/128] formatted code, removed comments and cluttering debugs, updated error messages --- .../templates/domain_nameservers.html | 2 +- src/registrar/tests/test_nameserver_error.py | 2 +- src/registrar/tests/test_views.py | 44 +++++-------------- src/registrar/utility/errors.py | 4 +- src/registrar/views/domain.py | 3 -- 5 files changed, 15 insertions(+), 40 deletions(-) diff --git a/src/registrar/templates/domain_nameservers.html b/src/registrar/templates/domain_nameservers.html index eebba921d..d00126698 100644 --- a/src/registrar/templates/domain_nameservers.html +++ b/src/registrar/templates/domain_nameservers.html @@ -13,7 +13,7 @@

Before your domain can be used we’ll need information about your domain name servers. Name server records indicate which DNS server is authoritative for your domain.

-

Add a name server record by entering the address (e.g., ns1.nameserver.com) in the name server fields below. You may add up to 13 name servers.

+

Add a name server record by entering the address (e.g., ns1.nameserver.com) in the name server fields below. You must add at least two name servers (13 max).

diff --git a/src/registrar/tests/test_nameserver_error.py b/src/registrar/tests/test_nameserver_error.py index 0657b6599..89a6dfcce 100644 --- a/src/registrar/tests/test_nameserver_error.py +++ b/src/registrar/tests/test_nameserver_error.py @@ -10,7 +10,7 @@ class TestNameserverError(TestCase): def test_with_no_ip(self): """Test NameserverError when no ip address is passed""" nameserver = "nameserver val" - expected = "Subdomains require an IP address" + expected = "Using your domain for a name server requires an IP address" nsException = NameserverError( code=nsErrorCodes.MISSING_IP, nameserver=nameserver diff --git a/src/registrar/tests/test_views.py b/src/registrar/tests/test_views.py index 59a4ab700..28384ca91 100644 --- a/src/registrar/tests/test_views.py +++ b/src/registrar/tests/test_views.py @@ -1440,7 +1440,7 @@ class TestDomainNameservers(TestDomainOverview): result, str(NameserverError(code=NameserverErrorCodes.MISSING_IP)), count=2, - status_code=200 + status_code=200, ) def test_domain_nameservers_form_submit_missing_host(self): @@ -1466,7 +1466,7 @@ class TestDomainNameservers(TestDomainOverview): result, str(NameserverError(code=NameserverErrorCodes.MISSING_HOST)), count=2, - status_code=200 + status_code=200, ) def test_domain_nameservers_form_submit_glue_record_not_allowed(self): @@ -1495,11 +1495,9 @@ class TestDomainNameservers(TestDomainOverview): # the required field. nameserver has ip but missing host self.assertContains( result, - str(NameserverError( - code=NameserverErrorCodes.GLUE_RECORD_NOT_ALLOWED - )), + str(NameserverError(code=NameserverErrorCodes.GLUE_RECORD_NOT_ALLOWED)), count=2, - status_code=200 + status_code=200, ) def test_domain_nameservers_form_submit_invalid_ip(self): @@ -1526,12 +1524,13 @@ class TestDomainNameservers(TestDomainOverview): # the required field. nameserver has ip but missing host self.assertContains( result, - str(NameserverError( - code=NameserverErrorCodes.INVALID_IP, - nameserver=nameserver - )), + str( + NameserverError( + code=NameserverErrorCodes.INVALID_IP, nameserver=nameserver + ) + ), count=2, - status_code=200 + status_code=200, ) def test_domain_nameservers_form_submits_successfully(self): @@ -1565,29 +1564,6 @@ class TestDomainNameservers(TestDomainOverview): page = result.follow() self.assertContains(page, "The name servers for this domain have been updated") - # self.assertContains(result, "This field is required.", count=2, status_code=200) - # add a second name server, which is a subdomain, but don't - # submit the required ip - # nameservers_page.form["form-1-server"] = "ns1.igorville.gov" - # with less_console_noise(): # swallow log warning message - # result = nameservers_page.form.submit() - # # form submission was a post with an error, response should be a 200 - # self.assertContains( - # result, - # str(NameserverError(code=NameserverErrorCodes.MISSING_IP)), - # count=2, - # status_code=200 - # ) - # form submission was a post, response should be a redirect - # self.assertEqual(result.status_code, 302) - # self.assertEqual( - # result["Location"], - # reverse("domain-dns-nameservers", kwargs={"pk": self.domain.id}), - # ) - # self.app.set_cookie(settings.SESSION_COOKIE_NAME, session_id) - # page = result.follow() - # self.assertContains(page, "The name servers for this domain have been updated") - def test_domain_nameservers_form_invalid(self): """Can change domain's nameservers. diff --git a/src/registrar/utility/errors.py b/src/registrar/utility/errors.py index 64c86ee01..c1d3c5849 100644 --- a/src/registrar/utility/errors.py +++ b/src/registrar/utility/errors.py @@ -83,7 +83,9 @@ class NameserverError(Exception): """ _error_mapping = { - NameserverErrorCodes.MISSING_IP: "Subdomains require an IP address", + NameserverErrorCodes.MISSING_IP: ( + "Using your domain for a name server requires an IP address" + ), NameserverErrorCodes.GLUE_RECORD_NOT_ALLOWED: ( "Name server address does not match domain name" ), diff --git a/src/registrar/views/domain.py b/src/registrar/views/domain.py index da501b273..f795c4333 100644 --- a/src/registrar/views/domain.py +++ b/src/registrar/views/domain.py @@ -254,18 +254,15 @@ class DomainNameserversView(DomainFormBaseView): def get_form(self, **kwargs): """Override the labels and required fields every time we get a formset.""" - # kwargs.update({"domain", self.object}) formset = super().get_form(**kwargs) for i, form in enumerate(formset): - # form = self.get_form(self, **kwargs) form.fields["server"].label += f" {i+1}" if i < 2: form.fields["server"].required = True else: form.fields["server"].required = False form.fields["domain"].initial = self.object.name - print(f"domain in get_form {self.object.name}") return formset def post(self, request, *args, **kwargs): From 167cb9d995ab6934e24e06e4abf46d56cbe0d9b8 Mon Sep 17 00:00:00 2001 From: David Kennedy Date: Wed, 25 Oct 2023 21:56:52 -0400 Subject: [PATCH 107/128] formatting change --- src/registrar/views/domain.py | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/src/registrar/views/domain.py b/src/registrar/views/domain.py index 784d2af94..be6026bc3 100644 --- a/src/registrar/views/domain.py +++ b/src/registrar/views/domain.py @@ -499,10 +499,15 @@ class DomainDsDataView(DomainFormBaseView): self.object.dnssecdata = dnssecdata except RegistryError as err: if err.is_connection_error(): - messages.error(self.request, GenericError(code=GenericErrorCodes.CANNOT_CONTACT_REGISTRY)) + messages.error( + self.request, + GenericError(code=GenericErrorCodes.CANNOT_CONTACT_REGISTRY), + ) logger.error(f"Registry connection error: {err}") else: - messages.error(self.request, GenericError(code=GenericErrorCodes.GENERIC_ERROR)) + messages.error( + self.request, GenericError(code=GenericErrorCodes.GENERIC_ERROR) + ) logger.error(f"Registry error: {err}") return self.form_invalid(formset) else: From eedd5d7dbff003b0c5597f51f86e5eb8f97a0016 Mon Sep 17 00:00:00 2001 From: David Kennedy Date: Wed, 25 Oct 2023 22:43:41 -0400 Subject: [PATCH 108/128] fixed create_host bug not properly submitting ip version for v6; refactored update_host as well --- src/registrar/models/domain.py | 18 +++++++++++++----- 1 file changed, 13 insertions(+), 5 deletions(-) diff --git a/src/registrar/models/domain.py b/src/registrar/models/domain.py index 522590e69..8215d0a7a 100644 --- a/src/registrar/models/domain.py +++ b/src/registrar/models/domain.py @@ -261,7 +261,13 @@ class Domain(TimeStampedModel, DomainHelper): doesn't add the created host to the domain returns ErrorCode (int)""" if addrs is not None and addrs != []: - addresses = [epp.Ip(addr=addr) for addr in addrs] + addresses = [ + epp.Ip( + addr=addr, + ip="v6" if self.is_ipv6(addr) else None + ) + for addr in addrs + ] request = commands.CreateHost(name=host, addrs=addresses) else: request = commands.CreateHost(name=host) @@ -1541,10 +1547,12 @@ class Domain(TimeStampedModel, DomainHelper): return [] for ip_addr in ip_list: - if self.is_ipv6(ip_addr): - edited_ip_list.append(epp.Ip(addr=ip_addr, ip="v6")) - else: # default ip addr is v4 - edited_ip_list.append(epp.Ip(addr=ip_addr)) + edited_ip_list.append( + epp.Ip( + addr=ip_addr, + ip="v6" if self.is_ipv6(ip_addr) else None + ) + ) return edited_ip_list From 2f05772623cf9318f8305fe02c217d1907be1ce5 Mon Sep 17 00:00:00 2001 From: David Kennedy Date: Thu, 26 Oct 2023 06:16:55 -0400 Subject: [PATCH 109/128] formatting changes --- src/registrar/models/domain.py | 10 ++-------- 1 file changed, 2 insertions(+), 8 deletions(-) diff --git a/src/registrar/models/domain.py b/src/registrar/models/domain.py index 8215d0a7a..5344a14c1 100644 --- a/src/registrar/models/domain.py +++ b/src/registrar/models/domain.py @@ -262,10 +262,7 @@ class Domain(TimeStampedModel, DomainHelper): returns ErrorCode (int)""" if addrs is not None and addrs != []: addresses = [ - epp.Ip( - addr=addr, - ip="v6" if self.is_ipv6(addr) else None - ) + epp.Ip(addr=addr, ip="v6" if self.is_ipv6(addr) else None) for addr in addrs ] request = commands.CreateHost(name=host, addrs=addresses) @@ -1548,10 +1545,7 @@ class Domain(TimeStampedModel, DomainHelper): for ip_addr in ip_list: edited_ip_list.append( - epp.Ip( - addr=ip_addr, - ip="v6" if self.is_ipv6(ip_addr) else None - ) + epp.Ip(addr=ip_addr, ip="v6" if self.is_ipv6(ip_addr) else None) ) return edited_ip_list 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 110/128] 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. From bde3653fb8670dacab6bcafdfad23e3722404c31 Mon Sep 17 00:00:00 2001 From: Neil Martinsen-Burrell Date: Fri, 20 Oct 2023 15:03:16 -0500 Subject: [PATCH 111/128] Review feedback: fix spacing with usa-list and open contact in new tab --- src/registrar/templates/domain_users.html | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/src/registrar/templates/domain_users.html b/src/registrar/templates/domain_users.html index f66eef5a6..7d522a515 100644 --- a/src/registrar/templates/domain_users.html +++ b/src/registrar/templates/domain_users.html @@ -12,11 +12,13 @@ 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. +
    • To remove a domain manager, contact us for + assistance.
    {% if domain.permissions %} From bd27bfc9de7cdd2fe2a5e16f95d3385d55d71bd2 Mon Sep 17 00:00:00 2001 From: Rachid Mrad Date: Thu, 26 Oct 2023 11:34:11 -0400 Subject: [PATCH 112/128] fix usa-alert class on form_messages --- src/registrar/templates/includes/form_messages.html | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/registrar/templates/includes/form_messages.html b/src/registrar/templates/includes/form_messages.html index e2888e4ee..c7b704f67 100644 --- a/src/registrar/templates/includes/form_messages.html +++ b/src/registrar/templates/includes/form_messages.html @@ -1,6 +1,6 @@ {% if messages %} {% for message in messages %} -
    +
    {{ message }}
    From 4fdbb231411d0a4b24419a3581fa24e147898342 Mon Sep 17 00:00:00 2001 From: David Kennedy Date: Thu, 26 Oct 2023 12:41:44 -0400 Subject: [PATCH 113/128] updated comment --- src/registrar/views/domain.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/registrar/views/domain.py b/src/registrar/views/domain.py index be6026bc3..b531fe8b7 100644 --- a/src/registrar/views/domain.py +++ b/src/registrar/views/domain.py @@ -296,7 +296,7 @@ class DomainNameserversView(DomainFormBaseView): # Split the string into a list using a comma as the delimiter ip_list = ip_string.split(",") # Remove any leading or trailing whitespace from each IP in the list - # this will return [''] if no ips have been entered, which is taken + # this will return [] if no ips have been entered, which is taken # into account in the model in checkHostIPCombo ip_list = [ip.strip() for ip in ip_list] From 1c99101c18788dfb2924492fbb39c7a0e64af030 Mon Sep 17 00:00:00 2001 From: Erin <121973038+erinysong@users.noreply.github.com> Date: Thu, 26 Oct 2023 12:16:01 -0700 Subject: [PATCH 114/128] Move nameserver formatting to view --- src/registrar/templates/domain_detail.html | 4 ++-- src/registrar/templates/includes/summary_item.html | 8 ++++++++ 2 files changed, 10 insertions(+), 2 deletions(-) diff --git a/src/registrar/templates/domain_detail.html b/src/registrar/templates/domain_detail.html index dfd583b1e..e220fe1aa 100644 --- a/src/registrar/templates/domain_detail.html +++ b/src/registrar/templates/domain_detail.html @@ -28,8 +28,8 @@
    {% url 'domain-dns-nameservers' pk=domain.id as url %} - {% if domain.format_nameservers|length > 0 %} - {% include "includes/summary_item.html" with title='DNS name servers' value=domain.format_nameservers list='true' edit_link=url %} + {% if domain.nameservers|length > 0 %} + {% include "includes/summary_item.html" with title='DNS name servers' domains='true' value=domain.nameservers list='true' edit_link=url %} {% else %}

    DNS name servers

    No DNS name servers have been added yet. Before your domain can be used we’ll need information about your domain name servers.

    diff --git a/src/registrar/templates/includes/summary_item.html b/src/registrar/templates/includes/summary_item.html index 6fcad0650..63eb62fb4 100644 --- a/src/registrar/templates/includes/summary_item.html +++ b/src/registrar/templates/includes/summary_item.html @@ -56,6 +56,14 @@ {% for item in value %} {% if users %}
  • {{ item.user.email }}
  • + {% elif domains %} +
  • {{ item.0 }} + {% if item.1 %} + ({% for addr in item.1 %} + {{addr}} + {% endfor %}) + {% endif %} +
  • {% else %}
  • {{ item }}
  • {% endif %} From eeaa4bfc27e50495170a1f418b438a5da0f4d815 Mon Sep 17 00:00:00 2001 From: Erin <121973038+erinysong@users.noreply.github.com> Date: Fri, 27 Oct 2023 09:46:25 -0700 Subject: [PATCH 115/128] Implement reformatting nameservers from model to views --- src/registrar/models/domain.py | 38 ------------------- .../templates/includes/summary_item.html | 11 ++++-- 2 files changed, 7 insertions(+), 42 deletions(-) diff --git a/src/registrar/models/domain.py b/src/registrar/models/domain.py index 4ba2ae3d4..78aecb927 100644 --- a/src/registrar/models/domain.py +++ b/src/registrar/models/domain.py @@ -229,44 +229,6 @@ class Domain(TimeStampedModel, DomainHelper): """ raise NotImplementedError() - @Cache - def format_nameservers(self): - """ - Formatting nameservers for display for this domain. - - Hosts are provided as a list of tuples, e.g. - - [("ns1.example.com",), ("ns1.example.gov", ["0.0.0.0"])] - - We want to display as: - ns1.example.gov - ns1.example.gov (0.0.0.0) - - Subordinate hosts (something.your-domain.gov) MUST have IP addresses, - while non-subordinate hosts MUST NOT. - - - """ - try: - hosts = self._get_property("hosts") - except Exception as err: - # Do not raise error when missing nameservers - # this is a standard occurence when a domain - # is first created - logger.info("Domain is missing nameservers %s" % err) - return [] - - hostList = [] - for host in hosts: - # can remove str conversion when EPP.IP address display - # changes from IP object -> IP address - host_addrs_stringified = [str(addr) for addr in host["addrs"]] - host_addrs_str = f'({", ".join(obj for obj in host_addrs_stringified)})' - host_info_str = f'{host["name"]} \ - {host_addrs_str if len(host["addrs"]) > 0 else ""}' - hostList.append(host_info_str) - return hostList - @Cache def nameservers(self) -> list[tuple[str, list]]: """ diff --git a/src/registrar/templates/includes/summary_item.html b/src/registrar/templates/includes/summary_item.html index 63eb62fb4..f5b6c609d 100644 --- a/src/registrar/templates/includes/summary_item.html +++ b/src/registrar/templates/includes/summary_item.html @@ -57,11 +57,14 @@ {% if users %}
  • {{ item.user.email }}
  • {% elif domains %} -
  • {{ item.0 }} +
  • + {{ item.0 }} {% if item.1 %} - ({% for addr in item.1 %} - {{addr}} - {% endfor %}) + ({% spaceless %} + {% for addr in item.1 %} + {{addr}}{% if not forloop.last %}, {% endif %} + {% endfor %} + {% endspaceless %}) {% endif %}
  • {% else %} From e135de66fc3f531c641a822d591fc1ab23fa2df9 Mon Sep 17 00:00:00 2001 From: Rebecca Hsieh Date: Fri, 27 Oct 2023 11:25:58 -0700 Subject: [PATCH 116/128] Fix test bc of weird detail page whitespace --- src/registrar/tests/test_views.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/src/registrar/tests/test_views.py b/src/registrar/tests/test_views.py index 10b2adee2..0a472b885 100644 --- a/src/registrar/tests/test_views.py +++ b/src/registrar/tests/test_views.py @@ -1269,7 +1269,9 @@ class TestDomainOverview(TestWithDomainPermissions, WebTest): self.assertContains(detail_page, "ns1.nameserverwithip.gov") self.assertContains(detail_page, "ns2.nameserverwithip.gov") self.assertContains(detail_page, "ns3.nameserverwithip.gov") - self.assertContains(detail_page, "(1.2.3.4, 2.3.4.5)") + # Splitting IP addresses bc there is odd whitespace and can't strip text + self.assertContains(detail_page, "(1.2.3.4,") + self.assertContains(detail_page, "2.3.4.5)") class TestDomainManagers(TestDomainOverview): From f3547f24c070b3edf68614a96abb1270abc12b86 Mon Sep 17 00:00:00 2001 From: Rebecca Hsieh Date: Fri, 27 Oct 2023 11:32:29 -0700 Subject: [PATCH 117/128] Remove comment --- src/registrar/templates/includes/summary_item.html | 1 - 1 file changed, 1 deletion(-) diff --git a/src/registrar/templates/includes/summary_item.html b/src/registrar/templates/includes/summary_item.html index f5b6c609d..8a33bb1d5 100644 --- a/src/registrar/templates/includes/summary_item.html +++ b/src/registrar/templates/includes/summary_item.html @@ -43,7 +43,6 @@ {% else %} {% include "includes/contact.html" with contact=value %} {% endif %} - {% elif list %} {% if value|length == 1 %} {% if users %} From bfb28a62f8bd7d5aea379c41ecb6faa2c48b0cb0 Mon Sep 17 00:00:00 2001 From: CocoByte Date: Fri, 27 Oct 2023 13:22:53 -0600 Subject: [PATCH 118/128] Updated domain model & added migration --- .../migrations/0043_domain_expiration_date.py | 20 +++++++++++++++++++ src/registrar/models/domain.py | 13 +++++++++--- src/registrar/tests/test_models_domain.py | 2 +- 3 files changed, 31 insertions(+), 4 deletions(-) create mode 100644 src/registrar/migrations/0043_domain_expiration_date.py diff --git a/src/registrar/migrations/0043_domain_expiration_date.py b/src/registrar/migrations/0043_domain_expiration_date.py new file mode 100644 index 000000000..51d2d384a --- /dev/null +++ b/src/registrar/migrations/0043_domain_expiration_date.py @@ -0,0 +1,20 @@ +# Generated by Django 4.2.6 on 2023-10-27 19:13 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + dependencies = [ + ("registrar", "0042_create_groups_v03"), + ] + + operations = [ + migrations.AddField( + model_name="domain", + name="expiration_date", + field=models.DateField( + help_text="Duplication of registry's expiration date saved for ease of reporting", + null=True, + ), + ), + ] diff --git a/src/registrar/models/domain.py b/src/registrar/models/domain.py index 942647ef1..443465ff5 100644 --- a/src/registrar/models/domain.py +++ b/src/registrar/models/domain.py @@ -5,6 +5,7 @@ import re from datetime import date from string import digits from typing import Optional + from django_fsm import FSMField, transition, TransitionNotAllowed # type: ignore from django.db import models @@ -29,6 +30,7 @@ from epplibwrapper import ( from registrar.models.utility.contact_error import ContactError, ContactErrorCodes +from django.db.models import DateField from .utility.domain_field import DomainField from .utility.domain_helper import DomainHelper from .utility.time_stamped_model import TimeStampedModel @@ -209,12 +211,12 @@ class Domain(TimeStampedModel, DomainHelper): return self._get_property("up_date") @Cache - def expiration_date(self) -> date: + def registry_expiration_date(self) -> date: """Get or set the `ex_date` element from the registry.""" return self._get_property("ex_date") - @expiration_date.setter # type: ignore - def expiration_date(self, ex_date: date): + @registry_expiration_date.setter # type: ignore + def registry_expiration_date(self, ex_date: date): pass @Cache @@ -944,6 +946,11 @@ class Domain(TimeStampedModel, DomainHelper): help_text="Very basic info about the lifecycle of this domain object", ) + expiration_date = DateField( + null = True, + help_text="Duplication of registry's expiration date saved for ease of reporting" + ) + def isActive(self): return self.state == Domain.State.CREATED diff --git a/src/registrar/tests/test_models_domain.py b/src/registrar/tests/test_models_domain.py index 1d87c6b1c..c6b2b7ce9 100644 --- a/src/registrar/tests/test_models_domain.py +++ b/src/registrar/tests/test_models_domain.py @@ -56,7 +56,7 @@ class TestDomainCache(MockEppLib): self.assertFalse("avail" in domain._cache.keys()) # using a setter should clear the cache - domain.expiration_date = datetime.date.today() + domain.registry_expiration_date = datetime.date.today() self.assertEquals(domain._cache, {}) # send should have been called only once From 1fe6267e2f5b8e424954a9d2cbfbc1b6bef368f8 Mon Sep 17 00:00:00 2001 From: CocoByte Date: Fri, 27 Oct 2023 16:00:50 -0600 Subject: [PATCH 119/128] linted --- src/registrar/models/domain.py | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/src/registrar/models/domain.py b/src/registrar/models/domain.py index 443465ff5..8e1e8cb20 100644 --- a/src/registrar/models/domain.py +++ b/src/registrar/models/domain.py @@ -947,8 +947,9 @@ class Domain(TimeStampedModel, DomainHelper): ) expiration_date = DateField( - null = True, - help_text="Duplication of registry's expiration date saved for ease of reporting" + null=True, + help_text=("Duplication of registry's expiration" + "date saved for ease of reporting"), ) def isActive(self): From 6303a8c60194882bac221248d5c9048fce1c1a53 Mon Sep 17 00:00:00 2001 From: CocoByte Date: Fri, 27 Oct 2023 16:09:54 -0600 Subject: [PATCH 120/128] linted..again --- src/registrar/models/domain.py | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/src/registrar/models/domain.py b/src/registrar/models/domain.py index 8e1e8cb20..344ef5a00 100644 --- a/src/registrar/models/domain.py +++ b/src/registrar/models/domain.py @@ -948,8 +948,9 @@ class Domain(TimeStampedModel, DomainHelper): expiration_date = DateField( null=True, - help_text=("Duplication of registry's expiration" - "date saved for ease of reporting"), + help_text=( + "Duplication of registry's expiration" "date saved for ease of reporting" + ), ) def isActive(self): From 11d35d18b1fdf6761d856d1f4a0ca1c5a803977c Mon Sep 17 00:00:00 2001 From: Cameron Dixon Date: Mon, 30 Oct 2023 09:50:46 -0400 Subject: [PATCH 121/128] Update rotate_application_secrets.md add link to what Login recommends --- docs/operations/runbooks/rotate_application_secrets.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/operations/runbooks/rotate_application_secrets.md b/docs/operations/runbooks/rotate_application_secrets.md index 456baf61d..e91e8427e 100644 --- a/docs/operations/runbooks/rotate_application_secrets.md +++ b/docs/operations/runbooks/rotate_application_secrets.md @@ -46,7 +46,7 @@ This is a standard Django secret key. See Django documentation for tips on gener This is the base64 encoded private key used in the OpenID Connect authentication flow with Login.gov. It is used to sign a token during user login; the signature is examined by Login.gov before their API grants access to user data. -Generate a new key using this command (or whatever is most recently recommended by Login.gov): +Generate a new key using this command (or whatever is most recently [recommended by Login.gov](https://developers.login.gov/testing/#creating-a-public-certificate)): ```bash openssl req -nodes -x509 -days 365 -newkey rsa:2048 -keyout private.pem -out public.crt From 3a284a224dcc98fef41e8ed0a1ec4a06b0debcfd Mon Sep 17 00:00:00 2001 From: Rachid Mrad Date: Mon, 30 Oct 2023 11:39:24 -0400 Subject: [PATCH 122/128] fix migration 43 - domain expiration date --- src/registrar/migrations/0043_domain_expiration_date.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/registrar/migrations/0043_domain_expiration_date.py b/src/registrar/migrations/0043_domain_expiration_date.py index 51d2d384a..f2269094c 100644 --- a/src/registrar/migrations/0043_domain_expiration_date.py +++ b/src/registrar/migrations/0043_domain_expiration_date.py @@ -1,4 +1,4 @@ -# Generated by Django 4.2.6 on 2023-10-27 19:13 +# Generated by Django 4.2.6 on 2023-10-30 15:37 from django.db import migrations, models @@ -13,7 +13,7 @@ class Migration(migrations.Migration): model_name="domain", name="expiration_date", field=models.DateField( - help_text="Duplication of registry's expiration date saved for ease of reporting", + help_text="Duplication of registry's expirationdate saved for ease of reporting", null=True, ), ), From f928bc4da42e268162afbdbcd8ee95031d59fa88 Mon Sep 17 00:00:00 2001 From: David Kennedy Date: Mon, 30 Oct 2023 12:37:37 -0400 Subject: [PATCH 123/128] remove all whitespace from ip addresses --- src/registrar/views/domain.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/registrar/views/domain.py b/src/registrar/views/domain.py index b531fe8b7..2f3f2b9ed 100644 --- a/src/registrar/views/domain.py +++ b/src/registrar/views/domain.py @@ -298,7 +298,7 @@ class DomainNameserversView(DomainFormBaseView): # Remove any leading or trailing whitespace from each IP in the list # this will return [] if no ips have been entered, which is taken # into account in the model in checkHostIPCombo - ip_list = [ip.strip() for ip in ip_list] + ip_list = [ip.replace(" ", "") for ip in ip_list] as_tuple = ( form.cleaned_data["server"], From 34629439ee1934b36e318a7fc8df81ef84afd07c Mon Sep 17 00:00:00 2001 From: David Kennedy Date: Mon, 30 Oct 2023 12:51:45 -0400 Subject: [PATCH 124/128] updated some js for readability and clean code --- src/registrar/assets/js/get-gov.js | 13 +++++-------- 1 file changed, 5 insertions(+), 8 deletions(-) diff --git a/src/registrar/assets/js/get-gov.js b/src/registrar/assets/js/get-gov.js index e456575c7..1c678a4d6 100644 --- a/src/registrar/assets/js/get-gov.js +++ b/src/registrar/assets/js/get-gov.js @@ -238,10 +238,8 @@ function handleValidationClick(e) { function prepareDeleteButtons(formLabel) { let deleteButtons = document.querySelectorAll(".delete-record"); let totalForms = document.querySelector("#id_form-TOTAL_FORMS"); - let isNameserversForm = false; + let isNameserversForm = document.title.includes("DNS name servers |"); let addButton = document.querySelector("#add-form"); - if (document.title.includes("DNS name servers |")) - isNameserversForm = true; // Loop through each delete button and attach the click event listener deleteButtons.forEach((deleteButton) => { @@ -256,7 +254,7 @@ function prepareDeleteButtons(formLabel) { let formNumberRegex = RegExp(`form-(\\d){1}-`, 'g'); let formLabelRegex = RegExp(`${formLabel} (\\d+){1}`, 'g'); - // For the eample on Nameservers + // For the example on Nameservers let formExampleRegex = RegExp(`ns(\\d+){1}`, 'g'); forms.forEach((form, index) => { @@ -305,7 +303,7 @@ function prepareDeleteButtons(formLabel) { } }); - // Remove the add more button if we have less than 13 forms + // Display the add more button if we have less than 13 forms if (isNameserversForm && forms.length <= 13) { addButton.classList.remove("display-none") } @@ -327,9 +325,8 @@ function prepareDeleteButtons(formLabel) { let totalForms = document.querySelector("#id_form-TOTAL_FORMS"); let cloneIndex = 0; let formLabel = ''; - let isNameserversForm = false; - if (document.title.includes("DNS name servers |")) { - isNameserversForm = true; + let isNameserversForm = document.title.includes("DNS name servers |"); + if (isNameserversForm) { cloneIndex = 2; formLabel = "Name server"; } else if ((document.title.includes("DS Data |")) || (document.title.includes("Key Data |"))) { From 91e2a7906bbeeba57aa04670ba7f433d1ce84f62 Mon Sep 17 00:00:00 2001 From: David Kennedy Date: Mon, 30 Oct 2023 14:23:14 -0400 Subject: [PATCH 125/128] updated validation and removal of whitespace; raise RegistryErrors on create_host and update_host --- src/registrar/forms/domain.py | 2 ++ src/registrar/models/domain.py | 4 ++-- src/registrar/views/domain.py | 4 ---- 3 files changed, 4 insertions(+), 6 deletions(-) diff --git a/src/registrar/forms/domain.py b/src/registrar/forms/domain.py index c7ebd0807..3aca7af6d 100644 --- a/src/registrar/forms/domain.py +++ b/src/registrar/forms/domain.py @@ -45,6 +45,8 @@ class DomainNameserverForm(forms.Form): self.clean_empty_strings(cleaned_data) server = cleaned_data.get("server", "") ip = cleaned_data.get("ip", None) + # remove ANY spaces in the ip field + ip = ip.replace(" ", "") domain = cleaned_data.get("domain", "") ip_list = self.extract_ip_list(ip) diff --git a/src/registrar/models/domain.py b/src/registrar/models/domain.py index acfdc1a7b..07e49dfdd 100644 --- a/src/registrar/models/domain.py +++ b/src/registrar/models/domain.py @@ -277,7 +277,7 @@ class Domain(TimeStampedModel, DomainHelper): return response.code except RegistryError as e: logger.error("Error _create_host, code was %s error was %s" % (e.code, e)) - return e.code + raise e def _convert_list_to_dict(self, listToConvert: list[tuple[str, list]]): """converts a list of hosts into a dictionary @@ -1593,7 +1593,7 @@ class Domain(TimeStampedModel, DomainHelper): return response.code except RegistryError as e: logger.error("Error _update_host, code was %s error was %s" % (e.code, e)) - return e.code + raise e def addAndRemoveHostsFromDomain( self, hostsToAdd: list[str], hostsToDelete: list[str] diff --git a/src/registrar/views/domain.py b/src/registrar/views/domain.py index 4fd01bd0c..ede44b1d5 100644 --- a/src/registrar/views/domain.py +++ b/src/registrar/views/domain.py @@ -295,10 +295,6 @@ class DomainNameserversView(DomainFormBaseView): if ip_string: # Split the string into a list using a comma as the delimiter ip_list = ip_string.split(",") - # Remove any leading or trailing whitespace from each IP in the list - # this will return [] if no ips have been entered, which is taken - # into account in the model in checkHostIPCombo - ip_list = [ip.replace(" ", "").strip() for ip in ip_list] as_tuple = ( form.cleaned_data["server"], From c81a9753c320057cfad847674e7a4a830ee749ed Mon Sep 17 00:00:00 2001 From: David Kennedy Date: Mon, 30 Oct 2023 14:35:09 -0400 Subject: [PATCH 126/128] comments on test cases --- src/registrar/tests/test_views.py | 15 ++++++++------- 1 file changed, 8 insertions(+), 7 deletions(-) diff --git a/src/registrar/tests/test_views.py b/src/registrar/tests/test_views.py index 29a89c740..95af4c542 100644 --- a/src/registrar/tests/test_views.py +++ b/src/registrar/tests/test_views.py @@ -1447,7 +1447,7 @@ class TestDomainNameservers(TestDomainOverview): self.assertContains(page, "DNS name servers") def test_domain_nameservers_form_submit_one_nameserver(self): - """Can change domain's nameservers. + """Nameserver form submitted with one nameserver throws error. Uses self.app WebTest because we need to interact with forms. """ @@ -1467,7 +1467,7 @@ class TestDomainNameservers(TestDomainOverview): self.assertContains(result, "This field is required.", count=2, status_code=200) def test_domain_nameservers_form_submit_subdomain_missing_ip(self): - """Can change domain's nameservers. + """Nameserver form catches missing ip error on subdomain. Uses self.app WebTest because we need to interact with forms. """ @@ -1493,7 +1493,7 @@ class TestDomainNameservers(TestDomainOverview): ) def test_domain_nameservers_form_submit_missing_host(self): - """Can change domain's nameservers. + """Nameserver form catches error when host is missing. Uses self.app WebTest because we need to interact with forms. """ @@ -1519,7 +1519,8 @@ class TestDomainNameservers(TestDomainOverview): ) def test_domain_nameservers_form_submit_glue_record_not_allowed(self): - """Can change domain's nameservers. + """Nameserver form catches error when IP is present + but host not subdomain. Uses self.app WebTest because we need to interact with forms. """ @@ -1550,7 +1551,7 @@ class TestDomainNameservers(TestDomainOverview): ) def test_domain_nameservers_form_submit_invalid_ip(self): - """Can change domain's nameservers. + """Nameserver form catches invalid IP on submission. Uses self.app WebTest because we need to interact with forms. """ @@ -1583,7 +1584,7 @@ class TestDomainNameservers(TestDomainOverview): ) def test_domain_nameservers_form_submits_successfully(self): - """Can change domain's nameservers. + """Nameserver form submits successfully with valid input. Uses self.app WebTest because we need to interact with forms. """ @@ -1614,7 +1615,7 @@ class TestDomainNameservers(TestDomainOverview): self.assertContains(page, "The name servers for this domain have been updated") def test_domain_nameservers_form_invalid(self): - """Can change domain's nameservers. + """Nameserver form does not submit with invalid data. Uses self.app WebTest because we need to interact with forms. """ From 65bb9ff2e1d28964e37d05db9b3cb0c86f43d0f3 Mon Sep 17 00:00:00 2001 From: Rebecca Hsieh Date: Mon, 30 Oct 2023 13:28:41 -0700 Subject: [PATCH 127/128] Fix typo --- src/registrar/templates/domain_dnssec.html | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/registrar/templates/domain_dnssec.html b/src/registrar/templates/domain_dnssec.html index 691ba79b2..c67a7243e 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 an 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 %} From 7eea1b5d83de4a2ad15d051359ef9cd828528aa0 Mon Sep 17 00:00:00 2001 From: Rebecca Hsieh Date: Mon, 30 Oct 2023 13:53:32 -0700 Subject: [PATCH 128/128] Fix wording from website to domain --- src/registrar/templates/domain_dnssec.html | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/registrar/templates/domain_dnssec.html b/src/registrar/templates/domain_dnssec.html index c67a7243e..c92ca2b78 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 an 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 an 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 %}