@pencil-agent/nano-pencil 2.0.0 → 2.0.1

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.
Files changed (195) hide show
  1. package/README.md +267 -267
  2. package/dist/build-meta.json +3 -3
  3. package/dist/core/export-html/AGENT.md +11 -11
  4. package/dist/core/export-html/template.css +971 -971
  5. package/dist/core/export-html/template.html +54 -54
  6. package/dist/core/mcp/mcp-client.d.ts +3 -1
  7. package/dist/core/mcp/mcp-client.js +6 -6
  8. package/dist/core/mcp/mcp-config.d.ts +3 -3
  9. package/dist/core/mcp/mcp-config.js +1 -1
  10. package/dist/core/mcp/mcp-manager.d.ts +5 -1
  11. package/dist/core/mcp/mcp-manager.js +1 -1
  12. package/dist/core/platform/config/resource-loader.d.ts +2 -0
  13. package/dist/core/platform/config/resource-loader.js +2 -2
  14. package/dist/core/runtime/agent-session.d.ts +12 -0
  15. package/dist/core/runtime/agent-session.js +8 -8
  16. package/dist/core/runtime/sdk.d.ts +8 -0
  17. package/dist/core/runtime/sdk.js +1 -1
  18. package/dist/extensions/builtin/AGENT.md +115 -115
  19. package/dist/extensions/builtin/browser/AGENT.md +17 -17
  20. package/dist/extensions/builtin/browser/agent-workspace/agent_helpers.py +12 -12
  21. package/dist/extensions/builtin/browser/agent-workspace/domain-skills/amazon/product-search.md +198 -198
  22. package/dist/extensions/builtin/browser/agent-workspace/domain-skills/archive-org/scraping.md +341 -341
  23. package/dist/extensions/builtin/browser/agent-workspace/domain-skills/arxiv/scraping.md +311 -311
  24. package/dist/extensions/builtin/browser/agent-workspace/domain-skills/arxiv-bulk/scraping.md +333 -333
  25. package/dist/extensions/builtin/browser/agent-workspace/domain-skills/atlas/overview.md +70 -70
  26. package/dist/extensions/builtin/browser/agent-workspace/domain-skills/booking-com/scraping.md +578 -578
  27. package/dist/extensions/builtin/browser/agent-workspace/domain-skills/capterra/scraping.md +440 -440
  28. package/dist/extensions/builtin/browser/agent-workspace/domain-skills/centilebrain/generate-estimates.md +110 -110
  29. package/dist/extensions/builtin/browser/agent-workspace/domain-skills/coingecko/scraping.md +325 -325
  30. package/dist/extensions/builtin/browser/agent-workspace/domain-skills/coinmarketcap/scraping.md +463 -463
  31. package/dist/extensions/builtin/browser/agent-workspace/domain-skills/coursera/scraping.md +360 -360
  32. package/dist/extensions/builtin/browser/agent-workspace/domain-skills/craigslist/scraping.md +390 -390
  33. package/dist/extensions/builtin/browser/agent-workspace/domain-skills/crossref/scraping.md +568 -568
  34. package/dist/extensions/builtin/browser/agent-workspace/domain-skills/dev-to/scraping.md +323 -323
  35. package/dist/extensions/builtin/browser/agent-workspace/domain-skills/duckduckgo/scraping.md +349 -349
  36. package/dist/extensions/builtin/browser/agent-workspace/domain-skills/ebay/scraping.md +435 -435
  37. package/dist/extensions/builtin/browser/agent-workspace/domain-skills/etsy/scraping.md +506 -506
  38. package/dist/extensions/builtin/browser/agent-workspace/domain-skills/eventbrite/scraping.md +363 -363
  39. package/dist/extensions/builtin/browser/agent-workspace/domain-skills/expedia/automation.md +168 -168
  40. package/dist/extensions/builtin/browser/agent-workspace/domain-skills/facebook/groups.md +236 -236
  41. package/dist/extensions/builtin/browser/agent-workspace/domain-skills/facebook/pages.md +295 -295
  42. package/dist/extensions/builtin/browser/agent-workspace/domain-skills/framer/editor.md +108 -108
  43. package/dist/extensions/builtin/browser/agent-workspace/domain-skills/fred/scraping.md +493 -493
  44. package/dist/extensions/builtin/browser/agent-workspace/domain-skills/g2/scraping.md +580 -580
  45. package/dist/extensions/builtin/browser/agent-workspace/domain-skills/genius/scraping.md +511 -511
  46. package/dist/extensions/builtin/browser/agent-workspace/domain-skills/github/repo-actions.md +65 -65
  47. package/dist/extensions/builtin/browser/agent-workspace/domain-skills/github/scraping.md +184 -184
  48. package/dist/extensions/builtin/browser/agent-workspace/domain-skills/glassdoor/scraping.md +543 -543
  49. package/dist/extensions/builtin/browser/agent-workspace/domain-skills/gmail/compose.md +122 -122
  50. package/dist/extensions/builtin/browser/agent-workspace/domain-skills/goodreads/scraping.md +461 -461
  51. package/dist/extensions/builtin/browser/agent-workspace/domain-skills/gutenberg/scraping.md +383 -383
  52. package/dist/extensions/builtin/browser/agent-workspace/domain-skills/hackernews/scraping.md +243 -243
  53. package/dist/extensions/builtin/browser/agent-workspace/domain-skills/howlongtobeat/scraping.md +473 -473
  54. package/dist/extensions/builtin/browser/agent-workspace/domain-skills/imdb/scraping.md +271 -271
  55. package/dist/extensions/builtin/browser/agent-workspace/domain-skills/itch-io/scraping.md +436 -436
  56. package/dist/extensions/builtin/browser/agent-workspace/domain-skills/job-boards/indeed-glassdoor.md +1021 -1021
  57. package/dist/extensions/builtin/browser/agent-workspace/domain-skills/letterboxd/scraping.md +349 -349
  58. package/dist/extensions/builtin/browser/agent-workspace/domain-skills/linkedin/invitation-manager.md +109 -109
  59. package/dist/extensions/builtin/browser/agent-workspace/domain-skills/loom/folder-enumeration.md +170 -170
  60. package/dist/extensions/builtin/browser/agent-workspace/domain-skills/macrotrends/scraping.md +537 -537
  61. package/dist/extensions/builtin/browser/agent-workspace/domain-skills/medium/article-hydration.md +120 -120
  62. package/dist/extensions/builtin/browser/agent-workspace/domain-skills/medium/scraping.md +414 -414
  63. package/dist/extensions/builtin/browser/agent-workspace/domain-skills/metacritic/scraping.md +477 -477
  64. package/dist/extensions/builtin/browser/agent-workspace/domain-skills/musicbrainz/scraping.md +478 -478
  65. package/dist/extensions/builtin/browser/agent-workspace/domain-skills/nasa/scraping.md +339 -339
  66. package/dist/extensions/builtin/browser/agent-workspace/domain-skills/news-aggregation/multi-source.md +205 -205
  67. package/dist/extensions/builtin/browser/agent-workspace/domain-skills/open-library/scraping.md +472 -472
  68. package/dist/extensions/builtin/browser/agent-workspace/domain-skills/openalex/scraping.md +470 -470
  69. package/dist/extensions/builtin/browser/agent-workspace/domain-skills/openstreetmap/scraping.md +490 -490
  70. package/dist/extensions/builtin/browser/agent-workspace/domain-skills/package-registries/npm-pypi.md +478 -478
  71. package/dist/extensions/builtin/browser/agent-workspace/domain-skills/polymarket/scraping.md +234 -234
  72. package/dist/extensions/builtin/browser/agent-workspace/domain-skills/producthunt/scraping.md +307 -307
  73. package/dist/extensions/builtin/browser/agent-workspace/domain-skills/pubmed/scraping.md +421 -421
  74. package/dist/extensions/builtin/browser/agent-workspace/domain-skills/quora/scraping.md +364 -364
  75. package/dist/extensions/builtin/browser/agent-workspace/domain-skills/rawg/scraping.md +352 -352
  76. package/dist/extensions/builtin/browser/agent-workspace/domain-skills/reddit/scraping.md +124 -124
  77. package/dist/extensions/builtin/browser/agent-workspace/domain-skills/rest-countries/scraping.md +233 -233
  78. package/dist/extensions/builtin/browser/agent-workspace/domain-skills/sec-edgar/scraping.md +361 -361
  79. package/dist/extensions/builtin/browser/agent-workspace/domain-skills/shopify-admin/README.md +36 -36
  80. package/dist/extensions/builtin/browser/agent-workspace/domain-skills/shopify-admin/embedded-apps.md +72 -72
  81. package/dist/extensions/builtin/browser/agent-workspace/domain-skills/shopify-admin/knowledge-base.md +109 -109
  82. package/dist/extensions/builtin/browser/agent-workspace/domain-skills/shopify-admin/polaris-inputs.md +137 -137
  83. package/dist/extensions/builtin/browser/agent-workspace/domain-skills/soundcloud/scraping.md +362 -362
  84. package/dist/extensions/builtin/browser/agent-workspace/domain-skills/spotify/scraping.md +339 -339
  85. package/dist/extensions/builtin/browser/agent-workspace/domain-skills/stackoverflow/scraping.md +435 -435
  86. package/dist/extensions/builtin/browser/agent-workspace/domain-skills/steam/scraping.md +575 -575
  87. package/dist/extensions/builtin/browser/agent-workspace/domain-skills/substack/scraping.md +338 -338
  88. package/dist/extensions/builtin/browser/agent-workspace/domain-skills/thetechgeeks/pricing.md +52 -52
  89. package/dist/extensions/builtin/browser/agent-workspace/domain-skills/tiktok/upload.md +107 -107
  90. package/dist/extensions/builtin/browser/agent-workspace/domain-skills/tradingview/scraping.md +309 -309
  91. package/dist/extensions/builtin/browser/agent-workspace/domain-skills/trello/boards-and-lists.md +88 -88
  92. package/dist/extensions/builtin/browser/agent-workspace/domain-skills/trustpilot/scraping.md +375 -375
  93. package/dist/extensions/builtin/browser/agent-workspace/domain-skills/walmart/scraping.md +444 -444
  94. package/dist/extensions/builtin/browser/agent-workspace/domain-skills/wayback-machine/scraping.md +306 -306
  95. package/dist/extensions/builtin/browser/agent-workspace/domain-skills/weather/scraping.md +398 -398
  96. package/dist/extensions/builtin/browser/agent-workspace/domain-skills/wellfound/scraping.md +596 -596
  97. package/dist/extensions/builtin/browser/agent-workspace/domain-skills/world-bank/scraping.md +356 -356
  98. package/dist/extensions/builtin/browser/agent-workspace/domain-skills/xiaohongshu/scraping.md +84 -84
  99. package/dist/extensions/builtin/browser/agent-workspace/domain-skills/youtube/scraping.md +418 -418
  100. package/dist/extensions/builtin/browser/agent-workspace/domain-skills/zillow/scraping.md +433 -433
  101. package/dist/extensions/builtin/browser/browser.md +73 -73
  102. package/dist/extensions/builtin/browser/install.md +142 -142
  103. package/dist/extensions/builtin/browser/interaction-skills/connection.md +48 -48
  104. package/dist/extensions/builtin/browser/interaction-skills/cookies.md +3 -3
  105. package/dist/extensions/builtin/browser/interaction-skills/cross-origin-iframes.md +3 -3
  106. package/dist/extensions/builtin/browser/interaction-skills/dialogs.md +64 -64
  107. package/dist/extensions/builtin/browser/interaction-skills/downloads.md +3 -3
  108. package/dist/extensions/builtin/browser/interaction-skills/drag-and-drop.md +3 -3
  109. package/dist/extensions/builtin/browser/interaction-skills/dropdowns.md +3 -3
  110. package/dist/extensions/builtin/browser/interaction-skills/iframes.md +3 -3
  111. package/dist/extensions/builtin/browser/interaction-skills/network-requests.md +3 -3
  112. package/dist/extensions/builtin/browser/interaction-skills/print-as-pdf.md +3 -3
  113. package/dist/extensions/builtin/browser/interaction-skills/profile-sync.md +90 -90
  114. package/dist/extensions/builtin/browser/interaction-skills/screenshots.md +17 -17
  115. package/dist/extensions/builtin/browser/interaction-skills/scrolling.md +3 -3
  116. package/dist/extensions/builtin/browser/interaction-skills/shadow-dom.md +3 -3
  117. package/dist/extensions/builtin/browser/interaction-skills/tabs.md +69 -69
  118. package/dist/extensions/builtin/browser/interaction-skills/uploads.md +1 -1
  119. package/dist/extensions/builtin/browser/interaction-skills/viewport.md +3 -3
  120. package/dist/extensions/builtin/browser/src/browser_harness/AGENT.md +15 -15
  121. package/dist/extensions/builtin/browser/src/browser_harness/__init__.py +8 -8
  122. package/dist/extensions/builtin/browser/src/browser_harness/_ipc.py +90 -90
  123. package/dist/extensions/builtin/browser/src/browser_harness/admin.py +722 -722
  124. package/dist/extensions/builtin/browser/src/browser_harness/daemon.py +328 -328
  125. package/dist/extensions/builtin/browser/src/browser_harness/helpers.py +396 -396
  126. package/dist/extensions/builtin/browser/src/browser_harness/run.py +103 -103
  127. package/dist/extensions/builtin/discipline/skills/brainstorming/SKILL.md +33 -33
  128. package/dist/extensions/builtin/discipline/skills/executing-plans/SKILL.md +25 -25
  129. package/dist/extensions/builtin/discipline/skills/finishing-development-branch/SKILL.md +25 -25
  130. package/dist/extensions/builtin/discipline/skills/receiving-code-review/SKILL.md +22 -22
  131. package/dist/extensions/builtin/discipline/skills/requesting-code-review/SKILL.md +31 -31
  132. package/dist/extensions/builtin/discipline/skills/systematic-debugging/SKILL.md +28 -28
  133. package/dist/extensions/builtin/discipline/skills/test-driven-development/SKILL.md +32 -32
  134. package/dist/extensions/builtin/discipline/skills/using-git-worktrees/SKILL.md +25 -25
  135. package/dist/extensions/builtin/discipline/skills/verification-before-completion/SKILL.md +27 -27
  136. package/dist/extensions/builtin/discipline/skills/writing-plans/SKILL.md +26 -26
  137. package/dist/extensions/builtin/goal/README.md +67 -67
  138. package/dist/extensions/builtin/grub/README.md +112 -112
  139. package/dist/extensions/builtin/link-world/agent-workspace/README.md +16 -16
  140. package/dist/extensions/builtin/link-world/internet-search/internet-search.md +65 -65
  141. package/dist/extensions/builtin/link-world/link-world-agent.md +82 -82
  142. package/dist/extensions/builtin/link-world/linkworld.md +313 -313
  143. package/dist/extensions/builtin/link-world/network-routing/network-routing.md +67 -67
  144. package/dist/extensions/builtin/loop/README.md +92 -92
  145. package/dist/extensions/builtin/mcp/figma-design.md +68 -68
  146. package/dist/extensions/builtin/mcp/mcp-management.md +85 -85
  147. package/dist/extensions/builtin/recap/AGENT.md +15 -15
  148. package/dist/extensions/builtin/sal/README.md +72 -72
  149. package/dist/extensions/builtin/security-audit/README.md +289 -289
  150. package/dist/extensions/builtin/team/AGENT.md +112 -112
  151. package/dist/extensions/builtin/team/TESTING.md +299 -299
  152. package/dist/extensions/builtin/token-save/README.md +56 -56
  153. package/dist/extensions/optional/AGENT.md +10 -10
  154. package/dist/modes/interactive/interactive-mode.js +36 -36
  155. package/dist/modes/interactive/theme/dark.json +85 -85
  156. package/dist/modes/interactive/theme/light.json +84 -84
  157. package/dist/modes/interactive/theme/theme-schema.json +335 -335
  158. package/dist/modes/interactive/theme/warm.json +81 -81
  159. package/dist/node_modules/@pencil-agent/agent-core/dist/agent-loop.js +3 -2
  160. package/dist/node_modules/@pencil-agent/agent-core/dist/structured-adaptive-agent-loop.js +2 -1
  161. package/dist/node_modules/@pencil-agent/ai/dist/cli.js +0 -0
  162. package/docs/cc-agent-design.md +1297 -0
  163. package/docs/cc-tui-design.md +1333 -0
  164. package/docs/codex-goal-command-impl.md +1055 -1055
  165. package/docs/codex-goal-vs-grub.md +500 -500
  166. package/docs/custom-provider.md +27 -27
  167. package/docs/extensions.md +27 -27
  168. package/docs/keybindings.md +27 -27
  169. package/docs/loop /351/207/215/346/236/204/345/256/214/346/210/220/346/200/273/347/273/223.md" +250 -250
  170. package/docs/loop /351/207/215/346/236/204/345/256/214/346/210/220/346/212/245/345/221/212.md" +122 -122
  171. package/docs/loop /351/207/215/346/236/204/346/226/271/346/241/210.md" +1222 -1222
  172. package/docs/loop /351/207/215/346/236/204/346/226/271/346/241/210/345/256/236/347/216/260/346/212/245/345/221/212.md" +158 -158
  173. package/docs/loop /351/207/215/346/236/204/346/226/271/346/241/210/345/257/271/346/257/224/345/210/206/346/236/220.md" +128 -128
  174. package/docs/loop /351/207/215/346/236/204/350/256/241/345/210/222.md" +320 -320
  175. package/docs/loop-usage-examples.md +214 -214
  176. package/docs/models.md +27 -27
  177. package/docs/nanoPencil-/345/255/246/344/271/240/350/256/241/345/210/222.md +170 -0
  178. package/docs/packages.md +27 -27
  179. package/docs/pi-design-philosophy.md +457 -457
  180. package/docs/planmode.md +1987 -1987
  181. package/docs/prompt-templates.md +27 -27
  182. package/docs/providers.md +27 -27
  183. package/docs/scan-report.md +3820 -0
  184. package/docs/sdk.md +27 -27
  185. package/docs/skills.md +27 -27
  186. package/docs/themes.md +27 -27
  187. package/docs/tui.md +27 -27
  188. package/docs//345/257/271/346/240/207Claude-Code.md +1775 -0
  189. package/docs//351/230/277/351/207/214/345/267/264/345/267/264/350/264/242/346/212/245/345/210/206/346/236/220/344/271/246.md +261 -0
  190. package/package.json +190 -190
  191. package/docs/ACP/345/215/217/350/256/256/351/233/206/346/210/220/345/274/200/345/217/221/346/226/207/346/241/243.md +0 -851
  192. package/docs/SDK-TESTING.md +0 -364
  193. package/docs/mem-core/346/212/200/346/234/257/346/226/207/346/241/243.md +0 -593
  194. package/docs/startup-performance-optimization.md +0 -301
  195. package/docs//350/256/244/347/237/245/345/234/260/345/233/276.md +0 -47
@@ -1,398 +1,398 @@
1
- # Weather APIs — Data Extraction
2
-
3
- Three free, no-auth weather APIs tested: **wttr.in** (simplest), **Open-Meteo** (most complete), **weather.gov / NWS** (US only, official).
4
-
5
- All work with `http_get` — no browser needed.
6
-
7
- ## Do this first: pick your API
8
-
9
- | Goal | Best API | Latency | Notes |
10
- |------|----------|---------|-------|
11
- | Quick current + 3-day forecast, any city name | wttr.in `?format=j1` | ~800ms | US + international |
12
- | Rich hourly/daily/historical, any coordinates | Open-Meteo | ~700ms | 10K req/day free |
13
- | City name → coordinates | Open-Meteo geocoding | ~700ms | Use with Open-Meteo forecast |
14
- | Official US forecasts with PoP and text | weather.gov NWS | ~90ms /points + ~70ms /forecast | US only, 2-call flow |
15
-
16
- **Never use a browser for any of these APIs.** All return JSON over plain HTTP.
17
-
18
- ---
19
-
20
- ## Fastest approach: wttr.in one-call current + 3-day forecast
21
-
22
- ```python
23
- import json
24
- data = json.loads(http_get("https://wttr.in/San+Francisco?format=j1"))
25
-
26
- # Current conditions
27
- cc = data['current_condition'][0]
28
- print(cc['temp_F'], '°F /', cc['temp_C'], '°C') # '47', '8'
29
- print(cc['FeelsLikeF'], '°F feels like') # '46'
30
- print(cc['humidity'], '%') # '80'
31
- print(cc['windspeedMiles'], 'mph', cc['winddir16Point']) # '3', 'SW'
32
- print(cc['weatherDesc'][0]['value']) # 'Partly cloudy'
33
- print(cc['precipMM'], 'mm precip') # '0.0'
34
- print(cc['visibility'], 'km', cc['visibilityMiles'], 'mi')
35
- print(cc['pressure'], 'hPa', cc['pressureInches'], 'inHg')
36
- print(cc['uvIndex']) # '0'
37
- print(cc['cloudcover'], '%') # '50'
38
- print(cc['observation_time']) # '10:48 AM' (UTC)
39
- print(cc['localObsDateTime']) # '2026-04-18 03:34 AM' (local)
40
-
41
- # 3-day forecast (today + 2 more)
42
- for day in data['weather']:
43
- print(day['date'], day['maxtempF'], '/', day['mintempF'], '°F')
44
- # also: maxtempC, mintempC, avgtempF, avgtempC, sunHour, uvIndex, totalSnow_cm
45
- astro = day['astronomy'][0]
46
- print(' sunrise:', astro['sunrise'], 'sunset:', astro['sunset'])
47
- print(' moon:', astro['moon_phase'], astro['moon_illumination'], '%')
48
- # Hourly breakdown (8 entries per day, every 3 hours: time 0,300,600,...,2100)
49
- for h in day['hourly']:
50
- print(h['time'], h['tempF'], '°F', h['weatherDesc'][0]['value'])
51
- # time is '0','300','600',...,'2100' (not HH:MM)
52
- # also: chanceofrain, chanceofsnow, chanceofthunder, chanceoffog, humidity, etc.
53
-
54
- # Location info
55
- na = data['nearest_area'][0]
56
- print(na['areaName'][0]['value']) # 'San Francisco'
57
- print(na['country'][0]['value']) # 'United States of America'
58
- print(na['latitude'], na['longitude']) # '37.775', '-122.418' (strings)
59
- print(na['region'][0]['value']) # 'California'
60
- ```
61
-
62
- **Works with city names, coordinates, airport codes (`~SFO`), and zip codes.**
63
-
64
- ---
65
-
66
- ## Open-Meteo: most complete free weather API
67
-
68
- ### Step 1: city name → coordinates (geocoding)
69
-
70
- ```python
71
- import json
72
- geo = json.loads(http_get("https://geocoding-api.open-meteo.com/v1/search?name=Chicago&count=1"))
73
- city = geo['results'][0]
74
- lat = city['latitude'] # 41.85003
75
- lon = city['longitude'] # -87.65005
76
- tz = city['timezone'] # 'America/Chicago'
77
- # Also available: city['elevation'], city['country'], city['country_code'],
78
- # city['admin1'] (state/province), city['population']
79
- ```
80
-
81
- Always use `count=1` and take `results[0]` for unambiguous city names. For "San Francisco" `results[0]` is always the California city (pop 864K).
82
-
83
- ### Current conditions (extended — preferred over current_weather)
84
-
85
- ```python
86
- data = json.loads(http_get(
87
- f"https://api.open-meteo.com/v1/forecast"
88
- f"?latitude={lat}&longitude={lon}"
89
- f"&current=temperature_2m,relative_humidity_2m,apparent_temperature,"
90
- f"precipitation,weathercode,windspeed_10m,winddirection_10m,"
91
- f"uv_index,surface_pressure"
92
- f"&timezone={tz}"
93
- ))
94
-
95
- cur = data['current']
96
- units = data['current_units']
97
- # cur keys and units (all confirmed):
98
- # temperature_2m °C (or °F with &temperature_unit=fahrenheit)
99
- # relative_humidity_2m %
100
- # apparent_temperature °C
101
- # precipitation mm
102
- # weathercode WMO code int (see table below)
103
- # windspeed_10m km/h (or mph with &windspeed_unit=mph)
104
- # winddirection_10m °
105
- # uv_index (unitless float)
106
- # surface_pressure hPa
107
- # time ISO8601 local time (e.g. '2026-04-18T10:45')
108
- # interval 900 (seconds — 15-min update cadence)
109
-
110
- print(cur['temperature_2m'], units['temperature_2m']) # 8.7 °C
111
- print(cur['apparent_temperature']) # 6.6
112
- print(cur['relative_humidity_2m']) # 80
113
- print(cur['windspeed_10m'], cur['winddirection_10m']) # 6.1 242
114
- print(cur['weathercode']) # 0 = clear sky
115
- ```
116
-
117
- The older `&current_weather=true` param works too — returns `data['current_weather']` with only temperature, windspeed, winddirection, weathercode, time, is_day, interval.
118
-
119
- ### Hourly forecast
120
-
121
- ```python
122
- data = json.loads(http_get(
123
- f"https://api.open-meteo.com/v1/forecast"
124
- f"?latitude={lat}&longitude={lon}"
125
- f"&hourly=temperature_2m,dewpoint_2m,apparent_temperature,"
126
- f"precipitation_probability,precipitation,rain,showers,snowfall,snow_depth,"
127
- f"weathercode,cloudcover,visibility,windspeed_10m,winddirection_10m,"
128
- f"windgusts_10m,uv_index"
129
- f"&forecast_days=3&timezone={tz}"
130
- ))
131
-
132
- hourly = data['hourly']
133
- units = data['hourly_units']
134
- # hourly is a dict of parallel arrays, all same length
135
- # time entries: ISO8601 strings, one per hour ('2026-04-18T00:00', etc.)
136
- # 3 forecast days → 72 entries
137
-
138
- for i, t in enumerate(hourly['time'][:5]):
139
- print(t,
140
- hourly['temperature_2m'][i], units['temperature_2m'],
141
- hourly['precipitation_probability'][i], units['precipitation_probability'],
142
- hourly['windspeed_10m'][i], units['windspeed_10m'])
143
-
144
- # Confirmed units (all from live response):
145
- # temperature_2m °C dewpoint_2m °C
146
- # apparent_temperature °C precipitation_probability %
147
- # precipitation mm rain mm
148
- # showers mm snowfall cm
149
- # snow_depth m weathercode wmo code
150
- # cloudcover % visibility m (not km!)
151
- # windspeed_10m km/h winddirection_10m °
152
- # windgusts_10m km/h uv_index (unitless)
153
- ```
154
-
155
- `forecast_days` defaults to 7, max is 16.
156
-
157
- ### Daily forecast
158
-
159
- ```python
160
- data = json.loads(http_get(
161
- f"https://api.open-meteo.com/v1/forecast"
162
- f"?latitude={lat}&longitude={lon}"
163
- f"&daily=temperature_2m_max,temperature_2m_min,apparent_temperature_max,"
164
- f"apparent_temperature_min,precipitation_sum,rain_sum,snowfall_sum,"
165
- f"precipitation_hours,precipitation_probability_max,"
166
- f"windspeed_10m_max,windgusts_10m_max,winddirection_10m_dominant,"
167
- f"shortwave_radiation_sum,uv_index_max,sunrise,sunset"
168
- f"&timezone={tz}&forecast_days=7"
169
- ))
170
-
171
- daily = data['daily']
172
- units = data['daily_units']
173
- for i, date in enumerate(daily['time']):
174
- print(date,
175
- daily['temperature_2m_max'][i], '/', daily['temperature_2m_min'][i], units['temperature_2m_max'],
176
- f"precip={daily['precipitation_sum'][i]}{units['precipitation_sum']}",
177
- f"pop={daily['precipitation_probability_max'][i]}%",
178
- f"UV={daily['uv_index_max'][i]}",
179
- f"sunrise={daily['sunrise'][i]}",
180
- f"sunset={daily['sunset'][i]}")
181
- # sunrise/sunset are ISO8601 local datetimes ('2026-04-18T06:29')
182
- # shortwave_radiation_sum in MJ/m²
183
- ```
184
-
185
- ### Historical data (archive API)
186
-
187
- Different subdomain — `archive-api.open-meteo.com`:
188
-
189
- ```python
190
- data = json.loads(http_get(
191
- "https://archive-api.open-meteo.com/v1/archive"
192
- "?latitude=37.7749&longitude=-122.4194"
193
- "&start_date=2024-01-01&end_date=2024-01-07"
194
- "&daily=temperature_2m_max,precipitation_sum"
195
- "&timezone=America/Los_Angeles"
196
- ))
197
- # Returns same structure as forecast — daily dict of parallel arrays
198
- # Hourly also works: &hourly=temperature_2m,precipitation,weathercode
199
- # Data goes back to 1940 for most locations
200
- ```
201
-
202
- ### Unit overrides
203
-
204
- All unit conversions are server-side — just add params:
205
-
206
- ```
207
- &temperature_unit=fahrenheit # default: celsius
208
- &windspeed_unit=mph # default: kmh (also: ms, kn)
209
- &precipitation_unit=inch # default: mm
210
- ```
211
-
212
- ---
213
-
214
- ## weather.gov NWS (US only — 2-call flow)
215
-
216
- Required for official NWS text forecasts with probability-of-precipitation text and storm warnings.
217
-
218
- ```python
219
- import json, urllib.request, gzip
220
-
221
- def nws_get(url):
222
- """NWS requires a descriptive User-Agent or returns 403."""
223
- h = {
224
- "User-Agent": "(myapp.example.com, contact@example.com)",
225
- "Accept": "application/geo+json",
226
- }
227
- req = urllib.request.Request(url, headers=h)
228
- with urllib.request.urlopen(req, timeout=20) as r:
229
- data = r.read()
230
- if r.headers.get("Content-Encoding") == "gzip":
231
- data = gzip.decompress(data)
232
- return data.decode()
233
-
234
- # Call 1: resolve lat/lon to forecast office + grid cell (~90ms)
235
- pts = json.loads(nws_get("https://api.weather.gov/points/37.7749,-122.4194"))
236
- prop = pts['properties']
237
- office = prop['gridId'] # 'MTR'
238
- gx = prop['gridX'] # 85
239
- gy = prop['gridY'] # 105
240
- forecast_url = prop['forecast'] # 7-day
241
- hourly_url = prop['forecastHourly'] # hourly
242
-
243
- # Also available from /points: prop['timeZone'], prop['observationStations'],
244
- # prop['relativeLocation']['properties']['city'] and ['state']
245
-
246
- # Call 2: 7-day forecast (14 half-day periods) (~70ms)
247
- fc = json.loads(nws_get(forecast_url))
248
- for p in fc['properties']['periods']:
249
- print(p['name'], # 'Saturday', 'Saturday Night', 'Sunday', ...
250
- p['temperature'], p['temperatureUnit'], # 74 F
251
- p['windSpeed'], p['windDirection'], # '6 to 14 mph' 'SW'
252
- p['shortForecast'], # 'Mostly Sunny'
253
- p['probabilityOfPrecipitation']['value'], # 0 (integer percent)
254
- p['isDaytime']) # True/False
255
- # p['detailedForecast'] — plain English paragraph, e.g.
256
- # 'Sunny, with a high near 74. Southwest wind 6 to 14 mph.'
257
-
258
- # Hourly (156 hours out — ~6.5 days)
259
- fch = json.loads(nws_get(hourly_url))
260
- for p in fch['properties']['periods'][:5]:
261
- print(p['startTime'], # '2026-04-18T03:00:00-07:00'
262
- p['temperature'], '°F',
263
- p['shortForecast'],
264
- p['windSpeed'],
265
- f"humidity={p['relativeHumidity']['value']}%",
266
- f"dewpoint={p['dewpoint']['value']:.1f}°C")
267
- ```
268
-
269
- `/points` response is cached `max-age=20500` (~5.7 hours) at the CDN — safe to call once per session and reuse grid coordinates.
270
-
271
- ---
272
-
273
- ## WMO weather code table (Open-Meteo `weathercode`)
274
-
275
- ```python
276
- WMO_CODES = {
277
- 0: "Clear sky",
278
- 1: "Mainly clear", 2: "Partly cloudy", 3: "Overcast",
279
- 45: "Fog", 48: "Icy fog",
280
- 51: "Light drizzle", 53: "Moderate drizzle", 55: "Dense drizzle",
281
- 61: "Slight rain", 63: "Moderate rain", 65: "Heavy rain",
282
- 71: "Slight snow", 73: "Moderate snow", 75: "Heavy snow",
283
- 77: "Snow grains",
284
- 80: "Slight rain showers", 81: "Moderate rain showers", 82: "Violent rain showers",
285
- 85: "Slight snow showers", 86: "Heavy snow showers",
286
- 95: "Thunderstorm",
287
- 96: "Thunderstorm with slight hail", 99: "Thunderstorm with heavy hail",
288
- }
289
-
290
- def wmo_desc(code):
291
- return WMO_CODES.get(code, f"Unknown code {code}")
292
- ```
293
-
294
- ---
295
-
296
- ## Complete end-to-end pattern: city name → rich forecast
297
-
298
- ```python
299
- import json
300
-
301
- def get_weather(city: str) -> dict:
302
- """City name → current + 7-day daily forecast via Open-Meteo."""
303
- # 1. Geocode
304
- geo = json.loads(http_get(
305
- f"https://geocoding-api.open-meteo.com/v1/search?name={city.replace(' ', '+')}&count=1"
306
- ))
307
- if not geo.get('results'):
308
- raise ValueError(f"City not found: {city}")
309
- loc = geo['results'][0]
310
- lat, lon, tz = loc['latitude'], loc['longitude'], loc['timezone']
311
-
312
- # 2. Forecast (single call: current + daily)
313
- data = json.loads(http_get(
314
- f"https://api.open-meteo.com/v1/forecast"
315
- f"?latitude={lat}&longitude={lon}"
316
- f"&current=temperature_2m,relative_humidity_2m,apparent_temperature,"
317
- f"precipitation,weathercode,windspeed_10m,winddirection_10m,uv_index"
318
- f"&daily=temperature_2m_max,temperature_2m_min,precipitation_sum,"
319
- f"precipitation_probability_max,weathercode,sunrise,sunset"
320
- f"&timezone={tz}&forecast_days=7"
321
- ))
322
- return {"location": loc, "current": data['current'],
323
- "daily": data['daily'], "units": {
324
- "current": data['current_units'],
325
- "daily": data['daily_units'],
326
- }}
327
-
328
- result = get_weather("Tokyo")
329
- cur = result['current']
330
- print(f"{result['location']['name']}: {cur['temperature_2m']}°C feels like {cur['apparent_temperature']}°C")
331
- print(f"Humidity {cur['relative_humidity_2m']}%, wind {cur['windspeed_10m']} km/h")
332
- ```
333
-
334
- Total: 2 API calls, ~1400ms combined.
335
-
336
- ---
337
-
338
- ## Gotchas
339
-
340
- **wttr.in returns HTML (or ANSI art) instead of JSON if you forget `?format=j1`.**
341
- The `?format=j1` suffix is mandatory for JSON. Without it:
342
- - Browser `User-Agent` → full HTML page (~21KB)
343
- - `curl`/`Wget` User-Agent → ANSI escape-code ASCII art (~500B)
344
- Neither is parseable as JSON.
345
-
346
- **wttr.in text formats require a non-browser User-Agent.**
347
- `http_get()` sends `Mozilla/5.0` — wttr.in responds with an HTML page for `?format=%t`, `?format=3`, `?format=4`.
348
- Use `Wget/1.21` (or any non-browser UA) for text format endpoints:
349
- ```python
350
- import urllib.request, gzip
351
-
352
- def http_get_wttr(url):
353
- req = urllib.request.Request(url, headers={"User-Agent": "Wget/1.21", "Accept": "*/*"})
354
- with urllib.request.urlopen(req, timeout=20) as r:
355
- data = r.read()
356
- if r.headers.get("Content-Encoding") == "gzip":
357
- data = gzip.decompress(data)
358
- return data.decode()
359
-
360
- # Text format tokens (URL-encode %): %25l=location, %25C=condition desc,
361
- # %25t=temp, %25f=feels-like, %25h=humidity, %25w=wind
362
- print(http_get_wttr("https://wttr.in/London?format=%25t")) # '+55°F'
363
- print(http_get_wttr("https://wttr.in/Tokyo?format=3")) # 'tokyo: ☀️ +69°F'
364
- print(http_get_wttr("https://wttr.in/Berlin?format=%25l:+%25C+%25t+(feels+%25f)+%25h+%25w"))
365
- # 'berlin: Sunny +65°F (feels +65°F) 42% ↖5mph'
366
- ```
367
-
368
- **wttr.in `format=j1` returns only 3 days** (today + 2). Use Open-Meteo for longer forecasts (up to 16 days).
369
-
370
- **wttr.in `nearest_area.areaName` is often wrong.** The returned area name is a reverse-geocoded neighborhood, not the city you queried (`"Mccormickville"` for Chicago, `"Lomita Park"` for SFO airport). Use `request[0].query` for what was actually resolved.
371
-
372
- **wttr.in `hourly[].time` is `'0'`, `'300'`, `'600'`...`'2100'`** — not HH:MM strings. Parse as `int(time) // 100` for hours.
373
-
374
- **wttr.in `weatherDesc` is a list**: `cc['weatherDesc'][0]['value']`, not a string. Same for `areaName`, `country`, `region`, `weatherIconUrl`.
375
-
376
- **wttr.in unknown city returns HTTP 500**, not 404 or a JSON error.
377
-
378
- **Open-Meteo default timezone is GMT.** Always pass `&timezone={tz}` or daily `sunrise`/`sunset` values will be in UTC, and daily buckets will be wrong.
379
-
380
- **Open-Meteo `visibility` is in metres** (not km). Divide by 1000 to get km.
381
-
382
- **Open-Meteo returns HTTP 400 with JSON error body on bad params:**
383
- ```json
384
- {"reason": "Latitude must be in range of -90 to 90°. Given: 999.0.", "error": true}
385
- ```
386
- `http_get()` raises an exception on 4xx — catch `urllib.error.HTTPError` and read `e.read()` (may be gzip-compressed) for the reason.
387
-
388
- **weather.gov requires a descriptive `User-Agent`.** The NWS API blocks generic `python-urllib` or `Mozilla/5.0` agents sporadically. Always set `User-Agent: (yourapp.com, your@email.com)` or use your actual app name.
389
-
390
- **weather.gov is US-only.** `/points/{lat},{lon}` returns HTTP 404 for coordinates outside the US (including territories like Puerto Rico for some grid edges). Fall back to Open-Meteo for non-US locations.
391
-
392
- **weather.gov `windSpeed` is a string like `"6 to 14 mph"`**, not a number. Parse with regex if you need a numeric value.
393
-
394
- **weather.gov `probabilityOfPrecipitation` is a dict**: `p['probabilityOfPrecipitation']['value']`, with `p['probabilityOfPrecipitation']['unitCode']` = `'wmoUnit:percent'`.
395
-
396
- **Open-Meteo rate limit: 10,000 requests/day on the free tier.** The geocoding API and forecast API count separately. No rate limit headers are returned — track usage yourself.
397
-
398
- **weather.gov /points response is heavily cached** (`Cache-Control: public, max-age=20500`). Store the office/gridX/gridY and reuse — only call `/points` once per location.
1
+ # Weather APIs — Data Extraction
2
+
3
+ Three free, no-auth weather APIs tested: **wttr.in** (simplest), **Open-Meteo** (most complete), **weather.gov / NWS** (US only, official).
4
+
5
+ All work with `http_get` — no browser needed.
6
+
7
+ ## Do this first: pick your API
8
+
9
+ | Goal | Best API | Latency | Notes |
10
+ |------|----------|---------|-------|
11
+ | Quick current + 3-day forecast, any city name | wttr.in `?format=j1` | ~800ms | US + international |
12
+ | Rich hourly/daily/historical, any coordinates | Open-Meteo | ~700ms | 10K req/day free |
13
+ | City name → coordinates | Open-Meteo geocoding | ~700ms | Use with Open-Meteo forecast |
14
+ | Official US forecasts with PoP and text | weather.gov NWS | ~90ms /points + ~70ms /forecast | US only, 2-call flow |
15
+
16
+ **Never use a browser for any of these APIs.** All return JSON over plain HTTP.
17
+
18
+ ---
19
+
20
+ ## Fastest approach: wttr.in one-call current + 3-day forecast
21
+
22
+ ```python
23
+ import json
24
+ data = json.loads(http_get("https://wttr.in/San+Francisco?format=j1"))
25
+
26
+ # Current conditions
27
+ cc = data['current_condition'][0]
28
+ print(cc['temp_F'], '°F /', cc['temp_C'], '°C') # '47', '8'
29
+ print(cc['FeelsLikeF'], '°F feels like') # '46'
30
+ print(cc['humidity'], '%') # '80'
31
+ print(cc['windspeedMiles'], 'mph', cc['winddir16Point']) # '3', 'SW'
32
+ print(cc['weatherDesc'][0]['value']) # 'Partly cloudy'
33
+ print(cc['precipMM'], 'mm precip') # '0.0'
34
+ print(cc['visibility'], 'km', cc['visibilityMiles'], 'mi')
35
+ print(cc['pressure'], 'hPa', cc['pressureInches'], 'inHg')
36
+ print(cc['uvIndex']) # '0'
37
+ print(cc['cloudcover'], '%') # '50'
38
+ print(cc['observation_time']) # '10:48 AM' (UTC)
39
+ print(cc['localObsDateTime']) # '2026-04-18 03:34 AM' (local)
40
+
41
+ # 3-day forecast (today + 2 more)
42
+ for day in data['weather']:
43
+ print(day['date'], day['maxtempF'], '/', day['mintempF'], '°F')
44
+ # also: maxtempC, mintempC, avgtempF, avgtempC, sunHour, uvIndex, totalSnow_cm
45
+ astro = day['astronomy'][0]
46
+ print(' sunrise:', astro['sunrise'], 'sunset:', astro['sunset'])
47
+ print(' moon:', astro['moon_phase'], astro['moon_illumination'], '%')
48
+ # Hourly breakdown (8 entries per day, every 3 hours: time 0,300,600,...,2100)
49
+ for h in day['hourly']:
50
+ print(h['time'], h['tempF'], '°F', h['weatherDesc'][0]['value'])
51
+ # time is '0','300','600',...,'2100' (not HH:MM)
52
+ # also: chanceofrain, chanceofsnow, chanceofthunder, chanceoffog, humidity, etc.
53
+
54
+ # Location info
55
+ na = data['nearest_area'][0]
56
+ print(na['areaName'][0]['value']) # 'San Francisco'
57
+ print(na['country'][0]['value']) # 'United States of America'
58
+ print(na['latitude'], na['longitude']) # '37.775', '-122.418' (strings)
59
+ print(na['region'][0]['value']) # 'California'
60
+ ```
61
+
62
+ **Works with city names, coordinates, airport codes (`~SFO`), and zip codes.**
63
+
64
+ ---
65
+
66
+ ## Open-Meteo: most complete free weather API
67
+
68
+ ### Step 1: city name → coordinates (geocoding)
69
+
70
+ ```python
71
+ import json
72
+ geo = json.loads(http_get("https://geocoding-api.open-meteo.com/v1/search?name=Chicago&count=1"))
73
+ city = geo['results'][0]
74
+ lat = city['latitude'] # 41.85003
75
+ lon = city['longitude'] # -87.65005
76
+ tz = city['timezone'] # 'America/Chicago'
77
+ # Also available: city['elevation'], city['country'], city['country_code'],
78
+ # city['admin1'] (state/province), city['population']
79
+ ```
80
+
81
+ Always use `count=1` and take `results[0]` for unambiguous city names. For "San Francisco" `results[0]` is always the California city (pop 864K).
82
+
83
+ ### Current conditions (extended — preferred over current_weather)
84
+
85
+ ```python
86
+ data = json.loads(http_get(
87
+ f"https://api.open-meteo.com/v1/forecast"
88
+ f"?latitude={lat}&longitude={lon}"
89
+ f"&current=temperature_2m,relative_humidity_2m,apparent_temperature,"
90
+ f"precipitation,weathercode,windspeed_10m,winddirection_10m,"
91
+ f"uv_index,surface_pressure"
92
+ f"&timezone={tz}"
93
+ ))
94
+
95
+ cur = data['current']
96
+ units = data['current_units']
97
+ # cur keys and units (all confirmed):
98
+ # temperature_2m °C (or °F with &temperature_unit=fahrenheit)
99
+ # relative_humidity_2m %
100
+ # apparent_temperature °C
101
+ # precipitation mm
102
+ # weathercode WMO code int (see table below)
103
+ # windspeed_10m km/h (or mph with &windspeed_unit=mph)
104
+ # winddirection_10m °
105
+ # uv_index (unitless float)
106
+ # surface_pressure hPa
107
+ # time ISO8601 local time (e.g. '2026-04-18T10:45')
108
+ # interval 900 (seconds — 15-min update cadence)
109
+
110
+ print(cur['temperature_2m'], units['temperature_2m']) # 8.7 °C
111
+ print(cur['apparent_temperature']) # 6.6
112
+ print(cur['relative_humidity_2m']) # 80
113
+ print(cur['windspeed_10m'], cur['winddirection_10m']) # 6.1 242
114
+ print(cur['weathercode']) # 0 = clear sky
115
+ ```
116
+
117
+ The older `&current_weather=true` param works too — returns `data['current_weather']` with only temperature, windspeed, winddirection, weathercode, time, is_day, interval.
118
+
119
+ ### Hourly forecast
120
+
121
+ ```python
122
+ data = json.loads(http_get(
123
+ f"https://api.open-meteo.com/v1/forecast"
124
+ f"?latitude={lat}&longitude={lon}"
125
+ f"&hourly=temperature_2m,dewpoint_2m,apparent_temperature,"
126
+ f"precipitation_probability,precipitation,rain,showers,snowfall,snow_depth,"
127
+ f"weathercode,cloudcover,visibility,windspeed_10m,winddirection_10m,"
128
+ f"windgusts_10m,uv_index"
129
+ f"&forecast_days=3&timezone={tz}"
130
+ ))
131
+
132
+ hourly = data['hourly']
133
+ units = data['hourly_units']
134
+ # hourly is a dict of parallel arrays, all same length
135
+ # time entries: ISO8601 strings, one per hour ('2026-04-18T00:00', etc.)
136
+ # 3 forecast days → 72 entries
137
+
138
+ for i, t in enumerate(hourly['time'][:5]):
139
+ print(t,
140
+ hourly['temperature_2m'][i], units['temperature_2m'],
141
+ hourly['precipitation_probability'][i], units['precipitation_probability'],
142
+ hourly['windspeed_10m'][i], units['windspeed_10m'])
143
+
144
+ # Confirmed units (all from live response):
145
+ # temperature_2m °C dewpoint_2m °C
146
+ # apparent_temperature °C precipitation_probability %
147
+ # precipitation mm rain mm
148
+ # showers mm snowfall cm
149
+ # snow_depth m weathercode wmo code
150
+ # cloudcover % visibility m (not km!)
151
+ # windspeed_10m km/h winddirection_10m °
152
+ # windgusts_10m km/h uv_index (unitless)
153
+ ```
154
+
155
+ `forecast_days` defaults to 7, max is 16.
156
+
157
+ ### Daily forecast
158
+
159
+ ```python
160
+ data = json.loads(http_get(
161
+ f"https://api.open-meteo.com/v1/forecast"
162
+ f"?latitude={lat}&longitude={lon}"
163
+ f"&daily=temperature_2m_max,temperature_2m_min,apparent_temperature_max,"
164
+ f"apparent_temperature_min,precipitation_sum,rain_sum,snowfall_sum,"
165
+ f"precipitation_hours,precipitation_probability_max,"
166
+ f"windspeed_10m_max,windgusts_10m_max,winddirection_10m_dominant,"
167
+ f"shortwave_radiation_sum,uv_index_max,sunrise,sunset"
168
+ f"&timezone={tz}&forecast_days=7"
169
+ ))
170
+
171
+ daily = data['daily']
172
+ units = data['daily_units']
173
+ for i, date in enumerate(daily['time']):
174
+ print(date,
175
+ daily['temperature_2m_max'][i], '/', daily['temperature_2m_min'][i], units['temperature_2m_max'],
176
+ f"precip={daily['precipitation_sum'][i]}{units['precipitation_sum']}",
177
+ f"pop={daily['precipitation_probability_max'][i]}%",
178
+ f"UV={daily['uv_index_max'][i]}",
179
+ f"sunrise={daily['sunrise'][i]}",
180
+ f"sunset={daily['sunset'][i]}")
181
+ # sunrise/sunset are ISO8601 local datetimes ('2026-04-18T06:29')
182
+ # shortwave_radiation_sum in MJ/m²
183
+ ```
184
+
185
+ ### Historical data (archive API)
186
+
187
+ Different subdomain — `archive-api.open-meteo.com`:
188
+
189
+ ```python
190
+ data = json.loads(http_get(
191
+ "https://archive-api.open-meteo.com/v1/archive"
192
+ "?latitude=37.7749&longitude=-122.4194"
193
+ "&start_date=2024-01-01&end_date=2024-01-07"
194
+ "&daily=temperature_2m_max,precipitation_sum"
195
+ "&timezone=America/Los_Angeles"
196
+ ))
197
+ # Returns same structure as forecast — daily dict of parallel arrays
198
+ # Hourly also works: &hourly=temperature_2m,precipitation,weathercode
199
+ # Data goes back to 1940 for most locations
200
+ ```
201
+
202
+ ### Unit overrides
203
+
204
+ All unit conversions are server-side — just add params:
205
+
206
+ ```
207
+ &temperature_unit=fahrenheit # default: celsius
208
+ &windspeed_unit=mph # default: kmh (also: ms, kn)
209
+ &precipitation_unit=inch # default: mm
210
+ ```
211
+
212
+ ---
213
+
214
+ ## weather.gov NWS (US only — 2-call flow)
215
+
216
+ Required for official NWS text forecasts with probability-of-precipitation text and storm warnings.
217
+
218
+ ```python
219
+ import json, urllib.request, gzip
220
+
221
+ def nws_get(url):
222
+ """NWS requires a descriptive User-Agent or returns 403."""
223
+ h = {
224
+ "User-Agent": "(myapp.example.com, contact@example.com)",
225
+ "Accept": "application/geo+json",
226
+ }
227
+ req = urllib.request.Request(url, headers=h)
228
+ with urllib.request.urlopen(req, timeout=20) as r:
229
+ data = r.read()
230
+ if r.headers.get("Content-Encoding") == "gzip":
231
+ data = gzip.decompress(data)
232
+ return data.decode()
233
+
234
+ # Call 1: resolve lat/lon to forecast office + grid cell (~90ms)
235
+ pts = json.loads(nws_get("https://api.weather.gov/points/37.7749,-122.4194"))
236
+ prop = pts['properties']
237
+ office = prop['gridId'] # 'MTR'
238
+ gx = prop['gridX'] # 85
239
+ gy = prop['gridY'] # 105
240
+ forecast_url = prop['forecast'] # 7-day
241
+ hourly_url = prop['forecastHourly'] # hourly
242
+
243
+ # Also available from /points: prop['timeZone'], prop['observationStations'],
244
+ # prop['relativeLocation']['properties']['city'] and ['state']
245
+
246
+ # Call 2: 7-day forecast (14 half-day periods) (~70ms)
247
+ fc = json.loads(nws_get(forecast_url))
248
+ for p in fc['properties']['periods']:
249
+ print(p['name'], # 'Saturday', 'Saturday Night', 'Sunday', ...
250
+ p['temperature'], p['temperatureUnit'], # 74 F
251
+ p['windSpeed'], p['windDirection'], # '6 to 14 mph' 'SW'
252
+ p['shortForecast'], # 'Mostly Sunny'
253
+ p['probabilityOfPrecipitation']['value'], # 0 (integer percent)
254
+ p['isDaytime']) # True/False
255
+ # p['detailedForecast'] — plain English paragraph, e.g.
256
+ # 'Sunny, with a high near 74. Southwest wind 6 to 14 mph.'
257
+
258
+ # Hourly (156 hours out — ~6.5 days)
259
+ fch = json.loads(nws_get(hourly_url))
260
+ for p in fch['properties']['periods'][:5]:
261
+ print(p['startTime'], # '2026-04-18T03:00:00-07:00'
262
+ p['temperature'], '°F',
263
+ p['shortForecast'],
264
+ p['windSpeed'],
265
+ f"humidity={p['relativeHumidity']['value']}%",
266
+ f"dewpoint={p['dewpoint']['value']:.1f}°C")
267
+ ```
268
+
269
+ `/points` response is cached `max-age=20500` (~5.7 hours) at the CDN — safe to call once per session and reuse grid coordinates.
270
+
271
+ ---
272
+
273
+ ## WMO weather code table (Open-Meteo `weathercode`)
274
+
275
+ ```python
276
+ WMO_CODES = {
277
+ 0: "Clear sky",
278
+ 1: "Mainly clear", 2: "Partly cloudy", 3: "Overcast",
279
+ 45: "Fog", 48: "Icy fog",
280
+ 51: "Light drizzle", 53: "Moderate drizzle", 55: "Dense drizzle",
281
+ 61: "Slight rain", 63: "Moderate rain", 65: "Heavy rain",
282
+ 71: "Slight snow", 73: "Moderate snow", 75: "Heavy snow",
283
+ 77: "Snow grains",
284
+ 80: "Slight rain showers", 81: "Moderate rain showers", 82: "Violent rain showers",
285
+ 85: "Slight snow showers", 86: "Heavy snow showers",
286
+ 95: "Thunderstorm",
287
+ 96: "Thunderstorm with slight hail", 99: "Thunderstorm with heavy hail",
288
+ }
289
+
290
+ def wmo_desc(code):
291
+ return WMO_CODES.get(code, f"Unknown code {code}")
292
+ ```
293
+
294
+ ---
295
+
296
+ ## Complete end-to-end pattern: city name → rich forecast
297
+
298
+ ```python
299
+ import json
300
+
301
+ def get_weather(city: str) -> dict:
302
+ """City name → current + 7-day daily forecast via Open-Meteo."""
303
+ # 1. Geocode
304
+ geo = json.loads(http_get(
305
+ f"https://geocoding-api.open-meteo.com/v1/search?name={city.replace(' ', '+')}&count=1"
306
+ ))
307
+ if not geo.get('results'):
308
+ raise ValueError(f"City not found: {city}")
309
+ loc = geo['results'][0]
310
+ lat, lon, tz = loc['latitude'], loc['longitude'], loc['timezone']
311
+
312
+ # 2. Forecast (single call: current + daily)
313
+ data = json.loads(http_get(
314
+ f"https://api.open-meteo.com/v1/forecast"
315
+ f"?latitude={lat}&longitude={lon}"
316
+ f"&current=temperature_2m,relative_humidity_2m,apparent_temperature,"
317
+ f"precipitation,weathercode,windspeed_10m,winddirection_10m,uv_index"
318
+ f"&daily=temperature_2m_max,temperature_2m_min,precipitation_sum,"
319
+ f"precipitation_probability_max,weathercode,sunrise,sunset"
320
+ f"&timezone={tz}&forecast_days=7"
321
+ ))
322
+ return {"location": loc, "current": data['current'],
323
+ "daily": data['daily'], "units": {
324
+ "current": data['current_units'],
325
+ "daily": data['daily_units'],
326
+ }}
327
+
328
+ result = get_weather("Tokyo")
329
+ cur = result['current']
330
+ print(f"{result['location']['name']}: {cur['temperature_2m']}°C feels like {cur['apparent_temperature']}°C")
331
+ print(f"Humidity {cur['relative_humidity_2m']}%, wind {cur['windspeed_10m']} km/h")
332
+ ```
333
+
334
+ Total: 2 API calls, ~1400ms combined.
335
+
336
+ ---
337
+
338
+ ## Gotchas
339
+
340
+ **wttr.in returns HTML (or ANSI art) instead of JSON if you forget `?format=j1`.**
341
+ The `?format=j1` suffix is mandatory for JSON. Without it:
342
+ - Browser `User-Agent` → full HTML page (~21KB)
343
+ - `curl`/`Wget` User-Agent → ANSI escape-code ASCII art (~500B)
344
+ Neither is parseable as JSON.
345
+
346
+ **wttr.in text formats require a non-browser User-Agent.**
347
+ `http_get()` sends `Mozilla/5.0` — wttr.in responds with an HTML page for `?format=%t`, `?format=3`, `?format=4`.
348
+ Use `Wget/1.21` (or any non-browser UA) for text format endpoints:
349
+ ```python
350
+ import urllib.request, gzip
351
+
352
+ def http_get_wttr(url):
353
+ req = urllib.request.Request(url, headers={"User-Agent": "Wget/1.21", "Accept": "*/*"})
354
+ with urllib.request.urlopen(req, timeout=20) as r:
355
+ data = r.read()
356
+ if r.headers.get("Content-Encoding") == "gzip":
357
+ data = gzip.decompress(data)
358
+ return data.decode()
359
+
360
+ # Text format tokens (URL-encode %): %25l=location, %25C=condition desc,
361
+ # %25t=temp, %25f=feels-like, %25h=humidity, %25w=wind
362
+ print(http_get_wttr("https://wttr.in/London?format=%25t")) # '+55°F'
363
+ print(http_get_wttr("https://wttr.in/Tokyo?format=3")) # 'tokyo: ☀️ +69°F'
364
+ print(http_get_wttr("https://wttr.in/Berlin?format=%25l:+%25C+%25t+(feels+%25f)+%25h+%25w"))
365
+ # 'berlin: Sunny +65°F (feels +65°F) 42% ↖5mph'
366
+ ```
367
+
368
+ **wttr.in `format=j1` returns only 3 days** (today + 2). Use Open-Meteo for longer forecasts (up to 16 days).
369
+
370
+ **wttr.in `nearest_area.areaName` is often wrong.** The returned area name is a reverse-geocoded neighborhood, not the city you queried (`"Mccormickville"` for Chicago, `"Lomita Park"` for SFO airport). Use `request[0].query` for what was actually resolved.
371
+
372
+ **wttr.in `hourly[].time` is `'0'`, `'300'`, `'600'`...`'2100'`** — not HH:MM strings. Parse as `int(time) // 100` for hours.
373
+
374
+ **wttr.in `weatherDesc` is a list**: `cc['weatherDesc'][0]['value']`, not a string. Same for `areaName`, `country`, `region`, `weatherIconUrl`.
375
+
376
+ **wttr.in unknown city returns HTTP 500**, not 404 or a JSON error.
377
+
378
+ **Open-Meteo default timezone is GMT.** Always pass `&timezone={tz}` or daily `sunrise`/`sunset` values will be in UTC, and daily buckets will be wrong.
379
+
380
+ **Open-Meteo `visibility` is in metres** (not km). Divide by 1000 to get km.
381
+
382
+ **Open-Meteo returns HTTP 400 with JSON error body on bad params:**
383
+ ```json
384
+ {"reason": "Latitude must be in range of -90 to 90°. Given: 999.0.", "error": true}
385
+ ```
386
+ `http_get()` raises an exception on 4xx — catch `urllib.error.HTTPError` and read `e.read()` (may be gzip-compressed) for the reason.
387
+
388
+ **weather.gov requires a descriptive `User-Agent`.** The NWS API blocks generic `python-urllib` or `Mozilla/5.0` agents sporadically. Always set `User-Agent: (yourapp.com, your@email.com)` or use your actual app name.
389
+
390
+ **weather.gov is US-only.** `/points/{lat},{lon}` returns HTTP 404 for coordinates outside the US (including territories like Puerto Rico for some grid edges). Fall back to Open-Meteo for non-US locations.
391
+
392
+ **weather.gov `windSpeed` is a string like `"6 to 14 mph"`**, not a number. Parse with regex if you need a numeric value.
393
+
394
+ **weather.gov `probabilityOfPrecipitation` is a dict**: `p['probabilityOfPrecipitation']['value']`, with `p['probabilityOfPrecipitation']['unitCode']` = `'wmoUnit:percent'`.
395
+
396
+ **Open-Meteo rate limit: 10,000 requests/day on the free tier.** The geocoding API and forecast API count separately. No rate limit headers are returned — track usage yourself.
397
+
398
+ **weather.gov /points response is heavily cached** (`Cache-Control: public, max-age=20500`). Store the office/gridX/gridY and reuse — only call `/points` once per location.