diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml new file mode 100644 index 0000000..0d96beb --- /dev/null +++ b/.github/workflows/ci.yml @@ -0,0 +1,41 @@ +name: CI + +on: + pull_request: + branches: [ "pipfile-experiment" ] + push: + branches: [ "pipfile-experiment" ] + workflow_dispatch: + +jobs: + test-and-build: + runs-on: ubuntu-latest + strategy: + matrix: + python-version: ["3.10", "3.12"] + + steps: + - name: Check out code + uses: actions/checkout@v4 + + - name: Set up Python + uses: actions/setup-python@v5 + with: + python-version: ${{ matrix.python-version }} + + - name: Install dependencies + run: | + python -m pip install --upgrade pip + python -m pip install -e . pytest build + + - name: Run tests + run: pytest -q + + - name: Build (sdist & wheel) + run: python -m build + + - name: Upload dist artifacts + uses: actions/upload-artifact@v4 + with: + name: pyflirt-dist-${{ matrix.python-version }} + path: dist/* diff --git a/.gitignore b/.gitignore index 2f24a10..135b19a 100644 --- a/.gitignore +++ b/.gitignore @@ -147,3 +147,22 @@ dmypy.json # Cython debug symbols cython_debug/ +# Python +__pycache__/ +*.pyc +*.pyo +*.pyd +*.egg-info/ +.build/ +.dist/ +.pytest_cache/ +.venv/ +.env/ +.envrc +.DS_Store +/.coverage +.mypy_cache/ +.python-version + +# Pipenv +Pipfile.lock diff --git a/Pipfile b/Pipfile new file mode 100644 index 0000000..b80fa08 --- /dev/null +++ b/Pipfile @@ -0,0 +1,15 @@ +[[source]] +url = "https://pypi.org/simple" +verify_ssl = true +name = "pypi" + +[packages] + +[dev-packages] +pytest = "*" +build = "*" +twine = "*" + +[requires] +python_version = "3.12" +python_full_version = "3.12.4" diff --git a/Pipfile.lock b/Pipfile.lock new file mode 100644 index 0000000..6c4d705 --- /dev/null +++ b/Pipfile.lock @@ -0,0 +1,392 @@ +{ + "_meta": { + "hash": { + "sha256": "04147162fb671d30f16143546df2d8184d009fca6fe12b1f9a5362c5ce374bdf" + }, + "pipfile-spec": 6, + "requires": { + "python_full_version": "3.12.4", + "python_version": "3.12" + }, + "sources": [ + { + "name": "pypi", + "url": "https://pypi.org/simple", + "verify_ssl": true + } + ] + }, + "default": {}, + "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" + }, + "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" + }, + "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" + } + } +} diff --git a/README.md b/README.md index 6022e0e..22b474d 100644 --- a/README.md +++ b/README.md @@ -1,3 +1,232 @@ -# Python Package Exercise +# pyflirt + +[![CI](https://github.com/swe-students-fall2025/3-python-package-team_quartz/actions/workflows/ci.yml/badge.svg)](https://github.com/swe-students-fall2025/3-python-package-team_quartz/actions/workflows/ci.yml) + +A small Python package with developer‑themed pickup lines and compliments. Nothing serious—just something light to play with while practicing packaging, testing, and CI. + +## What is this? + +This package has a collection of cheesy (but configurable) pickup lines and compliments for developers, designers, managers, and data scientists. You can get random lines, filter by category, adjust the cheesiness level, and customize compliments. + +## Installation + +```bash +pip install pyflirt +``` + +## Quick Start + +```python +from pyflirt import line, lines, compliment, categories + +# Get one random pickup line +print(line()) + +# Get a line in a specific category +print(line(category="nerdy")) + +# Get multiple lines +print(lines(n=5, category="cs")) + +# Get a compliment +print(compliment(role="developer", mood="sweet")) +``` + +## Demo program + +Run the example that showcases all functions (`categories`, `line`, `lines`, `compliment`, `search`, `stats`, `stylize`, `say`, `rate_line`, `rainbow`, `ascii_heart`): + +```bash +# from the repo root +PYTHONPATH=src python examples/demo.py + +# or with pipenv +pipenv run python -c "import sys; sys.path.insert(0, 'src'); import examples.demo as d; d.main()" +``` + +## Functions + +### `line(category="nerdy", name=None, cheese=2, seed=None)` + +Returns one random pickup line. + +- `category`: Pick a category like "nerdy", "cs", "math", "poetic", or "classic". Default is "nerdy". +- `name`: If the line supports it, this name will be inserted. +- `cheese`: How cheesy should it be? 1 (least cheesy) to 5 (very cheesy). Default is 2. +- `seed`: Optional number for reproducible results. + +Example: +```python +line(category="cs", name="Alex", cheese=2) +``` + +### `lines(n=5, category=None, name=None, cheese=2, seed=None)` + +Returns a list of pickup lines. + +- `n`: How many lines you want. +- Other parameters work the same as `line()`. + +Example: +```python +lines(n=3, category="math", cheese=3) +``` + +### `compliment(role="developer", mood="sweet", name=None, emojis=0, seed=None)` + +Returns a compliment for a specific role. + +- `role`: Choose from "developer", "designer", "manager", or "data". +- `mood`: "sweet", "cheeky", or "nerdy". +- `name`: Optional name to include in the compliment. +- `emojis`: Number of heart emojis to add (0-5). +- `seed`: Optional number for reproducible results. + +Example: +```python +compliment(role="designer", mood="cheeky", name="Sam", emojis=2) +``` + +### `categories()` + +Returns a list of all available pickup line categories. + +Example: +```python +print(categories()) +# ['classic', 'cs', 'math', 'nerdy', 'poetic'] +``` + +### `search(query, category=None, name=None, cheese=5, limit=10, seed=None)` + +Find up to `limit` lines containing `query` (case-insensitive), optionally filtered. + +- `query`: substring to match (required) +- `category`: filter by category or search all +- `name`: optional replacement for `{name}` placeholders +- `cheese`: 1–5 max cheese allowed in results +- `limit`: max results to return +- `seed`: for deterministic ordering + +Example: +```python +search("code", category="cs", limit=3, seed=7) +``` + +### `stats()` + +Return counts for available lines. + +Returns a dict with keys: `total`, `by_category`, and `cheese_hist`. + +Example: +```python +s = stats() +print(s["total"], s["by_category"], s["cheese_hist"]) +``` + +### `stylize(text, width=None, uppercase=False, color="auto")` + +Format a string with optional wrapping, uppercasing, and ANSI color. + +- `width`: wrap to this many columns (None = no wrap) +- `uppercase`: True to uppercase the text +- `color`: one of `"auto"`, `"none"`, `"magenta"`, `"cyan"`, `"green"` + +Example: +```python +stylize("hello world", width=8, uppercase=True, color="none") +``` + +### `say(category="nerdy", name=None, cheese=2, seed=None, width=None, uppercase=False, color="auto", emojis=0)` + +Generate a line, decorate it (wrap/case/color/emojis), print it, and return it. + +Example: +```python +say(category="nerdy", seed=1, width=16, color="none", emojis=2) +``` + +### `rate_line(text, metric="length"|"cheese_level"|"random", seed=None)` + +Score a line by the chosen metric. + +- `length`: higher for shorter lines (simple heuristic) +- `cheese_level`: counts cheesy keywords +- `random`: seeded 0–10 score + +Example: +```python +rate_line("You are so sweet", metric="cheese_level") +``` + +### `rainbow(text)` + +Colorize text with rainbow ANSI codes 🌈 +Useful for terminals that support color formatting. + +Example: +```python +print(rainbow("You light up my console! 💻")) +``` + +### `ascii_heart()` + +Return a multi-line ASCII heart ❤️. A great decoration after a compliment. + +Example: +```python +print(ascii_heart()) +``` +## Development Setup + +If you want to work on this package: + +1. Clone the repo: +```bash +git clone https://github.com/swe-students-fall2025/3-python-package-team_quartz.git +cd 3-python-package-team_quartz +``` + +2. Install pipenv (if you don't have it): +```bash +pip install pipenv +``` + +3. Install dependencies: +```bash +pipenv install --dev +``` + +4. Activate the virtual environment: +```bash +pipenv shell +``` + +5. Run tests: +```bash +pytest +``` + +6. Build the package: +```bash +python -m build +``` + +## Links + +- [PyPI Package](https://pypi.org/project/pyflirt/0.1.0/) +- [TestPyPI (0.1.0)](https://test.pypi.org/project/pyflirt/0.1.0/) +- [GitHub Repository](https://github.com/swe-students-fall2025/3-python-package-team_quartz) + +## Contributors + +Team +- Siqi Zhu — [@HelenZhutt](https://github.com/HelenZhutt) +- Daniel Lee - [@danielleesignup](https://github.com/danielleesignup) + + +More links +- [Contributors Graph](https://github.com/swe-students-fall2025/3-python-package-team_quartz/graphs/contributors) +- [Commits](https://github.com/swe-students-fall2025/3-python-package-team_quartz/commits) -An exercise to create a Python package, build it, test it, distribute it, and use it. See [instructions](./instructions.md) for details. diff --git a/examples/demo.py b/examples/demo.py new file mode 100644 index 0000000..7054877 --- /dev/null +++ b/examples/demo.py @@ -0,0 +1,68 @@ +from pyflirt import ( + line, + lines, + categories, + compliment, + search, + stats, + stylize, + say, + rate_line, + rainbow, + ascii_heart, +) + +def main(): + print("== categories() ==") + cats = categories() + print(cats) + + cat = "nerdy" if "nerdy" in cats else (cats[0] if cats else None) + + print("\n== line() ==") + print(line(category=cat or "nerdy", name="Alex", cheese=3, seed=1)) + + print("\n== lines() ==") + for s in lines(n=3, category=cat, name="Sam", cheese=3, seed=2): + print("-", s) + + print("\n== compliment() ==") + print(compliment(role="developer", mood="sweet", name="Jamie", emojis=2, seed=3)) + + print("\n== search() ==") + for h in search(query="you", limit=5, seed=4): + print("-", h) + + print("\n== stats() ==") + st = stats() + print("total:", st["total"]) + print("by_category:", st["by_category"]) + print("cheese_hist:", st["cheese_hist"]) + + print("\n== stylize() ==") + sample = line(category=cat or "nerdy", seed=5) + styled = stylize(sample, width=20, uppercase=True, color="none") + print(styled) + + print("\n== say() ==") + # say() prints the formatted line and returns it + returned = say(category=cat or "nerdy", seed=6, width=18, emojis=1, color="none") + print("(returned)", returned) + + print("\n== rate_line() ==") + txt = line(category=cat or "nerdy", seed=7) + print("text:", txt) + print("length score:", rate_line(txt, metric="length")) + print("cheese score:", rate_line(txt, metric="cheese_level")) + print("random score (seeded):", rate_line(txt, metric="random", seed=42)) + + print("\n== rainbow() ==") + rainbow_text = rainbow("You brighten up my terminal 💻💘") + print(rainbow_text) + + print("\n== ascii_heart() ==") + print(ascii_heart()) + + +if __name__ == "__main__": + main() diff --git a/pyproject.toml b/pyproject.toml new file mode 100644 index 0000000..56ead1f --- /dev/null +++ b/pyproject.toml @@ -0,0 +1,41 @@ +[build-system] +requires = ["setuptools>=61.0", "wheel"] +build-backend = "setuptools.build_meta" + +[project] +name = "pyflirt" +description = "A lighthearted Python package for developer-themed pickup lines." +version = "0.1.0" +authors = [ + { name = "Daniel", email = "dl4458@nyu.edu" }, +] +license = { file = "LICENSE" } +readme = "README.md" +keywords = ["fun", "pickup lines", "developer humor"] +requires-python = ">=3.9" +classifiers = [ + "Programming Language :: Python :: 3", + "Intended Audience :: Developers", + "License :: OSI Approved :: MIT License", + "Operating System :: OS Independent", +] + +dependencies = [] + +[project.optional-dependencies] +dev = ["pytest"] + +[project.urls] +"Homepage" = "https://github.com/swe-students-fall2025/3-python-package-team_quartz" +"Repository" = "https://github.com/swe-students-fall2025/3-python-package-team_quartz.git" +"Bug Tracker" = "https://github.com/swe-students-fall2025/3-python-package-team_quartz/issues" + +[tool.setuptools] +package-dir = {"" = "src"} + +[tool.setuptools.packages.find] +where = ["src"] + +[tool.pytest.ini_options] +testpaths = ["tests"] +pythonpath = ["src"] diff --git a/src/pyflirt/__init__.py b/src/pyflirt/__init__.py new file mode 100644 index 0000000..1eafcf4 --- /dev/null +++ b/src/pyflirt/__init__.py @@ -0,0 +1,56 @@ +# src/pyflirt/__init__.py +""" +pyflirt 💘 +Public API: +- categories() +- line(category, name, cheese, seed) +- lines(n, category, name, cheese, seed) +- compliment(role, mood, name, emojis, seed) +- search(query, category=None, name=None, cheese=5, limit=10, seed=None) +- stats() +- stylize(text, width=None, uppercase=False, color="auto") +- say(category="nerdy", name=None, cheese=2, seed=None, width=None, uppercase=False, color="auto", emojis=0) +- rate_line(text, metric="length", seed=None) +- rainbow(text: str) +- ascii_heart() +""" + +from .api import ( + line, + lines, + categories, + compliment, + search, + stats, + rate_line, + stylize, + say, + _check_cat, + _with_name, + _pool, + rainbow, + ascii_heart +) + +__all__ = [ + "categories", + "line", + "lines", + "compliment", + "search", + "stats", + "rate_line", + "stylize", + "say", + "rainbow", + "ascii_heart", +] +__version__ = "0.1.0" + + +try: + import sys + import builtins + builtins.pyflirt = sys.modules[__name__] +except Exception: + pass diff --git a/src/pyflirt/api.py b/src/pyflirt/api.py new file mode 100644 index 0000000..83b5e80 --- /dev/null +++ b/src/pyflirt/api.py @@ -0,0 +1,281 @@ +import random +from typing import List, Optional +from .data import BANK, COMPLIMENT_TEMPLATES, categories as _categories +from typing import cast, Dict +import os, textwrap +from typing import Literal + + +__all__ = ["line", "lines", "categories", "compliment", "search", "stats"] + +def categories() -> List[str]: + """Return a sorted list of all available pickup line categories.""" + return _categories() + +def _check_cat(cat: Optional[str]) -> Optional[str]: + if cat is None: + return None + if cat not in BANK: + valid = ", ".join(_categories()) + raise ValueError(f"Unknown category {cat!r}. Choose from {valid}.") + return cat + +def _pool(cat: Optional[str], cheese: int) -> List[dict]: + """Return candidate lines filtered by category and cheese. + If filtering wipes everything out, fall back to the unfiltered pool. + """ + def ok(e: dict) -> bool: + return e.get("cheese", 3) <= cheese + + if cat is None: + picks = [e for items in BANK.values() for e in items if ok(e)] + else: + picks = [e for e in BANK[cat] if ok(e)] + + if not picks: + picks = BANK[cat] if cat else [e for items in BANK.values() for e in items] + return picks + +def _with_name(text: str, name: Optional[str]) -> str: + if "{name}" in text: + return text.replace("{name}", name or "you") + return text + +def line( + category: Optional[str] = "nerdy", + name: Optional[str] = None, + cheese: int = 2, + seed: Optional[int] = None, +) -> str: + """Return one random pickup line for the given category, name, and cheese level.""" + if not 1 <= int(cheese) <= 5: + raise ValueError("cheese must be in 1..5") + category = _check_cat(category) + rng = random.Random(seed) + choice = rng.choice(_pool(category, cheese)) + return _with_name(choice["text"], name) + +def lines( + n: int = 5, + category: Optional[str] = None, + name: Optional[str] = None, + cheese: int = 2, + seed: Optional[int] = None, +) -> List[str]: + """Return a list of n pickup lines matching the given category and cheese level.""" + if n <= 0: + return [] + if not 1 <= int(cheese) <= 5: + raise ValueError("cheese must be in 1..5") + category = _check_cat(category) + rng = random.Random(seed) + pool = _pool(category, cheese) + + out: List[str] = [] + if len(pool) >= n: + for e in rng.sample(pool, n): + out.append(_with_name(e["text"], name)) # type: ignore[index] + return out + + for _ in range(n): + e = rng.choice(pool) + out.append(_with_name(e["text"], name)) + return out + +def compliment(role="developer", mood="sweet", name=None, emojis=0, seed=None): + """Return a list of n pickup lines matching the given category and cheese level.""" + rng = random.Random(seed) if seed is not None else random + + role = role.lower() + mood = mood.lower() + if role not in COMPLIMENT_TEMPLATES: + raise ValueError(f"Unknown role '{role}'. Choose from {list(COMPLIMENT_TEMPLATES.keys())}.") + if mood not in COMPLIMENT_TEMPLATES[role]: + raise ValueError(f"Unknown mood '{mood}'. Choose from {list(COMPLIMENT_TEMPLATES[role].keys())}.") + + template = rng.choice(COMPLIMENT_TEMPLATES[role][mood]) + name_bit = f", {name}" if name else "" + text = template.format(name_bit=name_bit) + + if emojis > 0: + text += " " + "💖" * emojis + return text + +def stylize( + text: str, + *, + width: Optional[int] = None, + uppercase: bool = False, + color: Literal["auto", "none", "magenta", "cyan", "green"] = "auto", +) -> str: + """ + Post-process a string with wrapping, casing, and ANSI color. + - width: wrap to N columns (None = no wrap) + - uppercase: True to SHOUT + - color: 'auto' enables color only on TTY; or force 'magenta'/'cyan'/'green'/'none' + """ + s = text.upper() if uppercase else text + if width: + s = "\n".join(textwrap.wrap(s, width=width)) + + palette = {"magenta": "\033[95m", "cyan": "\033[96m", "green": "\033[92m"} + reset = "\033[0m" + + if color == "none": + return s + if color == "auto": + color = "magenta" if os.getenv("TERM") else "none" + if color in palette: + return f"{palette[color]}{s}{reset}" + return s + +def say( + *, + category: Optional[str] = "nerdy", + name: Optional[str] = None, + cheese: int = 2, + seed: Optional[int] = None, + width: Optional[int] = None, + uppercase: bool = False, + color: Literal["auto", "none", "magenta", "cyan", "green"] = "auto", + emojis: int = 0, +) -> str: + """ + Generate a line, optionally decorate it (wrap/case/color), print it, and return it. + """ + txt = line(category=category, name=name, cheese=cheese, seed=seed) + if emojis > 0: + txt += " " + "💘" * emojis + pretty = stylize(txt, width=width, uppercase=uppercase, color=color) + print(pretty) + return pretty + +def rate_line(text: str, metric: str = "length", seed: Optional[int] = None) -> float: + """ + Rate a pickup line by various heuristics. + Args: + text: pickup line string. + metric: rating metric ('length', 'cheese_level', 'random'). + seed: random seed for repeatability. + Returns: + A float rating score. + Raises: + ValueError if metric unknown. + """ + if metric not in ("length", "cheese_level", "random"): + raise ValueError(f"Unknown metric {metric!r}") + + if metric == "length": + length = len(text) + return float(max(0, 100 - length)) + + if metric == "cheese_level": + cheesy_words = ["love", "heart", "cute", "sweet", "charm", "kiss"] + count = sum(word in text.lower() for word in cheesy_words) + return float(count) + + rng = random.Random(seed) + return rng.uniform(0, 10) + +def search( + query: str, + category: Optional[str] = None, + name: Optional[str] = None, + cheese: int = 5, + limit: int = 10, + seed: Optional[int] = None, +) -> List[str]: + """Return up to `limit` lines containing `query` (case-insensitive).""" + if not query: + raise ValueError("query must be non-empty") + if not 1 <= int(cheese) <= 5: + raise ValueError("cheese must be in 1..5") + if limit <= 0: + return [] + + category = _check_cat(category) + q = query.lower() + + def ok(e: dict) -> bool: + return e.get("cheese", 3) <= cheese and q in str(e.get("text", "")).lower() + + if category is None: + pool = [e for items in BANK.values() for e in items if ok(e)] + else: + pool = [e for e in BANK[category] if ok(e)] + + rng = random.Random(seed) if seed is not None else random + rng.shuffle(pool) + + out: List[str] = [] + for e in pool[:limit]: + out.append(_with_name(e.get("text", ""), name)) + return out + + +def stats() -> dict: + """Return counts: total, by_category, and cheese_hist (1..5).""" + cats = _categories() + by_category = {c: len(BANK.get(c, [])) for c in cats} + cheese_hist = {i: 0 for i in range(1, 6)} + for c in cats: + for e in BANK.get(c, []): + ch = int(e.get("cheese", 3)) + if 1 <= ch <= 5: + cheese_hist[ch] += 1 + total = sum(by_category.values()) + return {"total": total, "by_category": by_category, "cheese_hist": cheese_hist} + +def rainbow(text: str) -> str: + """ + Return the given text as a colorful rainbow string using ANSI color codes. + + Each character in the input text is assigned a different color in sequence, + cycling through a palette of bright colors for a cheerful rainbow effect. + + Example: + >>> print(rainbow("Hello World!")) + (Displays 'Hello World!' with rainbow colors in your terminal) + """ + # Define the ANSI escape codes for bright rainbow colors. + palette = [ + "\033[91m", # Red + "\033[93m", # Yellow + "\033[92m", # Green + "\033[94m", # Blue + "\033[95m", # Magenta + "\033[96m", # Cyan + ] + + # ANSI escape code to reset the color back to default at the end. + reset_code = "\033[0m" + + if not text: + return "" + + # The color cycles through the palette using modulo arithmetic. + colored_chars = [] + for index, char in enumerate(text): + color = palette[index % len(palette)] + colored_chars.append(f"{color}{char}") + + # Join all colored characters and append the reset code. + rainbow_text = "".join(colored_chars) + reset_code + + return rainbow_text + +def ascii_heart() -> str: + """Return a cute ASCII heart graphic.""" + return textwrap.dedent(""" + ***** ***** + ********* ********* + ********************* + ********************* + ******************* + ***************** + *************** + *********** + ******* + *** + * + """).strip("\n") diff --git a/src/pyflirt/data.py b/src/pyflirt/data.py new file mode 100644 index 0000000..0f85766 --- /dev/null +++ b/src/pyflirt/data.py @@ -0,0 +1,102 @@ +# pyflirt/data.py +BANK = { + "nerdy": [ + {"text": "Are you made of copper and tellurium? Because you’re Cu-Te.", "cheese": 2}, + {"text": "Are you a quantum tunnel? Because you went straight through my barriers.", "cheese": 3}, + {"text": "Is your name Wi-Fi? Because I feel a strong connection.", "cheese": 3}, + {"text": "Are you a neural net, {name}? Because I keep overfitting to you.", "cheese": 4}, + ], + "poetic": [ + {"text": "{name}, shall I compare thee to a stable release? Thou art rarer and far more dependable.", "cheese": 4}, + {"text": "I’d cross the version gulf for thee, and tag a release upon thy smile.", "cheese": 3}, + ], + "cs": [ + {"text": "If love were a bug, I’d still refuse to close your ticket.", "cheese": 1}, + {"text": "Do you believe in love at first compile, {name}, or should I re-run?", "cheese": 2}, + {"text": "You must be Git—my heart commits to you.", "cheese": 2}, + ], + "math": [ + {"text": "You must be my limit—I’m approaching you from every direction.", "cheese": 4}, + {"text": "If we were vectors, we’d be perfectly aligned.", "cheese": 2}, + {"text": "Are you √-1? You’re unreal—and I can’t stop imagining us.", "cheese": 4}, + {"text": "We are coprime; the only common divisor is one heart.", "cheese": 3}, + ], + "classic": [ + {"text": "Are you a magician? Because whenever I look at you, everyone else disappears.", "cheese": 2}, + ], +} + +COMPLIMENT_TEMPLATES = { + "developer": { + "sweet": [ + "Your code is cleaner than a freshly cloned repo{name_bit}", + "You commit kindness with every push{name_bit}", + "You’re the pull request everyone approves instantly{name_bit}", + ], + "cheeky": [ + "You refactor hearts, not just code{name_bit}", + "You’ve got more charm than a recursive function{name_bit}", + "You must be a keyboard shortcut—because you’re my type{name_bit}", + ], + "nerdy": [ + "You debug my sadness faster than VSCode{name_bit}", + "You’re the semicolon that completes my statement{name_bit}", + "If beauty were an algorithm, you’d be O(1){name_bit}", + ], + }, + "designer": { + "sweet": [ + "Your aesthetic sense brightens every UI{name_bit}", + "You bring color theory to my grayscale days{name_bit}", + "Pixels align themselves just to please you{name_bit}", + ], + "cheeky": [ + "You must be a vector—because you’ve got direction{name_bit}", + "Are you a grid system? Because my heart is well-aligned{name_bit}", + "You kerningly complete me{name_bit}", + ], + "nerdy": [ + "You optimize whitespace like a legend{name_bit}", + "Your Figma files are pure poetry{name_bit}", + "Even Helvetica blushes when you walk in{name_bit}", + ], + }, + "manager": { + "sweet": [ + "You lead with empathy{name_bit}", + "Your standups make Mondays bearable{name_bit}", + "You’re the reason meetings actually end early{name_bit}", + ], + "cheeky": [ + "You manage hearts better than timelines{name_bit}", + "You’re my favorite deliverable{name_bit}", + "You’ve got more charisma than a sprint demo{name_bit}", + ], + "nerdy": [ + "You allocate my attention like a well-balanced backlog{name_bit}", + "KPIs envy your energy{name_bit}", + "Your OKRs? Outrageously Kind & Radiant{name_bit}", + ], + }, + "data": { + "sweet": [ + "You turn noise into beauty{name_bit}", + "Every dataset wishes it were as clean as your heart{name_bit}", + "You make outliers feel included{name_bit}", + ], + "cheeky": [ + "You must be a correlation—because you complete my regression{name_bit}", + "You’re my favorite variable{name_bit}", + "You pivot-table my emotions{name_bit}", + ], + "nerdy": [ + "Your confidence interval? 100%{name_bit}", + "You’re statistically significant in my life{name_bit}", + "Your curves fit any model{name_bit}", + ], + }, +} + +def categories(): + """Return a sorted list of all available pickup line categories.""" + return sorted(BANK.keys()) diff --git a/tests/test_pyflirt.py b/tests/test_pyflirt.py new file mode 100644 index 0000000..c72d06f --- /dev/null +++ b/tests/test_pyflirt.py @@ -0,0 +1,228 @@ +from pyflirt import line, lines, categories, search, compliment, rate_line, ascii_heart, rainbow +import pytest +import re + +def test_categories_type_and_nonempty(): + cs = categories() + assert isinstance(cs, list) + assert all(isinstance(c, str) and c for c in cs) + assert len(cs) >= 1 + +def test_categories_expected_buckets_present(): + cs = set(categories()) + for c in ["classic", "cs", "math", "nerdy", "poetic"]: + assert c in cs + +def test_categories_stable_across_calls(): + assert categories() == categories() + +def test_line_seed_is_deterministic(): + a = line(category="nerdy", seed=123) + b = line(category="nerdy", seed=123) + assert a == b and isinstance(a, str) and a + +def test_line_category_param_accepts_valid_and_filters(): + out = line(category="cs", seed=7) + assert isinstance(out, str) and len(out) > 0 + +def test_line_cheese_bounds(): + with pytest.raises(ValueError): + line(cheese=0) + with pytest.raises(ValueError): + line(cheese=6) + +def test_lines_respects_n_and_types(): + arr = lines(n=5, name="Sam", seed=123) + assert len(arr) == 5 + assert all(isinstance(s, str) and s for s in arr) + +def test_lines_seed_is_deterministic(): + a = lines(n=3, category="nerdy", seed=42) + b = lines(n=3, category="nerdy", seed=42) + assert a == b + +def test_lines_cheese_bounds(): + with pytest.raises(ValueError): + lines(n=2, cheese=0) + with pytest.raises(ValueError): + lines(n=2, cheese=6) + +def test_compliment_returns_string_and_nonempty(): + out = compliment() + assert isinstance(out, str) and len(out) > 0 + +def test_compliment_includes_name_when_given(): + result = compliment(role="developer", mood="nerdy", name="Alex", seed=1) + assert "Alex" in result + +def test_compliment_emojis_count_and_suffix(): + result = compliment(role="data", emojis=3, seed=2) + assert result.endswith("💖" * 3) + +def test_compliment_determinism_with_seed(): + a = compliment(role="designer", mood="cheeky", seed=42) + b = compliment(role="designer", mood="cheeky", seed=42) + assert a == b + +def test_stats_shape_and_totals(): + s = pyflirt.stats() + assert isinstance(s, dict) + assert "total" in s and "by_category" in s and "cheese_hist" in s + assert isinstance(s["by_category"], dict) + assert isinstance(s["cheese_hist"], dict) + assert s["total"] == sum(s["by_category"].values()) + assert s["total"] == sum(s["cheese_hist"].values()) + +def test_stats_categories_match_categories_function(): + s = pyflirt.stats() + cats = pyflirt.categories() + assert set(s["by_category"].keys()) == set(cats) + +def test_stats_cheese_hist_keys(): + s = pyflirt.stats() + assert set(s["cheese_hist"].keys()) == {1, 2, 3, 4, 5} + +def test_search_basic_limit_and_seed_stability(): + sample = pyflirt.line(seed=12345) + token = None + for t in re.findall(r"[A-Za-z]+", sample): + if len(t) >= 3: + token = t + break + token = token or "love" + + r1 = pyflirt.search(token, limit=3, seed=7) + r2 = pyflirt.search(token, limit=3, seed=7) + r3 = pyflirt.search(token, limit=3, seed=8) + + assert len(r1) <= 3 + assert r1 == r2 + if len(r1) > 1 and len(r3) > 1: + assert r1 != r3 or r1[0] != r3[0] + +def test_search_category_filter_and_case_insensitivity(): + token = "code" + a = pyflirt.search(token, category="cs", limit=5, seed=11) + b = pyflirt.search(token.upper(), category="cs", limit=5, seed=11) + assert isinstance(a, list) and all(isinstance(x, str) for x in a) + assert a == b + +def test_search_invalid_inputs_raise(): + with pytest.raises(ValueError): + pyflirt.search("x", category="__nope__", limit=1) + with pytest.raises(ValueError): + pyflirt.search("", limit=1) + +def test_stylize_uppercase_and_width_wrap(): + s = pyflirt.stylize("hello world", uppercase=True, width=5, color="none") + lines = s.splitlines() + assert all(line.isupper() for line in lines) + assert len(lines) >= 2 + +def test_stylize_force_color_magenta_codes_present(): + out = pyflirt.stylize("x", color="magenta") + assert "\x1b[95m" in out and out.endswith("\x1b[0m") + +def test_stylize_color_none_returns_plain(): + out = pyflirt.stylize("abc", color="none") + assert out == "abc" + +def test_say_prints_and_returns_same(capsys): + res = pyflirt.say(category="nerdy", seed=1, color="none") + captured = capsys.readouterr().out.strip() + assert isinstance(res, str) and res + assert res.strip() == captured + +def test_say_emojis_suffix_and_uppercase(capsys): + res = pyflirt.say(category="nerdy", seed=2, emojis=2, uppercase=True, color="none") + assert res.endswith("💘💘") + assert res.split("💘")[0].strip().isupper() + +def test_say_width_wrap_occurs(capsys): + res = pyflirt.say(category="nerdy", seed=3, width=4, color="none") + assert "\n" in res + +def test_rate_line_length_metric(): + txt = "x" * 10 + v = pyflirt.rate_line(txt, metric="length") + assert v == float(max(0, 100 - len(txt))) + +def test_rate_line_cheese_level_counts_keywords(): + txt = "You are so sweet and cute, my heart melts" + v = pyflirt.rate_line(txt, metric="cheese_level") + assert v >= 3.0 + +def test_rate_line_random_seeded_deterministic(): + a = pyflirt.rate_line("hi", metric="random", seed=123) + b = pyflirt.rate_line("hi", metric="random", seed=123) + c = pyflirt.rate_line("hi", metric="random", seed=124) + assert 0.0 <= a <= 10.0 and 0.0 <= c <= 10.0 + assert a == b and a != c + +def test_rate_line_invalid_metric_raises(): + with pytest.raises(ValueError): + pyflirt.rate_line("x", metric="nope") + +def test__check_cat_none_ok_and_invalid_raises(): + fn = getattr(pyflirt, "_check_cat") + assert fn(None) is None + with pytest.raises(ValueError): + fn("__not_a_cat__") + +def test__with_name_inserts_or_defaults(): + fn = getattr(pyflirt, "_with_name") + assert fn("hi {name}", "Sam") == "hi Sam" + assert fn("hi {name}", None) == "hi you" + assert fn("hi there", "Sam") == "hi there" + +def test__pool_filters_by_cheese_and_fallback(): + fn = getattr(pyflirt, "_pool") + cat = pyflirt.categories()[0] + p = fn(cat, 1) + assert isinstance(p, list) and p + p2 = fn(cat, 1) + assert p2 + +def test_rainbow_basic_output(): + """Ensure rainbow() returns a non-empty string for normal input.""" + result = rainbow("Hello") + assert isinstance(result, str) + assert len(result) > 0 + +def test_rainbow_color_codes_present(): + """Rainbow text should contain ANSI color escape sequences.""" + result = rainbow("abc") + # ANSI escape codes start with \033[ + assert "\033[" in result, "Expected ANSI color codes in output" + +def test_rainbow_color_reset_at_end(): + """Rainbow text should reset color formatting at the end.""" + result = rainbow("Hello") + assert result.endswith("\033[0m"), "Output must end with reset escape code" + +def test_rainbow_empty_string(): + """Empty input should return an empty string (no escape codes).""" + result = rainbow("") + assert result == "", f"Expected empty string, got {result!r}" + +def test_ascii_heart_is_string(): + """ascii_heart() should return a string.""" + art = ascii_heart() + assert isinstance(art, str) + +def test_ascii_heart_contains_heart_shape(): + """ascii_heart() output should contain '*' as part of the ASCII art.""" + art = ascii_heart() + assert "*" in art, "Expected '*' characters in ASCII heart output" + +def test_ascii_heart_multiple_lines(): + """The ASCII heart should be multi-line.""" + art = ascii_heart() + lines = art.splitlines() + assert len(lines) > 3, "Expected at least 4 lines in ASCII heart" + +def test_ascii_heart_consistent_output(): + """The ASCII heart should produce consistent output each call.""" + art1 = ascii_heart() + art2 = ascii_heart() + assert art1 == art2, "ascii_heart() output should be deterministic" \ No newline at end of file