From 1e89ee2a9097a622ff9896e26212faba2163c2db Mon Sep 17 00:00:00 2001 From: amiraadum Date: Tue, 28 Oct 2025 18:19:46 -0400 Subject: [PATCH 01/42] Add dataset of Manhattan restaurants near NYU --- nyc_restaurant_data.csv | 51 +++++++++++++++++++++++++++++++++++++++++ 1 file changed, 51 insertions(+) create mode 100644 nyc_restaurant_data.csv diff --git a/nyc_restaurant_data.csv b/nyc_restaurant_data.csv new file mode 100644 index 0000000..8e477de --- /dev/null +++ b/nyc_restaurant_data.csv @@ -0,0 +1,51 @@ +name,cuisine,neighborhood,price,rating,sample_dish +Reyna,Mediterranean,Greenwhich Village,$$$,4.5,Lamb Baklava +4 Charles Prime Rib,American,West Village,$$$$,4.5,Burger +César,French,Hudson Square,$$$$,4.0,Uni Toast +Torrisi,Italian,Nolita,$$$$,4.5,Tortellini Pomodoro +Gene's Restaurant,Italian,Greenwhich Village,$$$,4.0,Vodka Pasta +Strip House,American,Greenwhich Village,$$$$,4.8,Chocolate Cake +Da Andrea,Italian,Greenwhich Village,$$,4.5,Carbonara +Village Taverna,Greek,Greenwhich Village,$$$,4.8,Chicken Souvlaki +Il Cantinori,Tuscan,Greenwhich Village,$$$$,4.8,Pasta Trio +Morandi,Italian ,West Village,$$,3.5,Lemon Pasta +Little Owl,Mediterranean,West Village,$$$,4.8,Meatball Sliders +Buvette,French,West Village,$$,4.7,Croque Madame +Mole,Mexican,West Village,$$,3.5,Sizzling Fajitas +ATLA,Mexican,NoHo,$$$,3.5,Pork Al Pastor Gringa +La Palapa,Mexican,East Village,$$,4.5,Burrito Bowl +Raoul's,French,SoHo,$$$,4.0,Steak Au Poivre +La Mercerie,French,SoHo,$$$,3.5,Crepe Complete +Cafe Zaffri,Syrian,Flatiron,$$$$,3.0,Lamb Wellington +Motek,Mediterranean,Flatiron,$$$,4.9,Chicken Schnitzel +Hen House,Lebanese,East Village,$,4.5,Lamb Birria +The Smith,American,East Village,$$,4.6,Mac and Cheese +Gjelina,American,NoHo,$$$,4.6,Pomodoro Pizza +Estela,American,Nolita,$$$$,4.7,Endive Salad +Shuka,Mediterranean,SoHo,$$,4.5,Fried Halloumi +The Corner Store,American,SoHo,$$$$,4.5,Samoa Sundae +Cho Dang Gol,Korean,Koreatown,$$,4.6,Cod Roe Omelette +HOJOKBAN NYC,Korean,Koreatown,$$$,4.6,Shin Ramen Fried Rice +Jongro BBQ,Korean,Koreatown,$$$,4.5,Beef Short Ribs Gobdol +BCD Tofu House,Korean,Koreatown,$$,4.3,Seafood Pancake +MUI,Korean,Koreatown,$$,4.5,Grilled Spicy Chicken +Samwoojung,Korean,Koreatown,$$,4.5,Soup Bulgogi +BANGIA,Korean,Koreatown,$$,4.3,Lychee Soju +Take 31,Korean,Koreatown,$$,4.4,Korean Fried Chicken +Izakaya MEW,Japanese,Chelsea,$$,4.5,Mentai Spaghetti +House of Joy,Chinese,West Village,$$,4.1,Egg Tart +Kimura,Japanese,East Village,$$,4.5,Nabe +Karakatta,Japanese,West Village,$$,4.5,Spicy Ginger Stamina Ramen +Kyuramen,Japanese,East Village,$$,4.6,Tonkotsu Ramen +Ootoya,Japanese,East Village,$$,4.2,Katsu Toji +Chow House,Chinese,West Village,$$,4.6,Braised Fish Filet W Spicy Broad Sauce +Lao Ma Spicy,Chinese,East Village,$$,4.1,Stirfried Spicy Pot +Pranakhon,Thai,East Village,$$,4.7,Short Ribs +Top Thai Greenwich,Thai,West Village,$$,4.6,Pad Thai Noodles +Raku,Japanese,West Village,$$,4.5,Kani Ankake +Grandpa Thai,Thai,East Village,$$,4.8,Pad Krapow Moo +Mountain House,Chinese,East Village,$$,4.6,Beef Sour Soup +OLIO E PIU,Italian,West Village,$$$,4.7,Spaghetti Alla Carbonara +Gen Korean BBQ House,Korean,East Village,$$,3.9,Gen Premium Steak +886,Taiwanese,East Village,$$,4.2,Honey Glazed Popcorn Chicken +ZIZI,Mediterranean,Chelsea,$$,4.5,Overnight Lamb \ No newline at end of file From 14324edac8d6fe77eda21dc83b7661e46c6bf39b Mon Sep 17 00:00:00 2001 From: amiraadum Date: Tue, 28 Oct 2025 18:53:31 -0400 Subject: [PATCH 02/42] Add initial project structure --- Pipfile | 0 pyproject.toml | 0 2 files changed, 0 insertions(+), 0 deletions(-) create mode 100644 Pipfile create mode 100644 pyproject.toml diff --git a/Pipfile b/Pipfile new file mode 100644 index 0000000..e69de29 diff --git a/pyproject.toml b/pyproject.toml new file mode 100644 index 0000000..e69de29 From a8a149f47b316af94df197d31de5fdae6296760e Mon Sep 17 00:00:00 2001 From: hyunkyuu Date: Tue, 28 Oct 2025 19:09:06 -0400 Subject: [PATCH 03/42] chore: update Pipfile and structure --- Pipfile | 12 +++++++ Pipfile.lock | 62 +++++++++++++++++++++++++++++++++ src/_init__.py | 0 src/core.py | 0 tests/test_filter_by_cuisine.py | 0 tests/test_load_data.py | 0 6 files changed, 74 insertions(+) create mode 100644 Pipfile.lock create mode 100644 src/_init__.py create mode 100644 src/core.py create mode 100644 tests/test_filter_by_cuisine.py create mode 100644 tests/test_load_data.py diff --git a/Pipfile b/Pipfile index e69de29..35b0192 100644 --- a/Pipfile +++ b/Pipfile @@ -0,0 +1,12 @@ +[[source]] +url = "https://pypi.org/simple" +verify_ssl = true +name = "pypi" + +[packages] +pytest = "*" + +[dev-packages] + +[requires] +python_version = "3.12" diff --git a/Pipfile.lock b/Pipfile.lock new file mode 100644 index 0000000..d15cb7e --- /dev/null +++ b/Pipfile.lock @@ -0,0 +1,62 @@ +{ + "_meta": { + "hash": { + "sha256": "ba413e0ed345ea6f15dc61758fa792b94b11728a012571d4750856ce4706b5d9" + }, + "pipfile-spec": 6, + "requires": { + "python_version": "3.12" + }, + "sources": [ + { + "name": "pypi", + "url": "https://pypi.org/simple", + "verify_ssl": true + } + ] + }, + "default": { + "iniconfig": { + "hashes": [ + "sha256:c76315c77db068650d49c5b56314774a7804df16fee4402c1f19d6d15d8c4730", + "sha256:f631c04d2c48c52b84d0d0549c99ff3859c98df65b3101406327ecc7d53fbf12" + ], + "markers": "python_version >= '3.10'", + "version": "==2.3.0" + }, + "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" + }, + "pytest": { + "hashes": [ + "sha256:86c0d0b93306b961d58d62a4db4879f27fe25513d4b969df351abdddb3c30e01", + "sha256:872f880de3fc3a5bdc88a11b39c9710c3497a547cfa9320bc3c5e62fbf272e79" + ], + "index": "pypi", + "markers": "python_version >= '3.9'", + "version": "==8.4.2" + } + }, + "develop": {} +} diff --git a/src/_init__.py b/src/_init__.py new file mode 100644 index 0000000..e69de29 diff --git a/src/core.py b/src/core.py new file mode 100644 index 0000000..e69de29 diff --git a/tests/test_filter_by_cuisine.py b/tests/test_filter_by_cuisine.py new file mode 100644 index 0000000..e69de29 diff --git a/tests/test_load_data.py b/tests/test_load_data.py new file mode 100644 index 0000000..e69de29 From d22b082a811c2cfee6254bca49c647824a8946f3 Mon Sep 17 00:00:00 2001 From: amiraadum Date: Tue, 28 Oct 2025 19:11:06 -0400 Subject: [PATCH 04/42] Add pyproject.toml with build metadata for eatnyc package --- pyproject.toml | 22 ++++++++++++++++++++++ 1 file changed, 22 insertions(+) diff --git a/pyproject.toml b/pyproject.toml index e69de29..bd37600 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -0,0 +1,22 @@ +[build-system] +requires = ["setuptools>=68", "wheel"] +build-backend = "setuptools.build_meta" + +[project] +name = "eatnyc" +version = "0.1.0" +description = "A tiny NYC restaurant recommender near NYU." +readme = "README.md" +requires-python = ">=3.10" +license = {text = "MIT"} +authors = [{ + name = Team Avalon +}] +keywords = ["nyc", "restaurants", "fun", "food", recommendations] + +[tool.setuptools] +package_dir = {"" = "src"} + +[tool.setuptools.packages.find] +where = ["src"] + From 687b9966890802f7ecfb1f342a004314c591a581 Mon Sep 17 00:00:00 2001 From: hyunkyuu Date: Tue, 28 Oct 2025 19:19:41 -0400 Subject: [PATCH 05/42] chore: update Pipfile --- Pipfile | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Pipfile b/Pipfile index 35b0192..996ed5b 100644 --- a/Pipfile +++ b/Pipfile @@ -9,4 +9,4 @@ pytest = "*" [dev-packages] [requires] -python_version = "3.12" +python_version = ">=3.10" From dbb6ae048a8b36eda9007c3f8c53a5562de15213 Mon Sep 17 00:00:00 2001 From: hyunkyuu Date: Tue, 28 Oct 2025 19:26:16 -0400 Subject: [PATCH 06/42] fix: update Pipfile --- Pipfile | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Pipfile b/Pipfile index 996ed5b..6c56da5 100644 --- a/Pipfile +++ b/Pipfile @@ -9,4 +9,4 @@ pytest = "*" [dev-packages] [requires] -python_version = ">=3.10" +python_version = "3.10" From 5df05ba8ea14b763d78af9e78d10311edc4ce2e8 Mon Sep 17 00:00:00 2001 From: hyunkyuu Date: Tue, 28 Oct 2025 20:55:13 -0400 Subject: [PATCH 07/42] chore: update Pipfile.lock --- Pipfile.lock | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/Pipfile.lock b/Pipfile.lock index d15cb7e..0a46c35 100644 --- a/Pipfile.lock +++ b/Pipfile.lock @@ -1,11 +1,11 @@ { "_meta": { "hash": { - "sha256": "ba413e0ed345ea6f15dc61758fa792b94b11728a012571d4750856ce4706b5d9" + "sha256": "20e1aad87698ead018f54bacfa6e7e1d5f1f0cae78c447d1465c006cbc99a583" }, "pipfile-spec": 6, "requires": { - "python_version": "3.12" + "python_version": "3.10" }, "sources": [ { From 8dc8d0156b3b42d9009b2e1a9933a64f2026410c Mon Sep 17 00:00:00 2001 From: amiraadum Date: Thu, 30 Oct 2025 11:22:42 -0400 Subject: [PATCH 08/42] CI on PRs; publish on tags; verify install from TestPyPI --- .github/workflows/build.yaml | 74 ++++++++++++++++++++++++++++++++++++ 1 file changed, 74 insertions(+) create mode 100644 .github/workflows/build.yaml diff --git a/.github/workflows/build.yaml b/.github/workflows/build.yaml new file mode 100644 index 0000000..4a9bffe --- /dev/null +++ b/.github/workflows/build.yaml @@ -0,0 +1,74 @@ +name: Build and Test + +on: + pull_request: + branches: [pipfile-experiment] + push: + tags: ["v*"] + workflow_dispatch: + +jobs: + build: + runs-on: ubuntu-latest + timeout-minutes: 5 + strategy: + matrix: + python-version: ["3.10", "3.11"] + steps: + - uses: actions/checkout@v4 + + - name: Install Python, pipenv and Pipfile packages + uses: kojoru/prepare-pipenv@v1 + with: + python-version: ${{ matrix.python-version }} + + #installing requirements.txt + - name: Install requirements.txt + run: | + if [ -f requirements.txt ]; then pipenv run pip install -r requirements.txt; fi + + - name: Turn on 'editable' mode + run: | + pipenv install -e . + + - name: Test with pytest + run: | + pipenv install pytest + pipenv --venv + pipenv run python -m pytest + + deliver: + if: github.event_name != 'pull_request' + needs: [build] + runs-on: ubuntu-latest + timeout-minutes: 5 + steps: + - uses: actions/checkout@v4 + + - name: Install Python, pipenv and Pipfile packages + uses: kojoru/prepare-pipenv@v1 + + - name: Build package + run: | + pipenv install build + pipenv run python -m build . + + #change the following to the published one (not PyPI TEST) + - name: Publish to PyPI test server + uses: pypa/gh-action-pypi-publish@release/v1 + with: + password: ${{ secrets.TEST_PYPI_API_TOKEN }} + repository-url: https://test.pypi.org/legacy/ + + #ADDED to verify upload + # verify-upload: + # if: github.event_name != 'pull_request' + # needs: [deliver] + # runs-on: ubuntu-latest + # timeout-minutes: 5 + # steps: + # - name: Install from TestPyPI and import + # run: | + # python -m pip install --upgrade pip + # pip install --index-url https://test.pypi.org/simple/ --extra-index-url https://pypi.org/simple eatnyc + # python - <<'PY' import eatnyc print("Imported eatnyc OK. Version:", getattr(eatnyc, "__version__", "unknown")) PY \ No newline at end of file From e405fd2499ba72bf7b13811f83e3bd20a73c320d Mon Sep 17 00:00:00 2001 From: hyunkyuu Date: Thu, 30 Oct 2025 11:24:55 -0400 Subject: [PATCH 09/42] chore: revise it according to the prof's file structure --- src/core.py | 0 src/{ => eatnyc}/_init__.py | 0 src/eatnyc/core.py | 23 +++++++++++++++++++ .../eatnyc/data/nyc_restaurant_data.csv | 0 4 files changed, 23 insertions(+) delete mode 100644 src/core.py rename src/{ => eatnyc}/_init__.py (100%) create mode 100644 src/eatnyc/core.py rename nyc_restaurant_data.csv => src/eatnyc/data/nyc_restaurant_data.csv (100%) diff --git a/src/core.py b/src/core.py deleted file mode 100644 index e69de29..0000000 diff --git a/src/_init__.py b/src/eatnyc/_init__.py similarity index 100% rename from src/_init__.py rename to src/eatnyc/_init__.py diff --git a/src/eatnyc/core.py b/src/eatnyc/core.py new file mode 100644 index 0000000..b76f203 --- /dev/null +++ b/src/eatnyc/core.py @@ -0,0 +1,23 @@ +import csv +import random +import os + + +def load_data(path=None, validate=True): + return + + +def filter_restaurants(data, cuisine=None, borough=None, price=None, min_rating=None, vibe=None, limit=None): + return + + +def top_n(data, n=5, sort_by="rating", descending=True): + return + + +def sample_dish(cuisine=None, seed=None): + return + + +def format_card(row, style="ascii", width=48, show_vibes=True): + return diff --git a/nyc_restaurant_data.csv b/src/eatnyc/data/nyc_restaurant_data.csv similarity index 100% rename from nyc_restaurant_data.csv rename to src/eatnyc/data/nyc_restaurant_data.csv From 898999c702c81fc8332545d3067c2b72d91029b3 Mon Sep 17 00:00:00 2001 From: amiraadum Date: Thu, 30 Oct 2025 11:30:30 -0400 Subject: [PATCH 10/42] Add build workflow; stage __init__ change --- src/{_init__.py => __init__.py} | 0 1 file changed, 0 insertions(+), 0 deletions(-) rename src/{_init__.py => __init__.py} (100%) diff --git a/src/_init__.py b/src/__init__.py similarity index 100% rename from src/_init__.py rename to src/__init__.py From 0b268733e270eb97d7e947c9009f41661c2b1fdd Mon Sep 17 00:00:00 2001 From: amiraadum Date: Thu, 30 Oct 2025 12:47:57 -0400 Subject: [PATCH 11/42] Fix package structure, update pyproject.toml and build.yaml, verified load_data works --- .github/workflows/build.yaml | 10 +++--- pyproject.toml | 13 ++++---- src/eatnyc/__init__.py | 3 ++ src/eatnyc/_init__.py | 0 src/eatnyc/core.py | 64 ++++++++++++++++++++++++++++++++++-- 5 files changed, 77 insertions(+), 13 deletions(-) create mode 100644 src/eatnyc/__init__.py delete mode 100644 src/eatnyc/_init__.py diff --git a/.github/workflows/build.yaml b/.github/workflows/build.yaml index 4a9bffe..010a74a 100644 --- a/.github/workflows/build.yaml +++ b/.github/workflows/build.yaml @@ -54,11 +54,11 @@ jobs: pipenv run python -m build . #change the following to the published one (not PyPI TEST) - - name: Publish to PyPI test server - uses: pypa/gh-action-pypi-publish@release/v1 - with: - password: ${{ secrets.TEST_PYPI_API_TOKEN }} - repository-url: https://test.pypi.org/legacy/ + # - name: Publish to PyPI test server + # uses: pypa/gh-action-pypi-publish@release/v1 + # with: + # password: ${{ secrets.TEST_PYPI_API_TOKEN }} + # repository-url: https://test.pypi.org/legacy/ #ADDED to verify upload # verify-upload: diff --git a/pyproject.toml b/pyproject.toml index bd37600..0a6492b 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -8,15 +8,16 @@ version = "0.1.0" description = "A tiny NYC restaurant recommender near NYU." readme = "README.md" requires-python = ">=3.10" -license = {text = "MIT"} -authors = [{ - name = Team Avalon -}] -keywords = ["nyc", "restaurants", "fun", "food", recommendations] +license = { text = "MIT" } +authors = [{ name = "Team Avalon" }] +keywords = ["nyc", "restaurants", "fun", "food", "recommendations"] [tool.setuptools] -package_dir = {"" = "src"} +package-dir = {"" = "src"} [tool.setuptools.packages.find] where = ["src"] +[tool.setuptools.package-data] +eatnyc = ["data/*.csv"] + diff --git a/src/eatnyc/__init__.py b/src/eatnyc/__init__.py new file mode 100644 index 0000000..1b190d9 --- /dev/null +++ b/src/eatnyc/__init__.py @@ -0,0 +1,3 @@ +from .core import load_data, filter_restaurants, top_n, sample_dish, format_card +___all__ = ["load_data", "filter_restaurants", "top_n", "sample_dish", "format_card"] +__version__ = "0.1.0" \ No newline at end of file diff --git a/src/eatnyc/_init__.py b/src/eatnyc/_init__.py deleted file mode 100644 index e69de29..0000000 diff --git a/src/eatnyc/core.py b/src/eatnyc/core.py index e8c4d77..96c2b9d 100644 --- a/src/eatnyc/core.py +++ b/src/eatnyc/core.py @@ -1,10 +1,70 @@ import csv import random import os +from importlib.resources import files +_DATA_PKG = "eatnyc.data" +_DEFAULT_CSV = "nyc_restaurant_data.csv" + +#columns we expect in the CSV +_REQUIRED_COLS = {"name", "cuisine","neighborhood", "price", "rating", "sample_dish"} + + +#new function (NORMALIZE_ROWS) +def _normalize_row(row: dict) -> dict: + """Clean up one CSV row: strip spaces, normalize case/types, compute helper fields.""" + clean = {k.strip().lower(): (v.strip() if isinstance(v, str) else v) for k, v in row.items()} + + # rating → float (default 0.0) + try: + clean["rating"] = float(clean.get("rating", "") or 0.0) + except ValueError: + clean["rating"] = 0.0 + + # cuisines list (split on / , ; | ) + raw_c = clean.get("cuisine", "") + parts = [] + for sep in ["/", ",", ";", "|"]: + raw_c = raw_c.replace(sep, " ") + parts = [p for p in (w.strip().lower() for w in raw_c.split()) if p] + clean["_cuisines"] = parts # helper list for filtering + + # price normalize (e.g., $, $$, $$$) + clean["price"] = (clean.get("price") or "").strip() + + return clean + +#Load the NYC restaurant CSV into a list of dicts. +def load_data(path: str | None = None, validate: bool = True) -> list[dict]: + + #If `path` is None, loads the bundled file from eatnyc/data/. + #Keys are lowercased; rating is converted to float. + #Adds helper lists: `_cuisines`, `_vibes`. + + if path is None: + # use the packaged resource + csv_path = files(_DATA_PKG) / _DEFAULT_CSV + f = csv_path.open("r", encoding="utf-8") + close_after = True + else: + f = open(path, "r", encoding="utf-8") + close_after = True + + try: + reader = csv.DictReader(f) + # validate required headers + if validate: + cols = {c.strip().lower() for c in reader.fieldnames or []} + missing = _REQUIRED_COLS - cols + if missing: + raise ValueError(f"CSV missing required columns: {sorted(missing)}") + + data = [_normalize_row(r) for r in reader] + return data + finally: + if close_after: + f.close() -def load_data(path=None, validate=True): - return def filter_restaurants(data, cuisine=None, neighborhood=None, price=None, min_rating=None, vibe=None, limit=None): From 188435acb33f028a12ad6c34cebcdd89e10d4e51 Mon Sep 17 00:00:00 2001 From: amiraadum Date: Thu, 30 Oct 2025 12:58:19 -0400 Subject: [PATCH 12/42] Add load_data test so CI collects tests --- tests/test_load_data.py | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/tests/test_load_data.py b/tests/test_load_data.py index e69de29..fac0b7a 100644 --- a/tests/test_load_data.py +++ b/tests/test_load_data.py @@ -0,0 +1,9 @@ +from eatnyc import load_data, filter_restaurants, top_n, sample_dish + +def test_load_data_has_rows(): + data = load_data() + assert isinstance(data, list) + assert len(data) >= 1 + # basic schema check + for key in ["name", "cuisine", "neighborhood", "price", "rating", "sample_dish"]: + assert key in data[0] \ No newline at end of file From d65ddb2a7a476a0d7d3a006f9cc921c855f699b0 Mon Sep 17 00:00:00 2001 From: lilyluo7412 Date: Thu, 30 Oct 2025 14:37:50 -0400 Subject: [PATCH 13/42] Implement and test top_n function for sorting data Completed the implementation of the top_n function in core.py, including input validation, flexible sorting, and error handling. Added comprehensive tests in tests/test_top_n.py to verify sorting, parameter validation, and edge cases. Also fixed a typo in __init__.py for __all__. --- src/eatnyc/__init__.py | 2 +- src/eatnyc/core.py | 43 +++++++++++++++++++++++++++++++++++- tests/test_top_n.py | 49 ++++++++++++++++++++++++++++++++++++++++++ 3 files changed, 92 insertions(+), 2 deletions(-) create mode 100644 tests/test_top_n.py diff --git a/src/eatnyc/__init__.py b/src/eatnyc/__init__.py index 1b190d9..e4c2ddd 100644 --- a/src/eatnyc/__init__.py +++ b/src/eatnyc/__init__.py @@ -1,3 +1,3 @@ from .core import load_data, filter_restaurants, top_n, sample_dish, format_card -___all__ = ["load_data", "filter_restaurants", "top_n", "sample_dish", "format_card"] +__all__ = ["load_data", "filter_restaurants", "top_n", "sample_dish", "format_card"] __version__ = "0.1.0" \ No newline at end of file diff --git a/src/eatnyc/core.py b/src/eatnyc/core.py index 96c2b9d..7248073 100644 --- a/src/eatnyc/core.py +++ b/src/eatnyc/core.py @@ -72,7 +72,48 @@ def filter_restaurants(data, cuisine=None, neighborhood=None, price=None, min_ra def top_n(data, n=5, sort_by="rating", descending=True): - return + if not isinstance(data, list): + raise TypeError("data must be a list of dicts") + + if not data: + return [] + + # normalize sort key to lower-case to match load_data normalization + sort_key = (sort_by or "").strip().lower() + if not sort_key: + raise ValueError("sort_by must be a non-empty string") + + # validate the key exists in at least one row; otherwise error for clarity + if all((sort_key not in row) for row in data if isinstance(row, dict)): + raise KeyError(f"sort_by key not found in data rows: '{sort_key}'") + + def key_func(row): + # Missing keys sort as None -> treated as smallest when descending=False, largest when descending=True + value = row.get(sort_key) + # Ensure consistent comparison for mixed types + if isinstance(value, (int, float)): + return value + return ("" if value is None else str(value).lower()) + + try: + sorted_rows = sorted( + (r for r in data if isinstance(r, dict)), + key=key_func, + reverse=bool(descending), + ) + except TypeError: + # Fallback: convert all keys to string for sorting if mixed incomparable types + sorted_rows = sorted( + (r for r in data if isinstance(r, dict)), + key=lambda r: str(r.get(sort_key, "")), + reverse=bool(descending), + ) + + if n is None: + return sorted_rows + if not isinstance(n, int) or n < 0: + raise ValueError("n must be a non-negative integer or None") + return sorted_rows[:n] def sample_dish(cuisine=None, seed=None): diff --git a/tests/test_top_n.py b/tests/test_top_n.py new file mode 100644 index 0000000..0ac127c --- /dev/null +++ b/tests/test_top_n.py @@ -0,0 +1,49 @@ +from eatnyc import top_n + + +def _sample_data(): + return [ + {"name": "B", "cuisine": "X", "neighborhood": "N1", "price": "$$", "rating": 4.2, "sample_dish": "d1"}, + {"name": "A", "cuisine": "Y", "neighborhood": "N2", "price": "$$$", "rating": 4.8, "sample_dish": "d2"}, + {"name": "C", "cuisine": "Z", "neighborhood": "N3", "price": "$", "rating": 3.9, "sample_dish": "d3"}, + {"name": "D", "cuisine": "Y", "neighborhood": "N2", "price": "$$$", "rating": 4.8, "sample_dish": "d4"}, + ] + + +def test_top_n_default_rating_desc(): + data = _sample_data() + result = top_n(data, n=2) + assert len(result) == 2 + # default: sort_by="rating", descending=True + assert [r["name"] for r in result] == ["A", "D"] + + +def test_top_n_sort_by_name_ascending_all(): + data = _sample_data() + result = top_n(data, n=None, sort_by="name", descending=False) + assert [r["name"] for r in result] == ["A", "B", "C", "D"] + + +def test_top_n_n_zero_returns_empty(): + data = _sample_data() + result = top_n(data, n=0) + assert result == [] + + +def test_top_n_invalid_params_raise(): + data = _sample_data() + # invalid n + try: + top_n(data, n=-1) + assert False, "expected ValueError" + except ValueError: + pass + + # missing key + try: + top_n(data, sort_by="does_not_exist") + assert False, "expected KeyError" + except KeyError: + pass + + From 3cc6b3429ec850b8db1c439c3a08d92fe2ca8f34 Mon Sep 17 00:00:00 2001 From: amiraadum Date: Thu, 30 Oct 2025 16:07:26 -0400 Subject: [PATCH 14/42] Add demo.py, improve load_data and tests, update pyproject.toml and README --- README.md | 3 +++ examples/demo.py | 11 +++++++++++ pyproject.toml | 13 ++++++++++++- src/eatnyc/core.py | 19 +++++++++++++++++++ ..._cuisine.py => test_filter_restaurants.py} | 0 tests/test_format_card.py | 0 tests/test_load_data.py | 11 ++++++++++- tests/test_sample_dish.py | 0 8 files changed, 55 insertions(+), 2 deletions(-) create mode 100644 examples/demo.py rename tests/{test_filter_by_cuisine.py => test_filter_restaurants.py} (100%) create mode 100644 tests/test_format_card.py create mode 100644 tests/test_sample_dish.py diff --git a/README.md b/README.md index 6022e0e..8e2fd4c 100644 --- a/README.md +++ b/README.md @@ -1,3 +1,6 @@ # Python Package Exercise An exercise to create a Python package, build it, test it, distribute it, and use it. See [instructions](./instructions.md) for details. + +# Badge +![Build and Test](https://github.com/swe-students-fall2025 3-python-package-team_avalon/actions/workflows/build.yaml/badge.svg?branch=pipfile-experiment) diff --git a/examples/demo.py b/examples/demo.py new file mode 100644 index 0000000..5a5866d --- /dev/null +++ b/examples/demo.py @@ -0,0 +1,11 @@ +from eatnyc import load_data, top_n # add others as they land + +def main(): + data = load_data() + print("rows:", len(data)) + print("Top by rating:") + for r in top_n(data, n=5): + print(f"- {r['name']} ({r['cuisine']}, {r['price']}) ★ {r['rating']} – {r['sample_dish']}") + +if __name__ == "__main__": + main() \ No newline at end of file diff --git a/pyproject.toml b/pyproject.toml index 0a6492b..0f9271a 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -4,13 +4,22 @@ build-backend = "setuptools.build_meta" [project] name = "eatnyc" -version = "0.1.0" +version = "0.1.0" description = "A tiny NYC restaurant recommender near NYU." readme = "README.md" requires-python = ">=3.10" license = { text = "MIT" } authors = [{ name = "Team Avalon" }] keywords = ["nyc", "restaurants", "fun", "food", "recommendations"] +classifiers = [ + "Programming Language :: Python :: 3", + "License :: OSI Approved :: MIT License", + "Operating System :: OS Independent" +] + +[project.urls] +Homepage = "https://github.com/swe-students-fall2025/3-python-package-team_avalon" +Issues = "https://github.com/swe-students-fall2025/3-python-package-team_avalon/issues" [tool.setuptools] package-dir = {"" = "src"} @@ -21,3 +30,5 @@ where = ["src"] [tool.setuptools.package-data] eatnyc = ["data/*.csv"] +[project.scripts] +eatnyc = "eatnyc.core:cli" \ No newline at end of file diff --git a/src/eatnyc/core.py b/src/eatnyc/core.py index 7248073..4ebb908 100644 --- a/src/eatnyc/core.py +++ b/src/eatnyc/core.py @@ -1,6 +1,7 @@ import csv import random import os +import argparse from importlib.resources import files _DATA_PKG = "eatnyc.data" @@ -122,3 +123,21 @@ def sample_dish(cuisine=None, seed=None): def format_card(row, style="ascii", width=48, show_vibes=True): return + + +def cli(argv=None): + # Simple command-line entrypoint: + # $ eatnyc -> prints top 5 by rating + # $ eatnyc -n 10 -> prints top 10 + # $ eatnyc --sort name --asc -> sort by name ascending + + parser = argparse.ArgumentParser(prog="eatnyc", description="NYC restaurant recommender") + parser.add_argument("-n", "--n", type=int, default=5, help="number of results") + parser.add_argument("--sort", default="rating", help="field to sort by (rating, name, price, etc.)") + parser.add_argument("--asc", action="store_true", help="sort ascending (default is descending)") + args = parser.parse_args(argv) + + data = load_data() + results = top_n(data, n=args.n, sort_by=args.sort, descending=not args.asc) + for r in results: + print(f"{r['name']} | {r['cuisine']} | {r['price']} | ★{r['rating']} | {r.get('sample_dish','')}") diff --git a/tests/test_filter_by_cuisine.py b/tests/test_filter_restaurants.py similarity index 100% rename from tests/test_filter_by_cuisine.py rename to tests/test_filter_restaurants.py diff --git a/tests/test_format_card.py b/tests/test_format_card.py new file mode 100644 index 0000000..e69de29 diff --git a/tests/test_load_data.py b/tests/test_load_data.py index fac0b7a..2014ef9 100644 --- a/tests/test_load_data.py +++ b/tests/test_load_data.py @@ -1,4 +1,4 @@ -from eatnyc import load_data, filter_restaurants, top_n, sample_dish +from eatnyc import load_data def test_load_data_has_rows(): data = load_data() @@ -6,4 +6,13 @@ def test_load_data_has_rows(): assert len(data) >= 1 # basic schema check for key in ["name", "cuisine", "neighborhood", "price", "rating", "sample_dish"]: + assert key in data[0] + +def test_load_data_missing_file_raises(): + with pytest.raises(FileNotFoundError): + load_data(path="does_not_exist.csv") + +def test_load_data_has_required_columns(): + data = load_data() + for key in ["name","cuisine","neighborhood","price","rating","sample_dish"]: assert key in data[0] \ No newline at end of file diff --git a/tests/test_sample_dish.py b/tests/test_sample_dish.py new file mode 100644 index 0000000..e69de29 From 5c115bb71223198bd9915f5460c4e5ec43cd30ac Mon Sep 17 00:00:00 2001 From: amiraadum Date: Thu, 30 Oct 2025 16:46:44 -0400 Subject: [PATCH 15/42] Add full pytest coverage for load_data function (4 passing tests) --- src/eatnyc/__main__.py | 4 ++++ src/eatnyc/core.py | 22 ++++++------------- tests/test_load_data.py | 47 ++++++++++++++++++++++++++++++++++------- tests/test_top_n.py | 1 + 4 files changed, 50 insertions(+), 24 deletions(-) create mode 100644 src/eatnyc/__main__.py diff --git a/src/eatnyc/__main__.py b/src/eatnyc/__main__.py new file mode 100644 index 0000000..616450a --- /dev/null +++ b/src/eatnyc/__main__.py @@ -0,0 +1,4 @@ +from .core import cli + +if __name__ == "__main__": + cli() \ No newline at end of file diff --git a/src/eatnyc/core.py b/src/eatnyc/core.py index 4ebb908..6275c52 100644 --- a/src/eatnyc/core.py +++ b/src/eatnyc/core.py @@ -1,7 +1,6 @@ import csv import random import os -import argparse from importlib.resources import files _DATA_PKG = "eatnyc.data" @@ -43,28 +42,18 @@ def load_data(path: str | None = None, validate: bool = True) -> list[dict]: #Adds helper lists: `_cuisines`, `_vibes`. if path is None: - # use the packaged resource - csv_path = files(_DATA_PKG) / _DEFAULT_CSV - f = csv_path.open("r", encoding="utf-8") - close_after = True - else: - f = open(path, "r", encoding="utf-8") - close_after = True + path = str(files(_DATA_PKG).joinpath(_DEFAULT_CSV)) - try: + with open(path, "r", encoding="utf-8", newline="") as f: reader = csv.DictReader(f) - # validate required headers + if validate: - cols = {c.strip().lower() for c in reader.fieldnames or []} + cols = {c.strip().lower() for c in (reader.fieldnames or [])} missing = _REQUIRED_COLS - cols if missing: raise ValueError(f"CSV missing required columns: {sorted(missing)}") - data = [_normalize_row(r) for r in reader] - return data - finally: - if close_after: - f.close() + return [_normalize_row(r) for r in reader] @@ -126,6 +115,7 @@ def format_card(row, style="ascii", width=48, show_vibes=True): def cli(argv=None): + import argparse # Simple command-line entrypoint: # $ eatnyc -> prints top 5 by rating # $ eatnyc -n 10 -> prints top 10 diff --git a/tests/test_load_data.py b/tests/test_load_data.py index 2014ef9..c1a4abe 100644 --- a/tests/test_load_data.py +++ b/tests/test_load_data.py @@ -1,18 +1,49 @@ +import pytest from eatnyc import load_data -def test_load_data_has_rows(): +def test_load_data_returns_list_and_dicts(): + #load_data should return a nonempty list of dicts with expected keys data = load_data() assert isinstance(data, list) - assert len(data) >= 1 - # basic schema check + assert data, "Dataset should not be empty" + first = data[0] + assert isinstance(first, dict) for key in ["name", "cuisine", "neighborhood", "price", "rating", "sample_dish"]: - assert key in data[0] + assert key in first def test_load_data_missing_file_raises(): + #specifying a nonexistent path should raise FileNotFoundError. with pytest.raises(FileNotFoundError): load_data(path="does_not_exist.csv") -def test_load_data_has_required_columns(): - data = load_data() - for key in ["name","cuisine","neighborhood","price","rating","sample_dish"]: - assert key in data[0] \ No newline at end of file +def test_load_data_missing_required_columns(tmp_path): + #if a CSV lacks required columns, load_data(validate=True) raises ValueError. + bad_csv = tmp_path / "bad.csv" + bad_csv.write_text("name,rating\nExample,4.5\n", encoding="utf-8") + + with pytest.raises(ValueError) as e: + load_data(path=str(bad_csv), validate=True) + + #must mention at least one of the other required columns + assert any(k in str(e.value) for k in ["cuisine","neighborhood","price","sample_dish"]) + +def test_load_data_normalizes_fields(tmp_path): + #normalization: rating -> float, cuisines tokenized, price trimmed. + csv_text = ( + "name,cuisine,neighborhood,price,rating,sample_dish\n" + "Foo,\"Italian/ Pizza ; Pasta\",SoHo, $$ ,4.7,Margherita\n" + ) + good_csv = tmp_path / "good.csv" + good_csv.write_text(csv_text, encoding="utf-8") + + data = load_data(path=str(good_csv), validate=True) + row = data[0] + + #rating parsed to float + assert isinstance(row["rating"], float) and row["rating"] == 4.7 + + #cuisines split & lowercased + assert all(c in row["_cuisines"] for c in ["italian", "pizza", "pasta"]) + + #price trimmed (e.g., "$$") + assert row["price"] == "$$" \ No newline at end of file diff --git a/tests/test_top_n.py b/tests/test_top_n.py index 0ac127c..2ed6136 100644 --- a/tests/test_top_n.py +++ b/tests/test_top_n.py @@ -1,3 +1,4 @@ +import pytest from eatnyc import top_n From 293496c7a1922ac84aa2aa5d55733736eb1b8889 Mon Sep 17 00:00:00 2001 From: amiraadum Date: Thu, 30 Oct 2025 17:31:35 -0400 Subject: [PATCH 16/42] Editted correct package in load_data --- src/eatnyc/core.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/eatnyc/core.py b/src/eatnyc/core.py index 6275c52..3eb6116 100644 --- a/src/eatnyc/core.py +++ b/src/eatnyc/core.py @@ -3,8 +3,8 @@ import os from importlib.resources import files -_DATA_PKG = "eatnyc.data" -_DEFAULT_CSV = "nyc_restaurant_data.csv" +_DATA_PKG = "eatnyc" +_DEFAULT_CSV = "data/nyc_restaurant_data.csv" #columns we expect in the CSV _REQUIRED_COLS = {"name", "cuisine","neighborhood", "price", "rating", "sample_dish"} @@ -12,7 +12,7 @@ #new function (NORMALIZE_ROWS) def _normalize_row(row: dict) -> dict: - """Clean up one CSV row: strip spaces, normalize case/types, compute helper fields.""" + #Clean up one CSV row: strip spaces, normalize case/types, compute helper fields. clean = {k.strip().lower(): (v.strip() if isinstance(v, str) else v) for k, v in row.items()} # rating → float (default 0.0) From bcacc184a9db342a83577082183635702b34145b Mon Sep 17 00:00:00 2001 From: amiraadum Date: Thu, 30 Oct 2025 18:05:00 -0400 Subject: [PATCH 17/42] Add editable install to Pipenv + update load_data and verify tests --- Pipfile | 1 + Pipfile.lock | 6 +++++- src/eatnyc/core.py | 14 ++++++++------ 3 files changed, 14 insertions(+), 7 deletions(-) diff --git a/Pipfile b/Pipfile index 6c56da5..ebd0dec 100644 --- a/Pipfile +++ b/Pipfile @@ -5,6 +5,7 @@ name = "pypi" [packages] pytest = "*" +eatnyc = {file = ".", editable = true} [dev-packages] diff --git a/Pipfile.lock b/Pipfile.lock index 0a46c35..2e5cb1d 100644 --- a/Pipfile.lock +++ b/Pipfile.lock @@ -1,7 +1,7 @@ { "_meta": { "hash": { - "sha256": "20e1aad87698ead018f54bacfa6e7e1d5f1f0cae78c447d1465c006cbc99a583" + "sha256": "50cc5e5b50ec9c8d518d42690687ceab893c30545fc40b48cc00dac6de3f44d7" }, "pipfile-spec": 6, "requires": { @@ -16,6 +16,10 @@ ] }, "default": { + "eatnyc": { + "editable": true, + "file": "." + }, "iniconfig": { "hashes": [ "sha256:c76315c77db068650d49c5b56314774a7804df16fee4402c1f19d6d15d8c4730", diff --git a/src/eatnyc/core.py b/src/eatnyc/core.py index 3eb6116..916ffb9 100644 --- a/src/eatnyc/core.py +++ b/src/eatnyc/core.py @@ -39,13 +39,16 @@ def load_data(path: str | None = None, validate: bool = True) -> list[dict]: #If `path` is None, loads the bundled file from eatnyc/data/. #Keys are lowercased; rating is converted to float. - #Adds helper lists: `_cuisines`, `_vibes`. - + #Adds helper lists: `_cuisines`. if path is None: - path = str(files(_DATA_PKG).joinpath(_DEFAULT_CSV)) + #use importlib.resources so this works from wheels/zip installs + resource = files(_DATA_PKG) / _DEFAULT_CSV + f = resource.open("r", encoding="utf-8", newline="") + else: + f = open(path, "r", encoding="utf-8", newline="") - with open(path, "r", encoding="utf-8", newline="") as f: - reader = csv.DictReader(f) + with f as fh: + reader = csv.DictReader(fh) if validate: cols = {c.strip().lower() for c in (reader.fieldnames or [])} @@ -56,7 +59,6 @@ def load_data(path: str | None = None, validate: bool = True) -> list[dict]: return [_normalize_row(r) for r in reader] - def filter_restaurants(data, cuisine=None, neighborhood=None, price=None, min_rating=None, vibe=None, limit=None): return From 861f88b802392f7f86f2e409df8064e26bc6a3bd Mon Sep 17 00:00:00 2001 From: amiraadum Date: Thu, 30 Oct 2025 20:00:49 -0400 Subject: [PATCH 18/42] Updated README file with description and badg e for 'build and test' --- README.md | 11 +++++++++-- 1 file changed, 9 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index 8e2fd4c..05e2249 100644 --- a/README.md +++ b/README.md @@ -2,5 +2,12 @@ An exercise to create a Python package, build it, test it, distribute it, and use it. See [instructions](./instructions.md) for details. -# Badge -![Build and Test](https://github.com/swe-students-fall2025 3-python-package-team_avalon/actions/workflows/build.yaml/badge.svg?branch=pipfile-experiment) +# eatnyc - NYC Restaurant Recommender +![Build and Test](https://github.com/swe-students-fall20253-python-package-team_avalon/actions/workflows/build.yaml/badge.svg?branch=pipfile-experiment) + +**eatnyc** is a lightweight Python package that recommends top-rated NYC restaurants based on cuisine, neighborhood, price, and rating. +It’s designed to help users explore the city’s dining scene and discover great places through data-driven recommendations — directly from the command line or in Python. + +--- + +## How to install and use this package From 5c4e79f5f9effaba186aea0bd65d40517278d550 Mon Sep 17 00:00:00 2001 From: amiraadum Date: Thu, 30 Oct 2025 22:27:35 -0400 Subject: [PATCH 19/42] Update Pipfile: include build and pytest dependencies --- Pipfile | 1 + Pipfile.lock | 19 ++++++++++++++++++- 2 files changed, 19 insertions(+), 1 deletion(-) diff --git a/Pipfile b/Pipfile index ebd0dec..2990999 100644 --- a/Pipfile +++ b/Pipfile @@ -6,6 +6,7 @@ name = "pypi" [packages] pytest = "*" eatnyc = {file = ".", editable = true} +build = "*" [dev-packages] diff --git a/Pipfile.lock b/Pipfile.lock index 2e5cb1d..89161cf 100644 --- a/Pipfile.lock +++ b/Pipfile.lock @@ -1,7 +1,7 @@ { "_meta": { "hash": { - "sha256": "50cc5e5b50ec9c8d518d42690687ceab893c30545fc40b48cc00dac6de3f44d7" + "sha256": "087ffcffa4455b910d9b6682ef108af3241832f9a152c9ef6a1024263c27a311" }, "pipfile-spec": 6, "requires": { @@ -16,6 +16,15 @@ ] }, "default": { + "build": { + "hashes": [ + "sha256:698edd0ea270bde950f53aed21f3a0135672206f3911e0176261a31e0e07b397", + "sha256:7145f0b5061ba90a1500d60bd1b13ca0a8a4cebdd0cc16ed8adf1c0e739f43b4" + ], + "index": "pypi", + "markers": "python_version >= '3.9'", + "version": "==1.3.0" + }, "eatnyc": { "editable": true, "file": "." @@ -52,6 +61,14 @@ "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", From ef59529c3a764077488e8fb8bb6592495c658297 Mon Sep 17 00:00:00 2001 From: amiraadum Date: Thu, 30 Oct 2025 23:29:01 -0400 Subject: [PATCH 20/42] Updated README: corrected badge and started installation steps --- README.md | 14 +++++++++++++- 1 file changed, 13 insertions(+), 1 deletion(-) diff --git a/README.md b/README.md index 05e2249..938f915 100644 --- a/README.md +++ b/README.md @@ -3,7 +3,7 @@ An exercise to create a Python package, build it, test it, distribute it, and use it. See [instructions](./instructions.md) for details. # eatnyc - NYC Restaurant Recommender -![Build and Test](https://github.com/swe-students-fall20253-python-package-team_avalon/actions/workflows/build.yaml/badge.svg?branch=pipfile-experiment) +![Build and Test](https://github.com/swe-students-fall2025/3-python-package-team_avalon/actions/workflows/build.yaml/badge.svg) **eatnyc** is a lightweight Python package that recommends top-rated NYC restaurants based on cuisine, neighborhood, price, and rating. It’s designed to help users explore the city’s dining scene and discover great places through data-driven recommendations — directly from the command line or in Python. @@ -11,3 +11,15 @@ It’s designed to help users explore the city’s dining scene and discover gre --- ## How to install and use this package +### Install from PyPI (users) +```bash +pip install eatnyc +``` +### Install locally (developers) +```bash +pipenv install -e . +``` +If that set up fails for you, use: +```bash +python3 -m pipenv install -e . +``` \ No newline at end of file From 385e7f38b0f5f214c3abac02c7a30d4e64922c59 Mon Sep 17 00:00:00 2001 From: amiraadum Date: Fri, 31 Oct 2025 09:08:54 -0400 Subject: [PATCH 21/42] Updated README with full instructions, needs update after upload --- Pipfile | 3 + Pipfile.lock | 383 ++++++++++++++++++++++++++++++++++++++++++++++++++- README.md | 107 +++++++++++++- 3 files changed, 489 insertions(+), 4 deletions(-) diff --git a/Pipfile b/Pipfile index 2990999..5b4e4c3 100644 --- a/Pipfile +++ b/Pipfile @@ -9,6 +9,9 @@ eatnyc = {file = ".", editable = true} build = "*" [dev-packages] +build = "*" +twine = "*" +pytest = "*" [requires] python_version = "3.10" diff --git a/Pipfile.lock b/Pipfile.lock index 89161cf..8fe6e69 100644 --- a/Pipfile.lock +++ b/Pipfile.lock @@ -1,7 +1,7 @@ { "_meta": { "hash": { - "sha256": "087ffcffa4455b910d9b6682ef108af3241832f9a152c9ef6a1024263c27a311" + "sha256": "0ff350e2a7e0f825f6b8f894f39d25742a4ff0cdc7c2d02ac8224a03aa1bc34b" }, "pipfile-spec": 6, "requires": { @@ -79,5 +79,384 @@ "version": "==8.4.2" } }, - "develop": {} + "develop": { + "backports.tarfile": { + "hashes": [ + "sha256:77e284d754527b01fb1e6fa8a1afe577858ebe4e9dad8919e34c862cb399bc34", + "sha256:d75e02c268746e1b8144c278978b6e98e85de6ad16f8e4b0844a154557eca991" + ], + "markers": "python_version >= '3.8'", + "version": "==1.2.0" + }, + "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" + }, + "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" + }, + "importlib-metadata": { + "hashes": [ + "sha256:d13b81ad223b890aa16c5471f2ac3056cf76c5f10f82d6f9292f0b415f389000", + "sha256:e5dd1551894c77868a30651cef00984d50e1002d06942a7101d34870c5f02afd" + ], + "markers": "python_version >= '3.9'", + "version": "==8.7.0" + }, + "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" + }, + "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" + }, + "zipp": { + "hashes": [ + "sha256:071652d6115ed432f5ce1d34c336c0adfd6a884660d1e9712a256d3d3bd4b14e", + "sha256:a07157588a12518c9d4034df3fbbee09c814741a33ff63c05fa29d26a2404166" + ], + "markers": "python_version >= '3.9'", + "version": "==3.23.0" + } + } } diff --git a/README.md b/README.md index 938f915..9278c54 100644 --- a/README.md +++ b/README.md @@ -11,7 +11,75 @@ It’s designed to help users explore the city’s dining scene and discover gre --- ## How to install and use this package -### Install from PyPI (users) +### Option 1: Try it from TestPyPI (current test version) +You can try out the latest build of eatnyc from the [TestPyPI](https://test.pypi.org/project/eatnyc/) repository + +1. Create and Activate a virtual environment +```bash +pipenv --python 3.11 +pipenv shell +``` +2. Install from TestPyPI +Replace 0.1.0 with your latest version number (see pyproject.toml) +```bash +pipenv install -i https://test.pypi.org/simple/ --extra-index-url https://pypi.org/simple eatnyc==0.1.0 +``` +For now: The --extra-index-url flag ensures dependencies are installed from the real PyPI, while your package is pulled from TestPyPI + +3. Run the package +You can use eatnyc either as a command-line app or a Python module. + +#### Run from the command line: +```bash +eatnyc -n 5 --sort rating +``` +#### Run as a module: +```bash +python -m eatnyc +``` +--- +### Example Program +```python +from eatnyc import load_data, filter_restaurants, top_n, sample_dish, format_card + +data = load_data() + +# Filter restaurants by cuisine and borough +italian_manhattan = filter_restaurants( + data, + cuisine="Italian", + borough="Manhattan", + min_rating=4.0 +) + +# Get top 5 restaurants by rating +best = top_n(data, n=5, sort_by="rating") + +# Show a sample dish recommendation +print(sample_dish(cuisine="Japanese")) + +# Print formatted cards +for r in best: + print(format_card(r, style="ascii", width=48)) +``` +Run the example: +```bash +pipenv run python examples/demo.py +``` + +## How to Run Unit Tests +Simple unit tests are included in the 'tests' directory. To run them: +1. Install 'pytest' inside your virtual environment: +```bash +pipenv install pytest +``` +2. Run the tests from project root: +```bash +python3 -m pytest +``` +3. All tests should pass. Any failed test indicates that the package code is behaving differently from the expected results. + +### Option 2: Install from PyPI (users) ```bash pip install eatnyc ``` @@ -22,4 +90,39 @@ pipenv install -e . If that set up fails for you, use: ```bash python3 -m pipenv install -e . -``` \ No newline at end of file +``` + +# Developer Workflow +If you modify the code and want to publish a new version to TestPyPI, follow these steps: +```bash +#clean old build artifacts +rm -rf dist build src/*.egg-info +pipenv install build + +#bump version number in pyproject.toml (e.g., 0.1.0 → 0.1.1) +pipenv run python -m build + +#upload new version to TestPyPI +pipenv install twine +pipenv run twine upload -r testpypi dist/* +``` +Then reinstall to test it: +```bash +pipenv run pip uninstall -y eatnyc +pipenv install -i https://test.pypi.org/simple/ --extra-index-url https://pypi.org/simple eatnyc==0.1.1 +``` +Once final, upload to real PyPI with: +```bash +pipenv run twine upload dist/* +``` +--- +## PyPI Link +- **PyPI:** [https://pypi.org/project/eatnyc](https://pypi.org/project/eatnyc) +- **TestPyPI:** [https://test.pypi.org/project/eatnyc](https://test.pypi.org/project/eatnyc) + +# Contributors +- [amiraadum](https://github.com/amiraadum) +- [Ivan-Wang-tech](https://github.com/Ivan-Wang-tech) +- [hyunkyuu](https://github.com/hyunkyuu) +- [jmo7728](https://github.com/jmo7728) +- [lilyluo7412](https://github.com/lilyluo7412) From 959a7effb2d19b6db315655e391447e622463b95 Mon Sep 17 00:00:00 2001 From: Ivan-Wang-tech <157322972+Ivan-Wang-tech@users.noreply.github.com> Date: Sat, 1 Nov 2025 15:03:27 -0400 Subject: [PATCH 22/42] update Pipfile dev dependencies; add format_card() function --- Pipfile | 3 +- Pipfile.lock | 125 ++++++++++++++++++++++++--------------------- examples/demo.py | 4 +- src/eatnyc/core.py | 59 ++++++++++++++++++++- 4 files changed, 126 insertions(+), 65 deletions(-) diff --git a/Pipfile b/Pipfile index 5b4e4c3..3bc2fd7 100644 --- a/Pipfile +++ b/Pipfile @@ -4,14 +4,13 @@ verify_ssl = true name = "pypi" [packages] -pytest = "*" eatnyc = {file = ".", editable = true} -build = "*" [dev-packages] build = "*" twine = "*" pytest = "*" +exceptiongroup = "*" [requires] python_version = "3.10" diff --git a/Pipfile.lock b/Pipfile.lock index 8fe6e69..9ce269f 100644 --- a/Pipfile.lock +++ b/Pipfile.lock @@ -1,7 +1,7 @@ { "_meta": { "hash": { - "sha256": "0ff350e2a7e0f825f6b8f894f39d25742a4ff0cdc7c2d02ac8224a03aa1bc34b" + "sha256": "ea8860411b371f0c7365fa8a2fcce5c703ae330d48460cfd08b6c4e3fcc18e98" }, "pipfile-spec": 6, "requires": { @@ -16,67 +16,9 @@ ] }, "default": { - "build": { - "hashes": [ - "sha256:698edd0ea270bde950f53aed21f3a0135672206f3911e0176261a31e0e07b397", - "sha256:7145f0b5061ba90a1500d60bd1b13ca0a8a4cebdd0cc16ed8adf1c0e739f43b4" - ], - "index": "pypi", - "markers": "python_version >= '3.9'", - "version": "==1.3.0" - }, "eatnyc": { "editable": true, "file": "." - }, - "iniconfig": { - "hashes": [ - "sha256:c76315c77db068650d49c5b56314774a7804df16fee4402c1f19d6d15d8c4730", - "sha256:f631c04d2c48c52b84d0d0549c99ff3859c98df65b3101406327ecc7d53fbf12" - ], - "markers": "python_version >= '3.10'", - "version": "==2.3.0" - }, - "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" } }, "develop": { @@ -232,6 +174,15 @@ "markers": "python_version >= '3.9'", "version": "==0.22.2" }, + "exceptiongroup": { + "hashes": [ + "sha256:4d111e6e0c13d0644cad6ddaa7ed0261a0b36971f6d23e7ec9b4b9097da78a10", + "sha256:b241f5885f560bc56a59ee63ca4c6a8bfa46ae4ad651af316d4e81817bb9fd88" + ], + "index": "pypi", + "markers": "python_version >= '3.7'", + "version": "==1.3.0" + }, "id": { "hashes": [ "sha256:292cb8a49eacbbdbce97244f47a97b4c62540169c976552e497fd57df0734c1d", @@ -433,6 +384,54 @@ "markers": "python_full_version >= '3.8.0'", "version": "==14.2.0" }, + "tomli": { + "hashes": [ + "sha256:00b5f5d95bbfc7d12f91ad8c593a1659b6387b43f054104cda404be6bda62456", + "sha256:0a154a9ae14bfcf5d8917a59b51ffd5a3ac1fd149b71b47a3a104ca4edcfa845", + "sha256:0c95ca56fbe89e065c6ead5b593ee64b84a26fca063b5d71a1122bf26e533999", + "sha256:0eea8cc5c5e9f89c9b90c4896a8deefc74f518db5927d0e0e8d4a80953d774d0", + "sha256:1cb4ed918939151a03f33d4242ccd0aa5f11b3547d0cf30f7c74a408a5b99878", + "sha256:4021923f97266babc6ccab9f5068642a0095faa0a51a246a6a02fccbb3514eaf", + "sha256:4c2ef0244c75aba9355561272009d934953817c49f47d768070c3c94355c2aa3", + "sha256:4dc4ce8483a5d429ab602f111a93a6ab1ed425eae3122032db7e9acf449451be", + "sha256:4f195fe57ecceac95a66a75ac24d9d5fbc98ef0962e09b2eddec5d39375aae52", + "sha256:5192f562738228945d7b13d4930baffda67b69425a7f0da96d360b0a3888136b", + "sha256:5e01decd096b1530d97d5d85cb4dff4af2d8347bd35686654a004f8dea20fc67", + "sha256:64be704a875d2a59753d80ee8a533c3fe183e3f06807ff7dc2232938ccb01549", + "sha256:70a251f8d4ba2d9ac2542eecf008b3c8a9fc5c3f9f02c56a9d7952612be2fdba", + "sha256:73ee0b47d4dad1c5e996e3cd33b8a76a50167ae5f96a2607cbe8cc773506ab22", + "sha256:74bf8464ff93e413514fefd2be591c3b0b23231a77f901db1eb30d6f712fc42c", + "sha256:792262b94d5d0a466afb5bc63c7daa9d75520110971ee269152083270998316f", + "sha256:7b0882799624980785240ab732537fcfc372601015c00f7fc367c55308c186f6", + "sha256:883b1c0d6398a6a9d29b508c331fa56adbcdff647f6ace4dfca0f50e90dfd0ba", + "sha256:88bd15eb972f3664f5ed4b57c1634a97153b4bac4479dcb6a495f41921eb7f45", + "sha256:8a35dd0e643bb2610f156cca8db95d213a90015c11fee76c946aa62b7ae7e02f", + "sha256:940d56ee0410fa17ee1f12b817b37a4d4e4dc4d27340863cc67236c74f582e77", + "sha256:97d5eec30149fd3294270e889b4234023f2c69747e555a27bd708828353ab606", + "sha256:a0e285d2649b78c0d9027570d4da3425bdb49830a6156121360b3f8511ea3441", + "sha256:a1f7f282fe248311650081faafa5f4732bdbfef5d45fe3f2e702fbc6f2d496e0", + "sha256:a4ea38c40145a357d513bffad0ed869f13c1773716cf71ccaa83b0fa0cc4e42f", + "sha256:a56212bdcce682e56b0aaf79e869ba5d15a6163f88d5451cbde388d48b13f530", + "sha256:ad805ea85eda330dbad64c7ea7a4556259665bdf9d2672f5dccc740eb9d3ca05", + "sha256:b273fcbd7fc64dc3600c098e39136522650c49bca95df2d11cf3b626422392c8", + "sha256:b5870b50c9db823c595983571d1296a6ff3e1b88f734a4c8f6fc6188397de005", + "sha256:b74a0e59ec5d15127acdabd75ea17726ac4c5178ae51b85bfe39c4f8a278e879", + "sha256:be71c93a63d738597996be9528f4abe628d1adf5e6eb11607bc8fe1a510b5dae", + "sha256:c22a8bf253bacc0cf11f35ad9808b6cb75ada2631c2d97c971122583b129afbc", + "sha256:c4665508bcbac83a31ff8ab08f424b665200c0e1e645d2bd9ab3d3e557b6185b", + "sha256:c5f3ffd1e098dfc032d4d3af5c0ac64f6d286d98bc148698356847b80fa4de1b", + "sha256:cebc6fe843e0733ee827a282aca4999b596241195f43b4cc371d64fc6639da9e", + "sha256:d1381caf13ab9f300e30dd8feadb3de072aeb86f1d34a8569453ff32a7dea4bf", + "sha256:d7d86942e56ded512a594786a5ba0a5e521d02529b3826e7761a05138341a2ac", + "sha256:e31d432427dcbf4d86958c184b9bfd1e96b5b71f8eb17e6d02531f434fd335b8", + "sha256:e95b1af3c5b07d9e643909b5abbec77cd9f1217e6d0bca72b0234736b9fb1f1b", + "sha256:f85209946d1fe94416debbb88d00eb92ce9cd5266775424ff81bc959e001acaf", + "sha256:feb0dacc61170ed7ab602d3d972a58f14ee3ee60494292d384649a3dc38ef463", + "sha256:ff72b71b5d10d22ecb084d345fc26f42b5143c5533db5e2eaba7d2d335358876" + ], + "markers": "python_version >= '3.8'", + "version": "==2.3.0" + }, "twine": { "hashes": [ "sha256:418ebf08ccda9a8caaebe414433b0ba5e25eb5e4a927667122fbe8f829f985d8", @@ -442,6 +441,14 @@ "markers": "python_version >= '3.9'", "version": "==6.2.0" }, + "typing-extensions": { + "hashes": [ + "sha256:0cea48d173cc12fa28ecabc3b837ea3cf6f38c6d1136f85cbaaf598984861466", + "sha256:f0fa19c6845758ab08074a0cfa8b7aecb71c999ca73d62883bc25cc018c4e548" + ], + "markers": "python_version >= '3.9'", + "version": "==4.15.0" + }, "urllib3": { "hashes": [ "sha256:3fc47733c7e419d4bc3f6b3dc2b4f890bb743906a30d56ba4a5bfa4bbff92760", diff --git a/examples/demo.py b/examples/demo.py index 5a5866d..c1ca5bf 100644 --- a/examples/demo.py +++ b/examples/demo.py @@ -1,11 +1,11 @@ -from eatnyc import load_data, top_n # add others as they land +from eatnyc import load_data, top_n, format_card # add others as they land def main(): data = load_data() print("rows:", len(data)) print("Top by rating:") for r in top_n(data, n=5): - print(f"- {r['name']} ({r['cuisine']}, {r['price']}) ★ {r['rating']} – {r['sample_dish']}") + print(format_card(r, width=60)) if __name__ == "__main__": main() \ No newline at end of file diff --git a/src/eatnyc/core.py b/src/eatnyc/core.py index 916ffb9..c326ffc 100644 --- a/src/eatnyc/core.py +++ b/src/eatnyc/core.py @@ -112,8 +112,63 @@ def sample_dish(cuisine=None, seed=None): return -def format_card(row, style="ascii", width=48, show_vibes=True): - return +def format_card(row, style="ascii", width=60, show_dish=True): + name = str(row.get("name", "") or "").strip() + cuisine = str(row.get("cuisine", "") or "").strip() + neighborhood = str(row.get("neighborhood", "") or "").strip() + price = str(row.get("price", "") or "").strip() + + try: + rating = float(row.get("rating", 0.0)) + except (TypeError, ValueError): + rating = 0.0 + + sample = str(row.get("sample_dish", "") or "").strip() + + title_line = name if name else "(unknown)" + sub_line = f"{cuisine}, {neighborhood}".strip(", ").strip() + meta_line = f"Rating: {rating:.1f} Price: {price}".strip() + dish_line = f"Dish: {sample}" if (show_dish and sample) else "" + + if style.lower() == "markdown": + body = f"**{title_line}** — *{sub_line}*\n{meta_line}" + if dish_line: + body += f"\n{dish_line}" + return body + + def _wrap_line(line: str, inner_width: int): + if not line: + return [""] + words = line.split() + out, cur = [], "" + for w in words: + if not cur: + cur = w + elif len(cur) + 1 + len(w) <= inner_width: + cur += " " + w + else: + out.append(cur) + cur = w + if cur: + out.append(cur) + return out + + box_width = max(24, int(width) if isinstance(width, int) else 48) + inner = box_width - 2 + + logical_lines = [f"{title_line} ({sub_line})" if sub_line else title_line, + meta_line, + dish_line] + + wrapped = [] + for ln in logical_lines: + wrapped.extend(_wrap_line(ln, inner) if ln else [""]) + + top = "+" + "-" * (box_width - 2) + "+" + content = "\n".join("|" + (ln.ljust(inner)) + "|" for ln in wrapped if ln is not None) + bottom = top + + return f"{top}\n{content}\n{bottom}" def cli(argv=None): From 1bfc0869150b8c9a66c960803b44bd1f099b0f4d Mon Sep 17 00:00:00 2001 From: John Ovalles Date: Sun, 2 Nov 2025 12:16:17 -0500 Subject: [PATCH 23/42] Added draft of filter restaurants --- src/eatnyc/core.py | 35 ++++++++++++++++++++++++++++++++++- 1 file changed, 34 insertions(+), 1 deletion(-) diff --git a/src/eatnyc/core.py b/src/eatnyc/core.py index 916ffb9..0eb9018 100644 --- a/src/eatnyc/core.py +++ b/src/eatnyc/core.py @@ -60,7 +60,40 @@ def load_data(path: str | None = None, validate: bool = True) -> list[dict]: def filter_restaurants(data, cuisine=None, neighborhood=None, price=None, min_rating=None, vibe=None, limit=None): - return + if not isinstance(data, list): + raise TypeError("Data must be a list of dicts") + + if not data: + return [] + + results = [] + for row in data: + if cuisine: + if cuisine.strip().lower() not in row.get("cuisines", []): + continue + + if neighborhood: + if neighborhood.strip().lower() != row.get("neighborhood", "").lower(): + continue + + if price: + if price.strip() != row.get("price", ""): + continue + + if min_rating: + try: + min_rating_val = float(min_rating) + except ValueError: + raise ValueError("min_rating must be a number") + if row.get("rating", 0.0) < min_rating_val: + continue + + results.append(row) + + if limit: + if len(results) >= limit: + break + return results def top_n(data, n=5, sort_by="rating", descending=True): From 1ed7dc9b868d1e7577642eca30a24e7882d02b8a Mon Sep 17 00:00:00 2001 From: John Ovalles Date: Sun, 2 Nov 2025 12:56:51 -0500 Subject: [PATCH 24/42] Final Draft of Filter_Restaurants, added tests and a demo of filter restaurants --- examples/demo.py | 6 ++++- src/eatnyc/core.py | 15 ++++++----- tests/test_filter_restaurants.py | 43 ++++++++++++++++++++++++++++++++ 3 files changed, 57 insertions(+), 7 deletions(-) diff --git a/examples/demo.py b/examples/demo.py index 5a5866d..cb3fd18 100644 --- a/examples/demo.py +++ b/examples/demo.py @@ -1,4 +1,4 @@ -from eatnyc import load_data, top_n # add others as they land +from eatnyc import load_data, top_n, filter_restaurants # add others as they land def main(): data = load_data() @@ -6,6 +6,10 @@ def main(): print("Top by rating:") for r in top_n(data, n=5): print(f"- {r['name']} ({r['cuisine']}, {r['price']}) ★ {r['rating']} – {r['sample_dish']}") + + print("filtering Korean restaurants in Koreatown with a $$ price and a min_rating of 4.5):") + for r in filter_restaurants(data, cuisine="Korean", neighborhood="Koreatown", price="$$", min_rating=4.5): + print(f"- {r['name']} ({r['cuisine']}, {r['price']}) ★ {r['rating']} – {r['sample_dish']}") if __name__ == "__main__": main() \ No newline at end of file diff --git a/src/eatnyc/core.py b/src/eatnyc/core.py index 0eb9018..7c36b03 100644 --- a/src/eatnyc/core.py +++ b/src/eatnyc/core.py @@ -59,7 +59,9 @@ def load_data(path: str | None = None, validate: bool = True) -> list[dict]: return [_normalize_row(r) for r in reader] -def filter_restaurants(data, cuisine=None, neighborhood=None, price=None, min_rating=None, vibe=None, limit=None): +def filter_restaurants(data, cuisine=None, neighborhood=None, price=None, min_rating=None, limit=None): + + #Error checking for empty or invalid data if not isinstance(data, list): raise TypeError("Data must be a list of dicts") @@ -67,25 +69,26 @@ def filter_restaurants(data, cuisine=None, neighborhood=None, price=None, min_ra return [] results = [] + + # Filtering logic for row in data: + clean = _normalize_row(row) if cuisine: - if cuisine.strip().lower() not in row.get("cuisines", []): + if cuisine.strip().lower() not in clean.get("_cuisines", []): continue if neighborhood: if neighborhood.strip().lower() != row.get("neighborhood", "").lower(): continue - if price: - if price.strip() != row.get("price", ""): + if price.strip() != clean.get("price", ""): continue - if min_rating: try: min_rating_val = float(min_rating) except ValueError: raise ValueError("min_rating must be a number") - if row.get("rating", 0.0) < min_rating_val: + if clean.get("rating", 0.0) < min_rating_val: continue results.append(row) diff --git a/tests/test_filter_restaurants.py b/tests/test_filter_restaurants.py index e69de29..27294ea 100644 --- a/tests/test_filter_restaurants.py +++ b/tests/test_filter_restaurants.py @@ -0,0 +1,43 @@ +import pytest +from eatnyc import filter_restaurants + +def _sample_data(): + return [ + {"name": "B", "cuisine": "X", "neighborhood": "N1", "price": "$$", "rating": 4.2, "sample_dish": "d1"}, + {"name": "A", "cuisine": "Y", "neighborhood": "N2", "price": "$$$", "rating": 4.8, "sample_dish": "d2"}, + {"name": "C", "cuisine": "Z", "neighborhood": "N3", "price": "$", "rating": 3.9, "sample_dish": "d3"}, + {"name": "D", "cuisine": "Y", "neighborhood": "N2", "price": "$$$", "rating": 4.8, "sample_dish": "d4"}, + ] + +def test_filter_restaurants_cuisine(): + data = _sample_data() + result = filter_restaurants(data, cuisine="Y") + assert [r["cuisine"] for r in result] == ["Y", "Y"] + +def test_filter_restaurants_neighborhood(): + data = _sample_data() + result = filter_restaurants(data, neighborhood="N2") + assert [r["neighborhood"] for r in result] == ["N2", "N2"] + +def test_filter_restaurants_price(): + data = _sample_data() + result = filter_restaurants(data, price="$$$") + assert [r["price"] for r in result] == ["$$$", "$$$"] + +def test_filter_restaurants_min_rating(): + data = _sample_data() + result = filter_restaurants(data, min_rating=4.5) + assert [r["rating"] for r in result] == [4.8, 4.8] + +def test_filter_restaurants_limit(): + data = _sample_data() + result = filter_restaurants(data, limit=2) + assert len(result) == 2 + +def test_filter_restaurants_no_matches(): + data = _sample_data() + result = filter_restaurants(data, cuisine="Nonexistent") + assert result == [] + + + From 3d95bd9626d627043e81caf4c25331ed6d002575 Mon Sep 17 00:00:00 2001 From: Ivan-Wang-tech <157322972+Ivan-Wang-tech@users.noreply.github.com> Date: Sun, 2 Nov 2025 16:45:50 -0500 Subject: [PATCH 25/42] add test function for format_card --- tests/test_format_card.py | 57 +++++++++++++++++++++++++++++++++++++++ 1 file changed, 57 insertions(+) diff --git a/tests/test_format_card.py b/tests/test_format_card.py index e69de29..e9a0d9c 100644 --- a/tests/test_format_card.py +++ b/tests/test_format_card.py @@ -0,0 +1,57 @@ +import pytest +from eatnyc import format_card + +# Reusable sample restaurant row +@pytest.fixture +def row(): + return { + "name": "Joe's Pizza", + "cuisine": "italian", + "neighborhood": "Manhattan", + "price": "$", + "rating": 4.5, + "sample_dish": "Classic cheese slice", + } + +# Test ASCII rendering at multiple widths, including min-width clamp +@pytest.mark.parametrize( + "width, expected_width", + [ + (60, 60), # normal width + (40, 40), # medium width + (10, 24), # smaller than minimum -> should clamp to 24 + ], +) +def test_ascii_box_width(row, width, expected_width): + output = format_card(row, width=width) + top_line = output.splitlines()[0] + bottom_line = output.splitlines()[-1] + + assert len(top_line) == expected_width + assert top_line == bottom_line # top and bottom frame match + + # Each inner line must be within the width and framed by | | + for line in output.splitlines()[1:-1]: + assert line.startswith("|") and line.endswith("|") + assert len(line) <= expected_width + +# Test markdown format and dish toggle +@pytest.mark.parametrize("show_dish, expected_contains", [(True, True), (False, False)]) +def test_markdown_format(row, show_dish, expected_contains): + md = format_card(row, style="markdown", show_dish=show_dish) + + assert "**Joe's Pizza**" in md # bold title + assert "*italian, Manhattan*" in md # italic subtitle + assert "Rating: 4.5" in md + assert "Price: $" in md + + assert ("Dish:" in md) is expected_contains + +# Test error on invalid input type +def test_invalid_input_type_error(): + # format_card expects a dict; passing wrong type should raise + with pytest.raises(AttributeError): + format_card(None) + + with pytest.raises(AttributeError): + format_card(["not", "a", "dict"]) From 2f15bc0d395a8d40a2b445d9c65273634735946e Mon Sep 17 00:00:00 2001 From: amiraadum Date: Sun, 2 Nov 2025 16:49:44 -0500 Subject: [PATCH 26/42] Update README, build.yaml, and version to 0.1.1 --- .github/workflows/build.yaml | 24 +- Pipfile | 3 + Pipfile.lock | 428 +++++++++++++++++++++++++++++++---- README.md | 41 ++-- pyproject.toml | 2 +- 5 files changed, 421 insertions(+), 77 deletions(-) diff --git a/.github/workflows/build.yaml b/.github/workflows/build.yaml index 010a74a..7567c10 100644 --- a/.github/workflows/build.yaml +++ b/.github/workflows/build.yaml @@ -9,6 +9,7 @@ on: jobs: build: + name: Build & Test runs-on: ubuntu-latest timeout-minutes: 5 strategy: @@ -23,13 +24,14 @@ jobs: python-version: ${{ matrix.python-version }} #installing requirements.txt - - name: Install requirements.txt + - name: Install requirements.txt + test deps run: | if [ -f requirements.txt ]; then pipenv run pip install -r requirements.txt; fi - name: Turn on 'editable' mode run: | pipenv install -e . + pipenv install pytest build - name: Test with pytest run: | @@ -37,8 +39,14 @@ jobs: pipenv --venv pipenv run python -m pytest + - name: Build package + run: | + rm -rf dist build src/*.egg-info + pipenv run python -m build . + + deliver: - if: github.event_name != 'pull_request' + if: github.event_name != 'pull_request' && startsWith(github.ref, 'refs/tags/v') needs: [build] runs-on: ubuntu-latest timeout-minutes: 5 @@ -53,12 +61,12 @@ jobs: pipenv install build pipenv run python -m build . - #change the following to the published one (not PyPI TEST) - # - name: Publish to PyPI test server - # uses: pypa/gh-action-pypi-publish@release/v1 - # with: - # password: ${{ secrets.TEST_PYPI_API_TOKEN }} - # repository-url: https://test.pypi.org/legacy/ + - name: Publish to PyPI test server + uses: pypa/gh-action-pypi-publish@release/v1 + with: + repository-url: https://test.pypi.org/legacy/ + password: ${{ secrets.TEST_PYPI_API_TOKEN }} + #ADDED to verify upload # verify-upload: diff --git a/Pipfile b/Pipfile index 3bc2fd7..f06be7a 100644 --- a/Pipfile +++ b/Pipfile @@ -4,7 +4,10 @@ verify_ssl = true name = "pypi" [packages] +pytest = "*" eatnyc = {file = ".", editable = true} +build = "*" +twine = "*" [dev-packages] build = "*" diff --git a/Pipfile.lock b/Pipfile.lock index 9ce269f..e6b36b4 100644 --- a/Pipfile.lock +++ b/Pipfile.lock @@ -1,7 +1,7 @@ { "_meta": { "hash": { - "sha256": "ea8860411b371f0c7365fa8a2fcce5c703ae330d48460cfd08b6c4e3fcc18e98" + "sha256": "542c1b089337be7a81ae6849c863b3dafcf7d86b48250ecc8cf7ed5731bc43be" }, "pipfile-spec": 6, "requires": { @@ -16,9 +16,387 @@ ] }, "default": { + "backports.tarfile": { + "hashes": [ + "sha256:77e284d754527b01fb1e6fa8a1afe577858ebe4e9dad8919e34c862cb399bc34", + "sha256:d75e02c268746e1b8144c278978b6e98e85de6ad16f8e4b0844a154557eca991" + ], + "markers": "python_version >= '3.8'", + "version": "==1.2.0" + }, + "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" + }, + "docutils": { + "hashes": [ + "sha256:9fdb771707c8784c8f2728b67cb2c691305933d68137ef95a75db5f4dfbc213d", + "sha256:b0e98d679283fc3bb0ead8a5da7f501baa632654e7056e9c5846842213d674d8" + ], + "markers": "python_version >= '3.9'", + "version": "==0.22.2" + }, "eatnyc": { "editable": true, "file": "." + }, + "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" + }, + "importlib-metadata": { + "hashes": [ + "sha256:d13b81ad223b890aa16c5471f2ac3056cf76c5f10f82d6f9292f0b415f389000", + "sha256:e5dd1551894c77868a30651cef00984d50e1002d06942a7101d34870c5f02afd" + ], + "markers": "python_version >= '3.9'", + "version": "==8.7.0" + }, + "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" + }, + "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" + }, + "zipp": { + "hashes": [ + "sha256:071652d6115ed432f5ce1d34c336c0adfd6a884660d1e9712a256d3d3bd4b14e", + "sha256:a07157588a12518c9d4034df3fbbee09c814741a33ff63c05fa29d26a2404166" + ], + "markers": "python_version >= '3.9'", + "version": "==3.23.0" } }, "develop": { @@ -384,54 +762,6 @@ "markers": "python_full_version >= '3.8.0'", "version": "==14.2.0" }, - "tomli": { - "hashes": [ - "sha256:00b5f5d95bbfc7d12f91ad8c593a1659b6387b43f054104cda404be6bda62456", - "sha256:0a154a9ae14bfcf5d8917a59b51ffd5a3ac1fd149b71b47a3a104ca4edcfa845", - "sha256:0c95ca56fbe89e065c6ead5b593ee64b84a26fca063b5d71a1122bf26e533999", - "sha256:0eea8cc5c5e9f89c9b90c4896a8deefc74f518db5927d0e0e8d4a80953d774d0", - "sha256:1cb4ed918939151a03f33d4242ccd0aa5f11b3547d0cf30f7c74a408a5b99878", - "sha256:4021923f97266babc6ccab9f5068642a0095faa0a51a246a6a02fccbb3514eaf", - "sha256:4c2ef0244c75aba9355561272009d934953817c49f47d768070c3c94355c2aa3", - "sha256:4dc4ce8483a5d429ab602f111a93a6ab1ed425eae3122032db7e9acf449451be", - "sha256:4f195fe57ecceac95a66a75ac24d9d5fbc98ef0962e09b2eddec5d39375aae52", - "sha256:5192f562738228945d7b13d4930baffda67b69425a7f0da96d360b0a3888136b", - "sha256:5e01decd096b1530d97d5d85cb4dff4af2d8347bd35686654a004f8dea20fc67", - "sha256:64be704a875d2a59753d80ee8a533c3fe183e3f06807ff7dc2232938ccb01549", - "sha256:70a251f8d4ba2d9ac2542eecf008b3c8a9fc5c3f9f02c56a9d7952612be2fdba", - "sha256:73ee0b47d4dad1c5e996e3cd33b8a76a50167ae5f96a2607cbe8cc773506ab22", - "sha256:74bf8464ff93e413514fefd2be591c3b0b23231a77f901db1eb30d6f712fc42c", - "sha256:792262b94d5d0a466afb5bc63c7daa9d75520110971ee269152083270998316f", - "sha256:7b0882799624980785240ab732537fcfc372601015c00f7fc367c55308c186f6", - "sha256:883b1c0d6398a6a9d29b508c331fa56adbcdff647f6ace4dfca0f50e90dfd0ba", - "sha256:88bd15eb972f3664f5ed4b57c1634a97153b4bac4479dcb6a495f41921eb7f45", - "sha256:8a35dd0e643bb2610f156cca8db95d213a90015c11fee76c946aa62b7ae7e02f", - "sha256:940d56ee0410fa17ee1f12b817b37a4d4e4dc4d27340863cc67236c74f582e77", - "sha256:97d5eec30149fd3294270e889b4234023f2c69747e555a27bd708828353ab606", - "sha256:a0e285d2649b78c0d9027570d4da3425bdb49830a6156121360b3f8511ea3441", - "sha256:a1f7f282fe248311650081faafa5f4732bdbfef5d45fe3f2e702fbc6f2d496e0", - "sha256:a4ea38c40145a357d513bffad0ed869f13c1773716cf71ccaa83b0fa0cc4e42f", - "sha256:a56212bdcce682e56b0aaf79e869ba5d15a6163f88d5451cbde388d48b13f530", - "sha256:ad805ea85eda330dbad64c7ea7a4556259665bdf9d2672f5dccc740eb9d3ca05", - "sha256:b273fcbd7fc64dc3600c098e39136522650c49bca95df2d11cf3b626422392c8", - "sha256:b5870b50c9db823c595983571d1296a6ff3e1b88f734a4c8f6fc6188397de005", - "sha256:b74a0e59ec5d15127acdabd75ea17726ac4c5178ae51b85bfe39c4f8a278e879", - "sha256:be71c93a63d738597996be9528f4abe628d1adf5e6eb11607bc8fe1a510b5dae", - "sha256:c22a8bf253bacc0cf11f35ad9808b6cb75ada2631c2d97c971122583b129afbc", - "sha256:c4665508bcbac83a31ff8ab08f424b665200c0e1e645d2bd9ab3d3e557b6185b", - "sha256:c5f3ffd1e098dfc032d4d3af5c0ac64f6d286d98bc148698356847b80fa4de1b", - "sha256:cebc6fe843e0733ee827a282aca4999b596241195f43b4cc371d64fc6639da9e", - "sha256:d1381caf13ab9f300e30dd8feadb3de072aeb86f1d34a8569453ff32a7dea4bf", - "sha256:d7d86942e56ded512a594786a5ba0a5e521d02529b3826e7761a05138341a2ac", - "sha256:e31d432427dcbf4d86958c184b9bfd1e96b5b71f8eb17e6d02531f434fd335b8", - "sha256:e95b1af3c5b07d9e643909b5abbec77cd9f1217e6d0bca72b0234736b9fb1f1b", - "sha256:f85209946d1fe94416debbb88d00eb92ce9cd5266775424ff81bc959e001acaf", - "sha256:feb0dacc61170ed7ab602d3d972a58f14ee3ee60494292d384649a3dc38ef463", - "sha256:ff72b71b5d10d22ecb084d345fc26f42b5143c5533db5e2eaba7d2d335358876" - ], - "markers": "python_version >= '3.8'", - "version": "==2.3.0" - }, "twine": { "hashes": [ "sha256:418ebf08ccda9a8caaebe414433b0ba5e25eb5e4a927667122fbe8f829f985d8", diff --git a/README.md b/README.md index 9278c54..75a2e7b 100644 --- a/README.md +++ b/README.md @@ -11,29 +11,29 @@ It’s designed to help users explore the city’s dining scene and discover gre --- ## How to install and use this package -### Option 1: Try it from TestPyPI (current test version) -You can try out the latest build of eatnyc from the [TestPyPI](https://test.pypi.org/project/eatnyc/) repository +### Option 1: Try it from **TestPyPI** (current test version) +You can try out the latest build of eatnyc from the [TestPyPI](https://test.pypi.org/project/eatnyc/) repository. -1. Create and Activate a virtual environment +1. **Create and Activate a virtual environment** ```bash pipenv --python 3.11 pipenv shell ``` -2. Install from TestPyPI +2. **Install from TestPyPI** Replace 0.1.0 with your latest version number (see pyproject.toml) ```bash pipenv install -i https://test.pypi.org/simple/ --extra-index-url https://pypi.org/simple eatnyc==0.1.0 ``` For now: The --extra-index-url flag ensures dependencies are installed from the real PyPI, while your package is pulled from TestPyPI -3. Run the package -You can use eatnyc either as a command-line app or a Python module. +3. **Run the package** +You can use eatnyc either as a CLI app or a Python module. -#### Run from the command line: +#### Command line: ```bash eatnyc -n 5 --sort rating ``` -#### Run as a module: +#### Python module: ```bash python -m eatnyc ``` @@ -69,21 +69,21 @@ pipenv run python examples/demo.py ## How to Run Unit Tests Simple unit tests are included in the 'tests' directory. To run them: -1. Install 'pytest' inside your virtual environment: +1. **Install 'pytest' inside your virtual environment:** ```bash pipenv install pytest ``` -2. Run the tests from project root: +2. **Run the tests from project root:** ```bash python3 -m pytest ``` 3. All tests should pass. Any failed test indicates that the package code is behaving differently from the expected results. -### Option 2: Install from PyPI (users) +### Option 2: Install from PyPI (for users) ```bash pip install eatnyc ``` -### Install locally (developers) +### Install locally (for developers) ```bash pipenv install -e . ``` @@ -92,33 +92,36 @@ If that set up fails for you, use: python3 -m pipenv install -e . ``` -# Developer Workflow +# Developer Workflow (Building & Publishing) If you modify the code and want to publish a new version to TestPyPI, follow these steps: ```bash -#clean old build artifacts +#1. CLEAN old build artifacts rm -rf dist build src/*.egg-info pipenv install build -#bump version number in pyproject.toml (e.g., 0.1.0 → 0.1.1) +#2. BUMP version number in pyproject.toml (e.g., 0.1.0 → 0.1.1) + +#3. BUILD the package pipenv run python -m build -#upload new version to TestPyPI +#4. UPLOAD new version to TestPyPI pipenv install twine pipenv run twine upload -r testpypi dist/* ``` -Then reinstall to test it: +5. REINSTALL to test it: ```bash pipenv run pip uninstall -y eatnyc pipenv install -i https://test.pypi.org/simple/ --extra-index-url https://pypi.org/simple eatnyc==0.1.1 ``` -Once final, upload to real PyPI with: +6. Once final, UPLOAD to real PyPI (final version) with: ```bash pipenv run twine upload dist/* ``` --- -## PyPI Link +## Project Links - **PyPI:** [https://pypi.org/project/eatnyc](https://pypi.org/project/eatnyc) - **TestPyPI:** [https://test.pypi.org/project/eatnyc](https://test.pypi.org/project/eatnyc) +- **Github Repo:** [https://github.com/swe-students-fall2025/3-python-package-team_avalon.git](https://github.com/swe-students-fall2025/3-python-package-team_avalon.git) # Contributors - [amiraadum](https://github.com/amiraadum) diff --git a/pyproject.toml b/pyproject.toml index 0f9271a..b205e50 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta" [project] name = "eatnyc" -version = "0.1.0" +version = "0.1.1" description = "A tiny NYC restaurant recommender near NYU." readme = "README.md" requires-python = ">=3.10" From b942bbc5c6be58160a14002406f485b0e2583bac Mon Sep 17 00:00:00 2001 From: Ivan-Wang-tech <157322972+Ivan-Wang-tech@users.noreply.github.com> Date: Sun, 2 Nov 2025 16:50:08 -0500 Subject: [PATCH 27/42] update demo.py --- examples/demo.py | 30 +++++++++++++++++++++++++----- 1 file changed, 25 insertions(+), 5 deletions(-) diff --git a/examples/demo.py b/examples/demo.py index c8f6c87..f9395a3 100644 --- a/examples/demo.py +++ b/examples/demo.py @@ -3,13 +3,33 @@ def main(): data = load_data() print("rows:", len(data)) - print("Top by rating:") + + # === Top restaurants by rating === + print("\n=== Top 5 by rating (simple print) ===") for r in top_n(data, n=5): - print(f"- {r['name']} ({r['cuisine']}, {r['price']}) ★ {r['rating']} – {r['sample_dish']}") + print(f"- {r['name']} ({r['cuisine']}, {r['price']}) ★ {r['rating']} – {r['sample_dish']}") + + # === Top restaurants by rating with format_card === + print("\n=== Top 5 by rating (format_card output) ===") + for r in top_n(data, n=5): + print(format_card(r, width=60)) - print("filtering Korean restaurants in Koreatown with a $$ price and a min_rating of 4.5):") - for r in filter_restaurants(data, cuisine="Korean", neighborhood="Koreatown", price="$$", min_rating=4.5): - print(f"- {r['name']} ({r['cuisine']}, {r['price']}) ★ {r['rating']} – {r['sample_dish']}") + # === Filtering example === + print("\n=== Korean restaurants in Koreatown with $$ and rating >= 4.5 (simple print) ===") + filtered = filter_restaurants( + data, + cuisine="Korean", + neighborhood="Koreatown", + price="$$", + min_rating=4.5, + ) + for r in filtered: + print(f"- {r['name']} ({r['cuisine']}, {r['price']}) ★ {r['rating']} – {r['sample_dish']}") + + # === Same filter + card display === + print("\n=== Same filtered results (format_card output) ===") + for r in filtered: + print(format_card(r, width=60)) if __name__ == "__main__": main() \ No newline at end of file From 0dd9383f97a13f385b57557efe8c86fa5374a933 Mon Sep 17 00:00:00 2001 From: Ivan-Wang-tech <157322972+Ivan-Wang-tech@users.noreply.github.com> Date: Sun, 2 Nov 2025 17:42:42 -0500 Subject: [PATCH 28/42] update init.py --- src/eatnyc/__init__.py | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/src/eatnyc/__init__.py b/src/eatnyc/__init__.py index e4c2ddd..4975421 100644 --- a/src/eatnyc/__init__.py +++ b/src/eatnyc/__init__.py @@ -1,3 +1,10 @@ +from importlib.metadata import version, PackageNotFoundError from .core import load_data, filter_restaurants, top_n, sample_dish, format_card + __all__ = ["load_data", "filter_restaurants", "top_n", "sample_dish", "format_card"] -__version__ = "0.1.0" \ No newline at end of file + +# Try to get the installed version; fallback for dev/local runs +try: + __version__ = version("eatnyc") +except PackageNotFoundError: + __version__ = "0.0.0" From 66334a508fcd041d36e8ee7685c9c9118db5db3f Mon Sep 17 00:00:00 2001 From: hyunkyuu Date: Mon, 3 Nov 2025 11:58:46 -0500 Subject: [PATCH 29/42] feat: add sample_dish function --- src/eatnyc/core.py | 49 ++++++++++++++++++++- tests/test_sample_dish.py | 93 +++++++++++++++++++++++++++++++++++++++ 2 files changed, 141 insertions(+), 1 deletion(-) diff --git a/src/eatnyc/core.py b/src/eatnyc/core.py index 0497228..c7e2d55 100644 --- a/src/eatnyc/core.py +++ b/src/eatnyc/core.py @@ -145,7 +145,54 @@ def key_func(row): def sample_dish(cuisine=None, seed=None): - return + ''' + Return a random restaurant with its sample dish recommendation. + ''' + if seed is not None: + random.seed(seed) + + # Load the restaurant data + data = load_data() + + # Filter by cuisine if provided + if cuisine: + cuisine_lower = cuisine.strip().lower() + filtered = [ + row for row in data + if cuisine_lower in row.get("_cuisines", []) + ] + + # Filter out entries without a sample_dish + with_dishes = [ + row for row in filtered + if row.get("sample_dish", "").strip() + ] + + if not with_dishes: + # Get all available cuisines + all_cuisines = set() + for row in data: + all_cuisines.update(row.get("_cuisines", [])) + + # Suggest some alternatives (random 3-5 cuisines) + suggestions = random.sample(sorted(all_cuisines), min(5, len(all_cuisines))) + + return { + "error": f"No restaurants found for cuisine '{cuisine}'", + "suggestions": suggestions, + "message": "Maybe try these instead?" + } + else: + # No cuisine specified, use all restaurants + with_dishes = [ + row for row in data + if row.get("sample_dish", "").strip() + ] + + if not with_dishes: + return None + + return random.choice(with_dishes) def format_card(row, style="ascii", width=60, show_dish=True): diff --git a/tests/test_sample_dish.py b/tests/test_sample_dish.py index e69de29..a7ed58d 100644 --- a/tests/test_sample_dish.py +++ b/tests/test_sample_dish.py @@ -0,0 +1,93 @@ +import pytest +from eatnyc.core import sample_dish, load_data + + +def test_sample_dish_returns_restaurant(): + """Test that sample_dish returns a restaurant dict""" + result = sample_dish(seed=42) + assert result is not None + assert isinstance(result, dict) + assert "name" in result + assert "sample_dish" in result + + +def test_sample_dish_with_cuisine_filter(): + """Test that sample_dish filters by cuisine correctly""" + result = sample_dish(cuisine="Italian", seed=42) + assert result is not None + assert isinstance(result, dict) + # Check that it's actually Italian cuisine + assert "italian" in result.get("_cuisines", []) + + +def test_sample_dish_has_dish(): + """Test that returned restaurant has a sample_dish""" + result = sample_dish(seed=42) + assert result is not None + dish = result.get("sample_dish", "").strip() + assert dish != "" + + +def test_sample_dish_reproducible_with_seed(): + """Test that same seed produces same result""" + result1 = sample_dish(cuisine="Italian", seed=123) + result2 = sample_dish(cuisine="Italian", seed=123) + + # Both should return same restaurant + assert result1.get("name") == result2.get("name") + + +def test_sample_dish_invalid_cuisine(): + """Test that invalid cuisine returns error with suggestions""" + result = sample_dish(cuisine="InvalidCuisine123", seed=42) + + assert isinstance(result, dict) + assert "error" in result + assert "suggestions" in result + assert "message" in result + assert result["message"] == "Maybe try these instead?" + assert isinstance(result["suggestions"], list) + assert len(result["suggestions"]) > 0 + + +def test_sample_dish_no_cuisine(): + """Test that no cuisine parameter returns any restaurant""" + result = sample_dish(seed=42) + assert result is not None + assert isinstance(result, dict) + assert "name" in result + + +def test_sample_dish_cuisine_case_insensitive(): + """Test that cuisine filter is case-insensitive""" + result1 = sample_dish(cuisine="italian", seed=42) + result2 = sample_dish(cuisine="ITALIAN", seed=42) + result3 = sample_dish(cuisine="Italian", seed=42) + + # All should return the same result with same seed + assert result1.get("name") == result2.get("name") + assert result2.get("name") == result3.get("name") + + +@pytest.mark.parametrize("cuisine", ["Mexican", "French", "American", "Greek"]) +def test_sample_dish_various_cuisines(cuisine): + """Test that sample_dish works with different cuisines""" + result = sample_dish(cuisine=cuisine, seed=42) + assert result is not None + assert isinstance(result, dict) + assert cuisine.lower() in result.get("_cuisines", []) + + +@pytest.fixture +def sample_data(): + """Fixture to load data once for multiple tests""" + return load_data() + + +def test_sample_dish_returns_from_actual_data(sample_data): + """Test that sample_dish returns data from the loaded dataset""" + result = sample_dish(seed=42) + assert result is not None + # Check that the result exists in the actual data + names = [row.get("name") for row in sample_data] + assert result.get("name") in names From 87420bfa1c68edb3b06cde52f3868bf09e129a06 Mon Sep 17 00:00:00 2001 From: hyunkyuu Date: Mon, 3 Nov 2025 12:07:17 -0500 Subject: [PATCH 30/42] fix linting and formatting --- src/eatnyc/__main__.py | 2 +- src/eatnyc/core.py | 40 +++++++++++++++++++--------------------- 2 files changed, 20 insertions(+), 22 deletions(-) diff --git a/src/eatnyc/__main__.py b/src/eatnyc/__main__.py index 616450a..e1aa4af 100644 --- a/src/eatnyc/__main__.py +++ b/src/eatnyc/__main__.py @@ -1,4 +1,4 @@ from .core import cli if __name__ == "__main__": - cli() \ No newline at end of file + cli() diff --git a/src/eatnyc/core.py b/src/eatnyc/core.py index c7e2d55..5bede70 100644 --- a/src/eatnyc/core.py +++ b/src/eatnyc/core.py @@ -1,18 +1,17 @@ import csv import random -import os -from importlib.resources import files +from importlib.resources import files _DATA_PKG = "eatnyc" _DEFAULT_CSV = "data/nyc_restaurant_data.csv" -#columns we expect in the CSV -_REQUIRED_COLS = {"name", "cuisine","neighborhood", "price", "rating", "sample_dish"} +# columns we expect in the CSV +_REQUIRED_COLS = {"name", "cuisine", "neighborhood", "price", "rating", "sample_dish"} -#new function (NORMALIZE_ROWS) +# new function (NORMALIZE_ROWS) def _normalize_row(row: dict) -> dict: - #Clean up one CSV row: strip spaces, normalize case/types, compute helper fields. + # Clean up one CSV row: strip spaces, normalize case/types, compute helper fields. clean = {k.strip().lower(): (v.strip() if isinstance(v, str) else v) for k, v in row.items()} # rating → float (default 0.0) @@ -34,14 +33,15 @@ def _normalize_row(row: dict) -> dict: return clean -#Load the NYC restaurant CSV into a list of dicts. + +# Load the NYC restaurant CSV into a list of dicts. def load_data(path: str | None = None, validate: bool = True) -> list[dict]: - #If `path` is None, loads the bundled file from eatnyc/data/. - #Keys are lowercased; rating is converted to float. - #Adds helper lists: `_cuisines`. + # If `path` is None, loads the bundled file from eatnyc/data/. + # Keys are lowercased; rating is converted to float. + # Adds helper lists: `_cuisines`. if path is None: - #use importlib.resources so this works from wheels/zip installs + # use importlib.resources so this works from wheels/zip installs resource = files(_DATA_PKG) / _DEFAULT_CSV f = resource.open("r", encoding="utf-8", newline="") else: @@ -61,13 +61,13 @@ def load_data(path: str | None = None, validate: bool = True) -> list[dict]: def filter_restaurants(data, cuisine=None, neighborhood=None, price=None, min_rating=None, limit=None): - #Error checking for empty or invalid data + # Error checking for empty or invalid data if not isinstance(data, list): raise TypeError("Data must be a list of dicts") - + if not data: return [] - + results = [] # Filtering logic @@ -205,7 +205,7 @@ def format_card(row, style="ascii", width=60, show_dish=True): rating = float(row.get("rating", 0.0)) except (TypeError, ValueError): rating = 0.0 - + sample = str(row.get("sample_dish", "") or "").strip() title_line = name if name else "(unknown)" @@ -239,10 +239,8 @@ def _wrap_line(line: str, inner_width: int): box_width = max(24, int(width) if isinstance(width, int) else 48) inner = box_width - 2 - logical_lines = [f"{title_line} ({sub_line})" if sub_line else title_line, - meta_line, - dish_line] - + logical_lines = [f"{title_line} ({sub_line})" if sub_line else title_line, meta_line, dish_line] + wrapped = [] for ln in logical_lines: wrapped.extend(_wrap_line(ln, inner) if ln else [""]) @@ -260,7 +258,7 @@ def cli(argv=None): # $ eatnyc -> prints top 5 by rating # $ eatnyc -n 10 -> prints top 10 # $ eatnyc --sort name --asc -> sort by name ascending - + parser = argparse.ArgumentParser(prog="eatnyc", description="NYC restaurant recommender") parser.add_argument("-n", "--n", type=int, default=5, help="number of results") parser.add_argument("--sort", default="rating", help="field to sort by (rating, name, price, etc.)") @@ -270,4 +268,4 @@ def cli(argv=None): data = load_data() results = top_n(data, n=args.n, sort_by=args.sort, descending=not args.asc) for r in results: - print(f"{r['name']} | {r['cuisine']} | {r['price']} | ★{r['rating']} | {r.get('sample_dish','')}") + print(f"{r['name']} | {r['cuisine']} | {r['price']} | ★{r['rating']} | {r.get('sample_dish', '')}") From 0f80e7f4b1b2143a8fffb25f9772854412932b21 Mon Sep 17 00:00:00 2001 From: hyunkyuu Date: Mon, 3 Nov 2025 12:18:20 -0500 Subject: [PATCH 31/42] feat: add sample_dish to demo.py --- examples/demo.py | 32 ++++++++++++++++++++++++++++---- 1 file changed, 28 insertions(+), 4 deletions(-) diff --git a/examples/demo.py b/examples/demo.py index f9395a3..3014da5 100644 --- a/examples/demo.py +++ b/examples/demo.py @@ -1,9 +1,10 @@ -from eatnyc import load_data, top_n, filter_restaurants, format_card # add others as they land +from eatnyc import load_data, top_n, filter_restaurants, sample_dish, format_card + def main(): data = load_data() print("rows:", len(data)) - + # === Top restaurants by rating === print("\n=== Top 5 by rating (simple print) ===") for r in top_n(data, n=5): @@ -13,7 +14,7 @@ def main(): print("\n=== Top 5 by rating (format_card output) ===") for r in top_n(data, n=5): print(format_card(r, width=60)) - + # === Filtering example === print("\n=== Korean restaurants in Koreatown with $$ and rating >= 4.5 (simple print) ===") filtered = filter_restaurants( @@ -31,5 +32,28 @@ def main(): for r in filtered: print(format_card(r, width=60)) + # === Sample dish recommendations === + print("\n=== Sample dish: Random Italian restaurant ===") + italian = sample_dish(cuisine="Italian", seed=42) + if isinstance(italian, dict) and "error" not in italian: + print(f"Try: {italian['sample_dish']} at {italian['name']}") + print(format_card(italian, width=60)) + else: + print(f"{italian['error']}") + print(f"{italian['message']} {', '.join(italian['suggestions'])}") + + print("\n=== Sample dish: Random restaurant (any cuisine) ===") + random_restaurant = sample_dish(seed=123) + if random_restaurant: + print(f"Try: {random_restaurant['sample_dish']} at {random_restaurant['name']}") + print(format_card(random_restaurant, width=60)) + + print("\n=== Sample dish: Invalid cuisine (should show suggestions) ===") + invalid = sample_dish(cuisine="Martian") + if isinstance(invalid, dict) and "error" in invalid: + print(f"{invalid['error']}") + print(f"{invalid['message']} {', '.join(invalid['suggestions'][:3])}") + + if __name__ == "__main__": - main() \ No newline at end of file + main() From 8a28cbbb75b326827e7d9ad6f86664a551b35274 Mon Sep 17 00:00:00 2001 From: lilyluo7412 Date: Mon, 3 Nov 2025 14:57:14 -0500 Subject: [PATCH 32/42] Update README to use 'neighborhood' in example Replaces 'borough' with 'neighborhood' in the filter_restaurants example to reflect the correct parameter name. --- README.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index 75a2e7b..e611605 100644 --- a/README.md +++ b/README.md @@ -44,11 +44,11 @@ from eatnyc import load_data, filter_restaurants, top_n, sample_dish, format_car data = load_data() -# Filter restaurants by cuisine and borough +# Filter restaurants by cuisine and neighborhood italian_manhattan = filter_restaurants( data, cuisine="Italian", - borough="Manhattan", + neighborhood="Manhattan", min_rating=4.0 ) From ddfb62de93c6b1f5defca2d6fcbe107410e864e4 Mon Sep 17 00:00:00 2001 From: jmo7728 Date: Mon, 3 Nov 2025 15:11:39 -0500 Subject: [PATCH 33/42] Added comments for better documentation --- src/eatnyc/core.py | 6 ++++-- tests/test_filter_restaurants.py | 7 ++++++- 2 files changed, 10 insertions(+), 3 deletions(-) diff --git a/src/eatnyc/core.py b/src/eatnyc/core.py index 5bede70..572676a 100644 --- a/src/eatnyc/core.py +++ b/src/eatnyc/core.py @@ -73,13 +73,15 @@ def filter_restaurants(data, cuisine=None, neighborhood=None, price=None, min_ra # Filtering logic for row in data: clean = _normalize_row(row) + #Check cuisine match if cuisine: if cuisine.strip().lower() not in clean.get("_cuisines", []): continue - + #Check neighborhood match if neighborhood: if neighborhood.strip().lower() != row.get("neighborhood", "").lower(): continue + #Check price match if price: if price.strip() != clean.get("price", ""): continue @@ -92,7 +94,7 @@ def filter_restaurants(data, cuisine=None, neighborhood=None, price=None, min_ra continue results.append(row) - + #if its over the limit, break if limit: if len(results) >= limit: break diff --git a/tests/test_filter_restaurants.py b/tests/test_filter_restaurants.py index 27294ea..bbe7412 100644 --- a/tests/test_filter_restaurants.py +++ b/tests/test_filter_restaurants.py @@ -9,31 +9,36 @@ def _sample_data(): {"name": "D", "cuisine": "Y", "neighborhood": "N2", "price": "$$$", "rating": 4.8, "sample_dish": "d4"}, ] +#test case for cuisine filter def test_filter_restaurants_cuisine(): data = _sample_data() result = filter_restaurants(data, cuisine="Y") assert [r["cuisine"] for r in result] == ["Y", "Y"] +#test case for neighborhood filter def test_filter_restaurants_neighborhood(): data = _sample_data() result = filter_restaurants(data, neighborhood="N2") assert [r["neighborhood"] for r in result] == ["N2", "N2"] +#test case for price filter def test_filter_restaurants_price(): data = _sample_data() result = filter_restaurants(data, price="$$$") assert [r["price"] for r in result] == ["$$$", "$$$"] +#test case for minimum rating filter def test_filter_restaurants_min_rating(): data = _sample_data() result = filter_restaurants(data, min_rating=4.5) assert [r["rating"] for r in result] == [4.8, 4.8] +#test case for sorting by name def test_filter_restaurants_limit(): data = _sample_data() result = filter_restaurants(data, limit=2) assert len(result) == 2 - +# test case for sorting by name def test_filter_restaurants_no_matches(): data = _sample_data() result = filter_restaurants(data, cuisine="Nonexistent") From baa4e164506b6694f9b1091ae2f1b11dfbe4b974 Mon Sep 17 00:00:00 2001 From: jmo7728 Date: Mon, 3 Nov 2025 15:15:21 -0500 Subject: [PATCH 34/42] Added small documentation --- tests/test_filter_restaurants.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/tests/test_filter_restaurants.py b/tests/test_filter_restaurants.py index bbe7412..e13f7d0 100644 --- a/tests/test_filter_restaurants.py +++ b/tests/test_filter_restaurants.py @@ -1,6 +1,8 @@ import pytest from eatnyc import filter_restaurants + +#Standard Sample Data def _sample_data(): return [ {"name": "B", "cuisine": "X", "neighborhood": "N1", "price": "$$", "rating": 4.2, "sample_dish": "d1"}, From 5b06a7145b911b6ecac0881b157cf397f129b5b3 Mon Sep 17 00:00:00 2001 From: Ivan-Wang-tech <157322972+Ivan-Wang-tech@users.noreply.github.com> Date: Tue, 4 Nov 2025 00:32:49 -0500 Subject: [PATCH 35/42] add Makefile --- Makefile | 62 +++++++++++++++++++++++++++++++++++++++++++++++++++++++ Pipfile | 2 +- README.md | 8 +++++++ 3 files changed, 71 insertions(+), 1 deletion(-) create mode 100644 Makefile diff --git a/Makefile b/Makefile new file mode 100644 index 0000000..d14a3b6 --- /dev/null +++ b/Makefile @@ -0,0 +1,62 @@ +PACKAGE := eatnyc + +# Read version from pyproject.toml +VERSION := $(shell python3 -c "import tomllib;print(tomllib.load(open('pyproject.toml','rb'))['project']['version'])") + +WHEEL := dist/$(PACKAGE)-$(VERSION)-py3-none-any.whl + +.PHONY: show-version clean build uninstall install-wheel reinstall test install-testpypi verify dev-on dev-off + + +# Info +show-version: + @echo "Detected version: $(VERSION)" + + +# Build & Install (local wheel testing) +clean: + @echo "Cleaning build artifacts..." + rm -rf dist build src/*.egg-info + +build: clean + @echo "Building distribution..." + pipenv run python -m build + +install-wheel: + @echo "Installing wheel: $(WHEEL)" + pipenv install $(WHEEL) + +uninstall: + @echo "Uninstalling $(PACKAGE)..." + -pipenv run pip uninstall -y $(PACKAGE) + +reinstall: uninstall build install-wheel + @echo "Reinstalled local wheel for $(PACKAGE) $(VERSION)." + + +# Quick tests +test: + @echo "Quick import test..." + pipenv run python -c "import $(PACKAGE), sys; print('OK:', $(PACKAGE).__file__)" + +verify: + @echo "Verify package path & version..." + pipenv run python -c "import importlib.metadata as m, $(PACKAGE); print('Path:', $(PACKAGE).__file__); print('Version:', m.version('$(PACKAGE)'))" + + +# TestPyPI Install Testing +install-testpypi: + @echo "Installing $(PACKAGE)==$(VERSION) from TestPyPI..." + PIP_INDEX_URL=https://test.pypi.org/simple \ + PIP_EXTRA_INDEX_URL=https://pypi.org/simple \ + pipenv run pip install $(PACKAGE)==$(VERSION) + + +# Developer Mode Toggle (editable) +dev-on: uninstall + @echo "Switching to developer editable mode..." + pipenv install -e . + @echo "✅ Developer mode ON (editable)." + +dev-off: uninstall install-testpypi + @echo "✅ Developer mode OFF (TestPyPI package installed)." diff --git a/Pipfile b/Pipfile index f06be7a..0fd8f54 100644 --- a/Pipfile +++ b/Pipfile @@ -5,9 +5,9 @@ name = "pypi" [packages] pytest = "*" -eatnyc = {file = ".", editable = true} build = "*" twine = "*" +eatnyc = {file = ".", editable = true} [dev-packages] build = "*" diff --git a/README.md b/README.md index 75a2e7b..3cabd72 100644 --- a/README.md +++ b/README.md @@ -92,6 +92,14 @@ If that set up fails for you, use: python3 -m pipenv install -e . ``` +## Developer Mode Switch (using Makefile) + +```bash +make dev-on # install editable +make dev-off # restore TestPyPI version +make verify # confirm path +``` + # Developer Workflow (Building & Publishing) If you modify the code and want to publish a new version to TestPyPI, follow these steps: ```bash From 9621495a0dfaedff023c3880570ddf5bbc905104 Mon Sep 17 00:00:00 2001 From: Ivan-Wang-tech <157322972+Ivan-Wang-tech@users.noreply.github.com> Date: Tue, 4 Nov 2025 01:09:50 -0500 Subject: [PATCH 36/42] update readme.md --- Makefile | 88 ++++++++++++++++++++++--- Pipfile | 2 +- Pipfile.lock | 8 ++- examples/test_eatnyc.py | 88 +++++++++++++++++++++++++ src/eatnyc/data/nyc_restaurant_data.csv | 12 ++-- 5 files changed, 179 insertions(+), 19 deletions(-) create mode 100644 examples/test_eatnyc.py diff --git a/Makefile b/Makefile index d14a3b6..18f5993 100644 --- a/Makefile +++ b/Makefile @@ -1,19 +1,55 @@ +# ======================= +# Project Settings +# ======================= PACKAGE := eatnyc -# Read version from pyproject.toml +# Read version from pyproject.toml (safe, single-line Python) VERSION := $(shell python3 -c "import tomllib;print(tomllib.load(open('pyproject.toml','rb'))['project']['version'])") WHEEL := dist/$(PACKAGE)-$(VERSION)-py3-none-any.whl -.PHONY: show-version clean build uninstall install-wheel reinstall test install-testpypi verify dev-on dev-off - - +# ======================= +# Phony Targets +# ======================= +.PHONY: help show-version clean build uninstall install-wheel reinstall \ + test test-smoke verify install-testpypi \ + dev-on dev-off \ + test-unit \ + release-test release \ + tag push-tag + +# ======================= +# Help +# ======================= +help: + @echo "Make targets:" + @echo " show-version - Print detected version from pyproject.toml" + @echo " clean - Remove build artifacts" + @echo " build - Build wheel/sdist" + @echo " uninstall - Uninstall $(PACKAGE) from current Pipenv env" + @echo " install-wheel - Install local wheel" + @echo " reinstall - Clean build + install local wheel" + @echo " test - Smoke test (import and show path)" + @echo " test-smoke - Alias of 'test'" + @echo " verify - Show import path and installed version" + @echo " test-unit - Run pytest (use in dev-on/editable mode)" + @echo " install-testpypi - Install $(PACKAGE)==$(VERSION) from TestPyPI" + @echo " dev-on - Editable install (-e .)" + @echo " dev-off - Switch to TestPyPI-installed package" + @echo " release-test - Upload dist/* to TestPyPI via twine" + @echo " release - Upload dist/* to PyPI via twine" + @echo " tag - Create git tag v$(VERSION)" + @echo " push-tag - Push tag v$(VERSION) to origin" + +# ======================= # Info +# ======================= show-version: @echo "Detected version: $(VERSION)" - +# ======================= # Build & Install (local wheel testing) +# ======================= clean: @echo "Cleaning build artifacts..." rm -rf dist build src/*.egg-info @@ -33,26 +69,38 @@ uninstall: reinstall: uninstall build install-wheel @echo "Reinstalled local wheel for $(PACKAGE) $(VERSION)." - -# Quick tests +# ======================= +# Quick tests (smoke) +# ======================= test: @echo "Quick import test..." pipenv run python -c "import $(PACKAGE), sys; print('OK:', $(PACKAGE).__file__)" +test-smoke: test + verify: @echo "Verify package path & version..." - pipenv run python -c "import importlib.metadata as m, $(PACKAGE); print('Path:', $(PACKAGE).__file__); print('Version:', m.version('$(PACKAGE)'))" + pipenv run python -c "import importlib.metadata as m, $(PACKAGE); print('Path :', $(PACKAGE).__file__); print('Version:', m.version('$(PACKAGE)'))" +# ======================= +# Unit tests (pytest) — run in dev-on/editable mode +# ======================= +test-unit: + @echo "Running pytest..." + pipenv run pytest -q +# ======================= # TestPyPI Install Testing +# ======================= install-testpypi: @echo "Installing $(PACKAGE)==$(VERSION) from TestPyPI..." PIP_INDEX_URL=https://test.pypi.org/simple \ PIP_EXTRA_INDEX_URL=https://pypi.org/simple \ pipenv run pip install $(PACKAGE)==$(VERSION) - +# ======================= # Developer Mode Toggle (editable) +# ======================= dev-on: uninstall @echo "Switching to developer editable mode..." pipenv install -e . @@ -60,3 +108,25 @@ dev-on: uninstall dev-off: uninstall install-testpypi @echo "✅ Developer mode OFF (TestPyPI package installed)." + +# ======================= +# Release (local twine) — requires ~/.pypirc or env TWINE_USERNAME/TWINE_PASSWORD +# ======================= +release-test: build + @echo "Uploading to TestPyPI via twine..." + pipenv run twine upload -r testpypi dist/* + +release: build + @echo "Uploading to PyPI via twine..." + pipenv run twine upload dist/* + +# ======================= +# Git tags (to trigger CI release workflows) +# ======================= +tag: + @git tag v$(VERSION) + @echo "Created tag v$(VERSION)." + +push-tag: + @git push origin v$(VERSION) + @echo "Pushed tag v$(VERSION) to origin." diff --git a/Pipfile b/Pipfile index 0fd8f54..4a9d9ec 100644 --- a/Pipfile +++ b/Pipfile @@ -7,7 +7,7 @@ name = "pypi" pytest = "*" build = "*" twine = "*" -eatnyc = {file = ".", editable = true} +eatnyc = {file = "dist/eatnyc-0.1.1-py3-none-any.whl"} [dev-packages] build = "*" diff --git a/Pipfile.lock b/Pipfile.lock index e6b36b4..7570530 100644 --- a/Pipfile.lock +++ b/Pipfile.lock @@ -1,7 +1,7 @@ { "_meta": { "hash": { - "sha256": "542c1b089337be7a81ae6849c863b3dafcf7d86b48250ecc8cf7ed5731bc43be" + "sha256": "667fdf761d831d60cfce2d6026e9df5abb4f093cbbf6f8a66baac5061a29dd2a" }, "pipfile-spec": 6, "requires": { @@ -169,8 +169,10 @@ "version": "==0.22.2" }, "eatnyc": { - "editable": true, - "file": "." + "file": "dist/eatnyc-0.1.1-py3-none-any.whl", + "hashes": [ + "sha256:80d084644a2c2ef62c332607ebc69f419379ff5601c3cad19cf26f110e296f2b" + ] }, "id": { "hashes": [ diff --git a/examples/test_eatnyc.py b/examples/test_eatnyc.py new file mode 100644 index 0000000..41c84cd --- /dev/null +++ b/examples/test_eatnyc.py @@ -0,0 +1,88 @@ +from importlib.metadata import version as pkg_version +from collections import Counter +from pprint import pprint + +from eatnyc import load_data, filter_restaurants, top_n, sample_dish, format_card + +def print_header(title: str): + print("\n" + "=" * 72) + print(title) + print("=" * 72) + +print_header("Version") +print("eatnyc version:", pkg_version("eatnyc")) + +# 1) Load data (list[dict]) +print_header("Load Data") +data = load_data() +print("raw rows:", len(data)) +print("first 3 rows:") +pprint(data[:3]) + +if not data: + raise SystemExit("No data loaded. Check bundled CSV and package-data config.") + +# 1.1 Explore distinct field values to choose valid filters +print_header("Distinct values (to choose valid filters)") +def distinct(key): + vals = [ (row.get(key) or "").strip() for row in data ] + vals = [v for v in vals if v] + return [v for v,_ in Counter(vals).most_common()] + +cuisines = sorted({c for row in data for c in row.get("_cuisines", [])}) +neighborhoods = distinct("neighborhood") +prices = distinct("price") + +print("cuisines:", cuisines[:15], ("... total %d" % len(cuisines)) if len(cuisines)>15 else "") +print("neighborhoods (sample):", neighborhoods[:10]) +print("prices:", prices) + +# 2) Filter — use values that exist in your CSV +# Example that should match your sample data: +# cuisine: "american" exists (4 Charles Prime Rib) +# neighborhood: "West Village" exists +# price: "$$$$" exists +print_header("Filter Restaurants (American @ West Village, $$$$, rating>=4.0, limit=10)") +filtered = filter_restaurants( + data, + cuisine="American", # case-insensitive via your normalizer + neighborhood="West Village", # must match CSV spelling + price="$$$$", + min_rating=4.0, + limit=10, +) +print("filtered rows:", len(filtered)) +pprint(filtered[:3]) + +# 3) Top-N from filtered +print_header("Top-N (rating desc) from filtered") +top = top_n(filtered, n=5, sort_by="rating", descending=True) +print(f"top size: {len(top)}") +for i, r in enumerate(top, 1): + print(f"[{i}] {r.get('name')} | {r.get('cuisine')} | {r.get('neighborhood')} | {r.get('price')} | ★{r.get('rating')}") + +# 4) Render a card for the first item (ASCII + Markdown) +if top: + first = top[0] + print_header("format_card (ASCII)") + print(format_card(first, style="ascii", width=48, show_dish=True)) + + print_header("format_card (Markdown)") + print(format_card(first, style="markdown", show_dish=True)) +else: + print_header("No results to format (top is empty)") + +# 5) Sample dish demo — robust handling of None / error dict / row dict +print_header("sample_dish (try an existing cuisine, e.g. 'american' or 'french')") +sd = sample_dish(cuisine="American", seed=42) + +if not sd: + print("No sample_dish available in dataset.") +elif isinstance(sd, dict) and "error" in sd: + print("No dish for selected cuisine.") + print("error:", sd["error"]) + print("suggestions:", sd.get("suggestions")) +else: + print("Random pick with dish:") + print(f"{sd.get('name')} — {sd.get('sample_dish')} " + f"(cuisine: {sd.get('cuisine')}, rating: {sd.get('rating')})") \ No newline at end of file diff --git a/src/eatnyc/data/nyc_restaurant_data.csv b/src/eatnyc/data/nyc_restaurant_data.csv index 8e477de..d8be3fc 100644 --- a/src/eatnyc/data/nyc_restaurant_data.csv +++ b/src/eatnyc/data/nyc_restaurant_data.csv @@ -1,13 +1,13 @@ name,cuisine,neighborhood,price,rating,sample_dish -Reyna,Mediterranean,Greenwhich Village,$$$,4.5,Lamb Baklava +Reyna,Mediterranean,Greenwich Village,$$$,4.5,Lamb Baklava 4 Charles Prime Rib,American,West Village,$$$$,4.5,Burger César,French,Hudson Square,$$$$,4.0,Uni Toast Torrisi,Italian,Nolita,$$$$,4.5,Tortellini Pomodoro -Gene's Restaurant,Italian,Greenwhich Village,$$$,4.0,Vodka Pasta -Strip House,American,Greenwhich Village,$$$$,4.8,Chocolate Cake -Da Andrea,Italian,Greenwhich Village,$$,4.5,Carbonara -Village Taverna,Greek,Greenwhich Village,$$$,4.8,Chicken Souvlaki -Il Cantinori,Tuscan,Greenwhich Village,$$$$,4.8,Pasta Trio +Gene's Restaurant,Italian,Greenwich Village,$$$,4.0,Vodka Pasta +Strip House,American,Greenwich Village,$$$$,4.8,Chocolate Cake +Da Andrea,Italian,Greenwich Village,$$,4.5,Carbonara +Village Taverna,Greek,Greenwich Village,$$$,4.8,Chicken Souvlaki +Il Cantinori,Tuscan,Greenwich Village,$$$$,4.8,Pasta Trio Morandi,Italian ,West Village,$$,3.5,Lemon Pasta Little Owl,Mediterranean,West Village,$$$,4.8,Meatball Sliders Buvette,French,West Village,$$,4.7,Croque Madame From 75beb0778e9cdf02eb198d56cd377f15de107212 Mon Sep 17 00:00:00 2001 From: Ivan-Wang-tech <157322972+Ivan-Wang-tech@users.noreply.github.com> Date: Tue, 4 Nov 2025 01:23:54 -0500 Subject: [PATCH 37/42] fix: clean Pipfile & regenerate lock --- Pipfile | 4 -- Pipfile.lock | 121 +++++++++++++++++++++++++++++++++++++++++++++++---- 2 files changed, 112 insertions(+), 13 deletions(-) diff --git a/Pipfile b/Pipfile index 4a9d9ec..735bc73 100644 --- a/Pipfile +++ b/Pipfile @@ -7,13 +7,9 @@ name = "pypi" pytest = "*" build = "*" twine = "*" -eatnyc = {file = "dist/eatnyc-0.1.1-py3-none-any.whl"} [dev-packages] build = "*" twine = "*" pytest = "*" exceptiongroup = "*" - -[requires] -python_version = "3.10" diff --git a/Pipfile.lock b/Pipfile.lock index 7570530..75d4449 100644 --- a/Pipfile.lock +++ b/Pipfile.lock @@ -1,12 +1,10 @@ { "_meta": { "hash": { - "sha256": "667fdf761d831d60cfce2d6026e9df5abb4f093cbbf6f8a66baac5061a29dd2a" + "sha256": "eae26392b57d731ef73e3f35a05d51b2be194dea5b27a243d863608a65bddda2" }, "pipfile-spec": 6, - "requires": { - "python_version": "3.10" - }, + "requires": {}, "sources": [ { "name": "pypi", @@ -168,11 +166,13 @@ "markers": "python_version >= '3.9'", "version": "==0.22.2" }, - "eatnyc": { - "file": "dist/eatnyc-0.1.1-py3-none-any.whl", + "exceptiongroup": { "hashes": [ - "sha256:80d084644a2c2ef62c332607ebc69f419379ff5601c3cad19cf26f110e296f2b" - ] + "sha256:4d111e6e0c13d0644cad6ddaa7ed0261a0b36971f6d23e7ec9b4b9097da78a10", + "sha256:b241f5885f560bc56a59ee63ca4c6a8bfa46ae4ad651af316d4e81817bb9fd88" + ], + "markers": "python_version >= '3.7'", + "version": "==1.3.0" }, "id": { "hashes": [ @@ -375,6 +375,54 @@ "markers": "python_full_version >= '3.8.0'", "version": "==14.2.0" }, + "tomli": { + "hashes": [ + "sha256:00b5f5d95bbfc7d12f91ad8c593a1659b6387b43f054104cda404be6bda62456", + "sha256:0a154a9ae14bfcf5d8917a59b51ffd5a3ac1fd149b71b47a3a104ca4edcfa845", + "sha256:0c95ca56fbe89e065c6ead5b593ee64b84a26fca063b5d71a1122bf26e533999", + "sha256:0eea8cc5c5e9f89c9b90c4896a8deefc74f518db5927d0e0e8d4a80953d774d0", + "sha256:1cb4ed918939151a03f33d4242ccd0aa5f11b3547d0cf30f7c74a408a5b99878", + "sha256:4021923f97266babc6ccab9f5068642a0095faa0a51a246a6a02fccbb3514eaf", + "sha256:4c2ef0244c75aba9355561272009d934953817c49f47d768070c3c94355c2aa3", + "sha256:4dc4ce8483a5d429ab602f111a93a6ab1ed425eae3122032db7e9acf449451be", + "sha256:4f195fe57ecceac95a66a75ac24d9d5fbc98ef0962e09b2eddec5d39375aae52", + "sha256:5192f562738228945d7b13d4930baffda67b69425a7f0da96d360b0a3888136b", + "sha256:5e01decd096b1530d97d5d85cb4dff4af2d8347bd35686654a004f8dea20fc67", + "sha256:64be704a875d2a59753d80ee8a533c3fe183e3f06807ff7dc2232938ccb01549", + "sha256:70a251f8d4ba2d9ac2542eecf008b3c8a9fc5c3f9f02c56a9d7952612be2fdba", + "sha256:73ee0b47d4dad1c5e996e3cd33b8a76a50167ae5f96a2607cbe8cc773506ab22", + "sha256:74bf8464ff93e413514fefd2be591c3b0b23231a77f901db1eb30d6f712fc42c", + "sha256:792262b94d5d0a466afb5bc63c7daa9d75520110971ee269152083270998316f", + "sha256:7b0882799624980785240ab732537fcfc372601015c00f7fc367c55308c186f6", + "sha256:883b1c0d6398a6a9d29b508c331fa56adbcdff647f6ace4dfca0f50e90dfd0ba", + "sha256:88bd15eb972f3664f5ed4b57c1634a97153b4bac4479dcb6a495f41921eb7f45", + "sha256:8a35dd0e643bb2610f156cca8db95d213a90015c11fee76c946aa62b7ae7e02f", + "sha256:940d56ee0410fa17ee1f12b817b37a4d4e4dc4d27340863cc67236c74f582e77", + "sha256:97d5eec30149fd3294270e889b4234023f2c69747e555a27bd708828353ab606", + "sha256:a0e285d2649b78c0d9027570d4da3425bdb49830a6156121360b3f8511ea3441", + "sha256:a1f7f282fe248311650081faafa5f4732bdbfef5d45fe3f2e702fbc6f2d496e0", + "sha256:a4ea38c40145a357d513bffad0ed869f13c1773716cf71ccaa83b0fa0cc4e42f", + "sha256:a56212bdcce682e56b0aaf79e869ba5d15a6163f88d5451cbde388d48b13f530", + "sha256:ad805ea85eda330dbad64c7ea7a4556259665bdf9d2672f5dccc740eb9d3ca05", + "sha256:b273fcbd7fc64dc3600c098e39136522650c49bca95df2d11cf3b626422392c8", + "sha256:b5870b50c9db823c595983571d1296a6ff3e1b88f734a4c8f6fc6188397de005", + "sha256:b74a0e59ec5d15127acdabd75ea17726ac4c5178ae51b85bfe39c4f8a278e879", + "sha256:be71c93a63d738597996be9528f4abe628d1adf5e6eb11607bc8fe1a510b5dae", + "sha256:c22a8bf253bacc0cf11f35ad9808b6cb75ada2631c2d97c971122583b129afbc", + "sha256:c4665508bcbac83a31ff8ab08f424b665200c0e1e645d2bd9ab3d3e557b6185b", + "sha256:c5f3ffd1e098dfc032d4d3af5c0ac64f6d286d98bc148698356847b80fa4de1b", + "sha256:cebc6fe843e0733ee827a282aca4999b596241195f43b4cc371d64fc6639da9e", + "sha256:d1381caf13ab9f300e30dd8feadb3de072aeb86f1d34a8569453ff32a7dea4bf", + "sha256:d7d86942e56ded512a594786a5ba0a5e521d02529b3826e7761a05138341a2ac", + "sha256:e31d432427dcbf4d86958c184b9bfd1e96b5b71f8eb17e6d02531f434fd335b8", + "sha256:e95b1af3c5b07d9e643909b5abbec77cd9f1217e6d0bca72b0234736b9fb1f1b", + "sha256:f85209946d1fe94416debbb88d00eb92ce9cd5266775424ff81bc959e001acaf", + "sha256:feb0dacc61170ed7ab602d3d972a58f14ee3ee60494292d384649a3dc38ef463", + "sha256:ff72b71b5d10d22ecb084d345fc26f42b5143c5533db5e2eaba7d2d335358876" + ], + "markers": "python_version >= '3.8'", + "version": "==2.3.0" + }, "twine": { "hashes": [ "sha256:418ebf08ccda9a8caaebe414433b0ba5e25eb5e4a927667122fbe8f829f985d8", @@ -384,6 +432,14 @@ "markers": "python_version >= '3.9'", "version": "==6.2.0" }, + "typing-extensions": { + "hashes": [ + "sha256:0cea48d173cc12fa28ecabc3b837ea3cf6f38c6d1136f85cbaaf598984861466", + "sha256:f0fa19c6845758ab08074a0cfa8b7aecb71c999ca73d62883bc25cc018c4e548" + ], + "markers": "python_version >= '3.9'", + "version": "==4.15.0" + }, "urllib3": { "hashes": [ "sha256:3fc47733c7e419d4bc3f6b3dc2b4f890bb743906a30d56ba4a5bfa4bbff92760", @@ -559,7 +615,6 @@ "sha256:4d111e6e0c13d0644cad6ddaa7ed0261a0b36971f6d23e7ec9b4b9097da78a10", "sha256:b241f5885f560bc56a59ee63ca4c6a8bfa46ae4ad651af316d4e81817bb9fd88" ], - "index": "pypi", "markers": "python_version >= '3.7'", "version": "==1.3.0" }, @@ -764,6 +819,54 @@ "markers": "python_full_version >= '3.8.0'", "version": "==14.2.0" }, + "tomli": { + "hashes": [ + "sha256:00b5f5d95bbfc7d12f91ad8c593a1659b6387b43f054104cda404be6bda62456", + "sha256:0a154a9ae14bfcf5d8917a59b51ffd5a3ac1fd149b71b47a3a104ca4edcfa845", + "sha256:0c95ca56fbe89e065c6ead5b593ee64b84a26fca063b5d71a1122bf26e533999", + "sha256:0eea8cc5c5e9f89c9b90c4896a8deefc74f518db5927d0e0e8d4a80953d774d0", + "sha256:1cb4ed918939151a03f33d4242ccd0aa5f11b3547d0cf30f7c74a408a5b99878", + "sha256:4021923f97266babc6ccab9f5068642a0095faa0a51a246a6a02fccbb3514eaf", + "sha256:4c2ef0244c75aba9355561272009d934953817c49f47d768070c3c94355c2aa3", + "sha256:4dc4ce8483a5d429ab602f111a93a6ab1ed425eae3122032db7e9acf449451be", + "sha256:4f195fe57ecceac95a66a75ac24d9d5fbc98ef0962e09b2eddec5d39375aae52", + "sha256:5192f562738228945d7b13d4930baffda67b69425a7f0da96d360b0a3888136b", + "sha256:5e01decd096b1530d97d5d85cb4dff4af2d8347bd35686654a004f8dea20fc67", + "sha256:64be704a875d2a59753d80ee8a533c3fe183e3f06807ff7dc2232938ccb01549", + "sha256:70a251f8d4ba2d9ac2542eecf008b3c8a9fc5c3f9f02c56a9d7952612be2fdba", + "sha256:73ee0b47d4dad1c5e996e3cd33b8a76a50167ae5f96a2607cbe8cc773506ab22", + "sha256:74bf8464ff93e413514fefd2be591c3b0b23231a77f901db1eb30d6f712fc42c", + "sha256:792262b94d5d0a466afb5bc63c7daa9d75520110971ee269152083270998316f", + "sha256:7b0882799624980785240ab732537fcfc372601015c00f7fc367c55308c186f6", + "sha256:883b1c0d6398a6a9d29b508c331fa56adbcdff647f6ace4dfca0f50e90dfd0ba", + "sha256:88bd15eb972f3664f5ed4b57c1634a97153b4bac4479dcb6a495f41921eb7f45", + "sha256:8a35dd0e643bb2610f156cca8db95d213a90015c11fee76c946aa62b7ae7e02f", + "sha256:940d56ee0410fa17ee1f12b817b37a4d4e4dc4d27340863cc67236c74f582e77", + "sha256:97d5eec30149fd3294270e889b4234023f2c69747e555a27bd708828353ab606", + "sha256:a0e285d2649b78c0d9027570d4da3425bdb49830a6156121360b3f8511ea3441", + "sha256:a1f7f282fe248311650081faafa5f4732bdbfef5d45fe3f2e702fbc6f2d496e0", + "sha256:a4ea38c40145a357d513bffad0ed869f13c1773716cf71ccaa83b0fa0cc4e42f", + "sha256:a56212bdcce682e56b0aaf79e869ba5d15a6163f88d5451cbde388d48b13f530", + "sha256:ad805ea85eda330dbad64c7ea7a4556259665bdf9d2672f5dccc740eb9d3ca05", + "sha256:b273fcbd7fc64dc3600c098e39136522650c49bca95df2d11cf3b626422392c8", + "sha256:b5870b50c9db823c595983571d1296a6ff3e1b88f734a4c8f6fc6188397de005", + "sha256:b74a0e59ec5d15127acdabd75ea17726ac4c5178ae51b85bfe39c4f8a278e879", + "sha256:be71c93a63d738597996be9528f4abe628d1adf5e6eb11607bc8fe1a510b5dae", + "sha256:c22a8bf253bacc0cf11f35ad9808b6cb75ada2631c2d97c971122583b129afbc", + "sha256:c4665508bcbac83a31ff8ab08f424b665200c0e1e645d2bd9ab3d3e557b6185b", + "sha256:c5f3ffd1e098dfc032d4d3af5c0ac64f6d286d98bc148698356847b80fa4de1b", + "sha256:cebc6fe843e0733ee827a282aca4999b596241195f43b4cc371d64fc6639da9e", + "sha256:d1381caf13ab9f300e30dd8feadb3de072aeb86f1d34a8569453ff32a7dea4bf", + "sha256:d7d86942e56ded512a594786a5ba0a5e521d02529b3826e7761a05138341a2ac", + "sha256:e31d432427dcbf4d86958c184b9bfd1e96b5b71f8eb17e6d02531f434fd335b8", + "sha256:e95b1af3c5b07d9e643909b5abbec77cd9f1217e6d0bca72b0234736b9fb1f1b", + "sha256:f85209946d1fe94416debbb88d00eb92ce9cd5266775424ff81bc959e001acaf", + "sha256:feb0dacc61170ed7ab602d3d972a58f14ee3ee60494292d384649a3dc38ef463", + "sha256:ff72b71b5d10d22ecb084d345fc26f42b5143c5533db5e2eaba7d2d335358876" + ], + "markers": "python_version >= '3.8'", + "version": "==2.3.0" + }, "twine": { "hashes": [ "sha256:418ebf08ccda9a8caaebe414433b0ba5e25eb5e4a927667122fbe8f829f985d8", From 58e9e6a3ae076648f55d631eb6370e74e5179c8d Mon Sep 17 00:00:00 2001 From: Ivan-Wang-tech <157322972+Ivan-Wang-tech@users.noreply.github.com> Date: Tue, 4 Nov 2025 01:38:13 -0500 Subject: [PATCH 38/42] add test file --- Makefile | 4 ++-- Pipfile | 1 + Pipfile.lock | 6 +++++- 3 files changed, 8 insertions(+), 3 deletions(-) diff --git a/Makefile b/Makefile index 18f5993..620044d 100644 --- a/Makefile +++ b/Makefile @@ -59,8 +59,8 @@ build: clean pipenv run python -m build install-wheel: - @echo "Installing wheel: $(WHEEL)" - pipenv install $(WHEEL) + @echo "Installing wheel (no Pipfile change): $(WHEEL)" + pipenv run pip install "$(WHEEL)" uninstall: @echo "Uninstalling $(PACKAGE)..." diff --git a/Pipfile b/Pipfile index 735bc73..9516a12 100644 --- a/Pipfile +++ b/Pipfile @@ -7,6 +7,7 @@ name = "pypi" pytest = "*" build = "*" twine = "*" +eatnyc = {file = ".", editable = true} [dev-packages] build = "*" diff --git a/Pipfile.lock b/Pipfile.lock index 75d4449..6e7d93d 100644 --- a/Pipfile.lock +++ b/Pipfile.lock @@ -1,7 +1,7 @@ { "_meta": { "hash": { - "sha256": "eae26392b57d731ef73e3f35a05d51b2be194dea5b27a243d863608a65bddda2" + "sha256": "29c4afc959a70193dda32588e149b4b1ce563573f10f0124aaa448f8ff97986e" }, "pipfile-spec": 6, "requires": {}, @@ -166,6 +166,10 @@ "markers": "python_version >= '3.9'", "version": "==0.22.2" }, + "eatnyc": { + "editable": true, + "file": "." + }, "exceptiongroup": { "hashes": [ "sha256:4d111e6e0c13d0644cad6ddaa7ed0261a0b36971f6d23e7ec9b4b9097da78a10", From 1185dff1c2dfeaf1d74c1e292cd5eb4110d55735 Mon Sep 17 00:00:00 2001 From: lilyluo7412 Date: Tue, 4 Nov 2025 23:13:37 -0500 Subject: [PATCH 39/42] Add PyPI production publish step and expand README docs Added a production PyPI publish step to the GitHub Actions workflow for automated releases. Expanded the README with detailed function documentation, updated the install example to use version 0.1.1, and provided usage examples for all major functions. --- .github/workflows/build.yaml | 5 ++ README.md | 96 +++++++++++++++++++++++++++++++++++- 2 files changed, 99 insertions(+), 2 deletions(-) diff --git a/.github/workflows/build.yaml b/.github/workflows/build.yaml index 7567c10..2774f0f 100644 --- a/.github/workflows/build.yaml +++ b/.github/workflows/build.yaml @@ -67,6 +67,11 @@ jobs: repository-url: https://test.pypi.org/legacy/ password: ${{ secrets.TEST_PYPI_API_TOKEN }} + - name: Publish to PyPI (production) + uses: pypa/gh-action-pypi-publish@release/v1 + with: + password: ${{ secrets.PYPI_API_TOKEN }} + #ADDED to verify upload # verify-upload: diff --git a/README.md b/README.md index e35bdd6..8114fb3 100644 --- a/README.md +++ b/README.md @@ -20,9 +20,9 @@ pipenv --python 3.11 pipenv shell ``` 2. **Install from TestPyPI** -Replace 0.1.0 with your latest version number (see pyproject.toml) +Replace 0.1.1 with your latest version number (see pyproject.toml) ```bash -pipenv install -i https://test.pypi.org/simple/ --extra-index-url https://pypi.org/simple eatnyc==0.1.0 +pipenv install -i https://test.pypi.org/simple/ --extra-index-url https://pypi.org/simple eatnyc==0.1.1 ``` For now: The --extra-index-url flag ensures dependencies are installed from the real PyPI, while your package is pulled from TestPyPI @@ -67,6 +67,98 @@ Run the example: pipenv run python examples/demo.py ``` +## Function Documentation + +### `load_data(path=None, validate=True)` +Loads NYC restaurant data from CSV file. + +**Parameters:** +- `path` (str, optional): Path to CSV file. If `None`, loads bundled data file. +- `validate` (bool, default=True): Whether to validate required columns. + +**Returns:** +- `list[dict]`: List of restaurant dictionaries with normalized fields (lowercase keys, float ratings). + +**Example:** +```python +data = load_data() # Load bundled data +data = load_data("custom.csv") # Load custom file +``` + +### `filter_restaurants(data, cuisine=None, neighborhood=None, price=None, min_rating=None, limit=None)` +Filters restaurants based on multiple criteria. + +**Parameters:** +- `data` (list): List of restaurant dictionaries (from `load_data()`). +- `cuisine` (str, optional): Filter by cuisine type. +- `neighborhood` (str, optional): Filter by neighborhood. +- `price` (str, optional): Filter by price range (e.g., "$", "$$", "$$$"). +- `min_rating` (float, optional): Minimum rating threshold. +- `limit` (int, optional): Maximum number of results to return. + +**Returns:** +- `list[dict]`: Filtered list of restaurant dictionaries. + +**Example:** +```python +italian = filter_restaurants(data, cuisine="Italian", min_rating=4.5, limit=10) +``` + +### `top_n(data, n=5, sort_by="rating", descending=True)` +Returns top N restaurants sorted by a specified field. + +**Parameters:** +- `data` (list): List of restaurant dictionaries. +- `n` (int, default=5): Number of results to return. Use `None` for all results. +- `sort_by` (str, default="rating"): Field to sort by (e.g., "rating", "name", "price"). +- `descending` (bool, default=True): Whether to sort in descending order. + +**Returns:** +- `list[dict]`: Top N restaurants sorted by the specified field. + +**Example:** +```python +top_5 = top_n(data, n=5, sort_by="rating", descending=True) +top_10_by_price = top_n(data, n=10, sort_by="price", descending=False) +``` + +### `sample_dish(cuisine=None, seed=None)` +Returns a random restaurant with a sample dish recommendation. + +**Parameters:** +- `cuisine` (str, optional): Filter by cuisine type. If `None`, returns any restaurant. +- `seed` (int, optional): Random seed for reproducible results. + +**Returns:** +- `dict` or `None`: Restaurant dictionary with sample dish, or `None` if no matches. +- If cuisine not found, returns dict with `error`, `suggestions`, and `message` keys. + +**Example:** +```python +random_dish = sample_dish() # Any restaurant +italian_dish = sample_dish(cuisine="Italian", seed=42) # Reproducible +``` + +### `format_card(row, style="ascii", width=60, show_dish=True)` +Formats a restaurant dictionary as a display card. + +**Parameters:** +- `row` (dict): Restaurant dictionary (from `load_data()` or filter functions). +- `style` (str, default="ascii"): Display style - "ascii" for box format, "markdown" for markdown. +- `width` (int, default=60): Card width in characters (minimum 24). +- `show_dish` (bool, default=True): Whether to include sample dish in output. + +**Returns:** +- `str`: Formatted card string. + +**Example:** +```python +card = format_card(restaurant, style="ascii", width=48) +print(card) +``` + +**See the complete example program:** [examples/demo.py](examples/demo.py) - demonstrates all functions working together. + ## How to Run Unit Tests Simple unit tests are included in the 'tests' directory. To run them: 1. **Install 'pytest' inside your virtual environment:** From 87fe938fc5d9d948a0180f244cca573847a665ae Mon Sep 17 00:00:00 2001 From: lilyluo7412 Date: Tue, 4 Nov 2025 23:32:45 -0500 Subject: [PATCH 40/42] Update pyproject.toml --- pyproject.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pyproject.toml b/pyproject.toml index b205e50..e38bc84 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta" [project] name = "eatnyc" -version = "0.1.1" +version = "0.1.2" description = "A tiny NYC restaurant recommender near NYU." readme = "README.md" requires-python = ">=3.10" From c6ec4da846ee9a485690fe5243b37d34cb83d8ee Mon Sep 17 00:00:00 2001 From: lilyluo7412 Date: Wed, 5 Nov 2025 00:13:46 -0500 Subject: [PATCH 41/42] Update README.md --- README.md | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index 8114fb3..ec190e7 100644 --- a/README.md +++ b/README.md @@ -20,9 +20,9 @@ pipenv --python 3.11 pipenv shell ``` 2. **Install from TestPyPI** -Replace 0.1.1 with your latest version number (see pyproject.toml) +Replace 0.1.2 with your latest version number (see pyproject.toml) ```bash -pipenv install -i https://test.pypi.org/simple/ --extra-index-url https://pypi.org/simple eatnyc==0.1.1 +pipenv install -i https://test.pypi.org/simple/ --extra-index-url https://pypi.org/simple eatnyc==0.1.2 ``` For now: The --extra-index-url flag ensures dependencies are installed from the real PyPI, while your package is pulled from TestPyPI @@ -175,6 +175,11 @@ python3 -m pytest ```bash pip install eatnyc ``` + +Or install a specific version: +```bash +pip install eatnyc==0.1.2 +``` ### Install locally (for developers) ```bash pipenv install -e . From ba071870b74d0f5c0ce99e8bad410216b8d0388b Mon Sep 17 00:00:00 2001 From: amiraadum Date: Wed, 5 Nov 2025 10:32:37 -0500 Subject: [PATCH 42/42] docs: polish README; ci: refine build workflow; dev: update Pipfile/lock --- .github/workflows/build.yaml | 51 +++++++---- .gitignore | 7 ++ Pipfile | 1 + Pipfile.lock | 54 ++---------- README.md | 164 ++++++++++++++++++++++------------- examples/demo.py | 1 + pyproject.toml | 9 +- 7 files changed, 157 insertions(+), 130 deletions(-) diff --git a/.github/workflows/build.yaml b/.github/workflows/build.yaml index 2774f0f..caf6143 100644 --- a/.github/workflows/build.yaml +++ b/.github/workflows/build.yaml @@ -18,20 +18,17 @@ jobs: steps: - uses: actions/checkout@v4 - - name: Install Python, pipenv and Pipfile packages + - name: Install Python, Pipenv and Pipfile packages uses: kojoru/prepare-pipenv@v1 with: python-version: ${{ matrix.python-version }} #installing requirements.txt - - name: Install requirements.txt + test deps + - name: Install project (editable) + test/build tools run: | if [ -f requirements.txt ]; then pipenv run pip install -r requirements.txt; fi - - - name: Turn on 'editable' mode - run: | pipenv install -e . - pipenv install pytest build + pipenv install pytest build twine - name: Test with pytest run: | @@ -44,8 +41,18 @@ jobs: rm -rf dist build src/*.egg-info pipenv run python -m build . + - name: Twine check (README/metadata) + run: pipenv run twine check dist/* + + - name: Upload dist artifacts + uses: actions/upload-artifact@v4 + with: + name: dist-${{ matrix.python-version }} + path: dist/* + deliver: + name: Publish if: github.event_name != 'pull_request' && startsWith(github.ref, 'refs/tags/v') needs: [build] runs-on: ubuntu-latest @@ -53,6 +60,12 @@ jobs: steps: - uses: actions/checkout@v4 + - name: Download artifacts + uses: actions/download-artifact@v4 + with: + name: dist-3.11 + path: dist + - name: Install Python, pipenv and Pipfile packages uses: kojoru/prepare-pipenv@v1 @@ -74,14 +87,18 @@ jobs: #ADDED to verify upload - # verify-upload: - # if: github.event_name != 'pull_request' - # needs: [deliver] - # runs-on: ubuntu-latest - # timeout-minutes: 5 - # steps: - # - name: Install from TestPyPI and import - # run: | - # python -m pip install --upgrade pip - # pip install --index-url https://test.pypi.org/simple/ --extra-index-url https://pypi.org/simple eatnyc - # python - <<'PY' import eatnyc print("Imported eatnyc OK. Version:", getattr(eatnyc, "__version__", "unknown")) PY \ No newline at end of file + verify-upload: + name: Verify install from registry + if: github.event_name != 'pull_request' + needs: [deliver] + runs-on: ubuntu-latest + timeout-minutes: 5 + steps: + - name: Verify Install + run: | + python -m pip install --upgrade pip + pip install eatnyc + python - <<'PY' + import eatnyc + print("✅ Imported eatnyc OK. Version:", getattr(eatnyc, "__version__", "unknown")) + PY \ No newline at end of file diff --git a/.gitignore b/.gitignore index 2f24a10..3c1268a 100644 --- a/.gitignore +++ b/.gitignore @@ -147,3 +147,10 @@ dmypy.json # Cython debug symbols cython_debug/ +dist/ +build/ +*.egg-info/ + +dist/ +build/ +*.egg-info/ diff --git a/Pipfile b/Pipfile index 9516a12..ddecb93 100644 --- a/Pipfile +++ b/Pipfile @@ -14,3 +14,4 @@ build = "*" twine = "*" pytest = "*" exceptiongroup = "*" +eatnyc = {file = ".", editable = true} diff --git a/Pipfile.lock b/Pipfile.lock index 6e7d93d..97f8efa 100644 --- a/Pipfile.lock +++ b/Pipfile.lock @@ -1,7 +1,7 @@ { "_meta": { "hash": { - "sha256": "29c4afc959a70193dda32588e149b4b1ce563573f10f0124aaa448f8ff97986e" + "sha256": "35ae6d1e238ec73daefd18987bd5f432accca14788aa0767d33c764802e0ddef" }, "pipfile-spec": 6, "requires": {}, @@ -614,6 +614,10 @@ "markers": "python_version >= '3.9'", "version": "==0.22.2" }, + "eatnyc": { + "editable": true, + "markers": "python_version >= '3.10'" + }, "exceptiongroup": { "hashes": [ "sha256:4d111e6e0c13d0644cad6ddaa7ed0261a0b36971f6d23e7ec9b4b9097da78a10", @@ -823,54 +827,6 @@ "markers": "python_full_version >= '3.8.0'", "version": "==14.2.0" }, - "tomli": { - "hashes": [ - "sha256:00b5f5d95bbfc7d12f91ad8c593a1659b6387b43f054104cda404be6bda62456", - "sha256:0a154a9ae14bfcf5d8917a59b51ffd5a3ac1fd149b71b47a3a104ca4edcfa845", - "sha256:0c95ca56fbe89e065c6ead5b593ee64b84a26fca063b5d71a1122bf26e533999", - "sha256:0eea8cc5c5e9f89c9b90c4896a8deefc74f518db5927d0e0e8d4a80953d774d0", - "sha256:1cb4ed918939151a03f33d4242ccd0aa5f11b3547d0cf30f7c74a408a5b99878", - "sha256:4021923f97266babc6ccab9f5068642a0095faa0a51a246a6a02fccbb3514eaf", - "sha256:4c2ef0244c75aba9355561272009d934953817c49f47d768070c3c94355c2aa3", - "sha256:4dc4ce8483a5d429ab602f111a93a6ab1ed425eae3122032db7e9acf449451be", - "sha256:4f195fe57ecceac95a66a75ac24d9d5fbc98ef0962e09b2eddec5d39375aae52", - "sha256:5192f562738228945d7b13d4930baffda67b69425a7f0da96d360b0a3888136b", - "sha256:5e01decd096b1530d97d5d85cb4dff4af2d8347bd35686654a004f8dea20fc67", - "sha256:64be704a875d2a59753d80ee8a533c3fe183e3f06807ff7dc2232938ccb01549", - "sha256:70a251f8d4ba2d9ac2542eecf008b3c8a9fc5c3f9f02c56a9d7952612be2fdba", - "sha256:73ee0b47d4dad1c5e996e3cd33b8a76a50167ae5f96a2607cbe8cc773506ab22", - "sha256:74bf8464ff93e413514fefd2be591c3b0b23231a77f901db1eb30d6f712fc42c", - "sha256:792262b94d5d0a466afb5bc63c7daa9d75520110971ee269152083270998316f", - "sha256:7b0882799624980785240ab732537fcfc372601015c00f7fc367c55308c186f6", - "sha256:883b1c0d6398a6a9d29b508c331fa56adbcdff647f6ace4dfca0f50e90dfd0ba", - "sha256:88bd15eb972f3664f5ed4b57c1634a97153b4bac4479dcb6a495f41921eb7f45", - "sha256:8a35dd0e643bb2610f156cca8db95d213a90015c11fee76c946aa62b7ae7e02f", - "sha256:940d56ee0410fa17ee1f12b817b37a4d4e4dc4d27340863cc67236c74f582e77", - "sha256:97d5eec30149fd3294270e889b4234023f2c69747e555a27bd708828353ab606", - "sha256:a0e285d2649b78c0d9027570d4da3425bdb49830a6156121360b3f8511ea3441", - "sha256:a1f7f282fe248311650081faafa5f4732bdbfef5d45fe3f2e702fbc6f2d496e0", - "sha256:a4ea38c40145a357d513bffad0ed869f13c1773716cf71ccaa83b0fa0cc4e42f", - "sha256:a56212bdcce682e56b0aaf79e869ba5d15a6163f88d5451cbde388d48b13f530", - "sha256:ad805ea85eda330dbad64c7ea7a4556259665bdf9d2672f5dccc740eb9d3ca05", - "sha256:b273fcbd7fc64dc3600c098e39136522650c49bca95df2d11cf3b626422392c8", - "sha256:b5870b50c9db823c595983571d1296a6ff3e1b88f734a4c8f6fc6188397de005", - "sha256:b74a0e59ec5d15127acdabd75ea17726ac4c5178ae51b85bfe39c4f8a278e879", - "sha256:be71c93a63d738597996be9528f4abe628d1adf5e6eb11607bc8fe1a510b5dae", - "sha256:c22a8bf253bacc0cf11f35ad9808b6cb75ada2631c2d97c971122583b129afbc", - "sha256:c4665508bcbac83a31ff8ab08f424b665200c0e1e645d2bd9ab3d3e557b6185b", - "sha256:c5f3ffd1e098dfc032d4d3af5c0ac64f6d286d98bc148698356847b80fa4de1b", - "sha256:cebc6fe843e0733ee827a282aca4999b596241195f43b4cc371d64fc6639da9e", - "sha256:d1381caf13ab9f300e30dd8feadb3de072aeb86f1d34a8569453ff32a7dea4bf", - "sha256:d7d86942e56ded512a594786a5ba0a5e521d02529b3826e7761a05138341a2ac", - "sha256:e31d432427dcbf4d86958c184b9bfd1e96b5b71f8eb17e6d02531f434fd335b8", - "sha256:e95b1af3c5b07d9e643909b5abbec77cd9f1217e6d0bca72b0234736b9fb1f1b", - "sha256:f85209946d1fe94416debbb88d00eb92ce9cd5266775424ff81bc959e001acaf", - "sha256:feb0dacc61170ed7ab602d3d972a58f14ee3ee60494292d384649a3dc38ef463", - "sha256:ff72b71b5d10d22ecb084d345fc26f42b5143c5533db5e2eaba7d2d335358876" - ], - "markers": "python_version >= '3.8'", - "version": "==2.3.0" - }, "twine": { "hashes": [ "sha256:418ebf08ccda9a8caaebe414433b0ba5e25eb5e4a927667122fbe8f829f985d8", diff --git a/README.md b/README.md index ec190e7..f566830 100644 --- a/README.md +++ b/README.md @@ -1,7 +1,3 @@ -# Python Package Exercise - -An exercise to create a Python package, build it, test it, distribute it, and use it. See [instructions](./instructions.md) for details. - # eatnyc - NYC Restaurant Recommender ![Build and Test](https://github.com/swe-students-fall2025/3-python-package-team_avalon/actions/workflows/build.yaml/badge.svg) @@ -11,15 +7,32 @@ It’s designed to help users explore the city’s dining scene and discover gre --- ## How to install and use this package -### Option 1: Try it from **TestPyPI** (current test version) -You can try out the latest build of eatnyc from the [TestPyPI](https://test.pypi.org/project/eatnyc/) repository. - 1. **Create and Activate a virtual environment** ```bash pipenv --python 3.11 pipenv shell ``` -2. **Install from TestPyPI** +2. ### Option 1: Install from PyPI (for users) +```bash +pip install eatnyc +``` + +Or install a specific version: +```bash +pip install eatnyc==0.1.2 +``` +### Install locally (for developers) +```bash +pipenv install -e . +``` +If that set up fails for you, use: +```bash +python3 -m pipenv install -e . +``` + +### Option 2: Try it from **TestPyPI** (current test version) +You can try out the latest build of eatnyc from the [TestPyPI](https://test.pypi.org/project/eatnyc/) repository. +**Install from TestPyPI** Replace 0.1.2 with your latest version number (see pyproject.toml) ```bash pipenv install -i https://test.pypi.org/simple/ --extra-index-url https://pypi.org/simple eatnyc==0.1.2 @@ -40,27 +53,66 @@ python -m eatnyc --- ### Example Program ```python -from eatnyc import load_data, filter_restaurants, top_n, sample_dish, format_card - -data = load_data() - -# Filter restaurants by cuisine and neighborhood -italian_manhattan = filter_restaurants( - data, - cuisine="Italian", - neighborhood="Manhattan", - min_rating=4.0 -) - -# Get top 5 restaurants by rating -best = top_n(data, n=5, sort_by="rating") - -# Show a sample dish recommendation -print(sample_dish(cuisine="Japanese")) - -# Print formatted cards -for r in best: - print(format_card(r, style="ascii", width=48)) +from eatnyc import load_data, top_n, filter_restaurants, sample_dish, format_card + + +def main(): + # === Load Data === + data = load_data() + print("rows:", len(data)) + + # === Top restaurants by rating === + print("\n=== Top 5 by rating (simple print) ===") + for r in top_n(data, n=5): + print(f"- {r['name']} ({r['cuisine']}, {r['price']}) ★ {r['rating']} – {r['sample_dish']}") + + # === Top restaurants by rating with format_card === + print("\n=== Top 5 by rating (format_card output) ===") + for r in top_n(data, n=5): + print(format_card(r, width=60)) + + # === Filtering example === + print("\n=== Korean restaurants in Koreatown with $$ and rating >= 4.5 (simple print) ===") + filtered = filter_restaurants( + data, + cuisine="Korean", + neighborhood="Koreatown", + price="$$", + min_rating=4.5, + ) + for r in filtered: + print(f"- {r['name']} ({r['cuisine']}, {r['price']}) ★ {r['rating']} – {r['sample_dish']}") + + # === Same filter + card display === + print("\n=== Same filtered results (format_card output) ===") + for r in filtered: + print(format_card(r, width=60)) + + # === Sample dish recommendations === + print("\n=== Sample dish: Random Italian restaurant ===") + italian = sample_dish(cuisine="Italian", seed=42) + if isinstance(italian, dict) and "error" not in italian: + print(f"Try: {italian['sample_dish']} at {italian['name']}") + print(format_card(italian, width=60)) + else: + print(f"{italian['error']}") + print(f"{italian['message']} {', '.join(italian['suggestions'])}") + + print("\n=== Sample dish: Random restaurant (any cuisine) ===") + random_restaurant = sample_dish(seed=123) + if random_restaurant: + print(f"Try: {random_restaurant['sample_dish']} at {random_restaurant['name']}") + print(format_card(random_restaurant, width=60)) + + print("\n=== Sample dish: Invalid cuisine (should show suggestions) ===") + invalid = sample_dish(cuisine="Martian") + if isinstance(invalid, dict) and "error" in invalid: + print(f"{invalid['error']}") + print(f"{invalid['message']} {', '.join(invalid['suggestions'][:3])}") + + +if __name__ == "__main__": + main() ``` Run the example: ```bash @@ -167,66 +219,54 @@ pipenv install pytest ``` 2. **Run the tests from project root:** ```bash -python3 -m pytest +pipenv run pytest -q ``` 3. All tests should pass. Any failed test indicates that the package code is behaving differently from the expected results. -### Option 2: Install from PyPI (for users) -```bash -pip install eatnyc -``` - -Or install a specific version: -```bash -pip install eatnyc==0.1.2 -``` -### Install locally (for developers) -```bash -pipenv install -e . -``` -If that set up fails for you, use: -```bash -python3 -m pipenv install -e . -``` - -## Developer Mode Switch (using Makefile) - -```bash -make dev-on # install editable -make dev-off # restore TestPyPI version -make verify # confirm path -``` - # Developer Workflow (Building & Publishing) If you modify the code and want to publish a new version to TestPyPI, follow these steps: ```bash #1. CLEAN old build artifacts rm -rf dist build src/*.egg-info -pipenv install build +pipenv install build twine #2. BUMP version number in pyproject.toml (e.g., 0.1.0 → 0.1.1) #3. BUILD the package pipenv run python -m build +pipenv run twine check dist/* + +#4. PUBLISH to PyPI +pipenv run twine upload dist/* -#4. UPLOAD new version to TestPyPI -pipenv install twine +#(Optional) UPLOAD new version to TestPyPI pipenv run twine upload -r testpypi dist/* ``` 5. REINSTALL to test it: ```bash pipenv run pip uninstall -y eatnyc -pipenv install -i https://test.pypi.org/simple/ --extra-index-url https://pypi.org/simple eatnyc==0.1.1 +pipenv install -i https://test.pypi.org/simple/ --extra-index-url https://pypi.org/simple eatnyc==0.1.2 ``` -6. Once final, UPLOAD to real PyPI (final version) with: +6. Verify installation with: ```bash -pipenv run twine upload dist/* +pip install eatnyc +python -c "import eatnyc; print(eatnyc.__version__)" ``` +## Developer Mode Switch (using Makefile) + +```bash +make dev-on # install editable +make dev-off # restore TestPyPI version +make verify # confirm path +``` + --- ## Project Links - **PyPI:** [https://pypi.org/project/eatnyc](https://pypi.org/project/eatnyc) - **TestPyPI:** [https://test.pypi.org/project/eatnyc](https://test.pypi.org/project/eatnyc) - **Github Repo:** [https://github.com/swe-students-fall2025/3-python-package-team_avalon.git](https://github.com/swe-students-fall2025/3-python-package-team_avalon.git) +- **Source:** [https://github.com/swe-students-fall2025/3-python-package-team_avalon](https://github.com/swe-students-fall2025/3-python-package-team_avalon) +- **Issues:** [https://github.com/swe-students-fall2025/3-python-package-team_avalon/issues](https://github.com/swe-students-fall2025/3-python-package-team_avalon/issues) # Contributors - [amiraadum](https://github.com/amiraadum) diff --git a/examples/demo.py b/examples/demo.py index 3014da5..116f480 100644 --- a/examples/demo.py +++ b/examples/demo.py @@ -2,6 +2,7 @@ def main(): + # === Load Data === data = load_data() print("rows:", len(data)) diff --git a/pyproject.toml b/pyproject.toml index e38bc84..47d87cc 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -6,23 +6,28 @@ build-backend = "setuptools.build_meta" name = "eatnyc" version = "0.1.2" description = "A tiny NYC restaurant recommender near NYU." -readme = "README.md" +readme = { file = "README.md", content-type = "text/markdown" } # explicit requires-python = ">=3.10" license = { text = "MIT" } authors = [{ name = "Team Avalon" }] keywords = ["nyc", "restaurants", "fun", "food", "recommendations"] classifiers = [ "Programming Language :: Python :: 3", + "Programming Language :: Python :: 3.10", + "Programming Language :: Python :: 3.11", "License :: OSI Approved :: MIT License", - "Operating System :: OS Independent" + "Operating System :: OS Independent", + "Topic :: Utilities" ] [project.urls] Homepage = "https://github.com/swe-students-fall2025/3-python-package-team_avalon" +Source = "https://github.com/swe-students-fall2025/3-python-package-team_avalon" Issues = "https://github.com/swe-students-fall2025/3-python-package-team_avalon/issues" [tool.setuptools] package-dir = {"" = "src"} +include-package-data = true [tool.setuptools.packages.find] where = ["src"]