@pencil-agent/nano-pencil 2.0.0-beta.8 → 2.0.0

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 (241) 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/extensions-host/index.d.ts +1 -1
  7. package/dist/core/extensions-host/loader.js +1 -1
  8. package/dist/core/extensions-host/runner.d.ts +1 -0
  9. package/dist/core/extensions-host/runner.js +2 -2
  10. package/dist/core/extensions-host/types.d.ts +17 -22
  11. package/dist/core/lib/ai/src/types.d.ts +12 -2
  12. package/dist/core/persona/persona-manager.js +5 -2
  13. package/dist/core/runtime/agent-session.js +3 -3
  14. package/dist/core/runtime/extension-core-bindings.d.ts +1 -0
  15. package/dist/core/runtime/extension-core-bindings.js +2 -2
  16. package/dist/extensions/builtin/AGENT.md +115 -115
  17. package/dist/extensions/builtin/browser/AGENT.md +17 -17
  18. package/dist/extensions/builtin/browser/agent-workspace/agent_helpers.py +12 -12
  19. package/dist/extensions/builtin/browser/agent-workspace/domain-skills/amazon/product-search.md +198 -198
  20. package/dist/extensions/builtin/browser/agent-workspace/domain-skills/archive-org/scraping.md +341 -341
  21. package/dist/extensions/builtin/browser/agent-workspace/domain-skills/arxiv/scraping.md +311 -311
  22. package/dist/extensions/builtin/browser/agent-workspace/domain-skills/arxiv-bulk/scraping.md +333 -333
  23. package/dist/extensions/builtin/browser/agent-workspace/domain-skills/atlas/overview.md +70 -70
  24. package/dist/extensions/builtin/browser/agent-workspace/domain-skills/booking-com/scraping.md +578 -578
  25. package/dist/extensions/builtin/browser/agent-workspace/domain-skills/capterra/scraping.md +440 -440
  26. package/dist/extensions/builtin/browser/agent-workspace/domain-skills/centilebrain/generate-estimates.md +110 -110
  27. package/dist/extensions/builtin/browser/agent-workspace/domain-skills/coingecko/scraping.md +325 -325
  28. package/dist/extensions/builtin/browser/agent-workspace/domain-skills/coinmarketcap/scraping.md +463 -463
  29. package/dist/extensions/builtin/browser/agent-workspace/domain-skills/coursera/scraping.md +360 -360
  30. package/dist/extensions/builtin/browser/agent-workspace/domain-skills/craigslist/scraping.md +390 -390
  31. package/dist/extensions/builtin/browser/agent-workspace/domain-skills/crossref/scraping.md +568 -568
  32. package/dist/extensions/builtin/browser/agent-workspace/domain-skills/dev-to/scraping.md +323 -323
  33. package/dist/extensions/builtin/browser/agent-workspace/domain-skills/duckduckgo/scraping.md +349 -349
  34. package/dist/extensions/builtin/browser/agent-workspace/domain-skills/ebay/scraping.md +435 -435
  35. package/dist/extensions/builtin/browser/agent-workspace/domain-skills/etsy/scraping.md +506 -506
  36. package/dist/extensions/builtin/browser/agent-workspace/domain-skills/eventbrite/scraping.md +363 -363
  37. package/dist/extensions/builtin/browser/agent-workspace/domain-skills/expedia/automation.md +168 -168
  38. package/dist/extensions/builtin/browser/agent-workspace/domain-skills/facebook/groups.md +236 -236
  39. package/dist/extensions/builtin/browser/agent-workspace/domain-skills/facebook/pages.md +295 -295
  40. package/dist/extensions/builtin/browser/agent-workspace/domain-skills/framer/editor.md +108 -108
  41. package/dist/extensions/builtin/browser/agent-workspace/domain-skills/fred/scraping.md +493 -493
  42. package/dist/extensions/builtin/browser/agent-workspace/domain-skills/g2/scraping.md +580 -580
  43. package/dist/extensions/builtin/browser/agent-workspace/domain-skills/genius/scraping.md +511 -511
  44. package/dist/extensions/builtin/browser/agent-workspace/domain-skills/github/repo-actions.md +65 -65
  45. package/dist/extensions/builtin/browser/agent-workspace/domain-skills/github/scraping.md +184 -184
  46. package/dist/extensions/builtin/browser/agent-workspace/domain-skills/glassdoor/scraping.md +543 -543
  47. package/dist/extensions/builtin/browser/agent-workspace/domain-skills/gmail/compose.md +122 -122
  48. package/dist/extensions/builtin/browser/agent-workspace/domain-skills/goodreads/scraping.md +461 -461
  49. package/dist/extensions/builtin/browser/agent-workspace/domain-skills/gutenberg/scraping.md +383 -383
  50. package/dist/extensions/builtin/browser/agent-workspace/domain-skills/hackernews/scraping.md +243 -243
  51. package/dist/extensions/builtin/browser/agent-workspace/domain-skills/howlongtobeat/scraping.md +473 -473
  52. package/dist/extensions/builtin/browser/agent-workspace/domain-skills/imdb/scraping.md +271 -271
  53. package/dist/extensions/builtin/browser/agent-workspace/domain-skills/itch-io/scraping.md +436 -436
  54. package/dist/extensions/builtin/browser/agent-workspace/domain-skills/job-boards/indeed-glassdoor.md +1021 -1021
  55. package/dist/extensions/builtin/browser/agent-workspace/domain-skills/letterboxd/scraping.md +349 -349
  56. package/dist/extensions/builtin/browser/agent-workspace/domain-skills/linkedin/invitation-manager.md +109 -109
  57. package/dist/extensions/builtin/browser/agent-workspace/domain-skills/loom/folder-enumeration.md +170 -170
  58. package/dist/extensions/builtin/browser/agent-workspace/domain-skills/macrotrends/scraping.md +537 -537
  59. package/dist/extensions/builtin/browser/agent-workspace/domain-skills/medium/article-hydration.md +120 -120
  60. package/dist/extensions/builtin/browser/agent-workspace/domain-skills/medium/scraping.md +414 -414
  61. package/dist/extensions/builtin/browser/agent-workspace/domain-skills/metacritic/scraping.md +477 -477
  62. package/dist/extensions/builtin/browser/agent-workspace/domain-skills/musicbrainz/scraping.md +478 -478
  63. package/dist/extensions/builtin/browser/agent-workspace/domain-skills/nasa/scraping.md +339 -339
  64. package/dist/extensions/builtin/browser/agent-workspace/domain-skills/news-aggregation/multi-source.md +205 -205
  65. package/dist/extensions/builtin/browser/agent-workspace/domain-skills/open-library/scraping.md +472 -472
  66. package/dist/extensions/builtin/browser/agent-workspace/domain-skills/openalex/scraping.md +470 -470
  67. package/dist/extensions/builtin/browser/agent-workspace/domain-skills/openstreetmap/scraping.md +490 -490
  68. package/dist/extensions/builtin/browser/agent-workspace/domain-skills/package-registries/npm-pypi.md +478 -478
  69. package/dist/extensions/builtin/browser/agent-workspace/domain-skills/polymarket/scraping.md +234 -234
  70. package/dist/extensions/builtin/browser/agent-workspace/domain-skills/producthunt/scraping.md +307 -307
  71. package/dist/extensions/builtin/browser/agent-workspace/domain-skills/pubmed/scraping.md +421 -421
  72. package/dist/extensions/builtin/browser/agent-workspace/domain-skills/quora/scraping.md +364 -364
  73. package/dist/extensions/builtin/browser/agent-workspace/domain-skills/rawg/scraping.md +352 -352
  74. package/dist/extensions/builtin/browser/agent-workspace/domain-skills/reddit/scraping.md +124 -124
  75. package/dist/extensions/builtin/browser/agent-workspace/domain-skills/rest-countries/scraping.md +233 -233
  76. package/dist/extensions/builtin/browser/agent-workspace/domain-skills/sec-edgar/scraping.md +361 -361
  77. package/dist/extensions/builtin/browser/agent-workspace/domain-skills/shopify-admin/README.md +36 -36
  78. package/dist/extensions/builtin/browser/agent-workspace/domain-skills/shopify-admin/embedded-apps.md +72 -72
  79. package/dist/extensions/builtin/browser/agent-workspace/domain-skills/shopify-admin/knowledge-base.md +109 -109
  80. package/dist/extensions/builtin/browser/agent-workspace/domain-skills/shopify-admin/polaris-inputs.md +137 -137
  81. package/dist/extensions/builtin/browser/agent-workspace/domain-skills/soundcloud/scraping.md +362 -362
  82. package/dist/extensions/builtin/browser/agent-workspace/domain-skills/spotify/scraping.md +339 -339
  83. package/dist/extensions/builtin/browser/agent-workspace/domain-skills/stackoverflow/scraping.md +435 -435
  84. package/dist/extensions/builtin/browser/agent-workspace/domain-skills/steam/scraping.md +575 -575
  85. package/dist/extensions/builtin/browser/agent-workspace/domain-skills/substack/scraping.md +338 -338
  86. package/dist/extensions/builtin/browser/agent-workspace/domain-skills/thetechgeeks/pricing.md +52 -52
  87. package/dist/extensions/builtin/browser/agent-workspace/domain-skills/tiktok/upload.md +107 -107
  88. package/dist/extensions/builtin/browser/agent-workspace/domain-skills/tradingview/scraping.md +309 -309
  89. package/dist/extensions/builtin/browser/agent-workspace/domain-skills/trello/boards-and-lists.md +88 -88
  90. package/dist/extensions/builtin/browser/agent-workspace/domain-skills/trustpilot/scraping.md +375 -375
  91. package/dist/extensions/builtin/browser/agent-workspace/domain-skills/walmart/scraping.md +444 -444
  92. package/dist/extensions/builtin/browser/agent-workspace/domain-skills/wayback-machine/scraping.md +306 -306
  93. package/dist/extensions/builtin/browser/agent-workspace/domain-skills/weather/scraping.md +398 -398
  94. package/dist/extensions/builtin/browser/agent-workspace/domain-skills/wellfound/scraping.md +596 -596
  95. package/dist/extensions/builtin/browser/agent-workspace/domain-skills/world-bank/scraping.md +356 -356
  96. package/dist/extensions/builtin/browser/agent-workspace/domain-skills/xiaohongshu/scraping.md +84 -84
  97. package/dist/extensions/builtin/browser/agent-workspace/domain-skills/youtube/scraping.md +418 -418
  98. package/dist/extensions/builtin/browser/agent-workspace/domain-skills/zillow/scraping.md +433 -433
  99. package/dist/extensions/builtin/browser/browser.md +73 -73
  100. package/dist/extensions/builtin/browser/install.md +142 -142
  101. package/dist/extensions/builtin/browser/interaction-skills/connection.md +48 -48
  102. package/dist/extensions/builtin/browser/interaction-skills/cookies.md +3 -3
  103. package/dist/extensions/builtin/browser/interaction-skills/cross-origin-iframes.md +3 -3
  104. package/dist/extensions/builtin/browser/interaction-skills/dialogs.md +64 -64
  105. package/dist/extensions/builtin/browser/interaction-skills/downloads.md +3 -3
  106. package/dist/extensions/builtin/browser/interaction-skills/drag-and-drop.md +3 -3
  107. package/dist/extensions/builtin/browser/interaction-skills/dropdowns.md +3 -3
  108. package/dist/extensions/builtin/browser/interaction-skills/iframes.md +3 -3
  109. package/dist/extensions/builtin/browser/interaction-skills/network-requests.md +3 -3
  110. package/dist/extensions/builtin/browser/interaction-skills/print-as-pdf.md +3 -3
  111. package/dist/extensions/builtin/browser/interaction-skills/profile-sync.md +90 -90
  112. package/dist/extensions/builtin/browser/interaction-skills/screenshots.md +17 -17
  113. package/dist/extensions/builtin/browser/interaction-skills/scrolling.md +3 -3
  114. package/dist/extensions/builtin/browser/interaction-skills/shadow-dom.md +3 -3
  115. package/dist/extensions/builtin/browser/interaction-skills/tabs.md +69 -69
  116. package/dist/extensions/builtin/browser/interaction-skills/uploads.md +1 -1
  117. package/dist/extensions/builtin/browser/interaction-skills/viewport.md +3 -3
  118. package/dist/extensions/builtin/browser/src/browser_harness/AGENT.md +15 -15
  119. package/dist/extensions/builtin/browser/src/browser_harness/__init__.py +8 -8
  120. package/dist/extensions/builtin/browser/src/browser_harness/_ipc.py +90 -90
  121. package/dist/extensions/builtin/browser/src/browser_harness/admin.py +722 -722
  122. package/dist/extensions/builtin/browser/src/browser_harness/daemon.py +328 -328
  123. package/dist/extensions/builtin/browser/src/browser_harness/helpers.py +396 -396
  124. package/dist/extensions/builtin/browser/src/browser_harness/run.py +103 -103
  125. package/dist/extensions/builtin/discipline/skills/brainstorming/SKILL.md +33 -33
  126. package/dist/extensions/builtin/discipline/skills/executing-plans/SKILL.md +25 -25
  127. package/dist/extensions/builtin/discipline/skills/finishing-development-branch/SKILL.md +25 -25
  128. package/dist/extensions/builtin/discipline/skills/receiving-code-review/SKILL.md +22 -22
  129. package/dist/extensions/builtin/discipline/skills/requesting-code-review/SKILL.md +31 -31
  130. package/dist/extensions/builtin/discipline/skills/systematic-debugging/SKILL.md +28 -28
  131. package/dist/extensions/builtin/discipline/skills/test-driven-development/SKILL.md +32 -32
  132. package/dist/extensions/builtin/discipline/skills/using-git-worktrees/SKILL.md +25 -25
  133. package/dist/extensions/builtin/discipline/skills/verification-before-completion/SKILL.md +27 -27
  134. package/dist/extensions/builtin/discipline/skills/writing-plans/SKILL.md +26 -26
  135. package/dist/extensions/builtin/goal/README.md +67 -67
  136. package/dist/extensions/builtin/goal/goal-controller.d.ts +39 -10
  137. package/dist/extensions/builtin/goal/goal-controller.js +1 -1
  138. package/dist/extensions/builtin/goal/goal-format.js +1 -1
  139. package/dist/extensions/builtin/goal/goal-prompts.d.ts +2 -0
  140. package/dist/extensions/builtin/goal/goal-prompts.js +5 -4
  141. package/dist/extensions/builtin/goal/goal-store.js +1 -1
  142. package/dist/extensions/builtin/goal/index.d.ts +1 -1
  143. package/dist/extensions/builtin/goal/index.js +10 -7
  144. package/dist/extensions/builtin/grub/README.md +112 -112
  145. package/dist/extensions/builtin/link-world/agent-workspace/README.md +16 -16
  146. package/dist/extensions/builtin/link-world/index.js +6 -6
  147. package/dist/extensions/builtin/link-world/internet-search/internet-search.md +65 -65
  148. package/dist/extensions/builtin/link-world/link-world-agent.md +82 -82
  149. package/dist/extensions/builtin/link-world/linkworld.md +313 -313
  150. package/dist/extensions/builtin/link-world/{network-routing.md → network-routing/network-routing.md} +67 -67
  151. package/dist/extensions/builtin/loop/README.md +92 -92
  152. package/dist/extensions/builtin/mcp/figma-design.md +68 -68
  153. package/dist/extensions/builtin/mcp/mcp-management.md +85 -85
  154. package/dist/extensions/builtin/plan/index.js +1 -1
  155. package/dist/extensions/builtin/recap/AGENT.md +15 -15
  156. package/dist/extensions/builtin/sal/README.md +72 -72
  157. package/dist/extensions/builtin/security-audit/README.md +289 -289
  158. package/dist/extensions/builtin/task/task-store.d.ts +4 -0
  159. package/dist/extensions/builtin/task/task-store.js +1 -1
  160. package/dist/extensions/builtin/team/AGENT.md +112 -112
  161. package/dist/extensions/builtin/team/TESTING.md +299 -299
  162. package/dist/extensions/builtin/token-save/README.md +56 -56
  163. package/dist/extensions/optional/AGENT.md +10 -10
  164. package/dist/index.d.ts +5 -30
  165. package/dist/index.js +1 -1
  166. package/dist/models.d.ts +7 -0
  167. package/dist/models.js +1 -0
  168. package/dist/modes/interactive/components/footer.js +1 -1
  169. package/dist/modes/interactive/components/task-status-panel.d.ts +36 -0
  170. package/dist/modes/interactive/components/task-status-panel.js +1 -0
  171. package/dist/modes/interactive/controllers/stream-render-controller.d.ts +7 -0
  172. package/dist/modes/interactive/controllers/stream-render-controller.js +2 -2
  173. package/dist/modes/interactive/interactive-mode.js +40 -40
  174. package/dist/modes/interactive/state/interactive-state.d.ts +2 -0
  175. package/dist/modes/interactive/state/interactive-state.js +1 -1
  176. package/dist/modes/interactive/theme/dark.json +85 -85
  177. package/dist/modes/interactive/theme/light.json +84 -84
  178. package/dist/modes/interactive/theme/theme-schema.json +335 -335
  179. package/dist/modes/interactive/theme/warm.json +81 -81
  180. package/dist/node_modules/@pencil-agent/ai/dist/cli.js +0 -0
  181. package/dist/node_modules/@pencil-agent/ai/dist/models.generated.js +1 -1
  182. package/dist/node_modules/@pencil-agent/ai/dist/providers/anthropic.js +2 -2
  183. package/dist/node_modules/@pencil-agent/ai/dist/providers/openai-completions.js +5 -5
  184. package/dist/node_modules/@pencil-agent/ai/dist/providers/openai-responses.js +1 -1
  185. package/dist/node_modules/@pencil-agent/ai/dist/stream.js +1 -1
  186. package/dist/packages/protocol/src/commands.d.ts +33 -0
  187. package/dist/packages/protocol/src/flags.d.ts +20 -0
  188. package/dist/packages/protocol/src/hooks.d.ts +17 -0
  189. package/dist/packages/protocol/src/hooks.js +0 -0
  190. package/dist/packages/{extension-sdk → protocol}/src/index.d.ts +7 -4
  191. package/dist/packages/protocol/src/index.js +1 -0
  192. package/dist/packages/{extension-sdk → protocol}/src/lifecycle.d.ts +15 -27
  193. package/dist/packages/protocol/src/lifecycle.js +0 -0
  194. package/dist/packages/{extension-sdk → protocol}/src/tools.d.ts +1 -1
  195. package/dist/packages/protocol/src/tools.js +0 -0
  196. package/dist/public-config.d.ts +12 -0
  197. package/dist/public-config.js +1 -0
  198. package/dist/runtime.d.ts +9 -0
  199. package/dist/runtime.js +1 -0
  200. package/dist/session-compaction.d.ts +7 -0
  201. package/dist/session-compaction.js +1 -0
  202. package/dist/session.d.ts +7 -0
  203. package/dist/session.js +1 -0
  204. package/dist/skills.d.ts +7 -0
  205. package/dist/skills.js +1 -0
  206. package/dist/tools.d.ts +7 -0
  207. package/dist/tools.js +1 -0
  208. 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 +851 -0
  209. package/docs/SDK-TESTING.md +364 -0
  210. package/docs/codex-goal-command-impl.md +1055 -1055
  211. package/docs/codex-goal-vs-grub.md +500 -500
  212. package/docs/custom-provider.md +27 -27
  213. package/docs/extensions.md +27 -27
  214. package/docs/keybindings.md +27 -27
  215. package/docs/loop /351/207/215/346/236/204/345/256/214/346/210/220/346/200/273/347/273/223.md" +250 -250
  216. package/docs/loop /351/207/215/346/236/204/345/256/214/346/210/220/346/212/245/345/221/212.md" +122 -122
  217. package/docs/loop /351/207/215/346/236/204/346/226/271/346/241/210.md" +1222 -1222
  218. 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
  219. 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
  220. package/docs/loop /351/207/215/346/236/204/350/256/241/345/210/222.md" +320 -320
  221. package/docs/loop-usage-examples.md +214 -214
  222. package/docs/mem-core/346/212/200/346/234/257/346/226/207/346/241/243.md +593 -0
  223. package/docs/models.md +27 -27
  224. package/docs/packages.md +27 -27
  225. package/docs/pi-design-philosophy.md +457 -457
  226. package/docs/planmode.md +1987 -1987
  227. package/docs/prompt-templates.md +27 -27
  228. package/docs/providers.md +27 -27
  229. package/docs/sdk.md +27 -27
  230. package/docs/skills.md +27 -27
  231. package/docs/startup-performance-optimization.md +301 -0
  232. package/docs/themes.md +27 -27
  233. package/docs/tui.md +27 -27
  234. package/docs//350/256/244/347/237/245/345/234/260/345/233/276.md +47 -0
  235. package/package.json +190 -162
  236. package/dist/packages/extension-sdk/src/index.js +0 -1
  237. package/docs/cc-agent-design.md +0 -1297
  238. package/docs/cc-tui-design.md +0 -1333
  239. package/docs//345/257/271/346/240/207Claude-Code.md +0 -1775
  240. /package/dist/packages/{extension-sdk/src/lifecycle.js → protocol/src/commands.js} +0 -0
  241. /package/dist/packages/{extension-sdk/src/tools.js → protocol/src/flags.js} +0 -0
@@ -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.