diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml new file mode 100644 index 0000000..ae46db3 --- /dev/null +++ b/.github/workflows/ci.yml @@ -0,0 +1,59 @@ +name: CI + +on: + pull_request: + branches: [ main ] + workflow_dispatch: + +jobs: + test-and-build: + runs-on: ubuntu-latest + strategy: + matrix: + python-version: ["3.10", "3.12"] + steps: + - uses: actions/checkout@v4 + - uses: actions/setup-python@v5 + with: + python-version: ${{ matrix.python-version }} + - name: Install pipenv + run: python -m pip install --upgrade pip pipenv + - name: Sync dependencies + run: pipenv sync --dev + python-version: ["3.10", "3.11"] + + steps: + - name: Checkout + uses: actions/checkout@v4 + + - name: Set up Python + - uses: actions/setup-python@v5 + with: + python-version: ${{ matrix.python-version }} + cache: pip + + - name: Clean pipenv environment if exists + run: pipenv --rm || true + + - name: Upgrade pip and install build tools + run: | + python -m pip install --upgrade pip + python -m pip install build pytest pipenv + + - name: Install dependencies with Pipenv + run: | + pipenv --python $(which python) install --dev --ignore-pipfile + pipenv run pip install -e . + + - name: Install package editable + run: pipenv run pip install -e . + + - name: Run tests + run: pipenv run pytest -q + - name: Build (sdist & wheel) + run: pipenv 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/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..dbbfa13 100644 --- a/README.md +++ b/README.md @@ -1,3 +1,128 @@ -# Python Package Exercise +# pyflirt -An exercise to create a Python package, build it, test it, distribute it, and use it. See [instructions](./instructions.md) for details. +A Python package that gives you developer-themed pickup lines and compliments. Because coding should be fun. + +## 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")) +``` + +## 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'] +``` + +## 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/) +- [GitHub Repository](https://github.com/swe-students-fall2025/3-python-package-team_quartz) + +## Contributors + +See the [contributors page](https://github.com/swe-students-fall2025/3-python-package-team_quartz/graphs/contributors) for a list of everyone who worked on this project. diff --git a/examples/demo.py b/examples/demo.py new file mode 100644 index 0000000..10d45ac --- /dev/null +++ b/examples/demo.py @@ -0,0 +1,32 @@ +from pyflirt import line, lines, categories, compliment, search, stats + +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"]) + +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..54c3d88 --- /dev/null +++ b/src/pyflirt/__init__.py @@ -0,0 +1,27 @@ +# 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) +""" + +from .api import line, lines, categories, compliment, search, stats + +__all__ = ["line", "lines", "categories", "compliment", "search", "stats", "rate_line"] + + +__all__ = ["categories", "line", "lines", "compliment"] +__version__ = "0.1.0" + +# Provide a convenient module reference for test usage like `pyflirt.search(...)` +# even when tests only do `from pyflirt import ...`. +try: + import sys + import builtins + builtins.pyflirt = sys.modules[__name__] +except Exception: + # If this fails, normal imports still work. + pass diff --git a/src/pyflirt/api.py b/src/pyflirt/api.py new file mode 100644 index 0000000..a9858d7 --- /dev/null +++ b/src/pyflirt/api.py @@ -0,0 +1,254 @@ +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 search( + substring: str, + category: Optional[str] = None, + cheese: Optional[int] = None +) -> List[str]: + category = _check_cat(category) + + if cheese is not None and not (1 <= int(cheese) <= 5): + raise ValueError("cheese must be in 1..5") + + if category is None: + lines = [line for cat in BANK.values() for line in cat] + else: + lines = BANK[category] + + if cheese is not None: + lines = [line for line in lines if line.get("cheese", 3) == cheese] + + results = [] + for line in lines: + text = line["text"].lower() + # Case-Insensitive Match + if text.contains(substring.lower()): + results.append(line["text"]) + + return results +def stylize( + text: str, + *, + width: int | None = 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: int | None = 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} + 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..9fab6d1 --- /dev/null +++ b/tests/test_pyflirt.py @@ -0,0 +1,44 @@ +from pyflirt import line, lines, categories, search +import pytest + +from pyflirt import categories, line, lines, compliment +from pyflirt import rate_line +import re + +def test_categories_sorted_and_nonempty(): + cats = categories() + assert cats == sorted(cats) + assert len(cats) > 0 + +def test_categories_reflect_bank_contents(): + # If BANK has "nerdy" in data, ensure it's present + assert "nerdy" in categories() + +def test_line_invalid_cheese_raises(): + with pytest.raises(ValueError): + line(cheese=0) + with pytest.raises(ValueError): + line(cheese=6) + +def test_line_unknown_category_raises(): + with pytest.raises(ValueError): + line(category="not-a-cat") + +def test_lines_n_zero_returns_empty(): + assert lines(n=0) == [] + +def test_lines_invalid_cheese_raises(): + with pytest.raises(ValueError): + lines(cheese=99) + +def test_compliment_invalid_role_raises(): + with pytest.raises(ValueError): + compliment(role="astronaut") + +def test_compliment_invalid_mood_raises(): + with pytest.raises(ValueError): + compliment(role="developer", mood="salty") + +def test_compliment_emojis_appended(): + base = compliment(role="developer", mood="sweet", emojis=3, seed=1) + assert base.endswith("💖💖💖")