From b47d3c23112878cd13d654f63ae1c12fc9014e0a Mon Sep 17 00:00:00 2001 From: HelenZhutt Date: Mon, 3 Nov 2025 14:42:39 -0500 Subject: [PATCH 01/26] initialize the repo and add two functions --- pyflirt/__init__.py | 2 ++ pyflirt/api.py | 76 +++++++++++++++++++++++++++++++++++++++++++ pyflirt/data.py | 30 +++++++++++++++++ pyproject.toml | 13 ++++++++ tests/test_pyflirt.py | 19 +++++++++++ 5 files changed, 140 insertions(+) create mode 100644 pyflirt/__init__.py create mode 100644 pyflirt/api.py create mode 100644 pyflirt/data.py create mode 100644 pyproject.toml create mode 100644 tests/test_pyflirt.py diff --git a/pyflirt/__init__.py b/pyflirt/__init__.py new file mode 100644 index 0000000..ebb0266 --- /dev/null +++ b/pyflirt/__init__.py @@ -0,0 +1,2 @@ +from .api import line, lines, categories +__all__ = ["line", "lines", "categories"] diff --git a/pyflirt/api.py b/pyflirt/api.py new file mode 100644 index 0000000..ea54c1d --- /dev/null +++ b/pyflirt/api.py @@ -0,0 +1,76 @@ +import random +from typing import List, Optional +from .data import BANK, categories as _categories + +__all__ = ["line", "lines", "categories"] + +def categories() -> List[str]: + 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: + 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]: + 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)) + return out + + for _ in range(n): + e = rng.choice(pool) + out.append(_with_name(e["text"], name)) + return out diff --git a/pyflirt/data.py b/pyflirt/data.py new file mode 100644 index 0000000..a24b92a --- /dev/null +++ b/pyflirt/data.py @@ -0,0 +1,30 @@ +# 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}, + ], +} + +def categories(): + return sorted(BANK.keys()) diff --git a/pyproject.toml b/pyproject.toml new file mode 100644 index 0000000..93599c0 --- /dev/null +++ b/pyproject.toml @@ -0,0 +1,13 @@ +# pyproject.toml +[build-system] +requires = ["setuptools>=68", "wheel"] +build-backend = "setuptools.build_meta" + +[project] +name = "pyflirt" +version = "0.1.0" +description = "Playful, seedable pickup lines for the terminal." +readme = "README.md" +requires-python = ">=3.9" +authors = [{ name = "Team Quartz" }] +dependencies = [] \ No newline at end of file diff --git a/tests/test_pyflirt.py b/tests/test_pyflirt.py new file mode 100644 index 0000000..e397366 --- /dev/null +++ b/tests/test_pyflirt.py @@ -0,0 +1,19 @@ +from pyflirt import line, lines, categories + +def test_categories_present(): + assert "nerdy" in categories() + +def test_line_seed_stable(): + assert line(category="nerdy", seed=42) == line(category="nerdy", seed=42) + +def test_lines_len_and_content(): + arr = lines(n=3, name="Sam", seed=123) + assert len(arr) == 3 + assert all(isinstance(s, str) and s for s in arr) + +def test_cheese_bounds(): + import pytest + with pytest.raises(ValueError): + line(cheese=0) + with pytest.raises(ValueError): + lines(n=2, cheese=6) From 2e48b360c7ac7b577ce1dfee452275ca660e1c40 Mon Sep 17 00:00:00 2001 From: danielleesignup Date: Mon, 3 Nov 2025 15:28:10 -0500 Subject: [PATCH 02/26] Set up project dirs and wrote compliment function --- .github/workflows/ci.yml | 55 ++++++ Pipfile | 15 ++ Pipfile.lock | 392 +++++++++++++++++++++++++++++++++++++++ pyproject.toml | 41 ++++ src/pyflirt/__init__.py | 20 ++ src/pyflirt/core.py | 118 ++++++++++++ src/pyflirt/main.py | 8 + tests/test_compliment.py | 35 ++++ 8 files changed, 684 insertions(+) create mode 100644 .github/workflows/ci.yml create mode 100644 Pipfile create mode 100644 Pipfile.lock create mode 100644 pyproject.toml create mode 100644 src/pyflirt/__init__.py create mode 100644 src/pyflirt/core.py create mode 100644 src/pyflirt/main.py create mode 100644 tests/test_compliment.py diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml new file mode 100644 index 0000000..ef9940c --- /dev/null +++ b/.github/workflows/ci.yml @@ -0,0 +1,55 @@ +name: CI + +on: + pull_request: + branches: [ pipfile-experiment ] + workflow_dispatch: + +permissions: + contents: read + +concurrency: + group: ci-${{ github.ref }} + cancel-in-progress: true + +jobs: + test-and-build: + runs-on: ubuntu-latest + strategy: + fail-fast: false + matrix: + python-version: ["3.10", "3.12"] + + 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: 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 install --dev --ignore-pipfile + pipenv run pip install -e . + + - name: Run tests + run: | + pipenv run pytest -q + + - name: Build package (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/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..84611f4 --- /dev/null +++ b/src/pyflirt/__init__.py @@ -0,0 +1,20 @@ +""" +pyflirt 💘 + +APIs: +- line(category, name, cheese, seed) +- lines(n, categories, name, cheese, seed) +- compliment(role, mood, name, emojis, seed) +- rate_line(text, metric, seed) +""" + +from .core import ( + compliment, +) + +__all__ = [ + "compliment", + +] + +__version__ = "0.1.0" diff --git a/src/pyflirt/core.py b/src/pyflirt/core.py new file mode 100644 index 0000000..a25b874 --- /dev/null +++ b/src/pyflirt/core.py @@ -0,0 +1,118 @@ +""" +core.py — core logic for pyflirt 💘 + +Currently includes: +- compliment(): generate a developer-themed compliment +""" + +import random + + +__all__ = ["compliment"] + + +def compliment(role="developer", mood="sweet", name=None, emojis=0, seed=None): + """ + Generate a developer-themed compliment. + + Args: + role (str): One of ["developer", "designer", "manager", "data"]. + mood (str): One of ["sweet", "cheeky", "nerdy"]. + name (str, optional): Person's name to personalize. + emojis (int): Number of 💖 emojis to append. + seed (int, optional): Random seed for deterministic selection. + + Returns: + str: A playful compliment string. + """ + rng = random.Random(seed) if seed is not None else random + + 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}", + ], + }, + } + + # Normalize and validate + role = role.lower() + mood = mood.lower() + if role not in templates: + raise ValueError(f"Unknown role '{role}'. Choose from {list(templates.keys())}.") + if mood not in templates[role]: + raise ValueError(f"Unknown mood '{mood}'. Choose from {list(templates[role].keys())}.") + + # Select random compliment + template = rng.choice(templates[role][mood]) + name_bit = f", {name}" if name else "" + compliment_text = template.format(name_bit=name_bit) + + # Add emojis if requested + if emojis > 0: + compliment_text += " " + "💖" * emojis + + return compliment_text diff --git a/src/pyflirt/main.py b/src/pyflirt/main.py new file mode 100644 index 0000000..42a2ffe --- /dev/null +++ b/src/pyflirt/main.py @@ -0,0 +1,8 @@ +from .core import compliment + +def main(): + print("💘 Welcome to pyflirt!") + print(compliment(role="developer", mood="cheeky", name="Alex", emojis=2)) + +if __name__ == "__main__": + main() diff --git a/tests/test_compliment.py b/tests/test_compliment.py new file mode 100644 index 0000000..93f7c30 --- /dev/null +++ b/tests/test_compliment.py @@ -0,0 +1,35 @@ +import pytest +from pyflirt import compliment + + +def test_compliment_returns_string(): + """Should always return a non-empty string.""" + out = compliment() + assert isinstance(out, str) + assert len(out) > 0 + + +def test_compliment_includes_name(): + """Name should appear in the compliment when provided.""" + result = compliment(role="developer", mood="nerdy", name="Alex", seed=1) + assert "Alex" in result + + +def test_compliment_emojis_count(): + """Correct number of emojis should be appended.""" + result = compliment(role="data", emojis=3, seed=2) + assert result.endswith("💖💖💖") + + +def test_compliment_is_deterministic_with_seed(): + """Using the same seed should produce the same output.""" + a = compliment(role="designer", mood="cheeky", seed=42) + b = compliment(role="designer", mood="cheeky", seed=42) + assert a == b + + +def test_compliment_differs_with_different_seeds(): + """Different seeds should generally give different compliments.""" + a = compliment(role="designer", mood="cheeky", seed=42) + b = compliment(role="designer", mood="cheeky", seed=43) + assert a != b From 3d15746685ab137f6c48071781a8abeaf8e5274b Mon Sep 17 00:00:00 2001 From: danielleesignup Date: Mon, 3 Nov 2025 16:06:28 -0500 Subject: [PATCH 03/26] Integrated compliment function to existing file structure --- pyflirt/__init__.py | 3 +- pyflirt/data.py | 30 -------- src/pyflirt/__init__.py | 5 +- {pyflirt => src/pyflirt}/api.py | 32 +++++++-- src/pyflirt/core.py | 118 -------------------------------- src/pyflirt/data.py | 102 +++++++++++++++++++++++++++ src/pyflirt/main.py | 8 --- tests/test_compliment.py | 35 ---------- tests/test_pyflirt.py | 35 ++++++++++ 9 files changed, 170 insertions(+), 198 deletions(-) delete mode 100644 pyflirt/data.py rename {pyflirt => src/pyflirt}/api.py (60%) delete mode 100644 src/pyflirt/core.py create mode 100644 src/pyflirt/data.py delete mode 100644 src/pyflirt/main.py delete mode 100644 tests/test_compliment.py diff --git a/pyflirt/__init__.py b/pyflirt/__init__.py index ebb0266..8b13789 100644 --- a/pyflirt/__init__.py +++ b/pyflirt/__init__.py @@ -1,2 +1 @@ -from .api import line, lines, categories -__all__ = ["line", "lines", "categories"] + diff --git a/pyflirt/data.py b/pyflirt/data.py deleted file mode 100644 index a24b92a..0000000 --- a/pyflirt/data.py +++ /dev/null @@ -1,30 +0,0 @@ -# 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}, - ], -} - -def categories(): - return sorted(BANK.keys()) diff --git a/src/pyflirt/__init__.py b/src/pyflirt/__init__.py index 84611f4..31d78b3 100644 --- a/src/pyflirt/__init__.py +++ b/src/pyflirt/__init__.py @@ -7,6 +7,7 @@ - compliment(role, mood, name, emojis, seed) - rate_line(text, metric, seed) """ +from .api import line, lines, categories, compliment from .core import ( compliment, @@ -14,7 +15,9 @@ __all__ = [ "compliment", - + "line", + "lines", + "categories", ] __version__ = "0.1.0" diff --git a/pyflirt/api.py b/src/pyflirt/api.py similarity index 60% rename from pyflirt/api.py rename to src/pyflirt/api.py index ea54c1d..405fb61 100644 --- a/pyflirt/api.py +++ b/src/pyflirt/api.py @@ -1,10 +1,13 @@ import random from typing import List, Optional -from .data import BANK, categories as _categories +from .data import BANK, COMPLIMENT_TEMPLATES, categories as _categories +from typing import cast, Dict + __all__ = ["line", "lines", "categories"] def categories() -> List[str]: + """Return a sorted list of all available pickup line categories.""" return _categories() def _check_cat(cat: Optional[str]) -> Optional[str]: @@ -42,7 +45,8 @@ def line( cheese: int = 2, seed: Optional[int] = None, ) -> str: - if not (1 <= int(cheese) <= 5): + """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) @@ -56,9 +60,10 @@ def lines( 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): + if not 1 <= int(cheese) <= 5: raise ValueError("cheese must be in 1..5") category = _check_cat(category) rng = random.Random(seed) @@ -67,10 +72,29 @@ def lines( out: List[str] = [] if len(pool) >= n: for e in rng.sample(pool, n): - out.append(_with_name(e["text"], name)) + 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 \ No newline at end of file diff --git a/src/pyflirt/core.py b/src/pyflirt/core.py deleted file mode 100644 index a25b874..0000000 --- a/src/pyflirt/core.py +++ /dev/null @@ -1,118 +0,0 @@ -""" -core.py — core logic for pyflirt 💘 - -Currently includes: -- compliment(): generate a developer-themed compliment -""" - -import random - - -__all__ = ["compliment"] - - -def compliment(role="developer", mood="sweet", name=None, emojis=0, seed=None): - """ - Generate a developer-themed compliment. - - Args: - role (str): One of ["developer", "designer", "manager", "data"]. - mood (str): One of ["sweet", "cheeky", "nerdy"]. - name (str, optional): Person's name to personalize. - emojis (int): Number of 💖 emojis to append. - seed (int, optional): Random seed for deterministic selection. - - Returns: - str: A playful compliment string. - """ - rng = random.Random(seed) if seed is not None else random - - 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}", - ], - }, - } - - # Normalize and validate - role = role.lower() - mood = mood.lower() - if role not in templates: - raise ValueError(f"Unknown role '{role}'. Choose from {list(templates.keys())}.") - if mood not in templates[role]: - raise ValueError(f"Unknown mood '{mood}'. Choose from {list(templates[role].keys())}.") - - # Select random compliment - template = rng.choice(templates[role][mood]) - name_bit = f", {name}" if name else "" - compliment_text = template.format(name_bit=name_bit) - - # Add emojis if requested - if emojis > 0: - compliment_text += " " + "💖" * emojis - - return compliment_text 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/src/pyflirt/main.py b/src/pyflirt/main.py deleted file mode 100644 index 42a2ffe..0000000 --- a/src/pyflirt/main.py +++ /dev/null @@ -1,8 +0,0 @@ -from .core import compliment - -def main(): - print("💘 Welcome to pyflirt!") - print(compliment(role="developer", mood="cheeky", name="Alex", emojis=2)) - -if __name__ == "__main__": - main() diff --git a/tests/test_compliment.py b/tests/test_compliment.py deleted file mode 100644 index 93f7c30..0000000 --- a/tests/test_compliment.py +++ /dev/null @@ -1,35 +0,0 @@ -import pytest -from pyflirt import compliment - - -def test_compliment_returns_string(): - """Should always return a non-empty string.""" - out = compliment() - assert isinstance(out, str) - assert len(out) > 0 - - -def test_compliment_includes_name(): - """Name should appear in the compliment when provided.""" - result = compliment(role="developer", mood="nerdy", name="Alex", seed=1) - assert "Alex" in result - - -def test_compliment_emojis_count(): - """Correct number of emojis should be appended.""" - result = compliment(role="data", emojis=3, seed=2) - assert result.endswith("💖💖💖") - - -def test_compliment_is_deterministic_with_seed(): - """Using the same seed should produce the same output.""" - a = compliment(role="designer", mood="cheeky", seed=42) - b = compliment(role="designer", mood="cheeky", seed=42) - assert a == b - - -def test_compliment_differs_with_different_seeds(): - """Different seeds should generally give different compliments.""" - a = compliment(role="designer", mood="cheeky", seed=42) - b = compliment(role="designer", mood="cheeky", seed=43) - assert a != b diff --git a/tests/test_pyflirt.py b/tests/test_pyflirt.py index e397366..cda6cf6 100644 --- a/tests/test_pyflirt.py +++ b/tests/test_pyflirt.py @@ -1,4 +1,7 @@ from pyflirt import line, lines, categories +import pytest +from pyflirt import compliment + def test_categories_present(): assert "nerdy" in categories() @@ -17,3 +20,35 @@ def test_cheese_bounds(): line(cheese=0) with pytest.raises(ValueError): lines(n=2, cheese=6) + +def test_compliment_returns_string(): + """Should always return a non-empty string.""" + out = compliment() + assert isinstance(out, str) + assert len(out) > 0 + + +def test_compliment_includes_name(): + """Name should appear in the compliment when provided.""" + result = compliment(role="developer", mood="nerdy", name="Alex", seed=1) + assert "Alex" in result + + +def test_compliment_emojis_count(): + """Correct number of emojis should be appended.""" + result = compliment(role="data", emojis=3, seed=2) + assert result.endswith("💖💖💖") + + +def test_compliment_is_deterministic_with_seed(): + """Using the same seed should produce the same output.""" + a = compliment(role="designer", mood="cheeky", seed=42) + b = compliment(role="designer", mood="cheeky", seed=42) + assert a == b + + +def test_compliment_differs_with_different_seeds(): + """Different seeds should generally give different compliments.""" + a = compliment(role="designer", mood="cheeky", seed=42) + b = compliment(role="designer", mood="cheeky", seed=43) + assert a != b From 7f2178c4a7f108322996fde36c373cf4806acba6 Mon Sep 17 00:00:00 2001 From: MATTHEW VIOLA Date: Mon, 3 Nov 2025 17:14:11 -0500 Subject: [PATCH 04/26] added search function and test --- src/pyflirt/api.py | 31 +++++++++++++++++++++++++++++-- tests/test_pyflirt.py | 8 +++++++- 2 files changed, 36 insertions(+), 3 deletions(-) diff --git a/src/pyflirt/api.py b/src/pyflirt/api.py index 405fb61..1ad8b47 100644 --- a/src/pyflirt/api.py +++ b/src/pyflirt/api.py @@ -4,7 +4,7 @@ from typing import cast, Dict -__all__ = ["line", "lines", "categories"] +__all__ = ["line", "lines", "categories", "search"] def categories() -> List[str]: """Return a sorted list of all available pickup line categories.""" @@ -97,4 +97,31 @@ def compliment(role="developer", mood="sweet", name=None, emojis=0, seed=None): if emojis > 0: text += " " + "💖" * emojis - return text \ No newline at end of file + 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 diff --git a/tests/test_pyflirt.py b/tests/test_pyflirt.py index cda6cf6..fcc7055 100644 --- a/tests/test_pyflirt.py +++ b/tests/test_pyflirt.py @@ -1,4 +1,4 @@ -from pyflirt import line, lines, categories +from pyflirt import line, lines, categories, search import pytest from pyflirt import compliment @@ -52,3 +52,9 @@ def test_compliment_differs_with_different_seeds(): a = compliment(role="designer", mood="cheeky", seed=42) b = compliment(role="designer", mood="cheeky", seed=43) assert a != b + +def test_lines_search(): + results = search("Are") + for result in results: + result = result.lower() + assert result.contains("are") From d06baa81ae2e64cec81f927370421bab5ebe1dec Mon Sep 17 00:00:00 2001 From: SamMurshed Date: Mon, 3 Nov 2025 21:03:27 -0500 Subject: [PATCH 05/26] Units tests + rater --- src/pyflirt/__init__.py | 9 +++++---- src/pyflirt/api.py | 29 ++++++++++++++++++++++++++++- tests/test_pyflirt.py | 23 +++++++++++++++++++++++ 3 files changed, 56 insertions(+), 5 deletions(-) diff --git a/src/pyflirt/__init__.py b/src/pyflirt/__init__.py index 31d78b3..ab4b1d3 100644 --- a/src/pyflirt/__init__.py +++ b/src/pyflirt/__init__.py @@ -7,17 +7,18 @@ - compliment(role, mood, name, emojis, seed) - rate_line(text, metric, seed) """ -from .api import line, lines, categories, compliment +from .api import line, lines, categories, compliment, rate_line -from .core import ( - compliment, -) +#from .core import ( +# compliment, +#) __all__ = [ "compliment", "line", "lines", "categories", + "rate_line" ] __version__ = "0.1.0" diff --git a/src/pyflirt/api.py b/src/pyflirt/api.py index 405fb61..62318e4 100644 --- a/src/pyflirt/api.py +++ b/src/pyflirt/api.py @@ -97,4 +97,31 @@ def compliment(role="developer", mood="sweet", name=None, emojis=0, seed=None): if emojis > 0: text += " " + "💖" * emojis - return text \ No newline at end of file + return text + +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) \ No newline at end of file diff --git a/tests/test_pyflirt.py b/tests/test_pyflirt.py index cda6cf6..69bb827 100644 --- a/tests/test_pyflirt.py +++ b/tests/test_pyflirt.py @@ -1,6 +1,8 @@ from pyflirt import line, lines, categories import pytest from pyflirt import compliment +from pyflirt import rate_line + def test_categories_present(): @@ -52,3 +54,24 @@ def test_compliment_differs_with_different_seeds(): a = compliment(role="designer", mood="cheeky", seed=42) b = compliment(role="designer", mood="cheeky", seed=43) assert a != b + +def test_rate_line_length(): + short_line = "Hi!" + long_line = "This is a really long pickup line that could be annoying." + assert rate_line(short_line, metric="length") > rate_line(long_line, metric="length") + +def test_rate_line_cheese_level(): + cheesy = "You have a sweet heart." + bland = "Hello." + assert rate_line(cheesy, metric="cheese_level") > rate_line(bland, metric="cheese_level") + +def test_rate_line_random_seed(): + r1 = rate_line("Test", metric="random", seed=42) + r2 = rate_line("Test", metric="random", seed=42) + r3 = rate_line("Test", metric="random", seed=43) + assert r1 == r2 + assert r1 != r3 + +def test_rate_line_invalid_metric(): + with pytest.raises(ValueError): + rate_line("test", metric="unknown") From d5a19af2f02a4ac17dd048cccbab602602996d3f Mon Sep 17 00:00:00 2001 From: SamMurshed Date: Mon, 3 Nov 2025 21:07:04 -0500 Subject: [PATCH 06/26] update ci.yml --- .github/workflows/ci.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index ef9940c..3a2a13a 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -18,7 +18,7 @@ jobs: strategy: fail-fast: false matrix: - python-version: ["3.10", "3.12"] + python-version: ["3.10", "3.11"] steps: - name: Checkout From f02479462756a690eb7491de09217450e30a2ab6 Mon Sep 17 00:00:00 2001 From: SamMurshed Date: Mon, 3 Nov 2025 21:22:05 -0500 Subject: [PATCH 07/26] Try 3 --- .github/workflows/ci.yml | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 3a2a13a..f9b5090 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -25,11 +25,14 @@ jobs: uses: actions/checkout@v4 - name: Set up Python - uses: actions/setup-python@v5 + - 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 @@ -37,8 +40,11 @@ jobs: - name: Install dependencies with Pipenv run: | - pipenv install --dev --ignore-pipfile + 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: | From dddccaa795a68959b1b99c45df7697140b25f26f Mon Sep 17 00:00:00 2001 From: jjl9930 Date: Tue, 4 Nov 2025 19:27:06 -0500 Subject: [PATCH 08/26] Update api.py --- src/pyflirt/api.py | 51 +++++++++++++++++++++++++++++++++++++++++++++- 1 file changed, 50 insertions(+), 1 deletion(-) diff --git a/src/pyflirt/api.py b/src/pyflirt/api.py index 405fb61..f3d11f6 100644 --- a/src/pyflirt/api.py +++ b/src/pyflirt/api.py @@ -2,6 +2,8 @@ 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"] @@ -97,4 +99,51 @@ def compliment(role="developer", mood="sweet", name=None, emojis=0, seed=None): if emojis > 0: text += " " + "💖" * emojis - return text \ No newline at end of file + return text +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 From 4ce8d09a783390f6e8453579da06f7cb2cd56ac1 Mon Sep 17 00:00:00 2001 From: jjl9930 Date: Tue, 4 Nov 2025 20:36:31 -0500 Subject: [PATCH 09/26] Delete pyflirt directory --- pyflirt/__init__.py | 1 - 1 file changed, 1 deletion(-) delete mode 100644 pyflirt/__init__.py diff --git a/pyflirt/__init__.py b/pyflirt/__init__.py deleted file mode 100644 index 8b13789..0000000 --- a/pyflirt/__init__.py +++ /dev/null @@ -1 +0,0 @@ - From c144822600c231c8ea6b19059b85de37f94269e4 Mon Sep 17 00:00:00 2001 From: jjl9930 Date: Tue, 4 Nov 2025 20:36:51 -0500 Subject: [PATCH 10/26] Update __init__.py --- src/pyflirt/__init__.py | 22 ++++++---------------- 1 file changed, 6 insertions(+), 16 deletions(-) diff --git a/src/pyflirt/__init__.py b/src/pyflirt/__init__.py index 31d78b3..63454b1 100644 --- a/src/pyflirt/__init__.py +++ b/src/pyflirt/__init__.py @@ -1,23 +1,13 @@ +# src/pyflirt/__init__.py """ pyflirt 💘 - -APIs: +Public API: +- categories() - line(category, name, cheese, seed) -- lines(n, categories, name, cheese, seed) +- lines(n, category, name, cheese, seed) - compliment(role, mood, name, emojis, seed) -- rate_line(text, metric, seed) """ -from .api import line, lines, categories, compliment - -from .core import ( - compliment, -) - -__all__ = [ - "compliment", - "line", - "lines", - "categories", -] +from .api import categories, line, lines, compliment +__all__ = ["categories", "line", "lines", "compliment"] __version__ = "0.1.0" From 87fffc682ff50c2de8b97e4ad746d7a1a654b2d3 Mon Sep 17 00:00:00 2001 From: jjl9930 Date: Tue, 4 Nov 2025 20:37:49 -0500 Subject: [PATCH 11/26] Update test_pyflirt.py --- tests/test_pyflirt.py | 66 +++++++++++++++++-------------------------- 1 file changed, 26 insertions(+), 40 deletions(-) diff --git a/tests/test_pyflirt.py b/tests/test_pyflirt.py index cda6cf6..277f26b 100644 --- a/tests/test_pyflirt.py +++ b/tests/test_pyflirt.py @@ -1,54 +1,40 @@ -from pyflirt import line, lines, categories import pytest -from pyflirt import compliment +from pyflirt import categories, line, lines, compliment +def test_categories_sorted_and_nonempty(): + cats = categories() + assert cats == sorted(cats) + assert len(cats) > 0 -def test_categories_present(): +def test_categories_reflect_bank_contents(): + # If BANK has "nerdy" in data, ensure it's present assert "nerdy" in categories() -def test_line_seed_stable(): - assert line(category="nerdy", seed=42) == line(category="nerdy", seed=42) - -def test_lines_len_and_content(): - arr = lines(n=3, name="Sam", seed=123) - assert len(arr) == 3 - assert all(isinstance(s, str) and s for s in arr) - -def test_cheese_bounds(): - import pytest +def test_line_invalid_cheese_raises(): with pytest.raises(ValueError): line(cheese=0) with pytest.raises(ValueError): - lines(n=2, cheese=6) - -def test_compliment_returns_string(): - """Should always return a non-empty string.""" - out = compliment() - assert isinstance(out, str) - assert len(out) > 0 - - -def test_compliment_includes_name(): - """Name should appear in the compliment when provided.""" - result = compliment(role="developer", mood="nerdy", name="Alex", seed=1) - assert "Alex" in result + line(cheese=6) +def test_line_unknown_category_raises(): + with pytest.raises(ValueError): + line(category="not-a-cat") -def test_compliment_emojis_count(): - """Correct number of emojis should be appended.""" - result = compliment(role="data", emojis=3, seed=2) - assert result.endswith("💖💖💖") +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_is_deterministic_with_seed(): - """Using the same seed should produce the same output.""" - a = compliment(role="designer", mood="cheeky", seed=42) - b = compliment(role="designer", mood="cheeky", seed=42) - assert a == b +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_differs_with_different_seeds(): - """Different seeds should generally give different compliments.""" - a = compliment(role="designer", mood="cheeky", seed=42) - b = compliment(role="designer", mood="cheeky", seed=43) - assert a != b +def test_compliment_emojis_appended(): + base = compliment(role="developer", mood="sweet", emojis=3, seed=1) + assert base.endswith("💖💖💖") From c15ec594552e583dd88697131363b422ca39608c Mon Sep 17 00:00:00 2001 From: jjl9930 Date: Tue, 4 Nov 2025 20:43:46 -0500 Subject: [PATCH 12/26] Update ci.yml --- .github/workflows/ci.yml | 44 +++++++++------------------------------- 1 file changed, 10 insertions(+), 34 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index ef9940c..91c67f7 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -2,52 +2,28 @@ name: CI on: pull_request: - branches: [ pipfile-experiment ] + branches: [ main ] workflow_dispatch: -permissions: - contents: read - -concurrency: - group: ci-${{ github.ref }} - cancel-in-progress: true - jobs: test-and-build: runs-on: ubuntu-latest strategy: - fail-fast: false matrix: python-version: ["3.10", "3.12"] - steps: - - name: Checkout - uses: actions/checkout@v4 - - - name: Set up Python - uses: actions/setup-python@v5 + - uses: actions/checkout@v4 + - uses: actions/setup-python@v5 with: python-version: ${{ matrix.python-version }} - cache: pip - - - 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 install --dev --ignore-pipfile - pipenv run pip install -e . - + - name: Install pipenv + run: python -m pip install --upgrade pip pipenv + - name: Sync dependencies + run: pipenv sync --dev - name: Run tests - run: | - pipenv run pytest -q - - - name: Build package (sdist & wheel) - run: | - pipenv run python -m build - + 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: From b5341f9ec4ac3c0f8a4ad1dd6227df438037cf1a Mon Sep 17 00:00:00 2001 From: HelenZhutt Date: Wed, 5 Nov 2025 01:24:07 -0600 Subject: [PATCH 13/26] Add more functions, tests and the demo to run all the functions --- README.md | 129 +++++++++++++++++++++++++++++++++++++++- examples/demo.py | 32 ++++++++++ src/pyflirt/__init__.py | 23 +++---- src/pyflirt/api.py | 53 ++++++++++++++++- tests/test_pyflirt.py | 53 ++++++++++++++++- 5 files changed, 274 insertions(+), 16 deletions(-) create mode 100644 examples/demo.py 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/src/pyflirt/__init__.py b/src/pyflirt/__init__.py index 31d78b3..613812a 100644 --- a/src/pyflirt/__init__.py +++ b/src/pyflirt/__init__.py @@ -7,17 +7,18 @@ - compliment(role, mood, name, emojis, seed) - rate_line(text, metric, seed) """ -from .api import line, lines, categories, compliment +from .api import line, lines, categories, compliment, search, stats -from .core import ( - compliment, -) - -__all__ = [ - "compliment", - "line", - "lines", - "categories", -] +__all__ = ["line", "lines", "categories", "compliment", "search", "stats"] __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 index 405fb61..bbc9639 100644 --- a/src/pyflirt/api.py +++ b/src/pyflirt/api.py @@ -4,7 +4,7 @@ from typing import cast, Dict -__all__ = ["line", "lines", "categories"] +__all__ = ["line", "lines", "categories", "compliment", "search", "stats"] def categories() -> List[str]: """Return a sorted list of all available pickup line categories.""" @@ -97,4 +97,53 @@ def compliment(role="developer", mood="sweet", name=None, emojis=0, seed=None): if emojis > 0: text += " " + "💖" * emojis - return text \ No newline at end of file + return text + +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} \ No newline at end of file diff --git a/tests/test_pyflirt.py b/tests/test_pyflirt.py index cda6cf6..f1adbe1 100644 --- a/tests/test_pyflirt.py +++ b/tests/test_pyflirt.py @@ -1,7 +1,7 @@ from pyflirt import line, lines, categories import pytest from pyflirt import compliment - +import re def test_categories_present(): assert "nerdy" in categories() @@ -52,3 +52,54 @@ def test_compliment_differs_with_different_seeds(): a = compliment(role="designer", mood="cheeky", seed=42) b = compliment(role="designer", mood="cheeky", seed=43) assert a != b + +def test_stats_shapes_and_sums(): + s = pyflirt.stats() + assert isinstance(s, dict) + assert "total" in s and "by_category" in s and "cheese_hist" in s + cats = pyflirt.categories() + assert set(s["by_category"].keys()) == set(cats) + assert set(s["cheese_hist"].keys()) == {1, 2, 3, 4, 5} + assert s["total"] == sum(s["by_category"].values()) + assert s["total"] == sum(s["cheese_hist"].values()) + +def test_search_basic_and_limit_seed(): + sample = pyflirt.line(seed=12345) + token = "" + for t in re.findall(r"[A-Za-z]+", sample): + if len(t) >= 3: + token = t + break + if not token: + token = "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] + tl = token.lower() + assert all(tl in s.lower() for s in r1) + +def test_search_filters_and_errors(): + token = "the" + a = pyflirt.search(token, cheese=1, limit=50, seed=1) + b = pyflirt.search(token, cheese=5, limit=50, seed=1) + assert len(a) <= len(b) + try: + pyflirt.search("x", category="__nope__", limit=1) + ok = False + except ValueError: + ok = True + assert ok + + try: + pyflirt.search("", limit=1) + ok = False + except ValueError: + ok = True + assert ok + From 7bac3e28ef1868793bcba29e944f6d3248fbb669 Mon Sep 17 00:00:00 2001 From: HelenZhutt Date: Wed, 5 Nov 2025 02:10:28 -0600 Subject: [PATCH 14/26] Fix error appear due to late merge of pr --- .gitignore | 19 +++++++++++++++++++ README.md | 6 +++++- src/pyflirt/__init__.py | 7 ++----- src/pyflirt/api.py | 30 ++---------------------------- 4 files changed, 28 insertions(+), 34 deletions(-) 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/README.md b/README.md index dbbfa13..387cdd2 100644 --- a/README.md +++ b/README.md @@ -1,5 +1,7 @@ # 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 Python package that gives you developer-themed pickup lines and compliments. Because coding should be fun. ## What is this? @@ -125,4 +127,6 @@ python -m build ## 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. +- [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) + diff --git a/src/pyflirt/__init__.py b/src/pyflirt/__init__.py index 54c3d88..9131214 100644 --- a/src/pyflirt/__init__.py +++ b/src/pyflirt/__init__.py @@ -8,12 +8,9 @@ - compliment(role, mood, name, emojis, seed) """ -from .api import line, lines, categories, compliment, search, stats +from .api import line, lines, categories, compliment, search, stats, rate_line -__all__ = ["line", "lines", "categories", "compliment", "search", "stats", "rate_line"] - - -__all__ = ["categories", "line", "lines", "compliment"] +__all__ = ["categories", "line", "lines", "compliment", "search", "stats", "rate_line"] __version__ = "0.1.0" # Provide a convenient module reference for test usage like `pyflirt.search(...)` diff --git a/src/pyflirt/api.py b/src/pyflirt/api.py index a9858d7..b90a300 100644 --- a/src/pyflirt/api.py +++ b/src/pyflirt/api.py @@ -101,36 +101,10 @@ def compliment(role="developer", mood="sweet", name=None, emojis=0, seed=None): 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, + width: Optional[int] = None, uppercase: bool = False, color: Literal["auto", "none", "magenta", "cyan", "green"] = "auto", ) -> str: @@ -161,7 +135,7 @@ def say( name: Optional[str] = None, cheese: int = 2, seed: Optional[int] = None, - width: int | None = None, + width: Optional[int] = None, uppercase: bool = False, color: Literal["auto", "none", "magenta", "cyan", "green"] = "auto", emojis: int = 0, From c103aaa297f52d2d4a59f608483a11e85b22bff6 Mon Sep 17 00:00:00 2001 From: HelenZhutt Date: Wed, 5 Nov 2025 02:25:12 -0600 Subject: [PATCH 15/26] Add at least three test for each function --- tests/test_pyflirt.py | 190 ++++++++++++++++++++++++++++++++++++------ 1 file changed, 165 insertions(+), 25 deletions(-) diff --git a/tests/test_pyflirt.py b/tests/test_pyflirt.py index 9fab6d1..c129bd4 100644 --- a/tests/test_pyflirt.py +++ b/tests/test_pyflirt.py @@ -1,44 +1,184 @@ -from pyflirt import line, lines, categories, search +from pyflirt import line, lines, categories, search, compliment, rate_line 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_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_categories_reflect_bank_contents(): - # If BANK has "nerdy" in data, ensure it's present - assert "nerdy" in categories() +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_invalid_cheese_raises(): +def test_line_cheese_bounds(): with pytest.raises(ValueError): line(cheese=0) with pytest.raises(ValueError): line(cheese=6) -def test_line_unknown_category_raises(): +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): - line(category="not-a-cat") + 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_lines_n_zero_returns_empty(): - assert lines(n=0) == [] +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" -def test_lines_invalid_cheese_raises(): + 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): - lines(cheese=99) + 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_compliment_invalid_role_raises(): +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): - compliment(role="astronaut") + pyflirt.rate_line("x", metric="nope") -def test_compliment_invalid_mood_raises(): +def test__check_cat_none_ok_and_invalid_raises(): + fn = getattr(pyflirt, "_check_cat") + assert fn(None) is None with pytest.raises(ValueError): - compliment(role="developer", mood="salty") + 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_compliment_emojis_appended(): - base = compliment(role="developer", mood="sweet", emojis=3, seed=1) - assert base.endswith("💖💖💖") +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 From d8444d677165b49a4126691553367fe7fb9f2e61 Mon Sep 17 00:00:00 2001 From: HelenZhutt Date: Wed, 5 Nov 2025 02:31:25 -0600 Subject: [PATCH 16/26] Fix init to make the test run --- src/pyflirt/__init__.py | 36 +++++++++++++++++++++++++++++++----- 1 file changed, 31 insertions(+), 5 deletions(-) diff --git a/src/pyflirt/__init__.py b/src/pyflirt/__init__.py index 9131214..af936e6 100644 --- a/src/pyflirt/__init__.py +++ b/src/pyflirt/__init__.py @@ -6,19 +6,45 @@ - 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) """ -from .api import line, lines, categories, compliment, search, stats, rate_line +from .api import ( + line, + lines, + categories, + compliment, + search, + stats, + rate_line, + stylize, + say, + _check_cat, + _with_name, + _pool, +) -__all__ = ["categories", "line", "lines", "compliment", "search", "stats", "rate_line"] +__all__ = [ + "categories", + "line", + "lines", + "compliment", + "search", + "stats", + "rate_line", + "stylize", + "say", +] __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 From 592fe0f119c738c96c9bdc0a46aaabab1c6c8369 Mon Sep 17 00:00:00 2001 From: HelenZhutt Date: Wed, 5 Nov 2025 02:38:48 -0600 Subject: [PATCH 17/26] Update the demo of running all functions --- README.md | 12 ++++++++++++ examples/demo.py | 30 ++++++++++++++++++++++++++++-- 2 files changed, 40 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index 387cdd2..615b69e 100644 --- a/README.md +++ b/README.md @@ -32,6 +32,18 @@ print(lines(n=5, category="cs")) 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`): + +```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)` diff --git a/examples/demo.py b/examples/demo.py index 10d45ac..db4bd2f 100644 --- a/examples/demo.py +++ b/examples/demo.py @@ -1,4 +1,14 @@ -from pyflirt import line, lines, categories, compliment, search, stats +from pyflirt import ( + line, + lines, + categories, + compliment, + search, + stats, + stylize, + say, + rate_line, +) def main(): print("== categories() ==") @@ -18,7 +28,6 @@ def main(): 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) @@ -28,5 +37,22 @@ def main(): 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)) + if __name__ == "__main__": main() From 6fa8008e3eb71562f22a441d5219eae681029442 Mon Sep 17 00:00:00 2001 From: HelenZhutt Date: Wed, 5 Nov 2025 03:33:59 -0600 Subject: [PATCH 18/26] upload links and fix ci yaml --- .github/workflows/ci.yml | 39 ++++++++-------------- README.md | 72 ++++++++++++++++++++++++++++++++++++++-- 2 files changed, 83 insertions(+), 28 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index ae46db3..3391b5b 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -2,7 +2,9 @@ name: CI on: pull_request: - branches: [ main ] + branches: [ "main" ] + push: + branches: [ "main" ] workflow_dispatch: jobs: @@ -11,47 +13,32 @@ jobs: 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 + 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 + - name: Upgrade pip & install tools run: | python -m pip install --upgrade pip - python -m pip install build pytest pipenv + python -m pip install pipenv build pytest - - name: Install dependencies with Pipenv + - name: Install deps (Pipenv, system mode) run: | - pipenv --python $(which python) install --dev --ignore-pipfile - pipenv run pip install -e . - - - name: Install package editable - run: pipenv run pip install -e . + pipenv install --dev --deploy --system + python -m pip install -e . - name: Run tests - run: pipenv run pytest -q + run: pytest -q + - name: Build (sdist & wheel) - run: pipenv run python -m build + run: python -m build + - name: Upload dist artifacts uses: actions/upload-artifact@v4 with: diff --git a/README.md b/README.md index 615b69e..63d2599 100644 --- a/README.md +++ b/README.md @@ -2,7 +2,7 @@ [![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 Python package that gives you developer-themed pickup lines and compliments. Because coding should be fun. +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? @@ -97,6 +97,69 @@ 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") +``` + ## Development Setup If you want to work on this package: @@ -134,11 +197,16 @@ python -m build ## Links -- [PyPI Package](https://pypi.org/project/pyflirt/) +- [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) + +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) From 155eb8d92df6dbee9d1db6385438f65ec5aa0076 Mon Sep 17 00:00:00 2001 From: HelenZhutt Date: Wed, 5 Nov 2025 03:46:19 -0600 Subject: [PATCH 19/26] Fix ci yaml --- .github/workflows/ci.yml | 15 +++++---------- 1 file changed, 5 insertions(+), 10 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 3391b5b..0d96beb 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -2,9 +2,9 @@ name: CI on: pull_request: - branches: [ "main" ] + branches: [ "pipfile-experiment" ] push: - branches: [ "main" ] + branches: [ "pipfile-experiment" ] workflow_dispatch: jobs: @@ -15,7 +15,7 @@ jobs: python-version: ["3.10", "3.12"] steps: - - name: Checkout + - name: Check out code uses: actions/checkout@v4 - name: Set up Python @@ -23,15 +23,10 @@ jobs: with: python-version: ${{ matrix.python-version }} - - name: Upgrade pip & install tools + - name: Install dependencies run: | python -m pip install --upgrade pip - python -m pip install pipenv build pytest - - - name: Install deps (Pipenv, system mode) - run: | - pipenv install --dev --deploy --system - python -m pip install -e . + python -m pip install -e . pytest build - name: Run tests run: pytest -q From fdff5d0d5d26087a08f90f6d75d24d7fead4f173 Mon Sep 17 00:00:00 2001 From: danielleesignup Date: Wed, 5 Nov 2025 11:38:11 -0500 Subject: [PATCH 20/26] Added my name to contributers in README --- README.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/README.md b/README.md index 63d2599..7a21ac2 100644 --- a/README.md +++ b/README.md @@ -205,6 +205,8 @@ python -m build 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) From c2fa9dd8b5bde950e7e8f4f52eceae29ef5e29b6 Mon Sep 17 00:00:00 2001 From: danielleesignup Date: Wed, 5 Nov 2025 12:03:19 -0500 Subject: [PATCH 21/26] Adding functions rainbow and ascii heart --- src/pyflirt/__init__.py | 6 +++++ src/pyflirt/api.py | 53 +++++++++++++++++++++++++++++++++++++++++ 2 files changed, 59 insertions(+) diff --git a/src/pyflirt/__init__.py b/src/pyflirt/__init__.py index af936e6..1eafcf4 100644 --- a/src/pyflirt/__init__.py +++ b/src/pyflirt/__init__.py @@ -11,6 +11,8 @@ - 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 ( @@ -26,6 +28,8 @@ _check_cat, _with_name, _pool, + rainbow, + ascii_heart ) __all__ = [ @@ -38,6 +42,8 @@ "rate_line", "stylize", "say", + "rainbow", + "ascii_heart", ] __version__ = "0.1.0" diff --git a/src/pyflirt/api.py b/src/pyflirt/api.py index b90a300..83b5e80 100644 --- a/src/pyflirt/api.py +++ b/src/pyflirt/api.py @@ -226,3 +226,56 @@ def stats() -> dict: 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") From 0bf130fd3beaa5c22c38a4bb1e57b564d559d0be Mon Sep 17 00:00:00 2001 From: danielleesignup Date: Wed, 5 Nov 2025 12:07:49 -0500 Subject: [PATCH 22/26] Added test functions for rainbow function --- tests/test_pyflirt.py | 22 ++++++++++++++++++++++ 1 file changed, 22 insertions(+) diff --git a/tests/test_pyflirt.py b/tests/test_pyflirt.py index c129bd4..0d228b0 100644 --- a/tests/test_pyflirt.py +++ b/tests/test_pyflirt.py @@ -182,3 +182,25 @@ def test__pool_filters_by_cheese_and_fallback(): 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}" \ No newline at end of file From cc58cc484e4d12f3f4d035383c72286dbfb79fcc Mon Sep 17 00:00:00 2001 From: danielleesignup Date: Wed, 5 Nov 2025 12:08:26 -0500 Subject: [PATCH 23/26] Added test functions for ascii heart function --- tests/test_pyflirt.py | 24 +++++++++++++++++++++++- 1 file changed, 23 insertions(+), 1 deletion(-) diff --git a/tests/test_pyflirt.py b/tests/test_pyflirt.py index 0d228b0..c44fc13 100644 --- a/tests/test_pyflirt.py +++ b/tests/test_pyflirt.py @@ -203,4 +203,26 @@ def test_rainbow_color_reset_at_end(): 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}" \ No newline at end of file + 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 From d5047cee999abfa055cf99559e5927e3e00f31af Mon Sep 17 00:00:00 2001 From: danielleesignup Date: Wed, 5 Nov 2025 12:09:13 -0500 Subject: [PATCH 24/26] Added import heart and rainbow to test file --- tests/test_pyflirt.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/test_pyflirt.py b/tests/test_pyflirt.py index c44fc13..c72d06f 100644 --- a/tests/test_pyflirt.py +++ b/tests/test_pyflirt.py @@ -1,4 +1,4 @@ -from pyflirt import line, lines, categories, search, compliment, rate_line +from pyflirt import line, lines, categories, search, compliment, rate_line, ascii_heart, rainbow import pytest import re From 1dafab1bfed8f160fcaa1c7ca67fc9b841c9ef7e Mon Sep 17 00:00:00 2001 From: danielleesignup Date: Wed, 5 Nov 2025 12:13:04 -0500 Subject: [PATCH 25/26] Updated demo.py to include new functions --- examples/demo.py | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/examples/demo.py b/examples/demo.py index db4bd2f..7054877 100644 --- a/examples/demo.py +++ b/examples/demo.py @@ -8,6 +8,8 @@ stylize, say, rate_line, + rainbow, + ascii_heart, ) def main(): @@ -54,5 +56,13 @@ def main(): 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() From d25681750c7f3c1f4846274552cd1cf20cde1626 Mon Sep 17 00:00:00 2001 From: danielleesignup Date: Wed, 5 Nov 2025 12:20:39 -0500 Subject: [PATCH 26/26] Updated readme to include new functions --- README.md | 20 +++++++++++++++++++- 1 file changed, 19 insertions(+), 1 deletion(-) diff --git a/README.md b/README.md index 7a21ac2..22b474d 100644 --- a/README.md +++ b/README.md @@ -34,7 +34,7 @@ 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`): +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 @@ -160,6 +160,24 @@ Example: 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: