diff --git a/.github/actions/email/send/.venv/.gitkeep b/.github/actions/email/send/.venv/.gitkeep new file mode 100644 index 00000000..e69de29b diff --git a/.github/actions/email/send/Pipfile b/.github/actions/email/send/Pipfile new file mode 100644 index 00000000..8b7fe55b --- /dev/null +++ b/.github/actions/email/send/Pipfile @@ -0,0 +1,17 @@ +[[source]] +url = "https://pypi.org/simple" +verify_ssl = true +name = "pypi" + +[packages] +requests = "==2.31.0" + +[dev-packages] +black = "==23.1.0" +pytest = "==7.2.1" +mypy = "==1.6.1" +pylint = "==3.0.2" +types-requests = "==2.31.0.10" + +[requires] +python_version = "3.11" diff --git a/.github/actions/email/send/Pipfile.lock b/.github/actions/email/send/Pipfile.lock new file mode 100644 index 00000000..868eb118 --- /dev/null +++ b/.github/actions/email/send/Pipfile.lock @@ -0,0 +1,374 @@ +{ + "_meta": { + "hash": { + "sha256": "d8c822f88318b578f7eb48f3d8c524eea11a07e452511ff65bd255041b530f30" + }, + "pipfile-spec": 6, + "requires": { + "python_version": "3.11" + }, + "sources": [ + { + "name": "pypi", + "url": "https://pypi.org/simple", + "verify_ssl": true + } + ] + }, + "default": { + "certifi": { + "hashes": [ + "sha256:9b469f3a900bf28dc19b8cfbf8019bf47f7fdd1a65a1d4ffb98fc14166beb4d1", + "sha256:e036ab49d5b79556f99cfc2d9320b34cfbe5be05c5871b51de9329f0603b0474" + ], + "markers": "python_version >= '3.6'", + "version": "==2023.11.17" + }, + "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" + }, + "idna": { + "hashes": [ + "sha256:9ecdbbd083b06798ae1e86adcbfe8ab1479cf864e4ee30fe4e46a003d12491ca", + "sha256:c05567e9c24a6b9faaa835c4821bad0590fbb9d5779e7caa6e1cc4978e7eb24f" + ], + "markers": "python_version >= '3.5'", + "version": "==3.6" + }, + "requests": { + "hashes": [ + "sha256:58cd2187c01e70e6e26505bca751777aa9f2ee0b7f4300988b709f44e013003f", + "sha256:942c5a758f98d790eaed1a29cb6eefc7ffb0d1cf7af05c3d2791656dbd6ad1e1" + ], + "index": "pypi", + "version": "==2.31.0" + }, + "urllib3": { + "hashes": [ + "sha256:55901e917a5896a349ff771be919f8bd99aff50b79fe58fec595eb37bbc56bb3", + "sha256:df7aa8afb0148fa78488e7899b2c59b5f4ffcfa82e6c54ccb9dd37c1d7b52d54" + ], + "markers": "python_version >= '3.8'", + "version": "==2.1.0" + } + }, + "develop": { + "astroid": { + "hashes": [ + "sha256:4a61cf0a59097c7bb52689b0fd63717cd2a8a14dc9f1eee97b82d814881c8c91", + "sha256:d6e62862355f60e716164082d6b4b041d38e2a8cf1c7cd953ded5108bac8ff5c" + ], + "markers": "python_full_version >= '3.8.0'", + "version": "==3.0.2" + }, + "attrs": { + "hashes": [ + "sha256:1f28b4522cdc2fb4256ac1a020c78acf9cba2c6b461ccd2c126f3aa8e8335d04", + "sha256:6279836d581513a26f1bf235f9acd333bc9115683f14f7e8fae46c98fc50e015" + ], + "markers": "python_version >= '3.7'", + "version": "==23.1.0" + }, + "black": { + "hashes": [ + "sha256:0052dba51dec07ed029ed61b18183942043e00008ec65d5028814afaab9a22fd", + "sha256:0680d4380db3719ebcfb2613f34e86c8e6d15ffeabcf8ec59355c5e7b85bb555", + "sha256:121ca7f10b4a01fd99951234abdbd97728e1240be89fde18480ffac16503d481", + "sha256:162e37d49e93bd6eb6f1afc3e17a3d23a823042530c37c3c42eeeaf026f38468", + "sha256:2a951cc83ab535d248c89f300eccbd625e80ab880fbcfb5ac8afb5f01a258ac9", + "sha256:2bf649fda611c8550ca9d7592b69f0637218c2369b7744694c5e4902873b2f3a", + "sha256:382998821f58e5c8238d3166c492139573325287820963d2f7de4d518bd76958", + "sha256:49f7b39e30f326a34b5c9a4213213a6b221d7ae9d58ec70df1c4a307cf2a1580", + "sha256:57c18c5165c1dbe291d5306e53fb3988122890e57bd9b3dcb75f967f13411a26", + "sha256:7a0f701d314cfa0896b9001df70a530eb2472babb76086344e688829efd97d32", + "sha256:8178318cb74f98bc571eef19068f6ab5613b3e59d4f47771582f04e175570ed8", + "sha256:8b70eb40a78dfac24842458476135f9b99ab952dd3f2dab738c1881a9b38b753", + "sha256:9880d7d419bb7e709b37e28deb5e68a49227713b623c72b2b931028ea65f619b", + "sha256:9afd3f493666a0cd8f8df9a0200c6359ac53940cbde049dcb1a7eb6ee2dd7074", + "sha256:a29650759a6a0944e7cca036674655c2f0f63806ddecc45ed40b7b8aa314b651", + "sha256:a436e7881d33acaf2536c46a454bb964a50eff59b21b51c6ccf5a40601fbef24", + "sha256:a59db0a2094d2259c554676403fa2fac3473ccf1354c1c63eccf7ae65aac8ab6", + "sha256:a8471939da5e824b891b25751955be52ee7f8a30a916d570a5ba8e0f2eb2ecad", + "sha256:b0bd97bea8903f5a2ba7219257a44e3f1f9d00073d6cc1add68f0beec69692ac", + "sha256:b6a92a41ee34b883b359998f0c8e6eb8e99803aa8bf3123bf2b2e6fec505a221", + "sha256:bb460c8561c8c1bec7824ecbc3ce085eb50005883a6203dcfb0122e95797ee06", + "sha256:bfffba28dc52a58f04492181392ee380e95262af14ee01d4bc7bb1b1c6ca8d27", + "sha256:c1c476bc7b7d021321e7d93dc2cbd78ce103b84d5a4cf97ed535fbc0d6660648", + "sha256:c91dfc2c2a4e50df0026f88d2215e166616e0c80e86004d0003ece0488db2739", + "sha256:e6663f91b6feca5d06f2ccd49a10f254f9298cc1f7f49c46e498a0771b507104" + ], + "index": "pypi", + "version": "==23.1.0" + }, + "click": { + "hashes": [ + "sha256:ae74fb96c20a0277a1d615f1e4d73c8414f5a98db8b799a7931d1582f3390c28", + "sha256:ca9853ad459e787e2192211578cc907e7594e294c7ccc834310722b41b9ca6de" + ], + "markers": "python_version >= '3.7'", + "version": "==8.1.7" + }, + "dill": { + "hashes": [ + "sha256:76b122c08ef4ce2eedcd4d1abd8e641114bfc6c2867f49f3c41facf65bf19f5e", + "sha256:cc1c8b182eb3013e24bd475ff2e9295af86c1a38eb1aff128dac8962a9ce3c03" + ], + "markers": "python_version < '3.11'", + "version": "==0.3.7" + }, + "exceptiongroup": { + "hashes": [ + "sha256:4bfd3996ac73b41e9b9628b04e079f193850720ea5945fc96a08633c66912f14", + "sha256:91f5c769735f051a4290d52edd0858999b57e5876e9f85937691bd4c9fa3ed68" + ], + "markers": "python_version < '3.11'", + "version": "==1.2.0" + }, + "iniconfig": { + "hashes": [ + "sha256:2d91e135bf72d31a410b17c16da610a82cb55f6b0477d1a902134b24a455b8b3", + "sha256:b6a85871a79d2e3b22d2d1b94ac2824226a63c6b741c88f7ae975f18b6778374" + ], + "markers": "python_version >= '3.7'", + "version": "==2.0.0" + }, + "isort": { + "hashes": [ + "sha256:48fdfcb9face5d58a4f6dde2e72a1fb8dcaf8ab26f95ab49fab84c2ddefb0109", + "sha256:8ca5e72a8d85860d5a3fa69b8745237f2939afe12dbf656afbcb47fe72d947a6" + ], + "markers": "python_full_version >= '3.8.0'", + "version": "==5.13.2" + }, + "mccabe": { + "hashes": [ + "sha256:348e0240c33b60bbdf4e523192ef919f28cb2c3d7d5c7794f74009290f236325", + "sha256:6c2d30ab6be0e4a46919781807b4f0d834ebdd6c6e3dca0bda5a15f863427b6e" + ], + "markers": "python_version >= '3.6'", + "version": "==0.7.0" + }, + "mypy": { + "hashes": [ + "sha256:19f905bcfd9e167159b3d63ecd8cb5e696151c3e59a1742e79bc3bcb540c42c7", + "sha256:21a1ad938fee7d2d96ca666c77b7c494c3c5bd88dff792220e1afbebb2925b5e", + "sha256:40b1844d2e8b232ed92e50a4bd11c48d2daa351f9deee6c194b83bf03e418b0c", + "sha256:41697773aa0bf53ff917aa077e2cde7aa50254f28750f9b88884acea38a16169", + "sha256:49ae115da099dcc0922a7a895c1eec82c1518109ea5c162ed50e3b3594c71208", + "sha256:4c46b51de523817a0045b150ed11b56f9fff55f12b9edd0f3ed35b15a2809de0", + "sha256:4cbe68ef919c28ea561165206a2dcb68591c50f3bcf777932323bc208d949cf1", + "sha256:4d01c00d09a0be62a4ca3f933e315455bde83f37f892ba4b08ce92f3cf44bcc1", + "sha256:59a0d7d24dfb26729e0a068639a6ce3500e31d6655df8557156c51c1cb874ce7", + "sha256:68351911e85145f582b5aa6cd9ad666c8958bcae897a1bfda8f4940472463c45", + "sha256:7274b0c57737bd3476d2229c6389b2ec9eefeb090bbaf77777e9d6b1b5a9d143", + "sha256:81af8adaa5e3099469e7623436881eff6b3b06db5ef75e6f5b6d4871263547e5", + "sha256:82e469518d3e9a321912955cc702d418773a2fd1e91c651280a1bda10622f02f", + "sha256:8b27958f8c76bed8edaa63da0739d76e4e9ad4ed325c814f9b3851425582a3cd", + "sha256:8c223fa57cb154c7eab5156856c231c3f5eace1e0bed9b32a24696b7ba3c3245", + "sha256:8f57e6b6927a49550da3d122f0cb983d400f843a8a82e65b3b380d3d7259468f", + "sha256:925cd6a3b7b55dfba252b7c4561892311c5358c6b5a601847015a1ad4eb7d332", + "sha256:a43ef1c8ddfdb9575691720b6352761f3f53d85f1b57d7745701041053deff30", + "sha256:a8032e00ce71c3ceb93eeba63963b864bf635a18f6c0c12da6c13c450eedb183", + "sha256:b96ae2c1279d1065413965c607712006205a9ac541895004a1e0d4f281f2ff9f", + "sha256:bb8ccb4724f7d8601938571bf3f24da0da791fe2db7be3d9e79849cb64e0ae85", + "sha256:bbaf4662e498c8c2e352da5f5bca5ab29d378895fa2d980630656178bd607c46", + "sha256:cfd13d47b29ed3bbaafaff7d8b21e90d827631afda134836962011acb5904b71", + "sha256:d4473c22cc296425bbbce7e9429588e76e05bc7342da359d6520b6427bf76660", + "sha256:d8fbb68711905f8912e5af474ca8b78d077447d8f3918997fecbf26943ff3cbb", + "sha256:e5012e5cc2ac628177eaac0e83d622b2dd499e28253d4107a08ecc59ede3fc2c", + "sha256:eb4f18589d196a4cbe5290b435d135dee96567e07c2b2d43b5c4621b6501531a" + ], + "index": "pypi", + "version": "==1.6.1" + }, + "mypy-extensions": { + "hashes": [ + "sha256:4392f6c0eb8a5668a69e23d168ffa70f0be9ccfd32b5cc2d26a34ae5b844552d", + "sha256:75dbf8955dc00442a438fc4d0666508a9a97b6bd41aa2f0ffe9d2f2725af0782" + ], + "markers": "python_version >= '3.5'", + "version": "==1.0.0" + }, + "packaging": { + "hashes": [ + "sha256:048fb0e9405036518eaaf48a55953c750c11e1a1b68e0dd1a9d62ed0c092cfc5", + "sha256:8c491190033a9af7e1d931d0b5dacc2ef47509b34dd0de67ed209b5203fc88c7" + ], + "markers": "python_version >= '3.7'", + "version": "==23.2" + }, + "pathspec": { + "hashes": [ + "sha256:a0d503e138a4c123b27490a4f7beda6a01c6f288df0e4a8b79c7eb0dc7b4cc08", + "sha256:a482d51503a1ab33b1c67a6c3813a26953dbdc71c31dacaef9a838c4e29f5712" + ], + "markers": "python_version >= '3.8'", + "version": "==0.12.1" + }, + "platformdirs": { + "hashes": [ + "sha256:11c8f37bcca40db96d8144522d925583bdb7a31f7b0e37e3ed4318400a8e2380", + "sha256:906d548203468492d432bcb294d4bc2fff751bf84971fbb2c10918cc206ee420" + ], + "markers": "python_version >= '3.8'", + "version": "==4.1.0" + }, + "pluggy": { + "hashes": [ + "sha256:cf61ae8f126ac6f7c451172cf30e3e43d3ca77615509771b3a984a0730651e12", + "sha256:d89c696a773f8bd377d18e5ecda92b7a3793cbe66c87060a6fb58c7b6e1061f7" + ], + "markers": "python_version >= '3.8'", + "version": "==1.3.0" + }, + "pylint": { + "hashes": [ + "sha256:0d4c286ef6d2f66c8bfb527a7f8a629009e42c99707dec821a03e1b51a4c1496", + "sha256:60ed5f3a9ff8b61839ff0348b3624ceeb9e6c2a92c514d81c9cc273da3b6bcda" + ], + "index": "pypi", + "version": "==3.0.2" + }, + "pytest": { + "hashes": [ + "sha256:c7c6ca206e93355074ae32f7403e8ea12163b1163c976fee7d4d84027c162be5", + "sha256:d45e0952f3727241918b8fd0f376f5ff6b301cc0777c6f9a556935c92d8a7d42" + ], + "index": "pypi", + "version": "==7.2.1" + }, + "tomli": { + "hashes": [ + "sha256:939de3e7a6161af0c887ef91b7d41a53e7c5a1ca976325f429cb46ea9bc30ecc", + "sha256:de526c12914f0c550d15924c62d72abc48d6fe7364aa87328337a31007fe8a4f" + ], + "markers": "python_version < '3.11'", + "version": "==2.0.1" + }, + "tomlkit": { + "hashes": [ + "sha256:75baf5012d06501f07bee5bf8e801b9f343e7aac5a92581f20f80ce632e6b5a4", + "sha256:b0a645a9156dc7cb5d3a1f0d4bab66db287fcb8e0430bdd4664a095ea16414ba" + ], + "markers": "python_version >= '3.7'", + "version": "==0.12.3" + }, + "types-requests": { + "hashes": [ + "sha256:b32b9a86beffa876c0c3ac99a4cd3b8b51e973fb8e3bd4e0a6bb32c7efad80fc", + "sha256:dc5852a76f1eaf60eafa81a2e50aefa3d1f015c34cf0cba130930866b1b22a92" + ], + "index": "pypi", + "version": "==2.31.0.10" + }, + "typing-extensions": { + "hashes": [ + "sha256:23478f88c37f27d76ac8aee6c905017a143b0b1b886c3c9f66bc2fd94f9f5783", + "sha256:af72aea155e91adfc61c3ae9e0e342dbc0cba726d6cba4b6c72c1f34e47291cd" + ], + "markers": "python_version < '3.10'", + "version": "==4.9.0" + }, + "urllib3": { + "hashes": [ + "sha256:55901e917a5896a349ff771be919f8bd99aff50b79fe58fec595eb37bbc56bb3", + "sha256:df7aa8afb0148fa78488e7899b2c59b5f4ffcfa82e6c54ccb9dd37c1d7b52d54" + ], + "markers": "python_version >= '3.8'", + "version": "==2.1.0" + } + } +} diff --git a/.github/actions/email/send/__main__.py b/.github/actions/email/send/__main__.py new file mode 100644 index 00000000..5f5f8fe1 --- /dev/null +++ b/.github/actions/email/send/__main__.py @@ -0,0 +1,154 @@ +""" +ยฉ Ocado Group +Created on 29/12/2023 at 11:30:49(+00:00). + +Using DotDigital, send a transactional email using a triggered campaign as its +content. + +https://developer.dotdigital.com/reference/send-transactional-email-using-a-triggered-campaign +""" + +import json +import os +import typing as t + +import requests + +JsonBody = t.Dict[str, t.Any] + + +def get_settings(): + """Gets the general settings from environment variables. Variables are + parsed to the correct type. + + Returns: + A tuple with the values (region, auth, timeout). + """ + + region = os.getenv("REGION", "") + assert region != "", "Region path parameter not set." + + auth = os.getenv("AUTH", "") + assert auth != "", "Authorization header not set." + + timeout = int(os.getenv("TIMEOUT", "-1")) + assert timeout != -1, "Request timeout not set." + + return region, auth, timeout + + +def get_json_body() -> JsonBody: + """Gets the JSON body from environment variables. Variables are parsed to + the correct type. + + Returns: + A dictionary containing the request's JSON body. + """ + + body: JsonBody = {} + + def set_value( + env_key: str, + body_key: str, + required: bool, + json_loads: bool = True, + ): + """Helper to parse environment variables into body parameters. + + Args: + env_key: The key of the environment variable. + body_key: The key of the body parameter. + required: If this value is required in the request. + json_loads: If the value should be parsed as a JSON object. Strings + don't need to be parsed as JSON. + """ + + raw_value = os.getenv(env_key, "") + + if required: + assert raw_value != "", f'"{env_key}" environment variable not set.' + + if raw_value != "": + body[body_key] = json.loads(raw_value) if json_loads else raw_value + + set_value( + env_key="TO_ADDRESSES", + body_key="toAddresses", + required=True, + ) + set_value( + env_key="CC_ADDRESSES", + body_key="ccAddresses", + required=False, + ) + set_value( + env_key="BCC_ADDRESSES", + body_key="bccAddresses", + required=False, + ) + set_value( + env_key="FROM_ADDRESS", + body_key="fromAddress", + required=False, + json_loads=False, + ) + set_value( + env_key="CAMPAIGN_ID", + body_key="campaignId", + required=True, + ) + set_value( + env_key="PERSONALIZATION_VALUES", + body_key="personalizationValues", + required=False, + ) + set_value( + env_key="METADATA", + body_key="metadata", + required=False, + json_loads=False, + ) + set_value( + env_key="ATTACHMENTS", + body_key="attachments", + required=False, + ) + + return body + + +def send_email(region: str, auth: str, timeout: int, body: JsonBody): + """Sends the email. + + Args: + region: The API region to use. + auth: The authorization header used to authenticate with the API. + timeout: The number of seconds to wait before the request times out. + body: The request's JSON body. + """ + + response = requests.post( + url=f"https://{region}-api.dotdigital.com/v2/email/triggered-campaign", + json=body, + headers={ + "accept": "text/plain", + "authorization": auth, + }, + timeout=timeout, + ) + + assert response.ok, response.json() + + +def main(): + """Entry point.""" + + region, auth, timeout = get_settings() + + body = get_json_body() + + send_email(region, auth, timeout, body) + + +if __name__ == "__main__": + main() diff --git a/.github/actions/email/send/action.yaml b/.github/actions/email/send/action.yaml new file mode 100644 index 00000000..3adb28c0 --- /dev/null +++ b/.github/actions/email/send/action.yaml @@ -0,0 +1,62 @@ +name: "Code for Life - Email - Send" +description: "Using DotDigital, send a transactional email using a triggered campaign as its content." +inputs: + region: + description: "The Dotdigital region id your account belongs to e.g. r1, r2 or r3." + required: true + default: "r1" + auth: + description: "The authorization header used to authenticate with the api." + required: true + timeout: + description: "The number of seconds to wait for a response before timing out." + required: true + default: "60" + to-addresses: + description: "The email address(es) to send to." + required: true + cc-addresses: + description: "The CC email address or address to to send to. separate email addresses with a comma. Maximum: 100." + required: false + bcc-addresses: + description: "The BCC email address or address to to send to. separate email addresses with a comma. Maximum: 100." + required: false + from-address: + description: "The From address for your email. Note: The From address must already be added to your account. Otherwise, your account's default From address is used." + required: false + campaign-id: + description: "The ID of the triggered campaign, which needs to be included within the request body." + required: true + personalization-values: + description: "Each personalisation value is a key-value pair; the placeholder name of the personalization value needs to be included in the request body." + required: false + metadata: + description: "The metadata for your email. It can be either a single value or a series of values in a JSON object." + required: false + attachments: + description: "A Base64 encoded string. All attachment types are supported. Maximum file size: 15 MB." + required: false +runs: + using: composite + steps: + - uses: ocadotechnology/codeforlife-workspace/.github/actions/python/setup-environment@main + with: + python-version: 3.11 + working-directory: ${{ github.action_path }} + + - name: ๐Ÿ“ง Send Email + shell: bash + working-directory: ${{ github.action_path }} + run: pipenv run python . + env: + REGION: ${{ inputs.region }} + AUTH: ${{ inputs.auth }} + TIMEOUT: ${{ inputs.timeout }} + TO_ADDRESSES: ${{ inputs.to-addresses }} + CC_ADDRESSES: ${{ inputs.cc-addresses }} + BCC_ADDRESSES: ${{ inputs.bcc-addresses }} + FROM_ADDRESS: ${{ inputs.from-address }} + CAMPAIGN_ID: ${{ inputs.campaign-id }} + PERSONALIZATION_VALUES: ${{ inputs.personalization-values }} + METADATA: ${{ inputs.metadata }} + ATTACHMENTS: ${{ inputs.attachments }} diff --git a/.github/actions/python/setup-environment/action.yaml b/.github/actions/python/setup-environment/action.yaml new file mode 100644 index 00000000..3c579149 --- /dev/null +++ b/.github/actions/python/setup-environment/action.yaml @@ -0,0 +1,42 @@ +name: "Code for Life - Python - Setup Environment" +description: "Set up a python environment." +inputs: + checkout: + description: "A flag to designate if the code should be checked out." + required: true + default: "true" + python-version: + description: "The python version to set up." + required: true + default: "3.8" + working-directory: + description: "The current working directory." + required: true + default: "." + install-args: + description: "Arguments to pass to pipenv install." + required: false +runs: + using: composite + steps: + - name: ๐Ÿ›ซ Checkout + if: ${{ inputs.checkout == 'true' }} + uses: actions/checkout@v4 + + - name: ๐Ÿ Set up Python ${{ inputs.python-version }} + uses: actions/setup-python@v4 + with: + python-version: ${{ inputs.python-version }} + + - name: โฌ†๏ธ Upgrade pip + shell: bash + run: python -m pip install --upgrade pip + + - name: ๐Ÿ›  Install pipenv + shell: bash + run: python -m pip install pipenv + + - name: ๐Ÿ›  Install Dependencies + shell: bash + working-directory: ${{ inputs.working-directory }} + run: pipenv install ${{ inputs.install-args }} diff --git a/.github/actions/python/test/action.yaml b/.github/actions/python/test/action.yaml deleted file mode 100644 index 83a651f6..00000000 --- a/.github/actions/python/test/action.yaml +++ /dev/null @@ -1,55 +0,0 @@ -name: "Code for Life - Python - Test" -description: "Tests python code written in the CFL workspace." -inputs: - python-version: - description: "The python version to set up." - required: true - default: "3.8" - working-directory: - description: "The current working directory." - required: true - default: "." - check-django-migrations: - description: "Check if there are pending Django migrations." - required: true - default: "true" -runs: - using: composite - steps: - - name: ๐Ÿ›ซ Checkout - uses: actions/checkout@v4 - - - name: ๐Ÿ Set up Python ${{ inputs.python-version }} - uses: actions/setup-python@v4 - with: - python-version: ${{ inputs.python-version }} - - - name: ๐Ÿ›  Install Dependencies - shell: bash - working-directory: ${{ inputs.working-directory }} - run: | - python -m pip install --upgrade pip - python -m pip install pipenv - pipenv install --dev - - - name: ๐Ÿ”Ž Check Code Format - shell: bash - working-directory: ${{ inputs.working-directory }} - run: if ! pipenv run black --check .; then exit 1; fi - - # TODO: check static type hints with mypy - - # TODO: check linter error with pylint - - - name: ๐Ÿ”Ž Check Django Migrations - if: inputs.check-django-migrations == 'true' - shell: bash - working-directory: ${{ inputs.working-directory }} - run: pipenv run python manage.py makemigrations --check --dry-run - - - name: ๐Ÿงช Test Code Units - shell: bash - working-directory: ${{ inputs.working-directory }} - run: pipenv run pytest -n auto - - # TODO: assert code coverage target. diff --git a/.github/scripts/python/validate-existing-contributors/.venv/.gitkeep b/.github/scripts/python/validate-existing-contributors/.venv/.gitkeep new file mode 100644 index 00000000..e69de29b diff --git a/.github/scripts/python/validate-existing-contributors/Pipfile b/.github/scripts/python/validate-existing-contributors/Pipfile new file mode 100644 index 00000000..ffe061c7 --- /dev/null +++ b/.github/scripts/python/validate-existing-contributors/Pipfile @@ -0,0 +1,16 @@ +[[source]] +url = "https://pypi.org/simple" +verify_ssl = true +name = "pypi" + +[packages] + +[dev-packages] +black = "==23.1.0" +pytest = "==7.2.1" +mypy = "==1.6.1" +pylint = "==3.0.2" +types-requests = "==2.31.0.10" + +[requires] +python_version = "3.11" diff --git a/.github/scripts/python/validate-existing-contributors/Pipfile.lock b/.github/scripts/python/validate-existing-contributors/Pipfile.lock new file mode 100644 index 00000000..e31ed49f --- /dev/null +++ b/.github/scripts/python/validate-existing-contributors/Pipfile.lock @@ -0,0 +1,229 @@ +{ + "_meta": { + "hash": { + "sha256": "4dc4a5a52138e83335a5ec27a3e6fa7385d5654328e3b3dc0b33cebcf3d1bf80" + }, + "pipfile-spec": 6, + "requires": { + "python_version": "3.11" + }, + "sources": [ + { + "name": "pypi", + "url": "https://pypi.org/simple", + "verify_ssl": true + } + ] + }, + "default": {}, + "develop": { + "astroid": { + "hashes": [ + "sha256:4a61cf0a59097c7bb52689b0fd63717cd2a8a14dc9f1eee97b82d814881c8c91", + "sha256:d6e62862355f60e716164082d6b4b041d38e2a8cf1c7cd953ded5108bac8ff5c" + ], + "markers": "python_full_version >= '3.8.0'", + "version": "==3.0.2" + }, + "attrs": { + "hashes": [ + "sha256:935dc3b529c262f6cf76e50877d35a4bd3c1de194fd41f47a2b7ae8f19971f30", + "sha256:99b87a485a5820b23b879f04c2305b44b951b502fd64be915879d77a7e8fc6f1" + ], + "markers": "python_version >= '3.7'", + "version": "==23.2.0" + }, + "black": { + "hashes": [ + "sha256:0052dba51dec07ed029ed61b18183942043e00008ec65d5028814afaab9a22fd", + "sha256:0680d4380db3719ebcfb2613f34e86c8e6d15ffeabcf8ec59355c5e7b85bb555", + "sha256:121ca7f10b4a01fd99951234abdbd97728e1240be89fde18480ffac16503d481", + "sha256:162e37d49e93bd6eb6f1afc3e17a3d23a823042530c37c3c42eeeaf026f38468", + "sha256:2a951cc83ab535d248c89f300eccbd625e80ab880fbcfb5ac8afb5f01a258ac9", + "sha256:2bf649fda611c8550ca9d7592b69f0637218c2369b7744694c5e4902873b2f3a", + "sha256:382998821f58e5c8238d3166c492139573325287820963d2f7de4d518bd76958", + "sha256:49f7b39e30f326a34b5c9a4213213a6b221d7ae9d58ec70df1c4a307cf2a1580", + "sha256:57c18c5165c1dbe291d5306e53fb3988122890e57bd9b3dcb75f967f13411a26", + "sha256:7a0f701d314cfa0896b9001df70a530eb2472babb76086344e688829efd97d32", + "sha256:8178318cb74f98bc571eef19068f6ab5613b3e59d4f47771582f04e175570ed8", + "sha256:8b70eb40a78dfac24842458476135f9b99ab952dd3f2dab738c1881a9b38b753", + "sha256:9880d7d419bb7e709b37e28deb5e68a49227713b623c72b2b931028ea65f619b", + "sha256:9afd3f493666a0cd8f8df9a0200c6359ac53940cbde049dcb1a7eb6ee2dd7074", + "sha256:a29650759a6a0944e7cca036674655c2f0f63806ddecc45ed40b7b8aa314b651", + "sha256:a436e7881d33acaf2536c46a454bb964a50eff59b21b51c6ccf5a40601fbef24", + "sha256:a59db0a2094d2259c554676403fa2fac3473ccf1354c1c63eccf7ae65aac8ab6", + "sha256:a8471939da5e824b891b25751955be52ee7f8a30a916d570a5ba8e0f2eb2ecad", + "sha256:b0bd97bea8903f5a2ba7219257a44e3f1f9d00073d6cc1add68f0beec69692ac", + "sha256:b6a92a41ee34b883b359998f0c8e6eb8e99803aa8bf3123bf2b2e6fec505a221", + "sha256:bb460c8561c8c1bec7824ecbc3ce085eb50005883a6203dcfb0122e95797ee06", + "sha256:bfffba28dc52a58f04492181392ee380e95262af14ee01d4bc7bb1b1c6ca8d27", + "sha256:c1c476bc7b7d021321e7d93dc2cbd78ce103b84d5a4cf97ed535fbc0d6660648", + "sha256:c91dfc2c2a4e50df0026f88d2215e166616e0c80e86004d0003ece0488db2739", + "sha256:e6663f91b6feca5d06f2ccd49a10f254f9298cc1f7f49c46e498a0771b507104" + ], + "index": "pypi", + "version": "==23.1.0" + }, + "click": { + "hashes": [ + "sha256:ae74fb96c20a0277a1d615f1e4d73c8414f5a98db8b799a7931d1582f3390c28", + "sha256:ca9853ad459e787e2192211578cc907e7594e294c7ccc834310722b41b9ca6de" + ], + "markers": "python_version >= '3.7'", + "version": "==8.1.7" + }, + "dill": { + "hashes": [ + "sha256:76b122c08ef4ce2eedcd4d1abd8e641114bfc6c2867f49f3c41facf65bf19f5e", + "sha256:cc1c8b182eb3013e24bd475ff2e9295af86c1a38eb1aff128dac8962a9ce3c03" + ], + "markers": "python_version >= '3.11'", + "version": "==0.3.7" + }, + "iniconfig": { + "hashes": [ + "sha256:2d91e135bf72d31a410b17c16da610a82cb55f6b0477d1a902134b24a455b8b3", + "sha256:b6a85871a79d2e3b22d2d1b94ac2824226a63c6b741c88f7ae975f18b6778374" + ], + "markers": "python_version >= '3.7'", + "version": "==2.0.0" + }, + "isort": { + "hashes": [ + "sha256:48fdfcb9face5d58a4f6dde2e72a1fb8dcaf8ab26f95ab49fab84c2ddefb0109", + "sha256:8ca5e72a8d85860d5a3fa69b8745237f2939afe12dbf656afbcb47fe72d947a6" + ], + "markers": "python_full_version >= '3.8.0'", + "version": "==5.13.2" + }, + "mccabe": { + "hashes": [ + "sha256:348e0240c33b60bbdf4e523192ef919f28cb2c3d7d5c7794f74009290f236325", + "sha256:6c2d30ab6be0e4a46919781807b4f0d834ebdd6c6e3dca0bda5a15f863427b6e" + ], + "markers": "python_version >= '3.6'", + "version": "==0.7.0" + }, + "mypy": { + "hashes": [ + "sha256:19f905bcfd9e167159b3d63ecd8cb5e696151c3e59a1742e79bc3bcb540c42c7", + "sha256:21a1ad938fee7d2d96ca666c77b7c494c3c5bd88dff792220e1afbebb2925b5e", + "sha256:40b1844d2e8b232ed92e50a4bd11c48d2daa351f9deee6c194b83bf03e418b0c", + "sha256:41697773aa0bf53ff917aa077e2cde7aa50254f28750f9b88884acea38a16169", + "sha256:49ae115da099dcc0922a7a895c1eec82c1518109ea5c162ed50e3b3594c71208", + "sha256:4c46b51de523817a0045b150ed11b56f9fff55f12b9edd0f3ed35b15a2809de0", + "sha256:4cbe68ef919c28ea561165206a2dcb68591c50f3bcf777932323bc208d949cf1", + "sha256:4d01c00d09a0be62a4ca3f933e315455bde83f37f892ba4b08ce92f3cf44bcc1", + "sha256:59a0d7d24dfb26729e0a068639a6ce3500e31d6655df8557156c51c1cb874ce7", + "sha256:68351911e85145f582b5aa6cd9ad666c8958bcae897a1bfda8f4940472463c45", + "sha256:7274b0c57737bd3476d2229c6389b2ec9eefeb090bbaf77777e9d6b1b5a9d143", + "sha256:81af8adaa5e3099469e7623436881eff6b3b06db5ef75e6f5b6d4871263547e5", + "sha256:82e469518d3e9a321912955cc702d418773a2fd1e91c651280a1bda10622f02f", + "sha256:8b27958f8c76bed8edaa63da0739d76e4e9ad4ed325c814f9b3851425582a3cd", + "sha256:8c223fa57cb154c7eab5156856c231c3f5eace1e0bed9b32a24696b7ba3c3245", + "sha256:8f57e6b6927a49550da3d122f0cb983d400f843a8a82e65b3b380d3d7259468f", + "sha256:925cd6a3b7b55dfba252b7c4561892311c5358c6b5a601847015a1ad4eb7d332", + "sha256:a43ef1c8ddfdb9575691720b6352761f3f53d85f1b57d7745701041053deff30", + "sha256:a8032e00ce71c3ceb93eeba63963b864bf635a18f6c0c12da6c13c450eedb183", + "sha256:b96ae2c1279d1065413965c607712006205a9ac541895004a1e0d4f281f2ff9f", + "sha256:bb8ccb4724f7d8601938571bf3f24da0da791fe2db7be3d9e79849cb64e0ae85", + "sha256:bbaf4662e498c8c2e352da5f5bca5ab29d378895fa2d980630656178bd607c46", + "sha256:cfd13d47b29ed3bbaafaff7d8b21e90d827631afda134836962011acb5904b71", + "sha256:d4473c22cc296425bbbce7e9429588e76e05bc7342da359d6520b6427bf76660", + "sha256:d8fbb68711905f8912e5af474ca8b78d077447d8f3918997fecbf26943ff3cbb", + "sha256:e5012e5cc2ac628177eaac0e83d622b2dd499e28253d4107a08ecc59ede3fc2c", + "sha256:eb4f18589d196a4cbe5290b435d135dee96567e07c2b2d43b5c4621b6501531a" + ], + "index": "pypi", + "version": "==1.6.1" + }, + "mypy-extensions": { + "hashes": [ + "sha256:4392f6c0eb8a5668a69e23d168ffa70f0be9ccfd32b5cc2d26a34ae5b844552d", + "sha256:75dbf8955dc00442a438fc4d0666508a9a97b6bd41aa2f0ffe9d2f2725af0782" + ], + "markers": "python_version >= '3.5'", + "version": "==1.0.0" + }, + "packaging": { + "hashes": [ + "sha256:048fb0e9405036518eaaf48a55953c750c11e1a1b68e0dd1a9d62ed0c092cfc5", + "sha256:8c491190033a9af7e1d931d0b5dacc2ef47509b34dd0de67ed209b5203fc88c7" + ], + "markers": "python_version >= '3.7'", + "version": "==23.2" + }, + "pathspec": { + "hashes": [ + "sha256:a0d503e138a4c123b27490a4f7beda6a01c6f288df0e4a8b79c7eb0dc7b4cc08", + "sha256:a482d51503a1ab33b1c67a6c3813a26953dbdc71c31dacaef9a838c4e29f5712" + ], + "markers": "python_version >= '3.8'", + "version": "==0.12.1" + }, + "platformdirs": { + "hashes": [ + "sha256:11c8f37bcca40db96d8144522d925583bdb7a31f7b0e37e3ed4318400a8e2380", + "sha256:906d548203468492d432bcb294d4bc2fff751bf84971fbb2c10918cc206ee420" + ], + "markers": "python_version >= '3.8'", + "version": "==4.1.0" + }, + "pluggy": { + "hashes": [ + "sha256:cf61ae8f126ac6f7c451172cf30e3e43d3ca77615509771b3a984a0730651e12", + "sha256:d89c696a773f8bd377d18e5ecda92b7a3793cbe66c87060a6fb58c7b6e1061f7" + ], + "markers": "python_version >= '3.8'", + "version": "==1.3.0" + }, + "pylint": { + "hashes": [ + "sha256:0d4c286ef6d2f66c8bfb527a7f8a629009e42c99707dec821a03e1b51a4c1496", + "sha256:60ed5f3a9ff8b61839ff0348b3624ceeb9e6c2a92c514d81c9cc273da3b6bcda" + ], + "index": "pypi", + "version": "==3.0.2" + }, + "pytest": { + "hashes": [ + "sha256:c7c6ca206e93355074ae32f7403e8ea12163b1163c976fee7d4d84027c162be5", + "sha256:d45e0952f3727241918b8fd0f376f5ff6b301cc0777c6f9a556935c92d8a7d42" + ], + "index": "pypi", + "version": "==7.2.1" + }, + "tomlkit": { + "hashes": [ + "sha256:75baf5012d06501f07bee5bf8e801b9f343e7aac5a92581f20f80ce632e6b5a4", + "sha256:b0a645a9156dc7cb5d3a1f0d4bab66db287fcb8e0430bdd4664a095ea16414ba" + ], + "markers": "python_version >= '3.7'", + "version": "==0.12.3" + }, + "types-requests": { + "hashes": [ + "sha256:b32b9a86beffa876c0c3ac99a4cd3b8b51e973fb8e3bd4e0a6bb32c7efad80fc", + "sha256:dc5852a76f1eaf60eafa81a2e50aefa3d1f015c34cf0cba130930866b1b22a92" + ], + "index": "pypi", + "version": "==2.31.0.10" + }, + "typing-extensions": { + "hashes": [ + "sha256:23478f88c37f27d76ac8aee6c905017a143b0b1b886c3c9f66bc2fd94f9f5783", + "sha256:af72aea155e91adfc61c3ae9e0e342dbc0cba726d6cba4b6c72c1f34e47291cd" + ], + "markers": "python_version >= '3.8'", + "version": "==4.9.0" + }, + "urllib3": { + "hashes": [ + "sha256:55901e917a5896a349ff771be919f8bd99aff50b79fe58fec595eb37bbc56bb3", + "sha256:df7aa8afb0148fa78488e7899b2c59b5f4ffcfa82e6c54ccb9dd37c1d7b52d54" + ], + "markers": "python_version >= '3.8'", + "version": "==2.1.0" + } + } +} diff --git a/.github/scripts/python/validate-existing-contributors/__main__.py b/.github/scripts/python/validate-existing-contributors/__main__.py new file mode 100644 index 00000000..dbd4c538 --- /dev/null +++ b/.github/scripts/python/validate-existing-contributors/__main__.py @@ -0,0 +1,95 @@ +""" +ยฉ Ocado Group +Created on 08/01/2024 at 09:47:25(+00:00). + +Validate all contributors have signed the contribution agreement. +""" + +import json +import os +import typing as t +from email.utils import parseaddr + +PullRequest = t.Dict[str, t.Any] +Contributors = t.Set[str] + +# pylint: disable-next=line-too-long +CONTRIBUTING_FILE_NAME = "CONTRIBUTING.md" +CONTRIBUTORS_HEADER = "### ๐Ÿ‘จ\u200d๐Ÿ’ป Contributors ๐Ÿ‘ฉ\u200d๐Ÿ’ป" +BOTS = { + "49699333+dependabot[bot]@users.noreply.github.com", +} + + +def get_inputs(): + """Get script's inputs. + + Returns: + A JSON object of the pull request. + """ + + pull_request: PullRequest = json.loads(os.environ["PULL_REQUEST"]) + + return pull_request + + +def get_signed_contributors() -> Contributors: + """Get the contributors that have signed the contribution agreement. + + Returns: + A set of the contributors' email addresses. + """ + + with open( + f"../../../../{CONTRIBUTING_FILE_NAME}", + "r", + encoding="utf-8", + ) as contributing: + lines = contributing.read().splitlines() + + # NOTE: +2 because we don't want the header and its proceeding blank line. + lines = lines[lines.index(CONTRIBUTORS_HEADER) + 2 :] + + return {parseaddr(line)[1] for line in lines} + + +def assert_contributors( + pull_request: PullRequest, + signed_contributors: Contributors, +): + """Assert that all contributors have signed the contribution agreement. + + Args: + pull_request: The JSON object of the pull request. + signed_contributors: The contributors that have signed the contribution + agreement. + """ + + contributors: Contributors = { + author["email"] + for commit in pull_request["commits"] + for author in commit["authors"] + } + + unsigned_contributors = contributors.difference( + signed_contributors.union(BOTS), + ) + + assert not unsigned_contributors, ( + "The following contributors have not signed the agreement:" + f" {', '.join(unsigned_contributors)}." + ) + + +def main(): + """Entry point.""" + + pull_request = get_inputs() + + signed_contributors = get_signed_contributors() + + assert_contributors(pull_request, signed_contributors) + + +if __name__ == "__main__": + main() diff --git a/.github/scripts/python/validate-new-contributor/.venv/.gitkeep b/.github/scripts/python/validate-new-contributor/.venv/.gitkeep new file mode 100644 index 00000000..e69de29b diff --git a/.github/scripts/python/validate-new-contributor/Pipfile b/.github/scripts/python/validate-new-contributor/Pipfile new file mode 100644 index 00000000..c93ff348 --- /dev/null +++ b/.github/scripts/python/validate-new-contributor/Pipfile @@ -0,0 +1,15 @@ +[[source]] +url = "https://pypi.org/simple" +verify_ssl = true +name = "pypi" + +[packages] + +[dev-packages] +black = "==23.1.0" +pytest = "==7.2.1" +mypy = "==1.6.1" +pylint = "==3.0.2" + +[requires] +python_version = "3.11" diff --git a/.github/scripts/python/validate-new-contributor/Pipfile.lock b/.github/scripts/python/validate-new-contributor/Pipfile.lock new file mode 100644 index 00000000..d43d56b0 --- /dev/null +++ b/.github/scripts/python/validate-new-contributor/Pipfile.lock @@ -0,0 +1,213 @@ +{ + "_meta": { + "hash": { + "sha256": "be5bb9491279fa95591b4214435181afcd323d9ec48411dd6a25ccc240ee79c7" + }, + "pipfile-spec": 6, + "requires": { + "python_version": "3.11" + }, + "sources": [ + { + "name": "pypi", + "url": "https://pypi.org/simple", + "verify_ssl": true + } + ] + }, + "default": {}, + "develop": { + "astroid": { + "hashes": [ + "sha256:4a61cf0a59097c7bb52689b0fd63717cd2a8a14dc9f1eee97b82d814881c8c91", + "sha256:d6e62862355f60e716164082d6b4b041d38e2a8cf1c7cd953ded5108bac8ff5c" + ], + "markers": "python_full_version >= '3.8.0'", + "version": "==3.0.2" + }, + "attrs": { + "hashes": [ + "sha256:935dc3b529c262f6cf76e50877d35a4bd3c1de194fd41f47a2b7ae8f19971f30", + "sha256:99b87a485a5820b23b879f04c2305b44b951b502fd64be915879d77a7e8fc6f1" + ], + "markers": "python_version >= '3.7'", + "version": "==23.2.0" + }, + "black": { + "hashes": [ + "sha256:0052dba51dec07ed029ed61b18183942043e00008ec65d5028814afaab9a22fd", + "sha256:0680d4380db3719ebcfb2613f34e86c8e6d15ffeabcf8ec59355c5e7b85bb555", + "sha256:121ca7f10b4a01fd99951234abdbd97728e1240be89fde18480ffac16503d481", + "sha256:162e37d49e93bd6eb6f1afc3e17a3d23a823042530c37c3c42eeeaf026f38468", + "sha256:2a951cc83ab535d248c89f300eccbd625e80ab880fbcfb5ac8afb5f01a258ac9", + "sha256:2bf649fda611c8550ca9d7592b69f0637218c2369b7744694c5e4902873b2f3a", + "sha256:382998821f58e5c8238d3166c492139573325287820963d2f7de4d518bd76958", + "sha256:49f7b39e30f326a34b5c9a4213213a6b221d7ae9d58ec70df1c4a307cf2a1580", + "sha256:57c18c5165c1dbe291d5306e53fb3988122890e57bd9b3dcb75f967f13411a26", + "sha256:7a0f701d314cfa0896b9001df70a530eb2472babb76086344e688829efd97d32", + "sha256:8178318cb74f98bc571eef19068f6ab5613b3e59d4f47771582f04e175570ed8", + "sha256:8b70eb40a78dfac24842458476135f9b99ab952dd3f2dab738c1881a9b38b753", + "sha256:9880d7d419bb7e709b37e28deb5e68a49227713b623c72b2b931028ea65f619b", + "sha256:9afd3f493666a0cd8f8df9a0200c6359ac53940cbde049dcb1a7eb6ee2dd7074", + "sha256:a29650759a6a0944e7cca036674655c2f0f63806ddecc45ed40b7b8aa314b651", + "sha256:a436e7881d33acaf2536c46a454bb964a50eff59b21b51c6ccf5a40601fbef24", + "sha256:a59db0a2094d2259c554676403fa2fac3473ccf1354c1c63eccf7ae65aac8ab6", + "sha256:a8471939da5e824b891b25751955be52ee7f8a30a916d570a5ba8e0f2eb2ecad", + "sha256:b0bd97bea8903f5a2ba7219257a44e3f1f9d00073d6cc1add68f0beec69692ac", + "sha256:b6a92a41ee34b883b359998f0c8e6eb8e99803aa8bf3123bf2b2e6fec505a221", + "sha256:bb460c8561c8c1bec7824ecbc3ce085eb50005883a6203dcfb0122e95797ee06", + "sha256:bfffba28dc52a58f04492181392ee380e95262af14ee01d4bc7bb1b1c6ca8d27", + "sha256:c1c476bc7b7d021321e7d93dc2cbd78ce103b84d5a4cf97ed535fbc0d6660648", + "sha256:c91dfc2c2a4e50df0026f88d2215e166616e0c80e86004d0003ece0488db2739", + "sha256:e6663f91b6feca5d06f2ccd49a10f254f9298cc1f7f49c46e498a0771b507104" + ], + "index": "pypi", + "version": "==23.1.0" + }, + "click": { + "hashes": [ + "sha256:ae74fb96c20a0277a1d615f1e4d73c8414f5a98db8b799a7931d1582f3390c28", + "sha256:ca9853ad459e787e2192211578cc907e7594e294c7ccc834310722b41b9ca6de" + ], + "markers": "python_version >= '3.7'", + "version": "==8.1.7" + }, + "dill": { + "hashes": [ + "sha256:76b122c08ef4ce2eedcd4d1abd8e641114bfc6c2867f49f3c41facf65bf19f5e", + "sha256:cc1c8b182eb3013e24bd475ff2e9295af86c1a38eb1aff128dac8962a9ce3c03" + ], + "markers": "python_version >= '3.11'", + "version": "==0.3.7" + }, + "iniconfig": { + "hashes": [ + "sha256:2d91e135bf72d31a410b17c16da610a82cb55f6b0477d1a902134b24a455b8b3", + "sha256:b6a85871a79d2e3b22d2d1b94ac2824226a63c6b741c88f7ae975f18b6778374" + ], + "markers": "python_version >= '3.7'", + "version": "==2.0.0" + }, + "isort": { + "hashes": [ + "sha256:48fdfcb9face5d58a4f6dde2e72a1fb8dcaf8ab26f95ab49fab84c2ddefb0109", + "sha256:8ca5e72a8d85860d5a3fa69b8745237f2939afe12dbf656afbcb47fe72d947a6" + ], + "markers": "python_full_version >= '3.8.0'", + "version": "==5.13.2" + }, + "mccabe": { + "hashes": [ + "sha256:348e0240c33b60bbdf4e523192ef919f28cb2c3d7d5c7794f74009290f236325", + "sha256:6c2d30ab6be0e4a46919781807b4f0d834ebdd6c6e3dca0bda5a15f863427b6e" + ], + "markers": "python_version >= '3.6'", + "version": "==0.7.0" + }, + "mypy": { + "hashes": [ + "sha256:19f905bcfd9e167159b3d63ecd8cb5e696151c3e59a1742e79bc3bcb540c42c7", + "sha256:21a1ad938fee7d2d96ca666c77b7c494c3c5bd88dff792220e1afbebb2925b5e", + "sha256:40b1844d2e8b232ed92e50a4bd11c48d2daa351f9deee6c194b83bf03e418b0c", + "sha256:41697773aa0bf53ff917aa077e2cde7aa50254f28750f9b88884acea38a16169", + "sha256:49ae115da099dcc0922a7a895c1eec82c1518109ea5c162ed50e3b3594c71208", + "sha256:4c46b51de523817a0045b150ed11b56f9fff55f12b9edd0f3ed35b15a2809de0", + "sha256:4cbe68ef919c28ea561165206a2dcb68591c50f3bcf777932323bc208d949cf1", + "sha256:4d01c00d09a0be62a4ca3f933e315455bde83f37f892ba4b08ce92f3cf44bcc1", + "sha256:59a0d7d24dfb26729e0a068639a6ce3500e31d6655df8557156c51c1cb874ce7", + "sha256:68351911e85145f582b5aa6cd9ad666c8958bcae897a1bfda8f4940472463c45", + "sha256:7274b0c57737bd3476d2229c6389b2ec9eefeb090bbaf77777e9d6b1b5a9d143", + "sha256:81af8adaa5e3099469e7623436881eff6b3b06db5ef75e6f5b6d4871263547e5", + "sha256:82e469518d3e9a321912955cc702d418773a2fd1e91c651280a1bda10622f02f", + "sha256:8b27958f8c76bed8edaa63da0739d76e4e9ad4ed325c814f9b3851425582a3cd", + "sha256:8c223fa57cb154c7eab5156856c231c3f5eace1e0bed9b32a24696b7ba3c3245", + "sha256:8f57e6b6927a49550da3d122f0cb983d400f843a8a82e65b3b380d3d7259468f", + "sha256:925cd6a3b7b55dfba252b7c4561892311c5358c6b5a601847015a1ad4eb7d332", + "sha256:a43ef1c8ddfdb9575691720b6352761f3f53d85f1b57d7745701041053deff30", + "sha256:a8032e00ce71c3ceb93eeba63963b864bf635a18f6c0c12da6c13c450eedb183", + "sha256:b96ae2c1279d1065413965c607712006205a9ac541895004a1e0d4f281f2ff9f", + "sha256:bb8ccb4724f7d8601938571bf3f24da0da791fe2db7be3d9e79849cb64e0ae85", + "sha256:bbaf4662e498c8c2e352da5f5bca5ab29d378895fa2d980630656178bd607c46", + "sha256:cfd13d47b29ed3bbaafaff7d8b21e90d827631afda134836962011acb5904b71", + "sha256:d4473c22cc296425bbbce7e9429588e76e05bc7342da359d6520b6427bf76660", + "sha256:d8fbb68711905f8912e5af474ca8b78d077447d8f3918997fecbf26943ff3cbb", + "sha256:e5012e5cc2ac628177eaac0e83d622b2dd499e28253d4107a08ecc59ede3fc2c", + "sha256:eb4f18589d196a4cbe5290b435d135dee96567e07c2b2d43b5c4621b6501531a" + ], + "index": "pypi", + "version": "==1.6.1" + }, + "mypy-extensions": { + "hashes": [ + "sha256:4392f6c0eb8a5668a69e23d168ffa70f0be9ccfd32b5cc2d26a34ae5b844552d", + "sha256:75dbf8955dc00442a438fc4d0666508a9a97b6bd41aa2f0ffe9d2f2725af0782" + ], + "markers": "python_version >= '3.5'", + "version": "==1.0.0" + }, + "packaging": { + "hashes": [ + "sha256:048fb0e9405036518eaaf48a55953c750c11e1a1b68e0dd1a9d62ed0c092cfc5", + "sha256:8c491190033a9af7e1d931d0b5dacc2ef47509b34dd0de67ed209b5203fc88c7" + ], + "markers": "python_version >= '3.7'", + "version": "==23.2" + }, + "pathspec": { + "hashes": [ + "sha256:a0d503e138a4c123b27490a4f7beda6a01c6f288df0e4a8b79c7eb0dc7b4cc08", + "sha256:a482d51503a1ab33b1c67a6c3813a26953dbdc71c31dacaef9a838c4e29f5712" + ], + "markers": "python_version >= '3.8'", + "version": "==0.12.1" + }, + "platformdirs": { + "hashes": [ + "sha256:11c8f37bcca40db96d8144522d925583bdb7a31f7b0e37e3ed4318400a8e2380", + "sha256:906d548203468492d432bcb294d4bc2fff751bf84971fbb2c10918cc206ee420" + ], + "markers": "python_version >= '3.8'", + "version": "==4.1.0" + }, + "pluggy": { + "hashes": [ + "sha256:cf61ae8f126ac6f7c451172cf30e3e43d3ca77615509771b3a984a0730651e12", + "sha256:d89c696a773f8bd377d18e5ecda92b7a3793cbe66c87060a6fb58c7b6e1061f7" + ], + "markers": "python_version >= '3.8'", + "version": "==1.3.0" + }, + "pylint": { + "hashes": [ + "sha256:0d4c286ef6d2f66c8bfb527a7f8a629009e42c99707dec821a03e1b51a4c1496", + "sha256:60ed5f3a9ff8b61839ff0348b3624ceeb9e6c2a92c514d81c9cc273da3b6bcda" + ], + "index": "pypi", + "version": "==3.0.2" + }, + "pytest": { + "hashes": [ + "sha256:c7c6ca206e93355074ae32f7403e8ea12163b1163c976fee7d4d84027c162be5", + "sha256:d45e0952f3727241918b8fd0f376f5ff6b301cc0777c6f9a556935c92d8a7d42" + ], + "index": "pypi", + "version": "==7.2.1" + }, + "tomlkit": { + "hashes": [ + "sha256:75baf5012d06501f07bee5bf8e801b9f343e7aac5a92581f20f80ce632e6b5a4", + "sha256:b0a645a9156dc7cb5d3a1f0d4bab66db287fcb8e0430bdd4664a095ea16414ba" + ], + "markers": "python_version >= '3.7'", + "version": "==0.12.3" + }, + "typing-extensions": { + "hashes": [ + "sha256:23478f88c37f27d76ac8aee6c905017a143b0b1b886c3c9f66bc2fd94f9f5783", + "sha256:af72aea155e91adfc61c3ae9e0e342dbc0cba726d6cba4b6c72c1f34e47291cd" + ], + "markers": "python_version >= '3.8'", + "version": "==4.9.0" + } + } +} diff --git a/.github/scripts/python/validate-new-contributor/__main__.py b/.github/scripts/python/validate-new-contributor/__main__.py new file mode 100644 index 00000000..bb15efff --- /dev/null +++ b/.github/scripts/python/validate-new-contributor/__main__.py @@ -0,0 +1,196 @@ +""" +ยฉ Ocado Group +Created on 19/12/2023 at 16:07:39(+00:00). + +Validates adding a new contributor to the contributor agreement. +""" + +import os +import re +import subprocess +from email.utils import parseaddr + +# Global settings. +DEFAULT_BRANCH = "main" +CONTRIBUTING_FILE_NAME = "CONTRIBUTING.md" +CONTRIBUTING_FILE_PATH = f"../../../../{CONTRIBUTING_FILE_NAME}" +CONTRIBUTORS_HEADER = "### ๐Ÿ‘จ\u200d๐Ÿ’ป Contributors ๐Ÿ‘ฉ\u200d๐Ÿ’ป" + + +def fetch_default_branch(): + """Fetch default branch from origin.""" + + subprocess.run( + [ + "git", + "fetch", + "origin", + f"{DEFAULT_BRANCH}:{DEFAULT_BRANCH}", + ], + check=True, + ) + + +def assert_diff_stats(): + """Assert that only the contribution agreement is different and only one + line was added to the contribution agreement. + """ + + # Get raw diff stats from default branch. + diff_stats_str = subprocess.run( + [ + "git", + "diff", + DEFAULT_BRANCH, + "--numstat", + ], + check=True, + stdout=subprocess.PIPE, + ).stdout.decode("utf-8") + + print(f'Diff Stats: "{diff_stats_str}"') + + # Get diff stats per file. + diff_stats_per_file = diff_stats_str.splitlines() + assert ( + len(diff_stats_per_file) == 1 + ), f"Only {CONTRIBUTING_FILE_NAME} should be different." + + # Parse and assert diff stats. + diff_stats = diff_stats_per_file[0].split("\t") + assert ( + diff_stats[2] == CONTRIBUTING_FILE_NAME + ), f"Only {CONTRIBUTING_FILE_NAME} should be different." + assert int(diff_stats[1]) == 0, "You cannot modify or delete existing lines." + assert int(diff_stats[0]) == 1, "You must add just one line." + + +def get_diff_line(): + """Get the one differing line from the contribution agreement. Asserts that + the line is located in the list of contributors. + + Returns: + The one-based index and string of the one differing line from the + contribution agreement. + """ + + # Easier to parse diff stats for assertions. + assert_diff_stats() + + # Get raw diff from default branch. + diff_str = subprocess.run( + [ + "git", + "--no-pager", + "diff", + DEFAULT_BRANCH, + ], + check=True, + stdout=subprocess.PIPE, + ).stdout.decode("utf-8") + + print(f'Diff: "{diff_str}"') + + # Match the diff snippet. + diff_snippet = re.match( + r".*\@\@ -\d+,\d+ \+\d+,\d+ \@\@(.*)", + diff_str, + flags=re.DOTALL, + ) + assert diff_snippet is not None, "Failed to match difference snippet." + + diff_line = next( + line for line in diff_snippet.group(1).splitlines() if line.startswith("+") + )[1:] + + print(f'Diff Line: "{diff_line}"') + + # Split contribution agreement into lines. + with open(CONTRIBUTING_FILE_PATH, "r", encoding="utf-8") as contributing: + lines = contributing.read().splitlines() + + # Assert diff line is in the list of contributors. + # NOTE: +2 because we don't want the header or the space after it. + # NOTE: +1 to convert 0 based indexing to 1. + try: + diff_line_index = 1 + lines.index( + diff_line, + lines.index(CONTRIBUTORS_HEADER) + 2, + ) + except ValueError as ex: + raise AssertionError("Diff line must be in list of contributors.") from ex + + return diff_line_index, diff_line + + +def get_email_address(diff_line_index: int, diff_line: str): + """Get the email address from the diff line. Assert that the email address + used to sign the commit is the same as the signed email address. + + Args: + diff_line_index: The one-based index of the differing line in the + contribution agreement. + diff_line: The differing line in the contribution agreement. + + Returns: + The new contributor's email address. + """ + + # Get and assert the signed email address format. + _, signed_email_address = parseaddr(diff_line) + assert ( + signed_email_address != "" + and re.match(r"[^@]+@[^@]+\.[^@]+", signed_email_address) is not None + ), "Invalid email address format." + + blame = subprocess.run( + [ + "git", + "blame", + "-L", + f"{diff_line_index},{diff_line_index}", + CONTRIBUTING_FILE_PATH, + "--show-email", + ], + check=True, + stdout=subprocess.PIPE, + ).stdout.decode("utf-8") + + print(f'Blame: "{blame}"') + + # Assert commit's author. + commit_author = re.match(rf".+ \(<(.+)> .+\) {re.escape(diff_line)}", blame) + assert commit_author is not None, "Failed to match commit author from git blame." + + commit_author_email_address = commit_author.group(1) + assert ( + signed_email_address.lower() == commit_author_email_address.lower() + ), "The signed email address must be equal to the commit author's." + + return signed_email_address + + +# TODO: create a codeforlife.ci submodule for all CI helpers. +def write_to_github_output(**outputs: str): + """Write to GitHub's output.""" + + with open(os.environ["GITHUB_OUTPUT"], "a", encoding="utf-8") as github_out: + github_out.write( + "\n".join([f"{key}={value}" for key, value in outputs.items()]) + ) + + +def main(): + """Runs the scripts.""" + + fetch_default_branch() + + diff_line_index, diff_line = get_diff_line() + + email_address = get_email_address(diff_line_index, diff_line) + + write_to_github_output(EMAIL_ADDRESS=email_address) + + +if __name__ == "__main__": + main() diff --git a/.github/scripts/python/view-pull-request/.venv/.gitkeep b/.github/scripts/python/view-pull-request/.venv/.gitkeep new file mode 100644 index 00000000..e69de29b diff --git a/.github/scripts/python/view-pull-request/Pipfile b/.github/scripts/python/view-pull-request/Pipfile new file mode 100644 index 00000000..c93ff348 --- /dev/null +++ b/.github/scripts/python/view-pull-request/Pipfile @@ -0,0 +1,15 @@ +[[source]] +url = "https://pypi.org/simple" +verify_ssl = true +name = "pypi" + +[packages] + +[dev-packages] +black = "==23.1.0" +pytest = "==7.2.1" +mypy = "==1.6.1" +pylint = "==3.0.2" + +[requires] +python_version = "3.11" diff --git a/.github/scripts/python/view-pull-request/Pipfile.lock b/.github/scripts/python/view-pull-request/Pipfile.lock new file mode 100644 index 00000000..d43d56b0 --- /dev/null +++ b/.github/scripts/python/view-pull-request/Pipfile.lock @@ -0,0 +1,213 @@ +{ + "_meta": { + "hash": { + "sha256": "be5bb9491279fa95591b4214435181afcd323d9ec48411dd6a25ccc240ee79c7" + }, + "pipfile-spec": 6, + "requires": { + "python_version": "3.11" + }, + "sources": [ + { + "name": "pypi", + "url": "https://pypi.org/simple", + "verify_ssl": true + } + ] + }, + "default": {}, + "develop": { + "astroid": { + "hashes": [ + "sha256:4a61cf0a59097c7bb52689b0fd63717cd2a8a14dc9f1eee97b82d814881c8c91", + "sha256:d6e62862355f60e716164082d6b4b041d38e2a8cf1c7cd953ded5108bac8ff5c" + ], + "markers": "python_full_version >= '3.8.0'", + "version": "==3.0.2" + }, + "attrs": { + "hashes": [ + "sha256:935dc3b529c262f6cf76e50877d35a4bd3c1de194fd41f47a2b7ae8f19971f30", + "sha256:99b87a485a5820b23b879f04c2305b44b951b502fd64be915879d77a7e8fc6f1" + ], + "markers": "python_version >= '3.7'", + "version": "==23.2.0" + }, + "black": { + "hashes": [ + "sha256:0052dba51dec07ed029ed61b18183942043e00008ec65d5028814afaab9a22fd", + "sha256:0680d4380db3719ebcfb2613f34e86c8e6d15ffeabcf8ec59355c5e7b85bb555", + "sha256:121ca7f10b4a01fd99951234abdbd97728e1240be89fde18480ffac16503d481", + "sha256:162e37d49e93bd6eb6f1afc3e17a3d23a823042530c37c3c42eeeaf026f38468", + "sha256:2a951cc83ab535d248c89f300eccbd625e80ab880fbcfb5ac8afb5f01a258ac9", + "sha256:2bf649fda611c8550ca9d7592b69f0637218c2369b7744694c5e4902873b2f3a", + "sha256:382998821f58e5c8238d3166c492139573325287820963d2f7de4d518bd76958", + "sha256:49f7b39e30f326a34b5c9a4213213a6b221d7ae9d58ec70df1c4a307cf2a1580", + "sha256:57c18c5165c1dbe291d5306e53fb3988122890e57bd9b3dcb75f967f13411a26", + "sha256:7a0f701d314cfa0896b9001df70a530eb2472babb76086344e688829efd97d32", + "sha256:8178318cb74f98bc571eef19068f6ab5613b3e59d4f47771582f04e175570ed8", + "sha256:8b70eb40a78dfac24842458476135f9b99ab952dd3f2dab738c1881a9b38b753", + "sha256:9880d7d419bb7e709b37e28deb5e68a49227713b623c72b2b931028ea65f619b", + "sha256:9afd3f493666a0cd8f8df9a0200c6359ac53940cbde049dcb1a7eb6ee2dd7074", + "sha256:a29650759a6a0944e7cca036674655c2f0f63806ddecc45ed40b7b8aa314b651", + "sha256:a436e7881d33acaf2536c46a454bb964a50eff59b21b51c6ccf5a40601fbef24", + "sha256:a59db0a2094d2259c554676403fa2fac3473ccf1354c1c63eccf7ae65aac8ab6", + "sha256:a8471939da5e824b891b25751955be52ee7f8a30a916d570a5ba8e0f2eb2ecad", + "sha256:b0bd97bea8903f5a2ba7219257a44e3f1f9d00073d6cc1add68f0beec69692ac", + "sha256:b6a92a41ee34b883b359998f0c8e6eb8e99803aa8bf3123bf2b2e6fec505a221", + "sha256:bb460c8561c8c1bec7824ecbc3ce085eb50005883a6203dcfb0122e95797ee06", + "sha256:bfffba28dc52a58f04492181392ee380e95262af14ee01d4bc7bb1b1c6ca8d27", + "sha256:c1c476bc7b7d021321e7d93dc2cbd78ce103b84d5a4cf97ed535fbc0d6660648", + "sha256:c91dfc2c2a4e50df0026f88d2215e166616e0c80e86004d0003ece0488db2739", + "sha256:e6663f91b6feca5d06f2ccd49a10f254f9298cc1f7f49c46e498a0771b507104" + ], + "index": "pypi", + "version": "==23.1.0" + }, + "click": { + "hashes": [ + "sha256:ae74fb96c20a0277a1d615f1e4d73c8414f5a98db8b799a7931d1582f3390c28", + "sha256:ca9853ad459e787e2192211578cc907e7594e294c7ccc834310722b41b9ca6de" + ], + "markers": "python_version >= '3.7'", + "version": "==8.1.7" + }, + "dill": { + "hashes": [ + "sha256:76b122c08ef4ce2eedcd4d1abd8e641114bfc6c2867f49f3c41facf65bf19f5e", + "sha256:cc1c8b182eb3013e24bd475ff2e9295af86c1a38eb1aff128dac8962a9ce3c03" + ], + "markers": "python_version >= '3.11'", + "version": "==0.3.7" + }, + "iniconfig": { + "hashes": [ + "sha256:2d91e135bf72d31a410b17c16da610a82cb55f6b0477d1a902134b24a455b8b3", + "sha256:b6a85871a79d2e3b22d2d1b94ac2824226a63c6b741c88f7ae975f18b6778374" + ], + "markers": "python_version >= '3.7'", + "version": "==2.0.0" + }, + "isort": { + "hashes": [ + "sha256:48fdfcb9face5d58a4f6dde2e72a1fb8dcaf8ab26f95ab49fab84c2ddefb0109", + "sha256:8ca5e72a8d85860d5a3fa69b8745237f2939afe12dbf656afbcb47fe72d947a6" + ], + "markers": "python_full_version >= '3.8.0'", + "version": "==5.13.2" + }, + "mccabe": { + "hashes": [ + "sha256:348e0240c33b60bbdf4e523192ef919f28cb2c3d7d5c7794f74009290f236325", + "sha256:6c2d30ab6be0e4a46919781807b4f0d834ebdd6c6e3dca0bda5a15f863427b6e" + ], + "markers": "python_version >= '3.6'", + "version": "==0.7.0" + }, + "mypy": { + "hashes": [ + "sha256:19f905bcfd9e167159b3d63ecd8cb5e696151c3e59a1742e79bc3bcb540c42c7", + "sha256:21a1ad938fee7d2d96ca666c77b7c494c3c5bd88dff792220e1afbebb2925b5e", + "sha256:40b1844d2e8b232ed92e50a4bd11c48d2daa351f9deee6c194b83bf03e418b0c", + "sha256:41697773aa0bf53ff917aa077e2cde7aa50254f28750f9b88884acea38a16169", + "sha256:49ae115da099dcc0922a7a895c1eec82c1518109ea5c162ed50e3b3594c71208", + "sha256:4c46b51de523817a0045b150ed11b56f9fff55f12b9edd0f3ed35b15a2809de0", + "sha256:4cbe68ef919c28ea561165206a2dcb68591c50f3bcf777932323bc208d949cf1", + "sha256:4d01c00d09a0be62a4ca3f933e315455bde83f37f892ba4b08ce92f3cf44bcc1", + "sha256:59a0d7d24dfb26729e0a068639a6ce3500e31d6655df8557156c51c1cb874ce7", + "sha256:68351911e85145f582b5aa6cd9ad666c8958bcae897a1bfda8f4940472463c45", + "sha256:7274b0c57737bd3476d2229c6389b2ec9eefeb090bbaf77777e9d6b1b5a9d143", + "sha256:81af8adaa5e3099469e7623436881eff6b3b06db5ef75e6f5b6d4871263547e5", + "sha256:82e469518d3e9a321912955cc702d418773a2fd1e91c651280a1bda10622f02f", + "sha256:8b27958f8c76bed8edaa63da0739d76e4e9ad4ed325c814f9b3851425582a3cd", + "sha256:8c223fa57cb154c7eab5156856c231c3f5eace1e0bed9b32a24696b7ba3c3245", + "sha256:8f57e6b6927a49550da3d122f0cb983d400f843a8a82e65b3b380d3d7259468f", + "sha256:925cd6a3b7b55dfba252b7c4561892311c5358c6b5a601847015a1ad4eb7d332", + "sha256:a43ef1c8ddfdb9575691720b6352761f3f53d85f1b57d7745701041053deff30", + "sha256:a8032e00ce71c3ceb93eeba63963b864bf635a18f6c0c12da6c13c450eedb183", + "sha256:b96ae2c1279d1065413965c607712006205a9ac541895004a1e0d4f281f2ff9f", + "sha256:bb8ccb4724f7d8601938571bf3f24da0da791fe2db7be3d9e79849cb64e0ae85", + "sha256:bbaf4662e498c8c2e352da5f5bca5ab29d378895fa2d980630656178bd607c46", + "sha256:cfd13d47b29ed3bbaafaff7d8b21e90d827631afda134836962011acb5904b71", + "sha256:d4473c22cc296425bbbce7e9429588e76e05bc7342da359d6520b6427bf76660", + "sha256:d8fbb68711905f8912e5af474ca8b78d077447d8f3918997fecbf26943ff3cbb", + "sha256:e5012e5cc2ac628177eaac0e83d622b2dd499e28253d4107a08ecc59ede3fc2c", + "sha256:eb4f18589d196a4cbe5290b435d135dee96567e07c2b2d43b5c4621b6501531a" + ], + "index": "pypi", + "version": "==1.6.1" + }, + "mypy-extensions": { + "hashes": [ + "sha256:4392f6c0eb8a5668a69e23d168ffa70f0be9ccfd32b5cc2d26a34ae5b844552d", + "sha256:75dbf8955dc00442a438fc4d0666508a9a97b6bd41aa2f0ffe9d2f2725af0782" + ], + "markers": "python_version >= '3.5'", + "version": "==1.0.0" + }, + "packaging": { + "hashes": [ + "sha256:048fb0e9405036518eaaf48a55953c750c11e1a1b68e0dd1a9d62ed0c092cfc5", + "sha256:8c491190033a9af7e1d931d0b5dacc2ef47509b34dd0de67ed209b5203fc88c7" + ], + "markers": "python_version >= '3.7'", + "version": "==23.2" + }, + "pathspec": { + "hashes": [ + "sha256:a0d503e138a4c123b27490a4f7beda6a01c6f288df0e4a8b79c7eb0dc7b4cc08", + "sha256:a482d51503a1ab33b1c67a6c3813a26953dbdc71c31dacaef9a838c4e29f5712" + ], + "markers": "python_version >= '3.8'", + "version": "==0.12.1" + }, + "platformdirs": { + "hashes": [ + "sha256:11c8f37bcca40db96d8144522d925583bdb7a31f7b0e37e3ed4318400a8e2380", + "sha256:906d548203468492d432bcb294d4bc2fff751bf84971fbb2c10918cc206ee420" + ], + "markers": "python_version >= '3.8'", + "version": "==4.1.0" + }, + "pluggy": { + "hashes": [ + "sha256:cf61ae8f126ac6f7c451172cf30e3e43d3ca77615509771b3a984a0730651e12", + "sha256:d89c696a773f8bd377d18e5ecda92b7a3793cbe66c87060a6fb58c7b6e1061f7" + ], + "markers": "python_version >= '3.8'", + "version": "==1.3.0" + }, + "pylint": { + "hashes": [ + "sha256:0d4c286ef6d2f66c8bfb527a7f8a629009e42c99707dec821a03e1b51a4c1496", + "sha256:60ed5f3a9ff8b61839ff0348b3624ceeb9e6c2a92c514d81c9cc273da3b6bcda" + ], + "index": "pypi", + "version": "==3.0.2" + }, + "pytest": { + "hashes": [ + "sha256:c7c6ca206e93355074ae32f7403e8ea12163b1163c976fee7d4d84027c162be5", + "sha256:d45e0952f3727241918b8fd0f376f5ff6b301cc0777c6f9a556935c92d8a7d42" + ], + "index": "pypi", + "version": "==7.2.1" + }, + "tomlkit": { + "hashes": [ + "sha256:75baf5012d06501f07bee5bf8e801b9f343e7aac5a92581f20f80ce632e6b5a4", + "sha256:b0a645a9156dc7cb5d3a1f0d4bab66db287fcb8e0430bdd4664a095ea16414ba" + ], + "markers": "python_version >= '3.7'", + "version": "==0.12.3" + }, + "typing-extensions": { + "hashes": [ + "sha256:23478f88c37f27d76ac8aee6c905017a143b0b1b886c3c9f66bc2fd94f9f5783", + "sha256:af72aea155e91adfc61c3ae9e0e342dbc0cba726d6cba4b6c72c1f34e47291cd" + ], + "markers": "python_version >= '3.8'", + "version": "==4.9.0" + } + } +} diff --git a/.github/scripts/python/view-pull-request/__main__.py b/.github/scripts/python/view-pull-request/__main__.py new file mode 100644 index 00000000..65ed7e60 --- /dev/null +++ b/.github/scripts/python/view-pull-request/__main__.py @@ -0,0 +1,125 @@ +""" +ยฉ Ocado Group +Created on 05/01/2024 at 14:38:19(+00:00). + +View a GitHub pull request. Optionally, you can also validate a pull request's +state. +""" + +import json +import os +import subprocess +import typing as t + +PullRequest = t.Dict[str, t.Any] + + +def get_inputs(): + """Get the script's inputs. + + Returns: + A tuple with the values (the PR's number, the PR's fields to output to + github, a flag indicating whether or not to validate the PR's latest + review state). + """ + + number = int(os.environ["NUMBER"]) + + outputs = os.getenv("OUTPUTS", "").split(",") + outputs = [output.lower() for output in outputs if output != ""] + + review_state = os.getenv("REVIEW_STATE") + if review_state == "": + review_state = None + + return number, outputs, review_state + + +def get_pull_request(number: int, fields: t.List[str]) -> PullRequest: + """Gets the pull request object with the specified fields. + + Args: + number: The number of the PR. + fields: A the PR's fields to retrieve. + + Returns: + The pull request as a JSON object. + """ + + pull_request = subprocess.run( + [ + "gh", + "pr", + "view", + str(number), + "--json", + ",".join(fields), + ], + check=True, + stdout=subprocess.PIPE, + ).stdout.decode("utf-8") + + print(pull_request) + + return json.loads(pull_request) + + +def validate_reviews(pull_request: PullRequest, state: t.Optional[str] = None): + """Validate the PR's reviews. + + Args: + pull_request: The pull request. + state: The expected latest state. + """ + + # If all of the review fields is None, there's nothing to validate. + if all(field is None for field in [state]): + return + + reviews: t.List[t.Dict[str, t.Any]] = pull_request["reviews"] + reviews.sort(key=lambda review: review["submittedAt"]) + + if state is not None: + assert reviews, "The pull request has not been reviewed." + assert reviews[-1]["state"] == state, ( + "The latest review is not in the expected state." + f' Latest: "{reviews[-1]["state"]}". Expected: "{state}".' + ) + + +# TODO: create a codeforlife.ci submodule for all CI helpers. +def write_to_github_output(**outputs: str): + """Write to GitHub's output.""" + + with open(os.environ["GITHUB_OUTPUT"], "a", encoding="utf-8") as github_out: + github_out.write( + "\n".join([f"{key}={value}" for key, value in outputs.items()]) + ) + + +def main(): + """Run the script.""" + + number, outputs, review_state = get_inputs() + + pull_request_fields = outputs.copy() + + # If we're validating a field but not outputting it, we still need to get + # the field to validate it. + if review_state is not None and "reviews" not in outputs: + pull_request_fields.append("reviews") + + pull_request = get_pull_request(number, pull_request_fields) + + validate_reviews(pull_request, review_state) + + write_to_github_output( + **{ + output.replace("-", "_").upper(): str(pull_request[output]) + for output in outputs + } + ) + + +if __name__ == "__main__": + main() diff --git a/.github/workflows/test-python-code.yaml b/.github/workflows/test-python-code.yaml new file mode 100644 index 00000000..3da2ba40 --- /dev/null +++ b/.github/workflows/test-python-code.yaml @@ -0,0 +1,68 @@ +name: Test Python Code + +on: + workflow_call: + inputs: + python-version: + description: "The python version to set up." + type: number + required: true + default: 3.8 + working-directory: + description: "The current working directory." + type: string + required: true + default: "." + source-path: + description: "The path of the source files." + type: string + required: true + default: "." + check-django-migrations: + description: "Check if there are pending Django migrations." + type: boolean + required: true + default: true + pyproject-toml: + description: "The path to the pyproject.toml file." + type: string + required: true + default: "pyproject.toml" + +jobs: + test-py-code: + runs-on: ubuntu-latest + steps: + - name: ๐Ÿ Set up Python ${{ inputs.python-version }} Environment + uses: ocadotechnology/codeforlife-workspace/.github/actions/python/setup-environment@main + with: + python-version: ${{ inputs.python-version }} + working-directory: ${{ inputs.working-directory }} + install-args: --dev + + - name: ๐Ÿ”Ž Check Import Sort + working-directory: ${{ inputs.working-directory }} + run: pipenv run isort --settings-file=${{ inputs.pyproject-toml }} --check ${{ inputs.source-path }} + + - name: ๐Ÿ”Ž Check Code Format + working-directory: ${{ inputs.working-directory }} + run: if ! pipenv run black --config=${{ inputs.pyproject-toml }} --check ${{ inputs.source-path }}; then exit 1; fi + + - name: ๐Ÿ”Ž Check Static Type Hints + working-directory: ${{ inputs.working-directory }} + run: pipenv run mypy --config-file=${{ inputs.pyproject-toml }} ${{ inputs.source-path }} + + - name: ๐Ÿ”Ž Check Static Code + working-directory: ${{ inputs.working-directory }} + run: pipenv run pylint --rcfile=${{ inputs.pyproject-toml }} ${{ inputs.source-path }} + + - name: ๐Ÿ”Ž Check Django Migrations + if: inputs.check-django-migrations + working-directory: ${{ inputs.working-directory }} + run: pipenv run python manage.py makemigrations --check --dry-run + + - name: ๐Ÿงช Test Code Units + working-directory: ${{ inputs.working-directory }} + run: pipenv run pytest -n=auto -c=${{ inputs.pyproject-toml }} ${{ inputs.source-path }} + + # TODO: assert code coverage target. diff --git a/.github/workflows/validate-existing-contributors.yaml b/.github/workflows/validate-existing-contributors.yaml new file mode 100644 index 00000000..b0124165 --- /dev/null +++ b/.github/workflows/validate-existing-contributors.yaml @@ -0,0 +1,44 @@ +name: Validate Existing Contributors + +on: + pull_request: + paths-ignore: + - 'CONTRIBUTING.md' + workflow_call: + +env: + PYTHON_VERSION: 3.11 + WORKING_DIR: codeforlife-workspace/.github/scripts/python/validate-existing-contributors + +jobs: + validate-existing-contributors: + runs-on: ubuntu-latest + steps: + - name: ๐Ÿ›ซ Checkout Pull Request + uses: actions/checkout@v4 + + - name: ๐Ÿ”Ž View Pull Request's Commits + id: view-pr + run: echo "PULL_REQUEST=$(gh pr view ${{ github.event.pull_request.number }} --json commits)" >> $GITHUB_OUTPUT + env: + GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} + + - name: ๐Ÿ›ซ Checkout Workspace + uses: actions/checkout@v4 + with: + repository: ocadotechnology/codeforlife-workspace + ref: main + path: codeforlife-workspace + + - name: ๐Ÿ Set up Python ${{ env.PYTHON_VERSION }} Environment + uses: ocadotechnology/codeforlife-workspace/.github/actions/python/setup-environment@main + with: + checkout: 'false' + python-version: ${{ env.PYTHON_VERSION }} + working-directory: ${{ env.WORKING_DIR }} + + - name: ๐Ÿ•ต๏ธ Validate Existing Contributors + working-directory: ${{ env.WORKING_DIR }} + run: pipenv run python . + env: + PULL_REQUEST: ${{ steps.view-pr.outputs.PULL_REQUEST }} diff --git a/.github/workflows/validate-new-contributor.yaml b/.github/workflows/validate-new-contributor.yaml new file mode 100644 index 00000000..627d648a --- /dev/null +++ b/.github/workflows/validate-new-contributor.yaml @@ -0,0 +1,52 @@ +name: Validate New Contributor + +on: + pull_request: + branches: + - main + paths: + - 'CONTRIBUTING.md' + workflow_call: + inputs: + pull-request-number: + required: true + type: string # NOTE: github has a bug with type: number + outputs: + email-address: + description: "The new contributor's email address." + value: ${{ jobs.validate-new-contributor.outputs.email-address }} + +env: + PYTHON_VERSION: 3.11 + WORKING_DIR: .github/scripts/python/validate-new-contributor + +jobs: + validate-new-contributor: + runs-on: ubuntu-latest + outputs: + email-address: ${{ steps.validate-new-contributor.outputs.EMAIL_ADDRESS }} + steps: + - name: โš™๏ธ Set Pull Request Number + run: | + if [ "${{ github.event_name }}" == "pull_request" ] + then + echo "PR_NUM=${{ github.event.pull_request.number }}" >> $GITHUB_ENV + else + echo "PR_NUM=${{ inputs.pull-request-number }}" >> $GITHUB_ENV + fi + + - name: ๐Ÿ›ซ Checkout + uses: actions/checkout@v4 + with: + ref: refs/pull/${{ env.PR_NUM }}/head + + - uses: ocadotechnology/codeforlife-workspace/.github/actions/python/setup-environment@main + with: + checkout: 'false' + python-version: ${{ env.PYTHON_VERSION }} + working-directory: ${{ env.WORKING_DIR }} + + - name: ๐Ÿ•ต๏ธ Validate New Contributor + id: validate-new-contributor + working-directory: ${{ env.WORKING_DIR }} + run: pipenv run python . diff --git a/.github/workflows/validate-pull-request-refs.yaml b/.github/workflows/validate-pull-request-refs.yaml new file mode 100644 index 00000000..eba8077a --- /dev/null +++ b/.github/workflows/validate-pull-request-refs.yaml @@ -0,0 +1,20 @@ +name: Validate Pull Request Refs + +on: + workflow_call: + +jobs: + validate-pr-refs: + runs-on: ubuntu-latest + env: + PROD_REF: production + STAGING_REF: staging + DEV_REF: development + steps: + - name: Merge into "${{ env.STAGING_REF }}" from "${{ env.DEV_REF }}" + if: github.event.pull_request.base.ref == env.STAGING_REF + run: if [ ${{ github.event.pull_request.head.ref }} != ${{ env.DEV_REF }} ]; then exit 1; fi + + - name: Merge into "${{ env.PROD_REF }}" from "${{ env.STAGING_REF }}" + if: github.event.pull_request.base.ref == env.PROD_REF + run: if [ ${{ github.event.pull_request.head.ref }} != ${{ env.STAGING_REF }} ]; then exit 1; fi diff --git a/.github/workflows/verify-new-contributor.yaml b/.github/workflows/verify-new-contributor.yaml new file mode 100644 index 00000000..77db40b6 --- /dev/null +++ b/.github/workflows/verify-new-contributor.yaml @@ -0,0 +1,37 @@ +name: Verify New Contributor + +on: + workflow_dispatch: + inputs: + pull-request-number: + description: "The number of the new contributor's PR." + required: true + type: string # NOTE: github has a bug with type: number + +jobs: + view-pr: + uses: ocadotechnology/codeforlife-workspace/.github/workflows/view-pull-request.yaml@main + secrets: + GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} + with: + number: ${{ inputs.pull-request-number }} + outputs: url + review-state: APPROVED + + validate-new-contributor: + needs: [view-pr] + uses: ocadotechnology/codeforlife-workspace/.github/workflows/validate-new-contributor.yaml@main + with: + pull-request-number: ${{ inputs.pull-request-number }} + + verify-new-contributor: + needs: [view-pr, validate-new-contributor] + runs-on: ubuntu-latest + steps: + - name: ๐Ÿ“ง Send Verification Email + uses: ocadotechnology/codeforlife-workspace/.github/actions/email/send@main + with: + auth: ${{ secrets.DOTDIGITAL_API_USER_AUTH }} + to-addresses: '["${{ needs.validate-new-contributor.outputs.email-address }}"]' + campaign-id: 1506387 + personalization-values: '[{"name": "PR_URL", "value": "${{ needs.view-pr.outputs.url }}"}]' diff --git a/.github/workflows/view-pull-request.yaml b/.github/workflows/view-pull-request.yaml new file mode 100644 index 00000000..21ffb3f9 --- /dev/null +++ b/.github/workflows/view-pull-request.yaml @@ -0,0 +1,46 @@ +name: View Pull Request + +on: + workflow_call: + inputs: + number: + required: true + type: string # NOTE: github has a bug with type: number + outputs: + required: false + type: string + review-state: + required: false + type: string + outputs: + url: + value: ${{ jobs.view-pr.outputs.url }} + secrets: + GH_TOKEN: + required: true + +env: + PYTHON_VERSION: 3.11 + WORKING_DIR: .github/scripts/python/view-pull-request + +jobs: + view-pr: + runs-on: ubuntu-latest + outputs: + url: ${{ steps.view-pr.outputs.URL }} + steps: + - uses: ocadotechnology/codeforlife-workspace/.github/actions/python/setup-environment@main + with: + python-version: ${{ env.PYTHON_VERSION }} + working-directory: ${{ env.WORKING_DIR }} + + - name: ๐Ÿ•ต๏ธ View Pull Request + id: view-pr + shell: bash + working-directory: ${{ env.WORKING_DIR }} + run: pipenv run python . + env: + GH_TOKEN: ${{ secrets.GH_TOKEN }} + NUMBER: ${{ inputs.number }} + OUTPUTS: ${{ inputs.outputs }} + REVIEW_STATE: ${{ inputs.review-state }} diff --git a/.gitignore b/.gitignore new file mode 100644 index 00000000..ff08be71 --- /dev/null +++ b/.gitignore @@ -0,0 +1,169 @@ +# Official Python.gitignore from GitHub. +# https://github.com/github/gitignore/blob/main/Python.gitignore + +# 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 + +# Any graphs generated in the docs +docs/*.png + +# Any dot files generated by pydot +*.dot + +# Unit test / coverage reports +htmlcov/ +.tox/ +.nox/ +.coverage +.coverage.* +.cache +nosetests.xml +coverage.xml +*.cover +*.py,cover +.hypothesis/ +.pytest_cache/ +cover/ + +# 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 + +# pdm +# Similar to Pipfile.lock, it is generally recommended to include pdm.lock in version control. +#pdm.lock +# pdm stores project-wide configurations in .pdm.toml, but it is recommended to not include it +# in version control. +# https://pdm.fming.dev/#use-with-ide +.pdm.toml + +# PEP 582; used by e.g. github.com/David-OConnor/pyflow and github.com/pdm-project/pdm +__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 maintained 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/ diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 5b1b8c90..0592a332 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -172,8 +172,7 @@ To sign, you must: same email address stating: ```txt - I confirm that I made this commit and I agree to being a contributor to the - Code for Life project under the terms found in my commit. + I confirm that I made this commit and I agree to being a contributor to the Code for Life project under the terms found in my commit. ``` Your email address is now approved to make contributions! ๐Ÿฅณ @@ -183,6 +182,7 @@ If you have any trouble with the above process, please reach out to ### ๐Ÿ‘จโ€๐Ÿ’ป Contributors ๐Ÿ‘ฉโ€๐Ÿ’ป +