diff --git a/.github/pull-request-template.md b/.github/pull-request-template.md index 89fbadf..0b0c66a 100644 --- a/.github/pull-request-template.md +++ b/.github/pull-request-template.md @@ -1,38 +1,29 @@ -#### What does this PR do? - -Describe the overall purpose of the PR changes. Doesn't need to be as specific as the -individual commits. - -#### 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. -#### Includes new or updated dependencies? - +### Includes new or updated dependencies? YES | NO -#### What are the relevant tickets? - -Include links to Jira Software and/or Jira Service Management tickets here. +### Changes expectations for external applications? +YES | NO -#### Developer checklist +### 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 checklist - -- [ ] The commit message is clear and follows our guidelines (not just this pull request message) +### 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 documentation has been updated or is unnecessary -- [ ] The changes have been verified +- [ ] 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 diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml new file mode 100644 index 0000000..d842a91 --- /dev/null +++ b/.pre-commit-config.yaml @@ -0,0 +1,29 @@ +default_language_version: + python: python3.11 # set for project python version +repos: + - repo: local + hooks: + - id: black-apply + name: black-apply + entry: pipenv run black + language: system + pass_filenames: true + types: ["python"] + - id: mypy + name: mypy + entry: pipenv run mypy + language: system + pass_filenames: true + types: ["python"] + exclude: "tests/" + - id: ruff-apply + name: ruff-apply + entry: pipenv run ruff check --fix + language: system + pass_filenames: true + types: ["python"] + - id: safety + name: safety + entry: pipenv check + language: system + pass_filenames: false \ No newline at end of file diff --git a/Makefile b/Makefile index a8b9010..7ee5d4c 100644 --- a/Makefile +++ b/Makefile @@ -1,70 +1,86 @@ .PHONY: install test coveralls lint bandit black flake8 isort mypy dist-dev update publish-dev SHELL=/bin/bash DATETIME:=$(shell date -u +%Y%m%dT%H%M%SZ) + ### This is the Terraform-generated header for oai-pmh-harvester-dev ### ECR_NAME_DEV:=oai-pmh-harvester-dev ECR_URL_DEV:=222053980223.dkr.ecr.us-east-1.amazonaws.com/oai-pmh-harvester-dev ### End of Terraform-generated header ### -help: ## Print this message - @awk 'BEGIN { FS = ":.*##"; print "Usage: make \n\nTargets:" } \ -/^[-_[:alpha:]]+:.?*##/ { printf " %-15s%s\n", $$1, $$2 }' $(MAKEFILE_LIST) +help: # preview Makefile commands + @awk 'BEGIN { FS = ":.*#"; print "Usage: make \n\nTargets:" } \ +/^[-_[:alpha:]]+:.?*#/ { printf " %-15s%s\n", $$1, $$2 }' $(MAKEFILE_LIST) -install: ## Install script and dependencies +## Dependency commands +install: # install Python dependencies and pre-commit hook pipenv install --dev + pipenv run pre-commit install + +update: install # update Python dependencies + pipenv clean + pipenv update --dev -test: ## Run tests and print a coverage report +## Unit test commands +test: # run tests and print a coverage report pipenv run coverage run --source=harvester -m pytest -vv pipenv run coverage report -m -coveralls: test +coveralls: test # write coverage data to an LCOV report pipenv run coverage lcov -o ./coverage/lcov.info -### Linting commands ### -lint: bandit black flake8 isort mypy ## Lint the repo +## Code quality and safety commands -bandit: - pipenv run bandit -r harvester +lint: black mypy ruff safety # run linters -black: +black: # run 'black' linter and print a preview of suggested changes pipenv run black --check --diff . -flake8: - pipenv run flake8 . +mypy: # run 'mypy' linter + pipenv run mypy . -isort: - pipenv run isort . --diff +ruff: # run 'ruff' linter and print a preview of errors + pipenv run ruff check . -mypy: - pipenv run mypy harvester +safety: # check for security vulnerabilities and verify Pipfile.lock is up-to-date + pipenv check + pipenv verify -update: install ## Update all Python dependencies - pipenv clean - pipenv update --dev +lint-apply: # apply changes with 'black' and resolve fixable errors with 'ruff' + black-apply ruff-apply + +black-apply: # apply changes with 'black' + pipenv run black . + +ruff-apply: # resolve fixable errors with 'ruff' + pipenv run ruff check --fix . -### Terraform-generated Developer Deploy Commands for Dev environment ### -dist-dev: ## Build docker container (intended for developer-based manual build) +## Terraform-generated commands for container build and deployment in dev +dist-dev: # build docker container (intended for developer-based manual build) docker build --platform linux/amd64 \ -t $(ECR_URL_DEV):latest \ -t $(ECR_URL_DEV):`git describe --always` \ -t $(ECR_NAME_DEV):latest . -publish-dev: dist-dev ## Build, tag and push (intended for developer-based manual publish) +publish-dev: dist-dev # build, tag and push (intended for developer-based manual publish) docker login -u AWS -p $$(aws ecr get-login-password --region us-east-1) $(ECR_URL_DEV) docker push $(ECR_URL_DEV):latest docker push $(ECR_URL_DEV):`git describe --always` -### Terraform-generated manual shortcuts for deploying to Stage ### -### This requires that ECR_NAME_STAGE & ECR_URL_STAGE environment variables are set locally -### by the developer and that the developer has authenticated to the correct AWS Account. -### The values for the environment variables can be found in the stage_build.yml caller workflow. -dist-stage: ## Only use in an emergency +## Terraform-generated commands for container build and deployment in stage \ +This requires that ECR_NAME_STAGE and ECR_URL_STAGE environment variables \ +are set locally by the developer and that the developer has \ +authenticated to the correct AWS Account. The values for the environment \ +variables can be found in the stage_build.yml caller workflow. \ +While Stage should generally only be used in an emergency for most repos, \ +it is necessary for any testing requiring access to the Data Warehouse \ +because Cloud Connector is not enabled on Dev1. +dist-stage: docker build --platform linux/amd64 \ -t $(ECR_URL_STAGE):latest \ -t $(ECR_URL_STAGE):`git describe --always` \ -t $(ECR_NAME_STAGE):latest . -publish-stage: ## Only use in an emergency +publish-stage: docker login -u AWS -p $$(aws ecr get-login-password --region us-east-1) $(ECR_URL_STAGE) docker push $(ECR_URL_STAGE):latest docker push $(ECR_URL_STAGE):`git describe --always` diff --git a/Pipfile b/Pipfile index 44fc643..e5a16a7 100644 --- a/Pipfile +++ b/Pipfile @@ -13,11 +13,12 @@ urllib3 = "==1.26.18" [dev-packages] bandit = "*" black = "*" -coverage = "*" -flake8 = "*" -isort = "*" +coveralls = "*" mypy = "*" +pre-commit = "*" pytest = "*" +ruff = "*" +types-requests = "*" vcrpy = "==4.2.1" [requires] diff --git a/Pipfile.lock b/Pipfile.lock index 22adbd8..da238ac 100644 --- a/Pipfile.lock +++ b/Pipfile.lock @@ -1,7 +1,7 @@ { "_meta": { "hash": { - "sha256": "838dc96008a971c004290122685e140f19296be9c99ebda8483ba8067d438978" + "sha256": "8ae30f3f3e272ad30c9d3b56df3a540e06eef76720bb114f38bce468c09817cc" }, "pipfile-spec": 6, "requires": { @@ -18,26 +18,26 @@ "default": { "boto3": { "hashes": [ - "sha256:192695305fa65012d21f78ee852b91cb56dd571e84d51fb71f756302bf19d23f", - "sha256:20285ebf4e98b2905a88aeb162b4f77ff908b2e3e31038b3223e593789290aa3" + "sha256:1f94042f4efb5133b6b9b8b3243afc01143a81d21b3197a3afadf5780f97b05d", + "sha256:5c1bb487c68120aae236354d81b8a1a55d0aa3395d30748a01825ef90891921e" ], - "version": "==1.29.1" + "version": "==1.34.14" }, "botocore": { "hashes": [ - "sha256:1d9c0ff3eb7828a8bd8c5c7f12cd9d8c05c6fe4c616ef963fdaab538a0da3809", - "sha256:fcf3cc2913afba8e5f7ebcc15e8f6bfae844ab64bf983bf5a6fe3bb54cce239d" + "sha256:041bed0852649cab7e4dcd4d87f9d1cc084467fb846e5b60015e014761d96414", + "sha256:3b592f50f0406e236782a3a0a9ad1c3976060fdb2e04a23d18c3df5b7dfad3e0" ], - "markers": "python_version >= '3.7'", - "version": "==1.32.1" + "markers": "python_version >= '3.8'", + "version": "==1.34.14" }, "certifi": { "hashes": [ - "sha256:539cc1d13202e33ca466e88b2807e29f4c13049d6d87031a3c110744495cb082", - "sha256:92d6037539857d8206b8f6ae472e8b77db8058fec5937a1ef3f54304089edbb9" + "sha256:9b469f3a900bf28dc19b8cfbf8019bf47f7fdd1a65a1d4ffb98fc14166beb4d1", + "sha256:e036ab49d5b79556f99cfc2d9320b34cfbe5be05c5871b51de9329f0603b0474" ], "markers": "python_version >= '3.6'", - "version": "==2023.7.22" + "version": "==2023.11.17" }, "charset-normalizer": { "hashes": [ @@ -145,11 +145,11 @@ }, "idna": { "hashes": [ - "sha256:814f528e8dead7d329833b91c5faa87d60bf71824cd12a7530b5526063d02cb4", - "sha256:90b77e79eaa3eba6de819a0c442c0b4ceefc341a7a2ab77d7562bf49f425c5c2" + "sha256:9ecdbbd083b06798ae1e86adcbfe8ab1479cf864e4ee30fe4e46a003d12491ca", + "sha256:c05567e9c24a6b9faaa835c4821bad0590fbb9d5779e7caa6e1cc4978e7eb24f" ], "markers": "python_version >= '3.5'", - "version": "==3.4" + "version": "==3.6" }, "jmespath": { "hashes": [ @@ -161,101 +161,83 @@ }, "lxml": { "hashes": [ - "sha256:05186a0f1346ae12553d66df1cfce6f251589fea3ad3da4f3ef4e34b2d58c6a3", - "sha256:075b731ddd9e7f68ad24c635374211376aa05a281673ede86cbe1d1b3455279d", - "sha256:081d32421db5df44c41b7f08a334a090a545c54ba977e47fd7cc2deece78809a", - "sha256:0a3d3487f07c1d7f150894c238299934a2a074ef590b583103a45002035be120", - "sha256:0bfd0767c5c1de2551a120673b72e5d4b628737cb05414f03c3277bf9bed3305", - "sha256:0c0850c8b02c298d3c7006b23e98249515ac57430e16a166873fc47a5d549287", - "sha256:0e2cb47860da1f7e9a5256254b74ae331687b9672dfa780eed355c4c9c3dbd23", - "sha256:120fa9349a24c7043854c53cae8cec227e1f79195a7493e09e0c12e29f918e52", - "sha256:1247694b26342a7bf47c02e513d32225ededd18045264d40758abeb3c838a51f", - "sha256:141f1d1a9b663c679dc524af3ea1773e618907e96075262726c7612c02b149a4", - "sha256:14e019fd83b831b2e61baed40cab76222139926b1fb5ed0e79225bc0cae14584", - "sha256:1509dd12b773c02acd154582088820893109f6ca27ef7291b003d0e81666109f", - "sha256:17a753023436a18e27dd7769e798ce302963c236bc4114ceee5b25c18c52c693", - "sha256:1e224d5755dba2f4a9498e150c43792392ac9b5380aa1b845f98a1618c94eeef", - "sha256:1f447ea5429b54f9582d4b955f5f1985f278ce5cf169f72eea8afd9502973dd5", - "sha256:23eed6d7b1a3336ad92d8e39d4bfe09073c31bfe502f20ca5116b2a334f8ec02", - "sha256:25f32acefac14ef7bd53e4218fe93b804ef6f6b92ffdb4322bb6d49d94cad2bc", - "sha256:2c74524e179f2ad6d2a4f7caf70e2d96639c0954c943ad601a9e146c76408ed7", - "sha256:303bf1edce6ced16bf67a18a1cf8339d0db79577eec5d9a6d4a80f0fb10aa2da", - "sha256:3331bece23c9ee066e0fb3f96c61322b9e0f54d775fccefff4c38ca488de283a", - "sha256:3e9bdd30efde2b9ccfa9cb5768ba04fe71b018a25ea093379c857c9dad262c40", - "sha256:411007c0d88188d9f621b11d252cce90c4a2d1a49db6c068e3c16422f306eab8", - "sha256:42871176e7896d5d45138f6d28751053c711ed4d48d8e30b498da155af39aebd", - "sha256:46f409a2d60f634fe550f7133ed30ad5321ae2e6630f13657fb9479506b00601", - "sha256:48628bd53a426c9eb9bc066a923acaa0878d1e86129fd5359aee99285f4eed9c", - "sha256:48d6ed886b343d11493129e019da91d4039826794a3e3027321c56d9e71505be", - "sha256:4930be26af26ac545c3dffb662521d4e6268352866956672231887d18f0eaab2", - "sha256:4aec80cde9197340bc353d2768e2a75f5f60bacda2bab72ab1dc499589b3878c", - "sha256:4c28a9144688aef80d6ea666c809b4b0e50010a2aca784c97f5e6bf143d9f129", - "sha256:4d2d1edbca80b510443f51afd8496be95529db04a509bc8faee49c7b0fb6d2cc", - "sha256:4dd9a263e845a72eacb60d12401e37c616438ea2e5442885f65082c276dfb2b2", - "sha256:4f1026bc732b6a7f96369f7bfe1a4f2290fb34dce00d8644bc3036fb351a4ca1", - "sha256:4fb960a632a49f2f089d522f70496640fdf1218f1243889da3822e0a9f5f3ba7", - "sha256:50670615eaf97227d5dc60de2dc99fb134a7130d310d783314e7724bf163f75d", - "sha256:50baa9c1c47efcaef189f31e3d00d697c6d4afda5c3cde0302d063492ff9b477", - "sha256:53ace1c1fd5a74ef662f844a0413446c0629d151055340e9893da958a374f70d", - "sha256:5515edd2a6d1a5a70bfcdee23b42ec33425e405c5b351478ab7dc9347228f96e", - "sha256:56dc1f1ebccc656d1b3ed288f11e27172a01503fc016bcabdcbc0978b19352b7", - "sha256:578695735c5a3f51569810dfebd05dd6f888147a34f0f98d4bb27e92b76e05c2", - "sha256:57aba1bbdf450b726d58b2aea5fe47c7875f5afb2c4a23784ed78f19a0462574", - "sha256:57d6ba0ca2b0c462f339640d22882acc711de224d769edf29962b09f77129cbf", - "sha256:5c245b783db29c4e4fbbbfc9c5a78be496c9fea25517f90606aa1f6b2b3d5f7b", - "sha256:5c31c7462abdf8f2ac0577d9f05279727e698f97ecbb02f17939ea99ae8daa98", - "sha256:64f479d719dc9f4c813ad9bb6b28f8390360660b73b2e4beb4cb0ae7104f1c12", - "sha256:65299ea57d82fb91c7f019300d24050c4ddeb7c5a190e076b5f48a2b43d19c42", - "sha256:6689a3d7fd13dc687e9102a27e98ef33730ac4fe37795d5036d18b4d527abd35", - "sha256:690dafd0b187ed38583a648076865d8c229661ed20e48f2335d68e2cf7dc829d", - "sha256:6fc3c450eaa0b56f815c7b62f2b7fba7266c4779adcf1cece9e6deb1de7305ce", - "sha256:704f61ba8c1283c71b16135caf697557f5ecf3e74d9e453233e4771d68a1f42d", - "sha256:71c52db65e4b56b8ddc5bb89fb2e66c558ed9d1a74a45ceb7dcb20c191c3df2f", - "sha256:71d66ee82e7417828af6ecd7db817913cb0cf9d4e61aa0ac1fde0583d84358db", - "sha256:7d298a1bd60c067ea75d9f684f5f3992c9d6766fadbc0bcedd39750bf344c2f4", - "sha256:8b77946fd508cbf0fccd8e400a7f71d4ac0e1595812e66025bac475a8e811694", - "sha256:8d7e43bd40f65f7d97ad8ef5c9b1778943d02f04febef12def25f7583d19baac", - "sha256:8df133a2ea5e74eef5e8fc6f19b9e085f758768a16e9877a60aec455ed2609b2", - "sha256:8ed74706b26ad100433da4b9d807eae371efaa266ffc3e9191ea436087a9d6a7", - "sha256:92af161ecbdb2883c4593d5ed4815ea71b31fafd7fd05789b23100d081ecac96", - "sha256:97047f0d25cd4bcae81f9ec9dc290ca3e15927c192df17331b53bebe0e3ff96d", - "sha256:9719fe17307a9e814580af1f5c6e05ca593b12fb7e44fe62450a5384dbf61b4b", - "sha256:9767e79108424fb6c3edf8f81e6730666a50feb01a328f4a016464a5893f835a", - "sha256:9a92d3faef50658dd2c5470af249985782bf754c4e18e15afb67d3ab06233f13", - "sha256:9bb6ad405121241e99a86efff22d3ef469024ce22875a7ae045896ad23ba2340", - "sha256:9e28c51fa0ce5674be9f560c6761c1b441631901993f76700b1b30ca6c8378d6", - "sha256:aca086dc5f9ef98c512bac8efea4483eb84abbf926eaeedf7b91479feb092458", - "sha256:ae8b9c6deb1e634ba4f1930eb67ef6e6bf6a44b6eb5ad605642b2d6d5ed9ce3c", - "sha256:b0a545b46b526d418eb91754565ba5b63b1c0b12f9bd2f808c852d9b4b2f9b5c", - "sha256:b4e4bc18382088514ebde9328da057775055940a1f2e18f6ad2d78aa0f3ec5b9", - "sha256:b6420a005548ad52154c8ceab4a1290ff78d757f9e5cbc68f8c77089acd3c432", - "sha256:b86164d2cff4d3aaa1f04a14685cbc072efd0b4f99ca5708b2ad1b9b5988a991", - "sha256:bb3bb49c7a6ad9d981d734ef7c7193bc349ac338776a0360cc671eaee89bcf69", - "sha256:bef4e656f7d98aaa3486d2627e7d2df1157d7e88e7efd43a65aa5dd4714916cf", - "sha256:c0781a98ff5e6586926293e59480b64ddd46282953203c76ae15dbbbf302e8bb", - "sha256:c2006f5c8d28dee289f7020f721354362fa304acbaaf9745751ac4006650254b", - "sha256:c41bfca0bd3532d53d16fd34d20806d5c2b1ace22a2f2e4c0008570bf2c58833", - "sha256:cd47b4a0d41d2afa3e58e5bf1f62069255aa2fd6ff5ee41604418ca925911d76", - "sha256:cdb650fc86227eba20de1a29d4b2c1bfe139dc75a0669270033cb2ea3d391b85", - "sha256:cef2502e7e8a96fe5ad686d60b49e1ab03e438bd9123987994528febd569868e", - "sha256:d27be7405547d1f958b60837dc4c1007da90b8b23f54ba1f8b728c78fdb19d50", - "sha256:d37017287a7adb6ab77e1c5bee9bcf9660f90ff445042b790402a654d2ad81d8", - "sha256:d3ff32724f98fbbbfa9f49d82852b159e9784d6094983d9a8b7f2ddaebb063d4", - "sha256:d73d8ecf8ecf10a3bd007f2192725a34bd62898e8da27eb9d32a58084f93962b", - "sha256:dd708cf4ee4408cf46a48b108fb9427bfa00b9b85812a9262b5c668af2533ea5", - "sha256:e3cd95e10c2610c360154afdc2f1480aea394f4a4f1ea0a5eacce49640c9b190", - "sha256:e4da8ca0c0c0aea88fd46be8e44bd49716772358d648cce45fe387f7b92374a7", - "sha256:eadfbbbfb41b44034a4c757fd5d70baccd43296fb894dba0295606a7cf3124aa", - "sha256:ed667f49b11360951e201453fc3967344d0d0263aa415e1619e85ae7fd17b4e0", - "sha256:f3df3db1d336b9356dd3112eae5f5c2b8b377f3bc826848567f10bfddfee77e9", - "sha256:f6bdac493b949141b733c5345b6ba8f87a226029cbabc7e9e121a413e49441e0", - "sha256:fbf521479bcac1e25a663df882c46a641a9bff6b56dc8b0fafaebd2f66fb231b", - "sha256:fc9b106a1bf918db68619fdcd6d5ad4f972fdd19c01d19bdb6bf63f3589a9ec5", - "sha256:fcdd00edfd0a3001e0181eab3e63bd5c74ad3e67152c84f93f13769a40e073a7", - "sha256:fe4bda6bd4340caa6e5cf95e73f8fea5c4bfc55763dd42f1b50a94c1b4a2fbd4" - ], - "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3, 3.4'", - "version": "==4.9.3" + "sha256:13521a321a25c641b9ea127ef478b580b5ec82aa2e9fc076c86169d161798b01", + "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:5e53d7e6a98b64fe54775d23a7c669763451340c3d44ad5e3a3b48a1efbdc96f", + "sha256:5fcfbebdb0c5d8d18b84118842f31965d59ee3e66996ac842e21f957eb76138c", + "sha256:601f4a75797d7a770daed8b42b97cd1bb1ba18bd51a9382077a6a247a12aa38d", + "sha256:61c5a7edbd7c695e54fca029ceb351fc45cd8860119a0f83e48be44e1c464862", + "sha256:6a2a2c724d97c1eb8cf966b16ca2915566a4904b9aad2ed9a09c748ffe14f969", + "sha256:6d48fc57e7c1e3df57be5ae8614bab6d4e7b60f65c5457915c26892c41afc59e", + "sha256:6f11b77ec0979f7e4dc5ae081325a2946f1fe424148d3945f943ceaede98adb8", + "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:9b99f564659cfa704a2dd82d0684207b1aadf7d02d33e54845f9fc78e06b7581", + "sha256:9bcf86dfc8ff3e992fed847c077bd875d9e0ba2fa25d859c3a0f0f76f07f0c8d", + "sha256:9bd0ae7cc2b85320abd5e0abad5ccee5564ed5f0cc90245d2f9a8ef330a8deae", + "sha256:9d3c0f8567ffe7502d969c2c1b809892dc793b5d0665f602aad19895f8d508da", + "sha256:9e5ac3437746189a9b4121db2a7b86056ac8786b12e88838696899328fc44bb2", + "sha256:a36c506e5f8aeb40680491d39ed94670487ce6614b9d27cabe45d94cd5d63e1e", + "sha256:a5ab722ae5a873d8dcee1f5f45ddd93c34210aed44ff2dc643b5025981908cda", + "sha256:a96f02ba1bcd330807fc060ed91d1f7a20853da6dd449e5da4b09bfcc08fdcf5", + "sha256:acb6b2f96f60f70e7f34efe0c3ea34ca63f19ca63ce90019c6cbca6b676e81fa", + "sha256:af8920ce4a55ff41167ddbc20077f5698c2e710ad3353d32a07d3264f3a2021e", + "sha256:afd825e30f8d1f521713a5669b63657bcfe5980a916c95855060048b88e1adb7", + "sha256:b21b4031b53d25b0858d4e124f2f9131ffc1530431c6d1321805c90da78388d1", + "sha256:b4b68c961b5cc402cbd99cca5eb2547e46ce77260eb705f4d117fd9c3f932b95", + "sha256:b66aa6357b265670bb574f050ffceefb98549c721cf28351b748be1ef9577d93", + "sha256:b9e240ae0ba96477682aa87899d94ddec1cc7926f9df29b1dd57b39e797d5ab5", + "sha256:bc64d1b1dab08f679fb89c368f4c05693f58a9faf744c4d390d7ed1d8223869b", + "sha256:bf8443781533b8d37b295016a4b53c1494fa9a03573c09ca5104550c138d5c05", + "sha256:c26aab6ea9c54d3bed716b8851c8bfc40cb249b8e9880e250d1eddde9f709bf5", + "sha256:c3cd1fc1dc7c376c54440aeaaa0dcc803d2126732ff5c6b68ccd619f2e64be4f", + "sha256:c7257171bb8d4432fe9d6fdde4d55fdbe663a63636a17f7f9aaba9bcb3153ad7", + "sha256:cfbac9f6149174f76df7e08c2e28b19d74aed90cad60383ad8671d3af7d0502f", + "sha256:d42e3a3fc18acc88b838efded0e6ec3edf3e328a58c68fbd36a7263a874906c8", + "sha256:d74fcaf87132ffc0447b3c685a9f862ffb5b43e70ea6beec2fb8057d5d2a1fea", + "sha256:d8c1d679df4361408b628f42b26a5d62bd3e9ba7f0c0e7969f925021554755aa", + "sha256:e856c1c7255c739434489ec9c8aa9cdf5179785d10ff20add308b5d673bed5cd", + "sha256:eac68f96539b32fce2c9b47eb7c25bb2582bdaf1bbb360d25f564ee9e04c542b", + "sha256:ed7326563024b6e91fef6b6c7a1a2ff0a71b97793ac33dbbcf38f6005e51ff6e", + "sha256:ed8c3d2cd329bf779b7ed38db176738f3f8be637bb395ce9629fc76f78afe3d4", + "sha256:f4c9bda132ad108b387c33fabfea47866af87f4ea6ffb79418004f0521e63204", + "sha256:f643ffd2669ffd4b5a3e9b41c909b72b2a1d5e4915da90a77e119b8d48ce867a" + ], + "markers": "python_version >= '3.6'", + "version": "==5.1.0" }, "python-dateutil": { "hashes": [ @@ -275,19 +257,19 @@ }, "s3transfer": { "hashes": [ - "sha256:10d6923c6359175f264811ef4bf6161a3156ce8e350e705396a7557d6293c33a", - "sha256:fd3889a66f5fe17299fe75b82eae6cf722554edca744ca5d5fe308b104883d2e" + "sha256:3cdb40f5cfa6966e812209d0994f2a4709b561c88e90cf00c2696d2df4e56b2e", + "sha256:d0c8bbf672d5eebbe4e57945e23b972d963f07d82f661cabf678a5c88831595b" ], - "markers": "python_version >= '3.7'", - "version": "==0.7.0" + "markers": "python_version >= '3.8'", + "version": "==0.10.0" }, "sentry-sdk": { "hashes": [ - "sha256:04e392db9a0d59bd49a51b9e3a92410ac5867556820465057c2ef89a38e953e9", - "sha256:a7865952701e46d38b41315c16c075367675c48d049b90a4cc2e41991ebc7efa" + "sha256:320a55cdf9da9097a0bead239c35b7e61f53660ef9878861824fd6d9b2eaf3b5", + "sha256:81b5b9ffdd1a374e9eb0c053b5d2012155db9cbe76393a8585677b753bd5fdc1" ], "index": "pypi", - "version": "==1.35.0" + "version": "==1.39.1" }, "sickle": { "hashes": [ @@ -328,35 +310,151 @@ "develop": { "bandit": { "hashes": [ - "sha256:75665181dc1e0096369112541a056c59d1c5f66f9bb74a8d686c3c362b83f549", - "sha256:bdfc739baa03b880c2d15d0431b31c658ffc348e907fe197e54e0389dd59e11e" + "sha256:36da17c67fc87579a5d20c323c8d0b1643a890a2b93f00b3d1229966624694ff", + "sha256:72ce7bc9741374d96fb2f1c9a8960829885f1243ffde743de70a19cee353e8f3" ], "index": "pypi", - "version": "==1.7.5" + "version": "==1.7.6" }, "black": { "hashes": [ - "sha256:250d7e60f323fcfc8ea6c800d5eba12f7967400eb6c2d21ae85ad31c204fb1f4", - "sha256:2a9acad1451632021ee0d146c8765782a0c3846e0e0ea46659d7c4f89d9b212b", - "sha256:412f56bab20ac85927f3a959230331de5614aecda1ede14b373083f62ec24e6f", - "sha256:421f3e44aa67138ab1b9bfbc22ee3780b22fa5b291e4db8ab7eee95200726b07", - "sha256:45aa1d4675964946e53ab81aeec7a37613c1cb71647b5394779e6efb79d6d187", - "sha256:4c44b7211a3a0570cc097e81135faa5f261264f4dfaa22bd5ee2875a4e773bd6", - "sha256:4c68855825ff432d197229846f971bc4d6666ce90492e5b02013bcaca4d9ab05", - "sha256:5133f5507007ba08d8b7b263c7aa0f931af5ba88a29beacc4b2dc23fcefe9c06", - "sha256:54caaa703227c6e0c87b76326d0862184729a69b73d3b7305b6288e1d830067e", - "sha256:58e5f4d08a205b11800332920e285bd25e1a75c54953e05502052738fe16b3b5", - "sha256:698c1e0d5c43354ec5d6f4d914d0d553a9ada56c85415700b81dc90125aac244", - "sha256:6c1cac07e64433f646a9a838cdc00c9768b3c362805afc3fce341af0e6a9ae9f", - "sha256:760415ccc20f9e8747084169110ef75d545f3b0932ee21368f63ac0fee86b221", - "sha256:7f622b6822f02bfaf2a5cd31fdb7cd86fcf33dab6ced5185c35f5db98260b055", - "sha256:cf57719e581cfd48c4efe28543fea3d139c6b6f1238b3f0102a9c73992cbb479", - "sha256:d136ef5b418c81660ad847efe0e55c58c8208b77a57a28a503a5f345ccf01394", - "sha256:dbea0bb8575c6b6303cc65017b46351dc5953eea5c0a59d7b7e3a2d2f433a911", - "sha256:fc7f6a44d52747e65a02558e1d807c82df1d66ffa80a601862040a43ec2e3142" + "sha256:0808494f2b2df923ffc5723ed3c7b096bd76341f6213989759287611e9837d50", + "sha256:1fa88a0f74e50e4487477bc0bb900c6781dbddfdfa32691e780bf854c3b4a47f", + "sha256:25e57fd232a6d6ff3f4478a6fd0580838e47c93c83eaf1ccc92d4faf27112c4e", + "sha256:2d9e13db441c509a3763a7a3d9a49ccc1b4e974a47be4e08ade2a228876500ec", + "sha256:3e1b38b3135fd4c025c28c55ddfc236b05af657828a8a6abe5deec419a0b7055", + "sha256:3fa4be75ef2a6b96ea8d92b1587dd8cb3a35c7e3d51f0738ced0781c3aa3a5a3", + "sha256:4ce3ef14ebe8d9509188014d96af1c456a910d5b5cbf434a09fef7e024b3d0d5", + "sha256:4f0031eaa7b921db76decd73636ef3a12c942ed367d8c3841a0739412b260a54", + "sha256:602cfb1196dc692424c70b6507593a2b29aac0547c1be9a1d1365f0d964c353b", + "sha256:6d1bd9c210f8b109b1762ec9fd36592fdd528485aadb3f5849b2740ef17e674e", + "sha256:78baad24af0f033958cad29731e27363183e140962595def56423e626f4bee3e", + "sha256:8d4df77958a622f9b5a4c96edb4b8c0034f8434032ab11077ec6c56ae9f384ba", + "sha256:97e56155c6b737854e60a9ab1c598ff2533d57e7506d97af5481141671abf3ea", + "sha256:9c4352800f14be5b4864016882cdba10755bd50805c95f728011bcb47a4afd59", + "sha256:a4d6a9668e45ad99d2f8ec70d5c8c04ef4f32f648ef39048d010b0689832ec6d", + "sha256:a920b569dc6b3472513ba6ddea21f440d4b4c699494d2e972a1753cdc25df7b0", + "sha256:ae76c22bde5cbb6bfd211ec343ded2163bba7883c7bc77f6b756a1049436fbb9", + "sha256:b18fb2ae6c4bb63eebe5be6bd869ba2f14fd0259bda7d18a46b764d8fb86298a", + "sha256:c04b6d9d20e9c13f43eee8ea87d44156b8505ca8a3c878773f68b4e4812a421e", + "sha256:c88b3711d12905b74206227109272673edce0cb29f27e1385f33b0163c414bba", + "sha256:dd15245c8b68fe2b6bd0f32c1556509d11bb33aec9b5d0866dd8e2ed3dba09c2", + "sha256:e0aaf6041986767a5e0ce663c7a2f0e9eaf21e6ff87a5f95cbf3675bfd4c41d2" ], "index": "pypi", - "version": "==23.11.0" + "version": "==23.12.1" + }, + "certifi": { + "hashes": [ + "sha256:9b469f3a900bf28dc19b8cfbf8019bf47f7fdd1a65a1d4ffb98fc14166beb4d1", + "sha256:e036ab49d5b79556f99cfc2d9320b34cfbe5be05c5871b51de9329f0603b0474" + ], + "markers": "python_version >= '3.6'", + "version": "==2023.11.17" + }, + "cfgv": { + "hashes": [ + "sha256:b7265b1f29fd3316bfcd2b330d63d024f2bfd8bcb8b0272f8e19a504856c48f9", + "sha256:e52591d4c5f5dead8e0f673fb16db7949d2cfb3f7da4582893288f0ded8fe560" + ], + "markers": "python_version >= '3.8'", + "version": "==3.4.0" + }, + "charset-normalizer": { + "hashes": [ + "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_full_version >= '3.7.0'", + "version": "==3.3.2" }, "click": { "hashes": [ @@ -368,69 +466,88 @@ }, "coverage": { "hashes": [ - "sha256:0cbf38419fb1a347aaf63481c00f0bdc86889d9fbf3f25109cf96c26b403fda1", - "sha256:12d15ab5833a997716d76f2ac1e4b4d536814fc213c85ca72756c19e5a6b3d63", - "sha256:149de1d2401ae4655c436a3dced6dd153f4c3309f599c3d4bd97ab172eaf02d9", - "sha256:1981f785239e4e39e6444c63a98da3a1db8e971cb9ceb50a945ba6296b43f312", - "sha256:2443cbda35df0d35dcfb9bf8f3c02c57c1d6111169e3c85fc1fcc05e0c9f39a3", - "sha256:289fe43bf45a575e3ab10b26d7b6f2ddb9ee2dba447499f5401cfb5ecb8196bb", - "sha256:2f11cc3c967a09d3695d2a6f03fb3e6236622b93be7a4b5dc09166a861be6d25", - "sha256:307adb8bd3abe389a471e649038a71b4eb13bfd6b7dd9a129fa856f5c695cf92", - "sha256:310b3bb9c91ea66d59c53fa4989f57d2436e08f18fb2f421a1b0b6b8cc7fffda", - "sha256:315a989e861031334d7bee1f9113c8770472db2ac484e5b8c3173428360a9148", - "sha256:3a4006916aa6fee7cd38db3bfc95aa9c54ebb4ffbfc47c677c8bba949ceba0a6", - "sha256:3c7bba973ebee5e56fe9251300c00f1579652587a9f4a5ed8404b15a0471f216", - "sha256:4175e10cc8dda0265653e8714b3174430b07c1dca8957f4966cbd6c2b1b8065a", - "sha256:43668cabd5ca8258f5954f27a3aaf78757e6acf13c17604d89648ecc0cc66640", - "sha256:4cbae1051ab791debecc4a5dcc4a1ff45fc27b91b9aee165c8a27514dd160836", - "sha256:5c913b556a116b8d5f6ef834038ba983834d887d82187c8f73dec21049abd65c", - "sha256:5f7363d3b6a1119ef05015959ca24a9afc0ea8a02c687fe7e2d557705375c01f", - "sha256:630b13e3036e13c7adc480ca42fa7afc2a5d938081d28e20903cf7fd687872e2", - "sha256:72c0cfa5250f483181e677ebc97133ea1ab3eb68645e494775deb6a7f6f83901", - "sha256:7dbc3ed60e8659bc59b6b304b43ff9c3ed858da2839c78b804973f613d3e92ed", - "sha256:88ed2c30a49ea81ea3b7f172e0269c182a44c236eb394718f976239892c0a27a", - "sha256:89a937174104339e3a3ffcf9f446c00e3a806c28b1841c63edb2b369310fd074", - "sha256:9028a3871280110d6e1aa2df1afd5ef003bab5fb1ef421d6dc748ae1c8ef2ebc", - "sha256:99b89d9f76070237975b315b3d5f4d6956ae354a4c92ac2388a5695516e47c84", - "sha256:9f805d62aec8eb92bab5b61c0f07329275b6f41c97d80e847b03eb894f38d083", - "sha256:a889ae02f43aa45032afe364c8ae84ad3c54828c2faa44f3bfcafecb5c96b02f", - "sha256:aa72dbaf2c2068404b9870d93436e6d23addd8bbe9295f49cbca83f6e278179c", - "sha256:ac8c802fa29843a72d32ec56d0ca792ad15a302b28ca6203389afe21f8fa062c", - "sha256:ae97af89f0fbf373400970c0a21eef5aa941ffeed90aee43650b81f7d7f47637", - "sha256:af3d828d2c1cbae52d34bdbb22fcd94d1ce715d95f1a012354a75e5913f1bda2", - "sha256:b4275802d16882cf9c8b3d057a0839acb07ee9379fa2749eca54efbce1535b82", - "sha256:b4767da59464bb593c07afceaddea61b154136300881844768037fd5e859353f", - "sha256:b631c92dfe601adf8f5ebc7fc13ced6bb6e9609b19d9a8cd59fa47c4186ad1ce", - "sha256:be32ad29341b0170e795ca590e1c07e81fc061cb5b10c74ce7203491484404ef", - "sha256:beaa5c1b4777f03fc63dfd2a6bd820f73f036bfb10e925fce067b00a340d0f3f", - "sha256:c0ba320de3fb8c6ec16e0be17ee1d3d69adcda99406c43c0409cb5c41788a611", - "sha256:c9eacf273e885b02a0273bb3a2170f30e2d53a6d53b72dbe02d6701b5296101c", - "sha256:cb536f0dcd14149425996821a168f6e269d7dcd2c273a8bff8201e79f5104e76", - "sha256:d1bc430677773397f64a5c88cb522ea43175ff16f8bfcc89d467d974cb2274f9", - "sha256:d1c88ec1a7ff4ebca0219f5b1ef863451d828cccf889c173e1253aa84b1e07ce", - "sha256:d3d9df4051c4a7d13036524b66ecf7a7537d14c18a384043f30a303b146164e9", - "sha256:d51ac2a26f71da1b57f2dc81d0e108b6ab177e7d30e774db90675467c847bbdf", - "sha256:d872145f3a3231a5f20fd48500274d7df222e291d90baa2026cc5152b7ce86bf", - "sha256:d8f17966e861ff97305e0801134e69db33b143bbfb36436efb9cfff6ec7b2fd9", - "sha256:dbc1b46b92186cc8074fee9d9fbb97a9dd06c6cbbef391c2f59d80eabdf0faa6", - "sha256:e10c39c0452bf6e694511c901426d6b5ac005acc0f78ff265dbe36bf81f808a2", - "sha256:e267e9e2b574a176ddb983399dec325a80dbe161f1a32715c780b5d14b5f583a", - "sha256:f47d39359e2c3779c5331fc740cf4bce6d9d680a7b4b4ead97056a0ae07cb49a", - "sha256:f6e9589bd04d0461a417562649522575d8752904d35c12907d8c9dfeba588faf", - "sha256:f94b734214ea6a36fe16e96a70d941af80ff3bfd716c141300d95ebc85339738", - "sha256:fa28e909776dc69efb6ed975a63691bc8172b64ff357e663a1bb06ff3c9b589a", - "sha256:fe494faa90ce6381770746077243231e0b83ff3f17069d748f645617cefe19d4" + "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" ], - "index": "pypi", - "version": "==7.3.2" + "markers": "python_version >= '3.7'", + "version": "==6.5.0" }, - "flake8": { + "coveralls": { "hashes": [ - "sha256:d5b3857f07c030bdb5bf41c7f53799571d75c4491748a3adcd47de929e34cd23", - "sha256:ffdfce58ea94c6580c77888a86506937f9a1a227dfcd15f245d694ae20a6b6e5" + "sha256:b32a8bb5d2df585207c119d6c01567b81fba690c9c10a753bfe27a335bfc43ea", + "sha256:f42015f31d386b351d4226389b387ae173207058832fbf5c8ec4b40e27b16026" ], "index": "pypi", - "version": "==6.1.0" + "version": "==3.3.1" + }, + "distlib": { + "hashes": [ + "sha256:034db59a0b96f8ca18035f36290806a9a6e6bd9d1ff91e45a7f172eb17e51784", + "sha256:1530ea13e350031b6312d8580ddb6b27a104275a31106523b8f123787f494f64" + ], + "version": "==0.3.8" + }, + "docopt": { + "hashes": [ + "sha256:49b3a825280bd66b3aa83585ef59c4a8c82f2c8a522dbe754a8bc8d08c85c491" + ], + "version": "==0.6.2" + }, + "filelock": { + "hashes": [ + "sha256:521f5f56c50f8426f5e03ad3b281b490a87ef15bc6c526f168290f0c7148d44e", + "sha256:57dbda9b35157b05fb3e58ee91448612eb674172fab98ee235ccb0b5bee19a1c" + ], + "markers": "python_version >= '3.8'", + "version": "==3.13.1" }, "gitdb": { "hashes": [ @@ -448,13 +565,21 @@ "markers": "python_version >= '3.7'", "version": "==3.1.40" }, + "identify": { + "hashes": [ + "sha256:161558f9fe4559e1557e1bff323e8631f6a0e4837f7497767c1782832f16b62d", + "sha256:d40ce5fcd762817627670da8a7d8d8e65f24342d14539c59488dc603bf662e34" + ], + "markers": "python_version >= '3.8'", + "version": "==2.5.33" + }, "idna": { "hashes": [ - "sha256:814f528e8dead7d329833b91c5faa87d60bf71824cd12a7530b5526063d02cb4", - "sha256:90b77e79eaa3eba6de819a0c442c0b4ceefc341a7a2ab77d7562bf49f425c5c2" + "sha256:9ecdbbd083b06798ae1e86adcbfe8ab1479cf864e4ee30fe4e46a003d12491ca", + "sha256:c05567e9c24a6b9faaa835c4821bad0590fbb9d5779e7caa6e1cc4978e7eb24f" ], "markers": "python_version >= '3.5'", - "version": "==3.4" + "version": "==3.6" }, "iniconfig": { "hashes": [ @@ -464,14 +589,6 @@ "markers": "python_version >= '3.7'", "version": "==2.0.0" }, - "isort": { - "hashes": [ - "sha256:8bef7dde241278824a6d83f44a544709b065191b95b6e50894bdc722fcba0504", - "sha256:f84c2818376e66cf843d497486ea8fed8700b340f308f076c6fb1229dff318b6" - ], - "index": "pypi", - "version": "==5.12.0" - }, "markdown-it-py": { "hashes": [ "sha256:355216845c60bd96232cd8d8c40e8f9765cc86f46880e43a8fd22dc1a1a8cab1", @@ -480,14 +597,6 @@ "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", @@ -578,36 +687,36 @@ }, "mypy": { "hashes": [ - "sha256:0e81ffd120ee24959b449b647c4b2fbfcf8acf3465e082b8d58fd6c4c2b27e46", - "sha256:185cff9b9a7fec1f9f7d8352dff8a4c713b2e3eea9c6c4b5ff7f0edf46b91e41", - "sha256:1e280b5697202efa698372d2f39e9a6713a0395a756b1c6bd48995f8d72690dc", - "sha256:1fe46e96ae319df21359c8db77e1aecac8e5949da4773c0274c0ef3d8d1268a9", - "sha256:2b53655a295c1ed1af9e96b462a736bf083adba7b314ae775563e3fb4e6795f5", - "sha256:551d4a0cdcbd1d2cccdcc7cb516bb4ae888794929f5b040bb51aae1846062901", - "sha256:55d28d7963bef00c330cb6461db80b0b72afe2f3c4e2963c99517cf06454e665", - "sha256:5da84d7bf257fd8f66b4f759a904fd2c5a765f70d8b52dde62b521972a0a2357", - "sha256:6cb8d5f6d0fcd9e708bb190b224089e45902cacef6f6915481806b0c77f7786d", - "sha256:7a7b1e399c47b18feb6f8ad4a3eef3813e28c1e871ea7d4ea5d444b2ac03c418", - "sha256:870bd1ffc8a5862e593185a4c169804f2744112b4a7c55b93eb50f48e7a77010", - "sha256:87c076c174e2c7ef8ab416c4e252d94c08cd4980a10967754f91571070bf5fbe", - "sha256:96650d9a4c651bc2a4991cf46f100973f656d69edc7faf91844e87fe627f7e96", - "sha256:a3637c03f4025f6405737570d6cbfa4f1400eb3c649317634d273687a09ffc2f", - "sha256:a79cdc12a02eb526d808a32a934c6fe6df07b05f3573d210e41808020aed8b5d", - "sha256:b633f188fc5ae1b6edca39dae566974d7ef4e9aaaae00bc36efe1f855e5173ac", - "sha256:bf7a2f0a6907f231d5e41adba1a82d7d88cf1f61a70335889412dec99feeb0f8", - "sha256:c1b06b4b109e342f7dccc9efda965fc3970a604db70f8560ddfdee7ef19afb05", - "sha256:cddee95dea7990e2215576fae95f6b78a8c12f4c089d7e4367564704e99118d3", - "sha256:d01921dbd691c4061a3e2ecdbfbfad029410c5c2b1ee88946bf45c62c6c91210", - "sha256:d0fa29919d2e720c8dbaf07d5578f93d7b313c3e9954c8ec05b6d83da592e5d9", - "sha256:d6ed9a3997b90c6f891138e3f83fb8f475c74db4ccaa942a1c7bf99e83a989a1", - "sha256:d93e76c2256aa50d9c82a88e2f569232e9862c9982095f6d54e13509f01222fc", - "sha256:df67fbeb666ee8828f675fee724cc2cbd2e4828cc3df56703e02fe6a421b7401", - "sha256:f29386804c3577c83d76520abf18cfcd7d68264c7e431c5907d250ab502658ee", - "sha256:f65f385a6f43211effe8c682e8ec3f55d79391f70a201575def73d08db68ead1", - "sha256:fc9fe455ad58a20ec68599139ed1113b21f977b536a91b42bef3ffed5cce7391" + "sha256:028cf9f2cae89e202d7b6593cd98db6759379f17a319b5faf4f9978d7084cdc6", + "sha256:2afecd6354bbfb6e0160f4e4ad9ba6e4e003b767dd80d85516e71f2e955ab50d", + "sha256:2b5b6c721bd4aabaadead3a5e6fa85c11c6c795e0c81a7215776ef8afc66de02", + "sha256:42419861b43e6962a649068a61f4a4839205a3ef525b858377a960b9e2de6e0d", + "sha256:42c6680d256ab35637ef88891c6bd02514ccb7e1122133ac96055ff458f93fc3", + "sha256:485a8942f671120f76afffff70f259e1cd0f0cfe08f81c05d8816d958d4577d3", + "sha256:4c886c6cce2d070bd7df4ec4a05a13ee20c0aa60cb587e8d1265b6c03cf91da3", + "sha256:4e6d97288757e1ddba10dd9549ac27982e3e74a49d8d0179fc14d4365c7add66", + "sha256:4ef4be7baf08a203170f29e89d79064463b7fc7a0908b9d0d5114e8009c3a259", + "sha256:51720c776d148bad2372ca21ca29256ed483aa9a4cdefefcef49006dff2a6835", + "sha256:52825b01f5c4c1c4eb0db253ec09c7aa17e1a7304d247c48b6f3599ef40db8bd", + "sha256:538fd81bb5e430cc1381a443971c0475582ff9f434c16cd46d2c66763ce85d9d", + "sha256:5c1538c38584029352878a0466f03a8ee7547d7bd9f641f57a0f3017a7c905b8", + "sha256:6ff8b244d7085a0b425b56d327b480c3b29cafbd2eff27316a004f9a7391ae07", + "sha256:7178def594014aa6c35a8ff411cf37d682f428b3b5617ca79029d8ae72f5402b", + "sha256:720a5ca70e136b675af3af63db533c1c8c9181314d207568bbe79051f122669e", + "sha256:7f1478736fcebb90f97e40aff11a5f253af890c845ee0c850fe80aa060a267c6", + "sha256:855fe27b80375e5c5878492f0729540db47b186509c98dae341254c8f45f42ae", + "sha256:8963b83d53ee733a6e4196954502b33567ad07dfd74851f32be18eb932fb1cb9", + "sha256:9261ed810972061388918c83c3f5cd46079d875026ba97380f3e3978a72f503d", + "sha256:99b00bc72855812a60d253420d8a2eae839b0afa4938f09f4d2aa9bb4654263a", + "sha256:ab3c84fa13c04aeeeabb2a7f67a25ef5d77ac9d6486ff33ded762ef353aa5592", + "sha256:afe3fe972c645b4632c563d3f3eff1cdca2fa058f730df2b93a35e3b0c538218", + "sha256:d19c413b3c07cbecf1f991e2221746b0d2a9410b59cb3f4fb9557f0365a1a817", + "sha256:df9824ac11deaf007443e7ed2a4a26bebff98d2bc43c6da21b2b64185da011c4", + "sha256:e46f44b54ebddbeedbd3d5b289a893219065ef805d95094d16a0af6630f5d410", + "sha256:f5ac9a4eeb1ec0f1ccdc6f326bcdb464de5f80eb07fb38b5ddd7b0de6bc61e55" ], "index": "pypi", - "version": "==1.7.0" + "version": "==1.8.0" }, "mypy-extensions": { "hashes": [ @@ -617,6 +726,14 @@ "markers": "python_version >= '3.5'", "version": "==1.0.0" }, + "nodeenv": { + "hashes": [ + "sha256:d51e0c37e64fbf47d017feac3145cdbb58836d7eee8c6f6d3b6880c5456227d2", + "sha256:df865724bb3c3adc86b3876fa209771517b0cfe596beff01a92700e0e8be4cec" + ], + "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3, 3.4, 3.5, 3.6'", + "version": "==1.8.0" + }, "packaging": { "hashes": [ "sha256:048fb0e9405036518eaaf48a55953c750c11e1a1b68e0dd1a9d62ed0c092cfc5", @@ -627,11 +744,11 @@ }, "pathspec": { "hashes": [ - "sha256:1d6ed233af05e679efb96b1851550ea95bbb64b7c490b0f5aa52996c11e92a20", - "sha256:e0d8d0ac2f12da61956eb2306b69f9469b42f4deb0f3cb6ed47b9cce9996ced3" + "sha256:a0d503e138a4c123b27490a4f7beda6a01c6f288df0e4a8b79c7eb0dc7b4cc08", + "sha256:a482d51503a1ab33b1c67a6c3813a26953dbdc71c31dacaef9a838c4e29f5712" ], - "markers": "python_version >= '3.7'", - "version": "==0.11.2" + "markers": "python_version >= '3.8'", + "version": "==0.12.1" }, "pbr": { "hashes": [ @@ -643,11 +760,11 @@ }, "platformdirs": { "hashes": [ - "sha256:118c954d7e949b35437270383a3f2531e99dd93cf7ce4dc8340d3356d30f173b", - "sha256:cb633b2bcf10c51af60beb0ab06d2f1d69064b43abf4c185ca6b28865f3f9731" + "sha256:11c8f37bcca40db96d8144522d925583bdb7a31f7b0e37e3ed4318400a8e2380", + "sha256:906d548203468492d432bcb294d4bc2fff751bf84971fbb2c10918cc206ee420" ], - "markers": "python_version >= '3.7'", - "version": "==4.0.0" + "markers": "python_version >= '3.8'", + "version": "==4.1.0" }, "pluggy": { "hashes": [ @@ -657,37 +774,29 @@ "markers": "python_version >= '3.8'", "version": "==1.3.0" }, - "pycodestyle": { - "hashes": [ - "sha256:41ba0e7afc9752dfb53ced5489e89f8186be00e599e712660695b7a75ff2663f", - "sha256:44fe31000b2d866f2e41841b18528a505fbd7fef9017b04eff4e2648a0fadc67" - ], - "markers": "python_version >= '3.8'", - "version": "==2.11.1" - }, - "pyflakes": { + "pre-commit": { "hashes": [ - "sha256:4132f6d49cb4dae6819e5379898f2b8cce3c5f23994194c24b77d5da2e36f774", - "sha256:a0aae034c444db0071aa077972ba4768d40c830d9539fd45bf4cd3f8f6992efc" + "sha256:c255039ef399049a5544b6ce13d135caba8f2c28c3b4033277a788f434308376", + "sha256:d30bad9abf165f7785c15a21a1f46da7d0677cb00ee7ff4c579fd38922efe15d" ], - "markers": "python_version >= '3.8'", - "version": "==3.1.0" + "index": "pypi", + "version": "==3.6.0" }, "pygments": { "hashes": [ - "sha256:13fc09fa63bc8d8671a6d247e1eb303c4b343eaee81d861f3404db2935653692", - "sha256:1daff0494820c69bc8941e407aa20f577374ee88364ee10a98fdbe0aece96e29" + "sha256:b27c2826c47d0f3219f29554824c30c5e8945175d888647acd804ddd04af846c", + "sha256:da46cec9fd2de5be3a8a784f434e4c4ab670b4ff54d605c4c2717e9d49c4c367" ], "markers": "python_version >= '3.7'", - "version": "==2.16.1" + "version": "==2.17.2" }, "pytest": { "hashes": [ - "sha256:0d009c083ea859a71b76adf7c1d502e4bc170b80a8ef002da5806527b9591fac", - "sha256:d989d136982de4e3b29dabcc838ad581c64e8ed52c11fbe86ddebd9da0818cd5" + "sha256:2cf0005922c6ace4a3e2ec8b4080eb0d9753fdc93107415332f50ce9e7994280", + "sha256:b090cdf5ed60bf4c45261be03239c2c1c22df034fbffe691abe93cd80cea01d8" ], "index": "pypi", - "version": "==7.4.3" + "version": "==7.4.4" }, "pyyaml": { "hashes": [ @@ -745,6 +854,14 @@ "markers": "python_version >= '3.6'", "version": "==6.0.1" }, + "requests": { + "hashes": [ + "sha256:58cd2187c01e70e6e26505bca751777aa9f2ee0b7f4300988b709f44e013003f", + "sha256:942c5a758f98d790eaed1a29cb6eefc7ffb0d1cf7af05c3d2791656dbd6ad1e1" + ], + "markers": "python_version >= '3.7'", + "version": "==2.31.0" + }, "rich": { "hashes": [ "sha256:5cb5123b5cf9ee70584244246816e9114227e0b98ad9176eede6ad54bf5403fa", @@ -753,6 +870,37 @@ "markers": "python_full_version >= '3.7.0'", "version": "==13.7.0" }, + "ruff": { + "hashes": [ + "sha256:09c415716884950080921dd6237767e52e227e397e2008e2bed410117679975b", + "sha256:0f58948c6d212a6b8d41cd59e349751018797ce1727f961c2fa755ad6208ba45", + "sha256:190a566c8f766c37074d99640cd9ca3da11d8deae2deae7c9505e68a4a30f740", + "sha256:231d8fb11b2cc7c0366a326a66dafc6ad449d7fcdbc268497ee47e1334f66f77", + "sha256:4b077ce83f47dd6bea1991af08b140e8b8339f0ba8cb9b7a484c30ebab18a23f", + "sha256:5b25093dad3b055667730a9b491129c42d45e11cdb7043b702e97125bcec48a1", + "sha256:6464289bd67b2344d2a5d9158d5eb81025258f169e69a46b741b396ffb0cda95", + "sha256:934832f6ed9b34a7d5feea58972635c2039c7a3b434fe5ba2ce015064cb6e955", + "sha256:97ce4d752f964ba559c7023a86e5f8e97f026d511e48013987623915431c7ea9", + "sha256:9b8f397902f92bc2e70fb6bebfa2139008dc72ae5177e66c383fa5426cb0bf2c", + "sha256:9bd4025b9c5b429a48280785a2b71d479798a69f5c2919e7d274c5f4b32c3607", + "sha256:a7f772696b4cdc0a3b2e527fc3c7ccc41cdcb98f5c80fdd4f2b8c50eb1458196", + "sha256:c4a88efecec23c37b11076fe676e15c6cdb1271a38f2b415e381e87fe4517f18", + "sha256:e1ad00662305dcb1e987f5ec214d31f7d6a062cae3e74c1cbccef15afd96611d", + "sha256:ea0d3e950e394c4b332bcdd112aa566010a9f9c95814844a7468325290aabfd9", + "sha256:eb85ee287b11f901037a6683b2374bb0ec82928c5cbc984f575d0437979c521a", + "sha256:f9d4d88cb6eeb4dfe20f9f0519bd2eaba8119bde87c3d5065c541dbae2b5a2cb" + ], + "index": "pypi", + "version": "==0.1.11" + }, + "setuptools": { + "hashes": [ + "sha256:385eb4edd9c9d5c17540511303e39a147ce2fc04bc55289c322b9e5904fe2c05", + "sha256:be1af57fc409f93647f2e8e4573a142ed38724b8cdd389706a867bb4efcf1e78" + ], + "markers": "python_version >= '3.8'", + "version": "==69.0.3" + }, "six": { "hashes": [ "sha256:1e61c37477a1626458e36f7b1d82aa5c9b094fa4802892072e49de9c60c4c926", @@ -777,13 +925,36 @@ "markers": "python_version >= '3.8'", "version": "==5.1.0" }, + "types-requests": { + "hashes": [ + "sha256:a2db9cb228a81da8348b49ad6db3f5519452dd20a9c1e1a868c83c5fe88fd1a9", + "sha256:cd74ce3b53c461f1228a9b783929ac73a666658f223e28ed29753771477b3bd0" + ], + "index": "pypi", + "version": "==2.31.0.6" + }, + "types-urllib3": { + "hashes": [ + "sha256:229b7f577c951b8c1b92c1bc2b2fdb0b49847bd2af6d1cc2a2e3dd340f3bda8f", + "sha256:9683bbb7fb72e32bfe9d2be6e04875fbe1b3eeec3cbb4ea231435aa7fd6b4f0e" + ], + "version": "==1.26.25.14" + }, "typing-extensions": { "hashes": [ - "sha256:8f92fc8806f9a6b641eaa5318da32b44d401efaac0f6678c9bc448ba3605faa0", - "sha256:df8e4339e9cb77357558cbdbceca33c303714cf861d1eef15e1070055ae8b7ef" + "sha256:23478f88c37f27d76ac8aee6c905017a143b0b1b886c3c9f66bc2fd94f9f5783", + "sha256:af72aea155e91adfc61c3ae9e0e342dbc0cba726d6cba4b6c72c1f34e47291cd" ], "markers": "python_version >= '3.8'", - "version": "==4.8.0" + "version": "==4.9.0" + }, + "urllib3": { + "hashes": [ + "sha256:34b97092d7e0a3a8cf7cd10e386f401b3737364026c45e622aa02903dffe0f07", + "sha256:f8ecc1bba5667413457c529ab955bf8c67b45db799d159066261719e328580a0" + ], + "index": "pypi", + "version": "==1.26.18" }, "vcrpy": { "hashes": [ @@ -793,6 +964,14 @@ "index": "pypi", "version": "==4.2.1" }, + "virtualenv": { + "hashes": [ + "sha256:4238949c5ffe6876362d9c0180fc6c3a824a7b12b80604eeb8085f2ed7460de3", + "sha256:bf51c0d9c7dd63ea8e44086fa1e4fb1093a31e963b86959257378aef020e1f1b" + ], + "markers": "python_version >= '3.7'", + "version": "==20.25.0" + }, "wrapt": { "hashes": [ "sha256:0d2691979e93d06a95a26257adb7bfd0c93818e89b1406f5a28f36e0d8c1e1fc", @@ -871,83 +1050,99 @@ }, "yarl": { "hashes": [ - "sha256:04ab9d4b9f587c06d801c2abfe9317b77cdf996c65a90d5e84ecc45010823571", - "sha256:066c163aec9d3d073dc9ffe5dd3ad05069bcb03fcaab8d221290ba99f9f69ee3", - "sha256:13414591ff516e04fcdee8dc051c13fd3db13b673c7a4cb1350e6b2ad9639ad3", - "sha256:149ddea5abf329752ea5051b61bd6c1d979e13fbf122d3a1f9f0c8be6cb6f63c", - "sha256:159d81f22d7a43e6eabc36d7194cb53f2f15f498dbbfa8edc8a3239350f59fe7", - "sha256:1b1bba902cba32cdec51fca038fd53f8beee88b77efc373968d1ed021024cc04", - "sha256:22a94666751778629f1ec4280b08eb11815783c63f52092a5953faf73be24191", - "sha256:2a96c19c52ff442a808c105901d0bdfd2e28575b3d5f82e2f5fd67e20dc5f4ea", - "sha256:2b0738fb871812722a0ac2154be1f049c6223b9f6f22eec352996b69775b36d4", - "sha256:2c315df3293cd521033533d242d15eab26583360b58f7ee5d9565f15fee1bef4", - "sha256:32f1d071b3f362c80f1a7d322bfd7b2d11e33d2adf395cc1dd4df36c9c243095", - "sha256:3458a24e4ea3fd8930e934c129b676c27452e4ebda80fbe47b56d8c6c7a63a9e", - "sha256:38a3928ae37558bc1b559f67410df446d1fbfa87318b124bf5032c31e3447b74", - "sha256:3da8a678ca8b96c8606bbb8bfacd99a12ad5dd288bc6f7979baddd62f71c63ef", - "sha256:494053246b119b041960ddcd20fd76224149cfea8ed8777b687358727911dd33", - "sha256:50f33040f3836e912ed16d212f6cc1efb3231a8a60526a407aeb66c1c1956dde", - "sha256:52a25809fcbecfc63ac9ba0c0fb586f90837f5425edfd1ec9f3372b119585e45", - "sha256:53338749febd28935d55b41bf0bcc79d634881195a39f6b2f767870b72514caf", - "sha256:5415d5a4b080dc9612b1b63cba008db84e908b95848369aa1da3686ae27b6d2b", - "sha256:5610f80cf43b6202e2c33ba3ec2ee0a2884f8f423c8f4f62906731d876ef4fac", - "sha256:566185e8ebc0898b11f8026447eacd02e46226716229cea8db37496c8cdd26e0", - "sha256:56ff08ab5df8429901ebdc5d15941b59f6253393cb5da07b4170beefcf1b2528", - "sha256:59723a029760079b7d991a401386390c4be5bfec1e7dd83e25a6a0881859e716", - "sha256:5fcd436ea16fee7d4207c045b1e340020e58a2597301cfbcfdbe5abd2356c2fb", - "sha256:61016e7d582bc46a5378ffdd02cd0314fb8ba52f40f9cf4d9a5e7dbef88dee18", - "sha256:63c48f6cef34e6319a74c727376e95626f84ea091f92c0250a98e53e62c77c72", - "sha256:646d663eb2232d7909e6601f1a9107e66f9791f290a1b3dc7057818fe44fc2b6", - "sha256:662e6016409828ee910f5d9602a2729a8a57d74b163c89a837de3fea050c7582", - "sha256:674ca19cbee4a82c9f54e0d1eee28116e63bc6fd1e96c43031d11cbab8b2afd5", - "sha256:6a5883464143ab3ae9ba68daae8e7c5c95b969462bbe42e2464d60e7e2698368", - "sha256:6e7221580dc1db478464cfeef9b03b95c5852cc22894e418562997df0d074ccc", - "sha256:75df5ef94c3fdc393c6b19d80e6ef1ecc9ae2f4263c09cacb178d871c02a5ba9", - "sha256:783185c75c12a017cc345015ea359cc801c3b29a2966c2655cd12b233bf5a2be", - "sha256:822b30a0f22e588b32d3120f6d41e4ed021806418b4c9f0bc3048b8c8cb3f92a", - "sha256:8288d7cd28f8119b07dd49b7230d6b4562f9b61ee9a4ab02221060d21136be80", - "sha256:82aa6264b36c50acfb2424ad5ca537a2060ab6de158a5bd2a72a032cc75b9eb8", - "sha256:832b7e711027c114d79dffb92576acd1bd2decc467dec60e1cac96912602d0e6", - "sha256:838162460b3a08987546e881a2bfa573960bb559dfa739e7800ceeec92e64417", - "sha256:83fcc480d7549ccebe9415d96d9263e2d4226798c37ebd18c930fce43dfb9574", - "sha256:84e0b1599334b1e1478db01b756e55937d4614f8654311eb26012091be109d59", - "sha256:891c0e3ec5ec881541f6c5113d8df0315ce5440e244a716b95f2525b7b9f3608", - "sha256:8c2ad583743d16ddbdf6bb14b5cd76bf43b0d0006e918809d5d4ddf7bde8dd82", - "sha256:8c56986609b057b4839968ba901944af91b8e92f1725d1a2d77cbac6972b9ed1", - "sha256:8ea48e0a2f931064469bdabca50c2f578b565fc446f302a79ba6cc0ee7f384d3", - "sha256:8ec53a0ea2a80c5cd1ab397925f94bff59222aa3cf9c6da938ce05c9ec20428d", - "sha256:95d2ecefbcf4e744ea952d073c6922e72ee650ffc79028eb1e320e732898d7e8", - "sha256:9b3152f2f5677b997ae6c804b73da05a39daa6a9e85a512e0e6823d81cdad7cc", - "sha256:9bf345c3a4f5ba7f766430f97f9cc1320786f19584acc7086491f45524a551ac", - "sha256:a60347f234c2212a9f0361955007fcf4033a75bf600a33c88a0a8e91af77c0e8", - "sha256:a74dcbfe780e62f4b5a062714576f16c2f3493a0394e555ab141bf0d746bb955", - "sha256:a83503934c6273806aed765035716216cc9ab4e0364f7f066227e1aaea90b8d0", - "sha256:ac9bb4c5ce3975aeac288cfcb5061ce60e0d14d92209e780c93954076c7c4367", - "sha256:aff634b15beff8902d1f918012fc2a42e0dbae6f469fce134c8a0dc51ca423bb", - "sha256:b03917871bf859a81ccb180c9a2e6c1e04d2f6a51d953e6a5cdd70c93d4e5a2a", - "sha256:b124e2a6d223b65ba8768d5706d103280914d61f5cae3afbc50fc3dfcc016623", - "sha256:b25322201585c69abc7b0e89e72790469f7dad90d26754717f3310bfe30331c2", - "sha256:b7232f8dfbd225d57340e441d8caf8652a6acd06b389ea2d3222b8bc89cbfca6", - "sha256:b8cc1863402472f16c600e3e93d542b7e7542a540f95c30afd472e8e549fc3f7", - "sha256:b9a4e67ad7b646cd6f0938c7ebfd60e481b7410f574c560e455e938d2da8e0f4", - "sha256:be6b3fdec5c62f2a67cb3f8c6dbf56bbf3f61c0f046f84645cd1ca73532ea051", - "sha256:bf74d08542c3a9ea97bb8f343d4fcbd4d8f91bba5ec9d5d7f792dbe727f88938", - "sha256:c027a6e96ef77d401d8d5a5c8d6bc478e8042f1e448272e8d9752cb0aff8b5c8", - "sha256:c0c77533b5ed4bcc38e943178ccae29b9bcf48ffd1063f5821192f23a1bd27b9", - "sha256:c1012fa63eb6c032f3ce5d2171c267992ae0c00b9e164efe4d73db818465fac3", - "sha256:c3a53ba34a636a256d767c086ceb111358876e1fb6b50dfc4d3f4951d40133d5", - "sha256:d4e2c6d555e77b37288eaf45b8f60f0737c9efa3452c6c44626a5455aeb250b9", - "sha256:de119f56f3c5f0e2fb4dee508531a32b069a5f2c6e827b272d1e0ff5ac040333", - "sha256:e65610c5792870d45d7b68c677681376fcf9cc1c289f23e8e8b39c1485384185", - "sha256:e9fdc7ac0d42bc3ea78818557fab03af6181e076a2944f43c38684b4b6bed8e3", - "sha256:ee4afac41415d52d53a9833ebae7e32b344be72835bbb589018c9e938045a560", - "sha256:f364d3480bffd3aa566e886587eaca7c8c04d74f6e8933f3f2c996b7f09bee1b", - "sha256:f3b078dbe227f79be488ffcfc7a9edb3409d018e0952cf13f15fd6512847f3f7", - "sha256:f4e2d08f07a3d7d3e12549052eb5ad3eab1c349c53ac51c209a0e5991bbada78", - "sha256:f7a3d8146575e08c29ed1cd287068e6d02f1c7bdff8970db96683b9591b86ee7" + "sha256:008d3e808d03ef28542372d01057fd09168419cdc8f848efe2804f894ae03e51", + "sha256:03caa9507d3d3c83bca08650678e25364e1843b484f19986a527630ca376ecce", + "sha256:07574b007ee20e5c375a8fe4a0789fad26db905f9813be0f9fef5a68080de559", + "sha256:09efe4615ada057ba2d30df871d2f668af661e971dfeedf0c159927d48bbeff0", + "sha256:0d2454f0aef65ea81037759be5ca9947539667eecebca092733b2eb43c965a81", + "sha256:0e9d124c191d5b881060a9e5060627694c3bdd1fe24c5eecc8d5d7d0eb6faabc", + "sha256:18580f672e44ce1238b82f7fb87d727c4a131f3a9d33a5e0e82b793362bf18b4", + "sha256:1f23e4fe1e8794f74b6027d7cf19dc25f8b63af1483d91d595d4a07eca1fb26c", + "sha256:206a55215e6d05dbc6c98ce598a59e6fbd0c493e2de4ea6cc2f4934d5a18d130", + "sha256:23d32a2594cb5d565d358a92e151315d1b2268bc10f4610d098f96b147370136", + "sha256:26a1dc6285e03f3cc9e839a2da83bcbf31dcb0d004c72d0730e755b33466c30e", + "sha256:29e0f83f37610f173eb7e7b5562dd71467993495e568e708d99e9d1944f561ec", + "sha256:2b134fd795e2322b7684155b7855cc99409d10b2e408056db2b93b51a52accc7", + "sha256:2d47552b6e52c3319fede1b60b3de120fe83bde9b7bddad11a69fb0af7db32f1", + "sha256:357495293086c5b6d34ca9616a43d329317feab7917518bc97a08f9e55648455", + "sha256:35a2b9396879ce32754bd457d31a51ff0a9d426fd9e0e3c33394bf4b9036b099", + "sha256:3777ce5536d17989c91696db1d459574e9a9bd37660ea7ee4d3344579bb6f129", + "sha256:3986b6f41ad22988e53d5778f91855dc0399b043fc8946d4f2e68af22ee9ff10", + "sha256:44d8ffbb9c06e5a7f529f38f53eda23e50d1ed33c6c869e01481d3fafa6b8142", + "sha256:49a180c2e0743d5d6e0b4d1a9e5f633c62eca3f8a86ba5dd3c471060e352ca98", + "sha256:4aa9741085f635934f3a2583e16fcf62ba835719a8b2b28fb2917bb0537c1dfa", + "sha256:4b21516d181cd77ebd06ce160ef8cc2a5e9ad35fb1c5930882baff5ac865eee7", + "sha256:4b3c1ffe10069f655ea2d731808e76e0f452fc6c749bea04781daf18e6039525", + "sha256:4c7d56b293cc071e82532f70adcbd8b61909eec973ae9d2d1f9b233f3d943f2c", + "sha256:4e9035df8d0880b2f1c7f5031f33f69e071dfe72ee9310cfc76f7b605958ceb9", + "sha256:54525ae423d7b7a8ee81ba189f131054defdb122cde31ff17477951464c1691c", + "sha256:549d19c84c55d11687ddbd47eeb348a89df9cb30e1993f1b128f4685cd0ebbf8", + "sha256:54beabb809ffcacbd9d28ac57b0db46e42a6e341a030293fb3185c409e626b8b", + "sha256:566db86717cf8080b99b58b083b773a908ae40f06681e87e589a976faf8246bf", + "sha256:5a2e2433eb9344a163aced6a5f6c9222c0786e5a9e9cac2c89f0b28433f56e23", + "sha256:5aef935237d60a51a62b86249839b51345f47564208c6ee615ed2a40878dccdd", + "sha256:604f31d97fa493083ea21bd9b92c419012531c4e17ea6da0f65cacdcf5d0bd27", + "sha256:63b20738b5aac74e239622d2fe30df4fca4942a86e31bf47a81a0e94c14df94f", + "sha256:686a0c2f85f83463272ddffd4deb5e591c98aac1897d65e92319f729c320eece", + "sha256:6a962e04b8f91f8c4e5917e518d17958e3bdee71fd1d8b88cdce74dd0ebbf434", + "sha256:6ad6d10ed9b67a382b45f29ea028f92d25bc0bc1daf6c5b801b90b5aa70fb9ec", + "sha256:6f5cb257bc2ec58f437da2b37a8cd48f666db96d47b8a3115c29f316313654ff", + "sha256:6fe79f998a4052d79e1c30eeb7d6c1c1056ad33300f682465e1b4e9b5a188b78", + "sha256:7855426dfbddac81896b6e533ebefc0af2f132d4a47340cee6d22cac7190022d", + "sha256:7d5aaac37d19b2904bb9dfe12cdb08c8443e7ba7d2852894ad448d4b8f442863", + "sha256:801e9264d19643548651b9db361ce3287176671fb0117f96b5ac0ee1c3530d53", + "sha256:81eb57278deb6098a5b62e88ad8281b2ba09f2f1147c4767522353eaa6260b31", + "sha256:824d6c50492add5da9374875ce72db7a0733b29c2394890aef23d533106e2b15", + "sha256:8397a3817d7dcdd14bb266283cd1d6fc7264a48c186b986f32e86d86d35fbac5", + "sha256:848cd2a1df56ddbffeb375535fb62c9d1645dde33ca4d51341378b3f5954429b", + "sha256:84fc30f71689d7fc9168b92788abc977dc8cefa806909565fc2951d02f6b7d57", + "sha256:8619d6915b3b0b34420cf9b2bb6d81ef59d984cb0fde7544e9ece32b4b3043c3", + "sha256:8a854227cf581330ffa2c4824d96e52ee621dd571078a252c25e3a3b3d94a1b1", + "sha256:8be9e837ea9113676e5754b43b940b50cce76d9ed7d2461df1af39a8ee674d9f", + "sha256:928cecb0ef9d5a7946eb6ff58417ad2fe9375762382f1bf5c55e61645f2c43ad", + "sha256:957b4774373cf6f709359e5c8c4a0af9f6d7875db657adb0feaf8d6cb3c3964c", + "sha256:992f18e0ea248ee03b5a6e8b3b4738850ae7dbb172cc41c966462801cbf62cf7", + "sha256:9fc5fc1eeb029757349ad26bbc5880557389a03fa6ada41703db5e068881e5f2", + "sha256:a00862fb23195b6b8322f7d781b0dc1d82cb3bcac346d1e38689370cc1cc398b", + "sha256:a3a6ed1d525bfb91b3fc9b690c5a21bb52de28c018530ad85093cc488bee2dd2", + "sha256:a6327976c7c2f4ee6816eff196e25385ccc02cb81427952414a64811037bbc8b", + "sha256:a7409f968456111140c1c95301cadf071bd30a81cbd7ab829169fb9e3d72eae9", + "sha256:a825ec844298c791fd28ed14ed1bffc56a98d15b8c58a20e0e08c1f5f2bea1be", + "sha256:a8c1df72eb746f4136fe9a2e72b0c9dc1da1cbd23b5372f94b5820ff8ae30e0e", + "sha256:a9bd00dc3bc395a662900f33f74feb3e757429e545d831eef5bb280252631984", + "sha256:aa102d6d280a5455ad6a0f9e6d769989638718e938a6a0a2ff3f4a7ff8c62cc4", + "sha256:aaaea1e536f98754a6e5c56091baa1b6ce2f2700cc4a00b0d49eca8dea471074", + "sha256:ad4d7a90a92e528aadf4965d685c17dacff3df282db1121136c382dc0b6014d2", + "sha256:b8477c1ee4bd47c57d49621a062121c3023609f7a13b8a46953eb6c9716ca392", + "sha256:ba6f52cbc7809cd8d74604cce9c14868306ae4aa0282016b641c661f981a6e91", + "sha256:bac8d525a8dbc2a1507ec731d2867025d11ceadcb4dd421423a5d42c56818541", + "sha256:bef596fdaa8f26e3d66af846bbe77057237cb6e8efff8cd7cc8dff9a62278bbf", + "sha256:c0ec0ed476f77db9fb29bca17f0a8fcc7bc97ad4c6c1d8959c507decb22e8572", + "sha256:c38c9ddb6103ceae4e4498f9c08fac9b590c5c71b0370f98714768e22ac6fa66", + "sha256:c7224cab95645c7ab53791022ae77a4509472613e839dab722a72abe5a684575", + "sha256:c74018551e31269d56fab81a728f683667e7c28c04e807ba08f8c9e3bba32f14", + "sha256:ca06675212f94e7a610e85ca36948bb8fc023e458dd6c63ef71abfd482481aa5", + "sha256:d1d2532b340b692880261c15aee4dc94dd22ca5d61b9db9a8a361953d36410b1", + "sha256:d25039a474c4c72a5ad4b52495056f843a7ff07b632c1b92ea9043a3d9950f6e", + "sha256:d5ff2c858f5f6a42c2a8e751100f237c5e869cbde669a724f2062d4c4ef93551", + "sha256:d7d7f7de27b8944f1fee2c26a88b4dabc2409d2fea7a9ed3df79b67277644e17", + "sha256:d7eeb6d22331e2fd42fce928a81c697c9ee2d51400bd1a28803965883e13cead", + "sha256:d8a1c6c0be645c745a081c192e747c5de06e944a0d21245f4cf7c05e457c36e0", + "sha256:d8b889777de69897406c9fb0b76cdf2fd0f31267861ae7501d93003d55f54fbe", + "sha256:d9e09c9d74f4566e905a0b8fa668c58109f7624db96a2171f21747abc7524234", + "sha256:db8e58b9d79200c76956cefd14d5c90af54416ff5353c5bfd7cbe58818e26ef0", + "sha256:ddb2a5c08a4eaaba605340fdee8fc08e406c56617566d9643ad8bf6852778fc7", + "sha256:e0381b4ce23ff92f8170080c97678040fc5b08da85e9e292292aba67fdac6c34", + "sha256:e23a6d84d9d1738dbc6e38167776107e63307dfc8ad108e580548d1f2c587f42", + "sha256:e516dc8baf7b380e6c1c26792610230f37147bb754d6426462ab115a02944385", + "sha256:ea65804b5dc88dacd4a40279af0cdadcfe74b3e5b4c897aa0d81cf86927fee78", + "sha256:ec61d826d80fc293ed46c9dd26995921e3a82146feacd952ef0757236fc137be", + "sha256:ee04010f26d5102399bd17f8df8bc38dc7ccd7701dc77f4a68c5b8d733406958", + "sha256:f3bc6af6e2b8f92eced34ef6a96ffb248e863af20ef4fde9448cc8c9b858b749", + "sha256:f7d6b36dd2e029b6bcb8a13cf19664c7b8e19ab3a58e0fefbb5b8461447ed5ec" ], "markers": "python_version >= '3.7'", - "version": "==1.9.2" + "version": "==1.9.4" } } } diff --git a/README.md b/README.md index 4a83c18..c4b6e94 100644 --- a/README.md +++ b/README.md @@ -2,65 +2,149 @@ # oai-pmh-harvester -CLI app for harvesting from repositories using OAI-PMH. +OAI-PMH-Harvester is a Python CLI application for harvesting metadata from repositories (also known as "Data Providers") available via the Open Archives Initiative Protocol for Metadata Harvesting (OAI-PMH). -## Harvesting +## 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 oai --help` -To install and run tests: +### Running the application on your local machine -- `make install` -- `make test` +Create a virtual environment and install dev dependencies: `make install`. -To view available commands and main options: +Additional notes: -- `pipenv run oai --help` +1. To execute the steps below, you can use the following sample url to an OAI-PMH repo: `https://aspace-staff-dev.mit.edu/oai`. -To run a harvest: +2. To write the output file to an S3 bucket, include S3 in the `-o/--output-file` argument. + * With AWS credentials: + ``` + -o s3://:@/.xml + ``` + * Wihout AWS credentials (if you have your credentials stored locally): + ``` + -o s3:///.xml + ``` -- `pipenv run oai -h [host repo oai-pmh url] -o [path to output file] harvest [any additional desired options]` +#### With Docker -## Development +1. Run `make dist-dev` to build the Docker container image. -Clone the repo and install the dependencies using [Pipenv](https://docs.pipenv.org/): +2. To run a harvest, execute the following command in your terminal: + ``` + docker run -it --volume :' oai-pmh-harvester-dev -h -o /.xml harvest + ``` -```bash -git git@github.com:MITLibraries/oai-pmh-harvester.git -cd oai-pmh-harvester -make install -``` + **Note:** The `-v/--volume` argument mounts the \ in the current directory into the container at \, which allows us to view the generated output file in \. + + +#### Without Docker -## Docker +1. To run a harvest, execute the following command in your terminal: -To build and run in docker: + ``` + pipenv run oai -h -o .xml harvest + ``` -```bash -make dist-dev -docker run -it oaiharvester +## Environment variables + +### Required + +```shell +# Set to dev for local development, this will be set to 'stage' and 'prod' in those environments by Terraform. +WORKSPACE=dev ``` -To run this locally in Docker while maintaining the ability to see the output file, you can do something like: +### Optional + +```shell +# Required only if a source has records that cause errors during a harvest and --method=get. The value provided must be a space-separated list of OAI-PMH record identifiers to skip during harvest. +RECORD_SKIP_LIST= + +# Sets the interval for logging status updates as records are written to the output file. Defaults to 1000, which will log a status update for every thousandth record. +STATUS_UPDATE_INTERVAL = 1000 -```bash -docker run -it --volume '/FULL/PATH/TO/WHERE/YOU/WANT/FILES/tmp:/app/tmp' oaiharvester -h https://aspace-staff-dev.mit.edu/oai -o tmp/out.xml harvest -m oai_ead +# If set to a valid Sentry DSN, enables Sentry exception monitoring This is not needed for local development. +SENTRY_DSN = ``` -## S3 Output +## CLI commands -You can save to s3 by passing an s3 url as the --output-file (-o) in a format like: +All CLI commands can be run with pipenv run . -```bash --o s3://AWS_KEY:AWS_SECRET_KEY@BUCKET_NAME/FILENAME.xml +### `oai` + +```text +Usage: -c [OPTIONS] COMMAND [ARGS]... + +Options: + -h, --host TEXT Hostname of server for an OAI-PMH compliant source. + [required] + -o, --output-file TEXT Filepath for generated output (either an XML file + with harvested metadata or a JSON file describing + set structure of an OAI-PMH compliant source). This + value can be a local filepath or an S3 URI. + [required] + -v, --verbose Pass to log at debug level instead of info + --help Show this message and exit. + +Commands: + harvest Harvest command to retrieve records from an OAI-PMH compliant source. + setlist Create a JSON file describing the set structure of an OAI-PMH compliant source. ``` -If you have your credentials stored locally, you can omit the passed params like: +### `oai harvest` + +```text +Usage: -c harvest [OPTIONS] + + Harvest command to retrieve records from an OAI-PMH compliant source. + +Options: + --method [get|list] Method for record retrieval. The 'list' method + is faster and should be used in most cases; + 'get' method should be used for ArchivesSpace + due to errors retrieving a full record set with + the 'list' method. [default: list] + -m, --metadata-format TEXT Alternate metadata format for harvested records. + A record should only be returned if the format + specified can be disseminated from the item + identified by the value of the identifier + argument. [default: oai_dc] + -f, --from-date TEXT Filter for files modified on or after this date; + format YYYY-MM-DD. + -u, --until-date TEXT Filter for files modified before this date; + format YYYY-MM-DD. + -s, --set-spec TEXT SetSpec of set to be harvested. Limits harvest + to records in the provided set. + -sr, --skip-record TEXT Set of OAI-PMH identifiers for records to skip + during a harvest. Only works when --method=get. + Multiple identifiers can be provided using the + syntax: '-sr oai:12345 -sr oai:67890'. Values + can also be retrieved through the + RECORD_SKIP_LIST env var (see README for more + details). + --exclude-deleted Pass to exclude deleted records from harvest. + --help Show this message and exit. +``` -```bash --o s3://BUCKET_NAME/FILENAME.xml +### `oai setlist` ``` +Usage: -c setlist [OPTIONS] + + Create a JSON file describing the set structure of an OAI-PMH compliant + source. + + Uses the OAI-PMH ListSets verbs to retrieve all sets from a repository, and + writes the set names and specs to a JSON output file. + +Options: + --help Show this message and exit. +``` + -## ENV variables -- `RECORD_SKIP_LIST` = Required if a source has records that cause errors during harvest, otherwise those records will cause the harvest process to crash. Space-separated list of OAI-PMH record identifiers to skip during harvest, e.g. `RECORD_SKIP_LIST=record1 record2`. Note: this only works if the harvest method used is "get". -- `SENTRY_DSN` = Optional in dev. If set to a valid Sentry DSN, enables Sentry exception monitoring. This is not needed for local development. -- `STATUS_UPDATE_INTERVAL` = Optional. The transform process logs the # of records transformed every nth record (1000 by default). Set this env variable to any integer to change the frequency of logging status updates. Can be useful for development/debugging. -- `WORKSPACE` = Required. Set to `dev` for local development, this will be set to `stage` and `prod` in those environments by Terraform. diff --git a/harvester/__init__.py b/harvester/__init__.py index ef910eb..618b1ca 100644 --- a/harvester/__init__.py +++ b/harvester/__init__.py @@ -1,3 +1 @@ -""" -Harvester -""" +"""Harvester.""" diff --git a/harvester/cli.py b/harvester/cli.py old mode 100755 new mode 100644 index 74ee872..65a95ee --- a/harvester/cli.py +++ b/harvester/cli.py @@ -1,44 +1,50 @@ +# ruff: noqa: FBT001 + """harvester.cli module.""" import logging import sys from datetime import timedelta from time import perf_counter +from typing import Literal import click from sickle.oaiexceptions import NoRecordsMatch -from harvester.config import configure_logger, configure_sentry +from harvester.config import Config from harvester.oai import OAIClient, write_records, write_sets logger = logging.getLogger(__name__) +CONFIG = Config() + @click.group() @click.option( "-h", "--host", required=True, - help="Hostname of OAI-PMH server to harvest from, e.g. " - "https://dspace.mit.edu/oai/request.", + help="Hostname of server for an OAI-PMH compliant source.", ) @click.option( "-o", "--output-file", required=True, - help="Filepath to write output to. Can be a local filepath or an S3 URI, e.g. " - "S3://bucketname/filename.xml.", + help="Filepath for generated output (either an XML file with harvested metadata or " + "a JSON file describing set structure of an OAI-PMH compliant source). " + "This value can be a local filepath or an S3 URI.", +) +@click.option( + "-v", "--verbose", help="Pass to log at debug level instead of info", is_flag=True ) -@click.option("-v", "--verbose", help="Optional: enable debug output.", is_flag=True) @click.pass_context -def main(ctx, host, output_file, verbose): +def main(ctx: click.Context, host: str, output_file: str, verbose: bool) -> None: ctx.ensure_object(dict) ctx.obj["START_TIME"] = perf_counter() ctx.obj["HOST"] = host ctx.obj["OUTPUT_FILE"] = output_file - - root_logger = logging.getLogger() - logger.info(configure_logger(root_logger, verbose)) - logger.info(configure_sentry()) + logger.info(CONFIG.configure_logger(verbose)) + logger.info(CONFIG.configure_sentry()) + CONFIG.check_required_env_vars() @main.command() @@ -46,7 +52,7 @@ def main(ctx, host, output_file, verbose): "--method", default="list", show_default=True, - help="Record retrieval method to use. Default 'list' method is faster and should " + help="Method for record retrieval. The 'list' method is faster and should " "be used in most cases; 'get' method should be used for ArchivesSpace due to " "errors retrieving a full record set with the 'list' method.", type=click.Choice(["get", "list"], case_sensitive=False), @@ -56,29 +62,28 @@ def main(ctx, host, output_file, verbose): "--metadata-format", default="oai_dc", show_default=True, - help="Optional: specify alternate metadata format for harvested records (e.g. " - "mods, mets, oai_dc, qdc, ore).", + help="Alternate metadata format for harvested records. A record should only be " + "returned if the format specified can be disseminated from the item identified " + "by the value of the identifier argument.", ) @click.option( "-f", "--from-date", default=None, - help="Optional: starting date to harvest records from, in format YYYY-MM-DD. " - "Limits harvest to records added/updated on or after the provided date.", + help="Filter for files modified on or after this date; format YYYY-MM-DD.", ) @click.option( "-u", "--until-date", default=None, - help="Optional: ending date to harvest records from, in format YYYY-MM-DD. " - "Limits harvest to records added/updated on or before the provided date.", + help="Filter for files modified before this date; format YYYY-MM-DD.", ) @click.option( "-s", "--set-spec", default=None, show_default=True, - help="Optional: SetSpec of set to be harvested. Limits harvest to records in the " + help="SetSpec of set to be harvested. Limits harvest to records in the " "provided set.", ) @click.option( @@ -87,28 +92,28 @@ def main(ctx, host, output_file, verbose): envvar="RECORD_SKIP_LIST", multiple=True, show_default=True, - help="Optional: OAI-PMH identifier of record to skip during harvest. Only works if " - "the harvest method used is 'get'. Can be repeated to skip multiple records, e.g. " - "'-sr oai:12345 -sr oai:67890'. Can also be set via ENV variable, see README for " - "details.", + help="Set of OAI-PMH identifiers for records to skip during a harvest. Only works " + "when --method=get. Multiple identifiers can be provided using the syntax: " + "'-sr oai:12345 -sr oai:67890'. Values can also be retrieved through the " + "RECORD_SKIP_LIST env var (see README for more details).", ) @click.option( "--exclude-deleted", - help="Optional: exclude deleted records from harvest.", + help="Pass to exclude deleted records from harvest.", is_flag=True, ) @click.pass_context def harvest( - ctx, - method, - metadata_format, - from_date, - until_date, - set_spec, - skip_record, - exclude_deleted, -): - """Harvest records from an OAI-PMH compliant source and write to an output file.""" + ctx: click.Context, + method: Literal["get", "list"], + metadata_format: str, + from_date: str, + until_date: str, + set_spec: str, + skip_record: tuple[str] | None, + exclude_deleted: bool, +) -> None: + """Harvest command to retrieve records from an OAI-PMH compliant source.""" logger.info( "OAI-PMH harvesting from source %s with parameters: method=%s, " "metadata_format=%s, from_date=%s, until_date=%s, set=%s, skip_record=%s, " @@ -126,7 +131,7 @@ def harvest( oai_client = OAIClient( ctx.obj["HOST"], metadata_format, from_date, until_date, set_spec ) - if method == "list" and len(skip_record) > 0: + if method == "list" and skip_record: logger.warning( "Option --skip-record only works with the 'get' --method option, these " "records will not be skipped during harvest: %s", @@ -158,8 +163,8 @@ def harvest( @main.command() @click.pass_context -def setlist(ctx): - """Get set info from an OAI-PMH compliant source and write to an output file. +def setlist(ctx: click.Context) -> None: + """Create a JSON file describing the set structure of an OAI-PMH compliant source. Uses the OAI-PMH ListSets verbs to retrieve all sets from a repository, and writes the set names and specs to a JSON output file. diff --git a/harvester/config.py b/harvester/config.py index 084aa26..1af6a1f 100644 --- a/harvester/config.py +++ b/harvester/config.py @@ -1,6 +1,9 @@ +# ruff: noqa: FBT001 + """harvester.config module.""" import logging import os +from typing import Any import sentry_sdk @@ -10,30 +13,60 @@ MAX_ALLOWED_ERRORS = 10 -def configure_logger(logger: logging.Logger, verbose: bool) -> str: - if verbose: - logging.basicConfig( - format="%(asctime)s %(levelname)s %(name)s.%(funcName)s() line %(lineno)d: " - "%(message)s" - ) - logger.setLevel(logging.DEBUG) +class Config: + REQUIRED_ENV_VARS = ("WORKSPACE", "SENTRY_DSN") + OPTIONAL_ENV_VARS = ("RECORD_SKIP_LIST", "STATUS_UPDATE_INTERVAL") + + def __init__(self, logger: logging.Logger | None = None): + """Set root logger as default when creating class instance.""" + if logger is None: + self.logger = logging.getLogger() + else: + self.logger = logger + + def check_required_env_vars(self) -> None: + """Method to raise exception if required env vars not set.""" + missing_vars = [var for var in self.REQUIRED_ENV_VARS if not os.getenv(var)] + if missing_vars: + message = f"Missing required environment variables: {', '.join(missing_vars)}" + raise OSError(message) + + def configure_logger(self, verbose: bool) -> str: for handler in logging.root.handlers: - handler.addFilter(logging.Filter("harvester")) - else: - logging.basicConfig( - format=("%(asctime)s %(levelname)s %(name)s.%(funcName)s(): %(message)s") + handler.filters.clear() + if verbose: + logging.basicConfig( + format="%(asctime)s %(levelname)s %(name)s.%(funcName)s() " + "line %(lineno)d: %(message)s" + ) + self.logger.setLevel(logging.DEBUG) + for handler in logging.root.handlers: + handler.addFilter(logging.Filter("harvester")) + else: + logging.basicConfig( + format=("%(asctime)s %(levelname)s %(name)s.%(funcName)s(): %(message)s") + ) + self.logger.setLevel(logging.INFO) + return ( + f"Logger '{self.logger.name}' configured with level=" + f"{logging.getLevelName(self.logger.getEffectiveLevel())}" ) - logger.setLevel(logging.INFO) - return ( - f"Logger '{logger.name}' configured with level=" - f"{logging.getLevelName(logger.getEffectiveLevel())}" - ) - - -def configure_sentry() -> str: - env = os.getenv("WORKSPACE") - sentry_dsn = os.getenv("SENTRY_DSN") - if sentry_dsn and sentry_dsn.lower() != "none": - sentry_sdk.init(sentry_dsn, environment=env) - return f"Sentry DSN found, exceptions will be sent to Sentry with env={env}" - return "No Sentry DSN found, exceptions will not be sent to Sentry" + + def configure_sentry(self) -> str: + sentry_dsn = self.SENTRY_DSN + if sentry_dsn and sentry_dsn.lower() != "none": + sentry_sdk.init(sentry_dsn, environment=self.WORKSPACE) + return ( + "Sentry DSN found, exceptions will be sent to Sentry with env=" + f"{self.WORKSPACE}" + ) + return "No Sentry DSN found, exceptions will not be sent to Sentry" + + def __getattr__(self, name: str) -> Any: # noqa: ANN401 + """Provide dot notation access to configurations and env vars on this class.""" + if name in self.REQUIRED_ENV_VARS or name in self.OPTIONAL_ENV_VARS: + if name == "STATUS_UPDATE_INTERVAL": + return os.getenv(name, "1000") + return os.getenv(name) + message = f"'{name}' not a valid configuration variable" + raise AttributeError(message) diff --git a/harvester/exceptions.py b/harvester/exceptions.py index 4b629e7..bb7d252 100644 --- a/harvester/exceptions.py +++ b/harvester/exceptions.py @@ -1,7 +1,7 @@ +# ruff: noqa: N818 + """exceptions.py module.""" class MaxAllowedErrorsReached(Exception): """Thrown when maximum numbers of errors reached during GetRecords harvest.""" - - pass diff --git a/harvester/oai.py b/harvester/oai.py index fa60569..9627c65 100644 --- a/harvester/oai.py +++ b/harvester/oai.py @@ -1,37 +1,42 @@ +# ruff: noqa: FBT001, UP012 + """oai.py module.""" import json import logging -import os -from typing import Any, Iterator, Literal, Optional +from collections.abc import Iterator +from typing import Any, Literal -from requests import HTTPError import smart_open +from requests import HTTPError from sickle import Sickle from sickle.models import Record from sickle.oaiexceptions import IdDoesNotExist, OAIError from harvester.config import ( DEFAULT_RETRY_AFTER, + MAX_ALLOWED_ERRORS, MAX_RETRIES, RETRY_STATUS_CODES, - MAX_ALLOWED_ERRORS, + Config, ) from harvester.exceptions import MaxAllowedErrorsReached from harvester.utils import send_sentry_message logger = logging.getLogger(__name__) +CONFIG = Config() + class OAIClient: def __init__( self, source_url: str, - metadata_format: Optional[str] = None, - from_date: Optional[str] = None, - until_date: Optional[str] = None, - set_spec: Optional[str] = None, - max_retries: Optional[int] = MAX_RETRIES, + metadata_format: str | None = None, + from_date: str | None = None, + until_date: str | None = None, + set_spec: str | None = None, + max_retries: int | None = MAX_RETRIES, retry_status_codes: list[int] = RETRY_STATUS_CODES, ) -> None: self.source_url = source_url @@ -46,10 +51,10 @@ def __init__( def _set_params( self, - metadata_format: Optional[str], - from_date: Optional[str], - until_date: Optional[str], - set_spec: Optional[str], + metadata_format: str | None = None, + from_date: str | None = None, + until_date: str | None = None, + set_spec: str | None = None, ) -> None: params = {} if metadata_format: @@ -72,7 +77,7 @@ def get_identifiers(self, exclude_deleted: bool) -> Iterator[str]: def get_records( self, identifiers: Iterator[str], - skip_list: Optional[tuple[str]] = None, + skip_list: tuple[str] | None = None, max_allowed_errors: int = MAX_ALLOWED_ERRORS, ) -> Iterator[Record]: failed_records: list[tuple[str, Any | str | None]] = [] @@ -121,10 +126,12 @@ def get_records( {"failed_records": failed_records}, ) - def get_sets(self): + def get_sets(self) -> list[dict]: responses = self.client.ListSets() - sets = [{"Set name": set.setName, "Set spec": set.setSpec} for set in responses] - return sets + return [ + {"Set name": response_set.setName, "Set spec": response_set.setSpec} + for response_set in responses + ] def list_records(self, exclude_deleted: bool) -> Iterator[Record]: return self.client.ListRecords(ignore_deleted=exclude_deleted, **self.params) @@ -133,17 +140,16 @@ def retrieve_records( self, method: Literal["get", "list"], exclude_deleted: bool, - skip_records: Optional[tuple[str]] = None, + skip_records: tuple[str] | None = None, ) -> Iterator[Record]: if method == "get": identifiers = self.get_identifiers(exclude_deleted) return self.get_records(identifiers, skip_list=skip_records) - elif method == "list": + if method == "list": return self.list_records(exclude_deleted) - else: - raise ValueError( - f'Method must be either "get" or "list", method provided was "{method}"' - ) + + message = f'Method must be either "get" or "list", method provided was "{method}"' + raise ValueError(message) def write_records(records: Iterator, filepath: str) -> int: @@ -153,7 +159,7 @@ def write_records(records: Iterator, filepath: str) -> int: for record in records: file.write(" ".encode() + record.raw.encode() + "\n".encode()) count += 1 - if count % int(os.getenv("STATUS_UPDATE_INTERVAL", "1000")) == 0: + if count % int(CONFIG.STATUS_UPDATE_INTERVAL) == 0: logger.info( "Status update: %s records written to output file so far!", count, diff --git a/harvester/utils.py b/harvester/utils.py index 1ab4880..f0311b4 100644 --- a/harvester/utils.py +++ b/harvester/utils.py @@ -7,7 +7,7 @@ def send_sentry_message( message: str, scopes: dict | None = None, level: str = "warning", -): +) -> str | None: """Send message directly to Sentry. This allows both reporting information without raising an Exception, and optionally @@ -24,5 +24,4 @@ def send_sentry_message( if scopes: for scope_key, scope_value in scopes.items(): scope.set_extra(scope_key, scope_value) - send_receipt = sentry_sdk.capture_message(message, level=level) - return send_receipt + return sentry_sdk.capture_message(message, level=level) diff --git a/pyproject.toml b/pyproject.toml new file mode 100644 index 0000000..5035250 --- /dev/null +++ b/pyproject.toml @@ -0,0 +1,73 @@ +[tool.black] +line-length = 90 + +[tool.mypy] +disallow_untyped_calls = true +disallow_untyped_defs = true +exclude = ["tests/"] + +[[tool.mypy.overrides]] +module = [ + "setuptools", + "sickle.*", + "smart_open" +] +ignore_missing_imports=true + +[tool.pytest.ini_options] +log_level = "INFO" + +[tool.ruff] +target-version = "py311" +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"] + +# set max line length +line-length = 90 + +# enumerate all fixed violations +show-fixes = true + +[tool.ruff.flake8-annotations] +mypy-init-return = true + +[tool.ruff.flake8-pytest-style] +fixture-parentheses = false + +[tool.ruff.per-file-ignores] +"tests/**/*" = [ + "ANN", + "ARG001", + "S101", +] + +[tool.ruff.pycodestyle] +max-doc-length = 90 + +[tool.ruff.pydocstyle] +convention = "google" \ No newline at end of file diff --git a/setup.cfg b/setup.cfg deleted file mode 100644 index a87de27..0000000 --- a/setup.cfg +++ /dev/null @@ -1,20 +0,0 @@ -[flake8] -max-line-length = 90 -extend-ignore = E203 - -[isort] -profile = black - -[mypy] - -[mypy-sickle.*] -ignore_missing_imports = True - -[mypy-smart_open.*] -ignore_missing_imports = True - -[mypy-requests.*] -ignore_missing_imports = True - -[tool:pytest] -log_level = DEBUG diff --git a/tests/__init__.py b/tests/__init__.py new file mode 100644 index 0000000..d252439 --- /dev/null +++ b/tests/__init__.py @@ -0,0 +1 @@ +"""test package.""" diff --git a/tests/conftest.py b/tests/conftest.py index be7093f..29ff60c 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -1,20 +1,25 @@ -import os +from unittest.mock import patch import pytest from click.testing import CliRunner -from unittest.mock import patch + +from harvester.config import Config @pytest.fixture(autouse=True) -def test_env(): - os.environ = {"WORKSPACE": "test"} - yield +def _test_env(monkeypatch): + monkeypatch.setenv("SENTRY_DSN", "None") + monkeypatch.setenv("WORKSPACE", "test") + + +@pytest.fixture +def config(): + return Config() @pytest.fixture -def cli_runner(): - runner = CliRunner() - return runner +def runner(): + return CliRunner() @pytest.fixture diff --git a/tests/test_cli.py b/tests/test_cli.py index 8e123e4..9777f2e 100644 --- a/tests/test_cli.py +++ b/tests/test_cli.py @@ -4,11 +4,11 @@ @vcr.use_cassette("tests/fixtures/vcr_cassettes/cli-all-options-except-set-spec.yaml") -def test_harvest_all_options_except_set_spec(caplog, monkeypatch, cli_runner, tmp_path): +def test_harvest_all_options_except_set_spec(caplog, monkeypatch, runner, tmp_path): monkeypatch.setenv("SENTRY_DSN", "https://1234567890@00000.ingest.sentry.io/123456") - with cli_runner.isolated_filesystem(temp_dir=tmp_path): + with runner.isolated_filesystem(temp_dir=tmp_path): filepath = tmp_path / "records.xml" - result = cli_runner.invoke( + result = runner.invoke( main, [ "-h", @@ -56,10 +56,10 @@ def test_harvest_all_options_except_set_spec(caplog, monkeypatch, cli_runner, tm @vcr.use_cassette("tests/fixtures/vcr_cassettes/harvest-from-set.yaml") -def test_harvest_no_options_except_set_spec(caplog, cli_runner, tmp_path): - with cli_runner.isolated_filesystem(temp_dir=tmp_path): +def test_harvest_no_options_except_set_spec(caplog, runner, tmp_path): + with runner.isolated_filesystem(temp_dir=tmp_path): filepath = tmp_path / "records.xml" - result = cli_runner.invoke( + result = runner.invoke( main, [ "-h", @@ -73,9 +73,7 @@ def test_harvest_no_options_except_set_spec(caplog, cli_runner, tmp_path): ) assert result.exit_code == 0 assert "Logger 'root' configured with level=INFO" in caplog.text - assert ( - "No Sentry DSN found, exceptions will not be sent to Sentry" in caplog.text - ) + assert "No Sentry DSN found, exceptions will not be sent to Sentry" in caplog.text assert ( "OAI-PMH harvesting from source https://dspace.mit.edu/oai/request with " "parameters: method=list, metadata_format=oai_dc, from_date=None, " @@ -90,10 +88,10 @@ def test_harvest_no_options_except_set_spec(caplog, cli_runner, tmp_path): @vcr.use_cassette("tests/fixtures/vcr_cassettes/harvest-get-method-no-records.yaml") -def test_harvest_no_records_get_method(caplog, cli_runner, tmp_path): - with cli_runner.isolated_filesystem(temp_dir=tmp_path): +def test_harvest_no_records_get_method(caplog, runner, tmp_path): + with runner.isolated_filesystem(temp_dir=tmp_path): filepath = tmp_path / "records.xml" - result = cli_runner.invoke( + result = runner.invoke( main, [ "-h", @@ -115,10 +113,10 @@ def test_harvest_no_records_get_method(caplog, cli_runner, tmp_path): @vcr.use_cassette("tests/fixtures/vcr_cassettes/harvest-list-method-no-records.yaml") -def test_harvest_no_records_list_method(caplog, cli_runner, tmp_path): - with cli_runner.isolated_filesystem(temp_dir=tmp_path): +def test_harvest_no_records_list_method(caplog, runner, tmp_path): + with runner.isolated_filesystem(temp_dir=tmp_path): filepath = tmp_path / "records.xml" - result = cli_runner.invoke( + result = runner.invoke( main, [ "-h", @@ -138,10 +136,10 @@ def test_harvest_no_records_list_method(caplog, cli_runner, tmp_path): @vcr.use_cassette("tests/fixtures/vcr_cassettes/harvest-from-set.yaml") -def test_harvest_list_method_and_skip_record_logs_warning(caplog, cli_runner, tmp_path): - with cli_runner.isolated_filesystem(temp_dir=tmp_path): +def test_harvest_list_method_and_skip_record_logs_warning(caplog, runner, tmp_path): + with runner.isolated_filesystem(temp_dir=tmp_path): filepath = tmp_path / "records.xml" - result = cli_runner.invoke( + result = runner.invoke( main, [ "-h", @@ -163,10 +161,10 @@ def test_harvest_list_method_and_skip_record_logs_warning(caplog, cli_runner, tm @vcr.use_cassette("tests/fixtures/vcr_cassettes/get-sets.yaml") -def test_setlist(caplog, cli_runner, tmp_path): - with cli_runner.isolated_filesystem(temp_dir=tmp_path): +def test_setlist(caplog, runner, tmp_path): + with runner.isolated_filesystem(temp_dir=tmp_path): filepath = tmp_path / "sets.json" - result = cli_runner.invoke( + result = runner.invoke( main, [ "-h", diff --git a/tests/test_config.py b/tests/test_config.py index 49d7afb..e6e7060 100644 --- a/tests/test_config.py +++ b/tests/test_config.py @@ -1,35 +1,70 @@ import logging -from harvester.config import configure_logger, configure_sentry +import pytest +from harvester.config import Config -def test_configure_logger_not_verbose(): - logger = logging.getLogger(__name__) - result = configure_logger(logger, verbose=False) - assert logger.getEffectiveLevel() == 20 - assert result == "Logger 'test_config' configured with level=INFO" + +def test_configure_logger_defaults_to_root_logger(): + config = Config(logger=None) + assert config.logger.name == "root" -def test_configure_logger_verbose(caplog): +def test_configure_logger_accepts_specific_logger(): logger = logging.getLogger(__name__) - result = configure_logger(logger, verbose=True) - assert logger.getEffectiveLevel() == 10 - assert result == "Logger 'test_config' configured with level=DEBUG" + config = Config(logger=logger) + assert config.logger.name == "tests.test_config" + + +def test_configure_logger_not_verbose(config): + result = config.configure_logger(verbose=False) + assert config.logger.getEffectiveLevel() == int(logging.INFO) + assert result == "Logger 'root' configured with level=INFO" -def test_configure_sentry_no_env_variable(monkeypatch): +def test_configure_logger_verbose(config): + result = config.configure_logger(verbose=True) + assert config.logger.getEffectiveLevel() == int(logging.DEBUG) + assert result == "Logger 'root' configured with level=DEBUG" + + +def test_configure_sentry_no_env_variable(config, monkeypatch): + config.configure_logger(verbose=False) monkeypatch.delenv("SENTRY_DSN", raising=False) - result = configure_sentry() + result = config.configure_sentry() assert result == "No Sentry DSN found, exceptions will not be sent to Sentry" -def test_configure_sentry_env_variable_is_none(monkeypatch): +def test_configure_sentry_env_variable_is_none(config, monkeypatch): monkeypatch.setenv("SENTRY_DSN", "None") - result = configure_sentry() + result = config.configure_sentry() assert result == "No Sentry DSN found, exceptions will not be sent to Sentry" -def test_configure_sentry_env_variable_is_dsn(monkeypatch): +def test_configure_sentry_env_variable_is_dsn(config, monkeypatch): monkeypatch.setenv("SENTRY_DSN", "https://1234567890@00000.ingest.sentry.io/123456") - result = configure_sentry() + result = config.configure_sentry() assert result == "Sentry DSN found, exceptions will be sent to Sentry with env=test" + + +def test_config_check_required_env_vars_success(config): + config.check_required_env_vars() + + +def test_config_check_required_env_vars_error(config, monkeypatch): + monkeypatch.delenv("SENTRY_DSN") + with pytest.raises( + OSError, match="Missing required environment variables: SENTRY_DSN" + ): + config.check_required_env_vars() + + +def test_config_env_var_access_success(config): + assert config.STATUS_UPDATE_INTERVAL == "1000" + + +def test_config_env_var_access_error(config): + with pytest.raises( + AttributeError, match="'DOES_NOT_EXIST' not a valid configuration variable" + ): + _ = config.DOES_NOT_EXIST diff --git a/tests/test_oai.py b/tests/test_oai.py index 7579d50..63e1d8b 100644 --- a/tests/test_oai.py +++ b/tests/test_oai.py @@ -43,8 +43,9 @@ def test_get_identifiers(): until_date="2022-01-10", set_spec="hdl_1721.1_49432", ) - identifiers = list(oai_client.get_identifiers(False)) - assert len(identifiers) == 241 + identifiers = list(oai_client.get_identifiers(exclude_deleted=False)) + expected_identifiers_count = 241 + assert len(identifiers) == expected_identifiers_count assert "oai:dspace.mit.edu:1721.1/137340.2" in identifiers @@ -57,8 +58,8 @@ def test_get_identifiers_no_matches_raises_exception(): until_date="2021-12-26", set_spec="hdl_1721.1_49432", ) + identifiers = oai_client.get_identifiers(exclude_deleted=False) with pytest.raises(NoRecordsMatch): - identifiers = oai_client.get_identifiers(False) next(identifiers) @@ -187,7 +188,10 @@ def test_retrieve_records_list_method(): def test_retrieve_records_wrong_method_raises_error(): oai_client = OAIClient("https://dspace.mit.edu/oai/request") - with pytest.raises(ValueError): + with pytest.raises( + ValueError, + match='Method must be either "get" or "list", method provided was "wrong"', + ): oai_client.retrieve_records(method="wrong", exclude_deleted=False) @@ -209,7 +213,8 @@ def test_write_records(caplog, monkeypatch, tmp_path): '\n\n \n") - assert count == 44 + expected_records_count = 44 + assert count == expected_records_count assert "Status update: 40 records written to output file so far!" in caplog.text @@ -230,7 +235,7 @@ def test_complete_harvest_with_skipped_errors_and_report(mock_sentry_capture_mes oai_client = OAIClient( "https://dspace.mit.edu/oai/request", metadata_format="oai_dc", - retry_status_codes=tuple(), # skip retrying any HTTP codes + retry_status_codes=(), # skip retrying any HTTP codes ) identifiers = [ "oai:dspace.mit.edu:1721.1/152958", @@ -238,8 +243,9 @@ def test_complete_harvest_with_skipped_errors_and_report(mock_sentry_capture_mes "oai:dspace.mit.edu:1721.1/147573", # threw 500 error at time of recording "oai:dspace.mit.edu:1721.1/152939", ] - records = list(oai_client.get_records((identifier for identifier in identifiers))) - assert len(records) == 2 + records = list(oai_client.get_records(identifier for identifier in identifiers)) + expected_records_count = 2 + assert len(records) == expected_records_count assert mock_sentry_capture_message.called @@ -250,7 +256,7 @@ def test_aborted_harvest_with_max_errors_reached_and_report( oai_client = OAIClient( "https://dspace.mit.edu/oai/request", metadata_format="oai_dc", - retry_status_codes=tuple(), + retry_status_codes=(), ) identifiers = [ "oai:dspace.mit.edu:1721.1/152958",