#!/usr/bin/env python3
"""Generate zone-specific planting guides for SeedVault Market."""
import os
import subprocess
from datetime import datetime, timedelta
from dateutil.relativedelta import relativedelta
# Zone data: approximate last frost (as date), first frost, season days, notes
zones = {
3: {'last_frost': (5, 12), 'first_frost': (9, 25), 'season': '110-130 days',
'notes': 'Short season. Prioritize early-maturing varieties. Heavy mulch helps extend season.'},
4: {'last_frost': (4, 30), 'first_frost': (9, 25), 'season': '130-150 days',
'notes': 'Cold springs and falls. Use cold frames and row covers for extended season.'},
5: {'last_frost': (4, 20), 'first_frost': (10, 5), 'season': '150-170 days',
'notes': 'Solid growing season. Most varieties will reach maturity with careful timing.'},
6: {'last_frost': (4, 5), 'first_frost': (10, 20), 'season': '170-195 days',
'notes': 'One of the best zones for full-season crops. Succession planting works well.'},
7: {'last_frost': (3, 20), 'first_frost': (11, 5), 'season': '190-220 days',
'notes': 'Long season with warm summers. Heat-tolerant varieties perform best.'},
8: {'last_frost': (3, 5), 'first_frost': (11, 20), 'season': '220-250 days',
'notes': 'Mild winters allow fall/winter planting in many areas. Nearly year-round growing.'},
9: {'last_frost': (2, 20), 'first_frost': (12, 5), 'season': '250-280 days',
'notes': 'Very mild climate. Many crops can be planted year-round or in fall/winter.'},
10: {'last_frost': (1, 31), 'first_frost': (12, 20), 'season': 'Near year-round',
'notes': 'Subtropical. Focus on heat-loving crops. Summer heat can be challenging.'},
}
# Seed data: days to maturity, min soil temp F, indoor weeks before last frost, method
# Method: 'transplant', 'sow', 'both'
seeds = {
'Roma Tomatoes': {'maturity': '75-85', 'soil_min': 65, 'indoor_wks': 6, 'method': 'transplant', 'category': 'Fruiting Vegetables'},
'Jalapeno Peppers': {'maturity': '65-75', 'soil_min': 70, 'indoor_wks': 8, 'method': 'transplant', 'category': 'Fruiting Vegetables'},
'California Wonder Peppers': {'maturity': '70-80', 'soil_min': 70, 'indoor_wks': 8, 'method': 'transplant', 'category': 'Fruiting Vegetables'},
'Crimson Sweet Watermelon': {'maturity': '80-90', 'soil_min': 70, 'indoor_wks': 2, 'method': 'both', 'category': 'Fruiting Vegetables'},
'Pickling Cucumbers': {'maturity': '50-60', 'soil_min': 70, 'indoor_wks': 2, 'method': 'both', 'category': 'Fruiting Vegetables'},
'Dark Green Zucchini': {'maturity': '45-55', 'soil_min': 65, 'indoor_wks': 2, 'method': 'both', 'category': 'Fruiting Vegetables'},
'Jack-o-Lantern Pumpkin': {'maturity': '90-100', 'soil_min': 70, 'indoor_wks': 0, 'method': 'sow', 'category': 'Fruiting Vegetables'},
'Luffa Sponge Gourd': {'maturity': '90-120', 'soil_min': 70, 'indoor_wks': 2, 'method': 'sow', 'category': 'Fruiting Vegetables'},
'Large Leaf Basil': {'maturity': '45-55', 'soil_min': 65, 'indoor_wks': 4, 'method': 'transplant', 'category': 'Herbs'},
'Coriander (Cilantro)': {'maturity': '55-65', 'soil_min': 50, 'indoor_wks': 3, 'method': 'sow', 'category': 'Herbs'},
'Parsley': {'maturity': '60-70', 'soil_min': 50, 'indoor_wks': 3, 'method': 'sow', 'category': 'Herbs'},
'Catnip': {'maturity': '60-70', 'soil_min': 55, 'indoor_wks': 0, 'method': 'sow', 'category': 'Herbs'},
'Sugar Beets': {'maturity': '50-60', 'soil_min': 50, 'indoor_wks': 0, 'method': 'sow', 'category': 'Root Vegetables'},
'Bulls Blood Beets': {'maturity': '50-60', 'soil_min': 50, 'indoor_wks': 0, 'method': 'sow', 'category': 'Root Vegetables'},
'Snowball Turnips': {'maturity': '50-60', 'soil_min': 45, 'indoor_wks': 0, 'method': 'sow', 'category': 'Root Vegetables'},
'Scarlett Nantes Carrots': {'maturity': '60-70', 'soil_min': 55, 'indoor_wks': 0, 'method': 'sow', 'category': 'Root Vegetables'},
'Provider Green Bush Beans': {'maturity': '50-60', 'soil_min': 60, 'indoor_wks': 0, 'method': 'sow', 'category': 'Legumes & Corn'},
'Sugar Lace Snap Peas': {'maturity': '60-70', 'soil_min': 45, 'indoor_wks': 0, 'method': 'sow', 'category': 'Legumes & Corn'},
'Yellow Dent Corn': {'maturity': '80-90', 'soil_min': 60, 'indoor_wks': 0, 'method': 'sow', 'category': 'Legumes & Corn'},
'Utah Celery': {'maturity': '90-100', 'soil_min': 60, 'indoor_wks': 10, 'method': 'transplant', 'category': 'Leafy Greens & Brassicas'},
'Butter Crunch Lettuce': {'maturity': '50-60', 'soil_min': 40, 'indoor_wks': 4, 'method': 'both', 'category': 'Leafy Greens & Brassicas'},
'Red Russian Kale': {'maturity': '55-65', 'soil_min': 40, 'indoor_wks': 0, 'method': 'sow', 'category': 'Leafy Greens & Brassicas'},
'Long Island Brussels Sprouts': {'maturity': '90-100', 'soil_min': 60, 'indoor_wks': 8, 'method': 'transplant', 'category': 'Leafy Greens & Brassicas'},
'White Bunching Onion': {'maturity': '100-120', 'soil_min': 40, 'indoor_wks': 0, 'method': 'sow', 'category': 'Leafy Greens & Brassicas'},
}
# Tree seeds
trees = {
'Japanese Maple': {'plant': 'Spring/Fall', 'note': 'Fall preferred. Shade when young.'},
'Red Cedar': {'plant': 'Fall', 'note': 'Bare-root in dormant season.'},
'Southern Yew': {'plant': 'Spring', 'note': 'Slow grower. Good hedge plant.'},
'Chinese Elm': {'plant': 'Spring', 'note': 'Drought-tolerant once established.'},
}
# Special zone notes for certain seeds
zone_warnings = {
3: {
'Luffa Sponge Gourd': 'May not fully mature. Start indoors early.',
'Crimson Sweet Watermelon': 'Tight season. Consider earlier-maturing variety.',
'Yellow Dent Corn': 'Short season. Choose early dent corn varieties.',
},
4: {
'Luffa Sponge Gourd': 'Tight season. Start indoors, use black plastic mulch.',
}
}
def format_date(month, day):
months = ['Jan', 'Feb', 'Mar', 'Apr', 'May', 'Jun',
'Jul', 'Aug', 'Sep', 'Oct', 'Nov', 'Dec']
return f'{months[month-1]} {day}'
def calc_indoor_start(last_frost_month, last_frost_day, weeks):
from datetime import datetime, timedelta
date = datetime(2026, last_frost_month, last_frost_day) - timedelta(weeks=weeks)
return (date.month, date.day)
def generate_html(zone_num):
z = zones[zone_num]
lf_m, lf_d = z['last_frost']
ff_m, ff_d = z['first_frost']
# Group seeds by category
categories = {}
for name, data in seeds.items():
cat = data['category']
categories.setdefault(cat, []).append((name, data))
html = f'''<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8">
<title>Zone {zone_num} Planting Guide - SeedVault Market</title>
<style>
@import url('https://fonts.googleapis.com/css2?family=Merriweather:wght@400;700;900&family=Inter:wght@400;500;600;700&display=swap');
@page {{
size: 8.5in 11in;
margin: 0.5in 0.5in 0.5in 0.5in;
}}
* {{ margin: 0; padding: 0; box-sizing: border-box; }}
body {{
font-family: 'Inter', sans-serif;
font-size: 9.5pt;
line-height: 1.5;
color: #2d2d2d;
}}
.header {{
text-align: center;
margin-bottom: 0.15in;
padding-bottom: 0.12in;
border-bottom: 3px solid #4a7c59;
}}
.header h1 {{
font-family: 'Merriweather', Georgia, serif;
font-size: 24pt;
font-weight: 900;
color: #3a6b4a;
}}
.header .zone-label {{
font-size: 14pt;
color: #fff;
background: #4a7c59;
display: inline-block;
padding: 3px 16px;
border-radius: 4px;
margin-top: 4pt;
font-weight: 700;
}}
.zone-info {{
display: flex;
justify-content: space-around;
margin: 0.1in 0 0.12in 0;
padding: 8px 12px;
background: #f4f1ea;
border-radius: 6px;
font-size: 10pt;
}}
.zone-info-item {{ text-align: center; }}
.zone-info-item .label {{ font-weight: 700; color: #4a7c59; }}
.zone-info-item .value {{ color: #555; }}
.zone-note {{
background: #eef5ea;
border-left: 4px solid #4a7c59;
padding: 8px 12px;
font-size: 9pt;
margin: 0.08in 0 0.12in 0;
color: #3a5a30;
border-radius: 0 4px 4px 0;
}}
.cat-header {{
font-family: 'Merriweather', Georgia, serif;
font-size: 13pt;
font-weight: 700;
color: #fff;
background: #4a7c59;
padding: 5px 12px;
margin: 0.1in 0 0 0;
border-radius: 4px 4px 0 0;
}}
.seed-table {{
width: 100%;
border-collapse: collapse;
font-size: 8.5pt;
border: 1px solid #ddd;
border-top: none;
}}
.seed-table th {{
text-align: left;
font-weight: 700;
color: #3a6b4a;
border-bottom: 2px solid #c0c0b0;
padding: 4px 6px;
font-size: 8pt;
text-transform: uppercase;
background: #f9f9f5;
}}
.seed-table td {{
padding: 4px 6px;
border-bottom: 1px solid #e8e8e0;
vertical-align: top;
}}
.seed-table tr:last-child td {{ border-bottom: none; }}
.seed-table td:first-child {{ font-weight: 600; }}
.badge {{
display: inline-block;
padding: 1px 6px;
border-radius: 3px;
font-size: 7.5pt;
font-weight: 700;
color: #fff;
}}
.badge-indoor {{ background: #4a7c59; }}
.badge-sow {{ background: #3b82b8; }}
.badge-both {{ background: #d48806; }}
.badge-warn {{ background: #c0392b; }}
.warning {{
color: #c0392b;
font-size: 8pt;
font-style: italic;
}}
.tree-section {{
margin: 0.12in 0;
}}
.tree-grid {{
display: grid;
grid-template-columns: 1fr 1fr;
gap: 8px;
}}
.tree-card {{
border: 2px solid #e0e0d8;
border-radius: 6px;
overflow: hidden;
break-inside: avoid;
}}
.tree-card-header {{
padding: 5px 10px;
color: #fff;
font-weight: 700;
font-size: 10pt;
background: #6b4f3a;
}}
.tree-card-body {{
padding: 6px 10px;
background: #fafaf7;
font-size: 9pt;
}}
.footer {{
text-align: center;
margin-top: 0.12in;
padding-top: 8px;
border-top: 2px solid #4a7c59;
font-size: 9pt;
color: #555;
}}
.footer .brand {{ font-weight: 700; color: #3a6b4a; font-size: 11pt; }}
.footer .tagline {{ font-size: 8pt; color: #888; margin-top: 2px; }}
.page {{ page-break-after: always; }}
.page:last-child {{ page-break-after: auto; }}
</style>
</head>
<body>
<div class="page">
<div class="header">
<h1>Planting Guide</h1>
<div class="zone-label">USDA Zone {zone_num}</div>
</div>
<div class="zone-info">
<div class="zone-info-item">
<div class="label">Last Frost</div>
<div class="value">{format_date(lf_m, lf_d)}</div>
</div>
<div class="zone-info-item">
<div class="label">First Frost</div>
<div class="value">{format_date(ff_m, ff_d)}</div>
</div>
<div class="zone-info-item">
<div class="label">Growing Season</div>
<div class="value">{z['season']}</div>
</div>
</div>
<div class="zone-note">
<strong>Zone {zone_num} Tip:</strong> {z['notes']}
</div>
'''
# Generate tables by category
for cat, cat_seeds in categories.items():
html += f' <div class="cat-header">{cat}</div>\n'
html += ' <table class="seed-table">\n'
html += ' <tr><th>Seed</th><th>Method</th><th>Start Indoors</th><th>Direct Sow / Transplant</th><th>Harvest By</th></tr>\n'
for name, data in cat_seeds:
method = data['method']
maturity = data['maturity']
mat_avg = int(maturity.split('-')[0]) # use earlier number
indoor_wks = data['indoor_wks']
if indoor_wks > 0:
start_m, start_d = calc_indoor_start(lf_m, lf_d, indoor_wks)
indoor_str = f'{format_date(start_m, start_d)}'
else:
indoor_str = '—'
# Transplant date (2 weeks before last frost for transplants)
if method == 'transplant':
trans_m, trans_d = calc_indoor_start(lf_m, lf_d, 2)
transplant_str = f'{format_date(trans_m, trans_d)}'
elif method == 'both':
transplant_str = f'{format_date(lf_m, lf_d)} or earlier'
else:
# Direct sow after last frost (or a bit before for cold-hardy)
if data['soil_min'] <= 45:
sow_m, sow_d = calc_indoor_start(lf_m, lf_d, 2)
transplant_str = f'{format_date(sow_m, sow_d)}'
else:
transplant_str = f'{format_date(lf_m, lf_d)}'
# Harvest estimate
harvest_date = datetime(2026, lf_m, lf_d) + timedelta(days=mat_avg + indoor_wks * 7)
harvest_str = format_date(harvest_date.month, harvest_date.day)
# Badge
badge = ''
if method == 'transplant':
badge = '<span class="badge badge-indoor">Indoor</span>'
elif method == 'sow':
badge = '<span class="badge badge-sow">Sow</span>'
else:
badge = '<span class="badge badge-both">Both</span>'
# Zone warnings
warning = ''
if zone_num in zone_warnings and name in zone_warnings[zone_num]:
warning = f'<br><span class="warning">{zone_warnings[zone_num][name]}</span>'
html += f' <tr>'
html += f' <td>{name}{warning}</td>'
html += f' <td>{badge}</td>'
html += f' <td>{indoor_str}</td>'
html += f' <td>{transplant_str}</td>'
html += f' <td>{harvest_str}</td>'
html += f' </tr>\n'
html += ' </table>\n'
# Trees
html += ' <div class="tree-section">\n'
html += ' <div class="cat-header" style="background:#6b4f3a;">Tree Seeds</div>\n'
html += ' <div class="tree-grid">\n'
for tname, tdata in trees.items():
html += f''' <div class="tree-card">
<div class="tree-card-header">{tname}</div>
<div class="tree-card-body">
<strong>{tdata['plant']}</strong><br>{tdata['note']}
</div>
</div>\n'''
html += ' </div></div>\n'
html += '''
<div class="footer">
<div class="brand">SeedVault Market</div>
<div class="tagline">seedvault.market · Heirloom seeds for real food sovereignty · Bitcoin payments welcome</div>
</div>
</div>
</body>
</html>'''
return html
def main():
output_dir = os.path.dirname(os.path.abspath(__file__))
for zone in sorted(zones.keys()):
print(f'Generating Zone {zone}...')
html_content = generate_html(zone)
html_path = os.path.join(output_dir, f'zone_{zone}_guide.html')
pdf_path = os.path.join(output_dir, f'zone_{zone}_guide.pdf')
with open(html_path, 'w') as f:
f.write(html_content)
result = subprocess.run(
['weasyprint', html_path, pdf_path],
capture_output=True, text=True
)
if result.returncode == 0:
size = os.path.getsize(pdf_path)
print(f' Zone {zone}: {size/1024:.0f}KB')
else:
print(f' Zone {zone}: FAILED - {result.stderr}')
print('Done!')
if __name__ == '__main__':
main()