diff --git a/.github/workflows/python-package.yml b/.github/workflows/python-package.yml new file mode 100644 index 0000000..56127c7 --- /dev/null +++ b/.github/workflows/python-package.yml @@ -0,0 +1,28 @@ +name: CI + +on: + pull_request: + branches: [pipfile-experiment] + push: + branches: [pipfile-experiment] + +jobs: + test: + runs-on: ubuntu-latest + strategy: + matrix: + python-version: ["3.10", "3.11"] + steps: + - uses: actions/checkout@v4 + - name: Set up Python + uses: actions/setup-python@v4 + with: + python-version: ${{ matrix.python-version }} + - name: Install pipenv + run: python -m pip install --upgrade pip pipenv + - name: Install dev dependencies + run: pipenv --python $(which python) install --dev --deploy + - name: Run tests + run: pipenv run pytest -q + - name: Build package + run: pipenv run python -m build diff --git a/.gitignore b/.gitignore index 2f24a10..d653424 100644 --- a/.gitignore +++ b/.gitignore @@ -5,6 +5,9 @@ .vscode/ .vscode/settings.json +# Pycharm junk +.idea/ + # emacs backups *~ diff --git a/Pipfile b/Pipfile new file mode 100644 index 0000000..f3fbc5c --- /dev/null +++ b/Pipfile @@ -0,0 +1,18 @@ +[[source]] +name = "pypi" +url = "https://pypi.org/simple" +verify_ssl = true + +[dev-packages] +pytest = "*" +build = "*" +twine = "*" +tomli = "*" +typing_extensions = "*" +exceptiongroup = "*" + +[packages] +twine = "*" + +[requires] +python_version = "3.10" diff --git a/Pipfile.lock b/Pipfile.lock new file mode 100644 index 0000000..9a75c9f --- /dev/null +++ b/Pipfile.lock @@ -0,0 +1,779 @@ +{ + "_meta": { + "hash": { + "sha256": "e040e6eb1cbfc08ed0d7ccffc65bca1d9f83837a5b4ac8f701515d7a5ed65bf3" + }, + "pipfile-spec": 6, + "requires": { + "python_version": "3.10" + }, + "sources": [ + { + "name": "pypi", + "url": "https://pypi.org/simple", + "verify_ssl": true + } + ] + }, + "default": { + "certifi": { + "hashes": [ + "sha256:0f212c2744a9bb6de0c56639a6f68afe01ecd92d91f14ae897c4fe7bbeeef0de", + "sha256:47c09d31ccf2acf0be3f701ea53595ee7e0b8fa08801c6624be771df09ae7b43" + ], + "markers": "python_version >= '3.7'", + "version": "==2025.10.5" + }, + "charset-normalizer": { + "hashes": [ + "sha256:027f6de494925c0ab2a55eab46ae5129951638a49a34d87f4c3eda90f696b4ad", + "sha256:077fbb858e903c73f6c9db43374fd213b0b6a778106bc7032446a8e8b5b38b93", + "sha256:0a98e6759f854bd25a58a73fa88833fba3b7c491169f86ce1180c948ab3fd394", + "sha256:0d3d8f15c07f86e9ff82319b3d9ef6f4bf907608f53fe9d92b28ea9ae3d1fd89", + "sha256:0f04b14ffe5fdc8c4933862d8306109a2c51e0704acfa35d51598eb45a1e89fc", + "sha256:11d694519d7f29d6cd09f6ac70028dba10f92f6cdd059096db198c283794ac86", + "sha256:194f08cbb32dc406d6e1aea671a68be0823673db2832b38405deba2fb0d88f63", + "sha256:1bee1e43c28aa63cb16e5c14e582580546b08e535299b8b6158a7c9c768a1f3d", + "sha256:21d142cc6c0ec30d2efee5068ca36c128a30b0f2c53c1c07bd78cb6bc1d3be5f", + "sha256:2437418e20515acec67d86e12bf70056a33abdacb5cb1655042f6538d6b085a8", + "sha256:244bfb999c71b35de57821b8ea746b24e863398194a4014e4c76adc2bbdfeff0", + "sha256:2677acec1a2f8ef614c6888b5b4ae4060cc184174a938ed4e8ef690e15d3e505", + "sha256:277e970e750505ed74c832b4bf75dac7476262ee2a013f5574dd49075879e161", + "sha256:2aaba3b0819274cc41757a1da876f810a3e4d7b6eb25699253a4effef9e8e4af", + "sha256:2b7d8f6c26245217bd2ad053761201e9f9680f8ce52f0fcd8d0755aeae5b2152", + "sha256:2c9d3c380143a1fedbff95a312aa798578371eb29da42106a29019368a475318", + "sha256:3162d5d8ce1bb98dd51af660f2121c55d0fa541b46dff7bb9b9f86ea1d87de72", + "sha256:31fd66405eaf47bb62e8cd575dc621c56c668f27d46a61d975a249930dd5e2a4", + "sha256:362d61fd13843997c1c446760ef36f240cf81d3ebf74ac62652aebaf7838561e", + "sha256:376bec83a63b8021bb5c8ea75e21c4ccb86e7e45ca4eb81146091b56599b80c3", + "sha256:44c2a8734b333e0578090c4cd6b16f275e07aa6614ca8715e6c038e865e70576", + "sha256:47cc91b2f4dd2833fddaedd2893006b0106129d4b94fdb6af1f4ce5a9965577c", + "sha256:4902828217069c3c5c71094537a8e623f5d097858ac6ca8252f7b4d10b7560f1", + "sha256:4bd5d4137d500351a30687c2d3971758aac9a19208fc110ccb9d7188fbe709e8", + "sha256:4fe7859a4e3e8457458e2ff592f15ccb02f3da787fcd31e0183879c3ad4692a1", + "sha256:542d2cee80be6f80247095cc36c418f7bddd14f4a6de45af91dfad36d817bba2", + "sha256:554af85e960429cf30784dd47447d5125aaa3b99a6f0683589dbd27e2f45da44", + "sha256:5833d2c39d8896e4e19b689ffc198f08ea58116bee26dea51e362ecc7cd3ed26", + "sha256:5947809c8a2417be3267efc979c47d76a079758166f7d43ef5ae8e9f92751f88", + "sha256:5ae497466c7901d54b639cf42d5b8c1b6a4fead55215500d2f486d34db48d016", + "sha256:5bd2293095d766545ec1a8f612559f6b40abc0eb18bb2f5d1171872d34036ede", + "sha256:5bfbb1b9acf3334612667b61bd3002196fe2a1eb4dd74d247e0f2a4d50ec9bbf", + "sha256:5cb4d72eea50c8868f5288b7f7f33ed276118325c1dfd3957089f6b519e1382a", + "sha256:5dbe56a36425d26d6cfb40ce79c314a2e4dd6211d51d6d2191c00bed34f354cc", + "sha256:5f819d5fe9234f9f82d75bdfa9aef3a3d72c4d24a6e57aeaebba32a704553aa0", + "sha256:64b55f9dce520635f018f907ff1b0df1fdc31f2795a922fb49dd14fbcdf48c84", + "sha256:6515f3182dbe4ea06ced2d9e8666d97b46ef4c75e326b79bb624110f122551db", + "sha256:65e2befcd84bc6f37095f5961e68a6f077bf44946771354a28ad434c2cce0ae1", + "sha256:6aee717dcfead04c6eb1ce3bd29ac1e22663cdea57f943c87d1eab9a025438d7", + "sha256:6b39f987ae8ccdf0d2642338faf2abb1862340facc796048b604ef14919e55ed", + "sha256:6e1fcf0720908f200cd21aa4e6750a48ff6ce4afe7ff5a79a90d5ed8a08296f8", + "sha256:74018750915ee7ad843a774364e13a3db91682f26142baddf775342c3f5b1133", + "sha256:74664978bb272435107de04e36db5a9735e78232b85b77d45cfb38f758efd33e", + "sha256:74bb723680f9f7a6234dcf67aea57e708ec1fbdf5699fb91dfd6f511b0a320ef", + "sha256:752944c7ffbfdd10c074dc58ec2d5a8a4cd9493b314d367c14d24c17684ddd14", + "sha256:778d2e08eda00f4256d7f672ca9fef386071c9202f5e4607920b86d7803387f2", + "sha256:780236ac706e66881f3b7f2f32dfe90507a09e67d1d454c762cf642e6e1586e0", + "sha256:798d75d81754988d2565bff1b97ba5a44411867c0cf32b77a7e8f8d84796b10d", + "sha256:799a7a5e4fb2d5898c60b640fd4981d6a25f1c11790935a44ce38c54e985f828", + "sha256:7a32c560861a02ff789ad905a2fe94e3f840803362c84fecf1851cb4cf3dc37f", + "sha256:7c308f7e26e4363d79df40ca5b2be1c6ba9f02bdbccfed5abddb7859a6ce72cf", + "sha256:7fa17817dc5625de8a027cb8b26d9fefa3ea28c8253929b8d6649e705d2835b6", + "sha256:81d5eb2a312700f4ecaa977a8235b634ce853200e828fbadf3a9c50bab278328", + "sha256:82004af6c302b5d3ab2cfc4cc5f29db16123b1a8417f2e25f9066f91d4411090", + "sha256:837c2ce8c5a65a2035be9b3569c684358dfbf109fd3b6969630a87535495ceaa", + "sha256:840c25fb618a231545cbab0564a799f101b63b9901f2569faecd6b222ac72381", + "sha256:8a6562c3700cce886c5be75ade4a5db4214fda19fede41d9792d100288d8f94c", + "sha256:8af65f14dc14a79b924524b1e7fffe304517b2bff5a58bf64f30b98bbc5079eb", + "sha256:8ef3c867360f88ac904fd3f5e1f902f13307af9052646963ee08ff4f131adafc", + "sha256:94537985111c35f28720e43603b8e7b43a6ecfb2ce1d3058bbe955b73404e21a", + "sha256:99ae2cffebb06e6c22bdc25801d7b30f503cc87dbd283479e7b606f70aff57ec", + "sha256:9a26f18905b8dd5d685d6d07b0cdf98a79f3c7a918906af7cc143ea2e164c8bc", + "sha256:9b35f4c90079ff2e2edc5b26c0c77925e5d2d255c42c74fdb70fb49b172726ac", + "sha256:9cd98cdc06614a2f768d2b7286d66805f94c48cde050acdbbb7db2600ab3197e", + "sha256:9d1bb833febdff5c8927f922386db610b49db6e0d4f4ee29601d71e7c2694313", + "sha256:9f7fcd74d410a36883701fafa2482a6af2ff5ba96b9a620e9e0721e28ead5569", + "sha256:a59cb51917aa591b1c4e6a43c132f0cdc3c76dbad6155df4e28ee626cc77a0a3", + "sha256:a61900df84c667873b292c3de315a786dd8dac506704dea57bc957bd31e22c7d", + "sha256:a79cfe37875f822425b89a82333404539ae63dbdddf97f84dcbc3d339aae9525", + "sha256:a8a8b89589086a25749f471e6a900d3f662d1d3b6e2e59dcecf787b1cc3a1894", + "sha256:a8bf8d0f749c5757af2142fe7903a9df1d2e8aa3841559b2bad34b08d0e2bcf3", + "sha256:a9768c477b9d7bd54bc0c86dbaebdec6f03306675526c9927c0e8a04e8f94af9", + "sha256:ac1c4a689edcc530fc9d9aa11f5774b9e2f33f9a0c6a57864e90908f5208d30a", + "sha256:af2d8c67d8e573d6de5bc30cdb27e9b95e49115cd9baad5ddbd1a6207aaa82a9", + "sha256:b435cba5f4f750aa6c0a0d92c541fb79f69a387c91e61f1795227e4ed9cece14", + "sha256:b5b290ccc2a263e8d185130284f8501e3e36c5e02750fc6b6bdeb2e9e96f1e25", + "sha256:b5d84d37db046c5ca74ee7bb47dd6cbc13f80665fdde3e8040bdd3fb015ecb50", + "sha256:b7cf1017d601aa35e6bb650b6ad28652c9cd78ee6caff19f3c28d03e1c80acbf", + "sha256:bc7637e2f80d8530ee4a78e878bce464f70087ce73cf7c1caf142416923b98f1", + "sha256:c0463276121fdee9c49b98908b3a89c39be45d86d1dbaa22957e38f6321d4ce3", + "sha256:c4ef880e27901b6cc782f1b95f82da9313c0eb95c3af699103088fa0ac3ce9ac", + "sha256:c8ae8a0f02f57a6e61203a31428fa1d677cbe50c93622b4149d5c0f319c1d19e", + "sha256:ca5862d5b3928c4940729dacc329aa9102900382fea192fc5e52eb69d6093815", + "sha256:cb01158d8b88ee68f15949894ccc6712278243d95f344770fa7593fa2d94410c", + "sha256:cb6254dc36b47a990e59e1068afacdcd02958bdcce30bb50cc1700a8b9d624a6", + "sha256:cc00f04ed596e9dc0da42ed17ac5e596c6ccba999ba6bd92b0e0aef2f170f2d6", + "sha256:cd09d08005f958f370f539f186d10aec3377d55b9eeb0d796025d4886119d76e", + "sha256:cd4b7ca9984e5e7985c12bc60a6f173f3c958eae74f3ef6624bb6b26e2abbae4", + "sha256:ce8a0633f41a967713a59c4139d29110c07e826d131a316b50ce11b1d79b4f84", + "sha256:cead0978fc57397645f12578bfd2d5ea9138ea0fac82b2f63f7f7c6877986a69", + "sha256:d055ec1e26e441f6187acf818b73564e6e6282709e9bcb5b63f5b23068356a15", + "sha256:d1f13550535ad8cff21b8d757a3257963e951d96e20ec82ab44bc64aeb62a191", + "sha256:d9c7f57c3d666a53421049053eaacdd14bbd0a528e2186fcb2e672effd053bb0", + "sha256:d9e45d7faa48ee908174d8fe84854479ef838fc6a705c9315372eacbc2f02897", + "sha256:da3326d9e65ef63a817ecbcc0df6e94463713b754fe293eaa03da99befb9a5bd", + "sha256:de00632ca48df9daf77a2c65a484531649261ec9f25489917f09e455cb09ddb2", + "sha256:e1f185f86a6f3403aa2420e815904c67b2f9ebc443f045edd0de921108345794", + "sha256:e824f1492727fa856dd6eda4f7cee25f8518a12f3c4a56a74e8095695089cf6d", + "sha256:e912091979546adf63357d7e2ccff9b44f026c075aeaf25a52d0e95ad2281074", + "sha256:eaabd426fe94daf8fd157c32e571c85cb12e66692f15516a83a03264b08d06c3", + "sha256:ebf3e58c7ec8a8bed6d66a75d7fb37b55e5015b03ceae72a8e7c74495551e224", + "sha256:ecaae4149d99b1c9e7b88bb03e3221956f68fd6d50be2ef061b2381b61d20838", + "sha256:eecbc200c7fd5ddb9a7f16c7decb07b566c29fa2161a16cf67b8d068bd21690a", + "sha256:f155a433c2ec037d4e8df17d18922c3a0d9b3232a396690f17175d2946f0218d", + "sha256:f1e34719c6ed0b92f418c7c780480b26b5d9c50349e9a9af7d76bf757530350d", + "sha256:f34be2938726fc13801220747472850852fe6b1ea75869a048d6f896838c896f", + "sha256:f820802628d2694cb7e56db99213f930856014862f3fd943d290ea8438d07ca8", + "sha256:f8bf04158c6b607d747e93949aa60618b61312fe647a6369f88ce2ff16043490", + "sha256:f8e160feb2aed042cd657a72acc0b481212ed28b1b9a95c0cee1621b524e1966", + "sha256:f9d332f8c2a2fcbffe1378594431458ddbef721c1769d78e2cbc06280d8155f9", + "sha256:fa09f53c465e532f4d3db095e0c55b615f010ad81803d383195b6b5ca6cbf5f3", + "sha256:faa3a41b2b66b6e50f84ae4a68c64fcd0c44355741c6374813a800cd6695db9e", + "sha256:fd44c878ea55ba351104cb93cc85e74916eb8fa440ca7903e57575e97394f608" + ], + "markers": "python_version >= '3.7'", + "version": "==3.4.4" + }, + "docutils": { + "hashes": [ + "sha256:9fdb771707c8784c8f2728b67cb2c691305933d68137ef95a75db5f4dfbc213d", + "sha256:b0e98d679283fc3bb0ead8a5da7f501baa632654e7056e9c5846842213d674d8" + ], + "markers": "python_version >= '3.9'", + "version": "==0.22.2" + }, + "id": { + "hashes": [ + "sha256:292cb8a49eacbbdbce97244f47a97b4c62540169c976552e497fd57df0734c1d", + "sha256:f1434e1cef91f2cbb8a4ec64663d5a23b9ed43ef44c4c957d02583d61714c658" + ], + "markers": "python_version >= '3.8'", + "version": "==1.5.0" + }, + "idna": { + "hashes": [ + "sha256:771a87f49d9defaf64091e6e6fe9c18d4833f140bd19464795bc32d966ca37ea", + "sha256:795dafcc9c04ed0c1fb032c2aa73654d8e8c5023a7df64a53f39190ada629902" + ], + "markers": "python_version >= '3.8'", + "version": "==3.11" + }, + "jaraco.classes": { + "hashes": [ + "sha256:47a024b51d0239c0dd8c8540c6c7f484be3b8fcf0b2d85c13825780d3b3f3acd", + "sha256:f662826b6bed8cace05e7ff873ce0f9283b5c924470fe664fff1c2f00f581790" + ], + "markers": "python_version >= '3.8'", + "version": "==3.4.0" + }, + "jaraco.context": { + "hashes": [ + "sha256:9bae4ea555cf0b14938dc0aee7c9f32ed303aa20a3b73e7dc80111628792d1b3", + "sha256:f797fc481b490edb305122c9181830a3a5b76d84ef6d1aef2fb9b47ab956f9e4" + ], + "markers": "python_version >= '3.8'", + "version": "==6.0.1" + }, + "jaraco.functools": { + "hashes": [ + "sha256:227ff8ed6f7b8f62c56deff101545fa7543cf2c8e7b82a7c2116e672f29c26e8", + "sha256:cfd13ad0dd2c47a3600b439ef72d8615d482cedcff1632930d6f28924d92f294" + ], + "markers": "python_version >= '3.9'", + "version": "==4.3.0" + }, + "keyring": { + "hashes": [ + "sha256:0b39998aa941431eb3d9b0d4b2460bc773b9df6fed7621c2dfb291a7e0187a66", + "sha256:552a3f7af126ece7ed5c89753650eec89c7eaae8617d0aa4d9ad2b75111266bd" + ], + "markers": "python_version >= '3.9'", + "version": "==25.6.0" + }, + "markdown-it-py": { + "hashes": [ + "sha256:87327c59b172c5011896038353a81343b6754500a08cd7a4973bb48c6d578147", + "sha256:cb0a2b4aa34f932c007117b194e945bd74e0ec24133ceb5bac59009cda1cb9f3" + ], + "markers": "python_version >= '3.10'", + "version": "==4.0.0" + }, + "mdurl": { + "hashes": [ + "sha256:84008a41e51615a49fc9966191ff91509e3c40b939176e643fd50a5c2196b8f8", + "sha256:bb413d29f5eea38f31dd4754dd7377d4465116fb207585f97bf925588687c1ba" + ], + "markers": "python_version >= '3.7'", + "version": "==0.1.2" + }, + "more-itertools": { + "hashes": [ + "sha256:52d4362373dcf7c52546bc4af9a86ee7c4579df9a8dc268be0a2f949d376cc9b", + "sha256:f638ddf8a1a0d134181275fb5d58b086ead7c6a72429ad725c67503f13ba30bd" + ], + "markers": "python_version >= '3.9'", + "version": "==10.8.0" + }, + "nh3": { + "hashes": [ + "sha256:019ecbd007536b67fdf76fab411b648fb64e2257ca3262ec80c3425c24028c80", + "sha256:03d617e5c8aa7331bd2659c654e021caf9bba704b109e7b2b28b039a00949fe5", + "sha256:0dca4365db62b2d71ff1620ee4f800c4729849906c5dd504ee1a7b2389558e31", + "sha256:0fe7ee035dd7b2290715baf29cb27167dddd2ff70ea7d052c958dbd80d323c99", + "sha256:13398e676a14d6233f372c75f52d5ae74f98210172991f7a3142a736bd92b131", + "sha256:169db03df90da63286e0560ea0efa9b6f3b59844a9735514a1d47e6bb2c8c61b", + "sha256:1710f3901cd6440ca92494ba2eb6dc260f829fa8d9196b659fa10de825610ce0", + "sha256:1f9ba555a797dbdcd844b89523f29cdc90973d8bd2e836ea6b962cf567cadd93", + "sha256:2ab70e8c6c7d2ce953d2a58102eefa90c2d0a5ed7aa40c7e29a487bc5e613131", + "sha256:2c9850041b77a9147d6bbd6dbbf13eeec7009eb60b44e83f07fcb2910075bf9b", + "sha256:403c11563e50b915d0efdb622866d1d9e4506bce590ef7da57789bf71dd148b5", + "sha256:45c953e57028c31d473d6b648552d9cab1efe20a42ad139d78e11d8f42a36130", + "sha256:562da3dca7a17f9077593214a9781a94b8d76de4f158f8c895e62f09573945fe", + "sha256:6d66f41672eb4060cf87c037f760bdbc6847852ca9ef8e9c5a5da18f090abf87", + "sha256:7064ccf5ace75825bd7bf57859daaaf16ed28660c1c6b306b649a9eda4b54b1e", + "sha256:72d67c25a84579f4a432c065e8b4274e53b7cf1df8f792cf846abfe2c3090866", + "sha256:7bb18403f02b655a1bbe4e3a4696c2ae1d6ae8f5991f7cacb684b1ae27e6c9f7", + "sha256:91e9b001101fb4500a2aafe3e7c92928d85242d38bf5ac0aba0b7480da0a4cd6", + "sha256:a40202fd58e49129764f025bbaae77028e420f1d5b3c8e6f6fd3a6490d513868", + "sha256:c8745454cdd28bbbc90861b80a0111a195b0e3961b9fa2e672be89eb199fa5d8", + "sha256:cf5964d54edd405e68583114a7cba929468bcd7db5e676ae38ee954de1cfc104", + "sha256:d18957a90806d943d141cc5e4a0fefa1d77cf0d7a156878bf9a66eed52c9cc7d", + "sha256:dce4248edc427c9b79261f3e6e2b3ecbdd9b88c267012168b4a7b3fc6fd41d13", + "sha256:f2f55c4d2d5a207e74eefe4d828067bbb01300e06e2a7436142f915c5928de07", + "sha256:f394759a06df8b685a4ebfb1874fb67a9cbfd58c64fc5ed587a663c0e63ec376", + "sha256:f97f8b25cb2681d25e2338148159447e4d689aafdccfcf19e61ff7db3905768a" + ], + "markers": "python_version >= '3.8'", + "version": "==0.3.2" + }, + "packaging": { + "hashes": [ + "sha256:29572ef2b1f17581046b3a2227d5c611fb25ec70ca1ba8554b24b0e69331a484", + "sha256:d443872c98d677bf60f6a1f2f8c1cb748e8fe762d2bf9d3148b5599295b0fc4f" + ], + "markers": "python_version >= '3.8'", + "version": "==25.0" + }, + "pygments": { + "hashes": [ + "sha256:636cb2477cec7f8952536970bc533bc43743542f70392ae026374600add5b887", + "sha256:86540386c03d588bb81d44bc3928634ff26449851e99741617ecb9037ee5ec0b" + ], + "markers": "python_version >= '3.8'", + "version": "==2.19.2" + }, + "pywin32-ctypes": { + "hashes": [ + "sha256:8a1513379d709975552d202d942d9837758905c8d01eb82b8bcc30918929e7b8", + "sha256:d162dc04946d704503b2edc4d55f3dba5c1d539ead017afa00142c38b9885755" + ], + "markers": "python_version >= '3.6'", + "version": "==0.2.3" + }, + "readme-renderer": { + "hashes": [ + "sha256:2fbca89b81a08526aadf1357a8c2ae889ec05fb03f5da67f9769c9a592166151", + "sha256:8712034eabbfa6805cacf1402b4eeb2a73028f72d1166d6f5cb7f9c047c5d1e1" + ], + "markers": "python_version >= '3.9'", + "version": "==44.0" + }, + "requests": { + "hashes": [ + "sha256:2462f94637a34fd532264295e186976db0f5d453d1cdd31473c85a6a161affb6", + "sha256:dbba0bac56e100853db0ea71b82b4dfd5fe2bf6d3754a8893c3af500cec7d7cf" + ], + "markers": "python_version >= '3.9'", + "version": "==2.32.5" + }, + "requests-toolbelt": { + "hashes": [ + "sha256:7681a0a3d047012b5bdc0ee37d7f8f07ebe76ab08caeccfc3921ce23c88d5bc6", + "sha256:cccfdd665f0a24fcf4726e690f65639d272bb0637b9b92dfd91a5568ccf6bd06" + ], + "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3'", + "version": "==1.0.0" + }, + "rfc3986": { + "hashes": [ + "sha256:50b1502b60e289cb37883f3dfd34532b8873c7de9f49bb546641ce9cbd256ebd", + "sha256:97aacf9dbd4bfd829baad6e6309fa6573aaf1be3f6fa735c8ab05e46cecb261c" + ], + "markers": "python_version >= '3.7'", + "version": "==2.0.0" + }, + "rich": { + "hashes": [ + "sha256:73ff50c7c0c1c77c8243079283f4edb376f0f6442433aecb8ce7e6d0b92d1fe4", + "sha256:76bc51fe2e57d2b1be1f96c524b890b816e334ab4c1e45888799bfaab0021edd" + ], + "markers": "python_full_version >= '3.8.0'", + "version": "==14.2.0" + }, + "twine": { + "hashes": [ + "sha256:418ebf08ccda9a8caaebe414433b0ba5e25eb5e4a927667122fbe8f829f985d8", + "sha256:e5ed0d2fd70c9959770dce51c8f39c8945c574e18173a7b81802dab51b4b75cf" + ], + "index": "pypi", + "markers": "python_version >= '3.9'", + "version": "==6.2.0" + }, + "urllib3": { + "hashes": [ + "sha256:3fc47733c7e419d4bc3f6b3dc2b4f890bb743906a30d56ba4a5bfa4bbff92760", + "sha256:e6b01673c0fa6a13e374b50871808eb3bf7046c4b125b216f6bf1cc604cff0dc" + ], + "markers": "python_version >= '3.9'", + "version": "==2.5.0" + } + }, + "develop": { + "build": { + "hashes": [ + "sha256:698edd0ea270bde950f53aed21f3a0135672206f3911e0176261a31e0e07b397", + "sha256:7145f0b5061ba90a1500d60bd1b13ca0a8a4cebdd0cc16ed8adf1c0e739f43b4" + ], + "index": "pypi", + "markers": "python_version >= '3.9'", + "version": "==1.3.0" + }, + "certifi": { + "hashes": [ + "sha256:0f212c2744a9bb6de0c56639a6f68afe01ecd92d91f14ae897c4fe7bbeeef0de", + "sha256:47c09d31ccf2acf0be3f701ea53595ee7e0b8fa08801c6624be771df09ae7b43" + ], + "markers": "python_version >= '3.7'", + "version": "==2025.10.5" + }, + "charset-normalizer": { + "hashes": [ + "sha256:027f6de494925c0ab2a55eab46ae5129951638a49a34d87f4c3eda90f696b4ad", + "sha256:077fbb858e903c73f6c9db43374fd213b0b6a778106bc7032446a8e8b5b38b93", + "sha256:0a98e6759f854bd25a58a73fa88833fba3b7c491169f86ce1180c948ab3fd394", + "sha256:0d3d8f15c07f86e9ff82319b3d9ef6f4bf907608f53fe9d92b28ea9ae3d1fd89", + "sha256:0f04b14ffe5fdc8c4933862d8306109a2c51e0704acfa35d51598eb45a1e89fc", + "sha256:11d694519d7f29d6cd09f6ac70028dba10f92f6cdd059096db198c283794ac86", + "sha256:194f08cbb32dc406d6e1aea671a68be0823673db2832b38405deba2fb0d88f63", + "sha256:1bee1e43c28aa63cb16e5c14e582580546b08e535299b8b6158a7c9c768a1f3d", + "sha256:21d142cc6c0ec30d2efee5068ca36c128a30b0f2c53c1c07bd78cb6bc1d3be5f", + "sha256:2437418e20515acec67d86e12bf70056a33abdacb5cb1655042f6538d6b085a8", + "sha256:244bfb999c71b35de57821b8ea746b24e863398194a4014e4c76adc2bbdfeff0", + "sha256:2677acec1a2f8ef614c6888b5b4ae4060cc184174a938ed4e8ef690e15d3e505", + "sha256:277e970e750505ed74c832b4bf75dac7476262ee2a013f5574dd49075879e161", + "sha256:2aaba3b0819274cc41757a1da876f810a3e4d7b6eb25699253a4effef9e8e4af", + "sha256:2b7d8f6c26245217bd2ad053761201e9f9680f8ce52f0fcd8d0755aeae5b2152", + "sha256:2c9d3c380143a1fedbff95a312aa798578371eb29da42106a29019368a475318", + "sha256:3162d5d8ce1bb98dd51af660f2121c55d0fa541b46dff7bb9b9f86ea1d87de72", + "sha256:31fd66405eaf47bb62e8cd575dc621c56c668f27d46a61d975a249930dd5e2a4", + "sha256:362d61fd13843997c1c446760ef36f240cf81d3ebf74ac62652aebaf7838561e", + "sha256:376bec83a63b8021bb5c8ea75e21c4ccb86e7e45ca4eb81146091b56599b80c3", + "sha256:44c2a8734b333e0578090c4cd6b16f275e07aa6614ca8715e6c038e865e70576", + "sha256:47cc91b2f4dd2833fddaedd2893006b0106129d4b94fdb6af1f4ce5a9965577c", + "sha256:4902828217069c3c5c71094537a8e623f5d097858ac6ca8252f7b4d10b7560f1", + "sha256:4bd5d4137d500351a30687c2d3971758aac9a19208fc110ccb9d7188fbe709e8", + "sha256:4fe7859a4e3e8457458e2ff592f15ccb02f3da787fcd31e0183879c3ad4692a1", + "sha256:542d2cee80be6f80247095cc36c418f7bddd14f4a6de45af91dfad36d817bba2", + "sha256:554af85e960429cf30784dd47447d5125aaa3b99a6f0683589dbd27e2f45da44", + "sha256:5833d2c39d8896e4e19b689ffc198f08ea58116bee26dea51e362ecc7cd3ed26", + "sha256:5947809c8a2417be3267efc979c47d76a079758166f7d43ef5ae8e9f92751f88", + "sha256:5ae497466c7901d54b639cf42d5b8c1b6a4fead55215500d2f486d34db48d016", + "sha256:5bd2293095d766545ec1a8f612559f6b40abc0eb18bb2f5d1171872d34036ede", + "sha256:5bfbb1b9acf3334612667b61bd3002196fe2a1eb4dd74d247e0f2a4d50ec9bbf", + "sha256:5cb4d72eea50c8868f5288b7f7f33ed276118325c1dfd3957089f6b519e1382a", + "sha256:5dbe56a36425d26d6cfb40ce79c314a2e4dd6211d51d6d2191c00bed34f354cc", + "sha256:5f819d5fe9234f9f82d75bdfa9aef3a3d72c4d24a6e57aeaebba32a704553aa0", + "sha256:64b55f9dce520635f018f907ff1b0df1fdc31f2795a922fb49dd14fbcdf48c84", + "sha256:6515f3182dbe4ea06ced2d9e8666d97b46ef4c75e326b79bb624110f122551db", + "sha256:65e2befcd84bc6f37095f5961e68a6f077bf44946771354a28ad434c2cce0ae1", + "sha256:6aee717dcfead04c6eb1ce3bd29ac1e22663cdea57f943c87d1eab9a025438d7", + "sha256:6b39f987ae8ccdf0d2642338faf2abb1862340facc796048b604ef14919e55ed", + "sha256:6e1fcf0720908f200cd21aa4e6750a48ff6ce4afe7ff5a79a90d5ed8a08296f8", + "sha256:74018750915ee7ad843a774364e13a3db91682f26142baddf775342c3f5b1133", + "sha256:74664978bb272435107de04e36db5a9735e78232b85b77d45cfb38f758efd33e", + "sha256:74bb723680f9f7a6234dcf67aea57e708ec1fbdf5699fb91dfd6f511b0a320ef", + "sha256:752944c7ffbfdd10c074dc58ec2d5a8a4cd9493b314d367c14d24c17684ddd14", + "sha256:778d2e08eda00f4256d7f672ca9fef386071c9202f5e4607920b86d7803387f2", + "sha256:780236ac706e66881f3b7f2f32dfe90507a09e67d1d454c762cf642e6e1586e0", + "sha256:798d75d81754988d2565bff1b97ba5a44411867c0cf32b77a7e8f8d84796b10d", + "sha256:799a7a5e4fb2d5898c60b640fd4981d6a25f1c11790935a44ce38c54e985f828", + "sha256:7a32c560861a02ff789ad905a2fe94e3f840803362c84fecf1851cb4cf3dc37f", + "sha256:7c308f7e26e4363d79df40ca5b2be1c6ba9f02bdbccfed5abddb7859a6ce72cf", + "sha256:7fa17817dc5625de8a027cb8b26d9fefa3ea28c8253929b8d6649e705d2835b6", + "sha256:81d5eb2a312700f4ecaa977a8235b634ce853200e828fbadf3a9c50bab278328", + "sha256:82004af6c302b5d3ab2cfc4cc5f29db16123b1a8417f2e25f9066f91d4411090", + "sha256:837c2ce8c5a65a2035be9b3569c684358dfbf109fd3b6969630a87535495ceaa", + "sha256:840c25fb618a231545cbab0564a799f101b63b9901f2569faecd6b222ac72381", + "sha256:8a6562c3700cce886c5be75ade4a5db4214fda19fede41d9792d100288d8f94c", + "sha256:8af65f14dc14a79b924524b1e7fffe304517b2bff5a58bf64f30b98bbc5079eb", + "sha256:8ef3c867360f88ac904fd3f5e1f902f13307af9052646963ee08ff4f131adafc", + "sha256:94537985111c35f28720e43603b8e7b43a6ecfb2ce1d3058bbe955b73404e21a", + "sha256:99ae2cffebb06e6c22bdc25801d7b30f503cc87dbd283479e7b606f70aff57ec", + "sha256:9a26f18905b8dd5d685d6d07b0cdf98a79f3c7a918906af7cc143ea2e164c8bc", + "sha256:9b35f4c90079ff2e2edc5b26c0c77925e5d2d255c42c74fdb70fb49b172726ac", + "sha256:9cd98cdc06614a2f768d2b7286d66805f94c48cde050acdbbb7db2600ab3197e", + "sha256:9d1bb833febdff5c8927f922386db610b49db6e0d4f4ee29601d71e7c2694313", + "sha256:9f7fcd74d410a36883701fafa2482a6af2ff5ba96b9a620e9e0721e28ead5569", + "sha256:a59cb51917aa591b1c4e6a43c132f0cdc3c76dbad6155df4e28ee626cc77a0a3", + "sha256:a61900df84c667873b292c3de315a786dd8dac506704dea57bc957bd31e22c7d", + "sha256:a79cfe37875f822425b89a82333404539ae63dbdddf97f84dcbc3d339aae9525", + "sha256:a8a8b89589086a25749f471e6a900d3f662d1d3b6e2e59dcecf787b1cc3a1894", + "sha256:a8bf8d0f749c5757af2142fe7903a9df1d2e8aa3841559b2bad34b08d0e2bcf3", + "sha256:a9768c477b9d7bd54bc0c86dbaebdec6f03306675526c9927c0e8a04e8f94af9", + "sha256:ac1c4a689edcc530fc9d9aa11f5774b9e2f33f9a0c6a57864e90908f5208d30a", + "sha256:af2d8c67d8e573d6de5bc30cdb27e9b95e49115cd9baad5ddbd1a6207aaa82a9", + "sha256:b435cba5f4f750aa6c0a0d92c541fb79f69a387c91e61f1795227e4ed9cece14", + "sha256:b5b290ccc2a263e8d185130284f8501e3e36c5e02750fc6b6bdeb2e9e96f1e25", + "sha256:b5d84d37db046c5ca74ee7bb47dd6cbc13f80665fdde3e8040bdd3fb015ecb50", + "sha256:b7cf1017d601aa35e6bb650b6ad28652c9cd78ee6caff19f3c28d03e1c80acbf", + "sha256:bc7637e2f80d8530ee4a78e878bce464f70087ce73cf7c1caf142416923b98f1", + "sha256:c0463276121fdee9c49b98908b3a89c39be45d86d1dbaa22957e38f6321d4ce3", + "sha256:c4ef880e27901b6cc782f1b95f82da9313c0eb95c3af699103088fa0ac3ce9ac", + "sha256:c8ae8a0f02f57a6e61203a31428fa1d677cbe50c93622b4149d5c0f319c1d19e", + "sha256:ca5862d5b3928c4940729dacc329aa9102900382fea192fc5e52eb69d6093815", + "sha256:cb01158d8b88ee68f15949894ccc6712278243d95f344770fa7593fa2d94410c", + "sha256:cb6254dc36b47a990e59e1068afacdcd02958bdcce30bb50cc1700a8b9d624a6", + "sha256:cc00f04ed596e9dc0da42ed17ac5e596c6ccba999ba6bd92b0e0aef2f170f2d6", + "sha256:cd09d08005f958f370f539f186d10aec3377d55b9eeb0d796025d4886119d76e", + "sha256:cd4b7ca9984e5e7985c12bc60a6f173f3c958eae74f3ef6624bb6b26e2abbae4", + "sha256:ce8a0633f41a967713a59c4139d29110c07e826d131a316b50ce11b1d79b4f84", + "sha256:cead0978fc57397645f12578bfd2d5ea9138ea0fac82b2f63f7f7c6877986a69", + "sha256:d055ec1e26e441f6187acf818b73564e6e6282709e9bcb5b63f5b23068356a15", + "sha256:d1f13550535ad8cff21b8d757a3257963e951d96e20ec82ab44bc64aeb62a191", + "sha256:d9c7f57c3d666a53421049053eaacdd14bbd0a528e2186fcb2e672effd053bb0", + "sha256:d9e45d7faa48ee908174d8fe84854479ef838fc6a705c9315372eacbc2f02897", + "sha256:da3326d9e65ef63a817ecbcc0df6e94463713b754fe293eaa03da99befb9a5bd", + "sha256:de00632ca48df9daf77a2c65a484531649261ec9f25489917f09e455cb09ddb2", + "sha256:e1f185f86a6f3403aa2420e815904c67b2f9ebc443f045edd0de921108345794", + "sha256:e824f1492727fa856dd6eda4f7cee25f8518a12f3c4a56a74e8095695089cf6d", + "sha256:e912091979546adf63357d7e2ccff9b44f026c075aeaf25a52d0e95ad2281074", + "sha256:eaabd426fe94daf8fd157c32e571c85cb12e66692f15516a83a03264b08d06c3", + "sha256:ebf3e58c7ec8a8bed6d66a75d7fb37b55e5015b03ceae72a8e7c74495551e224", + "sha256:ecaae4149d99b1c9e7b88bb03e3221956f68fd6d50be2ef061b2381b61d20838", + "sha256:eecbc200c7fd5ddb9a7f16c7decb07b566c29fa2161a16cf67b8d068bd21690a", + "sha256:f155a433c2ec037d4e8df17d18922c3a0d9b3232a396690f17175d2946f0218d", + "sha256:f1e34719c6ed0b92f418c7c780480b26b5d9c50349e9a9af7d76bf757530350d", + "sha256:f34be2938726fc13801220747472850852fe6b1ea75869a048d6f896838c896f", + "sha256:f820802628d2694cb7e56db99213f930856014862f3fd943d290ea8438d07ca8", + "sha256:f8bf04158c6b607d747e93949aa60618b61312fe647a6369f88ce2ff16043490", + "sha256:f8e160feb2aed042cd657a72acc0b481212ed28b1b9a95c0cee1621b524e1966", + "sha256:f9d332f8c2a2fcbffe1378594431458ddbef721c1769d78e2cbc06280d8155f9", + "sha256:fa09f53c465e532f4d3db095e0c55b615f010ad81803d383195b6b5ca6cbf5f3", + "sha256:faa3a41b2b66b6e50f84ae4a68c64fcd0c44355741c6374813a800cd6695db9e", + "sha256:fd44c878ea55ba351104cb93cc85e74916eb8fa440ca7903e57575e97394f608" + ], + "markers": "python_version >= '3.7'", + "version": "==3.4.4" + }, + "colorama": { + "hashes": [ + "sha256:08695f5cb7ed6e0531a20572697297273c47b8cae5a63ffc6d6ed5c201be6e44", + "sha256:4f1d9991f5acc0ca119f9d443620b77f9d6b33703e51011c16baf57afb285fc6" + ], + "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3, 3.4, 3.5, 3.6'", + "version": "==0.4.6" + }, + "docutils": { + "hashes": [ + "sha256:9fdb771707c8784c8f2728b67cb2c691305933d68137ef95a75db5f4dfbc213d", + "sha256:b0e98d679283fc3bb0ead8a5da7f501baa632654e7056e9c5846842213d674d8" + ], + "markers": "python_version >= '3.9'", + "version": "==0.22.2" + }, + "exceptiongroup": { + "hashes": [ + "sha256:4d111e6e0c13d0644cad6ddaa7ed0261a0b36971f6d23e7ec9b4b9097da78a10", + "sha256:b241f5885f560bc56a59ee63ca4c6a8bfa46ae4ad651af316d4e81817bb9fd88" + ], + "index": "pypi", + "markers": "python_version >= '3.7'", + "version": "==1.3.0" + }, + "id": { + "hashes": [ + "sha256:292cb8a49eacbbdbce97244f47a97b4c62540169c976552e497fd57df0734c1d", + "sha256:f1434e1cef91f2cbb8a4ec64663d5a23b9ed43ef44c4c957d02583d61714c658" + ], + "markers": "python_version >= '3.8'", + "version": "==1.5.0" + }, + "idna": { + "hashes": [ + "sha256:771a87f49d9defaf64091e6e6fe9c18d4833f140bd19464795bc32d966ca37ea", + "sha256:795dafcc9c04ed0c1fb032c2aa73654d8e8c5023a7df64a53f39190ada629902" + ], + "markers": "python_version >= '3.8'", + "version": "==3.11" + }, + "iniconfig": { + "hashes": [ + "sha256:c76315c77db068650d49c5b56314774a7804df16fee4402c1f19d6d15d8c4730", + "sha256:f631c04d2c48c52b84d0d0549c99ff3859c98df65b3101406327ecc7d53fbf12" + ], + "markers": "python_version >= '3.10'", + "version": "==2.3.0" + }, + "jaraco.classes": { + "hashes": [ + "sha256:47a024b51d0239c0dd8c8540c6c7f484be3b8fcf0b2d85c13825780d3b3f3acd", + "sha256:f662826b6bed8cace05e7ff873ce0f9283b5c924470fe664fff1c2f00f581790" + ], + "markers": "python_version >= '3.8'", + "version": "==3.4.0" + }, + "jaraco.context": { + "hashes": [ + "sha256:9bae4ea555cf0b14938dc0aee7c9f32ed303aa20a3b73e7dc80111628792d1b3", + "sha256:f797fc481b490edb305122c9181830a3a5b76d84ef6d1aef2fb9b47ab956f9e4" + ], + "markers": "python_version >= '3.8'", + "version": "==6.0.1" + }, + "jaraco.functools": { + "hashes": [ + "sha256:227ff8ed6f7b8f62c56deff101545fa7543cf2c8e7b82a7c2116e672f29c26e8", + "sha256:cfd13ad0dd2c47a3600b439ef72d8615d482cedcff1632930d6f28924d92f294" + ], + "markers": "python_version >= '3.9'", + "version": "==4.3.0" + }, + "keyring": { + "hashes": [ + "sha256:0b39998aa941431eb3d9b0d4b2460bc773b9df6fed7621c2dfb291a7e0187a66", + "sha256:552a3f7af126ece7ed5c89753650eec89c7eaae8617d0aa4d9ad2b75111266bd" + ], + "markers": "python_version >= '3.9'", + "version": "==25.6.0" + }, + "markdown-it-py": { + "hashes": [ + "sha256:87327c59b172c5011896038353a81343b6754500a08cd7a4973bb48c6d578147", + "sha256:cb0a2b4aa34f932c007117b194e945bd74e0ec24133ceb5bac59009cda1cb9f3" + ], + "markers": "python_version >= '3.10'", + "version": "==4.0.0" + }, + "mdurl": { + "hashes": [ + "sha256:84008a41e51615a49fc9966191ff91509e3c40b939176e643fd50a5c2196b8f8", + "sha256:bb413d29f5eea38f31dd4754dd7377d4465116fb207585f97bf925588687c1ba" + ], + "markers": "python_version >= '3.7'", + "version": "==0.1.2" + }, + "more-itertools": { + "hashes": [ + "sha256:52d4362373dcf7c52546bc4af9a86ee7c4579df9a8dc268be0a2f949d376cc9b", + "sha256:f638ddf8a1a0d134181275fb5d58b086ead7c6a72429ad725c67503f13ba30bd" + ], + "markers": "python_version >= '3.9'", + "version": "==10.8.0" + }, + "nh3": { + "hashes": [ + "sha256:019ecbd007536b67fdf76fab411b648fb64e2257ca3262ec80c3425c24028c80", + "sha256:03d617e5c8aa7331bd2659c654e021caf9bba704b109e7b2b28b039a00949fe5", + "sha256:0dca4365db62b2d71ff1620ee4f800c4729849906c5dd504ee1a7b2389558e31", + "sha256:0fe7ee035dd7b2290715baf29cb27167dddd2ff70ea7d052c958dbd80d323c99", + "sha256:13398e676a14d6233f372c75f52d5ae74f98210172991f7a3142a736bd92b131", + "sha256:169db03df90da63286e0560ea0efa9b6f3b59844a9735514a1d47e6bb2c8c61b", + "sha256:1710f3901cd6440ca92494ba2eb6dc260f829fa8d9196b659fa10de825610ce0", + "sha256:1f9ba555a797dbdcd844b89523f29cdc90973d8bd2e836ea6b962cf567cadd93", + "sha256:2ab70e8c6c7d2ce953d2a58102eefa90c2d0a5ed7aa40c7e29a487bc5e613131", + "sha256:2c9850041b77a9147d6bbd6dbbf13eeec7009eb60b44e83f07fcb2910075bf9b", + "sha256:403c11563e50b915d0efdb622866d1d9e4506bce590ef7da57789bf71dd148b5", + "sha256:45c953e57028c31d473d6b648552d9cab1efe20a42ad139d78e11d8f42a36130", + "sha256:562da3dca7a17f9077593214a9781a94b8d76de4f158f8c895e62f09573945fe", + "sha256:6d66f41672eb4060cf87c037f760bdbc6847852ca9ef8e9c5a5da18f090abf87", + "sha256:7064ccf5ace75825bd7bf57859daaaf16ed28660c1c6b306b649a9eda4b54b1e", + "sha256:72d67c25a84579f4a432c065e8b4274e53b7cf1df8f792cf846abfe2c3090866", + "sha256:7bb18403f02b655a1bbe4e3a4696c2ae1d6ae8f5991f7cacb684b1ae27e6c9f7", + "sha256:91e9b001101fb4500a2aafe3e7c92928d85242d38bf5ac0aba0b7480da0a4cd6", + "sha256:a40202fd58e49129764f025bbaae77028e420f1d5b3c8e6f6fd3a6490d513868", + "sha256:c8745454cdd28bbbc90861b80a0111a195b0e3961b9fa2e672be89eb199fa5d8", + "sha256:cf5964d54edd405e68583114a7cba929468bcd7db5e676ae38ee954de1cfc104", + "sha256:d18957a90806d943d141cc5e4a0fefa1d77cf0d7a156878bf9a66eed52c9cc7d", + "sha256:dce4248edc427c9b79261f3e6e2b3ecbdd9b88c267012168b4a7b3fc6fd41d13", + "sha256:f2f55c4d2d5a207e74eefe4d828067bbb01300e06e2a7436142f915c5928de07", + "sha256:f394759a06df8b685a4ebfb1874fb67a9cbfd58c64fc5ed587a663c0e63ec376", + "sha256:f97f8b25cb2681d25e2338148159447e4d689aafdccfcf19e61ff7db3905768a" + ], + "markers": "python_version >= '3.8'", + "version": "==0.3.2" + }, + "packaging": { + "hashes": [ + "sha256:29572ef2b1f17581046b3a2227d5c611fb25ec70ca1ba8554b24b0e69331a484", + "sha256:d443872c98d677bf60f6a1f2f8c1cb748e8fe762d2bf9d3148b5599295b0fc4f" + ], + "markers": "python_version >= '3.8'", + "version": "==25.0" + }, + "pluggy": { + "hashes": [ + "sha256:7dcc130b76258d33b90f61b658791dede3486c3e6bfb003ee5c9bfb396dd22f3", + "sha256:e920276dd6813095e9377c0bc5566d94c932c33b27a3e3945d8389c374dd4746" + ], + "markers": "python_version >= '3.9'", + "version": "==1.6.0" + }, + "pygments": { + "hashes": [ + "sha256:636cb2477cec7f8952536970bc533bc43743542f70392ae026374600add5b887", + "sha256:86540386c03d588bb81d44bc3928634ff26449851e99741617ecb9037ee5ec0b" + ], + "markers": "python_version >= '3.8'", + "version": "==2.19.2" + }, + "pyproject-hooks": { + "hashes": [ + "sha256:1e859bd5c40fae9448642dd871adf459e5e2084186e8d2c2a79a824c970da1f8", + "sha256:9e5c6bfa8dcc30091c74b0cf803c81fdd29d94f01992a7707bc97babb1141913" + ], + "markers": "python_version >= '3.7'", + "version": "==1.2.0" + }, + "pytest": { + "hashes": [ + "sha256:86c0d0b93306b961d58d62a4db4879f27fe25513d4b969df351abdddb3c30e01", + "sha256:872f880de3fc3a5bdc88a11b39c9710c3497a547cfa9320bc3c5e62fbf272e79" + ], + "index": "pypi", + "markers": "python_version >= '3.9'", + "version": "==8.4.2" + }, + "pywin32-ctypes": { + "hashes": [ + "sha256:8a1513379d709975552d202d942d9837758905c8d01eb82b8bcc30918929e7b8", + "sha256:d162dc04946d704503b2edc4d55f3dba5c1d539ead017afa00142c38b9885755" + ], + "markers": "python_version >= '3.6'", + "version": "==0.2.3" + }, + "readme-renderer": { + "hashes": [ + "sha256:2fbca89b81a08526aadf1357a8c2ae889ec05fb03f5da67f9769c9a592166151", + "sha256:8712034eabbfa6805cacf1402b4eeb2a73028f72d1166d6f5cb7f9c047c5d1e1" + ], + "markers": "python_version >= '3.9'", + "version": "==44.0" + }, + "requests": { + "hashes": [ + "sha256:2462f94637a34fd532264295e186976db0f5d453d1cdd31473c85a6a161affb6", + "sha256:dbba0bac56e100853db0ea71b82b4dfd5fe2bf6d3754a8893c3af500cec7d7cf" + ], + "markers": "python_version >= '3.9'", + "version": "==2.32.5" + }, + "requests-toolbelt": { + "hashes": [ + "sha256:7681a0a3d047012b5bdc0ee37d7f8f07ebe76ab08caeccfc3921ce23c88d5bc6", + "sha256:cccfdd665f0a24fcf4726e690f65639d272bb0637b9b92dfd91a5568ccf6bd06" + ], + "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3'", + "version": "==1.0.0" + }, + "rfc3986": { + "hashes": [ + "sha256:50b1502b60e289cb37883f3dfd34532b8873c7de9f49bb546641ce9cbd256ebd", + "sha256:97aacf9dbd4bfd829baad6e6309fa6573aaf1be3f6fa735c8ab05e46cecb261c" + ], + "markers": "python_version >= '3.7'", + "version": "==2.0.0" + }, + "rich": { + "hashes": [ + "sha256:73ff50c7c0c1c77c8243079283f4edb376f0f6442433aecb8ce7e6d0b92d1fe4", + "sha256:76bc51fe2e57d2b1be1f96c524b890b816e334ab4c1e45888799bfaab0021edd" + ], + "markers": "python_full_version >= '3.8.0'", + "version": "==14.2.0" + }, + "tomli": { + "hashes": [ + "sha256:00b5f5d95bbfc7d12f91ad8c593a1659b6387b43f054104cda404be6bda62456", + "sha256:0a154a9ae14bfcf5d8917a59b51ffd5a3ac1fd149b71b47a3a104ca4edcfa845", + "sha256:0c95ca56fbe89e065c6ead5b593ee64b84a26fca063b5d71a1122bf26e533999", + "sha256:0eea8cc5c5e9f89c9b90c4896a8deefc74f518db5927d0e0e8d4a80953d774d0", + "sha256:1cb4ed918939151a03f33d4242ccd0aa5f11b3547d0cf30f7c74a408a5b99878", + "sha256:4021923f97266babc6ccab9f5068642a0095faa0a51a246a6a02fccbb3514eaf", + "sha256:4c2ef0244c75aba9355561272009d934953817c49f47d768070c3c94355c2aa3", + "sha256:4dc4ce8483a5d429ab602f111a93a6ab1ed425eae3122032db7e9acf449451be", + "sha256:4f195fe57ecceac95a66a75ac24d9d5fbc98ef0962e09b2eddec5d39375aae52", + "sha256:5192f562738228945d7b13d4930baffda67b69425a7f0da96d360b0a3888136b", + "sha256:5e01decd096b1530d97d5d85cb4dff4af2d8347bd35686654a004f8dea20fc67", + "sha256:64be704a875d2a59753d80ee8a533c3fe183e3f06807ff7dc2232938ccb01549", + "sha256:70a251f8d4ba2d9ac2542eecf008b3c8a9fc5c3f9f02c56a9d7952612be2fdba", + "sha256:73ee0b47d4dad1c5e996e3cd33b8a76a50167ae5f96a2607cbe8cc773506ab22", + "sha256:74bf8464ff93e413514fefd2be591c3b0b23231a77f901db1eb30d6f712fc42c", + "sha256:792262b94d5d0a466afb5bc63c7daa9d75520110971ee269152083270998316f", + "sha256:7b0882799624980785240ab732537fcfc372601015c00f7fc367c55308c186f6", + "sha256:883b1c0d6398a6a9d29b508c331fa56adbcdff647f6ace4dfca0f50e90dfd0ba", + "sha256:88bd15eb972f3664f5ed4b57c1634a97153b4bac4479dcb6a495f41921eb7f45", + "sha256:8a35dd0e643bb2610f156cca8db95d213a90015c11fee76c946aa62b7ae7e02f", + "sha256:940d56ee0410fa17ee1f12b817b37a4d4e4dc4d27340863cc67236c74f582e77", + "sha256:97d5eec30149fd3294270e889b4234023f2c69747e555a27bd708828353ab606", + "sha256:a0e285d2649b78c0d9027570d4da3425bdb49830a6156121360b3f8511ea3441", + "sha256:a1f7f282fe248311650081faafa5f4732bdbfef5d45fe3f2e702fbc6f2d496e0", + "sha256:a4ea38c40145a357d513bffad0ed869f13c1773716cf71ccaa83b0fa0cc4e42f", + "sha256:a56212bdcce682e56b0aaf79e869ba5d15a6163f88d5451cbde388d48b13f530", + "sha256:ad805ea85eda330dbad64c7ea7a4556259665bdf9d2672f5dccc740eb9d3ca05", + "sha256:b273fcbd7fc64dc3600c098e39136522650c49bca95df2d11cf3b626422392c8", + "sha256:b5870b50c9db823c595983571d1296a6ff3e1b88f734a4c8f6fc6188397de005", + "sha256:b74a0e59ec5d15127acdabd75ea17726ac4c5178ae51b85bfe39c4f8a278e879", + "sha256:be71c93a63d738597996be9528f4abe628d1adf5e6eb11607bc8fe1a510b5dae", + "sha256:c22a8bf253bacc0cf11f35ad9808b6cb75ada2631c2d97c971122583b129afbc", + "sha256:c4665508bcbac83a31ff8ab08f424b665200c0e1e645d2bd9ab3d3e557b6185b", + "sha256:c5f3ffd1e098dfc032d4d3af5c0ac64f6d286d98bc148698356847b80fa4de1b", + "sha256:cebc6fe843e0733ee827a282aca4999b596241195f43b4cc371d64fc6639da9e", + "sha256:d1381caf13ab9f300e30dd8feadb3de072aeb86f1d34a8569453ff32a7dea4bf", + "sha256:d7d86942e56ded512a594786a5ba0a5e521d02529b3826e7761a05138341a2ac", + "sha256:e31d432427dcbf4d86958c184b9bfd1e96b5b71f8eb17e6d02531f434fd335b8", + "sha256:e95b1af3c5b07d9e643909b5abbec77cd9f1217e6d0bca72b0234736b9fb1f1b", + "sha256:f85209946d1fe94416debbb88d00eb92ce9cd5266775424ff81bc959e001acaf", + "sha256:feb0dacc61170ed7ab602d3d972a58f14ee3ee60494292d384649a3dc38ef463", + "sha256:ff72b71b5d10d22ecb084d345fc26f42b5143c5533db5e2eaba7d2d335358876" + ], + "index": "pypi", + "markers": "python_version >= '3.8'", + "version": "==2.3.0" + }, + "twine": { + "hashes": [ + "sha256:418ebf08ccda9a8caaebe414433b0ba5e25eb5e4a927667122fbe8f829f985d8", + "sha256:e5ed0d2fd70c9959770dce51c8f39c8945c574e18173a7b81802dab51b4b75cf" + ], + "index": "pypi", + "markers": "python_version >= '3.9'", + "version": "==6.2.0" + }, + "typing-extensions": { + "hashes": [ + "sha256:0cea48d173cc12fa28ecabc3b837ea3cf6f38c6d1136f85cbaaf598984861466", + "sha256:f0fa19c6845758ab08074a0cfa8b7aecb71c999ca73d62883bc25cc018c4e548" + ], + "index": "pypi", + "markers": "python_version >= '3.9'", + "version": "==4.15.0" + }, + "urllib3": { + "hashes": [ + "sha256:3fc47733c7e419d4bc3f6b3dc2b4f890bb743906a30d56ba4a5bfa4bbff92760", + "sha256:e6b01673c0fa6a13e374b50871808eb3bf7046c4b125b216f6bf1cc604cff0dc" + ], + "markers": "python_version >= '3.9'", + "version": "==2.5.0" + } + } +} diff --git a/README.md b/README.md index 6022e0e..dd02cb6 100644 --- a/README.md +++ b/README.md @@ -1,3 +1,302 @@ -# Python Package Exercise +# StudyBuddy — Your (Unhelpfully) Helpful Study Companion -An exercise to create a Python package, build it, test it, distribute it, and use it. See [instructions](./instructions.md) for details. +[![log github events](https://github.com/swe-students-fall2025/3-python-package-team_cedar/actions/workflows/event-logger.yml/badge.svg)](https://github.com/swe-students-fall2025/3-python-package-team_cedar/actions/workflows/event-logger.yml) + +--- + +**StudyBuddy** is a lighthearted Python package that adds sarcasm, pep talks, and playful structure to your study routine. It gives you randomized study tips, motivational messages, funny excuses, and silly study plans to make your academic life a bit more entertaining. + +- **PyPI:** https://pypi.org/project/studybuddy-teamcedar/0.8.0/ +- **Example app:** [`example.py`](./example.py) + +--- + +## Installation + +From PyPI (recommended): +```bash +pip install studybuddy_teamcedar +``` + +From source: +```bash +git clone https://github.com/swe-students-fall2025/3-python-package-team_cedar.git +cd 3-python-package-team_cedar +pip install -e . +``` + +--- + +## Quick Start (Import & Use) +```python +from studybuddy import study_tip, motivate, excuse, study_plan + +# Get a humorous study tip +print(study_tip("physics", "chaotic")) + +# Get some motivation (sarcastic or genuine) +print(motivate("sarcastic")) + +# Get a funny excuse +print(excuse("homework")) + +# Generate a study plan +for step in study_plan(3, "high", seed=4): + print(step) +``` + +--- + +## API Reference (All Functions) + +All functions accept an optional `seed` parameter for reproducible randomness. + +### `study_tip(topic="math", mood="chaotic", seed=None) -> str` +Returns a humorous study tip for the given topic. + +**Parameters:** +- `topic` (str): The subject area. Options: `"math"`, `"physics"`, `"history"`. Unknown topics default to `"math"`. +- `mood` (str): Currently unused, reserved for future expansion. Default: `"chaotic"`. +- `seed` (int | None): Optional seed for reproducible results. + +**Returns:** A string containing a humorous study tip. + +**Example:** +```python +from studybuddy import study_tip + +print(study_tip("physics", "chaotic")) +# Output: "If it moves, it's probably physics. If not, hit it again." + +print(study_tip("math", seed=42)) +# Output: "If it's too complex, assume x = 0. Problem solved." +``` + +--- + +### `motivate(style="sarcastic", seed=None) -> str` +Returns a motivational or sarcastic message to keep you going. + +**Parameters:** +- `style` (str): The tone of motivation. Options: `"sarcastic"`, `"genuine"`. Unknown styles default to `"sarcastic"`. +- `seed` (int | None): Optional seed for reproducible results. + +**Returns:** A string containing a motivational message. + +**Example:** +```python +from studybuddy import motivate + +print(motivate("sarcastic")) +# Output: "Remember: diamonds are made under pressure. So start panicking." + +print(motivate("genuine")) +# Output: "One page at a time — just keep going." +``` + +--- + +### `excuse(reason="homework", seed=None) -> str` +Returns a funny excuse for various academic mishaps. + +**Parameters:** +- `reason` (str): The situation needing an excuse. Options: `"homework"`, `"late"`, `"exam"`. Unknown reasons default to `"homework"`. +- `seed` (int | None): Optional seed for reproducible results. + +**Returns:** A string containing a humorous excuse. + +**Example:** +```python +from studybuddy import excuse + +print(excuse("homework")) +# Output: "My cat deleted my assignment. She's learning cybersecurity." + +print(excuse("exam")) +# Output: "I didn't fail. I just found 99 ways that didn't work." + +print(excuse("late")) +# Output: "My Wi-Fi connected to another dimension." +``` + +--- + +### `study_plan(hours=3, caffeine_level="high", seed=None) -> list[str]` +Generates a humorous study plan with steps. + +**Parameters:** +- `hours` (int): Number of hours to plan for. Range: 1–5 (values above 5 are clamped to 5). Default: 3. +- `caffeine_level` (str): Caffeine consumption level. Options: `"low"`, `"high"`. When `"high"`, adds coffee-related steps. Default: `"high"`. +- `seed` (int | None): Optional seed for reproducible results. + +**Returns:** A list of strings, each representing a study step. + +**Example:** +```python +from studybuddy import study_plan + +plan = study_plan(3, "high", seed=4) +for step in plan: + print(step) +# Output: +# Step 1: Drink more coffee. Make coffee. +# Step 2: Drink more coffee. Open your notes. +# Step 3: Panic productively for 90 minutes. + +# With low caffeine +plan = study_plan(2, "low", seed=10) +for step in plan: + print(step) +# Output: +# Step 1: Reward yourself with a snack break. +# Step 2: Google half the material. +``` + +--- + +## Example Program + +See the complete working example at [`example.py`](./example.py): +```python +from studybuddy import study_tip, motivate, excuse, study_plan + +def main(): + print("=== StudyBuddy Demo ===") + print("\nStudy Tip:", study_tip("physics", "chaotic")) + print("\nMotivation:", motivate("sarcastic")) + print("\nExcuse:", excuse("homework")) + print("\nStudy Plan:") + for step in study_plan(3, "high", seed=4): + print(" -", step) + +if __name__ == "__main__": + main() +``` + +**Run it:** +```bash +python example.py +``` + +**Sample output:** +``` +=== StudyBuddy Demo === + +Study Tip: If it moves, it's probably physics. If not, hit it again. + +Motivation: Remember: diamonds are made under pressure. So start panicking. + +Excuse: My cat deleted my assignment. She's learning cybersecurity. + +Study Plan: + - Step 1: Drink more coffee. Make coffee. + - Step 2: Drink more coffee. Open your notes. + - Step 3: Panic productively for 90 minutes. +``` + +--- + +## Contributing + +We welcome contributions! Follow this workflow to contribute to the project. + +### Set up your development environment + +Clone the repository: +```bash +git clone https://github.com/swe-students-fall2025/3-python-package-team_cedar.git +cd 3-python-package-team_cedar +``` + +Create a virtual environment: +```bash +# Option 1: venv (recommended) +python -m venv .venv +source .venv/bin/activate # On Windows: .venv\Scripts\activate + +# Option 2: Pipenv +pip install pipenv +pipenv install --dev +pipenv shell +``` + +Install dependencies: +```bash +pip install -U pip +pip install -e . pytest build twine +``` + +### Run tests +```bash +pytest -q +``` + +All tests should pass before submitting a pull request. + +### Build the package +```bash +python -m build +``` + +This creates distribution files in the `./dist` directory. + +### Publish to PyPI (maintainer only) +```bash +twine upload dist/* +``` + +### Git workflow for new features + +1. **Create a feature branch:** +```bash +git switch -c feat/your-feature-name +``` + +2. **Make changes and add tests** for any new functionality + +3. **Commit your changes:** +```bash +git add -A +git commit -m "feat(core): add your feature description" +``` + +4. **Push to GitHub:** +```bash +git push -u origin feat/your-feature-name +``` + +5. **Open a Pull Request** on GitHub +6. **Request a teammate review** +7. **After approval, merge** into `main` +8. **Delete your feature branch** + + +## Continuous Integration + +Every pull request triggers automated testing via GitHub Actions on **Python 3.10** and **3.11**. + +The CI badge at the top of this README shows the current build status. + +**Workflow file:** [`.github/workflows/event-logger.yml`](.github/workflows/event-logger.yml) + +--- + +## Team Cedar + +| Name | GitHub | +|------|--------| +| Nicole Zhang | [@chzzznn](https://github.com/chzzznn) | +| Kylie Lin | [@kylin1209](https://github.com/kylin1209) | +| Sean Tang | [@plant445](https://github.com/plant445) | + +--- + +## PyPI Package + +**https://pypi.org/project/studybuddy-teamcedar/0.8.0/** + +--- + +## License + +MIT — do cool things responsibly (and sarcastically). \ No newline at end of file diff --git a/example.py b/example.py new file mode 100644 index 0000000..f8baa88 --- /dev/null +++ b/example.py @@ -0,0 +1,48 @@ +from studybuddy import ( + study_tip, motivate, excuse, study_plan, allocate_time, + roast, break_idea, pomodoro_schedule, study_playlist, + deadline_reminder, pep_talk, affirmation, challenge +) + + +def main(): + print("=== StudyBuddy Demo ===") + + print("\n📘 Study Tip:", study_tip("physics", "chaotic")) + + print("\n💬 Motivation:", motivate("sarcastic")) + + print("\n🙈 Excuse:", excuse("homework")) + + print("\n🧠 Study Plan:") + for step in study_plan(3, "high", seed=4): + print(" -", step) + print("\n") + + + print("\n Roast:", roast("cs", intensity=7)) + + print("\n☕ Break Idea:", break_idea(10, "walk")) + + print("\n⏱️ Pomodoro Schedule:") + for s in pomodoro_schedule(4): + print(" -", s) + + print("\n🎧 Study Playlist:") + for p in study_playlist("focus", 3): + print(" -", p) + + print("\n⏳ Deadline Reminder:", deadline_reminder(5, "motivational")) + + print("\n💪 Pep Talk:", pep_talk(name="Sean", goal="finish your project", theme="tough_love")) + + print("\n🌈 Affirmation:", affirmation()) + + print("\n🎯 Challenge:", challenge()) + + print("\n🕒 Time Allocation:") + print(allocate_time({"Math-UA 101": 3, "CSCI-UA 480": 2, "CSCI-UA 467": 1}, total_minutes=125, min_chunk=5)) + + +if __name__ == "__main__": + main() diff --git a/pyproject.toml b/pyproject.toml new file mode 100644 index 0000000..7bffe1d --- /dev/null +++ b/pyproject.toml @@ -0,0 +1,19 @@ +[build-system] +requires = ["setuptools>=61.0"] +build-backend = "setuptools.build_meta" + +[project] +name = "studybuddy_teamcedar" +version = "0.8.0" +description = "A lighthearted Python package for funny study tips, excuses, and motivation." +readme = "README.md" +authors = [{name = "Team Cedar"}] +license = {text = "MIT"} +requires-python = ">=3.10" +dependencies = [] + +[project.scripts] +studybuddy = "studybuddy.cli:main" + +[tool.pytest.ini_options] +testpaths = ["tests"] diff --git a/setup.cfg b/setup.cfg new file mode 100644 index 0000000..53ec924 --- /dev/null +++ b/setup.cfg @@ -0,0 +1,2 @@ +[metadata] +name = studybuddy_teamcedar diff --git a/studybuddy/__init__.py b/studybuddy/__init__.py new file mode 100644 index 0000000..076a11c --- /dev/null +++ b/studybuddy/__init__.py @@ -0,0 +1,12 @@ +"""studybuddy — Your (unhelpfully) helpful study companion.""" + +from .core import ( + study_tip, motivate, excuse, study_plan, roast, break_idea, + pomodoro_schedule, study_playlist, deadline_reminder, pep_talk, affirmation, challenge, allocate_time +) + +__all__ = [ + "study_tip", "motivate", "excuse", "study_plan", "roast", "break_idea", + "pomodoro_schedule", "study_playlist", "deadline_reminder", "pep_talk", "affirmation", "challenge", "allocate_time" +] +__version__ = "0.3.0" diff --git a/studybuddy/cli.py b/studybuddy/cli.py new file mode 100644 index 0000000..8cb4a71 --- /dev/null +++ b/studybuddy/cli.py @@ -0,0 +1,212 @@ +import argparse, json +from . import ( + study_tip, motivate, excuse, study_plan, + roast, break_idea, pomodoro_schedule, study_playlist, deadline_reminder, pep_talk, affirmation, challenge, + allocate_time +) +from argparse import RawTextHelpFormatter + + +def main(): + epilog = ( + "Examples:\n" + " studybuddy tip --topic algorithms --seed 3\n" + " studybuddy motivate --style genuine --seed 1\n" + " studybuddy excuse --reason \"missed deadline\" --seed 2\n" + " studybuddy plan --hours 3 --caffeine high --seed 1\n" + " studybuddy roast --topic cs --intensity 5 --seed 7\n" + " studybuddy break --minutes 5 --activity stretch --seed 0\n" + " studybuddy pomodoro --sessions 2 --work 25 --break 5 --long 15\n" + " studybuddy playlist --mood focus --n 4 --seed 11\n" + " studybuddy deadline --hours_left 10 --tone funny\n" + " studybuddy pep --name Gavin --goal \"study 2 hours\" --theme wholesome --seed 9\n" + " studybuddy affirm --seed 4\n" + " studybuddy challenge --seed 6\n" + " studybuddy allocate --minutes 120 --min-chunk 10 --topic DSA:5 --topic OS:3 --topic Math:2\n" + "\nTip: run `studybuddy -h` for subcommand-specific help.\n" + ) + + p = argparse.ArgumentParser( + prog="studybuddy", + description="StudyBuddy CLI", + epilog=epilog, + formatter_class=RawTextHelpFormatter + ) + sub = p.add_subparsers(dest="cmd") + + s1 = sub.add_parser( + "tip", + help="Get a study tip", + description="Return a witty/constructive study tip.\n\nExample:\n studybuddy tip --topic algorithms --seed 3", + formatter_class=RawTextHelpFormatter, + ) + # s1 = sub.add_parser("tip"); + s1.add_argument("--topic", default="math", help="Topic, e.g., algorithms/databases (default: math)") + s1.add_argument("--seed", type=int, help="Random seed for reproducibility") + + s2 = sub.add_parser( + "motivate", + help="Get a motivation line", + description="Different styles: genuine / sarcastic / toughlove.\n\nExample:\n studybuddy motivate --style genuine --seed 1", + formatter_class=RawTextHelpFormatter, + ) + # s2 = sub.add_parser("motivate"); + s2.add_argument("--style", default="sarcastic", help="Style (default: sarcastic)") + s2.add_argument("--seed", type=int, help="Random seed") + + s3 = sub.add_parser( + "excuse", + help="Generate a cheeky excuse", + description="Generate a fun excuse for a reason.\n\nExample:\n studybuddy excuse --reason \"missed deadline\" --seed 2", + formatter_class=RawTextHelpFormatter, + ) + # s3 = sub.add_parser("excuse"); + s3.add_argument("--reason", default="homework", help="Reason/context (default: homework)") + s3.add_argument("--seed", type=int, help="Random seed") + + s4 = sub.add_parser( + "plan", + help="Create a study plan", + description="Plan steps for a number of hours.\n\nExample:\n studybuddy plan --hours 3 --caffeine high --seed 1", + formatter_class=RawTextHelpFormatter, + ) + # s4 = sub.add_parser("plan"); + s4.add_argument("--hours", type=int, default=3, help="Study hours (default: 3)") + s4.add_argument("--caffeine", default="high", help="low/medium/high (default: high)") + s4.add_argument("--seed", type=int, help="Random seed") + + s5 = sub.add_parser( + "roast", + help="Get a playful roast", + description="Playful roast (for fun only!).\n\nExample:\n studybuddy roast --topic cs --intensity 5 --seed 7", + formatter_class=RawTextHelpFormatter, + ) + # s5 = sub.add_parser("roast"); + s5.add_argument("--topic", default="cs", help="Topic to roast (default: cs)") + s5.add_argument("--intensity", type=int, default=5, help="1-10 (default: 5)") + s5.add_argument("--seed", type=int, help="Random seed") + + s6 = sub.add_parser( + "break", + help="Break-time idea", + description="Suggest a tiny break activity.\n\nExample:\n studybuddy break --minutes 5 --activity stretch --seed 0", + formatter_class=RawTextHelpFormatter, + ) + # s6 = sub.add_parser("break"); + s6.add_argument("--minutes", type=int, default=5, help="Break minutes (default: 5)") + s6.add_argument("--activity", default="stretch", help="Activity name (default: stretch)") + s6.add_argument("--seed", type=int, help="Random seed") + + s7 = sub.add_parser( + "pomodoro", + help="Generate a pomodoro schedule", + description="Pomodoro cycles with custom durations.\n\nExample:\n studybuddy pomodoro --sessions 2 --work 25 --break 5 --long 15", + formatter_class=RawTextHelpFormatter, + ) + # s7 = sub.add_parser("pomodoro"); + s7.add_argument("--sessions", type=int, default=4, help="Number of sessions (default: 4)") + s7.add_argument("--work", type=int, default=25, help="Work minutes per session (default: 25)") + s7.add_argument("--break", dest="brk", type=int, default=5, help="Short break minutes (default: 5)") + s7.add_argument("--long", type=int, default=15, help="Long break minutes (default: 15)") + + s8 = sub.add_parser( + "playlist", + help="Suggest a study playlist", + description="Return a small themed playlist.\n\nExample:\n studybuddy playlist --mood focus --n 4 --seed 11", + formatter_class=RawTextHelpFormatter, + ) + # s8 = sub.add_parser("playlist"); + s8.add_argument("--mood", default="focus", help="Mood/genre (default: focus)") + s8.add_argument("--n", type=int, default=3, help="Number of tracks (default: 3)") + s8.add_argument("--seed", type=int, help="Random seed") + + s9 = sub.add_parser( + "deadline", + help="Deadline reminder", + description="Generate a fun deadline reminder line.\n\nExample:\n studybuddy deadline --hours_left 10 --tone funny", + formatter_class=RawTextHelpFormatter, + ) + # s9 = sub.add_parser("deadline"); + s9.add_argument("--hours_left", type=int, required=True, help="Hours left to deadline (required)") + s9.add_argument("--tone", default="funny", help="Tone (default: funny)") + + s10 = sub.add_parser( + "pep", + help="Get a short pep talk", + description="Short pep talk with name/goal/theme.\n\nExample:\n studybuddy pep --name Gavin --goal \"study 2 hours\" --theme wholesome --seed 9", + formatter_class=RawTextHelpFormatter, + ) + # s10 = sub.add_parser("pep"); + s10.add_argument("--name", default="friend", help="Name (default: friend)") + s10.add_argument("--goal", default="study 2 hours", help="Goal text (default: study 2 hours)") + s10.add_argument("--theme", default="wholesome", help="Theme (default: wholesome)") + s10.add_argument("--seed", type=int, help="Random seed") + + s11 = sub.add_parser( + "affirm", + help="Get an affirmation", + description="Short affirmation.\n\nExample:\n studybuddy affirm --seed 4", + formatter_class=RawTextHelpFormatter, + ) + # s11 = sub.add_parser("affirm") + s11.add_argument("--seed", type=int, help="Random seed") + + s12 = sub.add_parser( + "challenge", + help="Get a challenge", + description="Small challenge.\n\nExample:\n studybuddy challenge --seed 6", + formatter_class=RawTextHelpFormatter, + ) + # s12 = sub.add_parser("challenge") + s12.add_argument("--seed", type=int, help="Random seed") + + s13 = sub.add_parser( + "allocate", + help="Allocate minutes across topics by weight", + description=( + "Allocate study minutes across topics by weight; rounds to min-chunk and preserves total.\n\n" + "Multiple topics via repeated --topic flags in the form NAME:WEIGHT.\n" + "Example:\n studybuddy allocate --minutes 120 --min-chunk 10 --topic DSA:5 --topic OS:3 --topic Math:2" + ), + formatter_class=RawTextHelpFormatter, + ) + s13.add_argument("--minutes", type=int, required=True, help="Total minutes to distribute") + s13.add_argument("--min-chunk", type=int, default=5, dest="min_chunk", + help="Round each topic to multiple of this (default: 5)") + s13.add_argument("--topic", action="append", default=[], metavar="NAME:WEIGHT", + help="Repeatable. Example: --topic DSA:5 --topic OS:3") + + args = p.parse_args() + if not args.cmd: + p.print_help() + return + + if args.cmd == "tip": print(study_tip(args.topic, "chaotic", args.seed)) + elif args.cmd == "motivate": print(motivate(args.style, args.seed)) + elif args.cmd == "excuse": print(excuse(args.reason, args.seed)) + elif args.cmd == "plan": print("\n".join(study_plan(args.hours, args.caffeine, args.seed))) + elif args.cmd == "roast": print(roast(args.topic, args.intensity, args.seed)) + elif args.cmd == "break": print(break_idea(args.minutes, args.activity, args.seed)) + elif args.cmd == "pomodoro": print("\n".join(pomodoro_schedule(args.sessions, args.work, args.brk, args.long))) + elif args.cmd == "playlist": print(json.dumps(study_playlist(args.mood, args.n, args.seed))) + elif args.cmd == "deadline": print(deadline_reminder(args.hours_left, args.tone)) + elif args.cmd == "pep": print(pep_talk(args.name, args.goal, args.theme, args.seed)) + elif args.cmd == "affirm": + print(affirmation(args.seed)) + elif args.cmd == "challenge": + print(challenge(args.seed)) + elif args.cmd == "allocate": + topics = {} + for item in args.topic: + try: + name, w = item.split(":", 1) + topics[name.strip()] = int(w) + except Exception: + print(f"Invalid --topic '{item}'. Expected NAME:WEIGHT") + return + print(allocate_time(topics, args.minutes, args.min_chunk)) + + +if __name__ == "__main__": + main() + diff --git a/studybuddy/core.py b/studybuddy/core.py new file mode 100644 index 0000000..ef1dee0 --- /dev/null +++ b/studybuddy/core.py @@ -0,0 +1,301 @@ +import random +from typing import Dict, List + +# Core data +_TIPS = { + "math": [ + "If it’s too complex, assume x = 0. Problem solved.", + "Numbers never lie, but you might when asked if you understand them.", + ], + "history": [ + "If you forget the date, just say 'around that time.'", + "History repeats itself. So if you fail this exam, you’ll get another chance.", + ], + "physics": [ + "If it moves, it’s probably physics. If not, hit it again.", + "Remember: every action has an equal and opposite procrastination.", + ], +} + +_MOTIVATIONS = { + "sarcastic": [ + "Remember: diamonds are made under pressure. So start panicking.", + "Dream big, nap often.", + "You can do anything! Except maybe that.", + ], + "genuine": [ + "You’ve got this! Probably. Maybe. Let’s hope.", + "One page at a time — just keep going.", + "Even small progress counts. Keep at it.", + ], +} + +_EXCUSES = { + "homework": [ + "My cat deleted my assignment. She’s learning cybersecurity.", + "Google Docs went into witness protection.", + ], + "late": [ + "My Wi-Fi connected to another dimension.", + "I was stuck in traffic... on the information highway.", + ], + "exam": [ + "I didn’t fail. I just found 99 ways that didn’t work.", + "The test was multiple guess, and I guessed wrong multiple times.", + ], +} + +_STEPS = [ + "Make coffee.", + "Open your notes.", + "Panic productively for 90 minutes.", + "Reward yourself with a snack break.", + "Google half the material.", +] + +_ROASTS = { + "cs": [ + "Your code is like your dating life - full of bugs and nobody wants to debug it.", + "You code like you're trying to solve world hunger... one syntax error at a time.", + "Your algorithm is so inefficient, it makes bubble sort look like a speed demon.", + "I've seen more organized code in a toddler's finger painting.", + "Your variable names are more confusing than IKEA instructions.", + ], + "math": [ + "Your math skills are so bad, calculators file restraining orders.", + "You approach equations like they're written in ancient hieroglyphs.", + "Your algebra is weaker than decaf coffee on a Monday morning.", + "You solve problems like you're playing mathematical roulette.", + "Your geometry is so off, even abstract art looks realistic in comparison.", + ], + "physics": [ + "Your understanding of physics violates more laws than a parking ticket collector.", + "You handle momentum like you handle your life - poorly.", + "Your grasp of gravity is the only thing keeping your grades down.", + "You treat thermodynamics like it's thermo-optional-amics.", + "Your physics solutions defy more laws than they follow.", + ], +} + +_BREAK_ACTIVITIES = { + "stretch": [ + "Do the 'I've been sitting too long' neck roll dance.", + "Attempt yoga poses that would make a pretzel jealous.", + "Stretch like a cat who just discovered the concept of flexibility.", + "Channel your inner flamingo with some one-legged stretches.", + ], + "walk": [ + "Take a relaxing walk around your room (or building if you're feeling fancy).", + "Practice your 'deep in thought' walk around the block.", + "Walk to the kitchen and contemplate the meaning of snacks.", + "Do the 'I need fresh air but also Wi-Fi' walk of balance.", + ], + "snack": [ + "Fuel up with brain food (chips count as brain food, right?).", + "Have a philosophical discussion with your refrigerator contents.", + "Practice portion control by eating one cookie... at a time... repeatedly.", + "Conduct a scientific taste test of available snacks.", + ], +} + +_PLAYLIST_MOODS = { + "focus": [ + "Lofi Hip Hop Radio - beats to procrastinate/study to", + "Classical Music for People Who Think They're Sophisticated", + "Ambient Sounds That Definitely Won't Put You to Sleep", + ], + "energetic": [ + "Upbeat Songs to Make You Feel Productive (Even If You're Not)", + "High-Energy Tracks for Last-Minute Panic Sessions", + "Songs That Make Cramming Feel Like a Dance Party", + ], + "chill": [ + "Chill vibes only – lo-fi beats to relax to", + "Relaxing acoustic flow for study focus", + "Calm music to help you relax and chill out", + ], +} + +_DEADLINE_MESSAGES = { + "panic": [ + "Time to panic (just a little)! Activate MAXIMUM OVERDRIVE mode!", + "This is fine. Everything is fine. *nervous laughter*", + "Remember: pressure makes diamonds... or nervous breakdowns.", + "It's crunch time! Time to crunch those... study materials.", + ], + "funny": [ + "Deadline approaching faster than your motivation to start working!", + "Time left: {hours} hours. Panic level: Moderate to severe.", + "Your deadline called - it's running fashionably early.", + "Breaking news: Local student discovers deadlines don't extend themselves.", + ], + "motivational": [ + "You've got this! {hours} hours is plenty of time to work miracles!", + "Every hour counts - make them work for you!", + "You're closer to the finish line than you think!", + "Time to show this deadline who's boss!", + ], +} + +_PEP_TALKS = { + "wholesome": [ + "Hey {name}, you're doing great! {goal} is totally achievable.", + "{name}, remember that progress isn't always linear, but you're moving forward!", + "You've got the determination to reach your goal of {goal}, {name}!", + "Every small step towards {goal} counts, {name}. Keep it up!", + ], + "tough_love": [ + "Listen up {name}, {goal} isn't going to happen by itself!", + "{name}, stop making excuses and start making progress on {goal}!", + "You want to achieve {goal}? Then quit talking and start doing, {name}!", + "Reality check, {name}: {goal} requires actual work, not just wishful thinking!", + ], + "funny": [ + "{name}, your goal of {goal} is calling... it wants to know if you're still friends.", + "Hey {name}, {goal} just texted - it's wondering when you'll take it seriously!", + "{name}, your future self is judging your current commitment to {goal}.", + "Breaking news {name}: {goal} is still waiting for you to show up!", + ], +} + +_AFFIRMATIONS = [ + "You are 100% capable of finishing this assignment (eventually).", + "Progress > perfection.", + "You’re not behind — you’re just on your own timeline.", + "Even one line of code counts as productivity!", + "You’re basically the main character of this study session." +] + +_CHALLENGES = [ + "Study 10 pages without checking your phone.", + "Summarize the last topic in one sentence.", + "Do a 5-minute rapid-fire recall session.", + "Write a haiku about your subject.", + "Quiz yourself out loud — bonus points if you sound confident." +] + +# Functions +def _choose(lst, rnd): + return lst[rnd.randrange(len(lst))] + +def study_tip(topic: str = "math", mood: str = "chaotic", seed: int | None = None) -> str: + rnd = random.Random(seed) + tips = _TIPS.get(topic, _TIPS["math"]) + return _choose(tips, rnd) + +def motivate(style: str = "sarcastic", seed: int | None = None) -> str: + rnd = random.Random(seed) + msgs = _MOTIVATIONS.get(style, _MOTIVATIONS["sarcastic"]) + return _choose(msgs, rnd) + +def excuse(reason: str = "homework", seed: int | None = None) -> str: + rnd = random.Random(seed) + excuses = _EXCUSES.get(reason, _EXCUSES["homework"]) + return _choose(excuses, rnd) + +def study_plan(hours: int = 3, caffeine_level: str = "high", seed: int | None = None) -> list[str]: + rnd = random.Random(seed) + plan = [] + for i in range(min(hours, 5)): + step = _choose(_STEPS, rnd) + if caffeine_level == "high" and "coffee" not in step.lower(): + step = "Drink more coffee. " + step + plan.append(f"Step {i+1}: {step}") + return plan + +def roast(topic: str = "cs", intensity: int = 5, seed: int | None = None) -> str: + rnd = random.Random(seed) + roasts = _ROASTS.get(topic, _ROASTS["cs"]) + roast_msg = _choose(roasts, rnd) + + if intensity <= 3: + roast_msg = "gently speaking... " + roast_msg.lower() + elif intensity >= 8: + roast_msg = roast_msg.upper() + " 🔥" + return roast_msg + +def break_idea(minutes: int = 5, activity: str = "stretch", seed: int | None = None) -> str: + rnd = random.Random(seed) + activities = _BREAK_ACTIVITIES.get(activity, _BREAK_ACTIVITIES["stretch"]) + idea = _choose(activities, rnd) + + if activity not in _BREAK_ACTIVITIES: + activity = "stretch" + + if minutes <= 5: + return f"Quick {minutes}-minute break: {idea}" + else: + return f"Extended {minutes}-minute break: take a {activity}! {idea}" + +def pomodoro_schedule(sessions: int = 4, work_minutes: int = 25, break_minutes: int = 5, long_break: int = 15) -> list[str]: + schedule = [] + for i in range(sessions): + schedule.append(f"Session {i+1}: Work for {work_minutes} minutes") + if (i + 1) % 4 == 0 and i < sessions - 1: + schedule.append(f"Long break: {long_break} minutes") + elif i < sessions - 1: + schedule.append(f"Short break: {break_minutes} minutes") + schedule.append("🎉 Pomodoro session complete! Great work!") + return schedule + +def study_playlist(mood: str = "focus", n: int = 3, seed: int | None = None) -> list[str]: + rnd = random.Random(seed) + playlists = _PLAYLIST_MOODS.get(mood, _PLAYLIST_MOODS["focus"]) + selected = [] + for _ in range(n): + selected.append(_choose(playlists, rnd)) + return selected + +def deadline_reminder(hours_left: int, tone: str = "funny") -> str: + messages = _DEADLINE_MESSAGES.get(tone, _DEADLINE_MESSAGES["funny"]) + if hours_left <= 2: + return _DEADLINE_MESSAGES["panic"][0] + rnd = random.Random() + base_msg = _choose(messages, rnd) + return base_msg.format(hours=hours_left) if "{hours}" in base_msg else base_msg + +def pep_talk(name: str = "friend", goal: str = "study 2 hours", theme: str = "wholesome", seed: int | None = None) -> str: + rnd = random.Random(seed) + talks = _PEP_TALKS.get(theme, _PEP_TALKS["wholesome"]) + talk = _choose(talks, rnd) + return talk.format(name=name, goal=goal) + +def affirmation(seed: int | None = None) -> str: + rnd = random.Random(seed) + return _choose(_AFFIRMATIONS, rnd) + +def challenge(seed: int | None = None) -> str: + rnd = random.Random(seed) + return _choose(_CHALLENGES, rnd) + +def allocate_time(topics: Dict[str, int], total_minutes: int, min_chunk: int = 5) -> Dict[str, int]: + if total_minutes < 0 or min_chunk <= 0: + raise ValueError("total_minutes must be >= 0 and min_chunk > 0") + if not topics: + return {} + + weights = {k: max(0, int(v)) for k, v in topics.items()} + total_w = sum(weights.values()) + + if total_w == 0: + avg = total_minutes / max(1, len(weights)) + raw = {k: avg for k in weights} + else: + raw = {k: (total_minutes * w / total_w) for k, w in weights.items()} + + alloc = {k: max(0, int(round(x / min_chunk) * min_chunk)) for k, x in raw.items()} + + diff = total_minutes - sum(alloc.values()) + if diff != 0: + keys = sorted(weights, key=lambda k: weights[k], reverse=True) + step = min_chunk if diff > 0 else -min_chunk + i = 0 + while diff != 0 and keys: + k = keys[i % len(keys)] + if alloc[k] + step >= 0: + alloc[k] += step + diff -= step + i += 1 + if i > 10000: + break + return alloc diff --git a/tests/__init__.py b/tests/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/tests/test_affirmation_challenge.py b/tests/test_affirmation_challenge.py new file mode 100644 index 0000000..7918696 --- /dev/null +++ b/tests/test_affirmation_challenge.py @@ -0,0 +1,34 @@ +from studybuddy import affirmation, challenge + +def test_affirmation_returns_string(): + result = affirmation(seed=1) + assert isinstance(result, str) + assert len(result) > 0 + +def test_affirmation_repeatable_with_seed(): + result1 = affirmation(seed=42) + result2 = affirmation(seed=42) + assert result1 == result2 + + +def test_affirmation_varies_without_seed(): + results = {affirmation() for _ in range(10)} + assert len(results) > 1 + +def test_challenge_returns_string(): + result = challenge(seed=2) + assert isinstance(result, str) + assert "Study" in result or "Quiz" in result or "pages" in result + + +def test_challenge_repeatable_with_seed(): + result1 = challenge(seed=10) + result2 = challenge(seed=10) + assert result1 == result2 + + +def test_challenge_is_deterministic(): + """Ensure challenge() gives consistent output for the same seed.""" + r1 = challenge(seed=7) + r2 = challenge(seed=7) + assert r1 == r2 \ No newline at end of file diff --git a/tests/test_allocate_time.py b/tests/test_allocate_time.py new file mode 100644 index 0000000..434c8ee --- /dev/null +++ b/tests/test_allocate_time.py @@ -0,0 +1,21 @@ +import studybuddy as s + + +def test_allocate_sum_and_min_chunk(): + topics = {"algo": 3, "db": 2, "net": 1} + alloc = s.allocate_time(topics, total_minutes=125, min_chunk=5) + assert sum(alloc.values()) == 125 + assert all(v % 5 == 0 for v in alloc.values()) + assert set(alloc) == set(topics) + + +def test_allocate_respects_weights(): + topics = {"hard": 5, "easy": 1} + alloc = s.allocate_time(topics, total_minutes=60, min_chunk=5) + assert alloc["hard"] > alloc["easy"] + + +def test_allocate_edge_cases(): + assert s.allocate_time({}, 50) == {} + alloc = s.allocate_time({"one": 0}, 0) + assert alloc["one"] == 0 diff --git a/tests/test_break_idea.py b/tests/test_break_idea.py new file mode 100644 index 0000000..0dd929a --- /dev/null +++ b/tests/test_break_idea.py @@ -0,0 +1,16 @@ +from studybuddy import core + +def test_break_idea_default(): + idea = core.break_idea(seed=0) + assert "break" in idea.lower() + assert any(word in idea.lower() for word in ["stretch", "minute"]) + +def test_break_idea_long(): + idea = core.break_idea(minutes=10, activity="walk", seed=1) + assert "extended" in idea.lower() + assert "walk" in idea.lower() + +def test_break_invalid_activity_defaults_to_stretch(): + idea = core.break_idea(activity="invalid", seed=2) + assert "stretch" in idea.lower() or "neck roll" in idea.lower() + diff --git a/tests/test_deadline_reminder.py b/tests/test_deadline_reminder.py new file mode 100644 index 0000000..266f7d1 --- /dev/null +++ b/tests/test_deadline_reminder.py @@ -0,0 +1,15 @@ +from studybuddy import core + +def test_deadline_funny_message_contains_hours(): + msg = core.deadline_reminder(hours_left=10, tone="funny") + if "{hours}" in msg.lower(): + assert "10" in msg + + +def test_deadline_panic_mode_for_low_hours(): + msg = core.deadline_reminder(hours_left=1, tone="funny") + assert "panic" in msg.lower() or "fine" in msg.lower() + +def test_deadline_invalid_tone_defaults_to_funny(): + msg = core.deadline_reminder(hours_left=5, tone="nonexistent") + assert isinstance(msg, str) diff --git a/tests/test_excuse.py b/tests/test_excuse.py new file mode 100644 index 0000000..1d376f5 --- /dev/null +++ b/tests/test_excuse.py @@ -0,0 +1,14 @@ +from studybuddy import excuse + +def test_excuse_type(): + assert isinstance(excuse("homework"), str) + +def test_excuse_reason_variation(): + a = excuse("exam", seed=1) + b = excuse("homework", seed=1) + assert a != b + +def test_excuse_deterministic(): + a = excuse("late", seed=3) + b = excuse("late", seed=3) + assert a == b diff --git a/tests/test_motivate.py b/tests/test_motivate.py new file mode 100644 index 0000000..00e032a --- /dev/null +++ b/tests/test_motivate.py @@ -0,0 +1,14 @@ +from studybuddy import motivate + +def test_motivate_returns_string(): + assert isinstance(motivate("genuine"), str) + +def test_motivate_styles(): + sarcastic = motivate("sarcastic", seed=1) + genuine = motivate("genuine", seed=1) + assert sarcastic != genuine + +def test_motivate_deterministic(): + a = motivate("sarcastic", seed=2) + b = motivate("sarcastic", seed=2) + assert a == b diff --git a/tests/test_pep_talk.py b/tests/test_pep_talk.py new file mode 100644 index 0000000..235dc14 --- /dev/null +++ b/tests/test_pep_talk.py @@ -0,0 +1,16 @@ +from studybuddy import core + +def test_pep_talk_default(): + msg = core.pep_talk(name="Kylie", goal="finish this project", seed=0) + assert "Kylie" in msg + assert "finish this project" in msg + +def test_pep_talk_funny(): + msg = core.pep_talk(name="Alex", goal="ace the exam", theme="funny", seed=1) + assert "Alex" in msg + assert "ace the exam" in msg + +def test_pep_talk_tough_love(): + msg = core.pep_talk(name="Sam", goal="study 2 hours", theme="tough_love", seed=2) + assert "Sam" in msg + assert "study 2 hours" in msg diff --git a/tests/test_pomodoro_schedule.py b/tests/test_pomodoro_schedule.py new file mode 100644 index 0000000..9112318 --- /dev/null +++ b/tests/test_pomodoro_schedule.py @@ -0,0 +1,14 @@ +from studybuddy import core + +def test_pomodoro_basic_structure(): + sched = core.pomodoro_schedule(sessions=2, work_minutes=25, break_minutes=5) + assert sched[0].startswith("Session 1") + assert sched[-1].startswith("🎉") + +def test_pomodoro_includes_breaks(): + sched = core.pomodoro_schedule(sessions=3) + assert any("Short break" in s for s in sched) + +def test_pomodoro_long_break_every_four_sessions(): + sched = core.pomodoro_schedule(sessions=8) + assert any("Long break" in s for s in sched) diff --git a/tests/test_roast.py b/tests/test_roast.py new file mode 100644 index 0000000..ff32d14 --- /dev/null +++ b/tests/test_roast.py @@ -0,0 +1,16 @@ +import re +from studybuddy import core + +def test_roast_default_behavior(): + msg = core.roast(seed=0) + assert isinstance(msg, str) + assert any(word in msg.lower() for word in ["code", "bugs", "algorithm", "variable"]) + +def test_roast_low_intensity(): + msg = core.roast(intensity=2, seed=1) + assert msg.lower().startswith("gently speaking") + assert msg == msg.lower() # lowercased for gentle tone + +def test_roast_high_intensity(): + msg = core.roast(intensity=9, seed=2) + assert msg.isupper() or msg.endswith("🔥") diff --git a/tests/test_study_plan.py b/tests/test_study_plan.py new file mode 100644 index 0000000..2e3013d --- /dev/null +++ b/tests/test_study_plan.py @@ -0,0 +1,12 @@ +from studybuddy import study_plan + +def test_study_plan_list(): + assert isinstance(study_plan(2), list) + +def test_study_plan_length(): + assert len(study_plan(4)) == 4 + +def test_study_plan_deterministic(): + a = study_plan(3, "high", seed=5) + b = study_plan(3, "high", seed=5) + assert a == b diff --git a/tests/test_study_playlist.py b/tests/test_study_playlist.py new file mode 100644 index 0000000..66f595a --- /dev/null +++ b/tests/test_study_playlist.py @@ -0,0 +1,14 @@ +from studybuddy import core + +def test_study_playlist_default(): + pl = core.study_playlist(seed=0) + assert isinstance(pl, list) + assert len(pl) == 3 + +def test_study_playlist_chill_mode(): + pl = core.study_playlist(mood="chill", n=2, seed=1) + assert all("chill" in p.lower() or "relax" in p.lower() for p in pl) + +def test_study_playlist_length_matches_request(): + pl = core.study_playlist(n=5, seed=2) + assert len(pl) == 5 diff --git a/tests/test_study_tips.py b/tests/test_study_tips.py new file mode 100644 index 0000000..a592d53 --- /dev/null +++ b/tests/test_study_tips.py @@ -0,0 +1,12 @@ +from studybuddy import study_tip + +def test_study_tip_returns_string(): + assert isinstance(study_tip("math", "chaotic"), str) + +def test_study_tip_fallback_category(): + assert " " in study_tip("unknown", "chaotic") + +def test_study_tip_deterministic(): + a = study_tip("history", "lazy", seed=42) + b = study_tip("history", "lazy", seed=42) + assert a == b