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.
- harvestly-0.1.0/.gitignore +51 -0
- harvestly-0.1.0/LICENSE +26 -0
- harvestly-0.1.0/PKG-INFO +83 -0
- harvestly-0.1.0/README.md +62 -0
- harvestly-0.1.0/pyproject.toml +36 -0
- harvestly-0.1.0/src/harvestly/__init__.py +100 -0
- harvestly-0.1.0/src/harvestly/cli.py +82 -0
- harvestly-0.1.0/tests/fixture_dataset.json +716 -0
- harvestly-0.1.0/tests/test_harvestly.py +159 -0
|
@@ -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
|
harvestly-0.1.0/LICENSE
ADDED
|
@@ -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/
|
harvestly-0.1.0/PKG-INFO
ADDED
|
@@ -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()
|