From df6d945c51c068242924904f002bdeeaa2f76301 Mon Sep 17 00:00:00 2001 From: Eric Hanson Date: Fri, 9 Feb 2024 14:48:38 -0500 Subject: [PATCH 01/18] s3 --- Pipfile | 6 +- Pipfile.lock | 1295 ++++++++++++++++++++++++++++------------- dsaps/cli.py | 18 +- dsaps/helpers.py | 17 +- dsaps/models.py | 104 ++-- tests/conftest.py | 61 +- tests/test_cli.py | 16 +- tests/test_helpers.py | 8 +- tests/test_models.py | 30 +- 9 files changed, 1054 insertions(+), 501 deletions(-) diff --git a/Pipfile b/Pipfile index 5d462fa..01f565d 100644 --- a/Pipfile +++ b/Pipfile @@ -6,7 +6,7 @@ verify_ssl = true [dev-packages] pytest = "*" requests-mock = "*" -black = "==21.7b0" +black = "*" isort = "*" flake8 = "*" bandit = "*" @@ -19,7 +19,9 @@ structlog = "*" attrs = "*" click = "*" lxml = "*" - +smart-open = {extras = ["s3"], version = "*"} +boto3 = "*" +moto = {extras = ["s3"], version = "*"} [requires] python_version = "3.9" diff --git a/Pipfile.lock b/Pipfile.lock index c87009f..db02088 100644 --- a/Pipfile.lock +++ b/Pipfile.lock @@ -1,7 +1,7 @@ { "_meta": { "hash": { - "sha256": "b7306361b6e1460eafdc6750f37b09015073781c76c98d1acbb3045f998621a4" + "sha256": "a0d2003f6babe8356f6e8909b9c3b998e270ed1109e43538a500e650f73688c0" }, "pipfile-spec": 6, "requires": { @@ -18,266 +18,797 @@ "default": { "attrs": { "hashes": [ - "sha256:149e90d6d8ac20db7a955ad60cf0e6881a3f20d37096140088356da6c716b0b1", - "sha256:ef6aaac3ca6cd92904cdd0d83f629a15f18053ec84e6432106f7a4d04ae4f5fb" + "sha256:935dc3b529c262f6cf76e50877d35a4bd3c1de194fd41f47a2b7ae8f19971f30", + "sha256:99b87a485a5820b23b879f04c2305b44b951b502fd64be915879d77a7e8fc6f1" ], "index": "pypi", - "version": "==21.2.0" + "markers": "python_version >= '3.7'", + "version": "==23.2.0" + }, + "boto3": { + "hashes": [ + "sha256:7c70c6ceb2706c7fad6466a5de174fe6d0d6f5f8f1e052bfaad9cbe4e53b64cd", + "sha256:e74fbad79bc921a74a9a276ef9f38e1e31153f76690fe9bc5ec790007de36572" + ], + "index": "pypi", + "markers": "python_version >= '3.8'", + "version": "==1.34.38" + }, + "botocore": { + "hashes": [ + "sha256:773e49f5bf596191e796b2a15096ff381e61778cbe7c982b381bb9f6bfe5fef3", + "sha256:da9754a8e1798706427ede9c9c0a55263bd8e57f217c021807b2946eb4a0c2d8" + ], + "markers": "python_version >= '3.8'", + "version": "==1.34.38" }, "certifi": { "hashes": [ - "sha256:84c85a9078b11105f04f3036a9482ae10e4621616db313fe045dd24743a0820d", - "sha256:fe86415d55e84719d75f8b69414f6438ac3547d2078ab91b67e779ef69378412" + "sha256:0569859f95fc761b18b45ef421b1290a0f65f147e92a1e5eb3e635f9a5e4e66f", + "sha256:dc383c07b76109f368f6106eee2b593b04a011ea4d55f652c6ca24a754d1cdd1" ], "markers": "python_version >= '3.6'", - "version": "==2022.6.15" + "version": "==2024.2.2" + }, + "cffi": { + "hashes": [ + "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" + ], + "markers": "platform_python_implementation != 'PyPy'", + "version": "==1.16.0" }, "charset-normalizer": { "hashes": [ - "sha256:2857e29ff0d34db842cd7ca3230549d1a697f96ee6d3fb071cfa6c7393832597", - "sha256:6881edbebdb17b39b4eaaa821b438bf6eddffb4468cf344f09f89def34a8b1df" + "sha256:06435b539f889b1f6f4ac1758871aae42dc3a8c0e24ac9e60c2384973ad73027", + "sha256:06a81e93cd441c56a9b65d8e1d043daeb97a3d0856d177d5c90ba85acb3db087", + "sha256:0a55554a2fa0d408816b3b5cedf0045f4b8e1a6065aec45849de2d6f3f8e9786", + "sha256:0b2b64d2bb6d3fb9112bafa732def486049e63de9618b5843bcdd081d8144cd8", + "sha256:10955842570876604d404661fbccbc9c7e684caf432c09c715ec38fbae45ae09", + "sha256:122c7fa62b130ed55f8f285bfd56d5f4b4a5b503609d181f9ad85e55c89f4185", + "sha256:1ceae2f17a9c33cb48e3263960dc5fc8005351ee19db217e9b1bb15d28c02574", + "sha256:1d3193f4a680c64b4b6a9115943538edb896edc190f0b222e73761716519268e", + "sha256:1f79682fbe303db92bc2b1136016a38a42e835d932bab5b3b1bfcfbf0640e519", + "sha256:2127566c664442652f024c837091890cb1942c30937add288223dc895793f898", + "sha256:22afcb9f253dac0696b5a4be4a1c0f8762f8239e21b99680099abd9b2b1b2269", + "sha256:25baf083bf6f6b341f4121c2f3c548875ee6f5339300e08be3f2b2ba1721cdd3", + "sha256:2e81c7b9c8979ce92ed306c249d46894776a909505d8f5a4ba55b14206e3222f", + "sha256:3287761bc4ee9e33561a7e058c72ac0938c4f57fe49a09eae428fd88aafe7bb6", + "sha256:34d1c8da1e78d2e001f363791c98a272bb734000fcef47a491c1e3b0505657a8", + "sha256:37e55c8e51c236f95b033f6fb391d7d7970ba5fe7ff453dad675e88cf303377a", + "sha256:3d47fa203a7bd9c5b6cee4736ee84ca03b8ef23193c0d1ca99b5089f72645c73", + "sha256:3e4d1f6587322d2788836a99c69062fbb091331ec940e02d12d179c1d53e25fc", + "sha256:42cb296636fcc8b0644486d15c12376cb9fa75443e00fb25de0b8602e64c1714", + "sha256:45485e01ff4d3630ec0d9617310448a8702f70e9c01906b0d0118bdf9d124cf2", + "sha256:4a78b2b446bd7c934f5dcedc588903fb2f5eec172f3d29e52a9096a43722adfc", + "sha256:4ab2fe47fae9e0f9dee8c04187ce5d09f48eabe611be8259444906793ab7cbce", + "sha256:4d0d1650369165a14e14e1e47b372cfcb31d6ab44e6e33cb2d4e57265290044d", + "sha256:549a3a73da901d5bc3ce8d24e0600d1fa85524c10287f6004fbab87672bf3e1e", + "sha256:55086ee1064215781fff39a1af09518bc9255b50d6333f2e4c74ca09fac6a8f6", + "sha256:572c3763a264ba47b3cf708a44ce965d98555f618ca42c926a9c1616d8f34269", + "sha256:573f6eac48f4769d667c4442081b1794f52919e7edada77495aaed9236d13a96", + "sha256:5b4c145409bef602a690e7cfad0a15a55c13320ff7a3ad7ca59c13bb8ba4d45d", + "sha256:6463effa3186ea09411d50efc7d85360b38d5f09b870c48e4600f63af490e56a", + "sha256:65f6f63034100ead094b8744b3b97965785388f308a64cf8d7c34f2f2e5be0c4", + "sha256:663946639d296df6a2bb2aa51b60a2454ca1cb29835324c640dafb5ff2131a77", + "sha256:6897af51655e3691ff853668779c7bad41579facacf5fd7253b0133308cf000d", + "sha256:68d1f8a9e9e37c1223b656399be5d6b448dea850bed7d0f87a8311f1ff3dabb0", + "sha256:6ac7ffc7ad6d040517be39eb591cac5ff87416c2537df6ba3cba3bae290c0fed", + "sha256:6b3251890fff30ee142c44144871185dbe13b11bab478a88887a639655be1068", + "sha256:6c4caeef8fa63d06bd437cd4bdcf3ffefe6738fb1b25951440d80dc7df8c03ac", + "sha256:6ef1d82a3af9d3eecdba2321dc1b3c238245d890843e040e41e470ffa64c3e25", + "sha256:753f10e867343b4511128c6ed8c82f7bec3bd026875576dfd88483c5c73b2fd8", + "sha256:7cd13a2e3ddeed6913a65e66e94b51d80a041145a026c27e6bb76c31a853c6ab", + "sha256:7ed9e526742851e8d5cc9e6cf41427dfc6068d4f5a3bb03659444b4cabf6bc26", + "sha256:7f04c839ed0b6b98b1a7501a002144b76c18fb1c1850c8b98d458ac269e26ed2", + "sha256:802fe99cca7457642125a8a88a084cef28ff0cf9407060f7b93dca5aa25480db", + "sha256:80402cd6ee291dcb72644d6eac93785fe2c8b9cb30893c1af5b8fdd753b9d40f", + "sha256:8465322196c8b4d7ab6d1e049e4c5cb460d0394da4a27d23cc242fbf0034b6b5", + "sha256:86216b5cee4b06df986d214f664305142d9c76df9b6512be2738aa72a2048f99", + "sha256:87d1351268731db79e0f8e745d92493ee2841c974128ef629dc518b937d9194c", + "sha256:8bdb58ff7ba23002a4c5808d608e4e6c687175724f54a5dade5fa8c67b604e4d", + "sha256:8c622a5fe39a48f78944a87d4fb8a53ee07344641b0562c540d840748571b811", + "sha256:8d756e44e94489e49571086ef83b2bb8ce311e730092d2c34ca8f7d925cb20aa", + "sha256:8f4a014bc36d3c57402e2977dada34f9c12300af536839dc38c0beab8878f38a", + "sha256:9063e24fdb1e498ab71cb7419e24622516c4a04476b17a2dab57e8baa30d6e03", + "sha256:90d558489962fd4918143277a773316e56c72da56ec7aa3dc3dbbe20fdfed15b", + "sha256:923c0c831b7cfcb071580d3f46c4baf50f174be571576556269530f4bbd79d04", + "sha256:95f2a5796329323b8f0512e09dbb7a1860c46a39da62ecb2324f116fa8fdc85c", + "sha256:96b02a3dc4381e5494fad39be677abcb5e6634bf7b4fa83a6dd3112607547001", + "sha256:9f96df6923e21816da7e0ad3fd47dd8f94b2a5ce594e00677c0013018b813458", + "sha256:a10af20b82360ab00827f916a6058451b723b4e65030c5a18577c8b2de5b3389", + "sha256:a50aebfa173e157099939b17f18600f72f84eed3049e743b68ad15bd69b6bf99", + "sha256:a981a536974bbc7a512cf44ed14938cf01030a99e9b3a06dd59578882f06f985", + "sha256:a9a8e9031d613fd2009c182b69c7b2c1ef8239a0efb1df3f7c8da66d5dd3d537", + "sha256:ae5f4161f18c61806f411a13b0310bea87f987c7d2ecdbdaad0e94eb2e404238", + "sha256:aed38f6e4fb3f5d6bf81bfa990a07806be9d83cf7bacef998ab1a9bd660a581f", + "sha256:b01b88d45a6fcb69667cd6d2f7a9aeb4bf53760d7fc536bf679ec94fe9f3ff3d", + "sha256:b261ccdec7821281dade748d088bb6e9b69e6d15b30652b74cbbac25e280b796", + "sha256:b2b0a0c0517616b6869869f8c581d4eb2dd83a4d79e0ebcb7d373ef9956aeb0a", + "sha256:b4a23f61ce87adf89be746c8a8974fe1c823c891d8f86eb218bb957c924bb143", + "sha256:bd8f7df7d12c2db9fab40bdd87a7c09b1530128315d047a086fa3ae3435cb3a8", + "sha256:beb58fe5cdb101e3a055192ac291b7a21e3b7ef4f67fa1d74e331a7f2124341c", + "sha256:c002b4ffc0be611f0d9da932eb0f704fe2602a9a949d1f738e4c34c75b0863d5", + "sha256:c083af607d2515612056a31f0a8d9e0fcb5876b7bfc0abad3ecd275bc4ebc2d5", + "sha256:c180f51afb394e165eafe4ac2936a14bee3eb10debc9d9e4db8958fe36afe711", + "sha256:c235ebd9baae02f1b77bcea61bce332cb4331dc3617d254df3323aa01ab47bd4", + "sha256:cd70574b12bb8a4d2aaa0094515df2463cb429d8536cfb6c7ce983246983e5a6", + "sha256:d0eccceffcb53201b5bfebb52600a5fb483a20b61da9dbc885f8b103cbe7598c", + "sha256:d965bba47ddeec8cd560687584e88cf699fd28f192ceb452d1d7ee807c5597b7", + "sha256:db364eca23f876da6f9e16c9da0df51aa4f104a972735574842618b8c6d999d4", + "sha256:ddbb2551d7e0102e7252db79ba445cdab71b26640817ab1e3e3648dad515003b", + "sha256:deb6be0ac38ece9ba87dea880e438f25ca3eddfac8b002a2ec3d9183a454e8ae", + "sha256:e06ed3eb3218bc64786f7db41917d4e686cc4856944f53d5bdf83a6884432e12", + "sha256:e27ad930a842b4c5eb8ac0016b0a54f5aebbe679340c26101df33424142c143c", + "sha256:e537484df0d8f426ce2afb2d0f8e1c3d0b114b83f8850e5f2fbea0e797bd82ae", + "sha256:eb00ed941194665c332bf8e078baf037d6c35d7c4f3102ea2d4f16ca94a26dc8", + "sha256:eb6904c354526e758fda7167b33005998fb68c46fbc10e013ca97f21ca5c8887", + "sha256:eb8821e09e916165e160797a6c17edda0679379a4be5c716c260e836e122f54b", + "sha256:efcb3f6676480691518c177e3b465bcddf57cea040302f9f4e6e191af91174d4", + "sha256:f27273b60488abe721a075bcca6d7f3964f9f6f067c8c4c605743023d7d3944f", + "sha256:f30c3cb33b24454a82faecaf01b19c18562b1e89558fb6c56de4d9118a032fd5", + "sha256:fb69256e180cb6c8a894fee62b3afebae785babc1ee98b81cdf68bbca1987f33", + "sha256:fd1abc0d89e30cc4e02e4064dc67fcc51bd941eb395c502aac3ec19fab46b519", + "sha256:ff8fa367d09b717b2a17a052544193ad76cd49979c805768879cb63d9ca50561" ], - "markers": "python_version >= '3'", - "version": "==2.0.12" + "markers": "python_full_version >= '3.7.0'", + "version": "==3.3.2" }, "click": { "hashes": [ - "sha256:8c04c11192119b1ef78ea049e0a6f0463e4c48ef00a30160c704337586f3ad7a", - "sha256:fba402a4a47334742d782209a7c79bc448911afe1149d07bdabdf480b3e2f4b6" + "sha256:ae74fb96c20a0277a1d615f1e4d73c8414f5a98db8b799a7931d1582f3390c28", + "sha256:ca9853ad459e787e2192211578cc907e7594e294c7ccc834310722b41b9ca6de" ], "index": "pypi", - "version": "==8.0.1" + "markers": "python_version >= '3.7'", + "version": "==8.1.7" + }, + "cryptography": { + "hashes": [ + "sha256:087887e55e0b9c8724cf05361357875adb5c20dec27e5816b653492980d20380", + "sha256:09a77e5b2e8ca732a19a90c5bca2d124621a1edb5438c5daa2d2738bfeb02589", + "sha256:130c0f77022b2b9c99d8cebcdd834d81705f61c68e91ddd614ce74c657f8b3ea", + "sha256:141e2aa5ba100d3788c0ad7919b288f89d1fe015878b9659b307c9ef867d3a65", + "sha256:28cb2c41f131a5758d6ba6a0504150d644054fd9f3203a1e8e8d7ac3aea7f73a", + "sha256:2f9f14185962e6a04ab32d1abe34eae8a9001569ee4edb64d2304bf0d65c53f3", + "sha256:320948ab49883557a256eab46149df79435a22d2fefd6a66fe6946f1b9d9d008", + "sha256:36d4b7c4be6411f58f60d9ce555a73df8406d484ba12a63549c88bd64f7967f1", + "sha256:3b15c678f27d66d247132cbf13df2f75255627bcc9b6a570f7d2fd08e8c081d2", + "sha256:3dbd37e14ce795b4af61b89b037d4bc157f2cb23e676fa16932185a04dfbf635", + "sha256:4383b47f45b14459cab66048d384614019965ba6c1a1a141f11b5a551cace1b2", + "sha256:44c95c0e96b3cb628e8452ec060413a49002a247b2b9938989e23a2c8291fc90", + "sha256:4b063d3413f853e056161eb0c7724822a9740ad3caa24b8424d776cebf98e7ee", + "sha256:52ed9ebf8ac602385126c9a2fe951db36f2cb0c2538d22971487f89d0de4065a", + "sha256:55d1580e2d7e17f45d19d3b12098e352f3a37fe86d380bf45846ef257054b242", + "sha256:5ef9bc3d046ce83c4bbf4c25e1e0547b9c441c01d30922d812e887dc5f125c12", + "sha256:5fa82a26f92871eca593b53359c12ad7949772462f887c35edaf36f87953c0e2", + "sha256:61321672b3ac7aade25c40449ccedbc6db72c7f5f0fdf34def5e2f8b51ca530d", + "sha256:701171f825dcab90969596ce2af253143b93b08f1a716d4b2a9d2db5084ef7be", + "sha256:841ec8af7a8491ac76ec5a9522226e287187a3107e12b7d686ad354bb78facee", + "sha256:8a06641fb07d4e8f6c7dda4fc3f8871d327803ab6542e33831c7ccfdcb4d0ad6", + "sha256:8e88bb9eafbf6a4014d55fb222e7360eef53e613215085e65a13290577394529", + "sha256:a00aee5d1b6c20620161984f8ab2ab69134466c51f58c052c11b076715e72929", + "sha256:a047682d324ba56e61b7ea7c7299d51e61fd3bca7dad2ccc39b72bd0118d60a1", + "sha256:a7ef8dd0bf2e1d0a27042b231a3baac6883cdd5557036f5e8df7139255feaac6", + "sha256:ad28cff53f60d99a928dfcf1e861e0b2ceb2bc1f08a074fdd601b314e1cc9e0a", + "sha256:b9097a208875fc7bbeb1286d0125d90bdfed961f61f214d3f5be62cd4ed8a446", + "sha256:b97fe7d7991c25e6a31e5d5e795986b18fbbb3107b873d5f3ae6dc9a103278e9", + "sha256:e0ec52ba3c7f1b7d813cd52649a5b3ef1fc0d433219dc8c93827c57eab6cf888", + "sha256:ea2c3ffb662fec8bbbfce5602e2c159ff097a4631d96235fcf0fb00e59e3ece4", + "sha256:fa3dec4ba8fb6e662770b74f62f1a0c7d4e37e25b58b2bf2c1be4c95372b4a33", + "sha256:fbeb725c9dc799a574518109336acccaf1303c30d45c075c665c0793c2f79a7f" + ], + "markers": "python_version >= '3.7'", + "version": "==42.0.2" }, "idna": { "hashes": [ - "sha256:84d9dd047ffa80596e0f246e2eab0b391788b0503584e8945f2368256d2735ff", - "sha256:9d643ff0a55b762d5cdb124b8eaa99c66322e2157b69160bc32796e824360e6d" + "sha256:9ecdbbd083b06798ae1e86adcbfe8ab1479cf864e4ee30fe4e46a003d12491ca", + "sha256:c05567e9c24a6b9faaa835c4821bad0590fbb9d5779e7caa6e1cc4978e7eb24f" + ], + "markers": "python_version >= '3.5'", + "version": "==3.6" + }, + "jinja2": { + "hashes": [ + "sha256:7d6d50dd97d52cbc355597bd845fabfbac3f551e1f99619e39a35ce8c370b5fa", + "sha256:ac8bd6544d4bb2c9792bf3a159e80bba8fda7f07e81bc3aed565432d5925ba90" ], - "markers": "python_version >= '3'", - "version": "==3.3" + "markers": "python_version >= '3.7'", + "version": "==3.1.3" + }, + "jmespath": { + "hashes": [ + "sha256:02e2e4cc71b5bcab88332eebf907519190dd9e6e82107fa7f83b1003a6252980", + "sha256:90261b206d6defd58fdd5e85f478bf633a2901798906be2ad389150c5c60edbe" + ], + "markers": "python_version >= '3.7'", + "version": "==1.0.1" }, "lxml": { "hashes": [ - "sha256:04da965dfebb5dac2619cb90fcf93efdb35b3c6994fea58a157a834f2f94b318", - "sha256:0538747a9d7827ce3e16a8fdd201a99e661c7dee3c96c885d8ecba3c35d1032c", - "sha256:0645e934e940107e2fdbe7c5b6fb8ec6232444260752598bc4d09511bd056c0b", - "sha256:079b68f197c796e42aa80b1f739f058dcee796dc725cc9a1be0cdb08fc45b000", - "sha256:0f3f0059891d3254c7b5fb935330d6db38d6519ecd238ca4fce93c234b4a0f73", - "sha256:10d2017f9150248563bb579cd0d07c61c58da85c922b780060dcc9a3aa9f432d", - "sha256:1355755b62c28950f9ce123c7a41460ed9743c699905cbe664a5bcc5c9c7c7fb", - "sha256:13c90064b224e10c14dcdf8086688d3f0e612db53766e7478d7754703295c7c8", - "sha256:1423631e3d51008871299525b541413c9b6c6423593e89f9c4cfbe8460afc0a2", - "sha256:1436cf0063bba7888e43f1ba8d58824f085410ea2025befe81150aceb123e345", - "sha256:1a7c59c6ffd6ef5db362b798f350e24ab2cfa5700d53ac6681918f314a4d3b94", - "sha256:1e1cf47774373777936c5aabad489fef7b1c087dcd1f426b621fda9dcc12994e", - "sha256:206a51077773c6c5d2ce1991327cda719063a47adc02bd703c56a662cdb6c58b", - "sha256:21fb3d24ab430fc538a96e9fbb9b150029914805d551deeac7d7822f64631dfc", - "sha256:27e590352c76156f50f538dbcebd1925317a0f70540f7dc8c97d2931c595783a", - "sha256:287605bede6bd36e930577c5925fcea17cb30453d96a7b4c63c14a257118dbb9", - "sha256:2aaf6a0a6465d39b5ca69688fce82d20088c1838534982996ec46633dc7ad6cc", - "sha256:32a73c53783becdb7eaf75a2a1525ea8e49379fb7248c3eeefb9412123536387", - "sha256:41fb58868b816c202e8881fd0f179a4644ce6e7cbbb248ef0283a34b73ec73bb", - "sha256:4780677767dd52b99f0af1f123bc2c22873d30b474aa0e2fc3fe5e02217687c7", - "sha256:4878e667ebabe9b65e785ac8da4d48886fe81193a84bbe49f12acff8f7a383a4", - "sha256:487c8e61d7acc50b8be82bda8c8d21d20e133c3cbf41bd8ad7eb1aaeb3f07c97", - "sha256:4beea0f31491bc086991b97517b9683e5cfb369205dac0148ef685ac12a20a67", - "sha256:4cfbe42c686f33944e12f45a27d25a492cc0e43e1dc1da5d6a87cbcaf2e95627", - "sha256:4d5bae0a37af799207140652a700f21a85946f107a199bcb06720b13a4f1f0b7", - "sha256:4e285b5f2bf321fc0857b491b5028c5f276ec0c873b985d58d7748ece1d770dd", - "sha256:57e4d637258703d14171b54203fd6822fda218c6c2658a7d30816b10995f29f3", - "sha256:5974895115737a74a00b321e339b9c3f45c20275d226398ae79ac008d908bff7", - "sha256:5ef87fca280fb15342726bd5f980f6faf8b84a5287fcc2d4962ea8af88b35130", - "sha256:603a464c2e67d8a546ddaa206d98e3246e5db05594b97db844c2f0a1af37cf5b", - "sha256:6653071f4f9bac46fbc30f3c7838b0e9063ee335908c5d61fb7a4a86c8fd2036", - "sha256:6ca2264f341dd81e41f3fffecec6e446aa2121e0b8d026fb5130e02de1402785", - "sha256:6d279033bf614953c3fc4a0aa9ac33a21e8044ca72d4fa8b9273fe75359d5cca", - "sha256:6d949f53ad4fc7cf02c44d6678e7ff05ec5f5552b235b9e136bd52e9bf730b91", - "sha256:6daa662aba22ef3258934105be2dd9afa5bb45748f4f702a3b39a5bf53a1f4dc", - "sha256:6eafc048ea3f1b3c136c71a86db393be36b5b3d9c87b1c25204e7d397cee9536", - "sha256:830c88747dce8a3e7525defa68afd742b4580df6aa2fdd6f0855481e3994d391", - "sha256:86e92728ef3fc842c50a5cb1d5ba2bc66db7da08a7af53fb3da79e202d1b2cd3", - "sha256:8caf4d16b31961e964c62194ea3e26a0e9561cdf72eecb1781458b67ec83423d", - "sha256:8d1a92d8e90b286d491e5626af53afef2ba04da33e82e30744795c71880eaa21", - "sha256:8f0a4d179c9a941eb80c3a63cdb495e539e064f8054230844dcf2fcb812b71d3", - "sha256:9232b09f5efee6a495a99ae6824881940d6447debe272ea400c02e3b68aad85d", - "sha256:927a9dd016d6033bc12e0bf5dee1dde140235fc8d0d51099353c76081c03dc29", - "sha256:93e414e3206779ef41e5ff2448067213febf260ba747fc65389a3ddaa3fb8715", - "sha256:98cafc618614d72b02185ac583c6f7796202062c41d2eeecdf07820bad3295ed", - "sha256:9c3a88d20e4fe4a2a4a84bf439a5ac9c9aba400b85244c63a1ab7088f85d9d25", - "sha256:9f36de4cd0c262dd9927886cc2305aa3f2210db437aa4fed3fb4940b8bf4592c", - "sha256:a60f90bba4c37962cbf210f0188ecca87daafdf60271f4c6948606e4dabf8785", - "sha256:a614e4afed58c14254e67862456d212c4dcceebab2eaa44d627c2ca04bf86837", - "sha256:ae06c1e4bc60ee076292e582a7512f304abdf6c70db59b56745cca1684f875a4", - "sha256:b122a188cd292c4d2fcd78d04f863b789ef43aa129b233d7c9004de08693728b", - "sha256:b570da8cd0012f4af9fa76a5635cd31f707473e65a5a335b186069d5c7121ff2", - "sha256:bcaa1c495ce623966d9fc8a187da80082334236a2a1c7e141763ffaf7a405067", - "sha256:bd34f6d1810d9354dc7e35158aa6cc33456be7706df4420819af6ed966e85448", - "sha256:be9eb06489bc975c38706902cbc6888f39e946b81383abc2838d186f0e8b6a9d", - "sha256:c4b2e0559b68455c085fb0f6178e9752c4be3bba104d6e881eb5573b399d1eb2", - "sha256:c62e8dd9754b7debda0c5ba59d34509c4688f853588d75b53c3791983faa96fc", - "sha256:c852b1530083a620cb0de5f3cd6826f19862bafeaf77586f1aef326e49d95f0c", - "sha256:d9fc0bf3ff86c17348dfc5d322f627d78273eba545db865c3cd14b3f19e57fa5", - "sha256:dad7b164905d3e534883281c050180afcf1e230c3d4a54e8038aa5cfcf312b84", - "sha256:e5f66bdf0976ec667fc4594d2812a00b07ed14d1b44259d19a41ae3fff99f2b8", - "sha256:e8f0c9d65da595cfe91713bc1222af9ecabd37971762cb830dea2fc3b3bb2acf", - "sha256:edffbe3c510d8f4bf8640e02ca019e48a9b72357318383ca60e3330c23aaffc7", - "sha256:eea5d6443b093e1545ad0210e6cf27f920482bfcf5c77cdc8596aec73523bb7e", - "sha256:ef72013e20dd5ba86a8ae1aed7f56f31d3374189aa8b433e7b12ad182c0d2dfb", - "sha256:f05251bbc2145349b8d0b77c0d4e5f3b228418807b1ee27cefb11f69ed3d233b", - "sha256:f1be258c4d3dc609e654a1dc59d37b17d7fef05df912c01fc2e15eb43a9735f3", - "sha256:f9ced82717c7ec65a67667bb05865ffe38af0e835cdd78728f1209c8fffe0cad", - "sha256:fe17d10b97fdf58155f858606bddb4e037b805a60ae023c009f760d8361a4eb8", - "sha256:fe749b052bb7233fe5d072fcb549221a8cb1a16725c47c37e42b0b9cb3ff2c3f" + "sha256:13521a321a25c641b9ea127ef478b580b5ec82aa2e9fc076c86169d161798b01", + "sha256:14deca1460b4b0f6b01f1ddc9557704e8b365f55c63070463f6c18619ebf964f", + "sha256:16018f7099245157564d7148165132c70adb272fb5a17c048ba70d9cc542a1a1", + "sha256:16dd953fb719f0ffc5bc067428fc9e88f599e15723a85618c45847c96f11f431", + "sha256:19a1bc898ae9f06bccb7c3e1dfd73897ecbbd2c96afe9095a6026016e5ca97b8", + "sha256:1ad17c20e3666c035db502c78b86e58ff6b5991906e55bdbef94977700c72623", + "sha256:22b7ee4c35f374e2c20337a95502057964d7e35b996b1c667b5c65c567d2252a", + "sha256:24ef5a4631c0b6cceaf2dbca21687e29725b7c4e171f33a8f8ce23c12558ded1", + "sha256:25663d6e99659544ee8fe1b89b1a8c0aaa5e34b103fab124b17fa958c4a324a6", + "sha256:262bc5f512a66b527d026518507e78c2f9c2bd9eb5c8aeeb9f0eb43fcb69dc67", + "sha256:280f3edf15c2a967d923bcfb1f8f15337ad36f93525828b40a0f9d6c2ad24890", + "sha256:2ad3a8ce9e8a767131061a22cd28fdffa3cd2dc193f399ff7b81777f3520e372", + "sha256:2befa20a13f1a75c751f47e00929fb3433d67eb9923c2c0b364de449121f447c", + "sha256:2f37c6d7106a9d6f0708d4e164b707037b7380fcd0b04c5bd9cae1fb46a856fb", + "sha256:304128394c9c22b6569eba2a6d98392b56fbdfbad58f83ea702530be80d0f9df", + "sha256:342e95bddec3a698ac24378d61996b3ee5ba9acfeb253986002ac53c9a5f6f84", + "sha256:3aeca824b38ca78d9ee2ab82bd9883083d0492d9d17df065ba3b94e88e4d7ee6", + "sha256:3d184e0d5c918cff04cdde9dbdf9600e960161d773666958c9d7b565ccc60c45", + "sha256:3e3898ae2b58eeafedfe99e542a17859017d72d7f6a63de0f04f99c2cb125936", + "sha256:3eea6ed6e6c918e468e693c41ef07f3c3acc310b70ddd9cc72d9ef84bc9564ca", + "sha256:3f14a4fb1c1c402a22e6a341a24c1341b4a3def81b41cd354386dcb795f83897", + "sha256:436a943c2900bb98123b06437cdd30580a61340fbdb7b28aaf345a459c19046a", + "sha256:4946e7f59b7b6a9e27bef34422f645e9a368cb2be11bf1ef3cafc39a1f6ba68d", + "sha256:49a9b4af45e8b925e1cd6f3b15bbba2c81e7dba6dce170c677c9cda547411e14", + "sha256:4f8b0c78e7aac24979ef09b7f50da871c2de2def043d468c4b41f512d831e912", + "sha256:52427a7eadc98f9e62cb1368a5079ae826f94f05755d2d567d93ee1bc3ceb354", + "sha256:5e53d7e6a98b64fe54775d23a7c669763451340c3d44ad5e3a3b48a1efbdc96f", + "sha256:5fcfbebdb0c5d8d18b84118842f31965d59ee3e66996ac842e21f957eb76138c", + "sha256:601f4a75797d7a770daed8b42b97cd1bb1ba18bd51a9382077a6a247a12aa38d", + "sha256:61c5a7edbd7c695e54fca029ceb351fc45cd8860119a0f83e48be44e1c464862", + "sha256:6a2a2c724d97c1eb8cf966b16ca2915566a4904b9aad2ed9a09c748ffe14f969", + "sha256:6d48fc57e7c1e3df57be5ae8614bab6d4e7b60f65c5457915c26892c41afc59e", + "sha256:6f11b77ec0979f7e4dc5ae081325a2946f1fe424148d3945f943ceaede98adb8", + "sha256:704f5572ff473a5f897745abebc6df40f22d4133c1e0a1f124e4f2bd3330ff7e", + "sha256:725e171e0b99a66ec8605ac77fa12239dbe061482ac854d25720e2294652eeaa", + "sha256:7cfced4a069003d8913408e10ca8ed092c49a7f6cefee9bb74b6b3e860683b45", + "sha256:7ec465e6549ed97e9f1e5ed51c657c9ede767bc1c11552f7f4d022c4df4a977a", + "sha256:82bddf0e72cb2af3cbba7cec1d2fd11fda0de6be8f4492223d4a268713ef2147", + "sha256:82cd34f1081ae4ea2ede3d52f71b7be313756e99b4b5f829f89b12da552d3aa3", + "sha256:843b9c835580d52828d8f69ea4302537337a21e6b4f1ec711a52241ba4a824f3", + "sha256:877efb968c3d7eb2dad540b6cabf2f1d3c0fbf4b2d309a3c141f79c7e0061324", + "sha256:8b9f19df998761babaa7f09e6bc169294eefafd6149aaa272081cbddc7ba4ca3", + "sha256:8cf5877f7ed384dabfdcc37922c3191bf27e55b498fecece9fd5c2c7aaa34c33", + "sha256:8d2900b7f5318bc7ad8631d3d40190b95ef2aa8cc59473b73b294e4a55e9f30f", + "sha256:8d7b4beebb178e9183138f552238f7e6613162a42164233e2bda00cb3afac58f", + "sha256:8f52fe6859b9db71ee609b0c0a70fea5f1e71c3462ecf144ca800d3f434f0764", + "sha256:98f3f020a2b736566c707c8e034945c02aa94e124c24f77ca097c446f81b01f1", + "sha256:9aa543980ab1fbf1720969af1d99095a548ea42e00361e727c58a40832439114", + "sha256:9b99f564659cfa704a2dd82d0684207b1aadf7d02d33e54845f9fc78e06b7581", + "sha256:9bcf86dfc8ff3e992fed847c077bd875d9e0ba2fa25d859c3a0f0f76f07f0c8d", + "sha256:9bd0ae7cc2b85320abd5e0abad5ccee5564ed5f0cc90245d2f9a8ef330a8deae", + "sha256:9d3c0f8567ffe7502d969c2c1b809892dc793b5d0665f602aad19895f8d508da", + "sha256:9e5ac3437746189a9b4121db2a7b86056ac8786b12e88838696899328fc44bb2", + "sha256:a36c506e5f8aeb40680491d39ed94670487ce6614b9d27cabe45d94cd5d63e1e", + "sha256:a5ab722ae5a873d8dcee1f5f45ddd93c34210aed44ff2dc643b5025981908cda", + "sha256:a96f02ba1bcd330807fc060ed91d1f7a20853da6dd449e5da4b09bfcc08fdcf5", + "sha256:acb6b2f96f60f70e7f34efe0c3ea34ca63f19ca63ce90019c6cbca6b676e81fa", + "sha256:ae15347a88cf8af0949a9872b57a320d2605ae069bcdf047677318bc0bba45b1", + "sha256:af8920ce4a55ff41167ddbc20077f5698c2e710ad3353d32a07d3264f3a2021e", + "sha256:afd825e30f8d1f521713a5669b63657bcfe5980a916c95855060048b88e1adb7", + "sha256:b21b4031b53d25b0858d4e124f2f9131ffc1530431c6d1321805c90da78388d1", + "sha256:b4b68c961b5cc402cbd99cca5eb2547e46ce77260eb705f4d117fd9c3f932b95", + "sha256:b66aa6357b265670bb574f050ffceefb98549c721cf28351b748be1ef9577d93", + "sha256:b9e240ae0ba96477682aa87899d94ddec1cc7926f9df29b1dd57b39e797d5ab5", + "sha256:bc64d1b1dab08f679fb89c368f4c05693f58a9faf744c4d390d7ed1d8223869b", + "sha256:bf8443781533b8d37b295016a4b53c1494fa9a03573c09ca5104550c138d5c05", + "sha256:c26aab6ea9c54d3bed716b8851c8bfc40cb249b8e9880e250d1eddde9f709bf5", + "sha256:c3cd1fc1dc7c376c54440aeaaa0dcc803d2126732ff5c6b68ccd619f2e64be4f", + "sha256:c7257171bb8d4432fe9d6fdde4d55fdbe663a63636a17f7f9aaba9bcb3153ad7", + "sha256:d42e3a3fc18acc88b838efded0e6ec3edf3e328a58c68fbd36a7263a874906c8", + "sha256:d74fcaf87132ffc0447b3c685a9f862ffb5b43e70ea6beec2fb8057d5d2a1fea", + "sha256:d8c1d679df4361408b628f42b26a5d62bd3e9ba7f0c0e7969f925021554755aa", + "sha256:e856c1c7255c739434489ec9c8aa9cdf5179785d10ff20add308b5d673bed5cd", + "sha256:eac68f96539b32fce2c9b47eb7c25bb2582bdaf1bbb360d25f564ee9e04c542b", + "sha256:ed7326563024b6e91fef6b6c7a1a2ff0a71b97793ac33dbbcf38f6005e51ff6e", + "sha256:ed8c3d2cd329bf779b7ed38db176738f3f8be637bb395ce9629fc76f78afe3d4", + "sha256:f4c9bda132ad108b387c33fabfea47866af87f4ea6ffb79418004f0521e63204", + "sha256:f643ffd2669ffd4b5a3e9b41c909b72b2a1d5e4915da90a77e119b8d48ce867a" ], "index": "pypi", - "version": "==4.9.1" + "markers": "python_version >= '3.6'", + "version": "==5.1.0" + }, + "markupsafe": { + "hashes": [ + "sha256:00e046b6dd71aa03a41079792f8473dc494d564611a8f89bbbd7cb93295ebdcf", + "sha256:075202fa5b72c86ad32dc7d0b56024ebdbcf2048c0ba09f1cde31bfdd57bcfff", + "sha256:0e397ac966fdf721b2c528cf028494e86172b4feba51d65f81ffd65c63798f3f", + "sha256:17b950fccb810b3293638215058e432159d2b71005c74371d784862b7e4683f3", + "sha256:1f3fbcb7ef1f16e48246f704ab79d79da8a46891e2da03f8783a5b6fa41a9532", + "sha256:2174c595a0d73a3080ca3257b40096db99799265e1c27cc5a610743acd86d62f", + "sha256:2b7c57a4dfc4f16f7142221afe5ba4e093e09e728ca65c51f5620c9aaeb9a617", + "sha256:2d2d793e36e230fd32babe143b04cec8a8b3eb8a3122d2aceb4a371e6b09b8df", + "sha256:30b600cf0a7ac9234b2638fbc0fb6158ba5bdcdf46aeb631ead21248b9affbc4", + "sha256:397081c1a0bfb5124355710fe79478cdbeb39626492b15d399526ae53422b906", + "sha256:3a57fdd7ce31c7ff06cdfbf31dafa96cc533c21e443d57f5b1ecc6cdc668ec7f", + "sha256:3c6b973f22eb18a789b1460b4b91bf04ae3f0c4234a0a6aa6b0a92f6f7b951d4", + "sha256:3e53af139f8579a6d5f7b76549125f0d94d7e630761a2111bc431fd820e163b8", + "sha256:4096e9de5c6fdf43fb4f04c26fb114f61ef0bf2e5604b6ee3019d51b69e8c371", + "sha256:4275d846e41ecefa46e2015117a9f491e57a71ddd59bbead77e904dc02b1bed2", + "sha256:4c31f53cdae6ecfa91a77820e8b151dba54ab528ba65dfd235c80b086d68a465", + "sha256:4f11aa001c540f62c6166c7726f71f7573b52c68c31f014c25cc7901deea0b52", + "sha256:5049256f536511ee3f7e1b3f87d1d1209d327e818e6ae1365e8653d7e3abb6a6", + "sha256:58c98fee265677f63a4385256a6d7683ab1832f3ddd1e66fe948d5880c21a169", + "sha256:598e3276b64aff0e7b3451b72e94fa3c238d452e7ddcd893c3ab324717456bad", + "sha256:5b7b716f97b52c5a14bffdf688f971b2d5ef4029127f1ad7a513973cfd818df2", + "sha256:5dedb4db619ba5a2787a94d877bc8ffc0566f92a01c0ef214865e54ecc9ee5e0", + "sha256:619bc166c4f2de5caa5a633b8b7326fbe98e0ccbfacabd87268a2b15ff73a029", + "sha256:629ddd2ca402ae6dbedfceeba9c46d5f7b2a61d9749597d4307f943ef198fc1f", + "sha256:656f7526c69fac7f600bd1f400991cc282b417d17539a1b228617081106feb4a", + "sha256:6ec585f69cec0aa07d945b20805be741395e28ac1627333b1c5b0105962ffced", + "sha256:72b6be590cc35924b02c78ef34b467da4ba07e4e0f0454a2c5907f473fc50ce5", + "sha256:7502934a33b54030eaf1194c21c692a534196063db72176b0c4028e140f8f32c", + "sha256:7a68b554d356a91cce1236aa7682dc01df0edba8d043fd1ce607c49dd3c1edcf", + "sha256:7b2e5a267c855eea6b4283940daa6e88a285f5f2a67f2220203786dfa59b37e9", + "sha256:823b65d8706e32ad2df51ed89496147a42a2a6e01c13cfb6ffb8b1e92bc910bb", + "sha256:8590b4ae07a35970728874632fed7bd57b26b0102df2d2b233b6d9d82f6c62ad", + "sha256:8dd717634f5a044f860435c1d8c16a270ddf0ef8588d4887037c5028b859b0c3", + "sha256:8dec4936e9c3100156f8a2dc89c4b88d5c435175ff03413b443469c7c8c5f4d1", + "sha256:97cafb1f3cbcd3fd2b6fbfb99ae11cdb14deea0736fc2b0952ee177f2b813a46", + "sha256:a17a92de5231666cfbe003f0e4b9b3a7ae3afb1ec2845aadc2bacc93ff85febc", + "sha256:a549b9c31bec33820e885335b451286e2969a2d9e24879f83fe904a5ce59d70a", + "sha256:ac07bad82163452a6884fe8fa0963fb98c2346ba78d779ec06bd7a6262132aee", + "sha256:ae2ad8ae6ebee9d2d94b17fb62763125f3f374c25618198f40cbb8b525411900", + "sha256:b91c037585eba9095565a3556f611e3cbfaa42ca1e865f7b8015fe5c7336d5a5", + "sha256:bc1667f8b83f48511b94671e0e441401371dfd0f0a795c7daa4a3cd1dde55bea", + "sha256:bec0a414d016ac1a18862a519e54b2fd0fc8bbfd6890376898a6c0891dd82e9f", + "sha256:bf50cd79a75d181c9181df03572cdce0fbb75cc353bc350712073108cba98de5", + "sha256:bff1b4290a66b490a2f4719358c0cdcd9bafb6b8f061e45c7a2460866bf50c2e", + "sha256:c061bb86a71b42465156a3ee7bd58c8c2ceacdbeb95d05a99893e08b8467359a", + "sha256:c8b29db45f8fe46ad280a7294f5c3ec36dbac9491f2d1c17345be8e69cc5928f", + "sha256:ce409136744f6521e39fd8e2a24c53fa18ad67aa5bc7c2cf83645cce5b5c4e50", + "sha256:d050b3361367a06d752db6ead6e7edeb0009be66bc3bae0ee9d97fb326badc2a", + "sha256:d283d37a890ba4c1ae73ffadf8046435c76e7bc2247bbb63c00bd1a709c6544b", + "sha256:d9fad5155d72433c921b782e58892377c44bd6252b5af2f67f16b194987338a4", + "sha256:daa4ee5a243f0f20d528d939d06670a298dd39b1ad5f8a72a4275124a7819eff", + "sha256:db0b55e0f3cc0be60c1f19efdde9a637c32740486004f20d1cff53c3c0ece4d2", + "sha256:e61659ba32cf2cf1481e575d0462554625196a1f2fc06a1c777d3f48e8865d46", + "sha256:ea3d8a3d18833cf4304cd2fc9cbb1efe188ca9b5efef2bdac7adc20594a0e46b", + "sha256:ec6a563cff360b50eed26f13adc43e61bc0c04d94b8be985e6fb24b81f6dcfdf", + "sha256:f5dfb42c4604dddc8e4305050aa6deb084540643ed5804d7455b5df8fe16f5e5", + "sha256:fa173ec60341d6bb97a89f5ea19c85c5643c1e7dedebc22f5181eb73573142c5", + "sha256:fa9db3f79de01457b03d4f01b34cf91bc0048eb2c3846ff26f66687c2f6d16ab", + "sha256:fce659a462a1be54d2ffcacea5e3ba2d74daa74f30f5f143fe0c58636e355fdd", + "sha256:ffee1f21e5ef0d712f9033568f8344d5da8cc2869dbd08d87c84656e6a2d2f68" + ], + "markers": "python_version >= '3.7'", + "version": "==2.1.5" + }, + "moto": { + "extras": [ + "s3" + ], + "hashes": [ + "sha256:62b9798aef9028432194cebb7a671f4064257bb3be662d9c1b83b94411b694bb", + "sha256:94e3b07a403cc8078ffee94bf404ef677112d036a57ddb5e0f19c5fcf48987f5" + ], + "markers": "python_version >= '3.8'", + "version": "==5.0.1" + }, + "py-partiql-parser": { + "hashes": [ + "sha256:53053e70987dea2983e1990ad85f87a7d8cec13dd4a4b065a740bcfd661f5a6b", + "sha256:aeac8f46529d8651bbae88a1a6c14dc3aa38ebc4bc6bd1eb975044c0564246c6" + ], + "version": "==0.5.1" + }, + "pycparser": { + "hashes": [ + "sha256:8ee45429555515e1f6b185e78100aea234072576aa43ab53aefcae078162fca9", + "sha256:e644fdec12f7872f86c58ff790da456218b10f863970249516d60a5eaca77206" + ], + "version": "==2.21" + }, + "python-dateutil": { + "hashes": [ + "sha256:0123cacc1627ae19ddf3c27a5de5bd67ee4586fbdd6440d9748f8abb483d3e86", + "sha256:961d03dc3453ebbc59dbdea9e4e11c5651520a876d0f4db161e8674aae935da9" + ], + "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3'", + "version": "==2.8.2" + }, + "pyyaml": { + "hashes": [ + "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:a08c6f0fe150303c1c6b71ebcd7213c2858041a7e01975da3a99aed1e7a378ef", + "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" + ], + "version": "==6.0.1" }, "requests": { "hashes": [ - "sha256:6c1246513ecd5ecd4528a0906f910e8f0f9c6b8ec72030dc9fd154dc1a6efd24", - "sha256:b8aa58f8cf793ffd8782d3d8cb19e66ef36f7aba4353eec859e74678b01b07a7" + "sha256:58cd2187c01e70e6e26505bca751777aa9f2ee0b7f4300988b709f44e013003f", + "sha256:942c5a758f98d790eaed1a29cb6eefc7ffb0d1cf7af05c3d2791656dbd6ad1e1" ], "index": "pypi", - "version": "==2.26.0" + "markers": "python_version >= '3.7'", + "version": "==2.31.0" + }, + "responses": { + "hashes": [ + "sha256:a2b43f4c08bfb9c9bd242568328c65a34b318741d3fab884ac843c5ceeb543f9", + "sha256:b127c6ca3f8df0eb9cc82fd93109a3007a86acb24871834c47b77765152ecf8c" + ], + "markers": "python_version >= '3.8'", + "version": "==0.24.1" + }, + "s3transfer": { + "hashes": [ + "sha256:3cdb40f5cfa6966e812209d0994f2a4709b561c88e90cf00c2696d2df4e56b2e", + "sha256:d0c8bbf672d5eebbe4e57945e23b972d963f07d82f661cabf678a5c88831595b" + ], + "markers": "python_version >= '3.8'", + "version": "==0.10.0" + }, + "six": { + "hashes": [ + "sha256:1e61c37477a1626458e36f7b1d82aa5c9b094fa4802892072e49de9c60c4c926", + "sha256:8abb2f1d86890a2dfb989f9a77cfcfd3e47c2a354b01111771326f8aa26e0254" + ], + "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3'", + "version": "==1.16.0" + }, + "smart-open": { + "extras": [ + "s3" + ], + "hashes": [ + "sha256:8d3ef7e6997e8e42dd55c74166ed21e6ac70664caa32dd940b26d54a8f6b4142", + "sha256:be3c92c246fbe80ebce8fbacb180494a481a77fcdcb7c1aadb2ea5b9c2bee8b9" + ], + "markers": "python_version >= '3.6' and python_version < '4.0'", + "version": "==6.4.0" }, "structlog": { "hashes": [ - "sha256:62f06fc0ee32fb8580f0715eea66cb87271eb7efb0eaf9af6b639cba8981de47", - "sha256:d9d2d890532e8db83c6977a2a676fb1889922ff0c26ad4dc0ecac26f9fafbc57" + "sha256:3f6efe7d25fab6e86f277713c218044669906537bb717c1807a09d46bca0714d", + "sha256:41a09886e4d55df25bdcb9b5c9674bccfab723ff43e0a86a1b7b236be8e57b16" ], "index": "pypi", - "version": "==21.1.0" + "markers": "python_version >= '3.8'", + "version": "==24.1.0" }, "urllib3": { "hashes": [ - "sha256:44ece4d53fb1706f667c9bd1c648f5469a2ec925fcf3a776667042d645472c14", - "sha256:aabaf16477806a5e1dd19aa41f8c2b7950dd3c746362d7e3223dbe6de6ac448e" + "sha256:34b97092d7e0a3a8cf7cd10e386f401b3737364026c45e622aa02903dffe0f07", + "sha256:f8ecc1bba5667413457c529ab955bf8c67b45db799d159066261719e328580a0" ], - "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3, 3.4' and python_version < '4'", - "version": "==1.26.9" - } - }, - "develop": { - "appdirs": { + "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.18" + }, + "werkzeug": { "hashes": [ - "sha256:7d5d0167b2b1ba821647616af46a749d1c653740dd0d2415100fe26e27afdf41", - "sha256:a841dacd6b99318a741b166adb07e19ee71a274450e68237b4650ca1055ab128" + "sha256:507e811ecea72b18a404947aded4b3390e1db8f826b494d76550ef45bb3b1dcc", + "sha256:90a285dc0e42ad56b34e696398b8122ee4c681833fb35b8334a095d82c56da10" ], - "version": "==1.4.4" + "markers": "python_version >= '3.8'", + "version": "==3.0.1" }, - "attrs": { + "xmltodict": { "hashes": [ - "sha256:149e90d6d8ac20db7a955ad60cf0e6881a3f20d37096140088356da6c716b0b1", - "sha256:ef6aaac3ca6cd92904cdd0d83f629a15f18053ec84e6432106f7a4d04ae4f5fb" + "sha256:341595a488e3e01a85a9d8911d8912fd922ede5fecc4dce437eb4b6c8d037e56", + "sha256:aa89e8fd76320154a40d19a0df04a4695fb9dc5ba977cbb68ab3e4eb225e7852" ], - "index": "pypi", - "version": "==21.2.0" - }, + "markers": "python_version >= '3.4'", + "version": "==0.13.0" + } + }, + "develop": { "bandit": { "hashes": [ - "sha256:216be4d044209fa06cf2a3e51b319769a51be8318140659719aa7a115c35ed07", - "sha256:8a4c7415254d75df8ff3c3b15cfe9042ecee628a1e40b44c15a98890fbfc2608" + "sha256:17e60786a7ea3c9ec84569fd5aee09936d116cb0cb43151023258340dbffb7ed", + "sha256:527906bec6088cb499aae31bc962864b4e77569e9d529ee51df3a93b4b8ab28a" ], "index": "pypi", - "version": "==1.7.0" + "markers": "python_version >= '3.8'", + "version": "==1.7.7" }, "black": { "hashes": [ - "sha256:1c7aa6ada8ee864db745b22790a32f94b2795c253a75d6d9b5e439ff10d23116", - "sha256:c8373c6491de9362e39271630b65b964607bc5c79c83783547d76c839b3aa219" + "sha256:0269dfdea12442022e88043d2910429bed717b2d04523867a85dacce535916b8", + "sha256:07204d078e25327aad9ed2c64790d681238686bce254c910de640c7cc4fc3aa6", + "sha256:08b34e85170d368c37ca7bf81cf67ac863c9d1963b2c1780c39102187ec8dd62", + "sha256:1a95915c98d6e32ca43809d46d932e2abc5f1f7d582ffbe65a5b4d1588af7445", + "sha256:2588021038bd5ada078de606f2a804cadd0a3cc6a79cb3e9bb3a8bf581325a4c", + "sha256:2fa6a0e965779c8f2afb286f9ef798df770ba2b6cee063c650b96adec22c056a", + "sha256:34afe9da5056aa123b8bfda1664bfe6fb4e9c6f311d8e4a6eb089da9a9173bf9", + "sha256:3897ae5a21ca132efa219c029cce5e6bfc9c3d34ed7e892113d199c0b1b444a2", + "sha256:40657e1b78212d582a0edecafef133cf1dd02e6677f539b669db4746150d38f6", + "sha256:48b5760dcbfe5cf97fd4fba23946681f3a81514c6ab8a45b50da67ac8fbc6c7b", + "sha256:5242ecd9e990aeb995b6d03dc3b2d112d4a78f2083e5a8e86d566340ae80fec4", + "sha256:5cdc2e2195212208fbcae579b931407c1fa9997584f0a415421748aeafff1168", + "sha256:5d7b06ea8816cbd4becfe5f70accae953c53c0e53aa98730ceccb0395520ee5d", + "sha256:7258c27115c1e3b5de9ac6c4f9957e3ee2c02c0b39222a24dc7aa03ba0e986f5", + "sha256:854c06fb86fd854140f37fb24dbf10621f5dab9e3b0c29a690ba595e3d543024", + "sha256:a21725862d0e855ae05da1dd25e3825ed712eaaccef6b03017fe0853a01aa45e", + "sha256:a83fe522d9698d8f9a101b860b1ee154c1d25f8a82ceb807d319f085b2627c5b", + "sha256:b3d64db762eae4a5ce04b6e3dd745dcca0fb9560eb931a5be97472e38652a161", + "sha256:e298d588744efda02379521a19639ebcd314fba7a49be22136204d7ed1782717", + "sha256:e2c8dfa14677f90d976f68e0c923947ae68fa3961d61ee30976c388adc0b02c8", + "sha256:ecba2a15dfb2d97105be74bbfe5128bc5e9fa8477d8c46766505c1dda5883aac", + "sha256:fc1ec9aa6f4d98d022101e015261c056ddebe3da6a8ccfc2c792cbe0349d48b7" ], "index": "pypi", - "version": "==21.7b0" + "markers": "python_version >= '3.8'", + "version": "==24.1.1" }, "certifi": { "hashes": [ - "sha256:84c85a9078b11105f04f3036a9482ae10e4621616db313fe045dd24743a0820d", - "sha256:fe86415d55e84719d75f8b69414f6438ac3547d2078ab91b67e779ef69378412" + "sha256:0569859f95fc761b18b45ef421b1290a0f65f147e92a1e5eb3e635f9a5e4e66f", + "sha256:dc383c07b76109f368f6106eee2b593b04a011ea4d55f652c6ca24a754d1cdd1" ], "markers": "python_version >= '3.6'", - "version": "==2022.6.15" + "version": "==2024.2.2" }, "charset-normalizer": { "hashes": [ - "sha256:2857e29ff0d34db842cd7ca3230549d1a697f96ee6d3fb071cfa6c7393832597", - "sha256:6881edbebdb17b39b4eaaa821b438bf6eddffb4468cf344f09f89def34a8b1df" + "sha256:06435b539f889b1f6f4ac1758871aae42dc3a8c0e24ac9e60c2384973ad73027", + "sha256:06a81e93cd441c56a9b65d8e1d043daeb97a3d0856d177d5c90ba85acb3db087", + "sha256:0a55554a2fa0d408816b3b5cedf0045f4b8e1a6065aec45849de2d6f3f8e9786", + "sha256:0b2b64d2bb6d3fb9112bafa732def486049e63de9618b5843bcdd081d8144cd8", + "sha256:10955842570876604d404661fbccbc9c7e684caf432c09c715ec38fbae45ae09", + "sha256:122c7fa62b130ed55f8f285bfd56d5f4b4a5b503609d181f9ad85e55c89f4185", + "sha256:1ceae2f17a9c33cb48e3263960dc5fc8005351ee19db217e9b1bb15d28c02574", + "sha256:1d3193f4a680c64b4b6a9115943538edb896edc190f0b222e73761716519268e", + "sha256:1f79682fbe303db92bc2b1136016a38a42e835d932bab5b3b1bfcfbf0640e519", + "sha256:2127566c664442652f024c837091890cb1942c30937add288223dc895793f898", + "sha256:22afcb9f253dac0696b5a4be4a1c0f8762f8239e21b99680099abd9b2b1b2269", + "sha256:25baf083bf6f6b341f4121c2f3c548875ee6f5339300e08be3f2b2ba1721cdd3", + "sha256:2e81c7b9c8979ce92ed306c249d46894776a909505d8f5a4ba55b14206e3222f", + "sha256:3287761bc4ee9e33561a7e058c72ac0938c4f57fe49a09eae428fd88aafe7bb6", + "sha256:34d1c8da1e78d2e001f363791c98a272bb734000fcef47a491c1e3b0505657a8", + "sha256:37e55c8e51c236f95b033f6fb391d7d7970ba5fe7ff453dad675e88cf303377a", + "sha256:3d47fa203a7bd9c5b6cee4736ee84ca03b8ef23193c0d1ca99b5089f72645c73", + "sha256:3e4d1f6587322d2788836a99c69062fbb091331ec940e02d12d179c1d53e25fc", + "sha256:42cb296636fcc8b0644486d15c12376cb9fa75443e00fb25de0b8602e64c1714", + "sha256:45485e01ff4d3630ec0d9617310448a8702f70e9c01906b0d0118bdf9d124cf2", + "sha256:4a78b2b446bd7c934f5dcedc588903fb2f5eec172f3d29e52a9096a43722adfc", + "sha256:4ab2fe47fae9e0f9dee8c04187ce5d09f48eabe611be8259444906793ab7cbce", + "sha256:4d0d1650369165a14e14e1e47b372cfcb31d6ab44e6e33cb2d4e57265290044d", + "sha256:549a3a73da901d5bc3ce8d24e0600d1fa85524c10287f6004fbab87672bf3e1e", + "sha256:55086ee1064215781fff39a1af09518bc9255b50d6333f2e4c74ca09fac6a8f6", + "sha256:572c3763a264ba47b3cf708a44ce965d98555f618ca42c926a9c1616d8f34269", + "sha256:573f6eac48f4769d667c4442081b1794f52919e7edada77495aaed9236d13a96", + "sha256:5b4c145409bef602a690e7cfad0a15a55c13320ff7a3ad7ca59c13bb8ba4d45d", + "sha256:6463effa3186ea09411d50efc7d85360b38d5f09b870c48e4600f63af490e56a", + "sha256:65f6f63034100ead094b8744b3b97965785388f308a64cf8d7c34f2f2e5be0c4", + "sha256:663946639d296df6a2bb2aa51b60a2454ca1cb29835324c640dafb5ff2131a77", + "sha256:6897af51655e3691ff853668779c7bad41579facacf5fd7253b0133308cf000d", + "sha256:68d1f8a9e9e37c1223b656399be5d6b448dea850bed7d0f87a8311f1ff3dabb0", + "sha256:6ac7ffc7ad6d040517be39eb591cac5ff87416c2537df6ba3cba3bae290c0fed", + "sha256:6b3251890fff30ee142c44144871185dbe13b11bab478a88887a639655be1068", + "sha256:6c4caeef8fa63d06bd437cd4bdcf3ffefe6738fb1b25951440d80dc7df8c03ac", + "sha256:6ef1d82a3af9d3eecdba2321dc1b3c238245d890843e040e41e470ffa64c3e25", + "sha256:753f10e867343b4511128c6ed8c82f7bec3bd026875576dfd88483c5c73b2fd8", + "sha256:7cd13a2e3ddeed6913a65e66e94b51d80a041145a026c27e6bb76c31a853c6ab", + "sha256:7ed9e526742851e8d5cc9e6cf41427dfc6068d4f5a3bb03659444b4cabf6bc26", + "sha256:7f04c839ed0b6b98b1a7501a002144b76c18fb1c1850c8b98d458ac269e26ed2", + "sha256:802fe99cca7457642125a8a88a084cef28ff0cf9407060f7b93dca5aa25480db", + "sha256:80402cd6ee291dcb72644d6eac93785fe2c8b9cb30893c1af5b8fdd753b9d40f", + "sha256:8465322196c8b4d7ab6d1e049e4c5cb460d0394da4a27d23cc242fbf0034b6b5", + "sha256:86216b5cee4b06df986d214f664305142d9c76df9b6512be2738aa72a2048f99", + "sha256:87d1351268731db79e0f8e745d92493ee2841c974128ef629dc518b937d9194c", + "sha256:8bdb58ff7ba23002a4c5808d608e4e6c687175724f54a5dade5fa8c67b604e4d", + "sha256:8c622a5fe39a48f78944a87d4fb8a53ee07344641b0562c540d840748571b811", + "sha256:8d756e44e94489e49571086ef83b2bb8ce311e730092d2c34ca8f7d925cb20aa", + "sha256:8f4a014bc36d3c57402e2977dada34f9c12300af536839dc38c0beab8878f38a", + "sha256:9063e24fdb1e498ab71cb7419e24622516c4a04476b17a2dab57e8baa30d6e03", + "sha256:90d558489962fd4918143277a773316e56c72da56ec7aa3dc3dbbe20fdfed15b", + "sha256:923c0c831b7cfcb071580d3f46c4baf50f174be571576556269530f4bbd79d04", + "sha256:95f2a5796329323b8f0512e09dbb7a1860c46a39da62ecb2324f116fa8fdc85c", + "sha256:96b02a3dc4381e5494fad39be677abcb5e6634bf7b4fa83a6dd3112607547001", + "sha256:9f96df6923e21816da7e0ad3fd47dd8f94b2a5ce594e00677c0013018b813458", + "sha256:a10af20b82360ab00827f916a6058451b723b4e65030c5a18577c8b2de5b3389", + "sha256:a50aebfa173e157099939b17f18600f72f84eed3049e743b68ad15bd69b6bf99", + "sha256:a981a536974bbc7a512cf44ed14938cf01030a99e9b3a06dd59578882f06f985", + "sha256:a9a8e9031d613fd2009c182b69c7b2c1ef8239a0efb1df3f7c8da66d5dd3d537", + "sha256:ae5f4161f18c61806f411a13b0310bea87f987c7d2ecdbdaad0e94eb2e404238", + "sha256:aed38f6e4fb3f5d6bf81bfa990a07806be9d83cf7bacef998ab1a9bd660a581f", + "sha256:b01b88d45a6fcb69667cd6d2f7a9aeb4bf53760d7fc536bf679ec94fe9f3ff3d", + "sha256:b261ccdec7821281dade748d088bb6e9b69e6d15b30652b74cbbac25e280b796", + "sha256:b2b0a0c0517616b6869869f8c581d4eb2dd83a4d79e0ebcb7d373ef9956aeb0a", + "sha256:b4a23f61ce87adf89be746c8a8974fe1c823c891d8f86eb218bb957c924bb143", + "sha256:bd8f7df7d12c2db9fab40bdd87a7c09b1530128315d047a086fa3ae3435cb3a8", + "sha256:beb58fe5cdb101e3a055192ac291b7a21e3b7ef4f67fa1d74e331a7f2124341c", + "sha256:c002b4ffc0be611f0d9da932eb0f704fe2602a9a949d1f738e4c34c75b0863d5", + "sha256:c083af607d2515612056a31f0a8d9e0fcb5876b7bfc0abad3ecd275bc4ebc2d5", + "sha256:c180f51afb394e165eafe4ac2936a14bee3eb10debc9d9e4db8958fe36afe711", + "sha256:c235ebd9baae02f1b77bcea61bce332cb4331dc3617d254df3323aa01ab47bd4", + "sha256:cd70574b12bb8a4d2aaa0094515df2463cb429d8536cfb6c7ce983246983e5a6", + "sha256:d0eccceffcb53201b5bfebb52600a5fb483a20b61da9dbc885f8b103cbe7598c", + "sha256:d965bba47ddeec8cd560687584e88cf699fd28f192ceb452d1d7ee807c5597b7", + "sha256:db364eca23f876da6f9e16c9da0df51aa4f104a972735574842618b8c6d999d4", + "sha256:ddbb2551d7e0102e7252db79ba445cdab71b26640817ab1e3e3648dad515003b", + "sha256:deb6be0ac38ece9ba87dea880e438f25ca3eddfac8b002a2ec3d9183a454e8ae", + "sha256:e06ed3eb3218bc64786f7db41917d4e686cc4856944f53d5bdf83a6884432e12", + "sha256:e27ad930a842b4c5eb8ac0016b0a54f5aebbe679340c26101df33424142c143c", + "sha256:e537484df0d8f426ce2afb2d0f8e1c3d0b114b83f8850e5f2fbea0e797bd82ae", + "sha256:eb00ed941194665c332bf8e078baf037d6c35d7c4f3102ea2d4f16ca94a26dc8", + "sha256:eb6904c354526e758fda7167b33005998fb68c46fbc10e013ca97f21ca5c8887", + "sha256:eb8821e09e916165e160797a6c17edda0679379a4be5c716c260e836e122f54b", + "sha256:efcb3f6676480691518c177e3b465bcddf57cea040302f9f4e6e191af91174d4", + "sha256:f27273b60488abe721a075bcca6d7f3964f9f6f067c8c4c605743023d7d3944f", + "sha256:f30c3cb33b24454a82faecaf01b19c18562b1e89558fb6c56de4d9118a032fd5", + "sha256:fb69256e180cb6c8a894fee62b3afebae785babc1ee98b81cdf68bbca1987f33", + "sha256:fd1abc0d89e30cc4e02e4064dc67fcc51bd941eb395c502aac3ec19fab46b519", + "sha256:ff8fa367d09b717b2a17a052544193ad76cd49979c805768879cb63d9ca50561" ], - "markers": "python_version >= '3'", - "version": "==2.0.12" + "markers": "python_full_version >= '3.7.0'", + "version": "==3.3.2" }, "click": { "hashes": [ - "sha256:8c04c11192119b1ef78ea049e0a6f0463e4c48ef00a30160c704337586f3ad7a", - "sha256:fba402a4a47334742d782209a7c79bc448911afe1149d07bdabdf480b3e2f4b6" + "sha256:ae74fb96c20a0277a1d615f1e4d73c8414f5a98db8b799a7931d1582f3390c28", + "sha256:ca9853ad459e787e2192211578cc907e7594e294c7ccc834310722b41b9ca6de" ], "index": "pypi", - "version": "==8.0.1" + "markers": "python_version >= '3.7'", + "version": "==8.1.7" }, "coverage": { "hashes": [ - "sha256:004d1880bed2d97151facef49f08e255a20ceb6f9432df75f4eef018fdd5a78c", - "sha256:01d84219b5cdbfc8122223b39a954820929497a1cb1422824bb86b07b74594b6", - "sha256:040af6c32813fa3eae5305d53f18875bedd079960822ef8ec067a66dd8afcd45", - "sha256:06191eb60f8d8a5bc046f3799f8a07a2d7aefb9504b0209aff0b47298333302a", - "sha256:13034c4409db851670bc9acd836243aeee299949bd5673e11844befcb0149f03", - "sha256:13c4ee887eca0f4c5a247b75398d4114c37882658300e153113dafb1d76de529", - "sha256:184a47bbe0aa6400ed2d41d8e9ed868b8205046518c52464fde713ea06e3a74a", - "sha256:18ba8bbede96a2c3dde7b868de9dcbd55670690af0988713f0603f037848418a", - "sha256:1aa846f56c3d49205c952d8318e76ccc2ae23303351d9270ab220004c580cfe2", - "sha256:217658ec7187497e3f3ebd901afdca1af062b42cfe3e0dafea4cced3983739f6", - "sha256:24d4a7de75446be83244eabbff746d66b9240ae020ced65d060815fac3423759", - "sha256:2910f4d36a6a9b4214bb7038d537f015346f413a975d57ca6b43bf23d6563b53", - "sha256:2949cad1c5208b8298d5686d5a85b66aae46d73eec2c3e08c817dd3513e5848a", - "sha256:2a3859cb82dcbda1cfd3e6f71c27081d18aa251d20a17d87d26d4cd216fb0af4", - "sha256:2cafbbb3af0733db200c9b5f798d18953b1a304d3f86a938367de1567f4b5bff", - "sha256:2e0d881ad471768bf6e6c2bf905d183543f10098e3b3640fc029509530091502", - "sha256:30c77c1dc9f253283e34c27935fded5015f7d1abe83bc7821680ac444eaf7793", - "sha256:3487286bc29a5aa4b93a072e9592f22254291ce96a9fbc5251f566b6b7343cdb", - "sha256:372da284cfd642d8e08ef606917846fa2ee350f64994bebfbd3afb0040436905", - "sha256:41179b8a845742d1eb60449bdb2992196e211341818565abded11cfa90efb821", - "sha256:44d654437b8ddd9eee7d1eaee28b7219bec228520ff809af170488fd2fed3e2b", - "sha256:4a7697d8cb0f27399b0e393c0b90f0f1e40c82023ea4d45d22bce7032a5d7b81", - "sha256:51cb9476a3987c8967ebab3f0fe144819781fca264f57f89760037a2ea191cb0", - "sha256:52596d3d0e8bdf3af43db3e9ba8dcdaac724ba7b5ca3f6358529d56f7a166f8b", - "sha256:53194af30d5bad77fcba80e23a1441c71abfb3e01192034f8246e0d8f99528f3", - "sha256:5fec2d43a2cc6965edc0bb9e83e1e4b557f76f843a77a2496cbe719583ce8184", - "sha256:6c90e11318f0d3c436a42409f2749ee1a115cd8b067d7f14c148f1ce5574d701", - "sha256:74d881fc777ebb11c63736622b60cb9e4aee5cace591ce274fb69e582a12a61a", - "sha256:7501140f755b725495941b43347ba8a2777407fc7f250d4f5a7d2a1050ba8e82", - "sha256:796c9c3c79747146ebd278dbe1e5c5c05dd6b10cc3bcb8389dfdf844f3ead638", - "sha256:869a64f53488f40fa5b5b9dcb9e9b2962a66a87dab37790f3fcfb5144b996ef5", - "sha256:8963a499849a1fc54b35b1c9f162f4108017b2e6db2c46c1bed93a72262ed083", - "sha256:8d0a0725ad7c1a0bcd8d1b437e191107d457e2ec1084b9f190630a4fb1af78e6", - "sha256:900fbf7759501bc7807fd6638c947d7a831fc9fdf742dc10f02956ff7220fa90", - "sha256:92b017ce34b68a7d67bd6d117e6d443a9bf63a2ecf8567bb3d8c6c7bc5014465", - "sha256:970284a88b99673ccb2e4e334cfb38a10aab7cd44f7457564d11898a74b62d0a", - "sha256:972c85d205b51e30e59525694670de6a8a89691186012535f9d7dbaa230e42c3", - "sha256:9a1ef3b66e38ef8618ce5fdc7bea3d9f45f3624e2a66295eea5e57966c85909e", - "sha256:af0e781009aaf59e25c5a678122391cb0f345ac0ec272c7961dc5455e1c40066", - "sha256:b6d534e4b2ab35c9f93f46229363e17f63c53ad01330df9f2d6bd1187e5eaacf", - "sha256:b7895207b4c843c76a25ab8c1e866261bcfe27bfaa20c192de5190121770672b", - "sha256:c0891a6a97b09c1f3e073a890514d5012eb256845c451bd48f7968ef939bf4ae", - "sha256:c2723d347ab06e7ddad1a58b2a821218239249a9e4365eaff6649d31180c1669", - "sha256:d1f8bf7b90ba55699b3a5e44930e93ff0189aa27186e96071fac7dd0d06a1873", - "sha256:d1f9ce122f83b2305592c11d64f181b87153fc2c2bbd3bb4a3dde8303cfb1a6b", - "sha256:d314ed732c25d29775e84a960c3c60808b682c08d86602ec2c3008e1202e3bb6", - "sha256:d636598c8305e1f90b439dbf4f66437de4a5e3c31fdf47ad29542478c8508bbb", - "sha256:deee1077aae10d8fa88cb02c845cfba9b62c55e1183f52f6ae6a2df6a2187160", - "sha256:ebe78fe9a0e874362175b02371bdfbee64d8edc42a044253ddf4ee7d3c15212c", - "sha256:f030f8873312a16414c0d8e1a1ddff2d3235655a2174e3648b4fa66b3f2f1079", - "sha256:f0b278ce10936db1a37e6954e15a3730bea96a0997c26d7fee88e6c396c2086d", - "sha256:f11642dddbb0253cc8853254301b51390ba0081750a8ac03f20ea8103f0c56b6" - ], - "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3, 3.4' and python_version < '4'", - "version": "==5.5" + "sha256:027018943386e7b942fa832372ebc120155fd970837489896099f5cfa2890f79", + "sha256:11b990d520ea75e7ee8dcab5bc908072aaada194a794db9f6d7d5cfd19661e5a", + "sha256:12adf310e4aafddc58afdb04d686795f33f4d7a6fa67a7a9d4ce7d6ae24d949f", + "sha256:1431986dac3923c5945271f169f59c45b8802a114c8f548d611f2015133df77a", + "sha256:1ef221513e6f68b69ee9e159506d583d31aa3567e0ae84eaad9d6ec1107dddaa", + "sha256:20c8ac5386253717e5ccc827caad43ed66fea0efe255727b1053a8154d952398", + "sha256:2198ea6fc548de52adc826f62cb18554caedfb1d26548c1b7c88d8f7faa8f6ba", + "sha256:255758a1e3b61db372ec2736c8e2a1fdfaf563977eedbdf131de003ca5779b7d", + "sha256:265de0fa6778d07de30bcf4d9dc471c3dc4314a23a3c6603d356a3c9abc2dfcf", + "sha256:33a7da4376d5977fbf0a8ed91c4dffaaa8dbf0ddbf4c8eea500a2486d8bc4d7b", + "sha256:42eafe6778551cf006a7c43153af1211c3aaab658d4d66fa5fcc021613d02518", + "sha256:4433b90fae13f86fafff0b326453dd42fc9a639a0d9e4eec4d366436d1a41b6d", + "sha256:4a5375e28c5191ac38cca59b38edd33ef4cc914732c916f2929029b4bfb50795", + "sha256:4a8dbc1f0fbb2ae3de73eb0bdbb914180c7abfbf258e90b311dcd4f585d44bd2", + "sha256:59f53f1dc5b656cafb1badd0feb428c1e7bc19b867479ff72f7a9dd9b479f10e", + "sha256:5dbec3b9095749390c09ab7c89d314727f18800060d8d24e87f01fb9cfb40b32", + "sha256:633713d70ad6bfc49b34ead4060531658dc6dfc9b3eb7d8a716d5873377ab745", + "sha256:6b07130585d54fe8dff3d97b93b0e20290de974dc8177c320aeaf23459219c0b", + "sha256:6c4459b3de97b75e3bd6b7d4b7f0db13f17f504f3d13e2a7c623786289dd670e", + "sha256:6d4817234349a80dbf03640cec6109cd90cba068330703fa65ddf56b60223a6d", + "sha256:723e8130d4ecc8f56e9a611e73b31219595baa3bb252d539206f7bbbab6ffc1f", + "sha256:784f53ebc9f3fd0e2a3f6a78b2be1bd1f5575d7863e10c6e12504f240fd06660", + "sha256:7b6be138d61e458e18d8e6ddcddd36dd96215edfe5f1168de0b1b32635839b62", + "sha256:7ccf362abd726b0410bf8911c31fbf97f09f8f1061f8c1cf03dfc4b6372848f6", + "sha256:83516205e254a0cb77d2d7bb3632ee019d93d9f4005de31dca0a8c3667d5bc04", + "sha256:851cf4ff24062c6aec510a454b2584f6e998cada52d4cb58c5e233d07172e50c", + "sha256:8f830ed581b45b82451a40faabb89c84e1a998124ee4212d440e9c6cf70083e5", + "sha256:94e2565443291bd778421856bc975d351738963071e9b8839ca1fc08b42d4bef", + "sha256:95203854f974e07af96358c0b261f1048d8e1083f2de9b1c565e1be4a3a48cfc", + "sha256:97117225cdd992a9c2a5515db1f66b59db634f59d0679ca1fa3fe8da32749cae", + "sha256:98e8a10b7a314f454d9eff4216a9a94d143a7ee65018dd12442e898ee2310578", + "sha256:a1170fa54185845505fbfa672f1c1ab175446c887cce8212c44149581cf2d466", + "sha256:a6b7d95969b8845250586f269e81e5dfdd8ff828ddeb8567a4a2eaa7313460c4", + "sha256:a8fb6cf131ac4070c9c5a3e21de0f7dc5a0fbe8bc77c9456ced896c12fcdad91", + "sha256:af4fffaffc4067232253715065e30c5a7ec6faac36f8fc8d6f64263b15f74db0", + "sha256:b4a5be1748d538a710f87542f22c2cad22f80545a847ad91ce45e77417293eb4", + "sha256:b5604380f3415ba69de87a289a2b56687faa4fe04dbee0754bfcae433489316b", + "sha256:b9023e237f4c02ff739581ef35969c3739445fb059b060ca51771e69101efffe", + "sha256:bc8ef5e043a2af066fa8cbfc6e708d58017024dc4345a1f9757b329a249f041b", + "sha256:c4ed2820d919351f4167e52425e096af41bfabacb1857186c1ea32ff9983ed75", + "sha256:cca4435eebea7962a52bdb216dec27215d0df64cf27fc1dd538415f5d2b9da6b", + "sha256:d900bb429fdfd7f511f868cedd03a6bbb142f3f9118c09b99ef8dc9bf9643c3c", + "sha256:d9ecf0829c6a62b9b573c7bb6d4dcd6ba8b6f80be9ba4fc7ed50bf4ac9aecd72", + "sha256:dbdb91cd8c048c2b09eb17713b0c12a54fbd587d79adcebad543bc0cd9a3410b", + "sha256:de3001a203182842a4630e7b8d1a2c7c07ec1b45d3084a83d5d227a3806f530f", + "sha256:e07f4a4a9b41583d6eabec04f8b68076ab3cd44c20bd29332c6572dda36f372e", + "sha256:ef8674b0ee8cc11e2d574e3e2998aea5df5ab242e012286824ea3c6970580e53", + "sha256:f4f05d88d9a80ad3cac6244d36dd89a3c00abc16371769f1340101d3cb899fc3", + "sha256:f642e90754ee3e06b0e7e51bce3379590e76b7f76b708e1a71ff043f87025c84", + "sha256:fc2af30ed0d5ae0b1abdb4ebdce598eafd5b35397d4d75deb341a614d333d987" + ], + "markers": "python_version >= '3.7'", + "version": "==6.5.0" }, "coveralls": { "hashes": [ - "sha256:15a987d9df877fff44cd81948c5806ffb6eafb757b3443f737888358e96156ee", - "sha256:aedfcc5296b788ebaf8ace8029376e5f102f67c53d1373f2e821515c15b36527" + "sha256:b32a8bb5d2df585207c119d6c01567b81fba690c9c10a753bfe27a335bfc43ea", + "sha256:f42015f31d386b351d4226389b387ae173207058832fbf5c8ec4b40e27b16026" ], "index": "pypi", - "version": "==3.2.0" + "markers": "python_version >= '3.5'", + "version": "==3.3.1" }, "docopt": { "hashes": [ @@ -285,280 +816,242 @@ ], "version": "==0.6.2" }, + "exceptiongroup": { + "hashes": [ + "sha256:4bfd3996ac73b41e9b9628b04e079f193850720ea5945fc96a08633c66912f14", + "sha256:91f5c769735f051a4290d52edd0858999b57e5876e9f85937691bd4c9fa3ed68" + ], + "markers": "python_version < '3.11'", + "version": "==1.2.0" + }, "flake8": { "hashes": [ - "sha256:07528381786f2a6237b061f6e96610a4167b226cb926e2aa2b6b1d78057c576b", - "sha256:bf8fd333346d844f616e8d47905ef3a3384edae6b4e9beb0c5101e25e3110907" + "sha256:33f96621059e65eec474169085dc92bf26e7b2d47366b70be2f67ab80dc25132", + "sha256:a6dfbb75e03252917f2473ea9653f7cd799c3064e54d4c8140044c5c065f53c3" ], "index": "pypi", - "version": "==3.9.2" + "markers": "python_full_version >= '3.8.1'", + "version": "==7.0.0" }, - "gitdb": { + "idna": { "hashes": [ - "sha256:8033ad4e853066ba6ca92050b9df2f89301b8fc8bf7e9324d412a63f8bf1a8fd", - "sha256:bac2fd45c0a1c9cf619e63a90d62bdc63892ef92387424b855792a6cabe789aa" + "sha256:9ecdbbd083b06798ae1e86adcbfe8ab1479cf864e4ee30fe4e46a003d12491ca", + "sha256:c05567e9c24a6b9faaa835c4821bad0590fbb9d5779e7caa6e1cc4978e7eb24f" ], - "markers": "python_version >= '3.6'", - "version": "==4.0.9" + "markers": "python_version >= '3.5'", + "version": "==3.6" }, - "gitpython": { + "iniconfig": { "hashes": [ - "sha256:1c885ce809e8ba2d88a29befeb385fcea06338d3640712b59ca623c220bb5704", - "sha256:5b68b000463593e05ff2b261acff0ff0972df8ab1b70d3cdbd41b546c8b8fc3d" + "sha256:2d91e135bf72d31a410b17c16da610a82cb55f6b0477d1a902134b24a455b8b3", + "sha256:b6a85871a79d2e3b22d2d1b94ac2824226a63c6b741c88f7ae975f18b6778374" ], "markers": "python_version >= '3.7'", - "version": "==3.1.27" + "version": "==2.0.0" }, - "idna": { + "isort": { "hashes": [ - "sha256:84d9dd047ffa80596e0f246e2eab0b391788b0503584e8945f2368256d2735ff", - "sha256:9d643ff0a55b762d5cdb124b8eaa99c66322e2157b69160bc32796e824360e6d" + "sha256:48fdfcb9face5d58a4f6dde2e72a1fb8dcaf8ab26f95ab49fab84c2ddefb0109", + "sha256:8ca5e72a8d85860d5a3fa69b8745237f2939afe12dbf656afbcb47fe72d947a6" ], - "markers": "python_version >= '3'", - "version": "==3.3" + "index": "pypi", + "markers": "python_full_version >= '3.8.0'", + "version": "==5.13.2" }, - "iniconfig": { + "markdown-it-py": { "hashes": [ - "sha256:011e24c64b7f47f6ebd835bb12a743f2fbe9a26d4cecaa7f53bc4f35ee9da8b3", - "sha256:bc3af051d7d14b2ee5ef9969666def0cd1a000e121eaea580d4a313df4b37f32" + "sha256:355216845c60bd96232cd8d8c40e8f9765cc86f46880e43a8fd22dc1a1a8cab1", + "sha256:e3f60a94fa066dc52ec76661e37c851cb232d92f9886b15cb560aaada2df8feb" ], - "version": "==1.1.1" + "markers": "python_version >= '3.8'", + "version": "==3.0.0" }, - "isort": { + "mccabe": { "hashes": [ - "sha256:9c2ea1e62d871267b78307fe511c0838ba0da28698c5732d54e2790bf3ba9899", - "sha256:e17d6e2b81095c9db0a03a8025a957f334d6ea30b26f9ec70805411e5c7c81f2" + "sha256:348e0240c33b60bbdf4e523192ef919f28cb2c3d7d5c7794f74009290f236325", + "sha256:6c2d30ab6be0e4a46919781807b4f0d834ebdd6c6e3dca0bda5a15f863427b6e" ], - "index": "pypi", - "version": "==5.9.3" + "markers": "python_version >= '3.6'", + "version": "==0.7.0" }, - "mccabe": { + "mdurl": { "hashes": [ - "sha256:ab8a6258860da4b6677da4bd2fe5dc2c659cff31b3ee4f7f5d64e79735b80d42", - "sha256:dd8d182285a0fe56bace7f45b5e7d1a6ebcbf524e8f3bd87eb0f125271b8831f" + "sha256:84008a41e51615a49fc9966191ff91509e3c40b939176e643fd50a5c2196b8f8", + "sha256:bb413d29f5eea38f31dd4754dd7377d4465116fb207585f97bf925588687c1ba" ], - "version": "==0.6.1" + "markers": "python_version >= '3.7'", + "version": "==0.1.2" }, "mypy-extensions": { "hashes": [ - "sha256:090fedd75945a69ae91ce1303b5824f428daf5a028d2f6ab8a299250a846f15d", - "sha256:2d82818f5bb3e369420cb3c4060a7970edba416647068eb4c5343488a6c604a8" + "sha256:4392f6c0eb8a5668a69e23d168ffa70f0be9ccfd32b5cc2d26a34ae5b844552d", + "sha256:75dbf8955dc00442a438fc4d0666508a9a97b6bd41aa2f0ffe9d2f2725af0782" ], - "version": "==0.4.3" + "markers": "python_version >= '3.5'", + "version": "==1.0.0" }, "packaging": { "hashes": [ - "sha256:dd47c42927d89ab911e606518907cc2d3a1f38bbd026385970643f9c5b8ecfeb", - "sha256:ef103e05f519cdc783ae24ea4e2e0f508a9c99b2d4969652eed6a2e1ea5bd522" + "sha256:048fb0e9405036518eaaf48a55953c750c11e1a1b68e0dd1a9d62ed0c092cfc5", + "sha256:8c491190033a9af7e1d931d0b5dacc2ef47509b34dd0de67ed209b5203fc88c7" ], - "markers": "python_version >= '3.6'", - "version": "==21.3" + "markers": "python_version >= '3.7'", + "version": "==23.2" }, "pathspec": { "hashes": [ - "sha256:7d15c4ddb0b5c802d161efc417ec1a2558ea2653c2e8ad9c19098201dc1c993a", - "sha256:e564499435a2673d586f6b2130bb5b95f04a3ba06f81b8f895b651a3c76aabb1" + "sha256:a0d503e138a4c123b27490a4f7beda6a01c6f288df0e4a8b79c7eb0dc7b4cc08", + "sha256:a482d51503a1ab33b1c67a6c3813a26953dbdc71c31dacaef9a838c4e29f5712" ], - "version": "==0.9.0" + "markers": "python_version >= '3.8'", + "version": "==0.12.1" }, "pbr": { "hashes": [ - "sha256:e547125940bcc052856ded43be8e101f63828c2d94239ffbe2b327ba3d5ccf0a", - "sha256:e8dca2f4b43560edef58813969f52a56cef023146cbb8931626db80e6c1c4308" + "sha256:4a7317d5e3b17a3dccb6a8cfe67dab65b20551404c52c8ed41279fa4f0cb4cda", + "sha256:d1377122a5a00e2f940ee482999518efe16d745d423a670c27773dfbc3c9a7d9" ], "markers": "python_version >= '2.6'", - "version": "==5.9.0" + "version": "==6.0.0" }, - "pluggy": { + "platformdirs": { "hashes": [ - "sha256:15b2acde666561e1298d71b523007ed7364de07029219b604cf808bfa1c765b0", - "sha256:966c145cd83c96502c3c3868f50408687b38434af77734af1e9ca461a4081d2d" + "sha256:0614df2a2f37e1a662acbd8e2b25b92ccf8632929bc6d43467e17fe89c75e068", + "sha256:ef0cc731df711022c174543cb70a9b5bd22e5a9337c8624ef2c2ceb8ddad8768" ], - "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3'", - "version": "==0.13.1" + "markers": "python_version >= '3.8'", + "version": "==4.2.0" }, - "py": { + "pluggy": { "hashes": [ - "sha256:51c75c4126074b472f746a24399ad32f6053d1b34b68d2fa41e558e6f4a98719", - "sha256:607c53218732647dff4acdfcd50cb62615cedf612e72d1724fb1a0cc6405b378" + "sha256:7db9f7b503d67d1c5b95f59773ebb58a8c1c288129a88665838012cfb07b8981", + "sha256:8c85c2876142a764e5b7548e7d9a0e0ddb46f5185161049a79b7e974454223be" ], - "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3, 3.4'", - "version": "==1.11.0" + "markers": "python_version >= '3.8'", + "version": "==1.4.0" }, "pycodestyle": { "hashes": [ - "sha256:514f76d918fcc0b55c6680472f0a37970994e07bbb80725808c17089be302068", - "sha256:c389c1d06bf7904078ca03399a4816f974a1d590090fecea0c63ec26ebaf1cef" + "sha256:41ba0e7afc9752dfb53ced5489e89f8186be00e599e712660695b7a75ff2663f", + "sha256:44fe31000b2d866f2e41841b18528a505fbd7fef9017b04eff4e2648a0fadc67" ], - "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3'", - "version": "==2.7.0" + "markers": "python_version >= '3.8'", + "version": "==2.11.1" }, "pyflakes": { "hashes": [ - "sha256:7893783d01b8a89811dd72d7dfd4d84ff098e5eed95cfa8905b22bbffe52efc3", - "sha256:f5bc8ecabc05bb9d291eb5203d6810b49040f6ff446a756326104746cc00c1db" + "sha256:1c61603ff154621fb2a9172037d84dca3500def8c8b630657d1701f026f8af3f", + "sha256:84b5be138a2dfbb40689ca07e2152deb896a65c3a3e24c251c5c62489568074a" ], - "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3'", - "version": "==2.3.1" + "markers": "python_version >= '3.8'", + "version": "==3.2.0" }, - "pyparsing": { + "pygments": { "hashes": [ - "sha256:2b020ecf7d21b687f219b71ecad3631f644a47f01403fa1d1036b0c6416d70fb", - "sha256:5026bae9a10eeaefb61dab2f09052b9f4307d44aee4eda64b309723d8d206bbc" + "sha256:b27c2826c47d0f3219f29554824c30c5e8945175d888647acd804ddd04af846c", + "sha256:da46cec9fd2de5be3a8a784f434e4c4ab670b4ff54d605c4c2717e9d49c4c367" ], - "markers": "python_full_version >= '3.6.8'", - "version": "==3.0.9" + "markers": "python_version >= '3.7'", + "version": "==2.17.2" }, "pytest": { "hashes": [ - "sha256:50bcad0a0b9c5a72c8e4e7c9855a3ad496ca6a881a3641b4260605450772c54b", - "sha256:91ef2131a9bd6be8f76f1f08eac5c5317221d6ad1e143ae03894b862e8976890" + "sha256:249b1b0864530ba251b7438274c4d251c58d868edaaec8762893ad4a0d71c36c", + "sha256:50fb9cbe836c3f20f0dfa99c565201fb75dc54c8d76373cd1bde06b06657bdb6" ], "index": "pypi", - "version": "==6.2.4" + "markers": "python_version >= '3.8'", + "version": "==8.0.0" }, "pytest-cov": { "hashes": [ - "sha256:261bb9e47e65bd099c89c3edf92972865210c36813f80ede5277dceb77a4a62a", - "sha256:261ceeb8c227b726249b376b8526b600f38667ee314f910353fa318caa01f4d7" + "sha256:3904b13dfbfec47f003b8e77fd5b589cd11904a21ddf1ab38a64f204d6a10ef6", + "sha256:6ba70b9e97e69fcc3fb45bfeab2d0a138fb65c4d0d6a41ef33983ad114be8c3a" ], "index": "pypi", - "version": "==2.12.1" + "markers": "python_version >= '3.7'", + "version": "==4.1.0" }, "pyyaml": { "hashes": [ - "sha256:0283c35a6a9fbf047493e3a0ce8d79ef5030852c51e9d911a27badfde0605293", - "sha256:055d937d65826939cb044fc8c9b08889e8c743fdc6a32b33e2390f66013e449b", - "sha256:07751360502caac1c067a8132d150cf3d61339af5691fe9e87803040dbc5db57", - "sha256:0b4624f379dab24d3725ffde76559cff63d9ec94e1736b556dacdfebe5ab6d4b", - "sha256:0ce82d761c532fe4ec3f87fc45688bdd3a4c1dc5e0b4a19814b9009a29baefd4", - "sha256:1e4747bc279b4f613a09eb64bba2ba602d8a6664c6ce6396a4d0cd413a50ce07", - "sha256:213c60cd50106436cc818accf5baa1aba61c0189ff610f64f4a3e8c6726218ba", - "sha256:231710d57adfd809ef5d34183b8ed1eeae3f76459c18fb4a0b373ad56bedcdd9", - "sha256:277a0ef2981ca40581a47093e9e2d13b3f1fbbeffae064c1d21bfceba2030287", - "sha256:2cd5df3de48857ed0544b34e2d40e9fac445930039f3cfe4bcc592a1f836d513", - "sha256:40527857252b61eacd1d9af500c3337ba8deb8fc298940291486c465c8b46ec0", - "sha256:473f9edb243cb1935ab5a084eb238d842fb8f404ed2193a915d1784b5a6b5fc0", - "sha256:48c346915c114f5fdb3ead70312bd042a953a8ce5c7106d5bfb1a5254e47da92", - "sha256:50602afada6d6cbfad699b0c7bb50d5ccffa7e46a3d738092afddc1f9758427f", - "sha256:68fb519c14306fec9720a2a5b45bc9f0c8d1b9c72adf45c37baedfcd949c35a2", - "sha256:77f396e6ef4c73fdc33a9157446466f1cff553d979bd00ecb64385760c6babdc", - "sha256:819b3830a1543db06c4d4b865e70ded25be52a2e0631ccd2f6a47a2822f2fd7c", - "sha256:897b80890765f037df3403d22bab41627ca8811ae55e9a722fd0392850ec4d86", - "sha256:98c4d36e99714e55cfbaaee6dd5badbc9a1ec339ebfc3b1f52e293aee6bb71a4", - "sha256:9df7ed3b3d2e0ecfe09e14741b857df43adb5a3ddadc919a2d94fbdf78fea53c", - "sha256:9fa600030013c4de8165339db93d182b9431076eb98eb40ee068700c9c813e34", - "sha256:a80a78046a72361de73f8f395f1f1e49f956c6be882eed58505a15f3e430962b", - "sha256:b3d267842bf12586ba6c734f89d1f5b871df0273157918b0ccefa29deb05c21c", - "sha256:b5b9eccad747aabaaffbc6064800670f0c297e52c12754eb1d976c57e4f74dcb", - "sha256:c5687b8d43cf58545ade1fe3e055f70eac7a5a1a0bf42824308d868289a95737", - "sha256:cba8c411ef271aa037d7357a2bc8f9ee8b58b9965831d9e51baf703280dc73d3", - "sha256:d15a181d1ecd0d4270dc32edb46f7cb7733c7c508857278d3d378d14d606db2d", - "sha256:d4db7c7aef085872ef65a8fd7d6d09a14ae91f691dec3e87ee5ee0539d516f53", - "sha256:d4eccecf9adf6fbcc6861a38015c2a64f38b9d94838ac1810a9023a0609e1b78", - "sha256:d67d839ede4ed1b28a4e8909735fc992a923cdb84e618544973d7dfc71540803", - "sha256:daf496c58a8c52083df09b80c860005194014c3698698d1a57cbcfa182142a3a", - "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:a08c6f0fe150303c1c6b71ebcd7213c2858041a7e01975da3a99aed1e7a378ef", + "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" - }, - "regex": { - "hashes": [ - "sha256:042d122f9fee3ceb6d7e3067d56557df697d1aad4ff5f64ecce4dc13a90a7c01", - "sha256:047b2d1323a51190c01b6604f49fe09682a5c85d3c1b2c8b67c1cd68419ce3c4", - "sha256:0afa6a601acf3c0dc6de4e8d7d8bbce4e82f8542df746226cd35d4a6c15e9456", - "sha256:166ae7674d0a0e0f8044e7335ba86d0716c9d49465cff1b153f908e0470b8300", - "sha256:17443f99b8f255273731f915fdbfea4d78d809bb9c3aaf67b889039825d06515", - "sha256:17764683ea01c2b8f103d99ae9de2473a74340df13ce306c49a721f0b1f0eb9e", - "sha256:179410c79fa86ef318d58ace233f95b87b05a1db6dc493fa29404a43f4b215e2", - "sha256:186c5a4a4c40621f64d771038ede20fca6c61a9faa8178f9e305aaa0c2442a97", - "sha256:1a6f2698cfa8340dfe4c0597782776b393ba2274fe4c079900c7c74f68752705", - "sha256:1ab5cf7d09515548044e69d3a0ec77c63d7b9dfff4afc19653f638b992573126", - "sha256:1c1264eb40a71cf2bff43d6694ab7254438ca19ef330175060262b3c8dd3931a", - "sha256:1fc26bb3415e7aa7495c000a2c13bf08ce037775db98c1a3fac9ff04478b6930", - "sha256:24908aefed23dd065b4a668c0b4ca04d56b7f09d8c8e89636cf6c24e64e67a1e", - "sha256:249437f7f5b233792234aeeecb14b0aab1566280de42dfc97c26e6f718297d68", - "sha256:24963f0b13cc63db336d8da2a533986419890d128c551baacd934c249d51a779", - "sha256:26dbe90b724efef7820c3cf4a0e5be7f130149f3d2762782e4e8ac2aea284a0b", - "sha256:27624b490b5d8880f25dac67e1e2ea93dfef5300b98c6755f585799230d6c746", - "sha256:2ac29b834100d2c171085ceba0d4a1e7046c434ddffc1434dbc7f9d59af1e945", - "sha256:2f4c101746a8dac0401abefa716b357c546e61ea2e3d4a564a9db9eac57ccbce", - "sha256:30637e7fa4acfed444525b1ab9683f714be617862820578c9fd4e944d4d9ad1f", - "sha256:3adafe6f2c6d86dbf3313866b61180530ca4dcd0c264932dc8fa1ffb10871d58", - "sha256:3b9b6289e03dbe6a6096880d8ac166cb23c38b4896ad235edee789d4e8697152", - "sha256:3de1ecf26ce85521bf73897828b6d0687cc6cf271fb6ff32ac63d26b21f5e764", - "sha256:48dddddce0ea7e7c3e92c1e0c5a28c13ca4dc9cf7e996c706d00479652bff76c", - "sha256:495a4165172848503303ed05c9d0409428f789acc27050fe2cf0a4549188a7d5", - "sha256:4a11cbe8eb5fb332ae474895b5ead99392a4ea568bd2a258ab8df883e9c2bf92", - "sha256:4a5449adef907919d4ce7a1eab2e27d0211d1b255bf0b8f5dd330ad8707e0fc3", - "sha256:4b8838f70be3ce9e706df9d72f88a0aa7d4c1fea61488e06fdf292ccb70ad2be", - "sha256:4d206703a96a39763b5b45cf42645776f5553768ea7f3c2c1a39a4f59cafd4ba", - "sha256:4d42e3b7b23473729adbf76103e7df75f9167a5a80b1257ca30688352b4bb2dc", - "sha256:52684da32d9003367dc1a1c07e059b9bbaf135ad0764cd47d8ac3dba2df109bc", - "sha256:53d69d77e9cfe468b000314dd656be85bb9e96de088a64f75fe128dfe1bf30dd", - "sha256:555f7596fd1f123f8c3a67974c01d6ef80b9769e04d660d6c1a7cc3e6cff7069", - "sha256:5aba3d13c77173e9bfed2c2cea7fc319f11c89a36fcec08755e8fb169cf3b0df", - "sha256:5c8d61883a38b1289fba9944a19a361875b5c0170b83cdcc95ea180247c1b7d3", - "sha256:5e201b1232d81ca1a7a22ab2f08e1eccad4e111579fd7f3bbf60b21ef4a16cea", - "sha256:663dca677bd3d2e2b5b7d0329e9f24247e6f38f3b740dd9a778a8ef41a76af41", - "sha256:67ae3601edf86e15ebe40885e5bfdd6002d34879070be15cf18fc0d80ea24fed", - "sha256:68e5c641645351eb9eb12c465876e76b53717f99e9b92aea7a2dd645a87aa7aa", - "sha256:71988a76fcb68cc091e901fddbcac0f9ad9a475da222c47d3cf8db0876cb5344", - "sha256:775694cd0bb2c4accf2f1cdd007381b33ec8b59842736fe61bdbad45f2ac7427", - "sha256:7f648037c503985aed39f85088acab6f1eb6a0482d7c6c665a5712c9ad9eaefc", - "sha256:809bbbbbcf8258049b031d80932ba71627d2274029386f0452e9950bcfa2c6e8", - "sha256:8fd5f8ae42f789538bb634bdfd69b9aa357e76fdfd7ad720f32f8994c0d84f1e", - "sha256:933e72fbe1829cbd59da2bc51ccd73d73162f087f88521a87a8ec9cb0cf10fa8", - "sha256:9c1f62ee2ba880e221bc950651a1a4b0176083d70a066c83a50ef0cb9b178e12", - "sha256:9faa01818dad9111dbf2af26c6e3c45140ccbd1192c3a0981f196255bf7ec5e6", - "sha256:a58d21dd1a2d6b50ed091554ff85e448fce3fe33a4db8b55d0eba2ca957ed626", - "sha256:a8a08ace913c4101f0dc0be605c108a3761842efd5f41a3005565ee5d169fb2b", - "sha256:b2932e728bee0a634fe55ee54d598054a5a9ffe4cd2be21ba2b4b8e5f8064c2c", - "sha256:b5f759a1726b995dc896e86f17f9c0582b54eb4ead00ed5ef0b5b22260eaf2d0", - "sha256:bc635ab319c9b515236bdf327530acda99be995f9d3b9f148ab1f60b2431e970", - "sha256:be456b4313a86be41706319c397c09d9fdd2e5cdfde208292a277b867e99e3d1", - "sha256:be57f9c7b0b423c66c266a26ad143b2c5514997c05dd32ce7ca95c8b209c2288", - "sha256:c1ea28f0ee6cbe4c0367c939b015d915aa9875f6e061ba1cf0796ca9a3010570", - "sha256:c3db393b21b53d7e1d3f881b64c29d886cbfdd3df007e31de68b329edbab7d02", - "sha256:c400dfed4137f32127ea4063447006d7153c974c680bf0fb1b724cce9f8567fc", - "sha256:c5429202bef174a3760690d912e3a80060b323199a61cef6c6c29b30ce09fd17", - "sha256:c5eac5d8a8ac9ccf00805d02a968a36f5c967db6c7d2b747ab9ed782b3b3a28b", - "sha256:c757f3a27b6345de13ef3ca956aa805d7734ce68023e84d0fc74e1f09ce66f7a", - "sha256:ceff75127f828dfe7ceb17b94113ec2df4df274c4cd5533bb299cb099a18a8ca", - "sha256:cff5c87e941292c97d11dc81bd20679f56a2830f0f0e32f75b8ed6e0eb40f704", - "sha256:d70596f20a03cb5f935d6e4aad9170a490d88fc4633679bf00c652e9def4619e", - "sha256:e7b2ff451f6c305b516281ec45425dd423223c8063218c5310d6f72a0a7a517c", - "sha256:e85b10280cf1e334a7c95629f6cbbfe30b815a4ea5f1e28d31f79eb92c2c3d93", - "sha256:ecd2b5d983eb0adf2049d41f95205bdc3de4e6cc2350e9c80d4409d3a75229de", - "sha256:ed657a07d8a47ef447224ea00478f1c7095065dfe70a89e7280e5f50a5725131", - "sha256:f43522fb5d676c99282ca4e2d41e8e2388427c0cf703db6b4a66e49b10b699a8", - "sha256:f57823f35b18d82b201c1b27ce4e55f88e79e81d9ca07b50ce625d33823e1439", - "sha256:f7b43acb2c46fb2cd506965b2d9cf4c5e64c9c612bac26c1187933c7296bf08c", - "sha256:fa7c7044aabdad2329974be2246babcc21d3ede852b3971a90fd8c2056c20360", - "sha256:fcd7c432202bcb8b642c3f43d5bcafc5930d82fe5b2bf2c008162df258445c1d", - "sha256:fdecb225d0f1d50d4b26ac423e0032e76d46a788b83b4e299a520717a47d968c", - "sha256:ffef4b30785dc2d1604dfb7cf9fca5dc27cd86d65f7c2a9ec34d6d3ae4565ec2" - ], - "markers": "python_version >= '3.6'", - "version": "==2022.6.2" + "version": "==6.0.1" }, "requests": { "hashes": [ - "sha256:6c1246513ecd5ecd4528a0906f910e8f0f9c6b8ec72030dc9fd154dc1a6efd24", - "sha256:b8aa58f8cf793ffd8782d3d8cb19e66ef36f7aba4353eec859e74678b01b07a7" + "sha256:58cd2187c01e70e6e26505bca751777aa9f2ee0b7f4300988b709f44e013003f", + "sha256:942c5a758f98d790eaed1a29cb6eefc7ffb0d1cf7af05c3d2791656dbd6ad1e1" ], "index": "pypi", - "version": "==2.26.0" + "markers": "python_version >= '3.7'", + "version": "==2.31.0" }, "requests-mock": { "hashes": [ - "sha256:0a2d38a117c08bb78939ec163522976ad59a6b7fdd82b709e23bb98004a44970", - "sha256:8d72abe54546c1fc9696fa1516672f1031d72a55a1d66c85184f972a24ba0eba" + "sha256:ef10b572b489a5f28e09b708697208c4a3b2b89ef80a9f01584340ea357ec3c4", + "sha256:f7fae383f228633f6bececebdab236c478ace2284d6292c6e7e2867b9ab74d15" ], "index": "pypi", - "version": "==1.9.3" + "version": "==1.11.0" + }, + "rich": { + "hashes": [ + "sha256:5cb5123b5cf9ee70584244246816e9114227e0b98ad9176eede6ad54bf5403fa", + "sha256:6da14c108c4866ee9520bbffa71f6fe3962e193b7da68720583850cd4548e235" + ], + "markers": "python_full_version >= '3.7.0'", + "version": "==13.7.0" }, "six": { "hashes": [ @@ -568,45 +1061,37 @@ "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3'", "version": "==1.16.0" }, - "smmap": { - "hashes": [ - "sha256:2aba19d6a040e78d8b09de5c57e96207b09ed71d8e55ce0959eeee6c8e190d94", - "sha256:c840e62059cd3be204b0c9c9f74be2c09d5648eddd4580d9314c3ecde0b30936" - ], - "markers": "python_version >= '3.6'", - "version": "==5.0.0" - }, "stevedore": { "hashes": [ - "sha256:a547de73308fd7e90075bb4d301405bebf705292fa90a90fc3bcf9133f58616c", - "sha256:f40253887d8712eaa2bb0ea3830374416736dc8ec0e22f5a65092c1174c44335" + "sha256:8cc040628f3cea5d7128f2e76cf486b2251a4e543c7b938f58d9a377f6694a2d", + "sha256:a54534acf9b89bc7ed264807013b505bf07f74dbe4bcfa37d32bd063870b087c" ], - "markers": "python_version >= '3.6'", - "version": "==3.5.0" + "markers": "python_version >= '3.8'", + "version": "==5.1.0" }, - "toml": { + "tomli": { "hashes": [ - "sha256:806143ae5bfb6a3c6e736a764057db0e6a0e05e338b5630894a5f779cabb4f9b", - "sha256:b3bda1d108d5dd99f4a20d24d9c348e91c4db7ab1b749200bded2f839ccbe68f" + "sha256:939de3e7a6161af0c887ef91b7d41a53e7c5a1ca976325f429cb46ea9bc30ecc", + "sha256:de526c12914f0c550d15924c62d72abc48d6fe7364aa87328337a31007fe8a4f" ], - "markers": "python_version >= '2.6' and python_version not in '3.0, 3.1, 3.2, 3.3'", - "version": "==0.10.2" + "markers": "python_version < '3.11'", + "version": "==2.0.1" }, - "tomli": { + "typing-extensions": { "hashes": [ - "sha256:05b6166bff487dc068d322585c7ea4ef78deed501cc124060e0f238e89a9231f", - "sha256:e3069e4be3ead9668e21cb9b074cd948f7b3113fd9c8bba083f48247aab8b11c" + "sha256:23478f88c37f27d76ac8aee6c905017a143b0b1b886c3c9f66bc2fd94f9f5783", + "sha256:af72aea155e91adfc61c3ae9e0e342dbc0cba726d6cba4b6c72c1f34e47291cd" ], - "markers": "python_version >= '3.6'", - "version": "==1.2.3" + "markers": "python_version < '3.11'", + "version": "==4.9.0" }, "urllib3": { "hashes": [ - "sha256:44ece4d53fb1706f667c9bd1c648f5469a2ec925fcf3a776667042d645472c14", - "sha256:aabaf16477806a5e1dd19aa41f8c2b7950dd3c746362d7e3223dbe6de6ac448e" + "sha256:34b97092d7e0a3a8cf7cd10e386f401b3737364026c45e622aa02903dffe0f07", + "sha256:f8ecc1bba5667413457c529ab955bf8c67b45db799d159066261719e328580a0" ], - "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3, 3.4' and python_version < '4'", - "version": "==1.26.9" + "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.18" } } } diff --git a/dsaps/cli.py b/dsaps/cli.py index dba6428..7c5d80d 100644 --- a/dsaps/cli.py +++ b/dsaps/cli.py @@ -62,7 +62,7 @@ def main(ctx, url, email, password): logger_factory=structlog.stdlib.LoggerFactory(), ) logging.basicConfig( - format="%(message)s", + format="%(asctime)s %(message)s", handlers=[logging.FileHandler(f"logs/log-{log_suffix}", "w")], level=logging.INFO, ) @@ -94,7 +94,6 @@ def main(ctx, url, email, password): "-d", "--content-directory", required=True, - type=click.Path(exists=True, dir_okay=True, file_okay=False), help="The full path to the content, either a directory of files " "or a URL for the storage location.", ) @@ -146,12 +145,10 @@ def additems( mapping = json.load(jsonfile) collection = Collection.create_metadata_for_items_from_csv(metadata, mapping) for item in collection.items: - item.bitstreams_in_directory(content_directory, file_type) + item.bitstreams_in_directory(content_directory, client.s3_client, file_type) collection.uuid = collection_uuid - items = collection.post_items(client) - if ingest_report: - report_name = metadata_csv.replace(".csv", "-ingest.csv") - helpers.create_ingest_report(items, report_name) + for item in collection.post_items(client): + logger.info(item.file_identifier) elapsed_time = datetime.timedelta(seconds=time.time() - start_time) logger.info(f"Total runtime : {elapsed_time}") @@ -190,7 +187,6 @@ def newcollection(ctx, community_handle, collection_name): @click.option( "-o", "--output-directory", - type=click.Path(exists=True, file_okay=False), default=f"{os.getcwd()}/", callback=validate_path, help="The path of the output files, include / at the end of the " "path.", @@ -208,12 +204,14 @@ def newcollection(ctx, community_handle, collection_name): help="The file type to be uploaded, if limited to one file " "type.", default="*", ) -def reconcile(metadata_csv, output_directory, content_directory, file_type): +@click.pass_context +def reconcile(ctx, metadata_csv, output_directory, content_directory, file_type): """Run a reconciliation of the specified files and metadata to produce reports of files with no metadata, metadata with no files, metadata matched to files, and an updated version of the metadata CSV with only the records that have matching files.""" - file_ids = helpers.create_file_list(content_directory, file_type) + client = ctx.obj["client"] + file_ids = helpers.create_file_list(content_directory, client.s3_client, file_type) metadata_ids = helpers.create_metadata_id_list(metadata_csv) metadata_matches = helpers.match_metadata_to_files(file_ids, metadata_ids) file_matches = helpers.match_files_to_metadata(file_ids, metadata_ids) diff --git a/dsaps/helpers.py b/dsaps/helpers.py index a64880a..d8eeb86 100644 --- a/dsaps/helpers.py +++ b/dsaps/helpers.py @@ -1,5 +1,4 @@ import csv -import glob import os @@ -12,11 +11,19 @@ def create_csv_from_list(list_name, output): writer.writerow([item]) -def create_file_list(file_path, file_type): +def create_file_list(file_path, s3_client, file_type): """Create a list of file names.""" - files = glob.glob(f"{file_path}/**/*.{file_type}", recursive=True) - file_list = [os.path.basename(file) for file in files] - return file_list + files = [] + paginator = s3_client.get_paginator("list_objects_v2") + for page in paginator.paginate(Bucket=file_path.removeprefix("s3://")): + files.extend( + [ + file["Key"] + for file in page["Contents"] + if file["Key"].endswith(f".{file_type}") + ] + ) + return files def create_ingest_report(items, file_name): diff --git a/dsaps/models.py b/dsaps/models.py index 21b731d..3741980 100644 --- a/dsaps/models.py +++ b/dsaps/models.py @@ -1,12 +1,14 @@ -import glob import operator -import os from functools import partial import attr +import boto3 import requests +import smart_open import structlog +from dsaps.helpers import create_file_list + Field = partial(attr.ib, default=None) Group = partial(attr.ib, default=[]) @@ -20,6 +22,7 @@ def __init__(self, url): self.url = url.rstrip("/") self.cookies = None self.header = header + self.s3_client = boto3.client("s3") logger.info("Initializing client") def authenticate(self, email, password): @@ -27,11 +30,11 @@ def authenticate(self, email, password): header = self.header data = {"email": email, "password": password} session = requests.post( - f"{self.url}/login", headers=header, params=data + f"{self.url}/login", headers=header, params=data, timeout=30 ).cookies["JSESSIONID"] cookies = {"JSESSIONID": session} status = requests.get( - f"{self.url}/status", headers=header, cookies=cookies + f"{self.url}/status", headers=header, cookies=cookies, timeout=30 ).json() self.user_full_name = status["fullname"] self.cookies = cookies @@ -55,7 +58,11 @@ def filtered_item_search(self, key, string, query_type, selected_collections="") } logger.info(params) response = requests.get( - endpoint, headers=self.header, params=params, cookies=self.cookies + endpoint, + headers=self.header, + params=params, + cookies=self.cookies, + timeout=30, ) logger.info(f"Response url: {response.url}") response = response.json() @@ -69,14 +76,16 @@ def get_uuid_from_handle(self, handle): """Get UUID for an object based on its handle.""" hdl_endpoint = f"{self.url}/handle/{handle}" rec_obj = requests.get( - hdl_endpoint, headers=self.header, cookies=self.cookies + hdl_endpoint, headers=self.header, cookies=self.cookies, timeout=30 ).json() return rec_obj["uuid"] def get_record(self, uuid, record_type): """Get an individual record of a specified type.""" url = f"{self.url}/{record_type}/{uuid}?expand=all" - record = requests.get(url, headers=self.header, cookies=self.cookies).json() + record = requests.get( + url, headers=self.header, cookies=self.cookies, timeout=30 + ).json() if record_type == "items": rec_obj = self._populate_class_instance(Item, record) elif record_type == "communities": @@ -93,18 +102,25 @@ def post_bitstream(self, item_uuid, bitstream): ID.""" endpoint = f"{self.url}/items/{item_uuid}" f"/bitstreams?name={bitstream.name}" header_upload = {"accept": "application/json"} - data = open(bitstream.file_path, "rb") - response = requests.post( - endpoint, headers=header_upload, cookies=self.cookies, data=data - ).json() - bitstream_uuid = response["uuid"] - return bitstream_uuid + with smart_open.open(bitstream.file_path, "rb") as data: + post_response = requests.post( + endpoint, + headers=header_upload, + cookies=self.cookies, + data=data, + timeout=30, + ) + logger.info(f"Bitstream POST status: {post_response}") + response = post_response.json() + logger.info(f"Bitstream POST response: {response}") + bitstream_uuid = response["uuid"] + return bitstream_uuid def post_coll_to_comm(self, comm_handle, coll_name): """Post a collection to a specified community.""" hdl_endpoint = f"{self.url}/handle/{comm_handle}" community = requests.get( - hdl_endpoint, headers=self.header, cookies=self.cookies + hdl_endpoint, headers=self.header, cookies=self.cookies, timeout=30 ).json() comm_uuid = community["uuid"] uuid_endpoint = f"{self.url}/communities/{comm_uuid}/collections" @@ -113,6 +129,7 @@ def post_coll_to_comm(self, comm_handle, coll_name): headers=self.header, cookies=self.cookies, json={"name": coll_name}, + timeout=30, ).json() coll_uuid = coll_uuid["uuid"] logger.info(f"Collection posted: {coll_uuid}") @@ -121,12 +138,17 @@ def post_coll_to_comm(self, comm_handle, coll_name): def post_item_to_collection(self, collection_uuid, item): """Post item to a specified collection and return the item ID.""" endpoint = f"{self.url}/collections/{collection_uuid}/items" - post_response = requests.post( + logger.info(endpoint) + post_resp = requests.post( endpoint, headers=self.header, cookies=self.cookies, json={"metadata": attr.asdict(item)["metadata"]}, - ).json() + timeout=30, + ) + logger.info(f"Item POST status: {post_resp}") + post_response = post_resp.json() + logger.info(f"Item POST response: {post_response}") item_uuid = post_response["uuid"] item_handle = post_response["handle"] return item_uuid, item_handle @@ -169,6 +191,7 @@ class Collection(BaseRecord): def post_items(self, client): """Post items to collection.""" for item in self.items: + logger.info(f"Posting item: {item}") item_uuid, item_handle = client.post_item_to_collection(self.uuid, item) item.uuid = item_uuid item.handle = item_handle @@ -199,13 +222,13 @@ class Item(BaseRecord): file_identifier = Field() source_system_identifier = Field() - def bitstreams_in_directory(self, directory, file_type="*"): + def bitstreams_in_directory(self, directory, s3_client, file_type=None): """Create a list of bitstreams from the specified directory and sort the list.""" - files = glob.iglob( - f"{directory}/**/{self.file_identifier}*.{file_type}", recursive=True - ) + files = create_file_list(directory, s3_client, file_type) self.bitstreams = [ - Bitstream(name=os.path.basename(f), file_path=f) for f in files + Bitstream(name=file, file_path=f"{directory}/{file}") + for file in files + if file.startswith(self.file_identifier) and file.endswith(file_type) ] self.bitstreams.sort(key=lambda x: x.name) @@ -215,24 +238,27 @@ def metadata_from_csv_row(cls, row, field_map): metadata = [] for f in field_map: field = row[field_map[f]["csv_field_name"]] - if f == "file_identifier": - file_identifier = field - continue # file_identifier is not included in DSpace metadata - if f == "source_system_identifier": - source_system_identifier = field - continue # source_system_identifier is not included in DSpace - # metadata - delimiter = field_map[f]["delimiter"] - language = field_map[f]["language"] - if delimiter: - metadata.extend( - [ - MetadataEntry(key=f, value=v, language=language) - for v in field.split(delimiter) - ] - ) - else: - metadata.append(MetadataEntry(key=f, value=field, language=language)) + if field != "": + if f == "file_identifier": + file_identifier = field + continue # file_identifier is not included in DSpace metadata + if f == "source_system_identifier": + source_system_identifier = field + continue # source_system_identifier is not included in DSpace + metadata + delimiter = field_map[f]["delimiter"] + language = field_map[f]["language"] + if delimiter: + metadata.extend( + [ + MetadataEntry(key=f, value=v, language=language) + for v in field.split(delimiter) + ] + ) + else: + metadata.append( + MetadataEntry(key=f, value=field, language=language) + ) return cls( metadata=metadata, file_identifier=file_identifier, diff --git a/tests/conftest.py b/tests/conftest.py index 4366c42..e441006 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -1,13 +1,57 @@ import csv import json +import boto3 import pytest import requests_mock from click.testing import CliRunner +from moto import mock_aws from dsaps import models +# Env fixtures +@pytest.fixture(autouse=True) +def _test_environment(monkeypatch): + monkeypatch.setenv("AWS_ACCESS_KEY_ID", "testing") + monkeypatch.setenv("AWS_SECRET_ACCESS_KEY", "testing") + monkeypatch.setenv("AWS_SECURITY_TOKEN", "testing") + monkeypatch.setenv("AWS_SESSION_TOKEN", "testing") + + +@pytest.fixture() +def mocked_s3(): + with mock_aws(): + s3_instance = boto3.client("s3", region_name="us-east-1") + s3_instance.create_bucket(Bucket="test-bucket") + s3_instance.put_object( + Body="", + Bucket="test-bucket", + Key="test_01.pdf", + ) + s3_instance.put_object( + Body="", + Bucket="test-bucket", + Key="test_02.pdf", + ) + s3_instance.put_object( + Body="", + Bucket="test-bucket", + Key="best_01.pdf", + ) + s3_instance.put_object( + Body="", + Bucket="test-bucket", + Key="test_01.jpg", + ) + yield s3_instance + + +@pytest.fixture() +def s3_client(): + return boto3.client("s3", region_name="us-east-1") + + @pytest.fixture() def client(): client = models.Client("mock://example.com/") @@ -17,23 +61,6 @@ def client(): return client -@pytest.fixture() -def input_dir(tmp_path): - input_dir = tmp_path / "files" - input_dir.mkdir() - input_2nd_lvl = input_dir / "more_files" - input_2nd_lvl.mkdir() - with open(f"{input_dir}/test_01.pdf", "w"): - pass - with open(f"{input_2nd_lvl}/test_02.pdf", "w"): - pass - with open(f"{input_dir}/best_01.pdf", "w"): - pass - with open(f"{input_dir}/test_01.jpg", "w"): - pass - return str(f"{input_dir}/") - - @pytest.fixture() def aspace_delimited_csv(): with open("tests/fixtures/aspace_metadata_delimited.csv") as f: diff --git a/tests/test_cli.py b/tests/test_cli.py index c740d02..d6d0e18 100644 --- a/tests/test_cli.py +++ b/tests/test_cli.py @@ -1,7 +1,10 @@ +from moto import mock_aws + from dsaps.cli import main -def test_additems(runner, input_dir): +@mock_aws +def test_additems(runner, mocked_s3): """Test adding items to a collection.""" result = runner.invoke( main, @@ -18,7 +21,7 @@ def test_additems(runner, input_dir): "--field-map", "config/aspace_mapping.json", "--content-directory", - input_dir, + "s3://test-bucket", "--file-type", "pdf", "--collection-handle", @@ -46,7 +49,7 @@ def test_additems(runner, input_dir): "--field-map", "config/aspace_mapping.json", "--content-directory", - input_dir, + "s3://test-bucket", "--file-type", "pdf", ], @@ -54,7 +57,7 @@ def test_additems(runner, input_dir): assert result.exit_code == 0 -def test_newcollection(runner, input_dir): +def test_newcollection(runner): """Test newcoll command.""" result = runner.invoke( main, @@ -75,7 +78,8 @@ def test_newcollection(runner, input_dir): assert result.exit_code == 0 -def test_reconcile(runner, input_dir, output_dir): +@mock_aws +def test_reconcile(runner, mocked_s3, output_dir): """Test reconcile command.""" result = runner.invoke( main, @@ -92,7 +96,7 @@ def test_reconcile(runner, input_dir, output_dir): "--output-directory", output_dir, "--content-directory", - input_dir, + "s3://test-bucket", "--file-type", "pdf", ], diff --git a/tests/test_helpers.py b/tests/test_helpers.py index ae6a9ee..211b919 100644 --- a/tests/test_helpers.py +++ b/tests/test_helpers.py @@ -14,9 +14,9 @@ def test_create_csv_from_list(output_dir): assert row["id"] == "123" -def test_create_file_list(input_dir): +def test_create_file_list(mocked_s3, s3_client): """Test create_file_list function.""" - file_list = helpers.create_file_list(input_dir, "pdf") + file_list = helpers.create_file_list("s3://test-bucket", s3_client, "pdf") for file_id in ["test_02.pdf", "test_01.pdf", "best_01.pdf"]: assert file_id in file_list @@ -33,7 +33,7 @@ def test_create_ingest_report(runner, output_dir): assert row["link"] == "https://hdl.handle.net/111.1111" -def test_create_metadata_id_list(input_dir): +def test_create_metadata_id_list(): """Test create_metadata_id_list function.""" metadata_path = "tests/fixtures/aspace_metadata_delimited.csv" metadata_ids = helpers.create_metadata_id_list(metadata_path) @@ -59,7 +59,7 @@ def test_match_metadata_to_files(): assert "test" in file_matches -def test_update_metadata_csv(input_dir, output_dir): +def test_update_metadata_csv(output_dir): """Test update_metadata_csv function.""" metadata_matches = ["test"] helpers.update_metadata_csv( diff --git a/tests/test_models.py b/tests/test_models.py index a01ee1c..9c65ddc 100644 --- a/tests/test_models.py +++ b/tests/test_models.py @@ -1,4 +1,5 @@ import attr +from moto import mock_aws from dsaps import models @@ -35,11 +36,11 @@ def test_get_record(client): assert attr.asdict(rec_obj)["metadata"] == {"title": "Sample title"} -def test_post_bitstream(client, input_dir): +def test_post_bitstream(client, mocked_s3): """Test post_bitstream method.""" item_uuid = "e5f6" bitstream = models.Bitstream( - name="test_01.pdf", file_path=f"{input_dir}test_01.pdf" + name="test_01.pdf", file_path="s3://test-bucket/test_01.pdf" ) bit_uuid = client.post_bitstream(item_uuid, bitstream) assert bit_uuid == "g7h8" @@ -53,11 +54,12 @@ def test_post_coll_to_comm(client): assert coll_uuid == "c3d4" -def test_post_item_to_collection(client, input_dir): +@mock_aws +def test_post_item_to_collection(client, mocked_s3): """Test post_item_to_collection method.""" item = models.Item() item.bitstreams = [ - models.Bitstream(name="test_01.pdf", file_path=f"{input_dir}test_01.pdf") + models.Bitstream(name="test_01.pdf", file_path="s3://test-bucket/test_01.pdf") ] item.metadata = [ models.MetadataEntry(key="file_identifier", value="test"), @@ -77,7 +79,7 @@ def test__populate_class_instance(client): class_type = models.Collection rec_obj = {"name": "Test title", "type": "collection", "items": []} rec_obj = client._populate_class_instance(class_type, rec_obj) - assert type(rec_obj) == class_type + assert type(rec_obj) is class_type assert rec_obj.name == "Test title" @@ -98,7 +100,13 @@ def test_collection_create_metadata_for_items_from_csv( assert 2 == len(collection.items) -def test_collection_post_items(client, input_dir, aspace_delimited_csv, aspace_mapping): +@mock_aws +def test_collection_post_items( + mocked_s3, + client, + aspace_delimited_csv, + aspace_mapping, +): collection = models.Collection.create_metadata_for_items_from_csv( aspace_delimited_csv, aspace_mapping ) @@ -109,14 +117,10 @@ def test_collection_post_items(client, input_dir, aspace_delimited_csv, aspace_m assert item.uuid == "e5f6" -def test_item_bitstreams_in_directory(input_dir): +@mock_aws +def test_item_bitstreams_in_directory(mocked_s3, s3_client): item = models.Item(file_identifier="test") - item.bitstreams_in_directory(input_dir) - assert 3 == len(item.bitstreams) - assert item.bitstreams[0].name == "test_01.jpg" - assert item.bitstreams[1].name == "test_01.pdf" - assert item.bitstreams[2].name == "test_02.pdf" - item.bitstreams_in_directory(input_dir, "pdf") + item.bitstreams_in_directory("s3://test-bucket", s3_client, "pdf") assert 2 == len(item.bitstreams) assert item.bitstreams[0].name == "test_01.pdf" assert item.bitstreams[1].name == "test_02.pdf" From 54e2cdc5105842ef7fbbc887b3170b222c300d2e Mon Sep 17 00:00:00 2001 From: jonavellecuerdo Date: Tue, 20 Feb 2024 14:38:02 -0500 Subject: [PATCH 02/18] Update linting dependencies in Pipfile --- Pipfile | 8 +- Pipfile.lock | 329 ++++++++++++--------------------------------------- 2 files changed, 79 insertions(+), 258 deletions(-) diff --git a/Pipfile b/Pipfile index 01f565d..8f999d0 100644 --- a/Pipfile +++ b/Pipfile @@ -4,14 +4,10 @@ url = "https://pypi.org/simple" verify_ssl = true [dev-packages] -pytest = "*" -requests-mock = "*" black = "*" -isort = "*" -flake8 = "*" -bandit = "*" coveralls = "*" -pytest-cov = "*" +pytest = "*" +requests-mock = "*" [packages] requests = "*" diff --git a/Pipfile.lock b/Pipfile.lock index db02088..45fb7b8 100644 --- a/Pipfile.lock +++ b/Pipfile.lock @@ -1,7 +1,7 @@ { "_meta": { "hash": { - "sha256": "a0d2003f6babe8356f6e8909b9c3b998e270ed1109e43538a500e650f73688c0" + "sha256": "ae30472c745abfd1ffe730b236a710b537a03d07505ae508db3748e8fb6d9ee6" }, "pipfile-spec": 6, "requires": { @@ -22,25 +22,23 @@ "sha256:99b87a485a5820b23b879f04c2305b44b951b502fd64be915879d77a7e8fc6f1" ], "index": "pypi", - "markers": "python_version >= '3.7'", "version": "==23.2.0" }, "boto3": { "hashes": [ - "sha256:7c70c6ceb2706c7fad6466a5de174fe6d0d6f5f8f1e052bfaad9cbe4e53b64cd", - "sha256:e74fbad79bc921a74a9a276ef9f38e1e31153f76690fe9bc5ec790007de36572" + "sha256:0d382baac02ba4ead82230f34ba377fbf5f6481321dca911e6664b752d79b682", + "sha256:eb5d84c2127ffddf8e7f4dd6f9084f86cb18dca8416fb5d6bea278298cf8d84c" ], "index": "pypi", - "markers": "python_version >= '3.8'", - "version": "==1.34.38" + "version": "==1.34.46" }, "botocore": { "hashes": [ - "sha256:773e49f5bf596191e796b2a15096ff381e61778cbe7c982b381bb9f6bfe5fef3", - "sha256:da9754a8e1798706427ede9c9c0a55263bd8e57f217c021807b2946eb4a0c2d8" + "sha256:21a6c391c6b4869aed66bc888b8e6d54581b343514cfe97dbe71ede12026c3cc", + "sha256:f54330ba1e8ce31489a4e09b4ba8afbf84be01bbc48dbb31d44897fb7657f7ad" ], "markers": "python_version >= '3.8'", - "version": "==1.34.38" + "version": "==1.34.46" }, "certifi": { "hashes": [ @@ -210,46 +208,45 @@ "sha256:ca9853ad459e787e2192211578cc907e7594e294c7ccc834310722b41b9ca6de" ], "index": "pypi", - "markers": "python_version >= '3.7'", "version": "==8.1.7" }, "cryptography": { "hashes": [ - "sha256:087887e55e0b9c8724cf05361357875adb5c20dec27e5816b653492980d20380", - "sha256:09a77e5b2e8ca732a19a90c5bca2d124621a1edb5438c5daa2d2738bfeb02589", - "sha256:130c0f77022b2b9c99d8cebcdd834d81705f61c68e91ddd614ce74c657f8b3ea", - "sha256:141e2aa5ba100d3788c0ad7919b288f89d1fe015878b9659b307c9ef867d3a65", - "sha256:28cb2c41f131a5758d6ba6a0504150d644054fd9f3203a1e8e8d7ac3aea7f73a", - "sha256:2f9f14185962e6a04ab32d1abe34eae8a9001569ee4edb64d2304bf0d65c53f3", - "sha256:320948ab49883557a256eab46149df79435a22d2fefd6a66fe6946f1b9d9d008", - "sha256:36d4b7c4be6411f58f60d9ce555a73df8406d484ba12a63549c88bd64f7967f1", - "sha256:3b15c678f27d66d247132cbf13df2f75255627bcc9b6a570f7d2fd08e8c081d2", - "sha256:3dbd37e14ce795b4af61b89b037d4bc157f2cb23e676fa16932185a04dfbf635", - "sha256:4383b47f45b14459cab66048d384614019965ba6c1a1a141f11b5a551cace1b2", - "sha256:44c95c0e96b3cb628e8452ec060413a49002a247b2b9938989e23a2c8291fc90", - "sha256:4b063d3413f853e056161eb0c7724822a9740ad3caa24b8424d776cebf98e7ee", - "sha256:52ed9ebf8ac602385126c9a2fe951db36f2cb0c2538d22971487f89d0de4065a", - "sha256:55d1580e2d7e17f45d19d3b12098e352f3a37fe86d380bf45846ef257054b242", - "sha256:5ef9bc3d046ce83c4bbf4c25e1e0547b9c441c01d30922d812e887dc5f125c12", - "sha256:5fa82a26f92871eca593b53359c12ad7949772462f887c35edaf36f87953c0e2", - "sha256:61321672b3ac7aade25c40449ccedbc6db72c7f5f0fdf34def5e2f8b51ca530d", - "sha256:701171f825dcab90969596ce2af253143b93b08f1a716d4b2a9d2db5084ef7be", - "sha256:841ec8af7a8491ac76ec5a9522226e287187a3107e12b7d686ad354bb78facee", - "sha256:8a06641fb07d4e8f6c7dda4fc3f8871d327803ab6542e33831c7ccfdcb4d0ad6", - "sha256:8e88bb9eafbf6a4014d55fb222e7360eef53e613215085e65a13290577394529", - "sha256:a00aee5d1b6c20620161984f8ab2ab69134466c51f58c052c11b076715e72929", - "sha256:a047682d324ba56e61b7ea7c7299d51e61fd3bca7dad2ccc39b72bd0118d60a1", - "sha256:a7ef8dd0bf2e1d0a27042b231a3baac6883cdd5557036f5e8df7139255feaac6", - "sha256:ad28cff53f60d99a928dfcf1e861e0b2ceb2bc1f08a074fdd601b314e1cc9e0a", - "sha256:b9097a208875fc7bbeb1286d0125d90bdfed961f61f214d3f5be62cd4ed8a446", - "sha256:b97fe7d7991c25e6a31e5d5e795986b18fbbb3107b873d5f3ae6dc9a103278e9", - "sha256:e0ec52ba3c7f1b7d813cd52649a5b3ef1fc0d433219dc8c93827c57eab6cf888", - "sha256:ea2c3ffb662fec8bbbfce5602e2c159ff097a4631d96235fcf0fb00e59e3ece4", - "sha256:fa3dec4ba8fb6e662770b74f62f1a0c7d4e37e25b58b2bf2c1be4c95372b4a33", - "sha256:fbeb725c9dc799a574518109336acccaf1303c30d45c075c665c0793c2f79a7f" + "sha256:01911714117642a3f1792c7f376db572aadadbafcd8d75bb527166009c9f1d1b", + "sha256:0e89f7b84f421c56e7ff69f11c441ebda73b8a8e6488d322ef71746224c20fce", + "sha256:12d341bd42cdb7d4937b0cabbdf2a94f949413ac4504904d0cdbdce4a22cbf88", + "sha256:15a1fb843c48b4a604663fa30af60818cd28f895572386e5f9b8a665874c26e7", + "sha256:1cdcdbd117681c88d717437ada72bdd5be9de117f96e3f4d50dab3f59fd9ab20", + "sha256:1df6fcbf60560d2113b5ed90f072dc0b108d64750d4cbd46a21ec882c7aefce9", + "sha256:3c6048f217533d89f2f8f4f0fe3044bf0b2090453b7b73d0b77db47b80af8dff", + "sha256:3e970a2119507d0b104f0a8e281521ad28fc26f2820687b3436b8c9a5fcf20d1", + "sha256:44a64043f743485925d3bcac548d05df0f9bb445c5fcca6681889c7c3ab12764", + "sha256:4e36685cb634af55e0677d435d425043967ac2f3790ec652b2b88ad03b85c27b", + "sha256:5f8907fcf57392cd917892ae83708761c6ff3c37a8e835d7246ff0ad251d9298", + "sha256:69b22ab6506a3fe483d67d1ed878e1602bdd5912a134e6202c1ec672233241c1", + "sha256:6bfadd884e7280df24d26f2186e4e07556a05d37393b0f220a840b083dc6a824", + "sha256:6d0fbe73728c44ca3a241eff9aefe6496ab2656d6e7a4ea2459865f2e8613257", + "sha256:6ffb03d419edcab93b4b19c22ee80c007fb2d708429cecebf1dd3258956a563a", + "sha256:810bcf151caefc03e51a3d61e53335cd5c7316c0a105cc695f0959f2c638b129", + "sha256:831a4b37accef30cccd34fcb916a5d7b5be3cbbe27268a02832c3e450aea39cb", + "sha256:887623fe0d70f48ab3f5e4dbf234986b1329a64c066d719432d0698522749929", + "sha256:a0298bdc6e98ca21382afe914c642620370ce0470a01e1bef6dd9b5354c36854", + "sha256:a1327f280c824ff7885bdeef8578f74690e9079267c1c8bd7dc5cc5aa065ae52", + "sha256:c1f25b252d2c87088abc8bbc4f1ecbf7c919e05508a7e8628e6875c40bc70923", + "sha256:c3a5cbc620e1e17009f30dd34cb0d85c987afd21c41a74352d1719be33380885", + "sha256:ce8613beaffc7c14f091497346ef117c1798c202b01153a8cc7b8e2ebaaf41c0", + "sha256:d2a27aca5597c8a71abbe10209184e1a8e91c1fd470b5070a2ea60cafec35bcd", + "sha256:dad9c385ba8ee025bb0d856714f71d7840020fe176ae0229de618f14dae7a6e2", + "sha256:db4b65b02f59035037fde0998974d84244a64c3265bdef32a827ab9b63d61b18", + "sha256:e09469a2cec88fb7b078e16d4adec594414397e8879a4341c6ace96013463d5b", + "sha256:e53dc41cda40b248ebc40b83b31516487f7db95ab8ceac1f042626bc43a2f992", + "sha256:f1e85a178384bf19e36779d91ff35c7617c885da487d689b05c1366f9933ad74", + "sha256:f47be41843200f7faec0683ad751e5ef11b9a56a220d57f300376cd8aba81660", + "sha256:fb0cef872d8193e487fc6bdb08559c3aa41b659a7d9be48b2e10747f47863925", + "sha256:ffc73996c4fca3d2b6c1c8c12bfd3ad00def8621da24f547626bf06441400449" ], "markers": "python_version >= '3.7'", - "version": "==42.0.2" + "version": "==42.0.4" }, "idna": { "hashes": [ @@ -357,7 +354,6 @@ "sha256:f643ffd2669ffd4b5a3e9b41c909b72b2a1d5e4915da90a77e119b8d48ce867a" ], "index": "pypi", - "markers": "python_version >= '3.6'", "version": "==5.1.0" }, "markupsafe": { @@ -431,11 +427,11 @@ "s3" ], "hashes": [ - "sha256:62b9798aef9028432194cebb7a671f4064257bb3be662d9c1b83b94411b694bb", - "sha256:94e3b07a403cc8078ffee94bf404ef677112d036a57ddb5e0f19c5fcf48987f5" + "sha256:71bb832a18b64f10fc4cec117b9b0e2305e5831d9a17eb74f6b9819ed7613843", + "sha256:7e27395e5c63ff9554ae14b5baa41bfe6d6b1be0e59eb02977c6ce28411246de" ], - "markers": "python_version >= '3.8'", - "version": "==5.0.1" + "index": "pypi", + "version": "==5.0.2" }, "py-partiql-parser": { "hashes": [ @@ -456,7 +452,7 @@ "sha256:0123cacc1627ae19ddf3c27a5de5bd67ee4586fbdd6440d9748f8abb483d3e86", "sha256:961d03dc3453ebbc59dbdea9e4e11c5651520a876d0f4db161e8674aae935da9" ], - "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3'", + "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2'", "version": "==2.8.2" }, "pyyaml": { @@ -521,16 +517,15 @@ "sha256:942c5a758f98d790eaed1a29cb6eefc7ffb0d1cf7af05c3d2791656dbd6ad1e1" ], "index": "pypi", - "markers": "python_version >= '3.7'", "version": "==2.31.0" }, "responses": { "hashes": [ - "sha256:a2b43f4c08bfb9c9bd242568328c65a34b318741d3fab884ac843c5ceeb543f9", - "sha256:b127c6ca3f8df0eb9cc82fd93109a3007a86acb24871834c47b77765152ecf8c" + "sha256:01ae6a02b4f34e39bffceb0fc6786b67a25eae919c6368d05eabc8d9576c2a66", + "sha256:2f0b9c2b6437db4b528619a77e5d565e4ec2a9532162ac1a131a83529db7be1a" ], "markers": "python_version >= '3.8'", - "version": "==0.24.1" + "version": "==0.25.0" }, "s3transfer": { "hashes": [ @@ -545,7 +540,7 @@ "sha256:1e61c37477a1626458e36f7b1d82aa5c9b094fa4802892072e49de9c60c4c926", "sha256:8abb2f1d86890a2dfb989f9a77cfcfd3e47c2a354b01111771326f8aa26e0254" ], - "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3'", + "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2'", "version": "==1.16.0" }, "smart-open": { @@ -556,7 +551,7 @@ "sha256:8d3ef7e6997e8e42dd55c74166ed21e6ac70664caa32dd940b26d54a8f6b4142", "sha256:be3c92c246fbe80ebce8fbacb180494a481a77fcdcb7c1aadb2ea5b9c2bee8b9" ], - "markers": "python_version >= '3.6' and python_version < '4.0'", + "index": "pypi", "version": "==6.4.0" }, "structlog": { @@ -565,7 +560,6 @@ "sha256:41a09886e4d55df25bdcb9b5c9674bccfab723ff43e0a86a1b7b236be8e57b16" ], "index": "pypi", - "markers": "python_version >= '3.8'", "version": "==24.1.0" }, "urllib3": { @@ -594,43 +588,33 @@ } }, "develop": { - "bandit": { - "hashes": [ - "sha256:17e60786a7ea3c9ec84569fd5aee09936d116cb0cb43151023258340dbffb7ed", - "sha256:527906bec6088cb499aae31bc962864b4e77569e9d529ee51df3a93b4b8ab28a" - ], - "index": "pypi", - "markers": "python_version >= '3.8'", - "version": "==1.7.7" - }, "black": { "hashes": [ - "sha256:0269dfdea12442022e88043d2910429bed717b2d04523867a85dacce535916b8", - "sha256:07204d078e25327aad9ed2c64790d681238686bce254c910de640c7cc4fc3aa6", - "sha256:08b34e85170d368c37ca7bf81cf67ac863c9d1963b2c1780c39102187ec8dd62", - "sha256:1a95915c98d6e32ca43809d46d932e2abc5f1f7d582ffbe65a5b4d1588af7445", - "sha256:2588021038bd5ada078de606f2a804cadd0a3cc6a79cb3e9bb3a8bf581325a4c", - "sha256:2fa6a0e965779c8f2afb286f9ef798df770ba2b6cee063c650b96adec22c056a", - "sha256:34afe9da5056aa123b8bfda1664bfe6fb4e9c6f311d8e4a6eb089da9a9173bf9", - "sha256:3897ae5a21ca132efa219c029cce5e6bfc9c3d34ed7e892113d199c0b1b444a2", - "sha256:40657e1b78212d582a0edecafef133cf1dd02e6677f539b669db4746150d38f6", - "sha256:48b5760dcbfe5cf97fd4fba23946681f3a81514c6ab8a45b50da67ac8fbc6c7b", - "sha256:5242ecd9e990aeb995b6d03dc3b2d112d4a78f2083e5a8e86d566340ae80fec4", - "sha256:5cdc2e2195212208fbcae579b931407c1fa9997584f0a415421748aeafff1168", - "sha256:5d7b06ea8816cbd4becfe5f70accae953c53c0e53aa98730ceccb0395520ee5d", - "sha256:7258c27115c1e3b5de9ac6c4f9957e3ee2c02c0b39222a24dc7aa03ba0e986f5", - "sha256:854c06fb86fd854140f37fb24dbf10621f5dab9e3b0c29a690ba595e3d543024", - "sha256:a21725862d0e855ae05da1dd25e3825ed712eaaccef6b03017fe0853a01aa45e", - "sha256:a83fe522d9698d8f9a101b860b1ee154c1d25f8a82ceb807d319f085b2627c5b", - "sha256:b3d64db762eae4a5ce04b6e3dd745dcca0fb9560eb931a5be97472e38652a161", - "sha256:e298d588744efda02379521a19639ebcd314fba7a49be22136204d7ed1782717", - "sha256:e2c8dfa14677f90d976f68e0c923947ae68fa3961d61ee30976c388adc0b02c8", - "sha256:ecba2a15dfb2d97105be74bbfe5128bc5e9fa8477d8c46766505c1dda5883aac", - "sha256:fc1ec9aa6f4d98d022101e015261c056ddebe3da6a8ccfc2c792cbe0349d48b7" + "sha256:057c3dc602eaa6fdc451069bd027a1b2635028b575a6c3acfd63193ced20d9c8", + "sha256:08654d0797e65f2423f850fc8e16a0ce50925f9337fb4a4a176a7aa4026e63f8", + "sha256:163baf4ef40e6897a2a9b83890e59141cc8c2a98f2dda5080dc15c00ee1e62cd", + "sha256:1e08fb9a15c914b81dd734ddd7fb10513016e5ce7e6704bdd5e1251ceee51ac9", + "sha256:4dd76e9468d5536abd40ffbc7a247f83b2324f0c050556d9c371c2b9a9a95e31", + "sha256:4f9de21bafcba9683853f6c96c2d515e364aee631b178eaa5145fc1c61a3cc92", + "sha256:61a0391772490ddfb8a693c067df1ef5227257e72b0e4108482b8d41b5aee13f", + "sha256:6981eae48b3b33399c8757036c7f5d48a535b962a7c2310d19361edeef64ce29", + "sha256:7e53a8c630f71db01b28cd9602a1ada68c937cbf2c333e6ed041390d6968faf4", + "sha256:810d445ae6069ce64030c78ff6127cd9cd178a9ac3361435708b907d8a04c693", + "sha256:93601c2deb321b4bad8f95df408e3fb3943d85012dddb6121336b8e24a0d1218", + "sha256:992e451b04667116680cb88f63449267c13e1ad134f30087dec8527242e9862a", + "sha256:9db528bccb9e8e20c08e716b3b09c6bdd64da0dd129b11e160bf082d4642ac23", + "sha256:a0057f800de6acc4407fe75bb147b0c2b5cbb7c3ed110d3e5999cd01184d53b0", + "sha256:ba15742a13de85e9b8f3239c8f807723991fbfae24bad92d34a2b12e81904982", + "sha256:bce4f25c27c3435e4dace4815bcb2008b87e167e3bf4ee47ccdc5ce906eb4894", + "sha256:ca610d29415ee1a30a3f30fab7a8f4144e9d34c89a235d81292a1edb2b55f540", + "sha256:d533d5e3259720fdbc1b37444491b024003e012c5173f7d06825a77508085430", + "sha256:d84f29eb3ee44859052073b7636533ec995bd0f64e2fb43aeceefc70090e752b", + "sha256:e37c99f89929af50ffaf912454b3e3b47fd64109659026b678c091a4cd450fb2", + "sha256:e8a6ae970537e67830776488bca52000eaa37fa63b9988e8c487458d9cd5ace6", + "sha256:faf2ee02e6612577ba0181f4347bcbcf591eb122f7841ae5ba233d12c39dcb4d" ], "index": "pypi", - "markers": "python_version >= '3.8'", - "version": "==24.1.1" + "version": "==24.2.0" }, "certifi": { "hashes": [ @@ -742,7 +726,6 @@ "sha256:ca9853ad459e787e2192211578cc907e7594e294c7ccc834310722b41b9ca6de" ], "index": "pypi", - "markers": "python_version >= '3.7'", "version": "==8.1.7" }, "coverage": { @@ -807,7 +790,6 @@ "sha256:f42015f31d386b351d4226389b387ae173207058832fbf5c8ec4b40e27b16026" ], "index": "pypi", - "markers": "python_version >= '3.5'", "version": "==3.3.1" }, "docopt": { @@ -824,15 +806,6 @@ "markers": "python_version < '3.11'", "version": "==1.2.0" }, - "flake8": { - "hashes": [ - "sha256:33f96621059e65eec474169085dc92bf26e7b2d47366b70be2f67ab80dc25132", - "sha256:a6dfbb75e03252917f2473ea9653f7cd799c3064e54d4c8140044c5c065f53c3" - ], - "index": "pypi", - "markers": "python_full_version >= '3.8.1'", - "version": "==7.0.0" - }, "idna": { "hashes": [ "sha256:9ecdbbd083b06798ae1e86adcbfe8ab1479cf864e4ee30fe4e46a003d12491ca", @@ -849,39 +822,6 @@ "markers": "python_version >= '3.7'", "version": "==2.0.0" }, - "isort": { - "hashes": [ - "sha256:48fdfcb9face5d58a4f6dde2e72a1fb8dcaf8ab26f95ab49fab84c2ddefb0109", - "sha256:8ca5e72a8d85860d5a3fa69b8745237f2939afe12dbf656afbcb47fe72d947a6" - ], - "index": "pypi", - "markers": "python_full_version >= '3.8.0'", - "version": "==5.13.2" - }, - "markdown-it-py": { - "hashes": [ - "sha256:355216845c60bd96232cd8d8c40e8f9765cc86f46880e43a8fd22dc1a1a8cab1", - "sha256:e3f60a94fa066dc52ec76661e37c851cb232d92f9886b15cb560aaada2df8feb" - ], - "markers": "python_version >= '3.8'", - "version": "==3.0.0" - }, - "mccabe": { - "hashes": [ - "sha256:348e0240c33b60bbdf4e523192ef919f28cb2c3d7d5c7794f74009290f236325", - "sha256:6c2d30ab6be0e4a46919781807b4f0d834ebdd6c6e3dca0bda5a15f863427b6e" - ], - "markers": "python_version >= '3.6'", - "version": "==0.7.0" - }, - "mdurl": { - "hashes": [ - "sha256:84008a41e51615a49fc9966191ff91509e3c40b939176e643fd50a5c2196b8f8", - "sha256:bb413d29f5eea38f31dd4754dd7377d4465116fb207585f97bf925588687c1ba" - ], - "markers": "python_version >= '3.7'", - "version": "==0.1.2" - }, "mypy-extensions": { "hashes": [ "sha256:4392f6c0eb8a5668a69e23d168ffa70f0be9ccfd32b5cc2d26a34ae5b844552d", @@ -906,14 +846,6 @@ "markers": "python_version >= '3.8'", "version": "==0.12.1" }, - "pbr": { - "hashes": [ - "sha256:4a7317d5e3b17a3dccb6a8cfe67dab65b20551404c52c8ed41279fa4f0cb4cda", - "sha256:d1377122a5a00e2f940ee482999518efe16d745d423a670c27773dfbc3c9a7d9" - ], - "markers": "python_version >= '2.6'", - "version": "==6.0.0" - }, "platformdirs": { "hashes": [ "sha256:0614df2a2f37e1a662acbd8e2b25b92ccf8632929bc6d43467e17fe89c75e068", @@ -930,103 +862,13 @@ "markers": "python_version >= '3.8'", "version": "==1.4.0" }, - "pycodestyle": { - "hashes": [ - "sha256:41ba0e7afc9752dfb53ced5489e89f8186be00e599e712660695b7a75ff2663f", - "sha256:44fe31000b2d866f2e41841b18528a505fbd7fef9017b04eff4e2648a0fadc67" - ], - "markers": "python_version >= '3.8'", - "version": "==2.11.1" - }, - "pyflakes": { - "hashes": [ - "sha256:1c61603ff154621fb2a9172037d84dca3500def8c8b630657d1701f026f8af3f", - "sha256:84b5be138a2dfbb40689ca07e2152deb896a65c3a3e24c251c5c62489568074a" - ], - "markers": "python_version >= '3.8'", - "version": "==3.2.0" - }, - "pygments": { - "hashes": [ - "sha256:b27c2826c47d0f3219f29554824c30c5e8945175d888647acd804ddd04af846c", - "sha256:da46cec9fd2de5be3a8a784f434e4c4ab670b4ff54d605c4c2717e9d49c4c367" - ], - "markers": "python_version >= '3.7'", - "version": "==2.17.2" - }, "pytest": { "hashes": [ - "sha256:249b1b0864530ba251b7438274c4d251c58d868edaaec8762893ad4a0d71c36c", - "sha256:50fb9cbe836c3f20f0dfa99c565201fb75dc54c8d76373cd1bde06b06657bdb6" + "sha256:267f6563751877d772019b13aacbe4e860d73fe8f651f28112e9ac37de7513ae", + "sha256:3e4f16fe1c0a9dc9d9389161c127c3edc5d810c38d6793042fb81d9f48a59fca" ], "index": "pypi", - "markers": "python_version >= '3.8'", - "version": "==8.0.0" - }, - "pytest-cov": { - "hashes": [ - "sha256:3904b13dfbfec47f003b8e77fd5b589cd11904a21ddf1ab38a64f204d6a10ef6", - "sha256:6ba70b9e97e69fcc3fb45bfeab2d0a138fb65c4d0d6a41ef33983ad114be8c3a" - ], - "index": "pypi", - "markers": "python_version >= '3.7'", - "version": "==4.1.0" - }, - "pyyaml": { - "hashes": [ - "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:a08c6f0fe150303c1c6b71ebcd7213c2858041a7e01975da3a99aed1e7a378ef", - "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" - ], - "version": "==6.0.1" + "version": "==8.0.1" }, "requests": { "hashes": [ @@ -1034,7 +876,6 @@ "sha256:942c5a758f98d790eaed1a29cb6eefc7ffb0d1cf7af05c3d2791656dbd6ad1e1" ], "index": "pypi", - "markers": "python_version >= '3.7'", "version": "==2.31.0" }, "requests-mock": { @@ -1045,30 +886,14 @@ "index": "pypi", "version": "==1.11.0" }, - "rich": { - "hashes": [ - "sha256:5cb5123b5cf9ee70584244246816e9114227e0b98ad9176eede6ad54bf5403fa", - "sha256:6da14c108c4866ee9520bbffa71f6fe3962e193b7da68720583850cd4548e235" - ], - "markers": "python_full_version >= '3.7.0'", - "version": "==13.7.0" - }, "six": { "hashes": [ "sha256:1e61c37477a1626458e36f7b1d82aa5c9b094fa4802892072e49de9c60c4c926", "sha256:8abb2f1d86890a2dfb989f9a77cfcfd3e47c2a354b01111771326f8aa26e0254" ], - "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3'", + "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2'", "version": "==1.16.0" }, - "stevedore": { - "hashes": [ - "sha256:8cc040628f3cea5d7128f2e76cf486b2251a4e543c7b938f58d9a377f6694a2d", - "sha256:a54534acf9b89bc7ed264807013b505bf07f74dbe4bcfa37d32bd063870b087c" - ], - "markers": "python_version >= '3.8'", - "version": "==5.1.0" - }, "tomli": { "hashes": [ "sha256:939de3e7a6161af0c887ef91b7d41a53e7c5a1ca976325f429cb46ea9bc30ecc", From 5abd5395185df4450cadd6c685445601674d8e1d Mon Sep 17 00:00:00 2001 From: jonavellecuerdo Date: Tue, 20 Feb 2024 14:44:30 -0500 Subject: [PATCH 03/18] Replace setup.cfg with pyproject.toml --- pyproject.toml | 70 ++++++++++++++++++++++++++++++++++++++++++++++++++ setup.cfg | 6 ----- 2 files changed, 70 insertions(+), 6 deletions(-) create mode 100644 pyproject.toml delete mode 100644 setup.cfg diff --git a/pyproject.toml b/pyproject.toml new file mode 100644 index 0000000..5536da1 --- /dev/null +++ b/pyproject.toml @@ -0,0 +1,70 @@ +# We do our best to sync this template with the latest version in our internal documentation. +# For MIT developers, we strongly recommend copying the pyproject.toml template from: +# https://mitlibraries.atlassian.net/wiki/spaces/IN/pages/3432415247/Python+Project+Linters#Template-for-pyproject.toml + +[tool.black] +line-length = 90 + +[tool.mypy] +disallow_untyped_calls = true +disallow_untyped_defs = true +exclude = ["tests/"] + +[tool.pytest.ini_options] +log_level = "INFO" + +[tool.ruff] +target-version = "py311" + +# set max line length +line-length = 90 + +# enumerate all fixed violations +show-fixes = true + +[tool.ruff.lint] +select = ["ALL", "PT"] +ignore = [ + # default + "ANN101", + "ANN102", + "COM812", + "D107", + "N812", + "PTH", + + # project-specific + "C90", + "D100", + "D101", + "D102", + "D103", + "D104", + "PLR0912", + "PLR0913", + "PLR0915", + "S320", + "S321", +] + +# allow autofix behavior for specified rules +fixable = ["E", "F", "I", "Q"] + +[tool.ruff.lint.flake8-annotations] +mypy-init-return = true + +[tool.ruff.lint.flake8-pytest-style] +fixture-parentheses = false + +[tool.ruff.lint.per-file-ignores] +"tests/**/*" = [ + "ANN", + "ARG001", + "S101", +] + +[tool.ruff.lint.pycodestyle] +max-doc-length = 90 + +[tool.ruff.lint.pydocstyle] +convention = "google" \ No newline at end of file diff --git a/setup.cfg b/setup.cfg deleted file mode 100644 index 6413b09..0000000 --- a/setup.cfg +++ /dev/null @@ -1,6 +0,0 @@ -[flake8] -max-line-length = 90 -extend-ignore = E203 - -[isort] -profile = black \ No newline at end of file From e1ef05326b093d41a86d3626b62d7967152f4cd5 Mon Sep 17 00:00:00 2001 From: jonavellecuerdo Date: Tue, 20 Feb 2024 14:46:56 -0500 Subject: [PATCH 04/18] Resolve linting errors from black --- dsaps/cli.py | 4 +--- dsaps/models.py | 4 +--- 2 files changed, 2 insertions(+), 6 deletions(-) diff --git a/dsaps/cli.py b/dsaps/cli.py index 7c5d80d..933c79a 100644 --- a/dsaps/cli.py +++ b/dsaps/cli.py @@ -219,7 +219,5 @@ def reconcile(ctx, metadata_csv, output_directory, content_directory, file_type) no_metadata = set(file_ids) - set(file_matches) helpers.create_csv_from_list(no_metadata, f"{output_directory}no_metadata") helpers.create_csv_from_list(no_files, f"{output_directory}no_files") - helpers.create_csv_from_list( - metadata_matches, f"{output_directory}metadata_matches" - ) + helpers.create_csv_from_list(metadata_matches, f"{output_directory}metadata_matches") helpers.update_metadata_csv(metadata_csv, output_directory, metadata_matches) diff --git a/dsaps/models.py b/dsaps/models.py index 3741980..0bb7989 100644 --- a/dsaps/models.py +++ b/dsaps/models.py @@ -256,9 +256,7 @@ def metadata_from_csv_row(cls, row, field_map): ] ) else: - metadata.append( - MetadataEntry(key=f, value=field, language=language) - ) + metadata.append(MetadataEntry(key=f, value=field, language=language)) return cls( metadata=metadata, file_identifier=file_identifier, From 92f58275c90579f2e1a2c7fed21e6e5de2350c22 Mon Sep 17 00:00:00 2001 From: jonavellecuerdo Date: Tue, 20 Feb 2024 14:55:08 -0500 Subject: [PATCH 05/18] Update Makefile --- Makefile | 48 +++++++++++++++++++++++++++++++++++------------- 1 file changed, 35 insertions(+), 13 deletions(-) diff --git a/Makefile b/Makefile index 1a2476f..9990ce3 100644 --- a/Makefile +++ b/Makefile @@ -1,19 +1,41 @@ -lint: bandit black flake8 isort +SHELL=/bin/bash +DATETIME:=$(shell date -u +%Y%m%dT%H%M%SZ) -bandit: - pipenv run bandit -r dsaps +help: # preview Makefile commands + @awk 'BEGIN { FS = ":.*#"; print "Usage: make \n\nTargets:" } \ +/^[-_[:alpha:]]+:.?*#/ { printf " %-15s%s\n", $$1, $$2 }' $(MAKEFILE_LIST) -black: - pipenv run black --check --diff dsaps tests +## Dependency Commands +install: # install Python dependencies + pipenv install --dev +#pipenv run pre-commit install -coveralls: test - pipenv run coveralls +update: install # update Python dependencies + pipenv clean + pipenv update --dev -flake8: - pipenv run flake8 dsaps tests +## Unit test commands +test: # run tests and print a coverage report + pipenv run coverage run --source=dsaps -m pytest -vv + pipenv run coverage report -m + +coveralls: test # write coverage data to an LCOV report + pipenv run coverage lcov -o ./coverage/lcov.info + +## Code quality and safety commands +lint: black safety # run linters + +black: # run 'black' linter and print a preview of suggested changes + pipenv run black --check --diff . + +safety: # check for security vulnerabilities and verify Pipfile.lock is up to date + pipenv check + pipenv verify + +lint-apply: # apply changes with 'black' and resolve 'fixable errors' with 'ruff' + black-apply + +black-apply: # apply changes with 'black' + pipenv run black . -isort: - pipenv run isort dsaps tests --diff -test: - pipenv run pytest --cov=dsaps From 137eb78885739e5fdbd154662e85c698b9b8f9af Mon Sep 17 00:00:00 2001 From: jonavellecuerdo Date: Wed, 21 Feb 2024 12:21:00 -0500 Subject: [PATCH 06/18] Update PR template --- .github/pull-request-template.md | 44 +++++++++++++++----------------- 1 file changed, 21 insertions(+), 23 deletions(-) diff --git a/.github/pull-request-template.md b/.github/pull-request-template.md index 08efd73..ad4ef3f 100644 --- a/.github/pull-request-template.md +++ b/.github/pull-request-template.md @@ -1,31 +1,29 @@ -#### What does this PR do? -A few sentences describing the overall goals of the pull request's commits. -Why are we making these changes? Is there more work to be done to fully -achieve these goals? - -#### Helpful background context - -Describe any additional context beyond what the PR accomplishes if it is likely -to be useful to a reviewer. - -Delete this section if it isn't applicable to the PR. - -#### How can a reviewer manually see the effects of these changes? +### Purpose and background context +Describe the overall purpose of the PR changes and any useful background context. +### How can a reviewer manually see the effects of these changes? Explain how to see the proposed changes in the application if possible. Delete this section if it isn't applicable to the PR. -#### What are the relevant tickets? - -- https://mitlibraries.atlassian.net/browse/DIP- - -#### Screenshots (if appropriate) - -Delete this section if it isn't applicable to the PR. - -#### Requires Database Migrations? +### Includes new or updated dependencies? YES | NO -#### Includes new or updated dependencies? +### Changes expectations for external applications? YES | NO + +### What are the relevant tickets? +- Include links to Jira Software and/or Jira Service Management tickets here. + +### Developer +- [ ] All new ENV is documented in README +- [ ] All new ENV has been added to staging and production environments +- [ ] All related Jira tickets are linked in commit message(s) +- [ ] Stakeholder approval has been confirmed (or is not needed) + +### Code Reviewer(s) +- [ ] The commit message is clear and follows our guidelines (not just this PR message) +- [ ] There are appropriate tests covering any new functionality +- [ ] The provided documentation is sufficient for understanding any new functionality introduced +- [ ] Any manual tests have been performed **or** provided examples verified +- [ ] New dependencies are appropriate or there were no changes From b9ce9d6082fabfdcf93531651eaa1b6a75083d30 Mon Sep 17 00:00:00 2001 From: jonavellecuerdo Date: Wed, 21 Feb 2024 11:29:28 -0500 Subject: [PATCH 07/18] Update documentation --- README.md | 157 ++++++++++++++++++++++++++++++++++++--------------- dsaps/cli.py | 63 +++++++++++++-------- 2 files changed, 148 insertions(+), 72 deletions(-) diff --git a/README.md b/README.md index be61a0e..3ece743 100644 --- a/README.md +++ b/README.md @@ -1,72 +1,135 @@ -# dsaps +# DSpace API Python Scripts -This command line application provides several ways of interacting with the [DSpace](https://github.com/DSpace/DSpace) API. This application was written for DSpace 6.3, it has not been tested against other DSpace versions. Previously, this branch of the repository was a set of self-contained scripts that could be run independently, those scripts can be found as a [release](https://github.com/MITLibraries/dspace-api-python-scripts/releases/tag/v1.0). +DSpace API Python Scripts (DSAPS) is a Python CLI application for managing uploads to DSpace. DSAPS has only been used on instances running DSpace 6.3 and has not been tested on other versions. + +Note: Previously, the repository comprised of self-contained scripts that could be run independently. Those scripts can be found as a [release](https://github.com/MITLibraries/dspace-api-python-scripts/releases/tag/v1.0). + +## Development + +- To preview a list of available Makefile commands: `make help` +- To install with dev dependencies: `make install` +- To update dependencies: `make update` +- To run unit tests: `make test` +- To lint the repo: `make lint` +- To run the app: `pipenv run dsaps --help` + +The example below shows how to run the `reconcile` command: -## Installation -Clone the repository and install using [pipenv](https://github.com/pypa/pipenv): -``` -pipenv install -``` -After installation, run the application with: ``` -pipenv run dsaps +pipenv run dsaps --url $DSPACE_URL -e $DSPACE_EMAIL -p $DSPACE_PASSWORD reconcile -m -o /output -d -t ``` -## Authentication +## Environment + +### Required -To authenticate, use the following parameters +```shell +# The url for the DSpace REST API +DSPACE_URL= -Option (short) | Option (long)     | Description ------- | ------ | ----------- -N/A | --url | The DSpace API URL (e.g. https://dspace.mit.edu/rest), defaults to the DSPACE_URL environmental variable if nothing is specified --e | --email | The email of the user for authentication. --p | --password | The password for authentication. +# The email associated with the DSpace user account used for authentication +DSPACE_EMAIL= -## Commands +# The password associated with the DSpace user account used for authentication +DSPACE_PASSWORD= +``` -### additems -Adds items to a specified collection from a metadata CSV, a field mapping file, and a directory of files. May be run in conjunction with the newcollection CLI command. +## CLI commands -Option (short) | Option (long)             | Description ------- | ------ | ------- --m | --metadata-csv | The path to the CSV file of metadata for the items. --f | --field-map | The path to JSON field mapping file. --d | --content-directory | The full path to the content, either a directory of files or a URL for the storage location. --t | --file-type | The file type to be uploaded, if limited to one file type. --r | --ingest-report| Create ingest report for updating other systems. --c | --collection-handle | The handle of the collection to which items are being added. +All CLI commands can be run with `pipenv run `. +### `dsaps` -#### Example Usage ``` -pipenv run dsaps --url https://dspace.com/rest -e abc@def.com -p ******** additems -m coll_metadata.csv -f config/aspace_mapping.json -d /files/pdfs -t pdf -r -c 111.1/111111 +Usage: -c [OPTIONS] COMMAND1 [ARGS]... [COMMAND2 [ARGS]...]... + +Options: + --url TEXT The url for the DSpace REST API. Defaults to env var + DSPACE_URL if not set. [required] + -e, --email TEXT The email associated with the DSpace user account used + for authentication. Defaults to env var DSPACE_EMAIL if + not set. [required] + -p, --password TEXT The password associated with the DSpace user account + used for authentication. Defaults to env var + DSPACE_PASSWORD if not set. [required] + --help Show this message and exit. + +Commands: + additems Add items to a DSpace collection. + newcollection Create a new DSpace collection within a community. + reconcile Match files in the content directory with entries in the metadata CSV file. ``` -### newcollection -Posts a new collection to a specified community. Used in conjunction with the additems CLI command to populate the new collection with items. +### `dsaps reconcile` -Option (short) | Option (long)            | Description ------- | ------ | ------- --c | --community-handle | The handle of the community in which to create the collection. --n | --collection-name | The name of the collection to be created. +``` +Usage: -c reconcile [OPTIONS] -#### Example Usage + Match files in the content directory with entries in the metadata CSV file. + + Running this method creates the following CSV files: + + * metadata_matches.csv: File identifiers for entries in metadata CSV + file with a corresponding file in the content directory. + + * no_files.csv: File identifiers for entries in metadata CSV file + without a corresponding file in the content directory. + + * no_metadata.csv: File identifiers for files in the content directory + without a corresponding entry in the metadata CSV file. + + * updated-.csv: Entries from the metadata CSV file with a + corresponding file in the content directory. + +Options: + -m, --metadata-csv FILE The filepath to a CSV file containing metadata + for Dspace uploads. [required] + -o, --output-directory TEXT The filepath where output files are written. + -d, --content-directory TEXT The name of the S3 bucket containing files for + DSpace uploads. [required] + -t, --file-type TEXT The file type for DSpace uploads (i.e., the + file extension, excluding the dot). + --help Show this message and exit. ``` -pipenv run dsaps --url https://dspace.com/rest -e abc@def.com -p ******** newcollection -c 222.2/222222 -n Test\ Collection additems -m coll_metadata.csv -f config/aspace_mapping.json -d /files/pdfs -t pdf -r + +### `dsaps new collection` ``` +Usage: -c newcollection [OPTIONS] -### reconcile -Runs a reconciliation of the specified files and metadata that produces reports of files with no metadata, metadata with no files, metadata matched to files, and an updated version of the metadata CSV with only the records that have matching files. + Create a new DSpace collection within a community. +Options: + -c, --community-handle TEXT The handle identifying a DSpace community in + which a new collection is created. [required] + -n, --collection-name TEXT The name assigned to the DSpace collection + being created. [required] + --help Show this message and exit. +``` -Option (short) | Option (long)             | Description ------- | ------ | ------- --m | --metadata-csv | The path of the CSV file of metadata. --o | --output-directory | The path of the output files, include / at the end of the path. --d | --content-directory | The full path to the content, either a directory of files or a URL for the storage location. --t | --file-type | The file type to be uploaded. +### `dsaps additems` -#### Example Usage ``` -pipenv run dsaps --url https://dspace.com/rest -e abc@def.com -p ******** reconcile -m coll_metadata.csv -o /output -d /files/pdfs -t pdf +Usage: -c additems [OPTIONS] + + Add items to a DSpace collection. + + The method relies on a CSV file with metadata for uploads, a JSON document + that maps metadata to a DSpace schema, and a directory containing the files + to be uploaded. + +Options: + -m, --metadata-csv FILE The filepath to a CSV file containing metadata + for Dspace uploads. [required] + -f, --field-map FILE The filepath to a JSON document that maps + columns in the metadata CSV file to a DSpace + schema. [required] + -d, --content-directory TEXT The name of the S3 bucket containing files for + DSpace uploads. [required] + -t, --file-type TEXT The file type for DSpace uploads (i.e., the + file extension, excluding the dot). + -r, --ingest-report Create ingest report for updating other + systems. + -c, --collection-handle TEXT The handle identifying a DSpace collection + into which uploads are deposited. + --help Show this message and exit. ``` diff --git a/dsaps/cli.py b/dsaps/cli.py index 933c79a..6c4a144 100644 --- a/dsaps/cli.py +++ b/dsaps/cli.py @@ -27,13 +27,17 @@ def validate_path(ctx, param, value): "--url", envvar="DSPACE_URL", required=True, + help="The url for the DSpace REST API. Defaults to env var DSPACE_URL if not set.", ) @click.option( "-e", "--email", envvar="DSPACE_EMAIL", required=True, - help="The email of the user for authentication.", + help=( + "The email associated with the DSpace user account used for authentication. " + "Defaults to env var DSPACE_EMAIL if not set." + ), ) @click.option( "-p", @@ -41,7 +45,10 @@ def validate_path(ctx, param, value): envvar="DSPACE_PASSWORD", required=True, hide_input=True, - help="The password for authentication.", + help=( + "The password associated with the DSpace user account used for authentication. " + "Defaults to env var DSPACE_PASSWORD if not set." + ), ) @click.pass_context def main(ctx, url, email, password): @@ -81,26 +88,25 @@ def main(ctx, url, email, password): "--metadata-csv", required=True, type=click.Path(exists=True, file_okay=True, dir_okay=False), - help="The path to the CSV file of metadata for the items.", + help="The filepath to a CSV file containing metadata for Dspace uploads.", ) @click.option( "-f", "--field-map", required=True, type=click.Path(exists=True, file_okay=True, dir_okay=False), - help="The path to JSON field mapping file.", + help="The filepath to a JSON document that maps columns in the metadata CSV file to a DSpace schema.", ) @click.option( "-d", "--content-directory", required=True, - help="The full path to the content, either a directory of files " - "or a URL for the storage location.", + help="The name of the S3 bucket containing files for DSpace uploads.", ) @click.option( "-t", "--file-type", - help="The file type to be uploaded, if limited to one file " "type.", + help="The file type for DSpace uploads (i.e., the file extension, excluding the dot).", default="*", ) @click.option( @@ -112,7 +118,7 @@ def main(ctx, url, email, password): @click.option( "-c", "--collection-handle", - help="The handle of the collection to which items are being " "added.", + help="The handle identifying a DSpace collection into which uploads are deposited.", default=None, ) @click.pass_context @@ -125,9 +131,11 @@ def additems( ingest_report, collection_handle, ): - """Add items to a specified collection from a metadata CSV, a field - mapping file, and a directory of files. May be run in conjunction with the - newcollection CLI command.""" + """Add items to a DSpace collection. + + The method relies on a CSV file with metadata for uploads, a JSON document that maps + metadata to a DSpace schema, and a directory containing the files to be uploaded. + """ client = ctx.obj["client"] start_time = ctx.obj["start_time"] if "collection_uuid" not in ctx.obj and collection_handle is None: @@ -158,19 +166,17 @@ def additems( "-c", "--community-handle", required=True, - help="The handle of the community in which to create the ," "collection.", + help="The handle identifying a DSpace community in which a new collection is created.", ) @click.option( "-n", "--collection-name", required=True, - help="The name of the collection to be created.", + help="The name assigned to the DSpace collection being created.", ) @click.pass_context def newcollection(ctx, community_handle, collection_name): - """Post a new collection to a specified community. Used in conjunction - with the additems CLI command to populate the new collection with - items.""" + """Create a new DSpace collection within a community.""" client = ctx.obj["client"] collection_uuid = client.post_coll_to_comm(community_handle, collection_name) ctx.obj["collection_uuid"] = collection_uuid @@ -182,34 +188,41 @@ def newcollection(ctx, community_handle, collection_name): "--metadata-csv", required=True, type=click.Path(exists=True, file_okay=True, dir_okay=False), - help="The path of the CSV file of metadata.", + help="The filepath to a CSV file containing metadata for Dspace uploads.", ) @click.option( "-o", "--output-directory", default=f"{os.getcwd()}/", callback=validate_path, - help="The path of the output files, include / at the end of the " "path.", + help="The filepath where output files are written.", ) @click.option( "-d", "--content-directory", required=True, - help="The full path to the content, either a directory of files " - "or a URL for the storage location.", + help="The name of the S3 bucket containing files for DSpace uploads.", ) @click.option( "-t", "--file-type", - help="The file type to be uploaded, if limited to one file " "type.", + help="The file type for DSpace uploads (i.e., the file extension, excluding the dot).", default="*", ) @click.pass_context def reconcile(ctx, metadata_csv, output_directory, content_directory, file_type): - """Run a reconciliation of the specified files and metadata to produce - reports of files with no metadata, metadata with no files, metadata - matched to files, and an updated version of the metadata CSV with only - the records that have matching files.""" + """Match files in the content directory with entries in the metadata CSV file. + + Running this method creates the following CSV files: + + * metadata_matches.csv: File identifiers for entries in metadata CSV file with a corresponding file in the content directory. + + * no_files.csv: File identifiers for entries in metadata CSV file without a corresponding file in the content directory. + + * no_metadata.csv: File identifiers for files in the content directory without a corresponding entry in the metadata CSV file. + + * updated-.csv: Entries from the metadata CSV file with a corresponding file in the content directory. + """ client = ctx.obj["client"] file_ids = helpers.create_file_list(content_directory, client.s3_client, file_type) metadata_ids = helpers.create_metadata_id_list(metadata_csv) From d69f7a074e90a1417c88e7bfe21bd23fef926ad0 Mon Sep 17 00:00:00 2001 From: jonavellecuerdo Date: Wed, 28 Feb 2024 11:03:38 -0500 Subject: [PATCH 08/18] Add example call for each CLI command --- README.md | 28 +++++++++++++++++++++++----- 1 file changed, 23 insertions(+), 5 deletions(-) diff --git a/README.md b/README.md index 3ece743..c920a5b 100644 --- a/README.md +++ b/README.md @@ -15,10 +15,6 @@ Note: Previously, the repository comprised of self-contained scripts that could The example below shows how to run the `reconcile` command: -``` -pipenv run dsaps --url $DSPACE_URL -e $DSPACE_EMAIL -p $DSPACE_PASSWORD reconcile -m -o /output -d -t -``` - ## Environment ### Required @@ -92,7 +88,14 @@ Options: --help Show this message and exit. ``` -### `dsaps new collection` +**Example method call**: + +```bash +pipenv run dsaps --url $DSPACE_URL -e $DSPACE_EMAIL -p $DSPACE_PASSWORD reconcile -m -o /output -d -t +``` + + +### `dsaps newcollection` ``` Usage: -c newcollection [OPTIONS] @@ -106,6 +109,14 @@ Options: --help Show this message and exit. ``` +**Example method call**: + +```bash +pipenv run dsaps --url $DSPACE_URL -e $DSPACE_EMAIL -p $DSPACE_PASSWORD newcollection -c -n +``` + + + ### `dsaps additems` ``` @@ -133,3 +144,10 @@ Options: into which uploads are deposited. --help Show this message and exit. ``` + +**Example method call**: + +```bash +pipenv run dsaps --url $DSPACE_URL -e $DSPACE_EMAIL -p $DSPACE_PASSWORD additems -m -f config/.json -d -t -c +``` + From 283444a0901cd9d846a84c6c2b7c4d7eb5d9e41a Mon Sep 17 00:00:00 2001 From: jonavellecuerdo Date: Wed, 28 Feb 2024 14:14:59 -0500 Subject: [PATCH 09/18] Consolidate examples into subsection of 'Development' --- README.md | 41 +++++++++++++++++++---------------------- 1 file changed, 19 insertions(+), 22 deletions(-) diff --git a/README.md b/README.md index c920a5b..a83bf0e 100644 --- a/README.md +++ b/README.md @@ -13,7 +13,25 @@ Note: Previously, the repository comprised of self-contained scripts that could - To lint the repo: `make lint` - To run the app: `pipenv run dsaps --help` -The example below shows how to run the `reconcile` command: +### Reconciling files with metadata CSV + +```bash +pipenv run dsaps --url $DSPACE_URL -e $DSPACE_EMAIL -p $DSPACE_PASSWORD reconcile -m -o /output -d -t +``` + +### Creating a new collection within a DSpace community + +```bash +pipenv run dsaps --url $DSPACE_URL -e $DSPACE_EMAIL -p $DSPACE_PASSWORD newcollection -c -n +``` + +### Adding items to a DSpace collection + +The command below shows `newcollection` and `additems` being run in conjunction with each other. Note that the invocation must call `newcollection` first. In practice, this is the command that is usually run: + +```bash +pipenv run dsaps --url $DSPACE_URL -e $DSPACE_EMAIL -p $DSPACE_PASSWORD newcollection -c -n additems -m -f config/.json -d -t +``` ## Environment @@ -88,13 +106,6 @@ Options: --help Show this message and exit. ``` -**Example method call**: - -```bash -pipenv run dsaps --url $DSPACE_URL -e $DSPACE_EMAIL -p $DSPACE_PASSWORD reconcile -m -o /output -d -t -``` - - ### `dsaps newcollection` ``` Usage: -c newcollection [OPTIONS] @@ -109,14 +120,6 @@ Options: --help Show this message and exit. ``` -**Example method call**: - -```bash -pipenv run dsaps --url $DSPACE_URL -e $DSPACE_EMAIL -p $DSPACE_PASSWORD newcollection -c -n -``` - - - ### `dsaps additems` ``` @@ -145,9 +148,3 @@ Options: --help Show this message and exit. ``` -**Example method call**: - -```bash -pipenv run dsaps --url $DSPACE_URL -e $DSPACE_EMAIL -p $DSPACE_PASSWORD additems -m -f config/.json -d -t -c -``` - From cf217a1e1e4d3783171a4688c2c2c9d1d9cef32f Mon Sep 17 00:00:00 2001 From: Eric Hanson Date: Tue, 19 Mar 2024 15:16:09 -0400 Subject: [PATCH 10/18] Update method for S3 file retrieval Why these changes are being introduced: * Add configurations for S3 file retrieval. How this addresses that need: * Add helpers.get_files_from_s3 * Add helper.parse_value_from_text to parse item IDs and S3 file paths * Add helper method to load source-specific configs * Update tests * Add fixtures mocking S3 buckets where (a) 1-to-many files per file ID and (b) 1-to-1 files per file ID * Add fixture mocking S3 bucket for DDC deposits from ArchivesSpace * Add tests verifying helpers.get_files_from_s3 can successfully retrieve files from 1-to-many or 1-1 structured buckets * Add test verifying helpers.parse_values_from_text can extract item IDs from S3 file object keys Side effects of this change: * None Relevant ticket(s): * https://mitlibraries.atlassian.net/browse/INFRA-426 --- config/aspace.json | 45 ++++++++++ dsaps/cli.py | 44 ++++++---- dsaps/helpers.py | 85 +++++++++++++++---- dsaps/models.py | 15 +--- tests/conftest.py | 38 +++++++++ tests/fixtures/source_config.json | 45 ++++++++++ tests/test_cli.py | 10 ++- tests/test_helpers.py | 133 ++++++++++++++++++++++++++---- tests/test_models.py | 9 -- 9 files changed, 352 insertions(+), 72 deletions(-) create mode 100644 config/aspace.json create mode 100644 tests/fixtures/source_config.json diff --git a/config/aspace.json b/config/aspace.json new file mode 100644 index 0000000..0244a2c --- /dev/null +++ b/config/aspace.json @@ -0,0 +1,45 @@ +{ + "settings": { + "bitstream_folders": [ + "objects" + ], + "id_regex": ".*-(.*?-.*)\\..*$" + }, + "mapping": { + "item_identifier": { + "csv_field_name": "item_identifier", + "language": null, + "delimiter": "" + }, + "dc.title": { + "csv_field_name": "title", + "language": "en_US", + "delimiter": "" + }, + "source_system_identifier": { + "csv_field_name": "uri", + "language": null, + "delimiter": "" + }, + "dc.contributor.author": { + "csv_field_name": "author", + "language": null, + "delimiter": "|" + }, + "dc.description": { + "csv_field_name": "description", + "language": "en_US", + "delimiter": "" + }, + "dc.rights": { + "csv_field_name": "rights_statement", + "language": "en_US", + "delimiter": "" + }, + "dc.rights.uri": { + "csv_field_name": "rights_uri", + "language": null, + "delimiter": "" + } + } +} \ No newline at end of file diff --git a/dsaps/cli.py b/dsaps/cli.py index 6c4a144..17ef937 100644 --- a/dsaps/cli.py +++ b/dsaps/cli.py @@ -23,6 +23,9 @@ def validate_path(ctx, param, value): @click.group(chain=True) +@click.option( + "--config-file", required=True, help="File path to source configuration JSON." +) @click.option( "--url", envvar="DSPACE_URL", @@ -51,7 +54,7 @@ def validate_path(ctx, param, value): ), ) @click.pass_context -def main(ctx, url, email, password): +def main(ctx, config_file, url, email, password): ctx.obj = {} if os.path.isdir("logs") is False: os.mkdir("logs") @@ -77,6 +80,7 @@ def main(ctx, url, email, password): client = Client(url) client.authenticate(email, password) start_time = time.time() + ctx.obj["config"] = helpers.load_source_config(config_file) ctx.obj["client"] = client ctx.obj["start_time"] = start_time ctx.obj["log_suffix"] = log_suffix @@ -203,34 +207,40 @@ def newcollection(ctx, community_handle, collection_name): required=True, help="The name of the S3 bucket containing files for DSpace uploads.", ) -@click.option( - "-t", - "--file-type", - help="The file type for DSpace uploads (i.e., the file extension, excluding the dot).", - default="*", -) @click.pass_context -def reconcile(ctx, metadata_csv, output_directory, content_directory, file_type): +def reconcile(ctx, metadata_csv, output_directory, content_directory): """Match files in the content directory with entries in the metadata CSV file. Running this method creates the following CSV files: - * metadata_matches.csv: File identifiers for entries in metadata CSV file with a corresponding file in the content directory. + * metadata_matches.csv: File identifiers for entries in metadata CSV file with a + corresponding file in the content directory. - * no_files.csv: File identifiers for entries in metadata CSV file without a corresponding file in the content directory. + * no_files.csv: File identifiers for entries in metadata CSV file without a + corresponding file in the content directory. - * no_metadata.csv: File identifiers for files in the content directory without a corresponding entry in the metadata CSV file. + * no_metadata.csv: File identifiers for files in the content directory without a + corresponding entry in the metadata CSV file. - * updated-.csv: Entries from the metadata CSV file with a corresponding file in the content directory. + * updated-.csv: Entries from the metadata CSV file with a + corresponding file in the content directory. """ + source_settings = ctx.obj["config"]["settings"] client = ctx.obj["client"] - file_ids = helpers.create_file_list(content_directory, client.s3_client, file_type) + files_dict = helpers.get_files_from_s3( + s3_path=content_directory, + s3_client=client.s3_client, + bitstream_folders=source_settings.get("bitstream_folders"), + id_regex=source_settings["id_regex"], + ) metadata_ids = helpers.create_metadata_id_list(metadata_csv) - metadata_matches = helpers.match_metadata_to_files(file_ids, metadata_ids) - file_matches = helpers.match_files_to_metadata(file_ids, metadata_ids) + metadata_matches = helpers.match_metadata_to_files(files_dict.keys(), metadata_ids) + file_matches = helpers.match_files_to_metadata(files_dict.keys(), metadata_ids) no_files = set(metadata_ids) - set(metadata_matches) - no_metadata = set(file_ids) - set(file_matches) + no_metadata = set(files_dict.keys()) - set(file_matches) helpers.create_csv_from_list(no_metadata, f"{output_directory}no_metadata") helpers.create_csv_from_list(no_files, f"{output_directory}no_files") helpers.create_csv_from_list(metadata_matches, f"{output_directory}metadata_matches") - helpers.update_metadata_csv(metadata_csv, output_directory, metadata_matches) + helpers.update_metadata_csv( + metadata_csv, output_directory, metadata_matches, files_dict + ) diff --git a/dsaps/helpers.py b/dsaps/helpers.py index d8eeb86..cb5f21b 100644 --- a/dsaps/helpers.py +++ b/dsaps/helpers.py @@ -1,5 +1,19 @@ import csv import os +import re +import yaml + +from typing import Literal + +import smart_open + +S3_BUCKET_REGEX = re.compile(r"^([^\/]*)") +S3_PREFIX_REGEX = re.compile(r"(?<=\/)(.*)") + + +def load_source_config(config_file: str): + with smart_open.open(config_file, "r") as file: + return yaml.safe_load(file) def create_csv_from_list(list_name, output): @@ -11,21 +25,58 @@ def create_csv_from_list(list_name, output): writer.writerow([item]) -def create_file_list(file_path, s3_client, file_type): - """Create a list of file names.""" - files = [] +def get_files_from_s3( + s3_path: str, + s3_client, + id_regex: str, + bitstream_folders=None, +) -> dict: + """Get a list of files on S3 per unique item identifier. + + Args: + s3_path (str): S3 path to the topmost level directory in which to look for files. + Formatted as "s3://bucket/prefix/" or "bucket/prefix/". + s3_client (_type_): S3 client. + id_regex (str): A regex expression for extracting the item identifier + from the S3 file object key. + bitstream_folders (list[str], optional): A list of subfolders in which to look for bitstreams. + If any of the subfolders are found in the S3 file object key for the bitstream, the file + object key is included in the returned output. Defaults to None. + + Returns: + dict: A dictionary where keys denote unique item identifiers and values are a list of + S3 file object keys associated with the item identifier. + """ + files = {} + s3_path = s3_path.removeprefix("s3://") + operation_parameters = {"Bucket": parse_value_from_text(s3_path, S3_BUCKET_REGEX)} + if prefix := parse_value_from_text(s3_path, S3_PREFIX_REGEX): + operation_parameters.update({"Prefix": prefix}) + paginator = s3_client.get_paginator("list_objects_v2") - for page in paginator.paginate(Bucket=file_path.removeprefix("s3://")): - files.extend( - [ - file["Key"] - for file in page["Contents"] - if file["Key"].endswith(f".{file_type}") - ] - ) + for page in paginator.paginate(**operation_parameters): + for file in page["Contents"]: + file_path = file["Key"] + file_name = file_path.split("/")[-1] + if bitstream_folders: + # if the object is not stored in any of the folders specified + # exclude object + if not [folder for folder in bitstream_folders if folder in file_path]: + continue + item_identifier = parse_value_from_text(file_name, id_regex) + files.setdefault(item_identifier, []).append(file["Key"]) return files +def parse_value_from_text( + text: str, + regex: str, +): + pattern = re.compile(regex) + if match := pattern.search(text): + return match.group(1) + + def create_ingest_report(items, file_name): """Create ingest report that matches external systems' identifiers with newly created DSpace handles.""" @@ -56,7 +107,7 @@ def match_files_to_metadata(file_list, metadata_ids): file_id for metadata_id in metadata_ids for file_id in file_list - if file_id.startswith(metadata_id) + if file_id == metadata_id ] return file_matches @@ -65,21 +116,23 @@ def match_metadata_to_files(file_list, metadata_ids): """Create list of metadata records matched to files.""" metadata_matches = [ metadata_id - for f in file_list + for file_id in file_list for metadata_id in metadata_ids - if f.startswith(metadata_id) + if file_id == metadata_id ] return metadata_matches -def update_metadata_csv(metadata_csv, output_directory, metadata_matches): +def update_metadata_csv(metadata_csv, output_directory, metadata_matches, files_dict): """Create an updated CSV of only metadata records that have matching files.""" with open(metadata_csv) as csvfile: reader = csv.DictReader(csvfile) upd_md_file_name = f"updated-{os.path.basename(metadata_csv)}" with open(f"{output_directory}{upd_md_file_name}", "w") as updated_csv: - writer = csv.DictWriter(updated_csv, fieldnames=reader.fieldnames) + fieldnames = [*reader.fieldnames, "bitstreams"] + writer = csv.DictWriter(updated_csv, fieldnames=fieldnames) writer.writeheader() for row in reader: if row["file_identifier"] in metadata_matches: + row["bitstreams"] = files_dict[row["file_identifier"]] writer.writerow(row) diff --git a/dsaps/models.py b/dsaps/models.py index 0bb7989..7d3962c 100644 --- a/dsaps/models.py +++ b/dsaps/models.py @@ -7,8 +7,6 @@ import smart_open import structlog -from dsaps.helpers import create_file_list - Field = partial(attr.ib, default=None) Group = partial(attr.ib, default=[]) @@ -224,13 +222,7 @@ class Item(BaseRecord): def bitstreams_in_directory(self, directory, s3_client, file_type=None): """Create a list of bitstreams from the specified directory and sort the list.""" - files = create_file_list(directory, s3_client, file_type) - self.bitstreams = [ - Bitstream(name=file, file_path=f"{directory}/{file}") - for file in files - if file.startswith(self.file_identifier) and file.endswith(file_type) - ] - self.bitstreams.sort(key=lambda x: x.name) + pass @classmethod def metadata_from_csv_row(cls, row, field_map): @@ -243,9 +235,8 @@ def metadata_from_csv_row(cls, row, field_map): file_identifier = field continue # file_identifier is not included in DSpace metadata if f == "source_system_identifier": - source_system_identifier = field + # source_system_identifier = field continue # source_system_identifier is not included in DSpace - metadata delimiter = field_map[f]["delimiter"] language = field_map[f]["language"] if delimiter: @@ -260,7 +251,7 @@ def metadata_from_csv_row(cls, row, field_map): return cls( metadata=metadata, file_identifier=file_identifier, - source_system_identifier=source_system_identifier, + # source_system_identifier=source_system_identifier, ) diff --git a/tests/conftest.py b/tests/conftest.py index e441006..06331b2 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -19,6 +19,44 @@ def _test_environment(monkeypatch): monkeypatch.setenv("AWS_SESSION_TOKEN", "testing") +@pytest.fixture +def mocked_s3_bucket(): + bucket_name = "mocked-bucket" + with mock_aws(): + s3 = boto3.client("s3") + s3.create_bucket(Bucket=bucket_name) + s3.put_object(Body="", Bucket=bucket_name, Key="one-to-one/aaaa_001_01.pdf") + s3.put_object( + Body="", + Bucket=bucket_name, + Key="one-to-one/aaaa_002_01.pdf", + ) + s3.put_object( + Body="", + Bucket=bucket_name, + Key="many-to-one/bbbb_003_01.pdf", + ) + s3.put_object( + Body="", + Bucket=bucket_name, + Key="many-to-one/bbbb_003_02.pdf", + ) + s3.put_object( + Body="", + Bucket=bucket_name, + Key="many-to-one/bbbb_004_01.pdf", + ) + s3.put_object( + Body="", + Bucket=bucket_name, + Key="many-to-one/bbbb_003_01.jpg", + ) + s3.put_object( + Body="", Bucket=bucket_name, Key="nested/prefix/objects/include_005_01.pdf" + ) + yield + + @pytest.fixture() def mocked_s3(): with mock_aws(): diff --git a/tests/fixtures/source_config.json b/tests/fixtures/source_config.json new file mode 100644 index 0000000..8b1bc59 --- /dev/null +++ b/tests/fixtures/source_config.json @@ -0,0 +1,45 @@ +{ + "settings": { + "bitstream_folders": [ + "objects" + ], + "id_regex": ".*-(.*?-.*)\\..*$" + }, + "mapping": { + "file_identifier": { + "csv_field_name": "file_identifier", + "language": null, + "delimiter": "" + }, + "dc.title": { + "csv_field_name": "title", + "language": "en_US", + "delimiter": "" + }, + "source_system_identifier": { + "csv_field_name": "uri", + "language": null, + "delimiter": "" + }, + "dc.contributor.author": { + "csv_field_name": "author", + "language": null, + "delimiter": "|" + }, + "dc.description": { + "csv_field_name": "description", + "language": "en_US", + "delimiter": "" + }, + "dc.rights": { + "csv_field_name": "rights_statement", + "language": "en_US", + "delimiter": "" + }, + "dc.rights.uri": { + "csv_field_name": "rights_uri", + "language": null, + "delimiter": "" + } + } +} \ No newline at end of file diff --git a/tests/test_cli.py b/tests/test_cli.py index d6d0e18..c0ea336 100644 --- a/tests/test_cli.py +++ b/tests/test_cli.py @@ -9,6 +9,8 @@ def test_additems(runner, mocked_s3): result = runner.invoke( main, [ + "--config-file", + "tests/fixtures/source_config.json", "--url", "mock://example.com/", "--email", @@ -32,6 +34,8 @@ def test_additems(runner, mocked_s3): result = runner.invoke( main, [ + "--config-file", + "tests/fixtures/source_config.json", "--url", "mock://example.com/", "--email", @@ -62,6 +66,8 @@ def test_newcollection(runner): result = runner.invoke( main, [ + "--config-file", + "tests/fixtures/source_config.json", "--url", "mock://example.com/", "--email", @@ -84,6 +90,8 @@ def test_reconcile(runner, mocked_s3, output_dir): result = runner.invoke( main, [ + "--config-file", + "tests/fixtures/source_config.json", "--url", "mock://example.com/", "--email", @@ -97,8 +105,6 @@ def test_reconcile(runner, mocked_s3, output_dir): output_dir, "--content-directory", "s3://test-bucket", - "--file-type", - "pdf", ], ) assert result.exit_code == 0 diff --git a/tests/test_helpers.py b/tests/test_helpers.py index 211b919..fb38680 100644 --- a/tests/test_helpers.py +++ b/tests/test_helpers.py @@ -1,31 +1,128 @@ import csv -from dsaps import helpers +from dsaps.helpers import ( + create_csv_from_list, + create_ingest_report, + create_metadata_id_list, + get_files_from_s3, + load_source_config, + match_files_to_metadata, + match_metadata_to_files, + parse_value_from_text, + update_metadata_csv, +) from dsaps.models import Item +REGEX_ID_BETWEEN_UNDERSCORES = "_(.*)_" +REGEX_ID_BEFORE_UNDERSCORES = "(.*?)_" +REGEX_ID_DDC = ".*-(.*?-.[^_\\.]*)" + + +def test_load_source_config(): + assert load_source_config("tests/fixtures/source_config.json")["settings"] == { + "bitstream_folders": ["objects"], + "id_regex": ".*-(.*?-.*)\\..*$", + } + def test_create_csv_from_list(output_dir): """Test create_csv_from_list function.""" list_name = ["123"] - helpers.create_csv_from_list(list_name, f"{output_dir}output") + create_csv_from_list(list_name, f"{output_dir}output") with open(f"{output_dir}output.csv") as csvfile: reader = csv.DictReader(csvfile) for row in reader: assert row["id"] == "123" -def test_create_file_list(mocked_s3, s3_client): - """Test create_file_list function.""" - file_list = helpers.create_file_list("s3://test-bucket", s3_client, "pdf") - for file_id in ["test_02.pdf", "test_01.pdf", "best_01.pdf"]: - assert file_id in file_list +def test_parse_identifier_from_file_name_regex(): + assert ( + parse_value_from_text(text="abc123_01.pdf", regex=REGEX_ID_BEFORE_UNDERSCORES) + == "abc123" + ) + + +def test_parse_identifier_from_file_name_ddc_regex(): + assert ( + parse_value_from_text(text="aaaa-bbbb-cccc-02-001.pdf", regex=REGEX_ID_DDC) + == "02-001" + ) + assert ( + parse_value_from_text(text="aaaa-bbbb-cccc-02-001_001.pdf", regex=REGEX_ID_DDC) + == "02-001" + ) + + +def test_get_files_from_s3_one_file_per_file_id(mocked_s3_bucket, s3_client): + files = get_files_from_s3( + s3_path="s3://mocked-bucket/one-to-one/", + s3_client=s3_client, + id_regex=REGEX_ID_BETWEEN_UNDERSCORES, + ) + assert files == { + "001": [ + "one-to-one/aaaa_001_01.pdf", + ], + "002": [ + "one-to-one/aaaa_002_01.pdf", + ], + } + + +def test_get_files_from_s3_many_files_per_file_id(mocked_s3_bucket, s3_client): + files = get_files_from_s3( + s3_path="s3://mocked-bucket/many-to-one/", + s3_client=s3_client, + id_regex=REGEX_ID_BETWEEN_UNDERSCORES, + ) + assert files == { + "003": [ + "many-to-one/bbbb_003_01.jpg", + "many-to-one/bbbb_003_01.pdf", + "many-to-one/bbbb_003_02.pdf", + ], + "004": ["many-to-one/bbbb_004_01.pdf"], + } + + +def test_get_files_from_s3_with_bitstream_folders(mocked_s3_bucket, s3_client): + files = get_files_from_s3( + s3_path="s3://mocked-bucket", + s3_client=s3_client, + bitstream_folders=["objects"], + id_regex=REGEX_ID_BETWEEN_UNDERSCORES, + ) + assert files == {"005": ["nested/prefix/objects/include_005_01.pdf"]} + + +def test_get_files_from_s3_without_bitstream_folders(mocked_s3_bucket, s3_client): + files = get_files_from_s3( + s3_path="s3://mocked-bucket", + s3_client=s3_client, + id_regex=REGEX_ID_BETWEEN_UNDERSCORES, + ) + assert files == { + "001": [ + "one-to-one/aaaa_001_01.pdf", + ], + "002": [ + "one-to-one/aaaa_002_01.pdf", + ], + "003": [ + "many-to-one/bbbb_003_01.jpg", + "many-to-one/bbbb_003_01.pdf", + "many-to-one/bbbb_003_02.pdf", + ], + "004": ["many-to-one/bbbb_004_01.pdf"], + "005": ["nested/prefix/objects/include_005_01.pdf"], + } def test_create_ingest_report(runner, output_dir): """Test create_ingest_report function.""" file_name = "ingest_report.csv" items = [Item(source_system_identifier="/repo/0/ao/123", handle="111.1111")] - helpers.create_ingest_report(items, f"{output_dir}{file_name}") + create_ingest_report(items, f"{output_dir}{file_name}") with open(f"{output_dir}{file_name}") as csvfile: reader = csv.DictReader(csvfile) for row in reader: @@ -36,25 +133,25 @@ def test_create_ingest_report(runner, output_dir): def test_create_metadata_id_list(): """Test create_metadata_id_list function.""" metadata_path = "tests/fixtures/aspace_metadata_delimited.csv" - metadata_ids = helpers.create_metadata_id_list(metadata_path) + metadata_ids = create_metadata_id_list(metadata_path) assert "test" in metadata_ids assert "tast" in metadata_ids def test_match_files_to_metadata(): """Test match_files_to_metadata function.""" - file_list = ["test_01.pdf"] + files_dict = {"test": "test_01.pdf"} metadata_ids = ["test", "tast"] - file_matches = helpers.match_files_to_metadata(file_list, metadata_ids) + file_matches = match_files_to_metadata(files_dict.keys(), metadata_ids) assert len(file_matches) == 1 - assert "test_01.pdf" in file_matches + assert "test" in file_matches def test_match_metadata_to_files(): """Test match_metadata_to_files function.""" - file_list = ["test_01.pdf", "tast_01.pdf"] + file_list = {"test": "test_01.pdf", "tast": "tast_01.pdf"} metadata_ids = ["test"] - file_matches = helpers.match_metadata_to_files(file_list, metadata_ids) + file_matches = match_metadata_to_files(file_list, metadata_ids) assert len(file_matches) == 1 assert "test" in file_matches @@ -62,8 +159,11 @@ def test_match_metadata_to_files(): def test_update_metadata_csv(output_dir): """Test update_metadata_csv function.""" metadata_matches = ["test"] - helpers.update_metadata_csv( - "tests/fixtures/aspace_metadata_delimited.csv", output_dir, metadata_matches + update_metadata_csv( + "tests/fixtures/aspace_metadata_delimited.csv", + output_dir, + metadata_matches, + {"test": ["/test/test_01.pdf"]}, ) with open(f"{output_dir}updated-aspace_metadata_delimited.csv") as csvfile: reader = csv.DictReader(csvfile) @@ -71,3 +171,4 @@ def test_update_metadata_csv(output_dir): assert row["uri"] == "/repo/0/ao/123" assert row["title"] == "Test Item" assert row["file_identifier"] == "test" + assert row["bitstreams"] == "['/test/test_01.pdf']" diff --git a/tests/test_models.py b/tests/test_models.py index 9c65ddc..6854570 100644 --- a/tests/test_models.py +++ b/tests/test_models.py @@ -117,15 +117,6 @@ def test_collection_post_items( assert item.uuid == "e5f6" -@mock_aws -def test_item_bitstreams_in_directory(mocked_s3, s3_client): - item = models.Item(file_identifier="test") - item.bitstreams_in_directory("s3://test-bucket", s3_client, "pdf") - assert 2 == len(item.bitstreams) - assert item.bitstreams[0].name == "test_01.pdf" - assert item.bitstreams[1].name == "test_02.pdf" - - def test_item_metadata_from_csv_row(aspace_delimited_csv, aspace_mapping): row = next(aspace_delimited_csv) item = models.Item.metadata_from_csv_row(row, aspace_mapping) From 72b6e2dd170b32e72f05d80b898a946d7bfab4a3 Mon Sep 17 00:00:00 2001 From: jonavellecuerdo Date: Thu, 21 Mar 2024 15:04:23 -0400 Subject: [PATCH 11/18] Rename module 'models.py' as 'dspace.py' --- dsaps/cli.py | 2 +- dsaps/{models.py => dspace.py} | 0 tests/conftest.py | 4 ++-- tests/test_helpers.py | 2 +- tests/test_models.py | 22 +++++++++++----------- 5 files changed, 15 insertions(+), 15 deletions(-) rename dsaps/{models.py => dspace.py} (100%) diff --git a/dsaps/cli.py b/dsaps/cli.py index 17ef937..16581e3 100644 --- a/dsaps/cli.py +++ b/dsaps/cli.py @@ -9,7 +9,7 @@ import structlog from dsaps import helpers -from dsaps.models import Client, Collection +from dsaps.dspace import Client, Collection logger = structlog.get_logger() diff --git a/dsaps/models.py b/dsaps/dspace.py similarity index 100% rename from dsaps/models.py rename to dsaps/dspace.py diff --git a/tests/conftest.py b/tests/conftest.py index 06331b2..4502ea4 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -7,7 +7,7 @@ from click.testing import CliRunner from moto import mock_aws -from dsaps import models +from dsaps import dspace # Env fixtures @@ -92,7 +92,7 @@ def s3_client(): @pytest.fixture() def client(): - client = models.Client("mock://example.com/") + client = dspace.Client("mock://example.com/") client.header = {} client.cookies = {} client.user_full_name = "" diff --git a/tests/test_helpers.py b/tests/test_helpers.py index fb38680..389fac3 100644 --- a/tests/test_helpers.py +++ b/tests/test_helpers.py @@ -11,7 +11,7 @@ parse_value_from_text, update_metadata_csv, ) -from dsaps.models import Item +from dsaps.dspace import Item REGEX_ID_BETWEEN_UNDERSCORES = "_(.*)_" REGEX_ID_BEFORE_UNDERSCORES = "(.*?)_" diff --git a/tests/test_models.py b/tests/test_models.py index 6854570..a142513 100644 --- a/tests/test_models.py +++ b/tests/test_models.py @@ -1,7 +1,7 @@ import attr from moto import mock_aws -from dsaps import models +from dsaps import dspace def test_authenticate(client): @@ -39,7 +39,7 @@ def test_get_record(client): def test_post_bitstream(client, mocked_s3): """Test post_bitstream method.""" item_uuid = "e5f6" - bitstream = models.Bitstream( + bitstream = dspace.Bitstream( name="test_01.pdf", file_path="s3://test-bucket/test_01.pdf" ) bit_uuid = client.post_bitstream(item_uuid, bitstream) @@ -57,16 +57,16 @@ def test_post_coll_to_comm(client): @mock_aws def test_post_item_to_collection(client, mocked_s3): """Test post_item_to_collection method.""" - item = models.Item() + item = dspace.Item() item.bitstreams = [ - models.Bitstream(name="test_01.pdf", file_path="s3://test-bucket/test_01.pdf") + dspace.Bitstream(name="test_01.pdf", file_path="s3://test-bucket/test_01.pdf") ] item.metadata = [ - models.MetadataEntry(key="file_identifier", value="test"), - models.MetadataEntry( + dspace.MetadataEntry(key="file_identifier", value="test"), + dspace.MetadataEntry( key="dc.title", value="Monitoring Works: Getting Teachers", language="en_US" ), - models.MetadataEntry(key="dc.relation.isversionof", value="repo/0/ao/123"), + dspace.MetadataEntry(key="dc.relation.isversionof", value="repo/0/ao/123"), ] coll_uuid = "c3d4" item_uuid, item_handle = client.post_item_to_collection(coll_uuid, item) @@ -76,7 +76,7 @@ def test_post_item_to_collection(client, mocked_s3): def test__populate_class_instance(client): """Test _populate_class_instance method.""" - class_type = models.Collection + class_type = dspace.Collection rec_obj = {"name": "Test title", "type": "collection", "items": []} rec_obj = client._populate_class_instance(class_type, rec_obj) assert type(rec_obj) is class_type @@ -94,7 +94,7 @@ def test__build_uuid_list(client): def test_collection_create_metadata_for_items_from_csv( aspace_delimited_csv, aspace_mapping ): - collection = models.Collection.create_metadata_for_items_from_csv( + collection = dspace.Collection.create_metadata_for_items_from_csv( aspace_delimited_csv, aspace_mapping ) assert 2 == len(collection.items) @@ -107,7 +107,7 @@ def test_collection_post_items( aspace_delimited_csv, aspace_mapping, ): - collection = models.Collection.create_metadata_for_items_from_csv( + collection = dspace.Collection.create_metadata_for_items_from_csv( aspace_delimited_csv, aspace_mapping ) collection.uuid = "c3d4" @@ -119,7 +119,7 @@ def test_collection_post_items( def test_item_metadata_from_csv_row(aspace_delimited_csv, aspace_mapping): row = next(aspace_delimited_csv) - item = models.Item.metadata_from_csv_row(row, aspace_mapping) + item = dspace.Item.metadata_from_csv_row(row, aspace_mapping) assert attr.asdict(item)["metadata"] == [ {"key": "dc.title", "value": "Tast Item", "language": "en_US"}, {"key": "dc.contributor.author", "value": "Smith, John", "language": None}, From 816e276f0119f17aec8b4902164d1a255131ebff Mon Sep 17 00:00:00 2001 From: jonavellecuerdo Date: Thu, 21 Mar 2024 15:15:25 -0400 Subject: [PATCH 12/18] Create module for S3 client --- dsaps/cli.py | 10 ++++++++-- dsaps/dspace.py | 1 - dsaps/s3.py | 7 +++++++ 3 files changed, 15 insertions(+), 3 deletions(-) create mode 100644 dsaps/s3.py diff --git a/dsaps/cli.py b/dsaps/cli.py index 16581e3..fc10f39 100644 --- a/dsaps/cli.py +++ b/dsaps/cli.py @@ -9,8 +9,10 @@ import structlog from dsaps import helpers +from dsaps.s3 import S3Client from dsaps.dspace import Client, Collection + logger = structlog.get_logger() @@ -78,10 +80,12 @@ def main(ctx, config_file, url, email, password): ) logger.info("Application start") client = Client(url) + s3_client = S3Client.get_client() client.authenticate(email, password) start_time = time.time() ctx.obj["config"] = helpers.load_source_config(config_file) ctx.obj["client"] = client + ctx.obj["s3_client"] = s3_client ctx.obj["start_time"] = start_time ctx.obj["log_suffix"] = log_suffix @@ -141,6 +145,7 @@ def additems( metadata to a DSpace schema, and a directory containing the files to be uploaded. """ client = ctx.obj["client"] + s3_client = ctx.obj["s3_client"] start_time = ctx.obj["start_time"] if "collection_uuid" not in ctx.obj and collection_handle is None: raise click.UsageError( @@ -157,7 +162,7 @@ def additems( mapping = json.load(jsonfile) collection = Collection.create_metadata_for_items_from_csv(metadata, mapping) for item in collection.items: - item.bitstreams_in_directory(content_directory, client.s3_client, file_type) + item.bitstreams_in_directory(content_directory, s3_client, file_type) collection.uuid = collection_uuid for item in collection.post_items(client): logger.info(item.file_identifier) @@ -227,9 +232,10 @@ def reconcile(ctx, metadata_csv, output_directory, content_directory): """ source_settings = ctx.obj["config"]["settings"] client = ctx.obj["client"] + s3_client = ctx.obj["s3_client"] files_dict = helpers.get_files_from_s3( s3_path=content_directory, - s3_client=client.s3_client, + s3_client=s3_client, bitstream_folders=source_settings.get("bitstream_folders"), id_regex=source_settings["id_regex"], ) diff --git a/dsaps/dspace.py b/dsaps/dspace.py index 7d3962c..1ef47c1 100644 --- a/dsaps/dspace.py +++ b/dsaps/dspace.py @@ -20,7 +20,6 @@ def __init__(self, url): self.url = url.rstrip("/") self.cookies = None self.header = header - self.s3_client = boto3.client("s3") logger.info("Initializing client") def authenticate(self, email, password): diff --git a/dsaps/s3.py b/dsaps/s3.py new file mode 100644 index 0000000..5b1bc63 --- /dev/null +++ b/dsaps/s3.py @@ -0,0 +1,7 @@ +import boto3 + + +class S3Client: + @classmethod + def get_client(cls): + return boto3.client("s3") From 63b3a7b1d890bc402ffeac02b2ace287ecb60391 Mon Sep 17 00:00:00 2001 From: jonavellecuerdo Date: Thu, 21 Mar 2024 15:28:36 -0400 Subject: [PATCH 13/18] Create module for DSpace client and models Why these changes are being introduced: * Improve code coherency and repo structure. This was previously 'models.py', but it has been updated to reflect the DSpace-centric classes defined in this module. How this addresses that need: * Rename classes to describe DSpace objects Side effects of this change: * None Relevant ticket(s): * TBD --- dsaps/cli.py | 21 +++++------ dsaps/dspace.py | 82 +++++++++++++++++++++---------------------- dsaps/helpers.py | 2 -- tests/conftest.py | 12 +++---- tests/test_helpers.py | 4 +-- tests/test_models.py | 68 +++++++++++++++++------------------ 6 files changed, 93 insertions(+), 96 deletions(-) diff --git a/dsaps/cli.py b/dsaps/cli.py index fc10f39..f20f5e7 100644 --- a/dsaps/cli.py +++ b/dsaps/cli.py @@ -10,7 +10,7 @@ from dsaps import helpers from dsaps.s3 import S3Client -from dsaps.dspace import Client, Collection +from dsaps.dspace import DSpaceClient, DSpaceCollection logger = structlog.get_logger() @@ -79,12 +79,12 @@ def main(ctx, config_file, url, email, password): level=logging.INFO, ) logger.info("Application start") - client = Client(url) + client = DSpaceClient(url) s3_client = S3Client.get_client() client.authenticate(email, password) start_time = time.time() ctx.obj["config"] = helpers.load_source_config(config_file) - ctx.obj["client"] = client + ctx.obj["dspace_client"] = client ctx.obj["s3_client"] = s3_client ctx.obj["start_time"] = start_time ctx.obj["log_suffix"] = log_suffix @@ -144,7 +144,7 @@ def additems( The method relies on a CSV file with metadata for uploads, a JSON document that maps metadata to a DSpace schema, and a directory containing the files to be uploaded. """ - client = ctx.obj["client"] + dspace_client = ctx.obj["dspace_client"] s3_client = ctx.obj["s3_client"] start_time = ctx.obj["start_time"] if "collection_uuid" not in ctx.obj and collection_handle is None: @@ -156,15 +156,17 @@ def additems( elif "collection_uuid" in ctx.obj: collection_uuid = ctx.obj["collection_uuid"] else: - collection_uuid = client.get_uuid_from_handle(collection_handle) + collection_uuid = dspace_client.get_uuid_from_handle(collection_handle) with open(metadata_csv, "r") as csvfile, open(field_map, "r") as jsonfile: metadata = csv.DictReader(csvfile) mapping = json.load(jsonfile) - collection = Collection.create_metadata_for_items_from_csv(metadata, mapping) + collection = DSpaceCollection.create_metadata_for_items_from_csv( + metadata, mapping + ) for item in collection.items: item.bitstreams_in_directory(content_directory, s3_client, file_type) collection.uuid = collection_uuid - for item in collection.post_items(client): + for item in collection.post_items(dspace_client): logger.info(item.file_identifier) elapsed_time = datetime.timedelta(seconds=time.time() - start_time) logger.info(f"Total runtime : {elapsed_time}") @@ -186,8 +188,8 @@ def additems( @click.pass_context def newcollection(ctx, community_handle, collection_name): """Create a new DSpace collection within a community.""" - client = ctx.obj["client"] - collection_uuid = client.post_coll_to_comm(community_handle, collection_name) + dspace_client = ctx.obj["dspace_client"] + collection_uuid = dspace_client.post_coll_to_comm(community_handle, collection_name) ctx.obj["collection_uuid"] = collection_uuid @@ -231,7 +233,6 @@ def reconcile(ctx, metadata_csv, output_directory, content_directory): corresponding file in the content directory. """ source_settings = ctx.obj["config"]["settings"] - client = ctx.obj["client"] s3_client = ctx.obj["s3_client"] files_dict = helpers.get_files_from_s3( s3_path=content_directory, diff --git a/dsaps/dspace.py b/dsaps/dspace.py index 1ef47c1..1d16e6d 100644 --- a/dsaps/dspace.py +++ b/dsaps/dspace.py @@ -2,19 +2,19 @@ from functools import partial import attr -import boto3 import requests import smart_open import structlog -Field = partial(attr.ib, default=None) +from attrs import field, define + Group = partial(attr.ib, default=[]) logger = structlog.get_logger() op = operator.attrgetter("name") -class Client: +class DSpaceClient: def __init__(self, url): header = {"content-type": "application/json", "accept": "application/json"} self.url = url.rstrip("/") @@ -84,11 +84,11 @@ def get_record(self, uuid, record_type): url, headers=self.header, cookies=self.cookies, timeout=30 ).json() if record_type == "items": - rec_obj = self._populate_class_instance(Item, record) + rec_obj = self._populate_class_instance(DSpaceItem, record) elif record_type == "communities": - rec_obj = self._populate_class_instance(Community, record) + rec_obj = self._populate_class_instance(DSpaceCommunity, record) elif record_type == "collections": - rec_obj = self._populate_class_instance(Collection, record) + rec_obj = self._populate_class_instance(DSpaceCollection, record) else: logger.info("Invalid record type.") exit() @@ -155,10 +155,10 @@ def _populate_class_instance(self, class_type, rec_obj): fields = [op(field) for field in attr.fields(class_type)] kwargs = {k: v for k, v in rec_obj.items() if k in fields} kwargs["objtype"] = rec_obj["type"] - if class_type == Community: + if class_type == DSpaceCommunity: collections = self._build_uuid_list(rec_obj, kwargs, "collections") rec_obj["collections"] = collections - elif class_type == Collection: + elif class_type == DSpaceCollection: items = self._build_uuid_list(rec_obj, "items") rec_obj["items"] = items rec_obj = class_type(**kwargs) @@ -172,18 +172,31 @@ def _build_uuid_list(self, rec_obj, children): return child_list -@attr.s -class BaseRecord: - uuid = Field() - name = Field() - handle = Field() - link = Field() - objtype = Field() +@define +class Bitstream: + name = field(default=None) + file_path = field(default=None) + + +@define +class MetadataEntry: + key = field(default=None) + value = field(default=None) + language = field(default=None) + + +@define +class DSpaceObject: + uuid = field(default=None) + name = field(default=None) + handle = field(default=None) + link = field(default=None) + objtype = field(default=None) -@attr.s -class Collection(BaseRecord): - items = Group() +@define +class DSpaceCollection(DSpaceObject): + items = field(factory=list) def post_items(self, client): """Post items to collection.""" @@ -203,21 +216,21 @@ def post_items(self, client): def create_metadata_for_items_from_csv(cls, csv_reader, field_map): """Create metadata for the collection's items based on a CSV and a JSON mapping field map.""" - items = [Item.metadata_from_csv_row(row, field_map) for row in csv_reader] + items = [DSpaceItem.metadata_from_csv_row(row, field_map) for row in csv_reader] return cls(items=items) -@attr.s -class Community(BaseRecord): - collections = Field() +@define +class DSpaceCommunity(DSpaceObject): + collections = field(default=None) -@attr.s -class Item(BaseRecord): - metadata = Group() - bitstreams = Group() - file_identifier = Field() - source_system_identifier = Field() +@define +class DSpaceItem(DSpaceObject): + metadata = field(factory=list) + bitstreams = field(factory=list) + file_identifier = field(default=None) + source_system_identifier = field(default=None) def bitstreams_in_directory(self, directory, s3_client, file_type=None): """Create a list of bitstreams from the specified directory and sort the list.""" @@ -252,16 +265,3 @@ def metadata_from_csv_row(cls, row, field_map): file_identifier=file_identifier, # source_system_identifier=source_system_identifier, ) - - -@attr.s -class Bitstream: - name = Field() - file_path = Field() - - -@attr.s -class MetadataEntry: - key = Field() - value = Field() - language = Field() diff --git a/dsaps/helpers.py b/dsaps/helpers.py index cb5f21b..fb0778e 100644 --- a/dsaps/helpers.py +++ b/dsaps/helpers.py @@ -3,8 +3,6 @@ import re import yaml -from typing import Literal - import smart_open S3_BUCKET_REGEX = re.compile(r"^([^\/]*)") diff --git a/tests/conftest.py b/tests/conftest.py index 4502ea4..0add046 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -91,12 +91,12 @@ def s3_client(): @pytest.fixture() -def client(): - client = dspace.Client("mock://example.com/") - client.header = {} - client.cookies = {} - client.user_full_name = "" - return client +def dspace_client(): + dspace_client = dspace.DSpaceClient("mock://example.com/") + dspace_client.header = {} + dspace_client.cookies = {} + dspace_client.user_full_name = "" + return dspace_client @pytest.fixture() diff --git a/tests/test_helpers.py b/tests/test_helpers.py index 389fac3..92843f2 100644 --- a/tests/test_helpers.py +++ b/tests/test_helpers.py @@ -11,7 +11,7 @@ parse_value_from_text, update_metadata_csv, ) -from dsaps.dspace import Item +from dsaps.dspace import DSpaceItem REGEX_ID_BETWEEN_UNDERSCORES = "_(.*)_" REGEX_ID_BEFORE_UNDERSCORES = "(.*?)_" @@ -121,7 +121,7 @@ def test_get_files_from_s3_without_bitstream_folders(mocked_s3_bucket, s3_client def test_create_ingest_report(runner, output_dir): """Test create_ingest_report function.""" file_name = "ingest_report.csv" - items = [Item(source_system_identifier="/repo/0/ao/123", handle="111.1111")] + items = [DSpaceItem(source_system_identifier="/repo/0/ao/123", handle="111.1111")] create_ingest_report(items, f"{output_dir}{file_name}") with open(f"{output_dir}{file_name}") as csvfile: reader = csv.DictReader(csvfile) diff --git a/tests/test_models.py b/tests/test_models.py index a142513..02c8d53 100644 --- a/tests/test_models.py +++ b/tests/test_models.py @@ -1,100 +1,98 @@ import attr from moto import mock_aws -from dsaps import dspace +from dsaps.dspace import Bitstream, DSpaceCollection, DSpaceItem, MetadataEntry -def test_authenticate(client): +def test_authenticate(dspace_client): """Test authenticate method.""" email = "test@test.mock" password = "1234" - client.authenticate(email, password) - assert client.user_full_name == "User Name" - assert client.cookies == {"JSESSIONID": "11111111"} + dspace_client.authenticate(email, password) + assert dspace_client.user_full_name == "User Name" + assert dspace_client.cookies == {"JSESSIONID": "11111111"} -def test_filtered_item_search(client): +def test_filtered_item_search(dspace_client): """Test filtered_item_search method.""" key = "dc.title" string = "test" query_type = "contains" - item_links = client.filtered_item_search( + item_links = dspace_client.filtered_item_search( key, string, query_type, selected_collections="" ) assert "1234" in item_links -def test_get_uuid_from_handle(client): +def test_get_uuid_from_handle(dspace_client): """Test get_uuid_from_handle method.""" - id = client.get_uuid_from_handle("111.1111") + id = dspace_client.get_uuid_from_handle("111.1111") assert id == "a1b2" -def test_get_record(client): +def test_get_record(dspace_client): """Test get_record method.""" - rec_obj = client.get_record("123", "items") + rec_obj = dspace_client.get_record("123", "items") assert attr.asdict(rec_obj)["metadata"] == {"title": "Sample title"} -def test_post_bitstream(client, mocked_s3): +def test_post_bitstream(dspace_client, mocked_s3): """Test post_bitstream method.""" item_uuid = "e5f6" - bitstream = dspace.Bitstream( - name="test_01.pdf", file_path="s3://test-bucket/test_01.pdf" - ) - bit_uuid = client.post_bitstream(item_uuid, bitstream) + bitstream = Bitstream(name="test_01.pdf", file_path="s3://test-bucket/test_01.pdf") + bit_uuid = dspace_client.post_bitstream(item_uuid, bitstream) assert bit_uuid == "g7h8" -def test_post_coll_to_comm(client): +def test_post_coll_to_comm(dspace_client): """Test post_coll_to_comm method.""" comm_handle = "111.1111" coll_name = "Test Collection" - coll_uuid = client.post_coll_to_comm(comm_handle, coll_name) + coll_uuid = dspace_client.post_coll_to_comm(comm_handle, coll_name) assert coll_uuid == "c3d4" @mock_aws -def test_post_item_to_collection(client, mocked_s3): +def test_post_item_to_collection(dspace_client, mocked_s3): """Test post_item_to_collection method.""" - item = dspace.Item() + item = DSpaceItem() item.bitstreams = [ - dspace.Bitstream(name="test_01.pdf", file_path="s3://test-bucket/test_01.pdf") + Bitstream(name="test_01.pdf", file_path="s3://test-bucket/test_01.pdf") ] item.metadata = [ - dspace.MetadataEntry(key="file_identifier", value="test"), - dspace.MetadataEntry( + MetadataEntry(key="file_identifier", value="test"), + MetadataEntry( key="dc.title", value="Monitoring Works: Getting Teachers", language="en_US" ), - dspace.MetadataEntry(key="dc.relation.isversionof", value="repo/0/ao/123"), + MetadataEntry(key="dc.relation.isversionof", value="repo/0/ao/123"), ] coll_uuid = "c3d4" - item_uuid, item_handle = client.post_item_to_collection(coll_uuid, item) + item_uuid, item_handle = dspace_client.post_item_to_collection(coll_uuid, item) assert item_uuid == "e5f6" assert item_handle == "222.2222" -def test__populate_class_instance(client): +def test__populate_class_instance(dspace_client): """Test _populate_class_instance method.""" - class_type = dspace.Collection + class_type = DSpaceCollection rec_obj = {"name": "Test title", "type": "collection", "items": []} - rec_obj = client._populate_class_instance(class_type, rec_obj) + rec_obj = dspace_client._populate_class_instance(class_type, rec_obj) assert type(rec_obj) is class_type assert rec_obj.name == "Test title" -def test__build_uuid_list(client): +def test__build_uuid_list(dspace_client): """Test _build_uuid_list method.""" rec_obj = {"items": [{"uuid": "1234"}]} children = "items" - child_list = client._build_uuid_list(rec_obj, children) + child_list = dspace_client._build_uuid_list(rec_obj, children) assert "1234" in child_list def test_collection_create_metadata_for_items_from_csv( aspace_delimited_csv, aspace_mapping ): - collection = dspace.Collection.create_metadata_for_items_from_csv( + collection = DSpaceCollection.create_metadata_for_items_from_csv( aspace_delimited_csv, aspace_mapping ) assert 2 == len(collection.items) @@ -103,15 +101,15 @@ def test_collection_create_metadata_for_items_from_csv( @mock_aws def test_collection_post_items( mocked_s3, - client, + dspace_client, aspace_delimited_csv, aspace_mapping, ): - collection = dspace.Collection.create_metadata_for_items_from_csv( + collection = DSpaceCollection.create_metadata_for_items_from_csv( aspace_delimited_csv, aspace_mapping ) collection.uuid = "c3d4" - items = collection.post_items(client) + items = collection.post_items(dspace_client) for item in items: assert item.handle == "222.2222" assert item.uuid == "e5f6" @@ -119,7 +117,7 @@ def test_collection_post_items( def test_item_metadata_from_csv_row(aspace_delimited_csv, aspace_mapping): row = next(aspace_delimited_csv) - item = dspace.Item.metadata_from_csv_row(row, aspace_mapping) + item = DSpaceItem.metadata_from_csv_row(row, aspace_mapping) assert attr.asdict(item)["metadata"] == [ {"key": "dc.title", "value": "Tast Item", "language": "en_US"}, {"key": "dc.contributor.author", "value": "Smith, John", "language": None}, From 12767a565b6b740206a2b43cd0e90fd333f1ccb2 Mon Sep 17 00:00:00 2001 From: jonavellecuerdo Date: Fri, 22 Mar 2024 08:10:02 -0400 Subject: [PATCH 14/18] Clean up CLI commands * Set DSpace credentials as required=False to avoid throwing errors for 'reconcile' * Distinguish between S3 and DSpace client variables * Improve logging --- dsaps/cli.py | 39 +++++++++++++++++++++------------------ 1 file changed, 21 insertions(+), 18 deletions(-) diff --git a/dsaps/cli.py b/dsaps/cli.py index f20f5e7..5bccc3f 100644 --- a/dsaps/cli.py +++ b/dsaps/cli.py @@ -3,7 +3,9 @@ import json import logging import os -import time + +from datetime import timedelta +from time import perf_counter import click import structlog @@ -31,14 +33,14 @@ def validate_path(ctx, param, value): @click.option( "--url", envvar="DSPACE_URL", - required=True, + required=False, help="The url for the DSpace REST API. Defaults to env var DSPACE_URL if not set.", ) @click.option( "-e", "--email", envvar="DSPACE_EMAIL", - required=True, + required=False, help=( "The email associated with the DSpace user account used for authentication. " "Defaults to env var DSPACE_EMAIL if not set." @@ -48,7 +50,7 @@ def validate_path(ctx, param, value): "-p", "--password", envvar="DSPACE_PASSWORD", - required=True, + required=False, hide_input=True, help=( "The password associated with the DSpace user account used for authentication. " @@ -78,16 +80,15 @@ def main(ctx, config_file, url, email, password): handlers=[logging.FileHandler(f"logs/log-{log_suffix}", "w")], level=logging.INFO, ) - logger.info("Application start") - client = DSpaceClient(url) - s3_client = S3Client.get_client() - client.authenticate(email, password) - start_time = time.time() - ctx.obj["config"] = helpers.load_source_config(config_file) - ctx.obj["dspace_client"] = client - ctx.obj["s3_client"] = s3_client - ctx.obj["start_time"] = start_time - ctx.obj["log_suffix"] = log_suffix + logger.info("Running process") + source_config = helpers.load_source_config(config_file) + if url: + dspace_client = DSpaceClient(url) + dspace_client.authenticate(email, password) + ctx.obj["dspace_client"] = dspace_client + ctx.obj["config"] = source_config + ctx.obj["s3_client"] = S3Client.get_client() + ctx.obj["start_time"] = perf_counter() @main.command() @@ -144,9 +145,9 @@ def additems( The method relies on a CSV file with metadata for uploads, a JSON document that maps metadata to a DSpace schema, and a directory containing the files to be uploaded. """ - dspace_client = ctx.obj["dspace_client"] s3_client = ctx.obj["s3_client"] - start_time = ctx.obj["start_time"] + dspace_client = ctx.obj["dspace_client"] + if "collection_uuid" not in ctx.obj and collection_handle is None: raise click.UsageError( "collection_handle option must be used or " @@ -168,8 +169,10 @@ def additems( collection.uuid = collection_uuid for item in collection.post_items(dspace_client): logger.info(item.file_identifier) - elapsed_time = datetime.timedelta(seconds=time.time() - start_time) - logger.info(f"Total runtime : {elapsed_time}") + logger.info( + "Total elapsed: %s", + str(timedelta(seconds=perf_counter() - ctx.obj["start_time"])), + ) @main.command() From 7eb7492f6738a99fff9fd9451cdfe5b2cd666103 Mon Sep 17 00:00:00 2001 From: jonavellecuerdo Date: Fri, 22 Mar 2024 08:10:24 -0400 Subject: [PATCH 15/18] Update tests * Simplify testing by making 'source_config.json' and 'source_metadata.csv' serve as test data * Add fixture 'mocked_s3_bucket_bitstreams' to show full S3 file path for bitstreams * Add fixture for metadata csv with 'bitstreams' column (test/fixtures/updated-source_metadata.csv) * Update fixture 'web_mock' to clarify mocked requests: * Update test_cli.py to use updated CLI command args * Deprecate 'mocked_s3' fixture --- tests/conftest.py | 212 +++++++++++++-------- tests/fixtures/source_config.json | 30 +-- tests/fixtures/source_metadata.csv | 6 + tests/fixtures/updated-source_metadata.csv | 6 + tests/test_cli.py | 25 +-- tests/test_helpers.py | 65 ++++--- tests/test_models.py | 89 ++++----- 7 files changed, 228 insertions(+), 205 deletions(-) create mode 100644 tests/fixtures/source_metadata.csv create mode 100644 tests/fixtures/updated-source_metadata.csv diff --git a/tests/conftest.py b/tests/conftest.py index 0add046..232d117 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -1,9 +1,12 @@ import csv import json +import yaml import boto3 import pytest import requests_mock +import smart_open + from click.testing import CliRunner from moto import mock_aws @@ -19,6 +22,33 @@ def _test_environment(monkeypatch): monkeypatch.setenv("AWS_SESSION_TOKEN", "testing") +@pytest.fixture +def source_config(): + with smart_open.open("tests/fixtures/source_config.json", "r") as file: + return yaml.safe_load(file) + + +@pytest.fixture +def source_metadata_csv(): + with open("tests/fixtures/source_metadata.csv") as file: + reader = csv.DictReader(file) + yield reader + + +@pytest.fixture() +def dspace_client(): + dspace_client = dspace.DSpaceClient("mock://example.com/") + dspace_client.header = {} + dspace_client.cookies = {} + dspace_client.user_full_name = "" + return dspace_client + + +@pytest.fixture() +def s3_client(): + return boto3.client("s3", region_name="us-east-1") + + @pytest.fixture def mocked_s3_bucket(): bucket_name = "mocked-bucket" @@ -57,60 +87,19 @@ def mocked_s3_bucket(): yield -@pytest.fixture() -def mocked_s3(): - with mock_aws(): - s3_instance = boto3.client("s3", region_name="us-east-1") - s3_instance.create_bucket(Bucket="test-bucket") - s3_instance.put_object( - Body="", - Bucket="test-bucket", - Key="test_01.pdf", - ) - s3_instance.put_object( - Body="", - Bucket="test-bucket", - Key="test_02.pdf", - ) - s3_instance.put_object( - Body="", - Bucket="test-bucket", - Key="best_01.pdf", - ) - s3_instance.put_object( - Body="", - Bucket="test-bucket", - Key="test_01.jpg", - ) - yield s3_instance - - -@pytest.fixture() -def s3_client(): - return boto3.client("s3", region_name="us-east-1") - - -@pytest.fixture() -def dspace_client(): - dspace_client = dspace.DSpaceClient("mock://example.com/") - dspace_client.header = {} - dspace_client.cookies = {} - dspace_client.user_full_name = "" - return dspace_client - - -@pytest.fixture() -def aspace_delimited_csv(): - with open("tests/fixtures/aspace_metadata_delimited.csv") as f: - reader = csv.DictReader(f) - yield reader - - -@pytest.fixture() -def aspace_mapping(): - with open("config/aspace_mapping.json") as f: - mapping = json.load(f) - yield mapping +@pytest.fixture +def mocked_s3_bucket_bitstreams(): + return { + "001": ["s3://mocked-bucket/one-to-one/aaaa_001_01.pdf"], + "002": ["s3://mocked-bucket/one-to-one/aaaa_002_01.pdf"], + "003": [ + "s3://mocked-bucket/many-to-one/bbbb_003_01.jpg", + "s3://mocked-bucket/many-to-one/bbbb_003_01.pdf", + "s3://mocked-bucket/many-to-one/bbbb_003_02.pdf", + ], + "004": ["s3://mocked-bucket/many-to-one/bbbb_004_01.pdf"], + "005": ["s3://mocked-bucket/nested/prefix/objects/include_005_01.pdf"], + } @pytest.fixture() @@ -127,34 +116,93 @@ def runner(): @pytest.fixture(autouse=True) def web_mock(): - with requests_mock.Mocker() as m: + with requests_mock.Mocker() as mocked_request: + # DSpace authentication cookies = {"JSESSIONID": "11111111"} - m.post("mock://example.com/login", cookies=cookies) + mocked_request.post("mock://example.com/login", cookies=cookies) user_json = {"fullname": "User Name"} - m.get("mock://example.com/status", json=user_json) - rec_json = {"metadata": {"title": "Sample title"}, "type": "item"} - m.get("mock://example.com/items/123?expand=all", json=rec_json) - results_json1 = {"items": [{"link": "1234"}]} - results_json2 = {"items": []} - m.get( - "mock://example.com/filtered-items?", - [{"json": results_json1}, {"json": results_json2}], + mocked_request.get("mock://example.com/status", json=user_json) + + # get - retrieve item + item_get_url = "mock://example.com/items/123?expand=all" + item_get_response = {"metadata": {"title": "Sample title"}, "type": "item"} + mocked_request.get(item_get_url, json=item_get_response) + + # get - retrieve uuid from handle + uuid_get_url = "mock://example.com/handle/111.1111" + uuid_get_response = {"uuid": "a1b2"} + mocked_request.get(uuid_get_url, json=uuid_get_response) + + # get - retrieve uuid from handle (for test_cli.test_additems ) + uuid_get_url_2 = "mock://example.com/handle/333.3333" + uuid_get_response_2 = {"uuid": "k1l2"} + mocked_request.get(uuid_get_url_2, json=uuid_get_response_2) + + # get - retrieve filtered set of items + filtered_items_get_url = "mock://example.com/filtered-items?" + filtered_items_get_response = [ + {"json": {"items": [{"link": "1234"}]}}, + {"json": {"items": []}}, + ] + mocked_request.get(filtered_items_get_url, filtered_items_get_response) + + # post - add collection to community + collection_post_url = "mock://example.com/communities/a1b2/collections" + collection_post_response = {"uuid": "c3d4"} + mocked_request.post(collection_post_url, json=collection_post_response) + + # post - add item to collection + item_post_url = "mock://example.com/collections/c3d4/items" + item_post_response = {"uuid": "e5f6", "handle": "222.2222"} + mocked_request.post(item_post_url, json=item_post_response) + + # post - add item to collection (for test_cli.test_additems) + item_post_url_2 = "mock://example.com/collections/k1l2/items" + item_post_response_2 = {"uuid": "e5f6", "handle": "222.2222"} + mocked_request.post(item_post_url_2, json=item_post_response_2) + + # post - add bitstream to item + bitstream_post_url = ( + "mock://example.com/items/e5f6/bitstreams?name=aaaa_001_01.pdf" + ) + bitstream_post_response = {"uuid": "g7h8"} + mocked_request.post(bitstream_post_url, json=bitstream_post_response) + + bitstream_post_url_2 = ( + "mock://example.com/items/e5f6/bitstreams?name=aaaa_002_01.pdf" + ) + bitstream_post_response_2 = {"uuid": "i9j0"} + mocked_request.post(bitstream_post_url_2, json=bitstream_post_response_2) + + bitstream_post_url_3 = ( + "mock://example.com/items/e5f6/bitstreams?name=bbbb_003_01.jpg" + ) + bitstream_post_response_3 = {"uuid": "item_003_01_a"} + mocked_request.post(bitstream_post_url_3, json=bitstream_post_response_3) + + bitstream_post_url_4 = ( + "mock://example.com/items/e5f6/bitstreams?name=bbbb_003_01.pdf" + ) + bitstream_post_response_4 = {"uuid": "item_003_01_b"} + mocked_request.post(bitstream_post_url_4, json=bitstream_post_response_4) + + bitstream_post_url_5 = ( + "mock://example.com/items/e5f6/bitstreams?name=bbbb_003_02.pdf" + ) + bitstream_post_response_5 = {"uuid": "item_003_02_a"} + mocked_request.post(bitstream_post_url_5, json=bitstream_post_response_5) + + bitstream_post_url_6 = ( + "mock://example.com/items/e5f6/bitstreams?name=bbbb_004_01.pdf" + ) + bitstream_post_response_6 = {"uuid": "item_004_01_a"} + mocked_request.post(bitstream_post_url_6, json=bitstream_post_response_6) + + bitstream_post_url_7 = ( + "mock://example.com/items/e5f6/bitstreams?name=include_005_01.pdf" ) - rec_json = {"uuid": "a1b2"} - m.get("mock://example.com/handle/111.1111", json=rec_json) - coll_json = {"uuid": "c3d4"} - m.post("mock://example.com/communities/a1b2/collections", json=coll_json) - item_json = {"uuid": "e5f6", "handle": "222.2222"} - m.post("mock://example.com/collections/c3d4/items", json=item_json) - b_json_1 = {"uuid": "g7h8"} - url_1 = "mock://example.com/items/e5f6/bitstreams?name=test_01.pdf" - m.post(url_1, json=b_json_1) - b_json_2 = {"uuid": "i9j0"} - url_2 = "mock://example.com/items/e5f6/bitstreams?name=test_02.pdf" - m.post(url_2, json=b_json_2) - m.get("mock://remoteserver.com/files/test_01.pdf", content=b"Sample") - coll_json = {"uuid": "k1l2"} - m.get("mock://example.com/handle/333.3333", json=coll_json) - item_json_2 = {"uuid": "e5f6", "handle": "222.2222"} - m.post("mock://example.com/collections/k1l2/items", json=item_json_2) - yield m + bitstream_post_response_7 = {"uuid": "item_005_01_a"} + mocked_request.post(bitstream_post_url_7, json=bitstream_post_response_7) + # mocked_request.get("mock://remoteserver.com/files/test_01.pdf", content=b"Sample") + + yield mocked_request diff --git a/tests/fixtures/source_config.json b/tests/fixtures/source_config.json index 8b1bc59..977de0a 100644 --- a/tests/fixtures/source_config.json +++ b/tests/fixtures/source_config.json @@ -1,13 +1,11 @@ { "settings": { - "bitstream_folders": [ - "objects" - ], - "id_regex": ".*-(.*?-.*)\\..*$" + "bitstream_folders": [], + "id_regex": "_(.*)_" }, "mapping": { - "file_identifier": { - "csv_field_name": "file_identifier", + "item_identifier": { + "csv_field_name": "item_identifier", "language": null, "delimiter": "" }, @@ -16,30 +14,10 @@ "language": "en_US", "delimiter": "" }, - "source_system_identifier": { - "csv_field_name": "uri", - "language": null, - "delimiter": "" - }, "dc.contributor.author": { "csv_field_name": "author", "language": null, "delimiter": "|" - }, - "dc.description": { - "csv_field_name": "description", - "language": "en_US", - "delimiter": "" - }, - "dc.rights": { - "csv_field_name": "rights_statement", - "language": "en_US", - "delimiter": "" - }, - "dc.rights.uri": { - "csv_field_name": "rights_uri", - "language": null, - "delimiter": "" } } } \ No newline at end of file diff --git a/tests/fixtures/source_metadata.csv b/tests/fixtures/source_metadata.csv new file mode 100644 index 0000000..c6cec15 --- /dev/null +++ b/tests/fixtures/source_metadata.csv @@ -0,0 +1,6 @@ +item_identifier,title,author +001,Title 1,May Smith +002,Title 2,May Smith +003,Title 3,June Smith +004,Title 4,June Smith +005,Title 5,July Smith \ No newline at end of file diff --git a/tests/fixtures/updated-source_metadata.csv b/tests/fixtures/updated-source_metadata.csv new file mode 100644 index 0000000..73d5a36 --- /dev/null +++ b/tests/fixtures/updated-source_metadata.csv @@ -0,0 +1,6 @@ +item_identifier,title,author,bitstreams +001,Title 1,May Smith,['s3://mocked-bucket/one-to-one/aaaa_001_01.pdf'] +002,Title 2,May Smith,['s3://mocked-bucket/one-to-one/aaaa_002_01.pdf'] +003,Title 3,June Smith,"['s3://mocked-bucket/many-to-one/bbbb_003_01.jpg', 's3://mocked-bucket/many-to-one/bbbb_003_01.pdf', 's3://mocked-bucket/many-to-one/bbbb_003_02.pdf']" +004,Title 4,June Smith,['s3://mocked-bucket/many-to-one/bbbb_004_01.pdf'] +005,Title 5,July Smith,['s3://mocked-bucket/nested/prefix/objects/include_005_01.pdf'] diff --git a/tests/test_cli.py b/tests/test_cli.py index c0ea336..feba613 100644 --- a/tests/test_cli.py +++ b/tests/test_cli.py @@ -4,7 +4,7 @@ @mock_aws -def test_additems(runner, mocked_s3): +def test_additems(runner, mocked_s3_bucket, caplog): """Test adding items to a collection.""" result = runner.invoke( main, @@ -19,18 +19,15 @@ def test_additems(runner, mocked_s3): "1234", "additems", "--metadata-csv", - "tests/fixtures/aspace_metadata_delimited.csv", - "--field-map", - "config/aspace_mapping.json", + "tests/fixtures/updated-source_metadata.csv", "--content-directory", - "s3://test-bucket", - "--file-type", - "pdf", + "s3://mocked-bucket", "--collection-handle", "333.3333", ], ) assert result.exit_code == 0 + result = runner.invoke( main, [ @@ -49,13 +46,9 @@ def test_additems(runner, mocked_s3): "Test Collection", "additems", "--metadata-csv", - "tests/fixtures/aspace_metadata_delimited.csv", - "--field-map", - "config/aspace_mapping.json", + "tests/fixtures/updated-source_metadata.csv", "--content-directory", - "s3://test-bucket", - "--file-type", - "pdf", + "s3://mocked-bucket", ], ) assert result.exit_code == 0 @@ -85,7 +78,7 @@ def test_newcollection(runner): @mock_aws -def test_reconcile(runner, mocked_s3, output_dir): +def test_reconcile(runner, mocked_s3_bucket, output_dir): """Test reconcile command.""" result = runner.invoke( main, @@ -100,11 +93,11 @@ def test_reconcile(runner, mocked_s3, output_dir): "1234", "reconcile", "--metadata-csv", - "tests/fixtures/aspace_metadata_delimited.csv", + "tests/fixtures/source_metadata.csv", "--output-directory", output_dir, "--content-directory", - "s3://test-bucket", + "s3://mocked-bucket", ], ) assert result.exit_code == 0 diff --git a/tests/test_helpers.py b/tests/test_helpers.py index 92843f2..34c12d3 100644 --- a/tests/test_helpers.py +++ b/tests/test_helpers.py @@ -20,8 +20,8 @@ def test_load_source_config(): assert load_source_config("tests/fixtures/source_config.json")["settings"] == { - "bitstream_folders": ["objects"], - "id_regex": ".*-(.*?-.*)\\..*$", + "bitstream_folders": [], + "id_regex": "_(.*)_", } @@ -61,10 +61,10 @@ def test_get_files_from_s3_one_file_per_file_id(mocked_s3_bucket, s3_client): ) assert files == { "001": [ - "one-to-one/aaaa_001_01.pdf", + "s3://mocked-bucket/one-to-one/aaaa_001_01.pdf", ], "002": [ - "one-to-one/aaaa_002_01.pdf", + "s3://mocked-bucket/one-to-one/aaaa_002_01.pdf", ], } @@ -77,11 +77,11 @@ def test_get_files_from_s3_many_files_per_file_id(mocked_s3_bucket, s3_client): ) assert files == { "003": [ - "many-to-one/bbbb_003_01.jpg", - "many-to-one/bbbb_003_01.pdf", - "many-to-one/bbbb_003_02.pdf", + "s3://mocked-bucket/many-to-one/bbbb_003_01.jpg", + "s3://mocked-bucket/many-to-one/bbbb_003_01.pdf", + "s3://mocked-bucket/many-to-one/bbbb_003_02.pdf", ], - "004": ["many-to-one/bbbb_004_01.pdf"], + "004": ["s3://mocked-bucket/many-to-one/bbbb_004_01.pdf"], } @@ -92,7 +92,9 @@ def test_get_files_from_s3_with_bitstream_folders(mocked_s3_bucket, s3_client): bitstream_folders=["objects"], id_regex=REGEX_ID_BETWEEN_UNDERSCORES, ) - assert files == {"005": ["nested/prefix/objects/include_005_01.pdf"]} + assert files == { + "005": ["s3://mocked-bucket/nested/prefix/objects/include_005_01.pdf"] + } def test_get_files_from_s3_without_bitstream_folders(mocked_s3_bucket, s3_client): @@ -103,18 +105,18 @@ def test_get_files_from_s3_without_bitstream_folders(mocked_s3_bucket, s3_client ) assert files == { "001": [ - "one-to-one/aaaa_001_01.pdf", + "s3://mocked-bucket/one-to-one/aaaa_001_01.pdf", ], "002": [ - "one-to-one/aaaa_002_01.pdf", + "s3://mocked-bucket/one-to-one/aaaa_002_01.pdf", ], "003": [ - "many-to-one/bbbb_003_01.jpg", - "many-to-one/bbbb_003_01.pdf", - "many-to-one/bbbb_003_02.pdf", + "s3://mocked-bucket/many-to-one/bbbb_003_01.jpg", + "s3://mocked-bucket/many-to-one/bbbb_003_01.pdf", + "s3://mocked-bucket/many-to-one/bbbb_003_02.pdf", ], - "004": ["many-to-one/bbbb_004_01.pdf"], - "005": ["nested/prefix/objects/include_005_01.pdf"], + "004": ["s3://mocked-bucket/many-to-one/bbbb_004_01.pdf"], + "005": ["s3://mocked-bucket/nested/prefix/objects/include_005_01.pdf"], } @@ -132,10 +134,9 @@ def test_create_ingest_report(runner, output_dir): def test_create_metadata_id_list(): """Test create_metadata_id_list function.""" - metadata_path = "tests/fixtures/aspace_metadata_delimited.csv" + metadata_path = "tests/fixtures/source_metadata.csv" metadata_ids = create_metadata_id_list(metadata_path) - assert "test" in metadata_ids - assert "tast" in metadata_ids + assert metadata_ids == ["001", "002", "003", "004", "005"] def test_match_files_to_metadata(): @@ -156,19 +157,21 @@ def test_match_metadata_to_files(): assert "test" in file_matches -def test_update_metadata_csv(output_dir): +def test_update_metadata_csv(output_dir, mocked_s3_bucket_bitstreams): """Test update_metadata_csv function.""" - metadata_matches = ["test"] update_metadata_csv( - "tests/fixtures/aspace_metadata_delimited.csv", - output_dir, - metadata_matches, - {"test": ["/test/test_01.pdf"]}, + metadata_csv="tests/fixtures/source_metadata.csv", + output_directory=output_dir, + metadata_matches=["001"], + files_dict=mocked_s3_bucket_bitstreams, ) - with open(f"{output_dir}updated-aspace_metadata_delimited.csv") as csvfile: + with open(f"{output_dir}/updated-source_metadata.csv") as csvfile: reader = csv.DictReader(csvfile) - for row in reader: - assert row["uri"] == "/repo/0/ao/123" - assert row["title"] == "Test Item" - assert row["file_identifier"] == "test" - assert row["bitstreams"] == "['/test/test_01.pdf']" + record = next(reader) + assert record is not None + assert record == { + "item_identifier": "001", + "title": "Title 1", + "author": "May Smith", + "bitstreams": "['s3://mocked-bucket/one-to-one/aaaa_001_01.pdf']", + } diff --git a/tests/test_models.py b/tests/test_models.py index 02c8d53..7fc9a53 100644 --- a/tests/test_models.py +++ b/tests/test_models.py @@ -15,11 +15,8 @@ def test_authenticate(dspace_client): def test_filtered_item_search(dspace_client): """Test filtered_item_search method.""" - key = "dc.title" - string = "test" - query_type = "contains" item_links = dspace_client.filtered_item_search( - key, string, query_type, selected_collections="" + key="dc.title", string="test", query_type="contains", selected_collections="" ) assert "1234" in item_links @@ -36,10 +33,12 @@ def test_get_record(dspace_client): assert attr.asdict(rec_obj)["metadata"] == {"title": "Sample title"} -def test_post_bitstream(dspace_client, mocked_s3): +def test_post_bitstream(dspace_client, mocked_s3_bucket): """Test post_bitstream method.""" item_uuid = "e5f6" - bitstream = Bitstream(name="test_01.pdf", file_path="s3://test-bucket/test_01.pdf") + bitstream = Bitstream( + name="aaaa_001_01.pdf", file_path="s3://mocked-bucket/one-to-one/aaaa_001_01.pdf" + ) bit_uuid = dspace_client.post_bitstream(item_uuid, bitstream) assert bit_uuid == "g7h8" @@ -53,21 +52,18 @@ def test_post_coll_to_comm(dspace_client): @mock_aws -def test_post_item_to_collection(dspace_client, mocked_s3): +def test_post_item_to_collection(dspace_client, mocked_s3_bucket): """Test post_item_to_collection method.""" item = DSpaceItem() item.bitstreams = [ - Bitstream(name="test_01.pdf", file_path="s3://test-bucket/test_01.pdf") + Bitstream(name="aaaa_001_01.pdf", file_path="s3://mocked-bucket/aaaa_001_01.pdf") ] item.metadata = [ - MetadataEntry(key="file_identifier", value="test"), - MetadataEntry( - key="dc.title", value="Monitoring Works: Getting Teachers", language="en_US" - ), - MetadataEntry(key="dc.relation.isversionof", value="repo/0/ao/123"), + MetadataEntry(key="dc.title", value="Title 1", language="en_US"), + MetadataEntry(key="dc.contributor.author", value="May Smith", language=None), ] - coll_uuid = "c3d4" - item_uuid, item_handle = dspace_client.post_item_to_collection(coll_uuid, item) + collection_uuid = "c3d4" + item_uuid, item_handle = dspace_client.post_item_to_collection(collection_uuid, item) assert item_uuid == "e5f6" assert item_handle == "222.2222" @@ -90,43 +86,36 @@ def test__build_uuid_list(dspace_client): def test_collection_create_metadata_for_items_from_csv( - aspace_delimited_csv, aspace_mapping + source_metadata_csv, source_config ): collection = DSpaceCollection.create_metadata_for_items_from_csv( - aspace_delimited_csv, aspace_mapping + source_metadata_csv, source_config["mapping"] ) - assert 2 == len(collection.items) - - -@mock_aws -def test_collection_post_items( - mocked_s3, - dspace_client, - aspace_delimited_csv, - aspace_mapping, -): - collection = DSpaceCollection.create_metadata_for_items_from_csv( - aspace_delimited_csv, aspace_mapping - ) - collection.uuid = "c3d4" - items = collection.post_items(dspace_client) - for item in items: - assert item.handle == "222.2222" - assert item.uuid == "e5f6" - - -def test_item_metadata_from_csv_row(aspace_delimited_csv, aspace_mapping): - row = next(aspace_delimited_csv) - item = DSpaceItem.metadata_from_csv_row(row, aspace_mapping) + assert len(collection.items) == 5 + + +# @mock_aws +# def test_collection_post_items( +# mocked_s3_bucket, +# dspace_client, +# source_metadata_csv, +# source_config, +# ): +# collection = DSpaceCollection(uuid="c3d4") +# collection.create_metadata_for_items_from_csv( +# source_metadata_csv, source_config["mapping"] +# ) +# items = collection.post_items(dspace_client) +# print(f"item: {next(items)}") +# for item in items: +# assert item.handle != "222.2222" +# assert item.uuid != "e5f6" + + +def test_item_metadata_from_csv_row(source_metadata_csv, source_config): + record = next(source_metadata_csv) + item = DSpaceItem.metadata_from_csv_row(record, source_config["mapping"]) assert attr.asdict(item)["metadata"] == [ - {"key": "dc.title", "value": "Tast Item", "language": "en_US"}, - {"key": "dc.contributor.author", "value": "Smith, John", "language": None}, - {"key": "dc.contributor.author", "value": "Smith, Jane", "language": None}, - { - "key": "dc.description", - "value": "More info at /repo/0/ao/456", - "language": "en_US", - }, - {"key": "dc.rights", "value": "Totally Free", "language": "en_US"}, - {"key": "dc.rights.uri", "value": "http://free.gov", "language": None}, + {"key": "dc.title", "value": "Title 1", "language": "en_US"}, + {"key": "dc.contributor.author", "value": "May Smith", "language": None}, ] From ea3891cca9643e91fadcb94f2e9df71de8487a70 Mon Sep 17 00:00:00 2001 From: jonavellecuerdo Date: Mon, 25 Mar 2024 16:03:32 -0400 Subject: [PATCH 16/18] Update 'additems' command * Deprecate 'field_map' and 'file_type' args * Remove unused S3 client * Reference bitstreams from metadata CSV file updated by 'reconcile' command * Pull out DSpaceCollection.post_items into CLI command --- Pipfile.lock | 266 ++++++++++++++++++++++++++---------------- README.md | 45 +++---- config/aspace.json | 2 +- dsaps/cli.py | 92 +++++++-------- dsaps/dspace.py | 192 ++++++++++++++++-------------- dsaps/helpers.py | 17 +-- tests/conftest.py | 7 ++ tests/test_helpers.py | 27 +++-- tests/test_models.py | 113 +++++++++--------- 9 files changed, 435 insertions(+), 326 deletions(-) diff --git a/Pipfile.lock b/Pipfile.lock index 45fb7b8..e51e3a6 100644 --- a/Pipfile.lock +++ b/Pipfile.lock @@ -26,19 +26,19 @@ }, "boto3": { "hashes": [ - "sha256:0d382baac02ba4ead82230f34ba377fbf5f6481321dca911e6664b752d79b682", - "sha256:eb5d84c2127ffddf8e7f4dd6f9084f86cb18dca8416fb5d6bea278298cf8d84c" + "sha256:a33585ef0d811ee0dffd92a96108344997a3059262c57349be0761d7885f6ae7", + "sha256:cbfabd99c113bbb1708c2892e864b6dd739593b97a76fbb2e090a7d965b63b82" ], "index": "pypi", - "version": "==1.34.46" + "version": "==1.34.72" }, "botocore": { "hashes": [ - "sha256:21a6c391c6b4869aed66bc888b8e6d54581b343514cfe97dbe71ede12026c3cc", - "sha256:f54330ba1e8ce31489a4e09b4ba8afbf84be01bbc48dbb31d44897fb7657f7ad" + "sha256:342edb6f91d5839e790411822fc39f9c712c87cdaa7f3b1999f50b1ca16c4a14", + "sha256:a6b92735a73c19a7e540d77320420da3af3f32c91fa661c738c0b8c9f912d782" ], "markers": "python_version >= '3.8'", - "version": "==1.34.46" + "version": "==1.34.72" }, "certifi": { "hashes": [ @@ -212,41 +212,41 @@ }, "cryptography": { "hashes": [ - "sha256:01911714117642a3f1792c7f376db572aadadbafcd8d75bb527166009c9f1d1b", - "sha256:0e89f7b84f421c56e7ff69f11c441ebda73b8a8e6488d322ef71746224c20fce", - "sha256:12d341bd42cdb7d4937b0cabbdf2a94f949413ac4504904d0cdbdce4a22cbf88", - "sha256:15a1fb843c48b4a604663fa30af60818cd28f895572386e5f9b8a665874c26e7", - "sha256:1cdcdbd117681c88d717437ada72bdd5be9de117f96e3f4d50dab3f59fd9ab20", - "sha256:1df6fcbf60560d2113b5ed90f072dc0b108d64750d4cbd46a21ec882c7aefce9", - "sha256:3c6048f217533d89f2f8f4f0fe3044bf0b2090453b7b73d0b77db47b80af8dff", - "sha256:3e970a2119507d0b104f0a8e281521ad28fc26f2820687b3436b8c9a5fcf20d1", - "sha256:44a64043f743485925d3bcac548d05df0f9bb445c5fcca6681889c7c3ab12764", - "sha256:4e36685cb634af55e0677d435d425043967ac2f3790ec652b2b88ad03b85c27b", - "sha256:5f8907fcf57392cd917892ae83708761c6ff3c37a8e835d7246ff0ad251d9298", - "sha256:69b22ab6506a3fe483d67d1ed878e1602bdd5912a134e6202c1ec672233241c1", - "sha256:6bfadd884e7280df24d26f2186e4e07556a05d37393b0f220a840b083dc6a824", - "sha256:6d0fbe73728c44ca3a241eff9aefe6496ab2656d6e7a4ea2459865f2e8613257", - "sha256:6ffb03d419edcab93b4b19c22ee80c007fb2d708429cecebf1dd3258956a563a", - "sha256:810bcf151caefc03e51a3d61e53335cd5c7316c0a105cc695f0959f2c638b129", - "sha256:831a4b37accef30cccd34fcb916a5d7b5be3cbbe27268a02832c3e450aea39cb", - "sha256:887623fe0d70f48ab3f5e4dbf234986b1329a64c066d719432d0698522749929", - "sha256:a0298bdc6e98ca21382afe914c642620370ce0470a01e1bef6dd9b5354c36854", - "sha256:a1327f280c824ff7885bdeef8578f74690e9079267c1c8bd7dc5cc5aa065ae52", - "sha256:c1f25b252d2c87088abc8bbc4f1ecbf7c919e05508a7e8628e6875c40bc70923", - "sha256:c3a5cbc620e1e17009f30dd34cb0d85c987afd21c41a74352d1719be33380885", - "sha256:ce8613beaffc7c14f091497346ef117c1798c202b01153a8cc7b8e2ebaaf41c0", - "sha256:d2a27aca5597c8a71abbe10209184e1a8e91c1fd470b5070a2ea60cafec35bcd", - "sha256:dad9c385ba8ee025bb0d856714f71d7840020fe176ae0229de618f14dae7a6e2", - "sha256:db4b65b02f59035037fde0998974d84244a64c3265bdef32a827ab9b63d61b18", - "sha256:e09469a2cec88fb7b078e16d4adec594414397e8879a4341c6ace96013463d5b", - "sha256:e53dc41cda40b248ebc40b83b31516487f7db95ab8ceac1f042626bc43a2f992", - "sha256:f1e85a178384bf19e36779d91ff35c7617c885da487d689b05c1366f9933ad74", - "sha256:f47be41843200f7faec0683ad751e5ef11b9a56a220d57f300376cd8aba81660", - "sha256:fb0cef872d8193e487fc6bdb08559c3aa41b659a7d9be48b2e10747f47863925", - "sha256:ffc73996c4fca3d2b6c1c8c12bfd3ad00def8621da24f547626bf06441400449" + "sha256:0270572b8bd2c833c3981724b8ee9747b3ec96f699a9665470018594301439ee", + "sha256:111a0d8553afcf8eb02a4fea6ca4f59d48ddb34497aa8706a6cf536f1a5ec576", + "sha256:16a48c23a62a2f4a285699dba2e4ff2d1cff3115b9df052cdd976a18856d8e3d", + "sha256:1b95b98b0d2af784078fa69f637135e3c317091b615cd0905f8b8a087e86fa30", + "sha256:1f71c10d1e88467126f0efd484bd44bca5e14c664ec2ede64c32f20875c0d413", + "sha256:2424ff4c4ac7f6b8177b53c17ed5d8fa74ae5955656867f5a8affaca36a27abb", + "sha256:2bce03af1ce5a5567ab89bd90d11e7bbdff56b8af3acbbec1faded8f44cb06da", + "sha256:329906dcc7b20ff3cad13c069a78124ed8247adcac44b10bea1130e36caae0b4", + "sha256:37dd623507659e08be98eec89323469e8c7b4c1407c85112634ae3dbdb926fdd", + "sha256:3eaafe47ec0d0ffcc9349e1708be2aaea4c6dd4978d76bf6eb0cb2c13636c6fc", + "sha256:5e6275c09d2badf57aea3afa80d975444f4be8d3bc58f7f80d2a484c6f9485c8", + "sha256:6fe07eec95dfd477eb9530aef5bead34fec819b3aaf6c5bd6d20565da607bfe1", + "sha256:7367d7b2eca6513681127ebad53b2582911d1736dc2ffc19f2c3ae49997496bc", + "sha256:7cde5f38e614f55e28d831754e8a3bacf9ace5d1566235e39d91b35502d6936e", + "sha256:9481ffe3cf013b71b2428b905c4f7a9a4f76ec03065b05ff499bb5682a8d9ad8", + "sha256:98d8dc6d012b82287f2c3d26ce1d2dd130ec200c8679b6213b3c73c08b2b7940", + "sha256:a011a644f6d7d03736214d38832e030d8268bcff4a41f728e6030325fea3e400", + "sha256:a2913c5375154b6ef2e91c10b5720ea6e21007412f6437504ffea2109b5a33d7", + "sha256:a30596bae9403a342c978fb47d9b0ee277699fa53bbafad14706af51fe543d16", + "sha256:b03c2ae5d2f0fc05f9a2c0c997e1bc18c8229f392234e8a0194f202169ccd278", + "sha256:b6cd2203306b63e41acdf39aa93b86fb566049aeb6dc489b70e34bcd07adca74", + "sha256:b7ffe927ee6531c78f81aa17e684e2ff617daeba7f189f911065b2ea2d526dec", + "sha256:b8cac287fafc4ad485b8a9b67d0ee80c66bf3574f655d3b97ef2e1082360faf1", + "sha256:ba334e6e4b1d92442b75ddacc615c5476d4ad55cc29b15d590cc6b86efa487e2", + "sha256:ba3e4a42397c25b7ff88cdec6e2a16c2be18720f317506ee25210f6d31925f9c", + "sha256:c41fb5e6a5fe9ebcd58ca3abfeb51dffb5d83d6775405305bfa8715b76521922", + "sha256:cd2030f6650c089aeb304cf093f3244d34745ce0cfcc39f20c6fbfe030102e2a", + "sha256:cd65d75953847815962c84a4654a84850b2bb4aed3f26fadcc1c13892e1e29f6", + "sha256:e4985a790f921508f36f81831817cbc03b102d643b5fcb81cd33df3fa291a1a1", + "sha256:e807b3188f9eb0eaa7bbb579b462c5ace579f1cedb28107ce8b48a9f7ad3679e", + "sha256:f12764b8fffc7a123f641d7d049d382b73f96a34117e0b637b80643169cec8ac", + "sha256:f8837fe1d6ac4a8052a9a8ddab256bc006242696f03368a4009be7ee3075cdb7" ], "markers": "python_version >= '3.7'", - "version": "==42.0.4" + "version": "==42.0.5" }, "idna": { "hashes": [ @@ -427,18 +427,18 @@ "s3" ], "hashes": [ - "sha256:71bb832a18b64f10fc4cec117b9b0e2305e5831d9a17eb74f6b9819ed7613843", - "sha256:7e27395e5c63ff9554ae14b5baa41bfe6d6b1be0e59eb02977c6ce28411246de" + "sha256:4054360b882b6e7bab25d52d057e196b978b8d15f1921333f534c4d8f6510bbb", + "sha256:8d19125d40c919cb40df62f4576904c2647c4e9a0e1ebc42491dd7787d09e107" ], "index": "pypi", - "version": "==5.0.2" + "version": "==5.0.4" }, "py-partiql-parser": { "hashes": [ - "sha256:53053e70987dea2983e1990ad85f87a7d8cec13dd4a4b065a740bcfd661f5a6b", - "sha256:aeac8f46529d8651bbae88a1a6c14dc3aa38ebc4bc6bd1eb975044c0564246c6" + "sha256:9c79b59bbe0cb50daa8090020f2e7f3e5a0e33f7846b48924f19a8f7704f4877", + "sha256:bdec65fe17d6093c05e9bc1742a99a041ef810b50a71cc0d9e74a88218d938cf" ], - "version": "==0.5.1" + "version": "==0.5.2" }, "pycparser": { "hashes": [ @@ -449,11 +449,11 @@ }, "python-dateutil": { "hashes": [ - "sha256:0123cacc1627ae19ddf3c27a5de5bd67ee4586fbdd6440d9748f8abb483d3e86", - "sha256:961d03dc3453ebbc59dbdea9e4e11c5651520a876d0f4db161e8674aae935da9" + "sha256:37dd54208da7e1cd875388217d5e00ebd4179249f90fb72437e91a35459a0ad3", + "sha256:a8b2bc7bffae282281c8140a97d3aa9c14da0b136dfe83f850eea9a5f7470427" ], - "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2'", - "version": "==2.8.2" + "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3'", + "version": "==2.9.0.post0" }, "pyyaml": { "hashes": [ @@ -529,18 +529,18 @@ }, "s3transfer": { "hashes": [ - "sha256:3cdb40f5cfa6966e812209d0994f2a4709b561c88e90cf00c2696d2df4e56b2e", - "sha256:d0c8bbf672d5eebbe4e57945e23b972d963f07d82f661cabf678a5c88831595b" + "sha256:5683916b4c724f799e600f41dd9e10a9ff19871bf87623cc8f491cb4f5fa0a19", + "sha256:ceb252b11bcf87080fb7850a224fb6e05c8a776bab8f2b64b7f25b969464839d" ], "markers": "python_version >= '3.8'", - "version": "==0.10.0" + "version": "==0.10.1" }, "six": { "hashes": [ "sha256:1e61c37477a1626458e36f7b1d82aa5c9b094fa4802892072e49de9c60c4c926", "sha256:8abb2f1d86890a2dfb989f9a77cfcfd3e47c2a354b01111771326f8aa26e0254" ], - "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2'", + "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3'", "version": "==1.16.0" }, "smart-open": { @@ -548,11 +548,11 @@ "s3" ], "hashes": [ - "sha256:8d3ef7e6997e8e42dd55c74166ed21e6ac70664caa32dd940b26d54a8f6b4142", - "sha256:be3c92c246fbe80ebce8fbacb180494a481a77fcdcb7c1aadb2ea5b9c2bee8b9" + "sha256:4e98489932b3372595cddc075e6033194775165702887216b65eba760dfd8d47", + "sha256:62b65852bdd1d1d516839fcb1f6bc50cd0f16e05b4ec44b52f43d38bcb838524" ], "index": "pypi", - "version": "==6.4.0" + "version": "==7.0.4" }, "structlog": { "hashes": [ @@ -578,6 +578,82 @@ "markers": "python_version >= '3.8'", "version": "==3.0.1" }, + "wrapt": { + "hashes": [ + "sha256:0d2691979e93d06a95a26257adb7bfd0c93818e89b1406f5a28f36e0d8c1e1fc", + "sha256:14d7dc606219cdd7405133c713f2c218d4252f2a469003f8c46bb92d5d095d81", + "sha256:1a5db485fe2de4403f13fafdc231b0dbae5eca4359232d2efc79025527375b09", + "sha256:1acd723ee2a8826f3d53910255643e33673e1d11db84ce5880675954183ec47e", + "sha256:1ca9b6085e4f866bd584fb135a041bfc32cab916e69f714a7d1d397f8c4891ca", + "sha256:1dd50a2696ff89f57bd8847647a1c363b687d3d796dc30d4dd4a9d1689a706f0", + "sha256:2076fad65c6736184e77d7d4729b63a6d1ae0b70da4868adeec40989858eb3fb", + "sha256:2a88e6010048489cda82b1326889ec075a8c856c2e6a256072b28eaee3ccf487", + "sha256:3ebf019be5c09d400cf7b024aa52b1f3aeebeff51550d007e92c3c1c4afc2a40", + "sha256:418abb18146475c310d7a6dc71143d6f7adec5b004ac9ce08dc7a34e2babdc5c", + "sha256:43aa59eadec7890d9958748db829df269f0368521ba6dc68cc172d5d03ed8060", + "sha256:44a2754372e32ab315734c6c73b24351d06e77ffff6ae27d2ecf14cf3d229202", + "sha256:490b0ee15c1a55be9c1bd8609b8cecd60e325f0575fc98f50058eae366e01f41", + "sha256:49aac49dc4782cb04f58986e81ea0b4768e4ff197b57324dcbd7699c5dfb40b9", + "sha256:5eb404d89131ec9b4f748fa5cfb5346802e5ee8836f57d516576e61f304f3b7b", + "sha256:5f15814a33e42b04e3de432e573aa557f9f0f56458745c2074952f564c50e664", + "sha256:5f370f952971e7d17c7d1ead40e49f32345a7f7a5373571ef44d800d06b1899d", + "sha256:66027d667efe95cc4fa945af59f92c5a02c6f5bb6012bff9e60542c74c75c362", + "sha256:66dfbaa7cfa3eb707bbfcd46dab2bc6207b005cbc9caa2199bcbc81d95071a00", + "sha256:685f568fa5e627e93f3b52fda002c7ed2fa1800b50ce51f6ed1d572d8ab3e7fc", + "sha256:6906c4100a8fcbf2fa735f6059214bb13b97f75b1a61777fcf6432121ef12ef1", + "sha256:6a42cd0cfa8ffc1915aef79cb4284f6383d8a3e9dcca70c445dcfdd639d51267", + "sha256:6dcfcffe73710be01d90cae08c3e548d90932d37b39ef83969ae135d36ef3956", + "sha256:6f6eac2360f2d543cc875a0e5efd413b6cbd483cb3ad7ebf888884a6e0d2e966", + "sha256:72554a23c78a8e7aa02abbd699d129eead8b147a23c56e08d08dfc29cfdddca1", + "sha256:73870c364c11f03ed072dda68ff7aea6d2a3a5c3fe250d917a429c7432e15228", + "sha256:73aa7d98215d39b8455f103de64391cb79dfcad601701a3aa0dddacf74911d72", + "sha256:75ea7d0ee2a15733684badb16de6794894ed9c55aa5e9903260922f0482e687d", + "sha256:7bd2d7ff69a2cac767fbf7a2b206add2e9a210e57947dd7ce03e25d03d2de292", + "sha256:807cc8543a477ab7422f1120a217054f958a66ef7314f76dd9e77d3f02cdccd0", + "sha256:8e9723528b9f787dc59168369e42ae1c3b0d3fadb2f1a71de14531d321ee05b0", + "sha256:9090c9e676d5236a6948330e83cb89969f433b1943a558968f659ead07cb3b36", + "sha256:9153ed35fc5e4fa3b2fe97bddaa7cbec0ed22412b85bcdaf54aeba92ea37428c", + "sha256:9159485323798c8dc530a224bd3ffcf76659319ccc7bbd52e01e73bd0241a0c5", + "sha256:941988b89b4fd6b41c3f0bfb20e92bd23746579736b7343283297c4c8cbae68f", + "sha256:94265b00870aa407bd0cbcfd536f17ecde43b94fb8d228560a1e9d3041462d73", + "sha256:98b5e1f498a8ca1858a1cdbffb023bfd954da4e3fa2c0cb5853d40014557248b", + "sha256:9b201ae332c3637a42f02d1045e1d0cccfdc41f1f2f801dafbaa7e9b4797bfc2", + "sha256:a0ea261ce52b5952bf669684a251a66df239ec6d441ccb59ec7afa882265d593", + "sha256:a33a747400b94b6d6b8a165e4480264a64a78c8a4c734b62136062e9a248dd39", + "sha256:a452f9ca3e3267cd4d0fcf2edd0d035b1934ac2bd7e0e57ac91ad6b95c0c6389", + "sha256:a86373cf37cd7764f2201b76496aba58a52e76dedfaa698ef9e9688bfd9e41cf", + "sha256:ac83a914ebaf589b69f7d0a1277602ff494e21f4c2f743313414378f8f50a4cf", + "sha256:aefbc4cb0a54f91af643660a0a150ce2c090d3652cf4052a5397fb2de549cd89", + "sha256:b3646eefa23daeba62643a58aac816945cadc0afaf21800a1421eeba5f6cfb9c", + "sha256:b47cfad9e9bbbed2339081f4e346c93ecd7ab504299403320bf85f7f85c7d46c", + "sha256:b935ae30c6e7400022b50f8d359c03ed233d45b725cfdd299462f41ee5ffba6f", + "sha256:bb2dee3874a500de01c93d5c71415fcaef1d858370d405824783e7a8ef5db440", + "sha256:bc57efac2da352a51cc4658878a68d2b1b67dbe9d33c36cb826ca449d80a8465", + "sha256:bf5703fdeb350e36885f2875d853ce13172ae281c56e509f4e6eca049bdfb136", + "sha256:c31f72b1b6624c9d863fc095da460802f43a7c6868c5dda140f51da24fd47d7b", + "sha256:c5cd603b575ebceca7da5a3a251e69561bec509e0b46e4993e1cac402b7247b8", + "sha256:d2efee35b4b0a347e0d99d28e884dfd82797852d62fcd7ebdeee26f3ceb72cf3", + "sha256:d462f28826f4657968ae51d2181a074dfe03c200d6131690b7d65d55b0f360f8", + "sha256:d5e49454f19ef621089e204f862388d29e6e8d8b162efce05208913dde5b9ad6", + "sha256:da4813f751142436b075ed7aa012a8778aa43a99f7b36afe9b742d3ed8bdc95e", + "sha256:db2e408d983b0e61e238cf579c09ef7020560441906ca990fe8412153e3b291f", + "sha256:db98ad84a55eb09b3c32a96c576476777e87c520a34e2519d3e59c44710c002c", + "sha256:dbed418ba5c3dce92619656802cc5355cb679e58d0d89b50f116e4a9d5a9603e", + "sha256:dcdba5c86e368442528f7060039eda390cc4091bfd1dca41e8046af7c910dda8", + "sha256:decbfa2f618fa8ed81c95ee18a387ff973143c656ef800c9f24fb7e9c16054e2", + "sha256:e4fdb9275308292e880dcbeb12546df7f3e0f96c6b41197e0cf37d2826359020", + "sha256:eb1b046be06b0fce7249f1d025cd359b4b80fc1c3e24ad9eca33e0dcdb2e4a35", + "sha256:eb6e651000a19c96f452c85132811d25e9264d836951022d6e81df2fff38337d", + "sha256:ed867c42c268f876097248e05b6117a65bcd1e63b779e916fe2e33cd6fd0d3c3", + "sha256:edfad1d29c73f9b863ebe7082ae9321374ccb10879eeabc84ba3b69f2579d537", + "sha256:f2058f813d4f2b5e3a9eb2eb3faf8f1d99b81c3e51aeda4b168406443e8ba809", + "sha256:f6b2d0c6703c988d334f297aa5df18c45e97b0af3679bb75059e0e0bd8b1069d", + "sha256:f8212564d49c50eb4565e502814f694e240c55551a5f1bc841d4fcaabb0a9b8a", + "sha256:ffa565331890b90056c01db69c0fe634a776f8019c143a5ae265f9c6bc4bd6d4" + ], + "markers": "python_version >= '3.6'", + "version": "==1.16.0" + }, "xmltodict": { "hashes": [ "sha256:341595a488e3e01a85a9d8911d8912fd922ede5fecc4dce437eb4b6c8d037e56", @@ -590,31 +666,31 @@ "develop": { "black": { "hashes": [ - "sha256:057c3dc602eaa6fdc451069bd027a1b2635028b575a6c3acfd63193ced20d9c8", - "sha256:08654d0797e65f2423f850fc8e16a0ce50925f9337fb4a4a176a7aa4026e63f8", - "sha256:163baf4ef40e6897a2a9b83890e59141cc8c2a98f2dda5080dc15c00ee1e62cd", - "sha256:1e08fb9a15c914b81dd734ddd7fb10513016e5ce7e6704bdd5e1251ceee51ac9", - "sha256:4dd76e9468d5536abd40ffbc7a247f83b2324f0c050556d9c371c2b9a9a95e31", - "sha256:4f9de21bafcba9683853f6c96c2d515e364aee631b178eaa5145fc1c61a3cc92", - "sha256:61a0391772490ddfb8a693c067df1ef5227257e72b0e4108482b8d41b5aee13f", - "sha256:6981eae48b3b33399c8757036c7f5d48a535b962a7c2310d19361edeef64ce29", - "sha256:7e53a8c630f71db01b28cd9602a1ada68c937cbf2c333e6ed041390d6968faf4", - "sha256:810d445ae6069ce64030c78ff6127cd9cd178a9ac3361435708b907d8a04c693", - "sha256:93601c2deb321b4bad8f95df408e3fb3943d85012dddb6121336b8e24a0d1218", - "sha256:992e451b04667116680cb88f63449267c13e1ad134f30087dec8527242e9862a", - "sha256:9db528bccb9e8e20c08e716b3b09c6bdd64da0dd129b11e160bf082d4642ac23", - "sha256:a0057f800de6acc4407fe75bb147b0c2b5cbb7c3ed110d3e5999cd01184d53b0", - "sha256:ba15742a13de85e9b8f3239c8f807723991fbfae24bad92d34a2b12e81904982", - "sha256:bce4f25c27c3435e4dace4815bcb2008b87e167e3bf4ee47ccdc5ce906eb4894", - "sha256:ca610d29415ee1a30a3f30fab7a8f4144e9d34c89a235d81292a1edb2b55f540", - "sha256:d533d5e3259720fdbc1b37444491b024003e012c5173f7d06825a77508085430", - "sha256:d84f29eb3ee44859052073b7636533ec995bd0f64e2fb43aeceefc70090e752b", - "sha256:e37c99f89929af50ffaf912454b3e3b47fd64109659026b678c091a4cd450fb2", - "sha256:e8a6ae970537e67830776488bca52000eaa37fa63b9988e8c487458d9cd5ace6", - "sha256:faf2ee02e6612577ba0181f4347bcbcf591eb122f7841ae5ba233d12c39dcb4d" + "sha256:2818cf72dfd5d289e48f37ccfa08b460bf469e67fb7c4abb07edc2e9f16fb63f", + "sha256:41622020d7120e01d377f74249e677039d20e6344ff5851de8a10f11f513bf93", + "sha256:4acf672def7eb1725f41f38bf6bf425c8237248bb0804faa3965c036f7672d11", + "sha256:4be5bb28e090456adfc1255e03967fb67ca846a03be7aadf6249096100ee32d0", + "sha256:4f1373a7808a8f135b774039f61d59e4be7eb56b2513d3d2f02a8b9365b8a8a9", + "sha256:56f52cfbd3dabe2798d76dbdd299faa046a901041faf2cf33288bc4e6dae57b5", + "sha256:65b76c275e4c1c5ce6e9870911384bff5ca31ab63d19c76811cb1fb162678213", + "sha256:65c02e4ea2ae09d16314d30912a58ada9a5c4fdfedf9512d23326128ac08ac3d", + "sha256:6905238a754ceb7788a73f02b45637d820b2f5478b20fec82ea865e4f5d4d9f7", + "sha256:79dcf34b33e38ed1b17434693763301d7ccbd1c5860674a8f871bd15139e7837", + "sha256:7bb041dca0d784697af4646d3b62ba4a6b028276ae878e53f6b4f74ddd6db99f", + "sha256:7d5e026f8da0322b5662fa7a8e752b3fa2dac1c1cbc213c3d7ff9bdd0ab12395", + "sha256:9f50ea1132e2189d8dff0115ab75b65590a3e97de1e143795adb4ce317934995", + "sha256:a0c9c4a0771afc6919578cec71ce82a3e31e054904e7197deacbc9382671c41f", + "sha256:aadf7a02d947936ee418777e0247ea114f78aff0d0959461057cae8a04f20597", + "sha256:b5991d523eee14756f3c8d5df5231550ae8993e2286b8014e2fdea7156ed0959", + "sha256:bf21b7b230718a5f08bd32d5e4f1db7fc8788345c8aea1d155fc17852b3410f5", + "sha256:c45f8dff244b3c431b36e3224b6be4a127c6aca780853574c00faf99258041eb", + "sha256:c7ed6668cbbfcd231fa0dc1b137d3e40c04c7f786e626b405c62bcd5db5857e4", + "sha256:d7de8d330763c66663661a1ffd432274a2f92f07feeddd89ffd085b5744f85e7", + "sha256:e19cb1c6365fd6dc38a6eae2dcb691d7d83935c10215aef8e6c38edee3f77abd", + "sha256:e2af80566f43c85f5797365077fb64a393861a3730bd110971ab7a0c94e873e7" ], "index": "pypi", - "version": "==24.2.0" + "version": "==24.3.0" }, "certifi": { "hashes": [ @@ -832,11 +908,11 @@ }, "packaging": { "hashes": [ - "sha256:048fb0e9405036518eaaf48a55953c750c11e1a1b68e0dd1a9d62ed0c092cfc5", - "sha256:8c491190033a9af7e1d931d0b5dacc2ef47509b34dd0de67ed209b5203fc88c7" + "sha256:2ddfb553fdf02fb784c234c7ba6ccc288296ceabec964ad2eae3777778130bc5", + "sha256:eb82c5e3e56209074766e6885bb04b8c38a0c015d0a30036ebe7ece34c9989e9" ], "markers": "python_version >= '3.7'", - "version": "==23.2" + "version": "==24.0" }, "pathspec": { "hashes": [ @@ -864,11 +940,11 @@ }, "pytest": { "hashes": [ - "sha256:267f6563751877d772019b13aacbe4e860d73fe8f651f28112e9ac37de7513ae", - "sha256:3e4f16fe1c0a9dc9d9389161c127c3edc5d810c38d6793042fb81d9f48a59fca" + "sha256:2a8386cfc11fa9d2c50ee7b2a57e7d898ef90470a7a34c4b949ff59662bb78b7", + "sha256:ac978141a75948948817d360297b7aae0fcb9d6ff6bc9ec6d514b85d5a65c044" ], "index": "pypi", - "version": "==8.0.1" + "version": "==8.1.1" }, "requests": { "hashes": [ @@ -880,19 +956,11 @@ }, "requests-mock": { "hashes": [ - "sha256:ef10b572b489a5f28e09b708697208c4a3b2b89ef80a9f01584340ea357ec3c4", - "sha256:f7fae383f228633f6bececebdab236c478ace2284d6292c6e7e2867b9ab74d15" + "sha256:4e34f2a2752f0b78397fb414526605d95fcdeab021ac1f26d18960e7eb41f6a8", + "sha256:4f6fdf956de568e0bac99eee4ad96b391c602e614cc0ad33e7f5c72edd699e70" ], "index": "pypi", - "version": "==1.11.0" - }, - "six": { - "hashes": [ - "sha256:1e61c37477a1626458e36f7b1d82aa5c9b094fa4802892072e49de9c60c4c926", - "sha256:8abb2f1d86890a2dfb989f9a77cfcfd3e47c2a354b01111771326f8aa26e0254" - ], - "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2'", - "version": "==1.16.0" + "version": "==1.12.0" }, "tomli": { "hashes": [ @@ -904,11 +972,11 @@ }, "typing-extensions": { "hashes": [ - "sha256:23478f88c37f27d76ac8aee6c905017a143b0b1b886c3c9f66bc2fd94f9f5783", - "sha256:af72aea155e91adfc61c3ae9e0e342dbc0cba726d6cba4b6c72c1f34e47291cd" + "sha256:69b1a937c3a517342112fb4c6df7e72fc39a38e7891a5730ed4985b5214b5475", + "sha256:b0abd7c89e8fb96f98db18d86106ff1d90ab692004eb746cf6eda2682f91b3cb" ], "markers": "python_version < '3.11'", - "version": "==4.9.0" + "version": "==4.10.0" }, "urllib3": { "hashes": [ diff --git a/README.md b/README.md index a83bf0e..73600aa 100644 --- a/README.md +++ b/README.md @@ -16,13 +16,13 @@ Note: Previously, the repository comprised of self-contained scripts that could ### Reconciling files with metadata CSV ```bash -pipenv run dsaps --url $DSPACE_URL -e $DSPACE_EMAIL -p $DSPACE_PASSWORD reconcile -m -o /output -d -t +pipenv run dsaps --config-file $CONFIG_FILE --url $DSPACE_URL -e $DSPACE_EMAIL -p $DSPACE_PASSWORD reconcile -m -o /output -d ``` ### Creating a new collection within a DSpace community ```bash -pipenv run dsaps --url $DSPACE_URL -e $DSPACE_EMAIL -p $DSPACE_PASSWORD newcollection -c -n +pipenv run dsaps --config-file $CONFIG_FILE --url $DSPACE_URL -e $DSPACE_EMAIL -p $DSPACE_PASSWORD newcollection -c -n ``` ### Adding items to a DSpace collection @@ -30,13 +30,20 @@ pipenv run dsaps --url $DSPACE_URL -e $DSPACE_EMAIL -p $DSPACE_PASSWORD newcolle The command below shows `newcollection` and `additems` being run in conjunction with each other. Note that the invocation must call `newcollection` first. In practice, this is the command that is usually run: ```bash -pipenv run dsaps --url $DSPACE_URL -e $DSPACE_EMAIL -p $DSPACE_PASSWORD newcollection -c -n additems -m -f config/.json -d -t +pipenv run dsaps --config-file $CONFIG_FILE --url $DSPACE_URL -e $DSPACE_EMAIL -p $DSPACE_PASSWORD newcollection -c -n additems -m -d ``` ## Environment ### Required +```shell +# The file path to the source configuration JSON with settings for bitstream retrieval and field mappings. +CONFIG_FILE= +``` + +### Optional + ```shell # The url for the DSpace REST API DSPACE_URL= @@ -58,14 +65,16 @@ All CLI commands can be run with `pipenv run `. Usage: -c [OPTIONS] COMMAND1 [ARGS]... [COMMAND2 [ARGS]...]... Options: + --config-file TEXT File path to source configuration JSON with settings + for bitstream retrieval and field mappings. [required] --url TEXT The url for the DSpace REST API. Defaults to env var - DSPACE_URL if not set. [required] + DSPACE_URL if not set. -e, --email TEXT The email associated with the DSpace user account used for authentication. Defaults to env var DSPACE_EMAIL if - not set. [required] + not set. -p, --password TEXT The password associated with the DSpace user account used for authentication. Defaults to env var - DSPACE_PASSWORD if not set. [required] + DSPACE_PASSWORD if not set. --help Show this message and exit. Commands: @@ -87,10 +96,10 @@ Usage: -c reconcile [OPTIONS] file with a corresponding file in the content directory. * no_files.csv: File identifiers for entries in metadata CSV file - without a corresponding file in the content directory. + without a corresponding file in the content directory. * no_metadata.csv: File identifiers for files in the content directory - without a corresponding entry in the metadata CSV file. + without a corresponding entry in the metadata CSV file. * updated-.csv: Entries from the metadata CSV file with a corresponding file in the content directory. @@ -101,8 +110,6 @@ Options: -o, --output-directory TEXT The filepath where output files are written. -d, --content-directory TEXT The name of the S3 bucket containing files for DSpace uploads. [required] - -t, --file-type TEXT The file type for DSpace uploads (i.e., the - file extension, excluding the dot). --help Show this message and exit. ``` @@ -127,20 +134,18 @@ Usage: -c additems [OPTIONS] Add items to a DSpace collection. - The method relies on a CSV file with metadata for uploads, a JSON document - that maps metadata to a DSpace schema, and a directory containing the files - to be uploaded. + The updated metadata CSV file from running 'reconcile' is used for this + process. The method will first add an item to the specified DSpace + collection. The bitstreams (i.e., files) associated with the item are read + from the metadata CSV file, and uploaded to the newly created item on + DSpace. Options: - -m, --metadata-csv FILE The filepath to a CSV file containing metadata - for Dspace uploads. [required] - -f, --field-map FILE The filepath to a JSON document that maps - columns in the metadata CSV file to a DSpace - schema. [required] + -m, --metadata-csv FILE File path to a CSV file describing the + metadata and bitstreams for DSpace uploads. + [required] -d, --content-directory TEXT The name of the S3 bucket containing files for DSpace uploads. [required] - -t, --file-type TEXT The file type for DSpace uploads (i.e., the - file extension, excluding the dot). -r, --ingest-report Create ingest report for updating other systems. -c, --collection-handle TEXT The handle identifying a DSpace collection diff --git a/config/aspace.json b/config/aspace.json index 0244a2c..cb7d463 100644 --- a/config/aspace.json +++ b/config/aspace.json @@ -3,7 +3,7 @@ "bitstream_folders": [ "objects" ], - "id_regex": ".*-(.*?-.*)\\..*$" + "id_regex": ".*-(\\d*?-\\d*).*$" }, "mapping": { "item_identifier": { diff --git a/dsaps/cli.py b/dsaps/cli.py index 5bccc3f..f51ded7 100644 --- a/dsaps/cli.py +++ b/dsaps/cli.py @@ -1,6 +1,5 @@ import csv import datetime -import json import logging import os @@ -10,9 +9,8 @@ import click import structlog -from dsaps import helpers +from dsaps import dspace, helpers from dsaps.s3 import S3Client -from dsaps.dspace import DSpaceClient, DSpaceCollection logger = structlog.get_logger() @@ -28,7 +26,10 @@ def validate_path(ctx, param, value): @click.group(chain=True) @click.option( - "--config-file", required=True, help="File path to source configuration JSON." + "--config-file", + envvar="CONFIG_FILE", + required=True, + help="File path to source configuration JSON with settings for bitstream retrieval and field mappings.", ) @click.option( "--url", @@ -83,10 +84,11 @@ def main(ctx, config_file, url, email, password): logger.info("Running process") source_config = helpers.load_source_config(config_file) if url: - dspace_client = DSpaceClient(url) + dspace_client = dspace.DSpaceClient(url) dspace_client.authenticate(email, password) ctx.obj["dspace_client"] = dspace_client - ctx.obj["config"] = source_config + ctx.obj["source_config"] = source_config + logger.info("Initializing S3 client") ctx.obj["s3_client"] = S3Client.get_client() ctx.obj["start_time"] = perf_counter() @@ -97,14 +99,7 @@ def main(ctx, config_file, url, email, password): "--metadata-csv", required=True, type=click.Path(exists=True, file_okay=True, dir_okay=False), - help="The filepath to a CSV file containing metadata for Dspace uploads.", -) -@click.option( - "-f", - "--field-map", - required=True, - type=click.Path(exists=True, file_okay=True, dir_okay=False), - help="The filepath to a JSON document that maps columns in the metadata CSV file to a DSpace schema.", + help="File path to a CSV file describing the metadata and bitstreams for DSpace uploads.", ) @click.option( "-d", @@ -112,12 +107,6 @@ def main(ctx, config_file, url, email, password): required=True, help="The name of the S3 bucket containing files for DSpace uploads.", ) -@click.option( - "-t", - "--file-type", - help="The file type for DSpace uploads (i.e., the file extension, excluding the dot).", - default="*", -) @click.option( "-r", "--ingest-report", @@ -134,41 +123,51 @@ def main(ctx, config_file, url, email, password): def additems( ctx, metadata_csv, - field_map, content_directory, - file_type, ingest_report, collection_handle, ): """Add items to a DSpace collection. - The method relies on a CSV file with metadata for uploads, a JSON document that maps - metadata to a DSpace schema, and a directory containing the files to be uploaded. + The updated metadata CSV file from running 'reconcile' is used for this process. + The method will first add an item to the specified DSpace collection. The bitstreams + (i.e., files) associated with the item are read from the metadata CSV file, and + uploaded to the newly created item on DSpace. """ - s3_client = ctx.obj["s3_client"] + mapping = ctx.obj["source_config"]["mapping"] dspace_client = ctx.obj["dspace_client"] if "collection_uuid" not in ctx.obj and collection_handle is None: raise click.UsageError( - "collection_handle option must be used or " - "additems must be run after newcollection " - "command." + "Option '--collection-handle' must be used or " + "run 'additems' after 'newcollection' command." ) elif "collection_uuid" in ctx.obj: collection_uuid = ctx.obj["collection_uuid"] else: collection_uuid = dspace_client.get_uuid_from_handle(collection_handle) - with open(metadata_csv, "r") as csvfile, open(field_map, "r") as jsonfile: + + if metadata_csv is None: + raise click.UsageError("Option '--metadata-csv' must be used.") + + dspace_collection = dspace.Collection(uuid=collection_uuid) + + with open(metadata_csv, "r") as csvfile: metadata = csv.DictReader(csvfile) - mapping = json.load(jsonfile) - collection = DSpaceCollection.create_metadata_for_items_from_csv( - metadata, mapping + dspace_collection = dspace_collection.add_items(metadata, mapping) + + for item in dspace_collection.items: + logger.info(f"Posting item: {item}") + item_uuid, item_handle = dspace_client.post_item_to_collection( + collection_uuid, item ) - for item in collection.items: - item.bitstreams_in_directory(content_directory, s3_client, file_type) - collection.uuid = collection_uuid - for item in collection.post_items(dspace_client): - logger.info(item.file_identifier) + item.uuid = item_uuid + item.handle = item_handle + logger.info(f"Item posted: {item_uuid}") + for bitstream in item.bitstreams: + logger.info(f"Posting bitstream: {bitstream}") + dspace_client.post_bitstream(item.uuid, bitstream) + logger.info( "Total elapsed: %s", str(timedelta(seconds=perf_counter() - ctx.obj["start_time"])), @@ -192,7 +191,9 @@ def additems( def newcollection(ctx, community_handle, collection_name): """Create a new DSpace collection within a community.""" dspace_client = ctx.obj["dspace_client"] - collection_uuid = dspace_client.post_coll_to_comm(community_handle, collection_name) + collection_uuid = dspace_client.post_collection_to_community( + community_handle, collection_name + ) ctx.obj["collection_uuid"] = collection_uuid @@ -235,22 +236,21 @@ def reconcile(ctx, metadata_csv, output_directory, content_directory): * updated-.csv: Entries from the metadata CSV file with a corresponding file in the content directory. """ - source_settings = ctx.obj["config"]["settings"] - s3_client = ctx.obj["s3_client"] - files_dict = helpers.get_files_from_s3( + source_settings = ctx.obj["source_config"]["settings"] + bitstreams = helpers.get_files_from_s3( s3_path=content_directory, - s3_client=s3_client, + s3_client=ctx.obj["s3_client"], bitstream_folders=source_settings.get("bitstream_folders"), id_regex=source_settings["id_regex"], ) metadata_ids = helpers.create_metadata_id_list(metadata_csv) - metadata_matches = helpers.match_metadata_to_files(files_dict.keys(), metadata_ids) - file_matches = helpers.match_files_to_metadata(files_dict.keys(), metadata_ids) + metadata_matches = helpers.match_metadata_to_files(bitstreams.keys(), metadata_ids) + file_matches = helpers.match_files_to_metadata(bitstreams.keys(), metadata_ids) no_files = set(metadata_ids) - set(metadata_matches) - no_metadata = set(files_dict.keys()) - set(file_matches) + no_metadata = set(bitstreams.keys()) - set(file_matches) helpers.create_csv_from_list(no_metadata, f"{output_directory}no_metadata") helpers.create_csv_from_list(no_files, f"{output_directory}no_files") helpers.create_csv_from_list(metadata_matches, f"{output_directory}metadata_matches") helpers.update_metadata_csv( - metadata_csv, output_directory, metadata_matches, files_dict + metadata_csv, output_directory, metadata_matches, bitstreams ) diff --git a/dsaps/dspace.py b/dsaps/dspace.py index 1d16e6d..fc004f8 100644 --- a/dsaps/dspace.py +++ b/dsaps/dspace.py @@ -1,14 +1,15 @@ -import operator -from functools import partial +from __future__ import annotations +import ast import attr +import operator import requests -import smart_open import structlog +import smart_open + from attrs import field, define -Group = partial(attr.ib, default=[]) logger = structlog.get_logger() op = operator.attrgetter("name") @@ -20,7 +21,7 @@ def __init__(self, url): self.url = url.rstrip("/") self.cookies = None self.header = header - logger.info("Initializing client") + logger.info("Initializing DSpace client") def authenticate(self, email, password): """Authenticate user to DSpace API.""" @@ -72,10 +73,10 @@ def filtered_item_search(self, key, string, query_type, selected_collections="") def get_uuid_from_handle(self, handle): """Get UUID for an object based on its handle.""" hdl_endpoint = f"{self.url}/handle/{handle}" - rec_obj = requests.get( + record = requests.get( hdl_endpoint, headers=self.header, cookies=self.cookies, timeout=30 ).json() - return rec_obj["uuid"] + return record["uuid"] def get_record(self, uuid, record_type): """Get an individual record of a specified type.""" @@ -84,21 +85,22 @@ def get_record(self, uuid, record_type): url, headers=self.header, cookies=self.cookies, timeout=30 ).json() if record_type == "items": - rec_obj = self._populate_class_instance(DSpaceItem, record) + dspace_object = self._populate_class_instance(Item, record) elif record_type == "communities": - rec_obj = self._populate_class_instance(DSpaceCommunity, record) + dspace_object = self._populate_class_instance(Community, record) elif record_type == "collections": - rec_obj = self._populate_class_instance(DSpaceCollection, record) + dspace_object = self._populate_class_instance(Collection, record) else: logger.info("Invalid record type.") exit() - return rec_obj + return dspace_object def post_bitstream(self, item_uuid, bitstream): """Post a bitstream to a specified item and return the bitstream ID.""" - endpoint = f"{self.url}/items/{item_uuid}" f"/bitstreams?name={bitstream.name}" + endpoint = f"{self.url}/items/{item_uuid}/bitstreams?name={bitstream.name}" header_upload = {"accept": "application/json"} + logger.info(endpoint) with smart_open.open(bitstream.file_path, "rb") as data: post_response = requests.post( endpoint, @@ -113,7 +115,7 @@ def post_bitstream(self, item_uuid, bitstream): bitstream_uuid = response["uuid"] return bitstream_uuid - def post_coll_to_comm(self, comm_handle, coll_name): + def post_collection_to_community(self, comm_handle, coll_name): """Post a collection to a specified community.""" hdl_endpoint = f"{self.url}/handle/{comm_handle}" community = requests.get( @@ -150,24 +152,23 @@ def post_item_to_collection(self, collection_uuid, item): item_handle = post_response["handle"] return item_uuid, item_handle - def _populate_class_instance(self, class_type, rec_obj): + def _populate_class_instance(self, class_type, record): """Populate class instance with data from record.""" fields = [op(field) for field in attr.fields(class_type)] - kwargs = {k: v for k, v in rec_obj.items() if k in fields} - kwargs["objtype"] = rec_obj["type"] - if class_type == DSpaceCommunity: - collections = self._build_uuid_list(rec_obj, kwargs, "collections") - rec_obj["collections"] = collections - elif class_type == DSpaceCollection: - items = self._build_uuid_list(rec_obj, "items") - rec_obj["items"] = items - rec_obj = class_type(**kwargs) - return rec_obj - - def _build_uuid_list(self, rec_obj, children): + kwargs = {k: v for k, v in record.items() if k in fields} + kwargs["type"] = record["type"] + if class_type == Community: + collections = self._build_uuid_list(record, kwargs, "collections") + kwargs["collections"] = collections + elif class_type == Collection: + items = self._build_uuid_list(record, "items") + kwargs["items"] = items + return class_type(**kwargs) + + def _build_uuid_list(self, record, children): """Build list of the uuids of the object's children.""" child_list = [] - for child in rec_obj[children]: + for child in record[children]: child_list.append(child["uuid"]) return child_list @@ -186,82 +187,95 @@ class MetadataEntry: @define -class DSpaceObject: +class Object: uuid = field(default=None) name = field(default=None) handle = field(default=None) link = field(default=None) - objtype = field(default=None) + type = field(default=None) @define -class DSpaceCollection(DSpaceObject): - items = field(factory=list) +class Item(Object): + metadata = field(factory=list) + bitstreams = field(factory=list) + item_identifier = field(default=None) + source_system_identifier = field(default=None) - def post_items(self, client): - """Post items to collection.""" - for item in self.items: - logger.info(f"Posting item: {item}") - item_uuid, item_handle = client.post_item_to_collection(self.uuid, item) - item.uuid = item_uuid - item.handle = item_handle - logger.info(f"Item posted: {item_uuid}") - for bitstream in item.bitstreams: - bitstream_uuid = client.post_bitstream(item_uuid, bitstream) - bitstream.uuid = bitstream_uuid - logger.info(f"Bitstream posted: {bitstream_uuid}") - yield item + @classmethod + def create(cls, record, mapping) -> Item: + return cls( + metadata=cls.get_metadata(record, mapping), + bitstreams=cls.get_bitstreams(record), + **cls.get_ids(record, mapping), + ) @classmethod - def create_metadata_for_items_from_csv(cls, csv_reader, field_map): - """Create metadata for the collection's items based on a CSV and a JSON mapping - field map.""" - items = [DSpaceItem.metadata_from_csv_row(row, field_map) for row in csv_reader] - return cls(items=items) + def get_bitstreams(cls, record) -> list: + if _bitstreams := record.get("bitstreams"): + bitstreams = [] + for file_path in ast.literal_eval(_bitstreams): + file_name = file_path.split("/")[-1] + bitstreams.append(Bitstream(name=file_name, file_path=file_path)) + return bitstreams + @classmethod + def get_ids(cls, record, mapping) -> dict: + ids = {} + if item_id_mapping := mapping.get("item_identifier"): + ids["item_identifier"] = record.get(item_id_mapping["csv_field_name"]) + if source_system_id_mapping := mapping.get("source_system_identifier"): + ids["source_system_identifier"] = record.get( + source_system_id_mapping["csv_field_name"] + ) + return ids -@define -class DSpaceCommunity(DSpaceObject): - collections = field(default=None) + @classmethod + def get_metadata(cls, record, mapping) -> list: + """Create metadata for an item based on a CSV row and a JSON mapping field map.""" + metadata = [] + for field_name, field_mapping in mapping.items(): + if field_name not in ["item_identifier", "source_system_identifier"]: + field_value = record[field_mapping["csv_field_name"]] -@define -class DSpaceItem(DSpaceObject): - metadata = field(factory=list) - bitstreams = field(factory=list) - file_identifier = field(default=None) - source_system_identifier = field(default=None) + if field_value: + delimiter = field_mapping["delimiter"] + language = field_mapping["language"] + if delimiter: + metadata.extend( + [ + MetadataEntry( + key=field_name, + value=value, + language=language, + ) + for value in field_value.split(delimiter) + ] + ) + else: + metadata.append( + MetadataEntry( + key=field_name, + value=field_value, + language=language, + ) + ) + return metadata - def bitstreams_in_directory(self, directory, s3_client, file_type=None): - """Create a list of bitstreams from the specified directory and sort the list.""" - pass + +@define +class Collection(Object): + items = field(factory=list) @classmethod - def metadata_from_csv_row(cls, row, field_map): - """Create metadata for an item based on a CSV row and a JSON mapping field map.""" - metadata = [] - for f in field_map: - field = row[field_map[f]["csv_field_name"]] - if field != "": - if f == "file_identifier": - file_identifier = field - continue # file_identifier is not included in DSpace metadata - if f == "source_system_identifier": - # source_system_identifier = field - continue # source_system_identifier is not included in DSpace - delimiter = field_map[f]["delimiter"] - language = field_map[f]["language"] - if delimiter: - metadata.extend( - [ - MetadataEntry(key=f, value=v, language=language) - for v in field.split(delimiter) - ] - ) - else: - metadata.append(MetadataEntry(key=f, value=field, language=language)) - return cls( - metadata=metadata, - file_identifier=file_identifier, - # source_system_identifier=source_system_identifier, - ) + def add_items(cls, csv_reader, field_map) -> Collection: + """Create metadata for the collection's items based on a CSV and a JSON mapping + field map.""" + items = [Item.create(row, field_map) for row in csv_reader] + return cls(items=items) + + +@define +class Community(Object): + collections = field(default=None) diff --git a/dsaps/helpers.py b/dsaps/helpers.py index fb0778e..c98e0db 100644 --- a/dsaps/helpers.py +++ b/dsaps/helpers.py @@ -1,3 +1,4 @@ +import ast import csv import os import re @@ -5,6 +6,7 @@ import smart_open + S3_BUCKET_REGEX = re.compile(r"^([^\/]*)") S3_PREFIX_REGEX = re.compile(r"(?<=\/)(.*)") @@ -47,14 +49,15 @@ def get_files_from_s3( """ files = {} s3_path = s3_path.removeprefix("s3://") - operation_parameters = {"Bucket": parse_value_from_text(s3_path, S3_BUCKET_REGEX)} + bucket = parse_value_from_text(s3_path, S3_BUCKET_REGEX) + operation_parameters = {"Bucket": bucket} if prefix := parse_value_from_text(s3_path, S3_PREFIX_REGEX): operation_parameters.update({"Prefix": prefix}) paginator = s3_client.get_paginator("list_objects_v2") for page in paginator.paginate(**operation_parameters): for file in page["Contents"]: - file_path = file["Key"] + file_path = f"s3://{bucket}/{file['Key']}" file_name = file_path.split("/")[-1] if bitstream_folders: # if the object is not stored in any of the folders specified @@ -62,8 +65,8 @@ def get_files_from_s3( if not [folder for folder in bitstream_folders if folder in file_path]: continue item_identifier = parse_value_from_text(file_name, id_regex) - files.setdefault(item_identifier, []).append(file["Key"]) - return files + files.setdefault(item_identifier, []).append(file_path) + return dict(sorted(files.items())) def parse_value_from_text( @@ -94,7 +97,7 @@ def create_metadata_id_list(metadata_csv): with open(metadata_csv) as csvfile: reader = csv.DictReader(csvfile) metadata_ids = [ - row["file_identifier"] for row in reader if row["file_identifier"] != "" + row["item_identifier"] for row in reader if row["item_identifier"] != "" ] return metadata_ids @@ -131,6 +134,6 @@ def update_metadata_csv(metadata_csv, output_directory, metadata_matches, files_ writer = csv.DictWriter(updated_csv, fieldnames=fieldnames) writer.writeheader() for row in reader: - if row["file_identifier"] in metadata_matches: - row["bitstreams"] = files_dict[row["file_identifier"]] + if row["item_identifier"] in metadata_matches: + row["bitstreams"] = files_dict[row["item_identifier"]] writer.writerow(row) diff --git a/tests/conftest.py b/tests/conftest.py index 232d117..5209596 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -35,6 +35,13 @@ def source_metadata_csv(): yield reader +@pytest.fixture +def source_metadata_csv_with_bitstreams(): + with open("tests/fixtures/updated-source_metadata.csv") as file: + reader = csv.DictReader(file) + yield reader + + @pytest.fixture() def dspace_client(): dspace_client = dspace.DSpaceClient("mock://example.com/") diff --git a/tests/test_helpers.py b/tests/test_helpers.py index 34c12d3..6d8ceb2 100644 --- a/tests/test_helpers.py +++ b/tests/test_helpers.py @@ -11,7 +11,7 @@ parse_value_from_text, update_metadata_csv, ) -from dsaps.dspace import DSpaceItem +from dsaps import dspace REGEX_ID_BETWEEN_UNDERSCORES = "_(.*)_" REGEX_ID_BEFORE_UNDERSCORES = "(.*?)_" @@ -123,7 +123,7 @@ def test_get_files_from_s3_without_bitstream_folders(mocked_s3_bucket, s3_client def test_create_ingest_report(runner, output_dir): """Test create_ingest_report function.""" file_name = "ingest_report.csv" - items = [DSpaceItem(source_system_identifier="/repo/0/ao/123", handle="111.1111")] + items = [dspace.Item(source_system_identifier="/repo/0/ao/123", handle="111.1111")] create_ingest_report(items, f"{output_dir}{file_name}") with open(f"{output_dir}{file_name}") as csvfile: reader = csv.DictReader(csvfile) @@ -157,21 +157,24 @@ def test_match_metadata_to_files(): assert "test" in file_matches -def test_update_metadata_csv(output_dir, mocked_s3_bucket_bitstreams): +def test_update_metadata_csv( + output_dir, mocked_s3_bucket_bitstreams, source_metadata_csv_with_bitstreams +): """Test update_metadata_csv function.""" + updated_records = [] + expected_records = [] update_metadata_csv( metadata_csv="tests/fixtures/source_metadata.csv", output_directory=output_dir, - metadata_matches=["001"], + metadata_matches=["001", "002", "003", "004", "005"], files_dict=mocked_s3_bucket_bitstreams, ) with open(f"{output_dir}/updated-source_metadata.csv") as csvfile: reader = csv.DictReader(csvfile) - record = next(reader) - assert record is not None - assert record == { - "item_identifier": "001", - "title": "Title 1", - "author": "May Smith", - "bitstreams": "['s3://mocked-bucket/one-to-one/aaaa_001_01.pdf']", - } + for row in reader: + updated_records.append(row) + + for row in source_metadata_csv_with_bitstreams: + expected_records.append(row) + + assert updated_records == expected_records diff --git a/tests/test_models.py b/tests/test_models.py index 7fc9a53..2058d70 100644 --- a/tests/test_models.py +++ b/tests/test_models.py @@ -1,11 +1,10 @@ import attr from moto import mock_aws -from dsaps.dspace import Bitstream, DSpaceCollection, DSpaceItem, MetadataEntry +from dsaps.dspace import Bitstream, Collection, Item, MetadataEntry -def test_authenticate(dspace_client): - """Test authenticate method.""" +def test_dspace_client_authenticate(dspace_client): email = "test@test.mock" password = "1234" dspace_client.authenticate(email, password) @@ -14,7 +13,6 @@ def test_authenticate(dspace_client): def test_filtered_item_search(dspace_client): - """Test filtered_item_search method.""" item_links = dspace_client.filtered_item_search( key="dc.title", string="test", query_type="contains", selected_collections="" ) @@ -22,19 +20,16 @@ def test_filtered_item_search(dspace_client): def test_get_uuid_from_handle(dspace_client): - """Test get_uuid_from_handle method.""" id = dspace_client.get_uuid_from_handle("111.1111") assert id == "a1b2" def test_get_record(dspace_client): - """Test get_record method.""" - rec_obj = dspace_client.get_record("123", "items") - assert attr.asdict(rec_obj)["metadata"] == {"title": "Sample title"} + dspace_item = dspace_client.get_record("123", "items") + assert attr.asdict(dspace_item)["metadata"] == {"title": "Sample title"} def test_post_bitstream(dspace_client, mocked_s3_bucket): - """Test post_bitstream method.""" item_uuid = "e5f6" bitstream = Bitstream( name="aaaa_001_01.pdf", file_path="s3://mocked-bucket/one-to-one/aaaa_001_01.pdf" @@ -43,18 +38,16 @@ def test_post_bitstream(dspace_client, mocked_s3_bucket): assert bit_uuid == "g7h8" -def test_post_coll_to_comm(dspace_client): - """Test post_coll_to_comm method.""" +def test_post_collection_to_community(dspace_client): comm_handle = "111.1111" coll_name = "Test Collection" - coll_uuid = dspace_client.post_coll_to_comm(comm_handle, coll_name) + coll_uuid = dspace_client.post_collection_to_community(comm_handle, coll_name) assert coll_uuid == "c3d4" @mock_aws def test_post_item_to_collection(dspace_client, mocked_s3_bucket): - """Test post_item_to_collection method.""" - item = DSpaceItem() + item = Item() item.bitstreams = [ Bitstream(name="aaaa_001_01.pdf", file_path="s3://mocked-bucket/aaaa_001_01.pdf") ] @@ -68,54 +61,70 @@ def test_post_item_to_collection(dspace_client, mocked_s3_bucket): assert item_handle == "222.2222" -def test__populate_class_instance(dspace_client): - """Test _populate_class_instance method.""" - class_type = DSpaceCollection - rec_obj = {"name": "Test title", "type": "collection", "items": []} - rec_obj = dspace_client._populate_class_instance(class_type, rec_obj) - assert type(rec_obj) is class_type - assert rec_obj.name == "Test title" +def test_populate_class_instance(dspace_client): + class_type = Collection + dspace_collection = {"name": "Test title", "type": "collection", "items": []} + dspace_collection = dspace_client._populate_class_instance( + class_type, dspace_collection + ) + assert type(dspace_collection) is class_type + assert dspace_collection.name == "Test title" -def test__build_uuid_list(dspace_client): - """Test _build_uuid_list method.""" - rec_obj = {"items": [{"uuid": "1234"}]} +def test_build_uuid_list(dspace_client): + dspace_items = {"items": [{"uuid": "1234"}]} children = "items" - child_list = dspace_client._build_uuid_list(rec_obj, children) + child_list = dspace_client._build_uuid_list(dspace_items, children) assert "1234" in child_list -def test_collection_create_metadata_for_items_from_csv( - source_metadata_csv, source_config -): - collection = DSpaceCollection.create_metadata_for_items_from_csv( - source_metadata_csv, source_config["mapping"] - ) +def test_collection_add_items(source_metadata_csv, source_config): + collection = Collection.add_items(source_metadata_csv, source_config["mapping"]) assert len(collection.items) == 5 -# @mock_aws -# def test_collection_post_items( -# mocked_s3_bucket, -# dspace_client, -# source_metadata_csv, -# source_config, -# ): -# collection = DSpaceCollection(uuid="c3d4") -# collection.create_metadata_for_items_from_csv( -# source_metadata_csv, source_config["mapping"] -# ) -# items = collection.post_items(dspace_client) -# print(f"item: {next(items)}") -# for item in items: -# assert item.handle != "222.2222" -# assert item.uuid != "e5f6" - - -def test_item_metadata_from_csv_row(source_metadata_csv, source_config): +def test_item_create(source_metadata_csv_with_bitstreams, source_config): + record = next(source_metadata_csv_with_bitstreams) + assert attr.asdict(Item.create(record, source_config["mapping"])) == { + "uuid": None, + "name": None, + "handle": None, + "link": None, + "type": None, + "metadata": [ + {"key": "dc.title", "value": "Title 1", "language": "en_US"}, + {"key": "dc.contributor.author", "value": "May Smith", "language": None}, + ], + "bitstreams": [ + { + "name": "aaaa_001_01.pdf", + "file_path": "s3://mocked-bucket/one-to-one/aaaa_001_01.pdf", + } + ], + "item_identifier": "001", + "source_system_identifier": None, + } + + +def test_item_get_ids(source_metadata_csv, source_config): + record = next(source_metadata_csv) + assert Item.get_ids(record, source_config["mapping"]) == {"item_identifier": "001"} + + +def test_item_get_bitstreams(source_metadata_csv_with_bitstreams, source_config): + record = next(source_metadata_csv_with_bitstreams) + assert Item.get_bitstreams(record) == [ + Bitstream( + name="aaaa_001_01.pdf", + file_path="s3://mocked-bucket/one-to-one/aaaa_001_01.pdf", + ) + ] + + +def test_item_get_metadata(source_metadata_csv, source_config): record = next(source_metadata_csv) - item = DSpaceItem.metadata_from_csv_row(record, source_config["mapping"]) - assert attr.asdict(item)["metadata"] == [ + metadata = Item.get_metadata(record, source_config["mapping"]) + assert [attr.asdict(m) for m in metadata] == [ {"key": "dc.title", "value": "Title 1", "language": "en_US"}, {"key": "dc.contributor.author", "value": "May Smith", "language": None}, ] From dd2d31e85ed2a6853516dbeaf083bfe619e76a36 Mon Sep 17 00:00:00 2001 From: Eric Hanson Date: Wed, 10 Apr 2024 13:13:09 -0400 Subject: [PATCH 17/18] Updates for DSpace@MIT uploads * Refactor parse_value_from_text method * Refactor get_files_from_s3 method to sort bitstreams * Update timeouts for POST requests to account for slow responses * Add dspacemit.json config * Remove outdated aspace_mapping.json --- config/aspace_mapping.json | 37 ----------------- config/dspacemit.json | 83 ++++++++++++++++++++++++++++++++++++++ dsaps/dspace.py | 8 ++-- dsaps/helpers.py | 8 +++- 4 files changed, 93 insertions(+), 43 deletions(-) delete mode 100644 config/aspace_mapping.json create mode 100644 config/dspacemit.json diff --git a/config/aspace_mapping.json b/config/aspace_mapping.json deleted file mode 100644 index ea41e3b..0000000 --- a/config/aspace_mapping.json +++ /dev/null @@ -1,37 +0,0 @@ -{ - "file_identifier": { - "csv_field_name": "file_identifier", - "language": null, - "delimiter": "" - }, - "dc.title": { - "csv_field_name": "title", - "language": "en_US", - "delimiter": "" - }, - "source_system_identifier": { - "csv_field_name": "uri", - "language": null, - "delimiter": "" - }, - "dc.contributor.author": { - "csv_field_name": "author", - "language": null, - "delimiter": "|" - }, - "dc.description": { - "csv_field_name": "description", - "language": "en_US", - "delimiter": "" - }, - "dc.rights": { - "csv_field_name": "rights_statement", - "language": "en_US", - "delimiter": "" - }, - "dc.rights.uri": { - "csv_field_name": "rights_uri", - "language": null, - "delimiter": "" - } -} \ No newline at end of file diff --git a/config/dspacemit.json b/config/dspacemit.json new file mode 100644 index 0000000..a2a5077 --- /dev/null +++ b/config/dspacemit.json @@ -0,0 +1,83 @@ +{ + "settings": { + "bitstream_folders": [], + "id_regex": "^.*$" + }, + "mapping": { + "item_identifier": { + "csv_field_name": "item_identifier", + "language": null, + "delimiter": "" + }, + "dc.publisher": { + "csv_field_name": "dc.publisher", + "language": "en_US", + "delimiter": "" + }, + "dc.identifier.mitlicense": { + "csv_field_name": "dc.identifier.mitlicense", + "language": "en_US", + "delimiter": "" + }, + "dc.eprint.version": { + "csv_field_name": "dc.eprint.version", + "language": "en_US", + "delimiter": "" + }, + "dc.type": { + "csv_field_name": "dc.type", + "language": "en_US", + "delimiter": "" + }, + "dc.source": { + "csv_field_name": "dc.source", + "language": "en_US", + "delimiter": "" + }, + "dc.contributor.author": { + "csv_field_name": "dc.contributor.author", + "language": "en_US", + "delimiter": "|" + }, + "dc.relation.isversionof": { + "csv_field_name": "dc.relation.isversionof", + "language": "", + "delimiter": "" + }, + "dc.title": { + "csv_field_name": "dc.title", + "language": "en_US", + "delimiter": "" + }, + "dc.relation.journal": { + "csv_field_name": "dc.relation.journal", + "language": "", + "delimiter": "" + }, + "dc.identifier.issn": { + "csv_field_name": "dc.identifier.issn", + "language": "", + "delimiter": "" + }, + "dc.date.issued": { + "csv_field_name": "dc.date.issued", + "language": "", + "delimiter": "" + }, + "dc.rights": { + "csv_field_name": "dc.rights", + "language": "en_US", + "delimiter": "" + }, + "dc.rights.uri": { + "csv_field_name": "dc.rights.uri", + "language": "", + "delimiter": "" + }, + "dc.description.sponsorship": { + "csv_field_name": "dc.description.sponsorship", + "language": "en_US", + "delimiter": "" + } + } +} \ No newline at end of file diff --git a/dsaps/dspace.py b/dsaps/dspace.py index fc004f8..1cbeeef 100644 --- a/dsaps/dspace.py +++ b/dsaps/dspace.py @@ -28,7 +28,7 @@ def authenticate(self, email, password): header = self.header data = {"email": email, "password": password} session = requests.post( - f"{self.url}/login", headers=header, params=data, timeout=30 + f"{self.url}/login", headers=header, params=data, timeout=120 ).cookies["JSESSIONID"] cookies = {"JSESSIONID": session} status = requests.get( @@ -107,7 +107,7 @@ def post_bitstream(self, item_uuid, bitstream): headers=header_upload, cookies=self.cookies, data=data, - timeout=30, + timeout=120, ) logger.info(f"Bitstream POST status: {post_response}") response = post_response.json() @@ -128,7 +128,7 @@ def post_collection_to_community(self, comm_handle, coll_name): headers=self.header, cookies=self.cookies, json={"name": coll_name}, - timeout=30, + timeout=120, ).json() coll_uuid = coll_uuid["uuid"] logger.info(f"Collection posted: {coll_uuid}") @@ -143,7 +143,7 @@ def post_item_to_collection(self, collection_uuid, item): headers=self.header, cookies=self.cookies, json={"metadata": attr.asdict(item)["metadata"]}, - timeout=30, + timeout=120, ) logger.info(f"Item POST status: {post_resp}") post_response = post_resp.json() diff --git a/dsaps/helpers.py b/dsaps/helpers.py index c98e0db..a71d5be 100644 --- a/dsaps/helpers.py +++ b/dsaps/helpers.py @@ -66,6 +66,10 @@ def get_files_from_s3( continue item_identifier = parse_value_from_text(file_name, id_regex) files.setdefault(item_identifier, []).append(file_path) + for key, value in files.items(): + files[key] = sorted( + value, key=lambda x: x.split(parse_value_from_text(x, id_regex))[1] + ) return dict(sorted(files.items())) @@ -74,8 +78,8 @@ def parse_value_from_text( regex: str, ): pattern = re.compile(regex) - if match := pattern.search(text): - return match.group(1) + if matches := pattern.findall(text): + return matches[0] def create_ingest_report(items, file_name): From 332dd81a729d59800825473e228194e7a897538a Mon Sep 17 00:00:00 2001 From: Eric Hanson Date: Fri, 12 Apr 2024 11:25:35 -0400 Subject: [PATCH 18/18] Update Pipfile.lock --- Pipfile.lock | 312 +++++++++++++++++++++++++++++++++------------------ 1 file changed, 201 insertions(+), 111 deletions(-) diff --git a/Pipfile.lock b/Pipfile.lock index e51e3a6..388ceb2 100644 --- a/Pipfile.lock +++ b/Pipfile.lock @@ -22,23 +22,25 @@ "sha256:99b87a485a5820b23b879f04c2305b44b951b502fd64be915879d77a7e8fc6f1" ], "index": "pypi", + "markers": "python_version >= '3.7'", "version": "==23.2.0" }, "boto3": { "hashes": [ - "sha256:a33585ef0d811ee0dffd92a96108344997a3059262c57349be0761d7885f6ae7", - "sha256:cbfabd99c113bbb1708c2892e864b6dd739593b97a76fbb2e090a7d965b63b82" + "sha256:33cf93f6de5176f1188c923f4de1ae149ed723b89ed12e434f2b2f628491769e", + "sha256:9733ce811bd82feab506ad9309e375a79cabe8c6149061971c17754ce8997551" ], "index": "pypi", - "version": "==1.34.72" + "markers": "python_version >= '3.8'", + "version": "==1.34.83" }, "botocore": { "hashes": [ - "sha256:342edb6f91d5839e790411822fc39f9c712c87cdaa7f3b1999f50b1ca16c4a14", - "sha256:a6b92735a73c19a7e540d77320420da3af3f32c91fa661c738c0b8c9f912d782" + "sha256:0a3fbbe018416aeefa8978454fb0b8129adbaf556647b72269bf02e4bf1f4161", + "sha256:0f302aa76283d4df62b4fbb6d3d20115c1a8957fc02171257fc93904d69d5636" ], "markers": "python_version >= '3.8'", - "version": "==1.34.72" + "version": "==1.34.83" }, "certifi": { "hashes": [ @@ -208,6 +210,7 @@ "sha256:ca9853ad459e787e2192211578cc907e7594e294c7ccc834310722b41b9ca6de" ], "index": "pypi", + "markers": "python_version >= '3.7'", "version": "==8.1.7" }, "cryptography": { @@ -250,11 +253,11 @@ }, "idna": { "hashes": [ - "sha256:9ecdbbd083b06798ae1e86adcbfe8ab1479cf864e4ee30fe4e46a003d12491ca", - "sha256:c05567e9c24a6b9faaa835c4821bad0590fbb9d5779e7caa6e1cc4978e7eb24f" + "sha256:028ff3aadf0609c1fd278d8ea3089299412a7a8b9bd005dd08b9f8285bcb5cfc", + "sha256:82fee1fc78add43492d3a1898bfa6d8a904cc97d8427f683ed8e798d07761aa0" ], "markers": "python_version >= '3.5'", - "version": "==3.6" + "version": "==3.7" }, "jinja2": { "hashes": [ @@ -274,87 +277,165 @@ }, "lxml": { "hashes": [ - "sha256:13521a321a25c641b9ea127ef478b580b5ec82aa2e9fc076c86169d161798b01", - "sha256:14deca1460b4b0f6b01f1ddc9557704e8b365f55c63070463f6c18619ebf964f", - "sha256:16018f7099245157564d7148165132c70adb272fb5a17c048ba70d9cc542a1a1", - "sha256:16dd953fb719f0ffc5bc067428fc9e88f599e15723a85618c45847c96f11f431", - "sha256:19a1bc898ae9f06bccb7c3e1dfd73897ecbbd2c96afe9095a6026016e5ca97b8", - "sha256:1ad17c20e3666c035db502c78b86e58ff6b5991906e55bdbef94977700c72623", - "sha256:22b7ee4c35f374e2c20337a95502057964d7e35b996b1c667b5c65c567d2252a", - "sha256:24ef5a4631c0b6cceaf2dbca21687e29725b7c4e171f33a8f8ce23c12558ded1", - "sha256:25663d6e99659544ee8fe1b89b1a8c0aaa5e34b103fab124b17fa958c4a324a6", - "sha256:262bc5f512a66b527d026518507e78c2f9c2bd9eb5c8aeeb9f0eb43fcb69dc67", - "sha256:280f3edf15c2a967d923bcfb1f8f15337ad36f93525828b40a0f9d6c2ad24890", - "sha256:2ad3a8ce9e8a767131061a22cd28fdffa3cd2dc193f399ff7b81777f3520e372", - "sha256:2befa20a13f1a75c751f47e00929fb3433d67eb9923c2c0b364de449121f447c", - "sha256:2f37c6d7106a9d6f0708d4e164b707037b7380fcd0b04c5bd9cae1fb46a856fb", - "sha256:304128394c9c22b6569eba2a6d98392b56fbdfbad58f83ea702530be80d0f9df", - "sha256:342e95bddec3a698ac24378d61996b3ee5ba9acfeb253986002ac53c9a5f6f84", - "sha256:3aeca824b38ca78d9ee2ab82bd9883083d0492d9d17df065ba3b94e88e4d7ee6", - "sha256:3d184e0d5c918cff04cdde9dbdf9600e960161d773666958c9d7b565ccc60c45", - "sha256:3e3898ae2b58eeafedfe99e542a17859017d72d7f6a63de0f04f99c2cb125936", - "sha256:3eea6ed6e6c918e468e693c41ef07f3c3acc310b70ddd9cc72d9ef84bc9564ca", - "sha256:3f14a4fb1c1c402a22e6a341a24c1341b4a3def81b41cd354386dcb795f83897", - "sha256:436a943c2900bb98123b06437cdd30580a61340fbdb7b28aaf345a459c19046a", - "sha256:4946e7f59b7b6a9e27bef34422f645e9a368cb2be11bf1ef3cafc39a1f6ba68d", - "sha256:49a9b4af45e8b925e1cd6f3b15bbba2c81e7dba6dce170c677c9cda547411e14", - "sha256:4f8b0c78e7aac24979ef09b7f50da871c2de2def043d468c4b41f512d831e912", - "sha256:52427a7eadc98f9e62cb1368a5079ae826f94f05755d2d567d93ee1bc3ceb354", - "sha256:5e53d7e6a98b64fe54775d23a7c669763451340c3d44ad5e3a3b48a1efbdc96f", - "sha256:5fcfbebdb0c5d8d18b84118842f31965d59ee3e66996ac842e21f957eb76138c", - "sha256:601f4a75797d7a770daed8b42b97cd1bb1ba18bd51a9382077a6a247a12aa38d", - "sha256:61c5a7edbd7c695e54fca029ceb351fc45cd8860119a0f83e48be44e1c464862", - "sha256:6a2a2c724d97c1eb8cf966b16ca2915566a4904b9aad2ed9a09c748ffe14f969", - "sha256:6d48fc57e7c1e3df57be5ae8614bab6d4e7b60f65c5457915c26892c41afc59e", - "sha256:6f11b77ec0979f7e4dc5ae081325a2946f1fe424148d3945f943ceaede98adb8", - "sha256:704f5572ff473a5f897745abebc6df40f22d4133c1e0a1f124e4f2bd3330ff7e", - "sha256:725e171e0b99a66ec8605ac77fa12239dbe061482ac854d25720e2294652eeaa", - "sha256:7cfced4a069003d8913408e10ca8ed092c49a7f6cefee9bb74b6b3e860683b45", - "sha256:7ec465e6549ed97e9f1e5ed51c657c9ede767bc1c11552f7f4d022c4df4a977a", - "sha256:82bddf0e72cb2af3cbba7cec1d2fd11fda0de6be8f4492223d4a268713ef2147", - "sha256:82cd34f1081ae4ea2ede3d52f71b7be313756e99b4b5f829f89b12da552d3aa3", - "sha256:843b9c835580d52828d8f69ea4302537337a21e6b4f1ec711a52241ba4a824f3", - "sha256:877efb968c3d7eb2dad540b6cabf2f1d3c0fbf4b2d309a3c141f79c7e0061324", - "sha256:8b9f19df998761babaa7f09e6bc169294eefafd6149aaa272081cbddc7ba4ca3", - "sha256:8cf5877f7ed384dabfdcc37922c3191bf27e55b498fecece9fd5c2c7aaa34c33", - "sha256:8d2900b7f5318bc7ad8631d3d40190b95ef2aa8cc59473b73b294e4a55e9f30f", - "sha256:8d7b4beebb178e9183138f552238f7e6613162a42164233e2bda00cb3afac58f", - "sha256:8f52fe6859b9db71ee609b0c0a70fea5f1e71c3462ecf144ca800d3f434f0764", - "sha256:98f3f020a2b736566c707c8e034945c02aa94e124c24f77ca097c446f81b01f1", - "sha256:9aa543980ab1fbf1720969af1d99095a548ea42e00361e727c58a40832439114", - "sha256:9b99f564659cfa704a2dd82d0684207b1aadf7d02d33e54845f9fc78e06b7581", - "sha256:9bcf86dfc8ff3e992fed847c077bd875d9e0ba2fa25d859c3a0f0f76f07f0c8d", - "sha256:9bd0ae7cc2b85320abd5e0abad5ccee5564ed5f0cc90245d2f9a8ef330a8deae", - "sha256:9d3c0f8567ffe7502d969c2c1b809892dc793b5d0665f602aad19895f8d508da", - "sha256:9e5ac3437746189a9b4121db2a7b86056ac8786b12e88838696899328fc44bb2", - "sha256:a36c506e5f8aeb40680491d39ed94670487ce6614b9d27cabe45d94cd5d63e1e", - "sha256:a5ab722ae5a873d8dcee1f5f45ddd93c34210aed44ff2dc643b5025981908cda", - "sha256:a96f02ba1bcd330807fc060ed91d1f7a20853da6dd449e5da4b09bfcc08fdcf5", - "sha256:acb6b2f96f60f70e7f34efe0c3ea34ca63f19ca63ce90019c6cbca6b676e81fa", - "sha256:ae15347a88cf8af0949a9872b57a320d2605ae069bcdf047677318bc0bba45b1", - "sha256:af8920ce4a55ff41167ddbc20077f5698c2e710ad3353d32a07d3264f3a2021e", - "sha256:afd825e30f8d1f521713a5669b63657bcfe5980a916c95855060048b88e1adb7", - "sha256:b21b4031b53d25b0858d4e124f2f9131ffc1530431c6d1321805c90da78388d1", - "sha256:b4b68c961b5cc402cbd99cca5eb2547e46ce77260eb705f4d117fd9c3f932b95", - "sha256:b66aa6357b265670bb574f050ffceefb98549c721cf28351b748be1ef9577d93", - "sha256:b9e240ae0ba96477682aa87899d94ddec1cc7926f9df29b1dd57b39e797d5ab5", - "sha256:bc64d1b1dab08f679fb89c368f4c05693f58a9faf744c4d390d7ed1d8223869b", - "sha256:bf8443781533b8d37b295016a4b53c1494fa9a03573c09ca5104550c138d5c05", - "sha256:c26aab6ea9c54d3bed716b8851c8bfc40cb249b8e9880e250d1eddde9f709bf5", - "sha256:c3cd1fc1dc7c376c54440aeaaa0dcc803d2126732ff5c6b68ccd619f2e64be4f", - "sha256:c7257171bb8d4432fe9d6fdde4d55fdbe663a63636a17f7f9aaba9bcb3153ad7", - "sha256:d42e3a3fc18acc88b838efded0e6ec3edf3e328a58c68fbd36a7263a874906c8", - "sha256:d74fcaf87132ffc0447b3c685a9f862ffb5b43e70ea6beec2fb8057d5d2a1fea", - "sha256:d8c1d679df4361408b628f42b26a5d62bd3e9ba7f0c0e7969f925021554755aa", - "sha256:e856c1c7255c739434489ec9c8aa9cdf5179785d10ff20add308b5d673bed5cd", - "sha256:eac68f96539b32fce2c9b47eb7c25bb2582bdaf1bbb360d25f564ee9e04c542b", - "sha256:ed7326563024b6e91fef6b6c7a1a2ff0a71b97793ac33dbbcf38f6005e51ff6e", - "sha256:ed8c3d2cd329bf779b7ed38db176738f3f8be637bb395ce9629fc76f78afe3d4", - "sha256:f4c9bda132ad108b387c33fabfea47866af87f4ea6ffb79418004f0521e63204", - "sha256:f643ffd2669ffd4b5a3e9b41c909b72b2a1d5e4915da90a77e119b8d48ce867a" + "sha256:04ab5415bf6c86e0518d57240a96c4d1fcfc3cb370bb2ac2a732b67f579e5a04", + "sha256:057cdc6b86ab732cf361f8b4d8af87cf195a1f6dc5b0ff3de2dced242c2015e0", + "sha256:058a1308914f20784c9f4674036527e7c04f7be6fb60f5d61353545aa7fcb739", + "sha256:08802f0c56ed150cc6885ae0788a321b73505d2263ee56dad84d200cab11c07a", + "sha256:0a15438253b34e6362b2dc41475e7f80de76320f335e70c5528b7148cac253a1", + "sha256:0c3f67e2aeda739d1cc0b1102c9a9129f7dc83901226cc24dd72ba275ced4218", + "sha256:0e7259016bc4345a31af861fdce942b77c99049d6c2107ca07dc2bba2435c1d9", + "sha256:0ed777c1e8c99b63037b91f9d73a6aad20fd035d77ac84afcc205225f8f41188", + "sha256:0f5d65c39f16717a47c36c756af0fb36144069c4718824b7533f803ecdf91138", + "sha256:0f8c09ed18ecb4ebf23e02b8e7a22a05d6411911e6fabef3a36e4f371f4f2585", + "sha256:11a04306fcba10cd9637e669fd73aa274c1c09ca64af79c041aa820ea992b637", + "sha256:1ae67b4e737cddc96c99461d2f75d218bdf7a0c3d3ad5604d1f5e7464a2f9ffe", + "sha256:1c5bb205e9212d0ebddf946bc07e73fa245c864a5f90f341d11ce7b0b854475d", + "sha256:1f7785f4f789fdb522729ae465adcaa099e2a3441519df750ebdccc481d961a1", + "sha256:200e63525948e325d6a13a76ba2911f927ad399ef64f57898cf7c74e69b71095", + "sha256:21c2e6b09565ba5b45ae161b438e033a86ad1736b8c838c766146eff8ceffff9", + "sha256:2213afee476546a7f37c7a9b4ad4d74b1e112a6fafffc9185d6d21f043128c81", + "sha256:27aa20d45c2e0b8cd05da6d4759649170e8dfc4f4e5ef33a34d06f2d79075d57", + "sha256:2a66bf12fbd4666dd023b6f51223aed3d9f3b40fef06ce404cb75bafd3d89536", + "sha256:2c9d147f754b1b0e723e6afb7ba1566ecb162fe4ea657f53d2139bbf894d050a", + "sha256:2ddfe41ddc81f29a4c44c8ce239eda5ade4e7fc305fb7311759dd6229a080052", + "sha256:31e9a882013c2f6bd2f2c974241bf4ba68c85eba943648ce88936d23209a2e01", + "sha256:3249cc2989d9090eeac5467e50e9ec2d40704fea9ab72f36b034ea34ee65ca98", + "sha256:3545039fa4779be2df51d6395e91a810f57122290864918b172d5dc7ca5bb433", + "sha256:394ed3924d7a01b5bd9a0d9d946136e1c2f7b3dc337196d99e61740ed4bc6fe1", + "sha256:3a6b45da02336895da82b9d472cd274b22dc27a5cea1d4b793874eead23dd14f", + "sha256:3a74c4f27167cb95c1d4af1c0b59e88b7f3e0182138db2501c353555f7ec57f4", + "sha256:3d0c3dd24bb4605439bf91068598d00c6370684f8de4a67c2992683f6c309d6b", + "sha256:3dbe858ee582cbb2c6294dc85f55b5f19c918c2597855e950f34b660f1a5ede6", + "sha256:3dc773b2861b37b41a6136e0b72a1a44689a9c4c101e0cddb6b854016acc0aa8", + "sha256:3e183c6e3298a2ed5af9d7a356ea823bccaab4ec2349dc9ed83999fd289d14d5", + "sha256:3f7765e69bbce0906a7c74d5fe46d2c7a7596147318dbc08e4a2431f3060e306", + "sha256:417d14450f06d51f363e41cace6488519038f940676ce9664b34ebf5653433a5", + "sha256:44f6c7caff88d988db017b9b0e4ab04934f11e3e72d478031efc7edcac6c622f", + "sha256:491755202eb21a5e350dae00c6d9a17247769c64dcf62d8c788b5c135e179dc4", + "sha256:4951e4f7a5680a2db62f7f4ab2f84617674d36d2d76a729b9a8be4b59b3659be", + "sha256:52421b41ac99e9d91934e4d0d0fe7da9f02bfa7536bb4431b4c05c906c8c6919", + "sha256:530e7c04f72002d2f334d5257c8a51bf409db0316feee7c87e4385043be136af", + "sha256:533658f8fbf056b70e434dff7e7aa611bcacb33e01f75de7f821810e48d1bb66", + "sha256:5670fb70a828663cc37552a2a85bf2ac38475572b0e9b91283dc09efb52c41d1", + "sha256:56c22432809085b3f3ae04e6e7bdd36883d7258fcd90e53ba7b2e463efc7a6af", + "sha256:58278b29cb89f3e43ff3e0c756abbd1518f3ee6adad9e35b51fb101c1c1daaec", + "sha256:588008b8497667f1ddca7c99f2f85ce8511f8f7871b4a06ceede68ab62dff64b", + "sha256:59565f10607c244bc4c05c0c5fa0c190c990996e0c719d05deec7030c2aa8289", + "sha256:59689a75ba8d7ffca577aefd017d08d659d86ad4585ccc73e43edbfc7476781a", + "sha256:5aea8212fb823e006b995c4dda533edcf98a893d941f173f6c9506126188860d", + "sha256:5c670c0406bdc845b474b680b9a5456c561c65cf366f8db5a60154088c92d102", + "sha256:5ca1e8188b26a819387b29c3895c47a5e618708fe6f787f3b1a471de2c4a94d9", + "sha256:5d077bc40a1fe984e1a9931e801e42959a1e6598edc8a3223b061d30fbd26bbc", + "sha256:5d5792e9b3fb8d16a19f46aa8208987cfeafe082363ee2745ea8b643d9cc5b45", + "sha256:5dd1537e7cc06efd81371f5d1a992bd5ab156b2b4f88834ca852de4a8ea523fa", + "sha256:5ea7b6766ac2dfe4bcac8b8595107665a18ef01f8c8343f00710b85096d1b53a", + "sha256:622020d4521e22fb371e15f580d153134bfb68d6a429d1342a25f051ec72df1c", + "sha256:627402ad8dea044dde2eccde4370560a2b750ef894c9578e1d4f8ffd54000461", + "sha256:644df54d729ef810dcd0f7732e50e5ad1bd0a135278ed8d6bcb06f33b6b6f708", + "sha256:64641a6068a16201366476731301441ce93457eb8452056f570133a6ceb15fca", + "sha256:64c2baa7774bc22dd4474248ba16fe1a7f611c13ac6123408694d4cc93d66dbd", + "sha256:6588c459c5627fefa30139be4d2e28a2c2a1d0d1c265aad2ba1935a7863a4913", + "sha256:66bc5eb8a323ed9894f8fa0ee6cb3e3fb2403d99aee635078fd19a8bc7a5a5da", + "sha256:68a2610dbe138fa8c5826b3f6d98a7cfc29707b850ddcc3e21910a6fe51f6ca0", + "sha256:6935bbf153f9a965f1e07c2649c0849d29832487c52bb4a5c5066031d8b44fd5", + "sha256:6992030d43b916407c9aa52e9673612ff39a575523c5f4cf72cdef75365709a5", + "sha256:6a014510830df1475176466b6087fc0c08b47a36714823e58d8b8d7709132a96", + "sha256:6ab833e4735a7e5533711a6ea2df26459b96f9eec36d23f74cafe03631647c41", + "sha256:6cc6ee342fb7fa2471bd9b6d6fdfc78925a697bf5c2bcd0a302e98b0d35bfad3", + "sha256:6cf58416653c5901e12624e4013708b6e11142956e7f35e7a83f1ab02f3fe456", + "sha256:70a9768e1b9d79edca17890175ba915654ee1725975d69ab64813dd785a2bd5c", + "sha256:70ac664a48aa64e5e635ae5566f5227f2ab7f66a3990d67566d9907edcbbf867", + "sha256:71e97313406ccf55d32cc98a533ee05c61e15d11b99215b237346171c179c0b0", + "sha256:7221d49259aa1e5a8f00d3d28b1e0b76031655ca74bb287123ef56c3db92f213", + "sha256:74b28c6334cca4dd704e8004cba1955af0b778cf449142e581e404bd211fb619", + "sha256:764b521b75701f60683500d8621841bec41a65eb739b8466000c6fdbc256c240", + "sha256:78bfa756eab503673991bdcf464917ef7845a964903d3302c5f68417ecdc948c", + "sha256:794f04eec78f1d0e35d9e0c36cbbb22e42d370dda1609fb03bcd7aeb458c6377", + "sha256:79bd05260359170f78b181b59ce871673ed01ba048deef4bf49a36ab3e72e80b", + "sha256:7a7efd5b6d3e30d81ec68ab8a88252d7c7c6f13aaa875009fe3097eb4e30b84c", + "sha256:7c17b64b0a6ef4e5affae6a3724010a7a66bda48a62cfe0674dabd46642e8b54", + "sha256:804f74efe22b6a227306dd890eecc4f8c59ff25ca35f1f14e7482bbce96ef10b", + "sha256:853e074d4931dbcba7480d4dcab23d5c56bd9607f92825ab80ee2bd916edea53", + "sha256:857500f88b17a6479202ff5fe5f580fc3404922cd02ab3716197adf1ef628029", + "sha256:865bad62df277c04beed9478fe665b9ef63eb28fe026d5dedcb89b537d2e2ea6", + "sha256:88e22fc0a6684337d25c994381ed8a1580a6f5ebebd5ad41f89f663ff4ec2885", + "sha256:8b9c07e7a45bb64e21df4b6aa623cb8ba214dfb47d2027d90eac197329bb5e94", + "sha256:8de8f9d6caa7f25b204fc861718815d41cbcf27ee8f028c89c882a0cf4ae4134", + "sha256:8e77c69d5892cb5ba71703c4057091e31ccf534bd7f129307a4d084d90d014b8", + "sha256:9123716666e25b7b71c4e1789ec829ed18663152008b58544d95b008ed9e21e9", + "sha256:958244ad566c3ffc385f47dddde4145088a0ab893504b54b52c041987a8c1863", + "sha256:96323338e6c14e958d775700ec8a88346014a85e5de73ac7967db0367582049b", + "sha256:9676bfc686fa6a3fa10cd4ae6b76cae8be26eb5ec6811d2a325636c460da1806", + "sha256:9b0ff53900566bc6325ecde9181d89afadc59c5ffa39bddf084aaedfe3b06a11", + "sha256:9b9ec9c9978b708d488bec36b9e4c94d88fd12ccac3e62134a9d17ddba910ea9", + "sha256:9c6ad0fbf105f6bcc9300c00010a2ffa44ea6f555df1a2ad95c88f5656104817", + "sha256:9ca66b8e90daca431b7ca1408cae085d025326570e57749695d6a01454790e95", + "sha256:9e2addd2d1866fe112bc6f80117bcc6bc25191c5ed1bfbcf9f1386a884252ae8", + "sha256:a0af35bd8ebf84888373630f73f24e86bf016642fb8576fba49d3d6b560b7cbc", + "sha256:a2b44bec7adf3e9305ce6cbfa47a4395667e744097faed97abb4728748ba7d47", + "sha256:a2dfe7e2473f9b59496247aad6e23b405ddf2e12ef0765677b0081c02d6c2c0b", + "sha256:a55ee573116ba208932e2d1a037cc4b10d2c1cb264ced2184d00b18ce585b2c0", + "sha256:a7baf9ffc238e4bf401299f50e971a45bfcc10a785522541a6e3179c83eabf0a", + "sha256:a8d5c70e04aac1eda5c829a26d1f75c6e5286c74743133d9f742cda8e53b9c2f", + "sha256:a91481dbcddf1736c98a80b122afa0f7296eeb80b72344d7f45dc9f781551f56", + "sha256:ab31a88a651039a07a3ae327d68ebdd8bc589b16938c09ef3f32a4b809dc96ef", + "sha256:abc25c3cab9ec7fcd299b9bcb3b8d4a1231877e425c650fa1c7576c5107ab851", + "sha256:adfb84ca6b87e06bc6b146dc7da7623395db1e31621c4785ad0658c5028b37d7", + "sha256:afbbdb120d1e78d2ba8064a68058001b871154cc57787031b645c9142b937a62", + "sha256:afd5562927cdef7c4f5550374acbc117fd4ecc05b5007bdfa57cc5355864e0a4", + "sha256:b070bbe8d3f0f6147689bed981d19bbb33070225373338df755a46893528104a", + "sha256:b0b58fbfa1bf7367dde8a557994e3b1637294be6cf2169810375caf8571a085c", + "sha256:b560e3aa4b1d49e0e6c847d72665384db35b2f5d45f8e6a5c0072e0283430533", + "sha256:b6241d4eee5f89453307c2f2bfa03b50362052ca0af1efecf9fef9a41a22bb4f", + "sha256:b6787b643356111dfd4032b5bffe26d2f8331556ecb79e15dacb9275da02866e", + "sha256:bcbf4af004f98793a95355980764b3d80d47117678118a44a80b721c9913436a", + "sha256:beb72935a941965c52990f3a32d7f07ce869fe21c6af8b34bf6a277b33a345d3", + "sha256:bf2e2458345d9bffb0d9ec16557d8858c9c88d2d11fed53998512504cd9df49b", + "sha256:c2d35a1d047efd68027817b32ab1586c1169e60ca02c65d428ae815b593e65d4", + "sha256:c38d7b9a690b090de999835f0443d8aa93ce5f2064035dfc48f27f02b4afc3d0", + "sha256:c6f2c8372b98208ce609c9e1d707f6918cc118fea4e2c754c9f0812c04ca116d", + "sha256:c817d420c60a5183953c783b0547d9eb43b7b344a2c46f69513d5952a78cddf3", + "sha256:c8ba129e6d3b0136a0f50345b2cb3db53f6bda5dd8c7f5d83fbccba97fb5dcb5", + "sha256:c94e75445b00319c1fad60f3c98b09cd63fe1134a8a953dcd48989ef42318534", + "sha256:cc4691d60512798304acb9207987e7b2b7c44627ea88b9d77489bbe3e6cc3bd4", + "sha256:cc518cea79fd1e2f6c90baafa28906d4309d24f3a63e801d855e7424c5b34144", + "sha256:cd53553ddad4a9c2f1f022756ae64abe16da1feb497edf4d9f87f99ec7cf86bd", + "sha256:cf22b41fdae514ee2f1691b6c3cdeae666d8b7fa9434de445f12bbeee0cf48dd", + "sha256:d38c8f50ecf57f0463399569aa388b232cf1a2ffb8f0a9a5412d0db57e054860", + "sha256:d3be9b2076112e51b323bdf6d5a7f8a798de55fb8d95fcb64bd179460cdc0704", + "sha256:d4f2cc7060dc3646632d7f15fe68e2fa98f58e35dd5666cd525f3b35d3fed7f8", + "sha256:d7520db34088c96cc0e0a3ad51a4fd5b401f279ee112aa2b7f8f976d8582606d", + "sha256:d793bebb202a6000390a5390078e945bbb49855c29c7e4d56a85901326c3b5d9", + "sha256:da052e7962ea2d5e5ef5bc0355d55007407087392cf465b7ad84ce5f3e25fe0f", + "sha256:dae0ed02f6b075426accbf6b2863c3d0a7eacc1b41fb40f2251d931e50188dad", + "sha256:ddc678fb4c7e30cf830a2b5a8d869538bc55b28d6c68544d09c7d0d8f17694dc", + "sha256:df2e6f546c4df14bc81f9498bbc007fbb87669f1bb707c6138878c46b06f6510", + "sha256:e02c5175f63effbd7c5e590399c118d5db6183bbfe8e0d118bdb5c2d1b48d937", + "sha256:e196a4ff48310ba62e53a8e0f97ca2bca83cdd2fe2934d8b5cb0df0a841b193a", + "sha256:e233db59c8f76630c512ab4a4daf5a5986da5c3d5b44b8e9fc742f2a24dbd460", + "sha256:e32be23d538753a8adb6c85bd539f5fd3b15cb987404327c569dfc5fd8366e85", + "sha256:e3d30321949861404323c50aebeb1943461a67cd51d4200ab02babc58bd06a86", + "sha256:e89580a581bf478d8dcb97d9cd011d567768e8bc4095f8557b21c4d4c5fea7d0", + "sha256:e998e304036198b4f6914e6a1e2b6f925208a20e2042563d9734881150c6c246", + "sha256:ec42088248c596dbd61d4ae8a5b004f97a4d91a9fd286f632e42e60b706718d7", + "sha256:efa7b51824aa0ee957ccd5a741c73e6851de55f40d807f08069eb4c5a26b2baa", + "sha256:f0a1bc63a465b6d72569a9bba9f2ef0334c4e03958e043da1920299100bc7c08", + "sha256:f18a5a84e16886898e51ab4b1d43acb3083c39b14c8caeb3589aabff0ee0b270", + "sha256:f2a9efc53d5b714b8df2b4b3e992accf8ce5bbdfe544d74d5c6766c9e1146a3a", + "sha256:f3bbbc998d42f8e561f347e798b85513ba4da324c2b3f9b7969e9c45b10f6169", + "sha256:f42038016852ae51b4088b2862126535cc4fc85802bfe30dea3500fdfaf1864e", + "sha256:f443cdef978430887ed55112b491f670bba6462cea7a7742ff8f14b7abb98d75", + "sha256:f51969bac61441fd31f028d7b3b45962f3ecebf691a510495e5d2cd8c8092dbd", + "sha256:f8aca2e3a72f37bfc7b14ba96d4056244001ddcc18382bd0daa087fd2e68a354", + "sha256:f9737bf36262046213a28e789cc82d82c6ef19c85a0cf05e75c670a33342ac2c", + "sha256:fd6037392f2d57793ab98d9e26798f44b8b4da2f2464388588f48ac52c489ea1", + "sha256:feaa45c0eae424d3e90d78823f3828e7dc42a42f21ed420db98da2c4ecf0a2cb", + "sha256:ff097ae562e637409b429a7ac958a20aab237a0378c42dabaa1e3abf2f896e5f", + "sha256:ff46d772d5f6f73564979cd77a4fffe55c916a05f3cb70e7c9c0590059fb29ef" ], "index": "pypi", - "version": "==5.1.0" + "markers": "python_version >= '3.6'", + "version": "==5.2.1" }, "markupsafe": { "hashes": [ @@ -427,25 +508,26 @@ "s3" ], "hashes": [ - "sha256:4054360b882b6e7bab25d52d057e196b978b8d15f1921333f534c4d8f6510bbb", - "sha256:8d19125d40c919cb40df62f4576904c2647c4e9a0e1ebc42491dd7787d09e107" + "sha256:2eaca2df7758f6868df420bf0725cd0b93d98709606f1fb8b2343b5bdc822d91", + "sha256:4ecdd4084491a2f25f7a7925416dcf07eee0031ce724957439a32ef764b22874" ], - "index": "pypi", - "version": "==5.0.4" + "markers": "python_version >= '3.8'", + "version": "==5.0.5" }, "py-partiql-parser": { "hashes": [ - "sha256:9c79b59bbe0cb50daa8090020f2e7f3e5a0e33f7846b48924f19a8f7704f4877", - "sha256:bdec65fe17d6093c05e9bc1742a99a041ef810b50a71cc0d9e74a88218d938cf" + "sha256:3dc4295a47da9587681a96b35c6e151886fdbd0a4acbe0d97c4c68e5f689d315", + "sha256:72e043919538fa63edae72fb59afc7e3fd93adbde656718a7d2b4666f23dd114" ], - "version": "==0.5.2" + "version": "==0.5.4" }, "pycparser": { "hashes": [ - "sha256:8ee45429555515e1f6b185e78100aea234072576aa43ab53aefcae078162fca9", - "sha256:e644fdec12f7872f86c58ff790da456218b10f863970249516d60a5eaca77206" + "sha256:491c8be9c040f5390f5bf44a5b07752bd07f56edf992381b05c701439eec10f6", + "sha256:c3702b6d3dd8c7abc1afa565d7e63d53a1d0bd86cdc24edd75470f4de499cfcc" ], - "version": "==2.21" + "markers": "python_version >= '3.8'", + "version": "==2.22" }, "python-dateutil": { "hashes": [ @@ -517,6 +599,7 @@ "sha256:942c5a758f98d790eaed1a29cb6eefc7ffb0d1cf7af05c3d2791656dbd6ad1e1" ], "index": "pypi", + "markers": "python_version >= '3.7'", "version": "==2.31.0" }, "responses": { @@ -551,7 +634,7 @@ "sha256:4e98489932b3372595cddc075e6033194775165702887216b65eba760dfd8d47", "sha256:62b65852bdd1d1d516839fcb1f6bc50cd0f16e05b4ec44b52f43d38bcb838524" ], - "index": "pypi", + "markers": "python_version >= '3.7' and python_version < '4.0'", "version": "==7.0.4" }, "structlog": { @@ -560,6 +643,7 @@ "sha256:41a09886e4d55df25bdcb9b5c9674bccfab723ff43e0a86a1b7b236be8e57b16" ], "index": "pypi", + "markers": "python_version >= '3.8'", "version": "==24.1.0" }, "urllib3": { @@ -572,11 +656,11 @@ }, "werkzeug": { "hashes": [ - "sha256:507e811ecea72b18a404947aded4b3390e1db8f826b494d76550ef45bb3b1dcc", - "sha256:90a285dc0e42ad56b34e696398b8122ee4c681833fb35b8334a095d82c56da10" + "sha256:3aac3f5da756f93030740bc235d3e09449efcf65f2f55e3602e1d851b8f48795", + "sha256:e39b645a6ac92822588e7b39a692e7828724ceae0b0d702ef96701f90e70128d" ], "markers": "python_version >= '3.8'", - "version": "==3.0.1" + "version": "==3.0.2" }, "wrapt": { "hashes": [ @@ -690,6 +774,7 @@ "sha256:e2af80566f43c85f5797365077fb64a393861a3730bd110971ab7a0c94e873e7" ], "index": "pypi", + "markers": "python_version >= '3.8'", "version": "==24.3.0" }, "certifi": { @@ -802,6 +887,7 @@ "sha256:ca9853ad459e787e2192211578cc907e7594e294c7ccc834310722b41b9ca6de" ], "index": "pypi", + "markers": "python_version >= '3.7'", "version": "==8.1.7" }, "coverage": { @@ -866,6 +952,7 @@ "sha256:f42015f31d386b351d4226389b387ae173207058832fbf5c8ec4b40e27b16026" ], "index": "pypi", + "markers": "python_version >= '3.5'", "version": "==3.3.1" }, "docopt": { @@ -884,11 +971,11 @@ }, "idna": { "hashes": [ - "sha256:9ecdbbd083b06798ae1e86adcbfe8ab1479cf864e4ee30fe4e46a003d12491ca", - "sha256:c05567e9c24a6b9faaa835c4821bad0590fbb9d5779e7caa6e1cc4978e7eb24f" + "sha256:028ff3aadf0609c1fd278d8ea3089299412a7a8b9bd005dd08b9f8285bcb5cfc", + "sha256:82fee1fc78add43492d3a1898bfa6d8a904cc97d8427f683ed8e798d07761aa0" ], "markers": "python_version >= '3.5'", - "version": "==3.6" + "version": "==3.7" }, "iniconfig": { "hashes": [ @@ -944,6 +1031,7 @@ "sha256:ac978141a75948948817d360297b7aae0fcb9d6ff6bc9ec6d514b85d5a65c044" ], "index": "pypi", + "markers": "python_version >= '3.8'", "version": "==8.1.1" }, "requests": { @@ -952,15 +1040,17 @@ "sha256:942c5a758f98d790eaed1a29cb6eefc7ffb0d1cf7af05c3d2791656dbd6ad1e1" ], "index": "pypi", + "markers": "python_version >= '3.7'", "version": "==2.31.0" }, "requests-mock": { "hashes": [ - "sha256:4e34f2a2752f0b78397fb414526605d95fcdeab021ac1f26d18960e7eb41f6a8", - "sha256:4f6fdf956de568e0bac99eee4ad96b391c602e614cc0ad33e7f5c72edd699e70" + "sha256:b1e37054004cdd5e56c84454cc7df12b25f90f382159087f4b6915aaeef39563", + "sha256:e9e12e333b525156e82a3c852f22016b9158220d2f47454de9cae8a77d371401" ], "index": "pypi", - "version": "==1.12.0" + "markers": "python_version >= '3.5'", + "version": "==1.12.1" }, "tomli": { "hashes": [ @@ -972,11 +1062,11 @@ }, "typing-extensions": { "hashes": [ - "sha256:69b1a937c3a517342112fb4c6df7e72fc39a38e7891a5730ed4985b5214b5475", - "sha256:b0abd7c89e8fb96f98db18d86106ff1d90ab692004eb746cf6eda2682f91b3cb" + "sha256:83f085bd5ca59c80295fc2a82ab5dac679cbe02b9f33f7d83af68e241bea51b0", + "sha256:c1f94d72897edaf4ce775bb7558d5b79d8126906a14ea5ed1635921406c0387a" ], "markers": "python_version < '3.11'", - "version": "==4.10.0" + "version": "==4.11.0" }, "urllib3": { "hashes": [