diff --git a/.dockerignore b/.dockerignore new file mode 100644 index 0000000..d37c967 --- /dev/null +++ b/.dockerignore @@ -0,0 +1,157 @@ +# Byte-compiled / optimized / DLL files +__pycache__/ +*.py[cod] +*$py.class + +# C extensions +*.so + +# Distribution / packaging +.Python +build/ +develop-eggs/ +dist/ +downloads/ +eggs/ +.eggs/ +lib/ +lib64/ +parts/ +sdist/ +var/ +wheels/ +share/python-wheels/ +*.egg-info/ +.installed.cfg +*.egg +MANIFEST + +# PyInstaller +# Usually these files are written by a python script from a template +# before PyInstaller builds the exe, so as to inject date/other infos into it. +*.manifest +*.spec + +# Installer logs +pip-log.txt +pip-delete-this-directory.txt + +# Unit test / coverage reports +htmlcov/ +.tox/ +.nox/ +.coverage +.coverage.* +.cache +nosetests.xml +coverage.xml +*.cover +*.py,cover +.hypothesis/ +.pytest_cache/ +cover/ +coverage/ + +# Translations +*.mo +*.pot + +# Django stuff: +*.log +local_settings.py +db.sqlite3 +db.sqlite3-journal + +# Flask stuff: +instance/ +.webassets-cache + +# Scrapy stuff: +.scrapy + +# Sphinx documentation +docs/_build/ + +# PyBuilder +.pybuilder/ +target/ + +# Jupyter Notebook +.ipynb_checkpoints + +# IPython +profile_default/ +ipython_config.py + +# pyenv +# For a library or package, you might want to ignore these files since the code is +# intended to run in multiple environments; otherwise, check them in: +# .python-version + +# pipenv +# According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control. +# However, in case of collaboration, if having platform-specific dependencies or dependencies +# having no cross-platform support, pipenv may install dependencies that don't work, or not +# install all needed dependencies. +#Pipfile.lock + +# poetry +# Similar to Pipfile.lock, it is generally recommended to include poetry.lock in version control. +# This is especially recommended for binary packages to ensure reproducibility, and is more +# commonly ignored for libraries. +# https://python-poetry.org/docs/basic-usage/#commit-your-poetrylock-file-to-version-control +#poetry.lock + +# PEP 582; used by e.g. github.com/David-OConnor/pyflow +__pypackages__/ + +# Celery stuff +celerybeat-schedule +celerybeat.pid + +# SageMath parsed files +*.sage.py + +# Environments +.env +.venv +env/ +venv/ +ENV/ +env.bak/ +venv.bak/ + +# Spyder project settings +.spyderproject +.spyproject + +# Rope project settings +.ropeproject + +# mkdocs documentation +/site + +# mypy +.mypy_cache/ +.dmypy.json +dmypy.json + +# Pyre type checker +.pyre/ + +# pytype static type analyzer +.pytype/ + +# Cython debug symbols +cython_debug/ + +# PyCharm +# JetBrains specific template is maintainted in a separate JetBrains.gitignore that can +# be found at https://github.com/github/gitignore/blob/main/Global/JetBrains.gitignore +# and can be added to the global gitignore or merged into this file. For a more nuclear +# option (not recommended) you can uncomment the following to ignore the entire idea folder. +#.idea/ + +.DS_Store +output/ +.vscode/ diff --git a/Pipfile.lock b/Pipfile.lock index 37aeb13..309eb38 100644 --- a/Pipfile.lock +++ b/Pipfile.lock @@ -18,19 +18,19 @@ "default": { "boto3": { "hashes": [ - "sha256:0c593017fa49dbc34dcdbd5659208f2daf293a499d5f4d7e61978cd6b5d72a97", - "sha256:488bf63d65864ab7fcdf9337c5aa4d825d444e253738a60f80789916bacc47dc" + "sha256:898fa38387e2f5a9136aec6cf9caa0a44ddbf78f1c4aaa33f7b1611ac44b4a4d", + "sha256:a631ce2d22662f326cdf121967b53c251338e4ae7cdfa166c11a31e94a8b6a60" ], "index": "pypi", - "version": "==1.26.78" + "version": "==1.26.81" }, "botocore": { "hashes": [ - "sha256:2bee6ed037590ef1e4884d944486232871513915f12a8590c63e3bb6046479bf", - "sha256:656ac8822a1b6c887a8efe1172bcefa9c9c450face26dc39998a249e8c340a23" + "sha256:b0b9f4b784e6e58e52f6756bd28d32240016377659adc04e0a0a12df2a90e3f9", + "sha256:b140c865fce64097c54cba37420c27f24b26cdec0fc00ce83ad439d3bdb053e1" ], "markers": "python_version >= '3.7'", - "version": "==1.29.78" + "version": "==1.29.81" }, "certifi": { "hashes": [ @@ -183,11 +183,11 @@ }, "sentry-sdk": { "hashes": [ - "sha256:69ecbb2e1ff4db02a06c4f20f6f69cb5dfe3ebfbc06d023e40d77cf78e9c37e7", - "sha256:7ad4d37dd093f4a7cb5ad804c6efe9e8fab8873f7ffc06042dc3f3fd700a93ec" + "sha256:633edefead34d976ff22e7edc367cdf57768e24bc714615ccae746d9d91795ae", + "sha256:a900845bd78c263d49695d48ce78a4bce1030bbd917e0b6cc021fc000c901113" ], "index": "pypi", - "version": "==1.15.0" + "version": "==1.16.0" }, "six": { "hashes": [ @@ -264,35 +264,35 @@ }, "boto3": { "hashes": [ - "sha256:0c593017fa49dbc34dcdbd5659208f2daf293a499d5f4d7e61978cd6b5d72a97", - "sha256:488bf63d65864ab7fcdf9337c5aa4d825d444e253738a60f80789916bacc47dc" + "sha256:898fa38387e2f5a9136aec6cf9caa0a44ddbf78f1c4aaa33f7b1611ac44b4a4d", + "sha256:a631ce2d22662f326cdf121967b53c251338e4ae7cdfa166c11a31e94a8b6a60" ], "index": "pypi", - "version": "==1.26.78" + "version": "==1.26.81" }, "boto3-stubs": { "hashes": [ - "sha256:0d48657c3f47b6455144020a11dab7d6049737136c5c85c9e07939696785d146", - "sha256:e8f1996078a696f88ba7ac1c39e3427cee82b9c9675150121a458ecc0a24e325" + "sha256:2d7ee70832b6d1a4c9192dd5f22d78fe95d1c0ffe830032bd73700c636dba96b", + "sha256:e44fc3e4b326777b68ea27d6e3d7228331051d3a7b05bb4fd829d7b8069be4a5" ], "index": "pypi", - "version": "==1.26.78" + "version": "==1.26.81" }, "botocore": { "hashes": [ - "sha256:2bee6ed037590ef1e4884d944486232871513915f12a8590c63e3bb6046479bf", - "sha256:656ac8822a1b6c887a8efe1172bcefa9c9c450face26dc39998a249e8c340a23" + "sha256:b0b9f4b784e6e58e52f6756bd28d32240016377659adc04e0a0a12df2a90e3f9", + "sha256:b140c865fce64097c54cba37420c27f24b26cdec0fc00ce83ad439d3bdb053e1" ], "markers": "python_version >= '3.7'", - "version": "==1.29.78" + "version": "==1.29.81" }, "botocore-stubs": { "hashes": [ - "sha256:48a5dd2496e2480bdcc45bdf06407d14680e7330dd3809e404b46b1efc59ecff", - "sha256:9eb8e43e7cb74f07cfe8bf7ca680e0ce111bd5d8490ed6bece5e435ac487b403" + "sha256:4b2d6020aea659f92826d2ef778beb49179918989ff1383a186c6ef0f97afb5c", + "sha256:f351120e07115d34692d63a28d47a8867f2e8f43e1736e9a345ad3abb2a15843" ], "markers": "python_version >= '3.7' and python_version < '4.0'", - "version": "==1.29.78" + "version": "==1.29.81" }, "certifi": { "hashes": [ @@ -589,10 +589,10 @@ }, "eradicate": { "hashes": [ - "sha256:8bfaca181db9227dc88bdbce4d051a9627604c2243e7d85324f6d6ce0fd08bb2", - "sha256:aac7384ab25b1bf21c4c012de9b4bf8398945a14c98c911545b2ea50ab558014" + "sha256:751813c315a48ce7e3d0483410991015342d380a956e86e0265c61bfb875bcbc", + "sha256:c329a05def6a4b558dab58bb1b694f5209706b7c99ba174d226dfdb69a5ba0da" ], - "version": "==2.1.0" + "version": "==2.2.0" }, "freezegun": { "hashes": [ @@ -1073,11 +1073,11 @@ }, "types-requests": { "hashes": [ - "sha256:232792870b60adb07d23175451ab4e6190021b0c584edf052d92d9b993118f06", - "sha256:f84613b0d4c5d0eeb7879dfa05e14a3702b9c1f7a4ee81dfe9b4321b13fe93a1" + "sha256:a05e4c7bc967518fba5789c341ea8b0c942776ee474c7873129a61161978e586", + "sha256:fc8eaa09cc014699c6b63c60c2e3add0c8b09a410c818b5ac6e65f92a26dde09" ], "index": "pypi", - "version": "==2.28.11.14" + "version": "==2.28.11.15" }, "types-s3transfer": { "hashes": [ @@ -1096,10 +1096,10 @@ }, "types-urllib3": { "hashes": [ - "sha256:28d2d7f5c31ff8ed4d9d2e396ce906c49d37523c3ec207d03d3b1695755a7199", - "sha256:df4d3e5472bf8830bd74eac12d56e659f88662ba040c7d106bf3a5bee26fff28" + "sha256:95ea847fbf0bf675f50c8ae19a665baedcf07e6b4641662c4c3c72e7b2edf1a9", + "sha256:ecf43c42d8ee439d732a1110b4901e9017a79a38daca26f08e42c8460069392c" ], - "version": "==1.26.25.7" + "version": "==1.26.25.8" }, "typing-extensions": { "hashes": [ @@ -1134,73 +1134,84 @@ }, "wrapt": { "hashes": [ - "sha256:00b6d4ea20a906c0ca56d84f93065b398ab74b927a7a3dbd470f6fc503f95dc3", - "sha256:01c205616a89d09827986bc4e859bcabd64f5a0662a7fe95e0d359424e0e071b", - "sha256:02b41b633c6261feff8ddd8d11c711df6842aba629fdd3da10249a53211a72c4", - "sha256:07f7a7d0f388028b2df1d916e94bbb40624c59b48ecc6cbc232546706fac74c2", - "sha256:11871514607b15cfeb87c547a49bca19fde402f32e2b1c24a632506c0a756656", - "sha256:1b376b3f4896e7930f1f772ac4b064ac12598d1c38d04907e696cc4d794b43d3", - "sha256:21ac0156c4b089b330b7666db40feee30a5d52634cc4560e1905d6529a3897ff", - "sha256:257fd78c513e0fb5cdbe058c27a0624c9884e735bbd131935fd49e9fe719d310", - "sha256:2b39d38039a1fdad98c87279b48bc5dce2c0ca0d73483b12cb72aa9609278e8a", - "sha256:2cf71233a0ed05ccdabe209c606fe0bac7379fdcf687f39b944420d2a09fdb57", - "sha256:2fe803deacd09a233e4762a1adcea5db5d31e6be577a43352936179d14d90069", - "sha256:3232822c7d98d23895ccc443bbdf57c7412c5a65996c30442ebe6ed3df335383", - "sha256:34aa51c45f28ba7f12accd624225e2b1e5a3a45206aa191f6f9aac931d9d56fe", - "sha256:36f582d0c6bc99d5f39cd3ac2a9062e57f3cf606ade29a0a0d6b323462f4dd87", - "sha256:380a85cf89e0e69b7cfbe2ea9f765f004ff419f34194018a6827ac0e3edfed4d", - "sha256:40e7bc81c9e2b2734ea4bc1aceb8a8f0ceaac7c5299bc5d69e37c44d9081d43b", - "sha256:43ca3bbbe97af00f49efb06e352eae40434ca9d915906f77def219b88e85d907", - "sha256:4fcc4649dc762cddacd193e6b55bc02edca674067f5f98166d7713b193932b7f", - "sha256:5a0f54ce2c092aaf439813735584b9537cad479575a09892b8352fea5e988dc0", - "sha256:5a9a0d155deafd9448baff28c08e150d9b24ff010e899311ddd63c45c2445e28", - "sha256:5b02d65b9ccf0ef6c34cba6cf5bf2aab1bb2f49c6090bafeecc9cd81ad4ea1c1", - "sha256:60db23fa423575eeb65ea430cee741acb7c26a1365d103f7b0f6ec412b893853", - "sha256:642c2e7a804fcf18c222e1060df25fc210b9c58db7c91416fb055897fc27e8cc", - "sha256:6a9a25751acb379b466ff6be78a315e2b439d4c94c1e99cb7266d40a537995d3", - "sha256:6b1a564e6cb69922c7fe3a678b9f9a3c54e72b469875aa8018f18b4d1dd1adf3", - "sha256:6d323e1554b3d22cfc03cd3243b5bb815a51f5249fdcbb86fda4bf62bab9e164", - "sha256:6e743de5e9c3d1b7185870f480587b75b1cb604832e380d64f9504a0535912d1", - "sha256:709fe01086a55cf79d20f741f39325018f4df051ef39fe921b1ebe780a66184c", - "sha256:7b7c050ae976e286906dd3f26009e117eb000fb2cf3533398c5ad9ccc86867b1", - "sha256:7d2872609603cb35ca513d7404a94d6d608fc13211563571117046c9d2bcc3d7", - "sha256:7ef58fb89674095bfc57c4069e95d7a31cfdc0939e2a579882ac7d55aadfd2a1", - "sha256:80bb5c256f1415f747011dc3604b59bc1f91c6e7150bd7db03b19170ee06b320", - "sha256:81b19725065dcb43df02b37e03278c011a09e49757287dca60c5aecdd5a0b8ed", - "sha256:833b58d5d0b7e5b9832869f039203389ac7cbf01765639c7309fd50ef619e0b1", - "sha256:88bd7b6bd70a5b6803c1abf6bca012f7ed963e58c68d76ee20b9d751c74a3248", - "sha256:8ad85f7f4e20964db4daadcab70b47ab05c7c1cf2a7c1e51087bfaa83831854c", - "sha256:8c0ce1e99116d5ab21355d8ebe53d9460366704ea38ae4d9f6933188f327b456", - "sha256:8d649d616e5c6a678b26d15ece345354f7c2286acd6db868e65fcc5ff7c24a77", - "sha256:903500616422a40a98a5a3c4ff4ed9d0066f3b4c951fa286018ecdf0750194ef", - "sha256:9736af4641846491aedb3c3f56b9bc5568d92b0692303b5a305301a95dfd38b1", - "sha256:988635d122aaf2bdcef9e795435662bcd65b02f4f4c1ae37fbee7401c440b3a7", - "sha256:9cca3c2cdadb362116235fdbd411735de4328c61425b0aa9f872fd76d02c4e86", - "sha256:9e0fd32e0148dd5dea6af5fee42beb949098564cc23211a88d799e434255a1f4", - "sha256:9f3e6f9e05148ff90002b884fbc2a86bd303ae847e472f44ecc06c2cd2fcdb2d", - "sha256:a85d2b46be66a71bedde836d9e41859879cc54a2a04fad1191eb50c2066f6e9d", - "sha256:a9a52172be0b5aae932bef82a79ec0a0ce87288c7d132946d645eba03f0ad8a8", - "sha256:aa31fdcc33fef9eb2552cbcbfee7773d5a6792c137b359e82879c101e98584c5", - "sha256:b014c23646a467558be7da3d6b9fa409b2c567d2110599b7cf9a0c5992b3b471", - "sha256:b21bb4c09ffabfa0e85e3a6b623e19b80e7acd709b9f91452b8297ace2a8ab00", - "sha256:b5901a312f4d14c59918c221323068fad0540e34324925c8475263841dbdfe68", - "sha256:b9b7a708dd92306328117d8c4b62e2194d00c365f18eff11a9b53c6f923b01e3", - "sha256:d1967f46ea8f2db647c786e78d8cc7e4313dbd1b0aca360592d8027b8508e24d", - "sha256:d52a25136894c63de15a35bc0bdc5adb4b0e173b9c0d07a2be9d3ca64a332735", - "sha256:d77c85fedff92cf788face9bfa3ebaa364448ebb1d765302e9af11bf449ca36d", - "sha256:d79d7d5dc8a32b7093e81e97dad755127ff77bcc899e845f41bf71747af0c569", - "sha256:dbcda74c67263139358f4d188ae5faae95c30929281bc6866d00573783c422b7", - "sha256:ddaea91abf8b0d13443f6dac52e89051a5063c7d014710dcb4d4abb2ff811a59", - "sha256:dee0ce50c6a2dd9056c20db781e9c1cfd33e77d2d569f5d1d9321c641bb903d5", - "sha256:dee60e1de1898bde3b238f18340eec6148986da0455d8ba7848d50470a7a32fb", - "sha256:e2f83e18fe2f4c9e7db597e988f72712c0c3676d337d8b101f6758107c42425b", - "sha256:e3fb1677c720409d5f671e39bac6c9e0e422584e5f518bfd50aa4cbbea02433f", - "sha256:ee2b1b1769f6707a8a445162ea16dddf74285c3964f605877a20e38545c3c462", - "sha256:ee6acae74a2b91865910eef5e7de37dc6895ad96fa23603d1d27ea69df545015", - "sha256:ef3f72c9666bba2bab70d2a8b79f2c6d2c1a42a7f7e2b0ec83bb2f9e383950af" + "sha256:02fce1852f755f44f95af51f69d22e45080102e9d00258053b79367d07af39c0", + "sha256:077ff0d1f9d9e4ce6476c1a924a3332452c1406e59d90a2cf24aeb29eeac9420", + "sha256:078e2a1a86544e644a68422f881c48b84fef6d18f8c7a957ffd3f2e0a74a0d4a", + "sha256:0970ddb69bba00670e58955f8019bec4a42d1785db3faa043c33d81de2bf843c", + "sha256:1286eb30261894e4c70d124d44b7fd07825340869945c79d05bda53a40caa079", + "sha256:21f6d9a0d5b3a207cdf7acf8e58d7d13d463e639f0c7e01d82cdb671e6cb7923", + "sha256:230ae493696a371f1dbffaad3dafbb742a4d27a0afd2b1aecebe52b740167e7f", + "sha256:26458da5653aa5b3d8dc8b24192f574a58984c749401f98fff994d41d3f08da1", + "sha256:2cf56d0e237280baed46f0b5316661da892565ff58309d4d2ed7dba763d984b8", + "sha256:2e51de54d4fb8fb50d6ee8327f9828306a959ae394d3e01a1ba8b2f937747d86", + "sha256:2fbfbca668dd15b744418265a9607baa970c347eefd0db6a518aaf0cfbd153c0", + "sha256:38adf7198f8f154502883242f9fe7333ab05a5b02de7d83aa2d88ea621f13364", + "sha256:3a8564f283394634a7a7054b7983e47dbf39c07712d7b177b37e03f2467a024e", + "sha256:3abbe948c3cbde2689370a262a8d04e32ec2dd4f27103669a45c6929bcdbfe7c", + "sha256:3bbe623731d03b186b3d6b0d6f51865bf598587c38d6f7b0be2e27414f7f214e", + "sha256:40737a081d7497efea35ab9304b829b857f21558acfc7b3272f908d33b0d9d4c", + "sha256:41d07d029dd4157ae27beab04d22b8e261eddfc6ecd64ff7000b10dc8b3a5727", + "sha256:46ed616d5fb42f98630ed70c3529541408166c22cdfd4540b88d5f21006b0eff", + "sha256:493d389a2b63c88ad56cdc35d0fa5752daac56ca755805b1b0c530f785767d5e", + "sha256:4ff0d20f2e670800d3ed2b220d40984162089a6e2c9646fdb09b85e6f9a8fc29", + "sha256:54accd4b8bc202966bafafd16e69da9d5640ff92389d33d28555c5fd4f25ccb7", + "sha256:56374914b132c702aa9aa9959c550004b8847148f95e1b824772d453ac204a72", + "sha256:578383d740457fa790fdf85e6d346fda1416a40549fe8db08e5e9bd281c6a475", + "sha256:58d7a75d731e8c63614222bcb21dd992b4ab01a399f1f09dd82af17bbfc2368a", + "sha256:5c5aa28df055697d7c37d2099a7bc09f559d5053c3349b1ad0c39000e611d317", + "sha256:5fc8e02f5984a55d2c653f5fea93531e9836abbd84342c1d1e17abc4a15084c2", + "sha256:63424c681923b9f3bfbc5e3205aafe790904053d42ddcc08542181a30a7a51bd", + "sha256:64b1df0f83706b4ef4cfb4fb0e4c2669100fd7ecacfb59e091fad300d4e04640", + "sha256:74934ebd71950e3db69960a7da29204f89624dde411afbfb3b4858c1409b1e98", + "sha256:75669d77bb2c071333417617a235324a1618dba66f82a750362eccbe5b61d248", + "sha256:75760a47c06b5974aa5e01949bf7e66d2af4d08cb8c1d6516af5e39595397f5e", + "sha256:76407ab327158c510f44ded207e2f76b657303e17cb7a572ffe2f5a8a48aa04d", + "sha256:76e9c727a874b4856d11a32fb0b389afc61ce8aaf281ada613713ddeadd1cfec", + "sha256:77d4c1b881076c3ba173484dfa53d3582c1c8ff1f914c6461ab70c8428b796c1", + "sha256:780c82a41dc493b62fc5884fb1d3a3b81106642c5c5c78d6a0d4cbe96d62ba7e", + "sha256:7dc0713bf81287a00516ef43137273b23ee414fe41a3c14be10dd95ed98a2df9", + "sha256:7eebcdbe3677e58dd4c0e03b4f2cfa346ed4049687d839adad68cc38bb559c92", + "sha256:896689fddba4f23ef7c718279e42f8834041a21342d95e56922e1c10c0cc7afb", + "sha256:96177eb5645b1c6985f5c11d03fc2dbda9ad24ec0f3a46dcce91445747e15094", + "sha256:96e25c8603a155559231c19c0349245eeb4ac0096fe3c1d0be5c47e075bd4f46", + "sha256:9d37ac69edc5614b90516807de32d08cb8e7b12260a285ee330955604ed9dd29", + "sha256:9ed6aa0726b9b60911f4aed8ec5b8dd7bf3491476015819f56473ffaef8959bd", + "sha256:a487f72a25904e2b4bbc0817ce7a8de94363bd7e79890510174da9d901c38705", + "sha256:a4cbb9ff5795cd66f0066bdf5947f170f5d63a9274f99bdbca02fd973adcf2a8", + "sha256:a74d56552ddbde46c246b5b89199cb3fd182f9c346c784e1a93e4dc3f5ec9975", + "sha256:a89ce3fd220ff144bd9d54da333ec0de0399b52c9ac3d2ce34b569cf1a5748fb", + "sha256:abd52a09d03adf9c763d706df707c343293d5d106aea53483e0ec8d9e310ad5e", + "sha256:abd8f36c99512755b8456047b7be10372fca271bf1467a1caa88db991e7c421b", + "sha256:af5bd9ccb188f6a5fdda9f1f09d9f4c86cc8a539bd48a0bfdc97723970348418", + "sha256:b02f21c1e2074943312d03d243ac4388319f2456576b2c6023041c4d57cd7019", + "sha256:b06fa97478a5f478fb05e1980980a7cdf2712015493b44d0c87606c1513ed5b1", + "sha256:b0724f05c396b0a4c36a3226c31648385deb6a65d8992644c12a4963c70326ba", + "sha256:b130fe77361d6771ecf5a219d8e0817d61b236b7d8b37cc045172e574ed219e6", + "sha256:b56d5519e470d3f2fe4aa7585f0632b060d532d0696c5bdfb5e8319e1d0f69a2", + "sha256:b67b819628e3b748fd3c2192c15fb951f549d0f47c0449af0764d7647302fda3", + "sha256:ba1711cda2d30634a7e452fc79eabcadaffedf241ff206db2ee93dd2c89a60e7", + "sha256:bbeccb1aa40ab88cd29e6c7d8585582c99548f55f9b2581dfc5ba68c59a85752", + "sha256:bd84395aab8e4d36263cd1b9308cd504f6cf713b7d6d3ce25ea55670baec5416", + "sha256:c99f4309f5145b93eca6e35ac1a988f0dc0a7ccf9ccdcd78d3c0adf57224e62f", + "sha256:ca1cccf838cd28d5a0883b342474c630ac48cac5df0ee6eacc9c7290f76b11c1", + "sha256:cd525e0e52a5ff16653a3fc9e3dd827981917d34996600bbc34c05d048ca35cc", + "sha256:cdb4f085756c96a3af04e6eca7f08b1345e94b53af8921b25c72f096e704e145", + "sha256:ce42618f67741d4697684e501ef02f29e758a123aa2d669e2d964ff734ee00ee", + "sha256:d06730c6aed78cee4126234cf2d071e01b44b915e725a6cb439a879ec9754a3a", + "sha256:d5fe3e099cf07d0fb5a1e23d399e5d4d1ca3e6dfcbe5c8570ccff3e9208274f7", + "sha256:d6bcbfc99f55655c3d93feb7ef3800bd5bbe963a755687cbf1f490a71fb7794b", + "sha256:d787272ed958a05b2c86311d3a4135d3c2aeea4fc655705f074130aa57d71653", + "sha256:e169e957c33576f47e21864cf3fc9ff47c223a4ebca8960079b8bd36cb014fd0", + "sha256:e20076a211cd6f9b44a6be58f7eeafa7ab5720eb796975d0c03f05b47d89eb90", + "sha256:e826aadda3cae59295b95343db8f3d965fb31059da7de01ee8d1c40a60398b29", + "sha256:eef4d64c650f33347c1f9266fa5ae001440b232ad9b98f1f43dfe7a79435c0a6", + "sha256:f2e69b3ed24544b0d3dbe2c5c0ba5153ce50dcebb576fdc4696d52aa22db6034", + "sha256:f87ec75864c37c4c6cb908d282e1969e79763e0d9becdfe9fe5473b7bb1e5f09", + "sha256:fbec11614dba0424ca72f4e8ba3c420dba07b4a7c206c8c8e4e73f2e98f4c559", + "sha256:fd69666217b62fa5d7c6aa88e507493a34dec4fa20c5bd925e4bc12fce586639" ], "markers": "python_version >= '3.11'", - "version": "==1.14.1" + "version": "==1.15.0" }, "xmltodict": { "hashes": [ diff --git a/README.md b/README.md index 11f8af5..64ee8df 100644 --- a/README.md +++ b/README.md @@ -21,3 +21,5 @@ A CLI application to generate and email credit card slips for Alma invoices via - `ALMA_API_TIMEOUT`: Request timeout for Alma API calls. Defaults to 30 seconds if not set. - `LOG_LEVEL`: Set to a valid Python logging level (e.g. `DEBUG`, case-insensitive) if desired. Can also be passed as an option directly to the ccslips command. Defaults to `INFO` if not set or passed to the command. - `SENTRY_DSN`: If set to a valid Sentry DSN enables Sentry exception monitoring. This is not needed for local development. +- `SES_RECIPIENT_EMAIL`: Email address(es) for recipient(s) who should receive the credit card slips email. Multiple email addresses should be separated by a space, e.g. `SES_RECIPIENT_EMAIL=recipient1@example.com recipient2@example.com`. This value can either be set in ENV or passed directly to the command line as an option. +- `SES_SEND_FROM_EMAIL`: Verified email address for sending emails via SES. This value can either be set in ENV or passed directly to the command as an option. diff --git a/ccslips/cli.py b/ccslips/cli.py index a7c1358..3be3d0e 100644 --- a/ccslips/cli.py +++ b/ccslips/cli.py @@ -1,4 +1,5 @@ import logging +import os from datetime import datetime, timedelta from time import perf_counter from typing import Optional @@ -6,6 +7,8 @@ import click from ccslips.config import configure_logger, configure_sentry +from ccslips.email import Email +from ccslips.polines import generate_credit_card_slips_html, process_po_lines logger = logging.getLogger(__name__) @@ -14,15 +17,20 @@ @click.option( "-s", "--source-email", + envvar="SES_SEND_FROM_EMAIL", required=True, help="The email address sending the credit card slips.", ) @click.option( "-r", "--recipient-email", + envvar="SES_RECIPIENT_EMAIL", required=True, multiple=True, - help="The email address receiving the credit card slips. Repeatable", + help="The email address(es) receiving the credit card slips. Repeatable, e.g. " + "`-r recipient1@example.com -r recipient2@example.com`. If setting via ENV " + "variable, separate multiple email addresses with a space, e.g. " + "`SES_RECIPIENT_EMAIL=recipient1@example.com recipient2@example.com`", ) @click.option( "-d", @@ -55,17 +63,32 @@ def main( logger.debug("Command called with options: %s", ctx.params) logger.info("Starting credit card slips process") - - # Do things here! date = date or (datetime.today() - timedelta(days=1)).strftime("%Y-%m-%d") - click.echo( - f"\nFunctionality to be added here will process the credit card invoices from " - f"date {date} and send the resulting email from {source_email} to " - f"{recipient_email}\n" + credit_card_slips_data = process_po_lines(date) + email_content = generate_credit_card_slips_html(credit_card_slips_data) + email = Email() + env = os.environ["WORKSPACE"] + subject_prefix = f"{env.upper()} " if env != "prod" else "" + email.populate( + from_address=source_email, + to_addresses=",".join(recipient_email), + subject=f"{subject_prefix}Credit card slips {date}", + attachments=[ + { + "content": email_content, + "filename": f"{date}_credit_card_slips.htm", + } + ], ) + response = email.send() + logger.debug(response) elapsed_time = perf_counter() - start_time logger.info( - "Finished! Total time to complete process: %s", + "Credit card slips processing complete for date %s. Email sent to recipient(s) " + "%s with SES message ID '%s'. Total time to complete process: %s", + date, + recipient_email, + response["MessageId"], str(timedelta(seconds=elapsed_time)), ) diff --git a/ccslips/polines.py b/ccslips/polines.py new file mode 100644 index 0000000..c6f4804 --- /dev/null +++ b/ccslips/polines.py @@ -0,0 +1,145 @@ +import xml.etree.ElementTree as ET # nosec B405 +from copy import deepcopy +from datetime import datetime +from decimal import Decimal +from typing import Generator, Iterator, Optional + +from ccslips.alma import AlmaClient + + +def process_po_lines(date: str) -> Generator[dict, None, None]: + """Retrieve PO line records for a given date and yield processed data for each.""" + client = AlmaClient() + for po_line in client.get_full_po_lines("PURCHASE_NOLETTER", date): + yield extract_credit_card_slip_data(client, po_line) + + +def extract_credit_card_slip_data(client: AlmaClient, po_line_record: dict) -> dict: + """Extract required data for a credit card slip from a PO line record. + + The keys of the returned dict map to the appropriate element classes in the XML + template used to generate a formatted slip. + """ + created_date = datetime.strptime( + po_line_record["created_date"], "%Y-%m-%dZ" + ).strftime("%y%m%d") + fund_distribution = po_line_record.get("fund_distribution", []) + price = Decimal(po_line_record.get("price", {}).get("sum", "0.00")) + title = po_line_record.get("resource_metadata", {}).get("title", "Unknown title") + + po_line_data = { + "cardholder": get_cardholder_from_notes(po_line_record.get("note")), + "invoice_number": ( + f"Invoice #: {created_date}{title.replace(' ', '')[:3].upper()}" + ), + "po_date": created_date, + "po_line_number": po_line_record["number"], + "price": f"${price:.2f}", + "quantity": get_quantity_from_locations(po_line_record.get("location")), + "item_title": title, + "total_price": ( + f"${get_total_price_from_fund_distribution(fund_distribution, price):.2f}" + ), + "vendor_code": po_line_record.get("vendor_account", "No vendor found"), + } + po_line_data.update(get_account_data(client, fund_distribution)) + + return po_line_data + + +def get_cardholder_from_notes(notes: Optional[list[dict]]) -> str: + """Get first note that begins with 'CC-' from a PO line record notes field.""" + if notes: + for note in [n for n in notes if n.get("note_text", "").startswith("CC-")]: + return note["note_text"][3:] + return "No cardholder note found" + + +def get_quantity_from_locations(locations: Optional[list[dict]]) -> str: + """Get the total quantity of items associated with PO line locations. + + This function adds the quantities from each location in the PO line. This is an + imperfect method as it may generate a total quantity than differs from what is + visible in the UI if some if the items are not associated with a location. + Regardless, this is the best available method given that the quantity listed in the + UI is not available in the API response. Stakeholders requested this with full + knowledge of the imperfections. + """ + if locations is None: + return "0" + return str(sum(location.get("quantity", 0) for location in locations)) + + +def get_total_price_from_fund_distribution( + fund_distribution: list[dict], unit_price: Decimal +) -> Decimal: + """Get total price from fund distribution of PO line record. + + If no amounts or amount sums are listed in the fund distribution, the unit price is + returned as the total price. + """ + return ( + sum( + Decimal(fund.get("amount", {}).get("sum", "0.00")) + for fund in fund_distribution + ) + or unit_price + ) + + +def get_account_data( + client: AlmaClient, fund_distribution: list[dict] +) -> dict[str, str]: + """Get account information needed for a credit card slip. + + If the fund_distribution is empty, returns a single account with default text. + Otherwise returns up to two accounts with their associated account numbers. + """ + result = {"account_1": "No fund code found"} + for count, fund in enumerate(fund_distribution, start=1): + if count == 3: + break + if account_number := get_account_number_from_fund(client, fund): + result[f"account_{count}"] = account_number + return result + + +def get_account_number_from_fund(client: AlmaClient, fund: dict) -> Optional[str]: + """Get account number for a given fund. + + Returns None if fund has no fund code, fund record cannot be retrieved via fund + code, or fund record does not contain an external_id field value. + """ + account = None + if fund_code := fund.get("fund_code", {}).get("value"): + if fund_records := client.get_fund_by_code(fund_code).get("fund"): + account = fund_records[0].get("external_id") + return account + + +def generate_credit_card_slips_html(po_line_data: Iterator[dict]) -> str: + """Create credit card slips HTML from a set of credit card slip data.""" + template_tree = ET.parse("config/credit_card_slip_template.xml") # nosec B314 + xml_template = template_tree.getroot() + output = ET.fromstring("") # nosec B314 + for line in po_line_data: + output.append( + populate_credit_card_slip_xml_fields(deepcopy(xml_template), line) + ) + if len(output) == 0: + return "

No credit card orders on this date

" + return ET.tostring(output, encoding="unicode", method="xml") + + +def populate_credit_card_slip_xml_fields( + credit_card_slip_xml_template: ET.Element, credit_card_slip_data: dict +) -> ET.Element: + """Populate credit card slip XML template with data extracted from a PO line. + + The credit_card_slip_data keys must correspond to their associated element classes + in the XML template. + """ + for key, value in credit_card_slip_data.items(): + for element in credit_card_slip_xml_template.findall(f'.//td[@class="{key}"]'): + element.text = value + return credit_card_slip_xml_template diff --git a/config/credit_card_slip_template.xml b/config/credit_card_slip_template.xml new file mode 100644 index 0000000..792044f --- /dev/null +++ b/config/credit_card_slip_template.xml @@ -0,0 +1,141 @@ + +

+ MIT Libraries Credit Card Purchase +
+ Monograph Acquisitions, Rm. NE36-6101

+
+
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ CHARGE INFORMATION +
Date:
Cardholder:
Vendor code: +
Account 1: +
Account 2: +
+
+
PO Line #:TITLE:QUANTITYPRICE
+
+
Transaction fee: + __________
TOTAL DUE: +
+ SAP INFORMATION +
SAP document #:
Date posted in SAP:
Verified by:
Verified date:
+
+
+ ALMA INVOICE INFORMATION +
Inv #: Date charged + 1st 3 letters of title (YYMMDD "xxx")
For Credit Memo #, use Invoice # + CRE (YYMMDD"XXX"CRE)
Use Charge Date above for Invoice Date.
+
+
Date entered in Alma: +
Entered by: +
+
+

+ diff --git a/setup.cfg b/setup.cfg index cc8b645..b87e038 100644 --- a/setup.cfg +++ b/setup.cfg @@ -16,7 +16,3 @@ profile = black [pylama:pydocstyle] convention = pep257 - -[tool:pytest] -log_level = DEBUG - diff --git a/tests/conftest.py b/tests/conftest.py index 0e6b252..2573daf 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -17,6 +17,8 @@ def test_env(): "ALMA_API_URL": "https://example.com", "ALMA_API_READ_KEY": "just-for-testing", "ALMA_API_TIMEOUT": "10", + "SES_SEND_FROM_EMAIL": "from@example.com", + "SES_RECIPIENT_EMAIL": "recipient1@example.com recipient2@example.com", "SENTRY_DSN": "None", "WORKSPACE": "test", } @@ -82,6 +84,18 @@ def mocked_alma(fund_records, po_line_records): "https://example.com/acq/funds?q=fund_code~FUND-abc", json={"fund": [fund_records["abc"]], "total_record_count": 1}, ) + mocker.get( + "https://example.com/acq/funds?q=fund_code~FUND-def", + json={"fund": [fund_records["def"]], "total_record_count": 1}, + ) + mocker.get( + "https://example.com/acq/funds?q=fund_code~FUND-no-external-id", + json={"fund": [fund_records["no-external-id"]], "total_record_count": 1}, + ) + mocker.get( + "https://example.com/acq/funds?q=fund_code~FUND-nothing-here", + json={"total_record_count": 0}, + ) # PO Line endpoints mocker.get( @@ -99,15 +113,20 @@ def mocked_alma(fund_records, po_line_records): json={ "po_line": [ po_line_records["all_fields"], + po_line_records["missing_fields"], po_line_records["wrong_date"], ], - "total_record_count": 2, + "total_record_count": 3, }, ) mocker.get( "https://example.com/acq/po-lines/POL-all-fields", json=po_line_records["all_fields"], ) + mocker.get( + "https://example.com/acq/po-lines/POL-missing-fields", + json=po_line_records["missing_fields"], + ) mocker.get( "https://example.com/acq/po-lines/POL-other-acq-method", json=po_line_records["other_acq_method"], diff --git a/tests/fixtures/fund_records.json b/tests/fixtures/fund_records.json index 4f8b285..2b04aaa 100644 --- a/tests/fixtures/fund_records.json +++ b/tests/fixtures/fund_records.json @@ -1,5 +1,13 @@ { "abc": { - "code": "FUND-abc" + "code": "FUND-abc", + "external_id": "account-abc" + }, + "def": { + "code": "FUND-def", + "external_id": "account-def" + }, + "no-external-id": { + "code": "FUND-no-external-id" } } diff --git a/tests/fixtures/po_line_records.json b/tests/fixtures/po_line_records.json index dc2ec86..099cbdd 100644 --- a/tests/fixtures/po_line_records.json +++ b/tests/fixtures/po_line_records.json @@ -4,7 +4,52 @@ "desc": "Credit Card" }, "created_date": "2023-01-02Z", - "number": "POL-all-fields" + "fund_distribution": [ + { + "fund_code": { + "value": "FUND-abc" + }, + "amount": { + "sum": "7" + } + }, + { + "fund_code": { + "value": "FUND-def" + }, + "amount": { + "sum": "5.0" + } + } + ], + "location": [ + { + "quantity": 1 + }, + { + "quantity": 2 + } + ], + "note": [ + { + "note_text": "CC-cardholder name" + } + ], + "number": "POL-all-fields", + "price": { + "sum": "12.0" + }, + "resource_metadata": { + "title": "Book title" + }, + "vendor_account": "Corporation" + }, + "missing_fields": { + "acquisition_method": { + "desc": "Credit Card" + }, + "created_date": "2023-01-02Z", + "number": "POL-missing-fields" }, "other_acq_method": { "acquisition_method": { diff --git a/tests/test_alma.py b/tests/test_alma.py index e74b4e6..81d0140 100644 --- a/tests/test_alma.py +++ b/tests/test_alma.py @@ -29,16 +29,26 @@ def test_get_brief_po_lines_without_acquisition_method(alma_client): def test_get_brief_po_lines_with_acquisition_method(alma_client): result = list(alma_client.get_brief_po_lines("PURCHASE_NOLETTER")) - assert len(result) == 2 + assert len(result) == 3 assert result[0]["number"] == "POL-all-fields" - assert result[1]["number"] == "POL-wrong-date" + assert result[1]["number"] == "POL-missing-fields" + assert result[2]["number"] == "POL-wrong-date" def test_get_full_po_line(alma_client): assert alma_client.get_full_po_line("POL-all-fields") == { "acquisition_method": {"desc": "Credit Card"}, "created_date": "2023-01-02Z", + "fund_distribution": [ + {"fund_code": {"value": "FUND-abc"}, "amount": {"sum": "7"}}, + {"fund_code": {"value": "FUND-def"}, "amount": {"sum": "5.0"}}, + ], + "location": [{"quantity": 1}, {"quantity": 2}], + "note": [{"note_text": "CC-cardholder name"}], "number": "POL-all-fields", + "price": {"sum": "12.0"}, + "resource_metadata": {"title": "Book title"}, + "vendor_account": "Corporation", } @@ -54,8 +64,9 @@ def test_get_full_po_lines_with_parameters(alma_client): acquisition_method="PURCHASE_NOLETTER", date="2023-01-02" ) ) - assert len(result) == 1 + assert len(result) == 2 assert result[0]["number"] == "POL-all-fields" + assert result[1]["number"] == "POL-missing-fields" def test_alma_get_fund_by_code(alma_client): diff --git a/tests/test_cli.py b/tests/test_cli.py index 7498048..5b81b09 100644 --- a/tests/test_cli.py +++ b/tests/test_cli.py @@ -3,23 +3,25 @@ from ccslips.cli import main -@freeze_time("2023-01-02") -def test_cli_only_required_options(caplog, runner): - result = runner.invoke( - main, ["-s", "source@example.com", "-r", "recipient@example.com"] - ) +@freeze_time("2023-01-03") +def test_cli_options_from_env(caplog, runner): + result = runner.invoke(main) assert result.exit_code == 0 assert "Logger 'root' configured with level=INFO" in caplog.text assert "Starting credit card slips process" in caplog.text - assert "Total time to complete process" in caplog.text + assert ( + "Credit card slips processing complete for date 2023-01-02. Email sent to " + "recipient(s) ('recipient1@example.com', 'recipient2@example.com')" + in caplog.text + ) -def test_cli_all_options(caplog, runner): +def test_cli_all_options_passed(caplog, runner): result = runner.invoke( main, [ "--source-email", - "source@example.com", + "from@example.com", "--recipient-email", "recipient1@example.com", "--recipient-email", @@ -33,8 +35,12 @@ def test_cli_all_options(caplog, runner): assert result.exit_code == 0 assert "Logger 'root' configured with level=DEBUG" in caplog.text assert ( - "Command called with options: {'source_email': 'source@example.com', " + "Command called with options: {'source_email': 'from@example.com', " "'recipient_email': ('recipient1@example.com', 'recipient2@example.com'), " "'date': '2023-01-02', 'log_level': 'debug'}" in caplog.text ) - assert "Total time to complete process" in caplog.text + assert ( + "Credit card slips processing complete for date 2023-01-02. Email sent to " + "recipient(s) ('recipient1@example.com', 'recipient2@example.com')" + in caplog.text + ) diff --git a/tests/test_polines.py b/tests/test_polines.py new file mode 100644 index 0000000..fe5594c --- /dev/null +++ b/tests/test_polines.py @@ -0,0 +1,335 @@ +from decimal import Decimal + +from ccslips import polines as po + + +def test_process_po_lines(): + result = list(po.process_po_lines("2023-01-02")) + assert len(result) == 2 + + +def test_extract_credit_card_slip_data_all_fields_present(alma_client, po_line_records): + assert po.extract_credit_card_slip_data( + alma_client, po_line_records["all_fields"] + ) == { + "account_1": "account-abc", + "account_2": "account-def", + "cardholder": "cardholder name", + "invoice_number": "Invoice #: 230102BOO", + "po_date": "230102", + "po_line_number": "POL-all-fields", + "price": "$12.00", + "quantity": "3", + "item_title": "Book title", + "total_price": "$12.00", + "vendor_code": "Corporation", + } + + +def test_extract_credit_card_slip_data_missing_fields(alma_client, po_line_records): + assert po.extract_credit_card_slip_data( + alma_client, po_line_records["missing_fields"] + ) == { + "account_1": "No fund code found", + "cardholder": "No cardholder note found", + "invoice_number": "Invoice #: 230102UNK", + "po_date": "230102", + "po_line_number": "POL-missing-fields", + "price": "$0.00", + "quantity": "0", + "item_title": "Unknown title", + "total_price": "$0.00", + "vendor_code": "No vendor found", + } + + +def test_get_cardholder_from_notes_no_notes(): + assert po.get_cardholder_from_notes(None) == "No cardholder note found" + + +def test_get_cardholder_from_notes_no_note_text_field(): + notes = [{"not-note_text": "CC-wrong field"}] + assert po.get_cardholder_from_notes(notes) == "No cardholder note found" + + +def test_get_cardholder_from_notes_gets_correct_note(): + notes = [ + {"note_text": "not this one"}, + {"note_text": "CC or this one"}, + {"note_text": "CC-winner"}, + {"note_text": "CC- nope, first one only"}, + ] + assert po.get_cardholder_from_notes(notes) == "winner" + + +def test_get_quantity_from_locations_no_locations(): + assert po.get_quantity_from_locations(None) == "0" + + +def test_get_quantity_from_locations_no_location_quantity_field(): + locations = [{"not-quantity": 1}] + assert po.get_quantity_from_locations(locations) == "0" + + +def test_get_quantity_from_locations_sums_quantities(): + locations = [{"quantity": 0}, {"quantity": 3}, {"quantity": 7}] + assert po.get_quantity_from_locations(locations) == "10" + + +def test_get_total_price_from_fund_distribution_no_funds_returns_unit_price(): + funds = [] + assert po.get_total_price_from_fund_distribution( + funds, unit_price=Decimal("1.23") + ) == Decimal("1.23") + + +def test_get_total_price_from_fund_distribution_no_amounts_or_sums_returns_unit_price(): + funds = [ + {"fund_code": {"value": "no-amount"}}, + {"fund_code": {"value": "no-amount-sum"}, "amount": {"no-sum-here": "0.00"}}, + ] + assert po.get_total_price_from_fund_distribution( + funds, unit_price=Decimal("1.23") + ) == Decimal("1.23") + + +def test_get_total_price_from_fund_distribution_sums_fund_amounts(): + funds = [ + {"fund_code": {"value": "amount-is-zero"}, "amount": {"sum": "0.00"}}, + {"fund_code": {"value": "first-amount"}, "amount": {"sum": "5.15"}}, + {"fund_code": {"value": "second-amount"}, "amount": {"sum": "4.85"}}, + ] + assert po.get_total_price_from_fund_distribution( + funds, unit_price=Decimal("1.23") + ) == Decimal("10.00") + + +def test_get_account_data_no_fund_distribution(alma_client): + assert po.get_account_data(alma_client, []) == {"account_1": "No fund code found"} + + +def test_get_account_data_with_one_account(alma_client): + fund_distribution = [{"fund_code": {"value": "FUND-abc"}}] + assert po.get_account_data(alma_client, fund_distribution) == { + "account_1": "account-abc" + } + + +def test_get_account_data_with_two_accounts(alma_client): + fund_distribution = [ + {"fund_code": {"value": "FUND-abc"}}, + {"fund_code": {"value": "FUND-def"}}, + ] + assert po.get_account_data(alma_client, fund_distribution) == { + "account_1": "account-abc", + "account_2": "account-def", + } + + +def test_get_account_data_with_more_than_two_accounts_only_returns_two(alma_client): + fund_distribution = [ + {"fund_code": {"value": "FUND-abc"}}, + {"fund_code": {"value": "FUND-def"}}, + {"fund_code": {"value": "FUND-ghi"}}, + ] + assert po.get_account_data(alma_client, fund_distribution) == { + "account_1": "account-abc", + "account_2": "account-def", + } + + +def test_get_account_number_from_fund_no_fund_code(alma_client): + fund = {"no-fund_code": {"value": "doesn't matter"}} + assert po.get_account_number_from_fund(alma_client, fund) is None + + +def test_get_account_number_from_fund_no_fund_code_value(alma_client): + fund = {"fund_code": {"no-value": "doesn't matter"}} + assert po.get_account_number_from_fund(alma_client, fund) is None + + +def test_get_account_number_from_fund_no_fund_retrieved(alma_client): + fund = {"fund_code": {"value": "FUND-nothing-here"}} + assert po.get_account_number_from_fund(alma_client, fund) is None + + +def test_get_account_number_from_fund_no_external_id(alma_client): + fund = {"fund_code": {"value": "FUND-no-external-id"}} + assert po.get_account_number_from_fund(alma_client, fund) is None + + +def test_get_account_number_from_fund_returns_account_number(alma_client): + fund = {"fund_code": {"value": "FUND-abc"}} + assert po.get_account_number_from_fund(alma_client, fund) == "account-abc" + + +def test_generate_credit_card_slips_html_populates_with_default_text_if_no_data(): + po_line_data = [] + assert ( + po.generate_credit_card_slips_html(po_line_data) + == "

No credit card orders on this date

" + ) + + +def test_generate_credit_card_slips_html_populates_all_fields(): + po_line_data = [ + { + "account_1": "account-abc", + "account_2": "account-def", + "cardholder": "cardholder name", + "invoice_number": "Invoice #: 230102BOO", + "po_date": "230102", + "po_line_number": "POL-all-fields", + "price": "$12.00", + "quantity": "3", + "item_title": "Book title", + "total_price": "$12.00", + "vendor_code": "Corporation", + } + ] + assert ( + po.generate_credit_card_slips_html(po_line_data) + == """ +

+ MIT Libraries Credit Card Purchase +
+ Monograph Acquisitions, Rm. NE36-6101

+
+
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ CHARGE INFORMATION +
Date:230102 +
Cardholder:cardholder name +
Vendor code: + Corporation +
Account 1: + +
Account 2: + +
+
+
PO Line #:TITLE:QUANTITYPRICE
POL-all-fieldsBook title3$12.00
+
+
+ Transaction fee: + __________
+ TOTAL DUE: + $12.00
+ SAP INFORMATION + +
SAP document #: +
Date posted in SAP: +
Verified by: +
Verified date: +
+
+
+ ALMA INVOICE INFORMATION + +
Invoice #: 230102BOO +
Inv #: Date charged + 1st 3 letters of\ + title (YYMMDD "xxx")
For Credit \ +Memo #, use Invoice # + CRE (YYMMDD"XXX"CRE)
Use Charge Date above for Invoice Date. +
+
+
Date entered in Alma: + +
Entered by: + +
+
+

+""" + )