harvestly 0.1.0__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,51 @@
1
+ # Build artifacts
2
+ node_modules/
3
+ .astro/
4
+ dist/
5
+ .cache/
6
+ __pycache__/
7
+ .env
8
+ .env.local
9
+ .session-active
10
+
11
+ # Data — raw + canonical, regenerated by ingest pipeline
12
+ data/raw/
13
+ data/canonical/
14
+ research/_phase0_data/
15
+
16
+ # Memory: do not commit transient session markers
17
+ .memory/.session-active
18
+
19
+ # OS
20
+ .DS_Store
21
+ *.log
22
+
23
+ # Operator-made banner reformats (D-1 image resized to Reddit/YouTube slots — not source)
24
+ launch/assets/D-1_*
25
+
26
+ # Wrangler build temp
27
+ site/.wrangler/
28
+ data/farms_canonical/
29
+ data/csa_canonical/
30
+ reports/.cache/
31
+
32
+ # Third-party licensed content — never commit/distribute (IBISWorld license)
33
+ OD6482*.pdf
34
+
35
+ # NTFS download metadata (WSL)
36
+ *:Zone.Identifier
37
+
38
+ # Data-viz tooling venv (uv-managed, machine-specific — never commit)
39
+ launch/assets/dataviz/.venv/
40
+ # Third-party academic source PDFs (copyright; CSA substrate papers) — keep local only
41
+ Benefits_of_Nutrition_Educatio.pdf
42
+ Perceived_sustainability_impac.pdf
43
+ Sustainability_as_business_str.pdf
44
+ data/*-bak-pa/
45
+ data/*-bak-sweep/
46
+ *.bak2
47
+
48
+ # Wrangler build temp (project root)
49
+ .wrangler/
50
+ # Editor/script backups
51
+ *.bak
@@ -0,0 +1,26 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Harvestly Markets (harvestlymarkets.com)
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE IN OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
22
+
23
+ ---
24
+
25
+ Note: the DATASET this client fetches is licensed separately under CC BY 4.0
26
+ by Harvestly Markets — https://creativecommons.org/licenses/by/4.0/
@@ -0,0 +1,83 @@
1
+ Metadata-Version: 2.4
2
+ Name: harvestly
3
+ Version: 0.1.0
4
+ Summary: US farmers-market statistics: official Python client for the Harvestly Markets open dataset (CC BY 4.0, USDA-derived)
5
+ Project-URL: Homepage, https://harvestlymarkets.com
6
+ Project-URL: Documentation, https://harvestlymarkets.com/data-sources/
7
+ Project-URL: Dataset & methodology, https://harvestlymarkets.com/about/methodology/
8
+ Project-URL: State statistics, https://harvestlymarkets.com/farmers-market-statistics/
9
+ Author-email: Quinn Vandt <quinn@harvestlymarkets.com>
10
+ License-Expression: MIT
11
+ License-File: LICENSE
12
+ Keywords: csa,dataset,farm-stands,farmers-markets,food,local-food,open-data,statistics,usda
13
+ Classifier: Development Status :: 4 - Beta
14
+ Classifier: Intended Audience :: Developers
15
+ Classifier: Intended Audience :: Science/Research
16
+ Classifier: Operating System :: OS Independent
17
+ Classifier: Programming Language :: Python :: 3
18
+ Classifier: Topic :: Scientific/Engineering :: Information Analysis
19
+ Requires-Python: >=3.9
20
+ Description-Content-Type: text/markdown
21
+
22
+ # harvestly
23
+
24
+ Official Python client for the [Harvestly Markets](https://harvestlymarkets.com) open dataset:
25
+ state-by-state statistics on **8,800+ US farmers markets**, ~2,000 CSA programs, and ~7,800 farm
26
+ stands — cleaned, deduplicated, and enriched from the [USDA AMS Local Food
27
+ Portal](https://www.usdalocalfoodportal.com/).
28
+
29
+ - **Data license:** [CC BY 4.0](https://creativecommons.org/licenses/by/4.0/) (this client: MIT)
30
+ - **Zero dependencies** — Python ≥ 3.9 standard library only
31
+ - Data + methodology: [harvestlymarkets.com/data-sources](https://harvestlymarkets.com/data-sources/)
32
+ - Human-readable reports: [farmers-market statistics by state](https://harvestlymarkets.com/farmers-market-statistics/)
33
+
34
+ ## Install
35
+
36
+ ```bash
37
+ pip install harvestly
38
+ ```
39
+
40
+ ## Python API
41
+
42
+ ```python
43
+ import harvestly
44
+
45
+ ds = harvestly.load() # fetches once, then ~24h local cache
46
+
47
+ ds.national["farmers_markets"] # 8863
48
+ ds.state("vt") # {'state_name': 'Vermont', 'farmers_markets': ..., ...}
49
+ ds.state("California") # abbreviation or full name
50
+
51
+ ds.top("website_pct", 5) # best online presence
52
+ ds.top("farmers_markets", 5, ascending=True) # fewest markets
53
+
54
+ harvestly.state_url("ny") # browse live listings for a state
55
+ harvestly.zip_url("12180") # market finder for a ZIP
56
+ ```
57
+
58
+ Each state row: `farmers_markets`, `csa_programs`, `farm_stands`, `snap_ebt_markets`,
59
+ `snap_ebt_pct`, `organic_vendor_markets`, `organic_pct`, `markets_with_website`, `website_pct`.
60
+
61
+ ## CLI
62
+
63
+ ```bash
64
+ harvestly national
65
+ harvestly state ca
66
+ harvestly top snap_ebt_pct -n 10
67
+ harvestly state vt --json # machine-readable
68
+ ```
69
+
70
+ ## Data notes
71
+
72
+ The dataset is rebuilt from the USDA substrate with documented cleaning: deduplication
73
+ (the raw feed double-counts ~270 records), website/season/SNAP enrichment from live state
74
+ sources, and per-state aggregation. Full methodology:
75
+ [harvestlymarkets.com/about/methodology](https://harvestlymarkets.com/about/methodology/).
76
+ Archived copies: [Zenodo (DOI 10.5281/zenodo.21247900)](https://doi.org/10.5281/zenodo.21247900),
77
+ Hugging Face, Kaggle, GitHub.
78
+
79
+ **Attribution (CC BY 4.0):** Harvestly Markets (2026). *US Farmers Market Statistics.*
80
+ https://harvestlymarkets.com/farmers-market-statistics/
81
+
82
+ Found a data issue? Every listing has a correction flow at
83
+ [harvestlymarkets.com/submit](https://harvestlymarkets.com/submit/).
@@ -0,0 +1,62 @@
1
+ # harvestly
2
+
3
+ Official Python client for the [Harvestly Markets](https://harvestlymarkets.com) open dataset:
4
+ state-by-state statistics on **8,800+ US farmers markets**, ~2,000 CSA programs, and ~7,800 farm
5
+ stands — cleaned, deduplicated, and enriched from the [USDA AMS Local Food
6
+ Portal](https://www.usdalocalfoodportal.com/).
7
+
8
+ - **Data license:** [CC BY 4.0](https://creativecommons.org/licenses/by/4.0/) (this client: MIT)
9
+ - **Zero dependencies** — Python ≥ 3.9 standard library only
10
+ - Data + methodology: [harvestlymarkets.com/data-sources](https://harvestlymarkets.com/data-sources/)
11
+ - Human-readable reports: [farmers-market statistics by state](https://harvestlymarkets.com/farmers-market-statistics/)
12
+
13
+ ## Install
14
+
15
+ ```bash
16
+ pip install harvestly
17
+ ```
18
+
19
+ ## Python API
20
+
21
+ ```python
22
+ import harvestly
23
+
24
+ ds = harvestly.load() # fetches once, then ~24h local cache
25
+
26
+ ds.national["farmers_markets"] # 8863
27
+ ds.state("vt") # {'state_name': 'Vermont', 'farmers_markets': ..., ...}
28
+ ds.state("California") # abbreviation or full name
29
+
30
+ ds.top("website_pct", 5) # best online presence
31
+ ds.top("farmers_markets", 5, ascending=True) # fewest markets
32
+
33
+ harvestly.state_url("ny") # browse live listings for a state
34
+ harvestly.zip_url("12180") # market finder for a ZIP
35
+ ```
36
+
37
+ Each state row: `farmers_markets`, `csa_programs`, `farm_stands`, `snap_ebt_markets`,
38
+ `snap_ebt_pct`, `organic_vendor_markets`, `organic_pct`, `markets_with_website`, `website_pct`.
39
+
40
+ ## CLI
41
+
42
+ ```bash
43
+ harvestly national
44
+ harvestly state ca
45
+ harvestly top snap_ebt_pct -n 10
46
+ harvestly state vt --json # machine-readable
47
+ ```
48
+
49
+ ## Data notes
50
+
51
+ The dataset is rebuilt from the USDA substrate with documented cleaning: deduplication
52
+ (the raw feed double-counts ~270 records), website/season/SNAP enrichment from live state
53
+ sources, and per-state aggregation. Full methodology:
54
+ [harvestlymarkets.com/about/methodology](https://harvestlymarkets.com/about/methodology/).
55
+ Archived copies: [Zenodo (DOI 10.5281/zenodo.21247900)](https://doi.org/10.5281/zenodo.21247900),
56
+ Hugging Face, Kaggle, GitHub.
57
+
58
+ **Attribution (CC BY 4.0):** Harvestly Markets (2026). *US Farmers Market Statistics.*
59
+ https://harvestlymarkets.com/farmers-market-statistics/
60
+
61
+ Found a data issue? Every listing has a correction flow at
62
+ [harvestlymarkets.com/submit](https://harvestlymarkets.com/submit/).
@@ -0,0 +1,36 @@
1
+ [build-system]
2
+ requires = ["hatchling"]
3
+ build-backend = "hatchling.build"
4
+
5
+ [project]
6
+ name = "harvestly"
7
+ version = "0.1.0"
8
+ description = "US farmers-market statistics: official Python client for the Harvestly Markets open dataset (CC BY 4.0, USDA-derived)"
9
+ readme = "README.md"
10
+ license = "MIT"
11
+ requires-python = ">=3.9"
12
+ authors = [{ name = "Quinn Vandt", email = "quinn@harvestlymarkets.com" }]
13
+ keywords = [
14
+ "farmers-markets", "open-data", "usda", "local-food", "csa",
15
+ "farm-stands", "dataset", "statistics", "food",
16
+ ]
17
+ classifiers = [
18
+ "Development Status :: 4 - Beta",
19
+ "Intended Audience :: Science/Research",
20
+ "Intended Audience :: Developers",
21
+ "Programming Language :: Python :: 3",
22
+ "Topic :: Scientific/Engineering :: Information Analysis",
23
+ "Operating System :: OS Independent",
24
+ ]
25
+
26
+ [project.urls]
27
+ Homepage = "https://harvestlymarkets.com"
28
+ Documentation = "https://harvestlymarkets.com/data-sources/"
29
+ "Dataset & methodology" = "https://harvestlymarkets.com/about/methodology/"
30
+ "State statistics" = "https://harvestlymarkets.com/farmers-market-statistics/"
31
+
32
+ [project.scripts]
33
+ harvestly = "harvestly.cli:main"
34
+
35
+ [tool.hatch.build.targets.wheel]
36
+ packages = ["src/harvestly"]
@@ -0,0 +1,100 @@
1
+ """harvestly — official Python client for the Harvestly Markets open dataset.
2
+
3
+ State-by-state statistics on 8,800+ US farmers markets (plus CSA programs and
4
+ farm stands), cleaned and enriched from the USDA AMS Local Food Portal.
5
+ CC BY 4.0 — https://harvestlymarkets.com/data-sources/
6
+ """
7
+ from __future__ import annotations
8
+
9
+ import json
10
+ import os
11
+ import pathlib
12
+ import time
13
+ import urllib.request
14
+
15
+ __version__ = "0.1.0"
16
+
17
+ DATA_URL = "https://harvestlymarkets.com/data/us-farmers-markets-by-state-2026.json"
18
+ SITE = "https://harvestlymarkets.com"
19
+ CACHE_TTL_SECONDS = 24 * 3600
20
+ _USER_AGENT = f"harvestly-python/{__version__} (+{SITE}/data-sources/)"
21
+
22
+
23
+ class Dataset:
24
+ """The by-state statistics dataset: metadata, national totals, 53 state rows."""
25
+
26
+ def __init__(self, metadata: dict, national: dict, states: list):
27
+ self.metadata = metadata
28
+ self.national = national
29
+ self._states = states
30
+ self._by_key = {}
31
+ for row in states:
32
+ self._by_key[row["state_abbr"].lower()] = row
33
+ self._by_key[row["state_name"].lower()] = row
34
+
35
+ @classmethod
36
+ def from_dict(cls, d: dict) -> "Dataset":
37
+ return cls(d["metadata"], d["national"], d["states"])
38
+
39
+ def states(self) -> list:
40
+ return list(self._states)
41
+
42
+ def state(self, key: str) -> dict:
43
+ try:
44
+ return self._by_key[key.strip().lower()]
45
+ except KeyError:
46
+ raise KeyError(f"unknown state: {key!r} (use a USPS abbreviation or full name)")
47
+
48
+ def top(self, metric: str, n: int = 10, ascending: bool = False) -> list:
49
+ if not self._states or metric not in self._states[0]:
50
+ valid = ", ".join(k for k in (self._states[0] if self._states else {}))
51
+ raise ValueError(f"unknown metric: {metric!r} (valid: {valid})")
52
+ rows = [r for r in self._states if r.get(metric) is not None]
53
+ return sorted(rows, key=lambda r: r[metric], reverse=not ascending)[:n]
54
+
55
+
56
+ def state_url(abbr: str) -> str:
57
+ """Link to the live state hub (full market listings)."""
58
+ return f"{SITE}/states/{abbr.strip().lower()}/"
59
+
60
+
61
+ def zip_url(zip_code: str) -> str:
62
+ """Link to the live ZIP market finder."""
63
+ z = zip_code.strip()
64
+ if not (len(z) == 5 and z.isdigit()):
65
+ raise ValueError(f"zip_code must be 5 digits, got {zip_code!r}")
66
+ return f"{SITE}/zip/{z}/"
67
+
68
+
69
+ def _cache_path() -> pathlib.Path:
70
+ base = os.environ.get("HARVESTLY_CACHE_DIR") or os.path.join(
71
+ os.environ.get("XDG_CACHE_HOME", os.path.expanduser("~/.cache")), "harvestly"
72
+ )
73
+ return pathlib.Path(base) / "dataset.json"
74
+
75
+
76
+ def _fetch_remote() -> dict:
77
+ req = urllib.request.Request(DATA_URL, headers={"User-Agent": _USER_AGENT})
78
+ with urllib.request.urlopen(req, timeout=30) as resp:
79
+ return json.loads(resp.read().decode("utf-8"))
80
+
81
+
82
+ def load(refresh: bool = False, fetch=None) -> Dataset:
83
+ """Load the dataset, using a local cache (~24h TTL).
84
+
85
+ refresh=True bypasses the cache. `fetch` overrides the HTTP fetcher (testing).
86
+ """
87
+ path = _cache_path()
88
+ if not refresh and path.exists():
89
+ age = time.time() - path.stat().st_mtime
90
+ if age < CACHE_TTL_SECONDS:
91
+ try:
92
+ return Dataset.from_dict(json.loads(path.read_text()))
93
+ except (ValueError, KeyError):
94
+ pass # corrupt cache — refetch
95
+ d = (fetch or _fetch_remote)()
96
+ path.parent.mkdir(parents=True, exist_ok=True)
97
+ tmp = path.with_suffix(".tmp")
98
+ tmp.write_text(json.dumps(d))
99
+ tmp.replace(path)
100
+ return Dataset.from_dict(d)
@@ -0,0 +1,82 @@
1
+ """harvestly CLI — quick lookups from the Harvestly Markets open dataset."""
2
+ from __future__ import annotations
3
+
4
+ import argparse
5
+ import json
6
+ import sys
7
+
8
+ import harvestly
9
+
10
+
11
+ def _fmt_row(row: dict) -> str:
12
+ return (
13
+ f"{row['state_name']} ({row['state_abbr']})\n"
14
+ f" farmers markets : {row['farmers_markets']:,}\n"
15
+ f" CSA programs : {row['csa_programs']:,}\n"
16
+ f" farm stands : {row['farm_stands']:,}\n"
17
+ f" SNAP/EBT : {row['snap_ebt_markets']:,} ({row['snap_ebt_pct']}%)\n"
18
+ f" organic vendors : {row['organic_vendor_markets']:,} ({row['organic_pct']}%)\n"
19
+ f" with website : {row['markets_with_website']:,} ({row['website_pct']}%)\n"
20
+ f" browse listings : {harvestly.state_url(row['state_abbr'])}"
21
+ )
22
+
23
+
24
+ def main(argv=None, fetch=None) -> int:
25
+ common = argparse.ArgumentParser(add_help=False)
26
+ common.add_argument("--json", action="store_true", help="emit raw JSON")
27
+ common.add_argument("--refresh", action="store_true", help="bypass the 24h cache")
28
+ p = argparse.ArgumentParser(
29
+ prog="harvestly",
30
+ description="US farmers-market statistics (CC BY 4.0, harvestlymarkets.com)",
31
+ )
32
+ sub = p.add_subparsers(dest="cmd", required=True)
33
+ sp = sub.add_parser("state", help="one state's statistics", parents=[common])
34
+ sp.add_argument("name", help="USPS abbreviation or full state name")
35
+ sub.add_parser("national", help="national totals", parents=[common])
36
+ tp = sub.add_parser("top", help="rank states by a metric", parents=[common])
37
+ tp.add_argument("metric", help="e.g. farmers_markets, website_pct, snap_ebt_pct")
38
+ tp.add_argument("-n", type=int, default=10)
39
+ tp.add_argument("--ascending", action="store_true")
40
+ args = p.parse_args(argv)
41
+
42
+ ds = harvestly.load(refresh=args.refresh, fetch=fetch)
43
+
44
+ if args.cmd == "state":
45
+ try:
46
+ row = ds.state(args.name)
47
+ except KeyError as e:
48
+ print(e, file=sys.stderr)
49
+ return 2
50
+ print(json.dumps(row, indent=2) if args.json else _fmt_row(row))
51
+ elif args.cmd == "national":
52
+ if args.json:
53
+ print(json.dumps(ds.national, indent=2))
54
+ else:
55
+ n = ds.national
56
+ print(
57
+ f"US totals ({ds.metadata.get('temporal_coverage', '')}): "
58
+ f"{n['farmers_markets']:,} farmers markets · "
59
+ f"{n['csa_programs']:,} CSA programs · "
60
+ f"{n['farm_stands']:,} farm stands\n"
61
+ f"SNAP/EBT {n['snap_ebt_pct']}% · organic {n['organic_pct']}% · "
62
+ f"with website {n['website_pct']}%\n"
63
+ f"source: {ds.metadata.get('url', harvestly.SITE)}"
64
+ )
65
+ elif args.cmd == "top":
66
+ try:
67
+ rows = ds.top(args.metric, args.n, ascending=args.ascending)
68
+ except ValueError as e:
69
+ print(e, file=sys.stderr)
70
+ return 2
71
+ if args.json:
72
+ print(json.dumps(rows, indent=2))
73
+ else:
74
+ for i, r in enumerate(rows, 1):
75
+ print(f"{i:>2}. {r['state_name']:<22} {r[args.metric]:,}"
76
+ if isinstance(r[args.metric], int)
77
+ else f"{i:>2}. {r['state_name']:<22} {r[args.metric]}")
78
+ return 0
79
+
80
+
81
+ if __name__ == "__main__":
82
+ raise SystemExit(main())
@@ -0,0 +1,716 @@
1
+ {
2
+ "metadata": {
3
+ "title": "US Farmers Markets by State, 2026",
4
+ "publisher": "Harvestly Markets",
5
+ "source": "USDA AMS Local Food Portal (https://www.usdalocalfoodportal.com/)",
6
+ "license": "https://creativecommons.org/licenses/by/4.0/",
7
+ "license_note": "CC BY 4.0 — free to reuse with attribution to Harvestly Markets.",
8
+ "attribution": "Harvestly Markets (2026). US Farmers Market Statistics. https://harvestlymarkets.com/farmers-market-statistics/",
9
+ "temporal_coverage": "2026",
10
+ "last_updated": "2026-07-08",
11
+ "url": "https://harvestlymarkets.com/farmers-market-statistics/"
12
+ },
13
+ "national": {
14
+ "farmers_markets": 8863,
15
+ "csa_programs": 1985,
16
+ "farm_stands": 7812,
17
+ "states_and_territories": 52,
18
+ "snap_ebt_markets": 924,
19
+ "snap_ebt_pct": 10.4,
20
+ "organic_vendor_markets": 1063,
21
+ "organic_pct": 12,
22
+ "markets_with_website": 4431,
23
+ "website_pct": 50
24
+ },
25
+ "states": [
26
+ {
27
+ "state_name": "California",
28
+ "state_abbr": "CA",
29
+ "farmers_markets": 679,
30
+ "csa_programs": 78,
31
+ "farm_stands": 223,
32
+ "snap_ebt_markets": 101,
33
+ "snap_ebt_pct": 14.9,
34
+ "organic_vendor_markets": 116,
35
+ "organic_pct": 17.1,
36
+ "markets_with_website": 275,
37
+ "website_pct": 40.5
38
+ },
39
+ {
40
+ "state_name": "New York",
41
+ "state_abbr": "NY",
42
+ "farmers_markets": 587,
43
+ "csa_programs": 144,
44
+ "farm_stands": 272,
45
+ "snap_ebt_markets": 42,
46
+ "snap_ebt_pct": 7.2,
47
+ "organic_vendor_markets": 46,
48
+ "organic_pct": 7.8,
49
+ "markets_with_website": 299,
50
+ "website_pct": 50.9
51
+ },
52
+ {
53
+ "state_name": "Illinois",
54
+ "state_abbr": "IL",
55
+ "farmers_markets": 485,
56
+ "csa_programs": 60,
57
+ "farm_stands": 713,
58
+ "snap_ebt_markets": 31,
59
+ "snap_ebt_pct": 6.4,
60
+ "organic_vendor_markets": 43,
61
+ "organic_pct": 8.9,
62
+ "markets_with_website": 239,
63
+ "website_pct": 49.3
64
+ },
65
+ {
66
+ "state_name": "Pennsylvania",
67
+ "state_abbr": "PA",
68
+ "farmers_markets": 441,
69
+ "csa_programs": 93,
70
+ "farm_stands": 886,
71
+ "snap_ebt_markets": 18,
72
+ "snap_ebt_pct": 4.1,
73
+ "organic_vendor_markets": 44,
74
+ "organic_pct": 10,
75
+ "markets_with_website": 291,
76
+ "website_pct": 66
77
+ },
78
+ {
79
+ "state_name": "Minnesota",
80
+ "state_abbr": "MN",
81
+ "farmers_markets": 438,
82
+ "csa_programs": 56,
83
+ "farm_stands": 80,
84
+ "snap_ebt_markets": 105,
85
+ "snap_ebt_pct": 24,
86
+ "organic_vendor_markets": 14,
87
+ "organic_pct": 3.2,
88
+ "markets_with_website": 183,
89
+ "website_pct": 41.8
90
+ },
91
+ {
92
+ "state_name": "Virginia",
93
+ "state_abbr": "VA",
94
+ "farmers_markets": 398,
95
+ "csa_programs": 78,
96
+ "farm_stands": 180,
97
+ "snap_ebt_markets": 22,
98
+ "snap_ebt_pct": 5.5,
99
+ "organic_vendor_markets": 28,
100
+ "organic_pct": 7,
101
+ "markets_with_website": 231,
102
+ "website_pct": 58
103
+ },
104
+ {
105
+ "state_name": "Wisconsin",
106
+ "state_abbr": "WI",
107
+ "farmers_markets": 356,
108
+ "csa_programs": 83,
109
+ "farm_stands": 152,
110
+ "snap_ebt_markets": 26,
111
+ "snap_ebt_pct": 7.3,
112
+ "organic_vendor_markets": 38,
113
+ "organic_pct": 10.7,
114
+ "markets_with_website": 238,
115
+ "website_pct": 66.9
116
+ },
117
+ {
118
+ "state_name": "Michigan",
119
+ "state_abbr": "MI",
120
+ "farmers_markets": 336,
121
+ "csa_programs": 96,
122
+ "farm_stands": 253,
123
+ "snap_ebt_markets": 38,
124
+ "snap_ebt_pct": 11.3,
125
+ "organic_vendor_markets": 42,
126
+ "organic_pct": 12.5,
127
+ "markets_with_website": 152,
128
+ "website_pct": 45.2
129
+ },
130
+ {
131
+ "state_name": "Ohio",
132
+ "state_abbr": "OH",
133
+ "farmers_markets": 283,
134
+ "csa_programs": 41,
135
+ "farm_stands": 582,
136
+ "snap_ebt_markets": 17,
137
+ "snap_ebt_pct": 6,
138
+ "organic_vendor_markets": 15,
139
+ "organic_pct": 5.3,
140
+ "markets_with_website": 134,
141
+ "website_pct": 47.3
142
+ },
143
+ {
144
+ "state_name": "Missouri",
145
+ "state_abbr": "MO",
146
+ "farmers_markets": 275,
147
+ "csa_programs": 27,
148
+ "farm_stands": 105,
149
+ "snap_ebt_markets": 12,
150
+ "snap_ebt_pct": 4.4,
151
+ "organic_vendor_markets": 16,
152
+ "organic_pct": 5.8,
153
+ "markets_with_website": 187,
154
+ "website_pct": 68
155
+ },
156
+ {
157
+ "state_name": "Colorado",
158
+ "state_abbr": "CO",
159
+ "farmers_markets": 267,
160
+ "csa_programs": 50,
161
+ "farm_stands": 69,
162
+ "snap_ebt_markets": 16,
163
+ "snap_ebt_pct": 6,
164
+ "organic_vendor_markets": 16,
165
+ "organic_pct": 6,
166
+ "markets_with_website": 174,
167
+ "website_pct": 65.2
168
+ },
169
+ {
170
+ "state_name": "Florida",
171
+ "state_abbr": "FL",
172
+ "farmers_markets": 262,
173
+ "csa_programs": 42,
174
+ "farm_stands": 203,
175
+ "snap_ebt_markets": 15,
176
+ "snap_ebt_pct": 5.7,
177
+ "organic_vendor_markets": 22,
178
+ "organic_pct": 8.4,
179
+ "markets_with_website": 143,
180
+ "website_pct": 54.6
181
+ },
182
+ {
183
+ "state_name": "Massachusetts",
184
+ "state_abbr": "MA",
185
+ "farmers_markets": 249,
186
+ "csa_programs": 67,
187
+ "farm_stands": 79,
188
+ "snap_ebt_markets": 33,
189
+ "snap_ebt_pct": 13.3,
190
+ "organic_vendor_markets": 34,
191
+ "organic_pct": 13.7,
192
+ "markets_with_website": 229,
193
+ "website_pct": 92
194
+ },
195
+ {
196
+ "state_name": "Georgia",
197
+ "state_abbr": "GA",
198
+ "farmers_markets": 247,
199
+ "csa_programs": 32,
200
+ "farm_stands": 95,
201
+ "snap_ebt_markets": 27,
202
+ "snap_ebt_pct": 10.9,
203
+ "organic_vendor_markets": 29,
204
+ "organic_pct": 11.7,
205
+ "markets_with_website": 105,
206
+ "website_pct": 42.5
207
+ },
208
+ {
209
+ "state_name": "Texas",
210
+ "state_abbr": "TX",
211
+ "farmers_markets": 231,
212
+ "csa_programs": 46,
213
+ "farm_stands": 145,
214
+ "snap_ebt_markets": 29,
215
+ "snap_ebt_pct": 12.6,
216
+ "organic_vendor_markets": 40,
217
+ "organic_pct": 17.3,
218
+ "markets_with_website": 80,
219
+ "website_pct": 34.6
220
+ },
221
+ {
222
+ "state_name": "North Carolina",
223
+ "state_abbr": "NC",
224
+ "farmers_markets": 211,
225
+ "csa_programs": 61,
226
+ "farm_stands": 660,
227
+ "snap_ebt_markets": 37,
228
+ "snap_ebt_pct": 17.5,
229
+ "organic_vendor_markets": 34,
230
+ "organic_pct": 16.1,
231
+ "markets_with_website": 75,
232
+ "website_pct": 35.5
233
+ },
234
+ {
235
+ "state_name": "Kentucky",
236
+ "state_abbr": "KY",
237
+ "farmers_markets": 191,
238
+ "csa_programs": 25,
239
+ "farm_stands": 63,
240
+ "snap_ebt_markets": 22,
241
+ "snap_ebt_pct": 11.5,
242
+ "organic_vendor_markets": 71,
243
+ "organic_pct": 37.2,
244
+ "markets_with_website": 31,
245
+ "website_pct": 16.2
246
+ },
247
+ {
248
+ "state_name": "Indiana",
249
+ "state_abbr": "IN",
250
+ "farmers_markets": 190,
251
+ "csa_programs": 37,
252
+ "farm_stands": 147,
253
+ "snap_ebt_markets": 24,
254
+ "snap_ebt_pct": 12.6,
255
+ "organic_vendor_markets": 47,
256
+ "organic_pct": 24.7,
257
+ "markets_with_website": 62,
258
+ "website_pct": 32.6
259
+ },
260
+ {
261
+ "state_name": "Iowa",
262
+ "state_abbr": "IA",
263
+ "farmers_markets": 157,
264
+ "csa_programs": 41,
265
+ "farm_stands": 56,
266
+ "snap_ebt_markets": 21,
267
+ "snap_ebt_pct": 13.4,
268
+ "organic_vendor_markets": 22,
269
+ "organic_pct": 14,
270
+ "markets_with_website": 71,
271
+ "website_pct": 45.2
272
+ },
273
+ {
274
+ "state_name": "Washington",
275
+ "state_abbr": "WA",
276
+ "farmers_markets": 154,
277
+ "csa_programs": 67,
278
+ "farm_stands": 156,
279
+ "snap_ebt_markets": 28,
280
+ "snap_ebt_pct": 18.2,
281
+ "organic_vendor_markets": 26,
282
+ "organic_pct": 16.9,
283
+ "markets_with_website": 48,
284
+ "website_pct": 31.2
285
+ },
286
+ {
287
+ "state_name": "Maryland",
288
+ "state_abbr": "MD",
289
+ "farmers_markets": 151,
290
+ "csa_programs": 59,
291
+ "farm_stands": 54,
292
+ "snap_ebt_markets": 7,
293
+ "snap_ebt_pct": 4.6,
294
+ "organic_vendor_markets": 14,
295
+ "organic_pct": 9.3,
296
+ "markets_with_website": 87,
297
+ "website_pct": 57.6
298
+ },
299
+ {
300
+ "state_name": "Connecticut",
301
+ "state_abbr": "CT",
302
+ "farmers_markets": 138,
303
+ "csa_programs": 38,
304
+ "farm_stands": 167,
305
+ "snap_ebt_markets": 11,
306
+ "snap_ebt_pct": 8,
307
+ "organic_vendor_markets": 10,
308
+ "organic_pct": 7.2,
309
+ "markets_with_website": 68,
310
+ "website_pct": 49.3
311
+ },
312
+ {
313
+ "state_name": "Kansas",
314
+ "state_abbr": "KS",
315
+ "farmers_markets": 129,
316
+ "csa_programs": 11,
317
+ "farm_stands": 51,
318
+ "snap_ebt_markets": 11,
319
+ "snap_ebt_pct": 8.5,
320
+ "organic_vendor_markets": 17,
321
+ "organic_pct": 13.2,
322
+ "markets_with_website": 58,
323
+ "website_pct": 45
324
+ },
325
+ {
326
+ "state_name": "Delaware",
327
+ "state_abbr": "DE",
328
+ "farmers_markets": 122,
329
+ "csa_programs": 5,
330
+ "farm_stands": 14,
331
+ "snap_ebt_markets": 4,
332
+ "snap_ebt_pct": 3.3,
333
+ "organic_vendor_markets": 5,
334
+ "organic_pct": 4.1,
335
+ "markets_with_website": 111,
336
+ "website_pct": 91
337
+ },
338
+ {
339
+ "state_name": "Alabama",
340
+ "state_abbr": "AL",
341
+ "farmers_markets": 118,
342
+ "csa_programs": 11,
343
+ "farm_stands": 69,
344
+ "snap_ebt_markets": 1,
345
+ "snap_ebt_pct": 0.8,
346
+ "organic_vendor_markets": 5,
347
+ "organic_pct": 4.2,
348
+ "markets_with_website": 30,
349
+ "website_pct": 25.4
350
+ },
351
+ {
352
+ "state_name": "Oklahoma",
353
+ "state_abbr": "OK",
354
+ "farmers_markets": 109,
355
+ "csa_programs": 10,
356
+ "farm_stands": 23,
357
+ "snap_ebt_markets": 10,
358
+ "snap_ebt_pct": 9.2,
359
+ "organic_vendor_markets": 4,
360
+ "organic_pct": 3.7,
361
+ "markets_with_website": 42,
362
+ "website_pct": 38.5
363
+ },
364
+ {
365
+ "state_name": "Arizona",
366
+ "state_abbr": "AZ",
367
+ "farmers_markets": 103,
368
+ "csa_programs": 16,
369
+ "farm_stands": 20,
370
+ "snap_ebt_markets": 4,
371
+ "snap_ebt_pct": 3.9,
372
+ "organic_vendor_markets": 7,
373
+ "organic_pct": 6.8,
374
+ "markets_with_website": 41,
375
+ "website_pct": 39.8
376
+ },
377
+ {
378
+ "state_name": "Oregon",
379
+ "state_abbr": "OR",
380
+ "farmers_markets": 101,
381
+ "csa_programs": 58,
382
+ "farm_stands": 142,
383
+ "snap_ebt_markets": 42,
384
+ "snap_ebt_pct": 41.6,
385
+ "organic_vendor_markets": 39,
386
+ "organic_pct": 38.6,
387
+ "markets_with_website": 65,
388
+ "website_pct": 64.4
389
+ },
390
+ {
391
+ "state_name": "Tennessee",
392
+ "state_abbr": "TN",
393
+ "farmers_markets": 99,
394
+ "csa_programs": 34,
395
+ "farm_stands": 293,
396
+ "snap_ebt_markets": 12,
397
+ "snap_ebt_pct": 12.1,
398
+ "organic_vendor_markets": 17,
399
+ "organic_pct": 17.2,
400
+ "markets_with_website": 55,
401
+ "website_pct": 55.6
402
+ },
403
+ {
404
+ "state_name": "South Carolina",
405
+ "state_abbr": "SC",
406
+ "farmers_markets": 93,
407
+ "csa_programs": 13,
408
+ "farm_stands": 123,
409
+ "snap_ebt_markets": 18,
410
+ "snap_ebt_pct": 19.4,
411
+ "organic_vendor_markets": 19,
412
+ "organic_pct": 20.4,
413
+ "markets_with_website": 40,
414
+ "website_pct": 43
415
+ },
416
+ {
417
+ "state_name": "New Jersey",
418
+ "state_abbr": "NJ",
419
+ "farmers_markets": 87,
420
+ "csa_programs": 42,
421
+ "farm_stands": 109,
422
+ "snap_ebt_markets": 19,
423
+ "snap_ebt_pct": 21.8,
424
+ "organic_vendor_markets": 24,
425
+ "organic_pct": 27.6,
426
+ "markets_with_website": 37,
427
+ "website_pct": 42.5
428
+ },
429
+ {
430
+ "state_name": "Montana",
431
+ "state_abbr": "MT",
432
+ "farmers_markets": 80,
433
+ "csa_programs": 10,
434
+ "farm_stands": 468,
435
+ "snap_ebt_markets": 6,
436
+ "snap_ebt_pct": 7.5,
437
+ "organic_vendor_markets": 6,
438
+ "organic_pct": 7.5,
439
+ "markets_with_website": 12,
440
+ "website_pct": 15
441
+ },
442
+ {
443
+ "state_name": "Vermont",
444
+ "state_abbr": "VT",
445
+ "farmers_markets": 78,
446
+ "csa_programs": 28,
447
+ "farm_stands": 66,
448
+ "snap_ebt_markets": 10,
449
+ "snap_ebt_pct": 12.8,
450
+ "organic_vendor_markets": 11,
451
+ "organic_pct": 14.1,
452
+ "markets_with_website": 24,
453
+ "website_pct": 30.8
454
+ },
455
+ {
456
+ "state_name": "Alaska",
457
+ "state_abbr": "AK",
458
+ "farmers_markets": 69,
459
+ "csa_programs": 6,
460
+ "farm_stands": 8,
461
+ "snap_ebt_markets": 3,
462
+ "snap_ebt_pct": 4.3,
463
+ "organic_vendor_markets": 4,
464
+ "organic_pct": 5.8,
465
+ "markets_with_website": 49,
466
+ "website_pct": 71
467
+ },
468
+ {
469
+ "state_name": "Arkansas",
470
+ "state_abbr": "AR",
471
+ "farmers_markets": 67,
472
+ "csa_programs": 7,
473
+ "farm_stands": 65,
474
+ "snap_ebt_markets": 9,
475
+ "snap_ebt_pct": 13.4,
476
+ "organic_vendor_markets": 20,
477
+ "organic_pct": 29.9,
478
+ "markets_with_website": 24,
479
+ "website_pct": 35.8
480
+ },
481
+ {
482
+ "state_name": "Louisiana",
483
+ "state_abbr": "LA",
484
+ "farmers_markets": 64,
485
+ "csa_programs": 4,
486
+ "farm_stands": 108,
487
+ "snap_ebt_markets": 11,
488
+ "snap_ebt_pct": 17.2,
489
+ "organic_vendor_markets": 12,
490
+ "organic_pct": 18.8,
491
+ "markets_with_website": 34,
492
+ "website_pct": 53.1
493
+ },
494
+ {
495
+ "state_name": "Wyoming",
496
+ "state_abbr": "WY",
497
+ "farmers_markets": 61,
498
+ "csa_programs": 5,
499
+ "farm_stands": 11,
500
+ "snap_ebt_markets": 1,
501
+ "snap_ebt_pct": 1.6,
502
+ "organic_vendor_markets": 3,
503
+ "organic_pct": 4.9,
504
+ "markets_with_website": 17,
505
+ "website_pct": 27.9
506
+ },
507
+ {
508
+ "state_name": "West Virginia",
509
+ "state_abbr": "WV",
510
+ "farmers_markets": 59,
511
+ "csa_programs": 4,
512
+ "farm_stands": 333,
513
+ "snap_ebt_markets": 6,
514
+ "snap_ebt_pct": 10.2,
515
+ "organic_vendor_markets": 5,
516
+ "organic_pct": 8.5,
517
+ "markets_with_website": 22,
518
+ "website_pct": 37.3
519
+ },
520
+ {
521
+ "state_name": "New Hampshire",
522
+ "state_abbr": "NH",
523
+ "farmers_markets": 53,
524
+ "csa_programs": 36,
525
+ "farm_stands": 140,
526
+ "snap_ebt_markets": 6,
527
+ "snap_ebt_pct": 11.3,
528
+ "organic_vendor_markets": 6,
529
+ "organic_pct": 11.3,
530
+ "markets_with_website": 32,
531
+ "website_pct": 60.4
532
+ },
533
+ {
534
+ "state_name": "Mississippi",
535
+ "state_abbr": "MS",
536
+ "farmers_markets": 52,
537
+ "csa_programs": 5,
538
+ "farm_stands": 25,
539
+ "snap_ebt_markets": 2,
540
+ "snap_ebt_pct": 3.8,
541
+ "organic_vendor_markets": 2,
542
+ "organic_pct": 3.8,
543
+ "markets_with_website": 17,
544
+ "website_pct": 32.7
545
+ },
546
+ {
547
+ "state_name": "Maine",
548
+ "state_abbr": "ME",
549
+ "farmers_markets": 44,
550
+ "csa_programs": 29,
551
+ "farm_stands": 80,
552
+ "snap_ebt_markets": 5,
553
+ "snap_ebt_pct": 11.4,
554
+ "organic_vendor_markets": 5,
555
+ "organic_pct": 11.4,
556
+ "markets_with_website": 22,
557
+ "website_pct": 50
558
+ },
559
+ {
560
+ "state_name": "Nebraska",
561
+ "state_abbr": "NE",
562
+ "farmers_markets": 42,
563
+ "csa_programs": 10,
564
+ "farm_stands": 16,
565
+ "snap_ebt_markets": 2,
566
+ "snap_ebt_pct": 4.8,
567
+ "organic_vendor_markets": 19,
568
+ "organic_pct": 45.2,
569
+ "markets_with_website": 11,
570
+ "website_pct": 26.2
571
+ },
572
+ {
573
+ "state_name": "New Mexico",
574
+ "state_abbr": "NM",
575
+ "farmers_markets": 39,
576
+ "csa_programs": 3,
577
+ "farm_stands": 25,
578
+ "snap_ebt_markets": 4,
579
+ "snap_ebt_pct": 10.3,
580
+ "organic_vendor_markets": 5,
581
+ "organic_pct": 12.8,
582
+ "markets_with_website": 8,
583
+ "website_pct": 20.5
584
+ },
585
+ {
586
+ "state_name": "Idaho",
587
+ "state_abbr": "ID",
588
+ "farmers_markets": 38,
589
+ "csa_programs": 16,
590
+ "farm_stands": 175,
591
+ "snap_ebt_markets": 7,
592
+ "snap_ebt_pct": 18.4,
593
+ "organic_vendor_markets": 8,
594
+ "organic_pct": 21.1,
595
+ "markets_with_website": 29,
596
+ "website_pct": 76.3
597
+ },
598
+ {
599
+ "state_name": "Rhode Island",
600
+ "state_abbr": "RI",
601
+ "farmers_markets": 32,
602
+ "csa_programs": 7,
603
+ "farm_stands": 22,
604
+ "snap_ebt_markets": 21,
605
+ "snap_ebt_pct": 65.6,
606
+ "organic_vendor_markets": 25,
607
+ "organic_pct": 78.1,
608
+ "markets_with_website": 21,
609
+ "website_pct": 65.6
610
+ },
611
+ {
612
+ "state_name": "Nevada",
613
+ "state_abbr": "NV",
614
+ "farmers_markets": 31,
615
+ "csa_programs": 2,
616
+ "farm_stands": 14,
617
+ "snap_ebt_markets": 5,
618
+ "snap_ebt_pct": 16.1,
619
+ "organic_vendor_markets": 5,
620
+ "organic_pct": 16.1,
621
+ "markets_with_website": 11,
622
+ "website_pct": 35.5
623
+ },
624
+ {
625
+ "state_name": "District Of Columbia",
626
+ "state_abbr": "DC",
627
+ "farmers_markets": 28,
628
+ "csa_programs": 0,
629
+ "farm_stands": 0,
630
+ "snap_ebt_markets": 5,
631
+ "snap_ebt_pct": 17.9,
632
+ "organic_vendor_markets": 5,
633
+ "organic_pct": 17.9,
634
+ "markets_with_website": 10,
635
+ "website_pct": 35.7
636
+ },
637
+ {
638
+ "state_name": "South Dakota",
639
+ "state_abbr": "SD",
640
+ "farmers_markets": 18,
641
+ "csa_programs": 10,
642
+ "farm_stands": 16,
643
+ "snap_ebt_markets": 1,
644
+ "snap_ebt_pct": 5.6,
645
+ "organic_vendor_markets": 1,
646
+ "organic_pct": 5.6,
647
+ "markets_with_website": 4,
648
+ "website_pct": 22.2
649
+ },
650
+ {
651
+ "state_name": "Utah",
652
+ "state_abbr": "UT",
653
+ "farmers_markets": 16,
654
+ "csa_programs": 12,
655
+ "farm_stands": 40,
656
+ "snap_ebt_markets": 5,
657
+ "snap_ebt_pct": 31.3,
658
+ "organic_vendor_markets": 2,
659
+ "organic_pct": 12.5,
660
+ "markets_with_website": 12,
661
+ "website_pct": 75
662
+ },
663
+ {
664
+ "state_name": "Hawaii",
665
+ "state_abbr": "HI",
666
+ "farmers_markets": 13,
667
+ "csa_programs": 11,
668
+ "farm_stands": 5,
669
+ "snap_ebt_markets": 5,
670
+ "snap_ebt_pct": 38.5,
671
+ "organic_vendor_markets": 6,
672
+ "organic_pct": 46.2,
673
+ "markets_with_website": 8,
674
+ "website_pct": 61.5
675
+ },
676
+ {
677
+ "state_name": "North Dakota",
678
+ "state_abbr": "ND",
679
+ "farmers_markets": 7,
680
+ "csa_programs": 6,
681
+ "farm_stands": 9,
682
+ "snap_ebt_markets": 2,
683
+ "snap_ebt_pct": 28.6,
684
+ "organic_vendor_markets": 4,
685
+ "organic_pct": 57.1,
686
+ "markets_with_website": 2,
687
+ "website_pct": 28.6
688
+ },
689
+ {
690
+ "state_name": "Puerto Rico",
691
+ "state_abbr": "PR",
692
+ "farmers_markets": 2,
693
+ "csa_programs": 0,
694
+ "farm_stands": 1,
695
+ "snap_ebt_markets": 0,
696
+ "snap_ebt_pct": 0,
697
+ "organic_vendor_markets": 2,
698
+ "organic_pct": 100,
699
+ "markets_with_website": 1,
700
+ "website_pct": 50
701
+ },
702
+ {
703
+ "state_name": "Virgin Islands",
704
+ "state_abbr": "VI",
705
+ "farmers_markets": 0,
706
+ "csa_programs": 0,
707
+ "farm_stands": 1,
708
+ "snap_ebt_markets": 0,
709
+ "snap_ebt_pct": 0,
710
+ "organic_vendor_markets": 0,
711
+ "organic_pct": 0,
712
+ "markets_with_website": 0,
713
+ "website_pct": 0
714
+ }
715
+ ]
716
+ }
@@ -0,0 +1,159 @@
1
+ """Offline tests for the harvestly client — fixture is the real production schema."""
2
+ import io
3
+ import json
4
+ import os
5
+ import pathlib
6
+ import tempfile
7
+ import unittest
8
+ from contextlib import redirect_stdout
9
+
10
+ import harvestly
11
+ from harvestly import Dataset, cli
12
+
13
+ FIXTURE = pathlib.Path(__file__).parent / "fixture_dataset.json"
14
+
15
+
16
+ def fixture_dict():
17
+ return json.loads(FIXTURE.read_text())
18
+
19
+
20
+ class TestDataset(unittest.TestCase):
21
+ def setUp(self):
22
+ self.ds = Dataset.from_dict(fixture_dict())
23
+
24
+ def test_states_returns_all_rows(self):
25
+ rows = self.ds.states()
26
+ self.assertEqual(len(rows), 53)
27
+ self.assertIn("state_abbr", rows[0])
28
+
29
+ def test_state_lookup_by_abbr_case_insensitive(self):
30
+ row = self.ds.state("ca")
31
+ self.assertEqual(row["state_name"], "California")
32
+ self.assertEqual(row["farmers_markets"], 679)
33
+ self.assertEqual(self.ds.state("CA"), row)
34
+
35
+ def test_state_lookup_by_full_name(self):
36
+ row = self.ds.state("California")
37
+ self.assertEqual(row["state_abbr"], "CA")
38
+
39
+ def test_state_unknown_raises_keyerror(self):
40
+ with self.assertRaises(KeyError):
41
+ self.ds.state("zz")
42
+
43
+ def test_national_block(self):
44
+ self.assertEqual(self.ds.national["farmers_markets"], 8863)
45
+
46
+ def test_metadata_carries_license_and_attribution(self):
47
+ self.assertIn("creativecommons.org/licenses/by/4.0", self.ds.metadata["license"])
48
+ self.assertIn("harvestlymarkets.com", self.ds.metadata["attribution"])
49
+
50
+ def test_top_sorts_descending_by_default(self):
51
+ top3 = self.ds.top("farmers_markets", 3)
52
+ self.assertEqual(top3[0]["state_abbr"], "CA")
53
+ vals = [r["farmers_markets"] for r in top3]
54
+ self.assertEqual(vals, sorted(vals, reverse=True))
55
+
56
+ def test_top_ascending(self):
57
+ bottom = self.ds.top("farmers_markets", 3, ascending=True)
58
+ vals = [r["farmers_markets"] for r in bottom]
59
+ self.assertEqual(vals, sorted(vals))
60
+
61
+ def test_top_unknown_metric_raises_valueerror(self):
62
+ with self.assertRaises(ValueError):
63
+ self.ds.top("nonsense_metric", 3)
64
+
65
+
66
+ class TestUrls(unittest.TestCase):
67
+ def test_state_url(self):
68
+ self.assertEqual(
69
+ harvestly.state_url("ny"), "https://harvestlymarkets.com/states/ny/"
70
+ )
71
+
72
+ def test_zip_url(self):
73
+ self.assertEqual(
74
+ harvestly.zip_url("12180"), "https://harvestlymarkets.com/zip/12180/"
75
+ )
76
+
77
+ def test_zip_url_rejects_non_5_digit(self):
78
+ with self.assertRaises(ValueError):
79
+ harvestly.zip_url("1218")
80
+
81
+
82
+ class TestLoadCache(unittest.TestCase):
83
+ def test_load_fetches_once_then_reads_cache(self):
84
+ calls = []
85
+
86
+ def fake_fetch():
87
+ calls.append(1)
88
+ return fixture_dict()
89
+
90
+ with tempfile.TemporaryDirectory() as tmp:
91
+ os.environ["HARVESTLY_CACHE_DIR"] = tmp
92
+ try:
93
+ ds1 = harvestly.load(fetch=fake_fetch)
94
+ ds2 = harvestly.load(fetch=fake_fetch)
95
+ finally:
96
+ del os.environ["HARVESTLY_CACHE_DIR"]
97
+ self.assertEqual(len(calls), 1)
98
+ self.assertEqual(ds1.national, ds2.national)
99
+
100
+ def test_load_refresh_true_refetches(self):
101
+ calls = []
102
+
103
+ def fake_fetch():
104
+ calls.append(1)
105
+ return fixture_dict()
106
+
107
+ with tempfile.TemporaryDirectory() as tmp:
108
+ os.environ["HARVESTLY_CACHE_DIR"] = tmp
109
+ try:
110
+ harvestly.load(fetch=fake_fetch)
111
+ harvestly.load(fetch=fake_fetch, refresh=True)
112
+ finally:
113
+ del os.environ["HARVESTLY_CACHE_DIR"]
114
+ self.assertEqual(len(calls), 2)
115
+
116
+
117
+ class TestCli(unittest.TestCase):
118
+ def run_cli(self, *argv):
119
+ def fake_fetch():
120
+ return fixture_dict()
121
+
122
+ buf = io.StringIO()
123
+ with tempfile.TemporaryDirectory() as tmp:
124
+ os.environ["HARVESTLY_CACHE_DIR"] = tmp
125
+ try:
126
+ with redirect_stdout(buf):
127
+ rc = cli.main(list(argv), fetch=fake_fetch)
128
+ finally:
129
+ del os.environ["HARVESTLY_CACHE_DIR"]
130
+ return rc, buf.getvalue()
131
+
132
+ def test_cli_state(self):
133
+ rc, out = self.run_cli("state", "ca")
134
+ self.assertEqual(rc, 0)
135
+ self.assertIn("California", out)
136
+ self.assertIn("679", out)
137
+
138
+ def test_cli_national(self):
139
+ rc, out = self.run_cli("national")
140
+ self.assertEqual(rc, 0)
141
+ self.assertIn("8,863", out)
142
+
143
+ def test_cli_top(self):
144
+ rc, out = self.run_cli("top", "website_pct", "-n", "3")
145
+ self.assertEqual(rc, 0)
146
+ self.assertEqual(len([l for l in out.splitlines() if l.strip()]) >= 3, True)
147
+
148
+ def test_cli_json_flag_emits_valid_json(self):
149
+ rc, out = self.run_cli("state", "vt", "--json")
150
+ self.assertEqual(rc, 0)
151
+ self.assertEqual(json.loads(out)["state_abbr"], "VT")
152
+
153
+ def test_cli_unknown_state_returns_nonzero(self):
154
+ rc, out = self.run_cli("state", "zz")
155
+ self.assertNotEqual(rc, 0)
156
+
157
+
158
+ if __name__ == "__main__":
159
+ unittest.main()