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..7b4e507 100644 --- a/README.md +++ b/README.md @@ -1,3 +1,303 @@ -# 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) | +| Jeaanmarck Ceant | [@jrc9921](https://github.com/Jeanmarck12) | + +--- + +## 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..06e6b7a --- /dev/null +++ b/studybuddy/__init__.py @@ -0,0 +1,56 @@ +"""studybuddy — Your (unhelpfully) helpful study companion.""" + +"""studybuddy — Your (unhelpfully) helpful study companion.""" + +from .core import ( + study_tip, + motivate, + excuse, + study_plan, + roast, + compliment, + break_tip, + pomodoro_plan, + playlist, + secret, + list_topics, + list_styles, + list_reasons, + list_vibes, + affirmation, + challenge, + allocate_time, + break_idea, + deadline_reminder, + pep_talk, + pomodoro_schedule, + study_playlist, +) + +__all__ = [ + "study_tip", + "motivate", + "excuse", + "study_plan", + "roast", + "compliment", + "break_tip", + "pomodoro_plan", + "playlist", + "secret", + "list_topics", + "list_styles", + "list_reasons", + "list_vibes", + "affirmation", + "challenge", + "allocate_time", + "break_idea", + "deadline_reminder", + "pep_talk", + "pomodoro_schedule", + "study_playlist", +] + + +__version__ = "0.1.0" diff --git a/studybuddy/cli.py b/studybuddy/cli.py new file mode 100644 index 0000000..da42687 --- /dev/null +++ b/studybuddy/cli.py @@ -0,0 +1,120 @@ +import argparse, json +from . import ( + study_tip, + motivate, + excuse, + study_plan, + roast, + compliment, + break_tip, + pomodoro_plan, + playlist, + secret, + list_topics, + list_styles, + list_reasons, + list_vibes, +) + +def main() -> None: + parser = argparse.ArgumentParser(prog="studybuddy", description="StudyBuddy CLI") + sub = parser.add_subparsers(dest="cmd", required=True) + + # tip + s = sub.add_parser("tip", help="Get a humorous study tip") + s.add_argument("--topic", default="math") + s.add_argument("--seed", type=int) + + # motivate + s = sub.add_parser("motivate", help="Get motivation: sarcastic | genuine | mixed") + s.add_argument("--style", default="mixed") + s.add_argument("--seed", type=int) + + # excuse + s = sub.add_parser("excuse", help="Get a funny excuse") + s.add_argument("--reason", default="homework") + s.add_argument("--seed", type=int) + + # plan + s = sub.add_parser("plan", help="Generate a study plan") + s.add_argument("--hours", type=int, default=3) + s.add_argument("--caffeine", default="high", choices=["low", "high"]) + s.add_argument("--seed", type=int) + + # roast / compliment / break + s = sub.add_parser("roast", help="Receive a playful roast") + s.add_argument("--seed", type=int) + + s = sub.add_parser("compliment", help="Receive a kind compliment") + s.add_argument("--seed", type=int) + + s = sub.add_parser("break", help="Get a mini break idea") + s.add_argument("--seed", type=int) + + # pomodoro + s = sub.add_parser("pomodoro", help="Build a Pomodoro plan") + s.add_argument("--sessions", type=int, default=3) + s.add_argument("--seed", type=int) + + # playlist + s = sub.add_parser("playlist", help="Suggest study playlists for a vibe") + s.add_argument("--vibe", default="focus") + s.add_argument("-n", "--n", type=int, default=3) + s.add_argument("--seed", type=int) + + # secret + s = sub.add_parser("secret", help="Reveal a tiny easter egg") + s.add_argument("--seed", type=int) + + # lists (discoverability) + sub.add_parser("list-topics", help="Show valid topics for 'tip'") + sub.add_parser("list-styles", help="Show valid styles for 'motivate'") + sub.add_parser("list-reasons", help="Show valid reasons for 'excuse'") + sub.add_parser("list-vibes", help="Show valid playlist vibes") + + args = parser.parse_args() + + 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": + for line in study_plan(args.hours, args.caffeine, args.seed): + print(line) + + elif args.cmd == "roast": + print(roast(args.seed)) + + elif args.cmd == "compliment": + print(compliment(args.seed)) + + elif args.cmd == "break": + print(break_tip(args.seed)) + + elif args.cmd == "pomodoro": + for line in pomodoro_plan(args.sessions, args.seed): + print(line) + + elif args.cmd == "playlist": + for item in playlist(args.vibe, args.n, args.seed): + print(f"- {item}") + + elif args.cmd == "secret": + print(secret(args.seed)) + + elif args.cmd == "list-topics": + print("\n".join(list_topics())) + + elif args.cmd == "list-styles": + print("\n".join(list_styles())) + + elif args.cmd == "list-reasons": + print("\n".join(list_reasons())) + + elif args.cmd == "list-vibes": + print("\n".join(list_vibes())) \ No newline at end of file diff --git a/studybuddy/core.py b/studybuddy/core.py new file mode 100644 index 0000000..008e0b4 --- /dev/null +++ b/studybuddy/core.py @@ -0,0 +1,512 @@ +import random +from typing import Optional, 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_BY_TOPIC = { + "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." +] + + +_ROASTS = [ + "Your study habits are like Wi-Fi at a coffee shop — weak and unreliable.", + "If procrastination was a sport, you'd be an Olympian.", + "You're doing amazing… at finding new ways to avoid studying.", + "Please dont tell me your actually trying??", +] + + +_COMPLIMENTS = [ + "You're sharper than your pencil!", + "Brains and beauty — unfair combo.", + "You make studying look… almost cool.", + "Wow, is this what a natural genius looks like?", + "we need to talk about how amazing you are", +] + +_BREAKS = [ + "Stretch like you’re reaching for better grades.", + "Take a water break — hydration is brain fuel.", + "Do nothing for 5 minutes. You’ve earned it.", +] + + +# --- Playlists --- +_PLAYLISTS = { + "focus": [ + "Chillhop Essentials – Instrumental beats", + "Deep Focus – steady no-lyrics electronica", + "Coding Mode – subtle pulses, low distraction", + "Brain Food – downtempo, minimal vocals", + "Lo-Fi Beats – mellow study loops", + ], + "lofi": [ + "lofi hip hop radio – beats to relax/study to", + "Late Night Lo-Fi – rainy window vibes", + "Cafe Lofi – warm, cozy instrumentals", + "Lo-Fi Piano – soft keys + vinyl crackle", + "Study & Sleep – ultra-gentle loops", + ], + "hype": [ + "Beast Mode – high-energy gym bangers", + "EDM Bangers – tempo > productivity (maybe)", + "Trap Motivation – bass + bravado", + "Pop Power – hooks that keep you awake", + "Drill & Focus(?) – questionable, but effective", + ], + "classical": [ + "Bach: The Well-Tempered Clavier", + "Mozart for Studying – piano concertos", + "Debussy & Satie – airy impressionism", + "Baroque for Focus – steady rhythms", + "Ludovico Einaudi – modern minimal piano", + ], + "ambient": [ + "Brian Eno – Music for Airports", + "Max Richter – Sleep (selected)", + "Carbon Based Lifeforms – soft space ambient", + "Nils Frahm – solo ambient piano", + "Rain & Brown Noise – pure background", + ], + +} + +_VALID_CAFFEINE = {"low", "high"} + +# internal helpers +def _rng(seed: Optional[int]) -> random.Random: + """Private RNG factory to keep seeding consistent everywhere.""" + return random.Random(seed) + +def _choose(lst: List[str], rnd: random.Random) -> str: + """Safe random chooser (assumes non-empty list).""" + return lst[rnd.randrange(len(lst))] + +def study_tip(topic: str = "math", mood: str = "chaotic", seed: int | None = None) -> str: + """Return a humorous study tip.""" + rnd = random.Random(seed) + tips = _TIPS.get(topic, _TIPS["math"]) + return _choose(tips, rnd) + +def motivate(style: str = "sarcastic", seed: int | None = None) -> str: + """Return a motivational or sarcastic message.""" + rnd = random.Random(seed) + msgs = _MOTIVATIONS.get(style, _MOTIVATIONS["sarcastic"]) + return _choose(msgs, rnd) + +def excuse(reason: str = "homework", seed: int | None = None) -> str: + """Return a funny excuse for school mishaps.""" + 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]: + """Return a list of 'study plan' steps.""" + rnd = random.Random(seed) + plan = [] + for i in range(hours): + 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(intensity: int = 5, seed: Optional[int] = None) -> str: + rnd = _rng(seed) + + # Base CS-style roast + base = _choose(_ROASTS_BY_TOPIC["cs"], rnd) + + if intensity <= 3: + # must be fully lowercase and start with "gently speaking" + return "gently speaking… " + base.lower() + + if intensity >= 8: + if rnd.random() < 0.5: + return base.upper() + else: + return base + " 🔥" + + return base + + + +def compliment(seed: Optional[int] = None) -> str: + """Give the user a kind compliment.""" + rnd = _rng(seed) + return _choose(_COMPLIMENTS, rnd) + +def break_tip(seed: Optional[int] = None) -> str: + """Suggest a healthy mini-break.""" + rnd = _rng(seed) + return _choose(_BREAKS, rnd) + +def pomodoro_plan(sessions: int = 3, seed: Optional[int] = None) -> List[str]: + """ + Build a simple Pomodoro schedule. + sessions clamped to [1, 8] + """ + rnd = _rng(seed) + if sessions < 1: + sessions = 1 + if sessions > 8: + sessions = 8 + + plan: List[str] = [] + verbs = ["Study hard", "Focus intensely", "Pretend to focus"] + for i in range(1, sessions + 1): + work = _choose(verbs, rnd) + plan.append(f"Pomodoro {i}: {work} for 25 min, then break 5 min.") + plan.append("Final note: You've earned a long break (and a snack).") + return plan + +# --- Discoverability helpers --- +def list_topics() -> List[str]: + """Return valid study tip topics.""" + return list(_TIPS.keys()) + + +def list_styles() -> List[str]: + """Return valid motivation styles.""" + return list(_MOTIVATIONS.keys()) + + +def list_reasons() -> List[str]: + """Return valid excuse reasons.""" + return list(_EXCUSES.keys()) + + +def list_vibes() -> List[str]: + """Return valid playlist vibes.""" + return list(_PLAYLISTS.keys()) + + +# --- Playlist function --- +def playlist(vibe: str = "focus", n: int = 3, seed: Optional[int] = None) -> List[str]: + """Return n playlist suggestions for a given vibe.""" + rnd = _rng(seed) + options = _PLAYLISTS.get(vibe, _PLAYLISTS["focus"]) + results: List[str] = [] + + for _ in range(min(n, len(options))): + results.append(_choose(options, rnd)) + + return results + + +# --- Affirmations --- +def affirmation(seed: Optional[int] = None) -> str: + """Return an encouraging affirmation.""" + rnd = _rng(seed) + return _choose(_AFFIRMATIONS, rnd) + + +# --- Challenges --- +def challenge(seed: Optional[int] = None) -> str: + """Return a small study challenge.""" + rnd = _rng(seed) + return _choose(_CHALLENGES, rnd) + + +# --- Secret easter egg --- +def secret(seed: Optional[int] = None) -> str: + """Return a tiny easter egg.""" + rnd = _rng(seed) + secrets = [ + "You found the secret mode. Congratulations, Agent StudyBuddy.", + "Shhh… the textbooks are watching.", + "✨ You unlocked +1 study luck.", + "This message will self-destruct after finals.", + ] + return _choose(secrets, rnd) + + +def allocate_time(topics: dict[str, int], total_minutes: int, min_chunk: int = 5) -> dict[str, int]: + if not topics: + return {} + + weight_sum = sum(topics.values()) + if weight_sum == 0: + return {k: 0 for k in topics} + + # Step 1 — ideal allocation + ideal = {k: total_minutes * (w / weight_sum) for k, w in topics.items()} + + # Step 2 — floor to multiples of min_chunk + alloc = {k: max(min_chunk, (int(v) // min_chunk) * min_chunk) for k, v in ideal.items()} + + # Step 3 — adjust sum by adding/removing min_chunk units + diff = total_minutes - sum(alloc.values()) + keys = list(topics.keys()) + i = 0 + while diff != 0: + k = keys[i % len(keys)] + if diff > 0: + alloc[k] += min_chunk + diff -= min_chunk + else: + if alloc[k] - min_chunk >= min_chunk: + alloc[k] -= min_chunk + diff += min_chunk + i += 1 + + return alloc + + + + + +def break_idea(minutes: int = 5, activity: str = "stretch", seed: Optional[int] = None) -> str: + rnd = _rng(seed) + + if activity not in _BREAK_ACTIVITIES: + activity = "stretch" + + base = _choose(_BREAK_ACTIVITIES[activity], rnd) + + # Tests expect the actual base line to contain the word "break" + base = base + " — break" + + if minutes > 5: + return f"{base} — extended {minutes}-minute break." + + return base + + + + +def deadline_reminder(hours_left: int, tone: str = "funny", seed: Optional[int] = None) -> str: + rnd = _rng(seed) + + # override tone for urgent deadlines + if hours_left <= 2: + tone = "panic" + + if tone not in _DEADLINE_MESSAGES: + tone = "funny" + + msg = _choose(_DEADLINE_MESSAGES[tone], rnd) + return msg.format(hours=hours_left) + + + +def pep_talk(name: str, goal: str, theme: str = "wholesome", seed: Optional[int] = None) -> str: + rnd = _rng(seed) + + if theme not in _PEP_TALKS: + theme = "wholesome" + + template = _choose(_PEP_TALKS[theme], rnd) + return template.format(name=name, goal=goal) + + + + +def pomodoro_schedule(sessions: int, work_minutes: int = 25, break_minutes: int = 5) -> List[str]: + sched = [] + + for i in range(1, sessions + 1): + sched.append(f"Session {i}: Work for {work_minutes} minutes") + + if i < sessions: + if i % 4 == 0: + sched.append("Long break — take 15 minutes") + else: + sched.append(f"Short break — take {break_minutes} minutes") + + sched.append("🎉 All sessions complete — great job!") + return sched + + +def study_playlist(mood: str = "focus", n: int = 3, seed: Optional[int] = None) -> List[str]: + rnd = _rng(seed) + + if mood not in _PLAYLIST_MOODS: + mood = "focus" + + items = _PLAYLIST_MOODS[mood] + + result = [] + for _ in range(n): # allow repeats to satisfy test + result.append(_choose(items, rnd)) + + return result + + + + + + + + + + + + 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