puremacro 0.92.0__py3-none-any.whl

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 (564) hide show
  1. puremacro/__init__.py +8 -0
  2. puremacro/_backend.py +92 -0
  3. puremacro/_cache_db.py +216 -0
  4. puremacro/_codes.py +31 -0
  5. puremacro/_http.py +198 -0
  6. puremacro/_http_cache.py +158 -0
  7. puremacro/_linalg.py +131 -0
  8. puremacro/bartik/__init__.py +14 -0
  9. puremacro/bartik/county_epu.py +84 -0
  10. puremacro/bartik/exposure_iv.py +110 -0
  11. puremacro/bartik/sensitivity.py +98 -0
  12. puremacro/bartik/shares.py +119 -0
  13. puremacro/bis_neer.py +154 -0
  14. puremacro/build_panel.py +841 -0
  15. puremacro/build_subnational_panel.py +559 -0
  16. puremacro/cache.py +158 -0
  17. puremacro/climate/__init__.py +25 -0
  18. puremacro/climate/annual_lp.py +75 -0
  19. puremacro/climate/degree_days.py +73 -0
  20. puremacro/climate/mediation.py +120 -0
  21. puremacro/climate/monthly_dl.py +201 -0
  22. puremacro/cointegration_modern.py +355 -0
  23. puremacro/connectedness/__init__.py +4 -0
  24. puremacro/connectedness/diebold_yilmaz.py +109 -0
  25. puremacro/credentials.py +188 -0
  26. puremacro/cross_nest.py +390 -0
  27. puremacro/cycles.py +86 -0
  28. puremacro/data.py +179 -0
  29. puremacro/did/__init__.py +49 -0
  30. puremacro/did/_results.py +314 -0
  31. puremacro/did/borusyak_jaravel_spiess.py +192 -0
  32. puremacro/did/callaway_santanna.py +196 -0
  33. puremacro/did/cdh.py +344 -0
  34. puremacro/did/sdid_multi.py +269 -0
  35. puremacro/did/sun_abraham.py +121 -0
  36. puremacro/did/synthetic_did.py +200 -0
  37. puremacro/did/types.py +81 -0
  38. puremacro/dsge/__init__.py +32 -0
  39. puremacro/dsge/_results.py +126 -0
  40. puremacro/dsge/_sw07_data.csv +170 -0
  41. puremacro/dsge/estimate.py +267 -0
  42. puremacro/dsge/fertility_adj_costs.py +866 -0
  43. puremacro/dsge/gensys.py +160 -0
  44. puremacro/dsge/klein.py +367 -0
  45. puremacro/dsge/load_dynare.py +125 -0
  46. puremacro/dsge/priors.py +124 -0
  47. puremacro/dsge/smets_wouters.py +877 -0
  48. puremacro/dsge/sw07_estimate.py +86 -0
  49. puremacro/dsge/sw07_observation.py +119 -0
  50. puremacro/dsge/sw07_priors.py +108 -0
  51. puremacro/dynpanel/__init__.py +32 -0
  52. puremacro/dynpanel/_results.py +119 -0
  53. puremacro/dynpanel/ab_gmm.py +334 -0
  54. puremacro/dynpanel/bb_gmm.py +476 -0
  55. puremacro/dynpanel/diagnostics.py +253 -0
  56. puremacro/dynpanel/instruments.py +627 -0
  57. puremacro/examples/__init__.py +5 -0
  58. puremacro/examples/asset_composition_dynamics.py +168 -0
  59. puremacro/examples/berkowitz_demo.py +66 -0
  60. puremacro/examples/bloom2009.py +119 -0
  61. puremacro/examples/bvar_fan_chart.py +123 -0
  62. puremacro/examples/caldara_iacoviello.py +63 -0
  63. puremacro/examples/cointegration_johansen.py +67 -0
  64. puremacro/examples/dcc_volatility.py +90 -0
  65. puremacro/examples/dfm_bai_ng.py +93 -0
  66. puremacro/examples/dfm_nowcast_kalman.py +115 -0
  67. puremacro/examples/did_callaway_santanna_demo.py +95 -0
  68. puremacro/examples/diebold_yilmaz_spillover.py +58 -0
  69. puremacro/examples/dsge_ar1_demo.py +63 -0
  70. puremacro/examples/dsge_fertility_demo.py +39 -0
  71. puremacro/examples/dsge_rbc_klein.py +157 -0
  72. puremacro/examples/exact_diffuse_kalman.py +101 -0
  73. puremacro/examples/fetch_fred_demo.py +112 -0
  74. puremacro/examples/fm_ols_dols_demo.py +107 -0
  75. puremacro/examples/forecast_density_eval.py +124 -0
  76. puremacro/examples/gali_1999_hours.py +210 -0
  77. puremacro/examples/gfevd_spillover.py +85 -0
  78. puremacro/examples/gk_robust_from_gibbs.py +97 -0
  79. puremacro/examples/gk_robust_signs.py +106 -0
  80. puremacro/examples/global_fiscal_stance.py +111 -0
  81. puremacro/examples/glp_lambda_search.py +89 -0
  82. puremacro/examples/govt_vs_private_investment.py +134 -0
  83. puremacro/examples/har_realized_vol.py +122 -0
  84. puremacro/examples/hfi_gertler_karadi.py +72 -0
  85. puremacro/examples/kilian_2009_oil.py +301 -0
  86. puremacro/examples/la_lp_pmw_demo.py +115 -0
  87. puremacro/examples/labor_flows_demo.py +102 -0
  88. puremacro/examples/lp_asymmetric_tenreyro.py +58 -0
  89. puremacro/examples/lp_panel_dk.py +52 -0
  90. puremacro/examples/lp_smooth_demo.py +66 -0
  91. puremacro/examples/lwui_wage_demo.py +96 -0
  92. puremacro/examples/mcmc_diagnostics.py +102 -0
  93. puremacro/examples/midas_quarterly_monthly.py +105 -0
  94. puremacro/examples/mle_sandwich_demo.py +95 -0
  95. puremacro/examples/ms_var_business_cycle.py +117 -0
  96. puremacro/examples/narrative_custom_corpus.py +115 -0
  97. puremacro/examples/narrative_event_study.py +171 -0
  98. puremacro/examples/narrative_g7_panel.py +192 -0
  99. puremacro/examples/narrative_homogeneous_panel.py +202 -0
  100. puremacro/examples/narrative_indices_demo.py +75 -0
  101. puremacro/examples/narrative_llm_pipeline.py +93 -0
  102. puremacro/examples/narrative_local_llm.py +65 -0
  103. puremacro/examples/narrative_multilingual_live.py +145 -0
  104. puremacro/examples/narrative_panel_lp.py +150 -0
  105. puremacro/examples/narrative_ramey_2011.py +158 -0
  106. puremacro/examples/narrative_romer_romer_2010.py +144 -0
  107. puremacro/examples/narrative_sign_adrr.py +163 -0
  108. puremacro/examples/narrative_us_canonical_compare.py +194 -0
  109. puremacro/examples/non_gaussian_svar.py +94 -0
  110. puremacro/examples/posterior_predictive_fan.py +83 -0
  111. puremacro/examples/ramey_zubairy_2018_multipliers.py +392 -0
  112. puremacro/examples/regime_workflow.py +107 -0
  113. puremacro/examples/report_workflow.py +79 -0
  114. puremacro/examples/shock_atlas_demo.py +64 -0
  115. puremacro/examples/sigma_decomposition.py +113 -0
  116. puremacro/examples/sign_restrictions_uhlig.py +95 -0
  117. puremacro/examples/spectral_business_cycle.py +93 -0
  118. puremacro/examples/svariv_mertens_ravn.py +212 -0
  119. puremacro/examples/synthetic_control_demo.py +116 -0
  120. puremacro/examples/tvar_threshold_demo.py +89 -0
  121. puremacro/examples/tvecm_demo.py +78 -0
  122. puremacro/examples/tvp_var_demo.py +120 -0
  123. puremacro/examples/tvp_var_sv_demo.py +100 -0
  124. puremacro/examples/vintage_revisions.py +113 -0
  125. puremacro/examples/vulnerable_growth.py +108 -0
  126. puremacro/examples/wavelet_business_cycle.py +113 -0
  127. puremacro/experiment.py +176 -0
  128. puremacro/factor.py +141 -0
  129. puremacro/fetch/__init__.py +50 -0
  130. puremacro/fetch/_classic.py +143 -0
  131. puremacro/fetch/_http.py +79 -0
  132. puremacro/fetch/_oecd_sdmx.py +70 -0
  133. puremacro/fetch/_seasonal.py +106 -0
  134. puremacro/fetch/_static_tables.py +31 -0
  135. puremacro/fetch/bea_cainc.py +163 -0
  136. puremacro/fetch/bea_industry_shares.py +101 -0
  137. puremacro/fetch/bls_state_panel.py +151 -0
  138. puremacro/fetch/cdc_births_county.py +529 -0
  139. puremacro/fetch/cdc_births_state.py +200 -0
  140. puremacro/fetch/cdc_socrata_natality.py +103 -0
  141. puremacro/fetch/census_bfs.py +129 -0
  142. puremacro/fetch/census_pep_births.py +151 -0
  143. puremacro/fetch/ces_states.py +74 -0
  144. puremacro/fetch/epu.py +91 -0
  145. puremacro/fetch/epu_news_historical.py +75 -0
  146. puremacro/fetch/epu_states.py +103 -0
  147. puremacro/fetch/fernald.py +63 -0
  148. puremacro/fetch/frb_phil_coincident.py +86 -0
  149. puremacro/fetch/fred.py +73 -0
  150. puremacro/fetch/fred_states.py +152 -0
  151. puremacro/fetch/gpr.py +81 -0
  152. puremacro/fetch/hrs_mpu.py +57 -0
  153. puremacro/fetch/imf_ifs.py +168 -0
  154. puremacro/fetch/jln.py +64 -0
  155. puremacro/fetch/kenfrench_industry.py +56 -0
  156. puremacro/fetch/labor.py +488 -0
  157. puremacro/fetch/labor_eurostat.py +498 -0
  158. puremacro/fetch/labor_ilostat.py +740 -0
  159. puremacro/fetch/laus.py +89 -0
  160. puremacro/fetch/lmn.py +61 -0
  161. puremacro/fetch/oecd.py +309 -0
  162. puremacro/fetch/oecd_energy.py +108 -0
  163. puremacro/fetch/oecd_fx.py +63 -0
  164. puremacro/fetch/oecd_mei.py +131 -0
  165. puremacro/fetch/oecd_qna_expenditure.py +158 -0
  166. puremacro/fetch/oecd_qna_labor.py +138 -0
  167. puremacro/fetch/oecd_qna_local.py +134 -0
  168. puremacro/fetch/qcew.py +83 -0
  169. puremacro/fetch/sdmx.py +203 -0
  170. puremacro/fetch/state_industry_panel.py +469 -0
  171. puremacro/fetch/wb_pink_sheet.py +102 -0
  172. puremacro/fetch/wui.py +86 -0
  173. puremacro/fetch/wui_extras.py +92 -0
  174. puremacro/fetch/yahoo.py +77 -0
  175. puremacro/forecast/__init__.py +14 -0
  176. puremacro/forecast/compare.py +118 -0
  177. puremacro/forecast/density.py +230 -0
  178. puremacro/gar/__init__.py +32 -0
  179. puremacro/gar/fci.py +164 -0
  180. puremacro/gar/qar.py +127 -0
  181. puremacro/gar/skewt.py +334 -0
  182. puremacro/garch/__init__.py +5 -0
  183. puremacro/garch/_results.py +110 -0
  184. puremacro/garch/dcc.py +120 -0
  185. puremacro/garch/fit.py +82 -0
  186. puremacro/hfi/__init__.py +21 -0
  187. puremacro/hfi/_results.py +90 -0
  188. puremacro/hfi/jk2020.py +147 -0
  189. puremacro/hfi/surprises.py +153 -0
  190. puremacro/inference/__init__.py +56 -0
  191. puremacro/inference/_ols_helpers.py +52 -0
  192. puremacro/inference/_results.py +165 -0
  193. puremacro/inference/balanced_panel.py +262 -0
  194. puremacro/inference/block_bootstrap.py +69 -0
  195. puremacro/inference/bootstrap.py +108 -0
  196. puremacro/inference/dk.py +41 -0
  197. puremacro/inference/hac.py +28 -0
  198. puremacro/inference/hac_fixed_b.py +111 -0
  199. puremacro/inference/lewbel_iv.py +179 -0
  200. puremacro/inference/lp_block_bootstrap.py +154 -0
  201. puremacro/inference/moving_block.py +231 -0
  202. puremacro/inference/moving_block_bootstrap.py +229 -0
  203. puremacro/inference/newey_west.py +26 -0
  204. puremacro/inference/over_id.py +118 -0
  205. puremacro/inference/pesaran.py +55 -0
  206. puremacro/inference/pesaran_cce.py +51 -0
  207. puremacro/inference/quandt_andrews.py +128 -0
  208. puremacro/inference/spec_curve.py +110 -0
  209. puremacro/inference/supt.py +233 -0
  210. puremacro/inference/swamy.py +40 -0
  211. puremacro/inference/swamy_test.py +40 -0
  212. puremacro/inference/weak_iv.py +389 -0
  213. puremacro/inference/wild_bootstrap.py +97 -0
  214. puremacro/instruments/__init__.py +21 -0
  215. puremacro/instruments/_catalog.py +811 -0
  216. puremacro/instruments/_compose.py +199 -0
  217. puremacro/instruments/_core.py +209 -0
  218. puremacro/instruments/_helpers.py +118 -0
  219. puremacro/instruments/_registry.py +174 -0
  220. puremacro/instruments/external/__init__.py +14 -0
  221. puremacro/instruments/external/bis.py +136 -0
  222. puremacro/instruments/external/fred.py +128 -0
  223. puremacro/instruments/external/imf_weo.py +176 -0
  224. puremacro/instruments/literature/__init__.py +19 -0
  225. puremacro/instruments/literature/_helpers.py +9 -0
  226. puremacro/instruments/literature/bbd_epu.py +79 -0
  227. puremacro/instruments/literature/bloom_2009.py +84 -0
  228. puremacro/instruments/literature/caldara_iacoviello_gpr.py +78 -0
  229. puremacro/instruments/literature/romer_romer_2004.py +93 -0
  230. puremacro/klems.py +596 -0
  231. puremacro/korv_gmm.py +890 -0
  232. puremacro/labor_flows.py +286 -0
  233. puremacro/labor_share.py +72 -0
  234. puremacro/long_panel.py +421 -0
  235. puremacro/lp/__init__.py +28 -0
  236. puremacro/lp/_garch_utils.py +170 -0
  237. puremacro/lp/_panel_helpers.py +219 -0
  238. puremacro/lp/asymmetric.py +89 -0
  239. puremacro/lp/cce.py +124 -0
  240. puremacro/lp/garch_in_mean.py +32 -0
  241. puremacro/lp/garch_state.py +39 -0
  242. puremacro/lp/iv.py +95 -0
  243. puremacro/lp/iv_helpers.py +90 -0
  244. puremacro/lp/iv_lewbel.py +114 -0
  245. puremacro/lp/jorda.py +78 -0
  246. puremacro/lp/la_lp.py +100 -0
  247. puremacro/lp/lp_did.py +443 -0
  248. puremacro/lp/mean_group.py +89 -0
  249. puremacro/lp/panel.py +277 -0
  250. puremacro/lp/panel_dk.py +46 -0
  251. puremacro/lp/panel_iv.py +50 -0
  252. puremacro/lp/quantile.py +111 -0
  253. puremacro/lp/smooth.py +96 -0
  254. puremacro/lp/state_dep.py +249 -0
  255. puremacro/mcmc.py +254 -0
  256. puremacro/midas.py +282 -0
  257. puremacro/models/__init__.py +10 -0
  258. puremacro/models/dmp_regime_dependent.py +526 -0
  259. puremacro/models/nested_dmp/__init__.py +133 -0
  260. puremacro/models/nested_dmp/backend.py +23 -0
  261. puremacro/models/nested_dmp/dynamics.py +417 -0
  262. puremacro/models/nested_dmp/equilibrium.py +392 -0
  263. puremacro/models/nested_dmp/estimation.py +332 -0
  264. puremacro/models/nested_dmp/kernels.py +158 -0
  265. puremacro/models/nested_dmp/kernels_numba.py +47 -0
  266. puremacro/models/nested_dmp/params.py +107 -0
  267. puremacro/models/nested_dmp/welfare.py +727 -0
  268. puremacro/narrative/__init__.py +119 -0
  269. puremacro/narrative/_local_engines.py +280 -0
  270. puremacro/narrative/_signal_quality.py +66 -0
  271. puremacro/narrative/aggregate.py +283 -0
  272. puremacro/narrative/dedup.py +177 -0
  273. puremacro/narrative/indices/__init__.py +55 -0
  274. puremacro/narrative/indices/_embedding_kernel.py +176 -0
  275. puremacro/narrative/indices/_fed_districts.py +221 -0
  276. puremacro/narrative/indices/_kernels.py +430 -0
  277. puremacro/narrative/indices/_lexicons.py +2352 -0
  278. puremacro/narrative/indices/_lexicons_expanded.py +609 -0
  279. puremacro/narrative/indices/_llm_kernel.py +312 -0
  280. puremacro/narrative/indices/_mnl_kernel.py +189 -0
  281. puremacro/narrative/indices/beige_book.py +181 -0
  282. puremacro/narrative/indices/bluesky.py +124 -0
  283. puremacro/narrative/indices/cross_source.py +133 -0
  284. puremacro/narrative/indices/epu.py +71 -0
  285. puremacro/narrative/indices/eu_legislative.py +94 -0
  286. puremacro/narrative/indices/gpr.py +56 -0
  287. puremacro/narrative/indices/ltui.py +182 -0
  288. puremacro/narrative/indices/lui.py +75 -0
  289. puremacro/narrative/indices/lwui.py +143 -0
  290. puremacro/narrative/indices/mpu.py +64 -0
  291. puremacro/narrative/indices/tone.py +100 -0
  292. puremacro/narrative/indices/us_executive.py +116 -0
  293. puremacro/narrative/indices/wui.py +54 -0
  294. puremacro/narrative/panel.py +516 -0
  295. puremacro/narrative/quality/__init__.py +7 -0
  296. puremacro/narrative/quality/magnitude_harmonizer.py +81 -0
  297. puremacro/narrative/quality/pipeline.py +71 -0
  298. puremacro/narrative/quality/surprise_filter.py +74 -0
  299. puremacro/narrative/quality/target_auditor.py +101 -0
  300. puremacro/narrative/replication/__init__.py +75 -0
  301. puremacro/narrative/replication/_within_year.py +94 -0
  302. puremacro/narrative/replication/adler_2024_consolidations.py +230 -0
  303. puremacro/narrative/replication/cloyne_2013_uk.py +110 -0
  304. puremacro/narrative/replication/devries_2011_consolidations.py +210 -0
  305. puremacro/narrative/replication/dglp_2011_consolidations.py +249 -0
  306. puremacro/narrative/replication/eu_nms_2023_consolidations.py +301 -0
  307. puremacro/narrative/replication/guajardo_2011_aeipf.py +243 -0
  308. puremacro/narrative/replication/imf_covid_2022_announcements.py +324 -0
  309. puremacro/narrative/replication/mertens_ravn_2013.py +171 -0
  310. puremacro/narrative/replication/mitchell_historical.py +134 -0
  311. puremacro/narrative/replication/ramey_2011_defense.py +118 -0
  312. puremacro/narrative/replication/romer_romer_2010.py +141 -0
  313. puremacro/narrative/replication/romer_romer_2017.py +122 -0
  314. puremacro/narrative/scoring/__init__.py +14 -0
  315. puremacro/narrative/scoring/keyword.py +231 -0
  316. puremacro/narrative/scoring/llm.py +536 -0
  317. puremacro/narrative/scoring/manual.py +79 -0
  318. puremacro/narrative/sources/__init__.py +121 -0
  319. puremacro/narrative/sources/_extractors.py +394 -0
  320. puremacro/narrative/sources/_fallback.py +181 -0
  321. puremacro/narrative/sources/_http.py +20 -0
  322. puremacro/narrative/sources/_playwright_helper.py +114 -0
  323. puremacro/narrative/sources/_ratedoc.py +85 -0
  324. puremacro/narrative/sources/_rss.py +104 -0
  325. puremacro/narrative/sources/_rss_filtered.py +66 -0
  326. puremacro/narrative/sources/_schema.py +108 -0
  327. puremacro/narrative/sources/_schema_check.py +78 -0
  328. puremacro/narrative/sources/_speeches.py +37 -0
  329. puremacro/narrative/sources/_telemetry.py +169 -0
  330. puremacro/narrative/sources/_wayback.py +61 -0
  331. puremacro/narrative/sources/banrep.py +20 -0
  332. puremacro/narrative/sources/banxico.py +127 -0
  333. puremacro/narrative/sources/bcb.py +176 -0
  334. puremacro/narrative/sources/bccl.py +20 -0
  335. puremacro/narrative/sources/bcra.py +20 -0
  336. puremacro/narrative/sources/beige_book.py +1100 -0
  337. puremacro/narrative/sources/bi.py +198 -0
  338. puremacro/narrative/sources/bis_speeches.py +204 -0
  339. puremacro/narrative/sources/bluesky.py +340 -0
  340. puremacro/narrative/sources/bnm.py +177 -0
  341. puremacro/narrative/sources/boe_decision.py +34 -0
  342. puremacro/narrative/sources/boe_minutes.py +105 -0
  343. puremacro/narrative/sources/boe_speeches.py +154 -0
  344. puremacro/narrative/sources/boj_decision.py +35 -0
  345. puremacro/narrative/sources/boj_speeches.py +104 -0
  346. puremacro/narrative/sources/bok.py +106 -0
  347. puremacro/narrative/sources/bot.py +128 -0
  348. puremacro/narrative/sources/bsp.py +315 -0
  349. puremacro/narrative/sources/ca_dof.py +38 -0
  350. puremacro/narrative/sources/cbe.py +200 -0
  351. puremacro/narrative/sources/cbk.py +218 -0
  352. puremacro/narrative/sources/cbn.py +213 -0
  353. puremacro/narrative/sources/de_bmf.py +42 -0
  354. puremacro/narrative/sources/ecb_decision.py +70 -0
  355. puremacro/narrative/sources/ecb_minutes.py +40 -0
  356. puremacro/narrative/sources/ecb_press.py +55 -0
  357. puremacro/narrative/sources/ecb_press_conf.py +56 -0
  358. puremacro/narrative/sources/ecb_speeches.py +41 -0
  359. puremacro/narrative/sources/eu_ecfin.py +63 -0
  360. puremacro/narrative/sources/eu_eurlex.py +354 -0
  361. puremacro/narrative/sources/eu_parliament.py +206 -0
  362. puremacro/narrative/sources/fed_decision.py +83 -0
  363. puremacro/narrative/sources/fed_minutes.py +140 -0
  364. puremacro/narrative/sources/fed_press_conf.py +64 -0
  365. puremacro/narrative/sources/fed_speeches.py +48 -0
  366. puremacro/narrative/sources/fr_tresor.py +28 -0
  367. puremacro/narrative/sources/frdb_strikes.py +170 -0
  368. puremacro/narrative/sources/google_news.py +77 -0
  369. puremacro/narrative/sources/hackernews.py +112 -0
  370. puremacro/narrative/sources/health.py +184 -0
  371. puremacro/narrative/sources/imf_articleiv.py +221 -0
  372. puremacro/narrative/sources/imf_news.py +39 -0
  373. puremacro/narrative/sources/it_mef.py +40 -0
  374. puremacro/narrative/sources/jp_mof.py +20 -0
  375. puremacro/narrative/sources/local_csv.py +30 -0
  376. puremacro/narrative/sources/mas.py +21 -0
  377. puremacro/narrative/sources/nber_wp.py +80 -0
  378. puremacro/narrative/sources/news_api.py +75 -0
  379. puremacro/narrative/sources/norges.py +21 -0
  380. puremacro/narrative/sources/oecd_surveys.py +209 -0
  381. puremacro/narrative/sources/pboc.py +92 -0
  382. puremacro/narrative/sources/rba.py +111 -0
  383. puremacro/narrative/sources/rbi.py +21 -0
  384. puremacro/narrative/sources/rbnz.py +26 -0
  385. puremacro/narrative/sources/reddit.py +105 -0
  386. puremacro/narrative/sources/riksbank.py +95 -0
  387. puremacro/narrative/sources/sarb.py +82 -0
  388. puremacro/narrative/sources/uk_hmt.py +23 -0
  389. puremacro/narrative/sources/uk_obr.py +21 -0
  390. puremacro/narrative/sources/us_cbo.py +269 -0
  391. puremacro/narrative/sources/us_dod_contracts.py +81 -0
  392. puremacro/narrative/sources/us_erp.py +223 -0
  393. puremacro/narrative/sources/us_federal_register.py +77 -0
  394. puremacro/narrative/sources/us_sotu.py +244 -0
  395. puremacro/narrative/sources/us_treasury.py +76 -0
  396. puremacro/narrative/sources/us_warn.py +1251 -0
  397. puremacro/narrative/types.py +529 -0
  398. puremacro/narrative/validate.py +138 -0
  399. puremacro/narrative/validation/__init__.py +36 -0
  400. puremacro/narrative/validation/external_benchmarks.py +295 -0
  401. puremacro/narrative/validation/gap_filler.py +87 -0
  402. puremacro/narrative/validation/report.py +182 -0
  403. puremacro/narrative/validation/spec_curve.py +275 -0
  404. puremacro/narrative/validation/stability.py +148 -0
  405. puremacro/nowcast/__init__.py +49 -0
  406. puremacro/nowcast/combine.py +192 -0
  407. puremacro/nowcast/dfm.py +171 -0
  408. puremacro/nowcast/mfvar.py +175 -0
  409. puremacro/nowcast/scoring.py +182 -0
  410. puremacro/numerics.py +167 -0
  411. puremacro/plot.py +227 -0
  412. puremacro/plotting/__init__.py +0 -0
  413. puremacro/plotting/bw_style.py +136 -0
  414. puremacro/plotting/irf_plot.py +162 -0
  415. puremacro/posterior.py +79 -0
  416. puremacro/realized_vol.py +124 -0
  417. puremacro/regime_dates.py +104 -0
  418. puremacro/regimes.py +95 -0
  419. puremacro/regress/__init__.py +4 -0
  420. puremacro/regress/lp.py +167 -0
  421. puremacro/replication/__init__.py +23 -0
  422. puremacro/replication/_data.py +17 -0
  423. puremacro/replication/_model.py +134 -0
  424. puremacro/replication/cases_ha_dsge_causal.py +144 -0
  425. puremacro/replication/cases_stylized_facts.py +44 -0
  426. puremacro/replication/data/__init__.py +0 -0
  427. puremacro/replication/data/gali1999.csv +314 -0
  428. puremacro/replication/data/kilian2009.csv +433 -0
  429. puremacro/replication/data/okun.csv +940 -0
  430. puremacro/replication/data/rz2018.csv +509 -0
  431. puremacro/replication/data/tax14_narrative_tax_shocks.csv +253 -0
  432. puremacro/replication/data/tax14_us_fiscal.csv +318 -0
  433. puremacro/replication/runner.py +51 -0
  434. puremacro/reports.py +235 -0
  435. puremacro/sa/__init__.py +25 -0
  436. puremacro/sa/stl.py +132 -0
  437. puremacro/sa/x13.py +159 -0
  438. puremacro/scale.py +98 -0
  439. puremacro/shock_atlas.py +321 -0
  440. puremacro/sigma/__init__.py +4 -0
  441. puremacro/sigma/shapley.py +232 -0
  442. puremacro/sigma/sigma_numpy.py +64 -0
  443. puremacro/spectral.py +186 -0
  444. puremacro/state_space.py +378 -0
  445. puremacro/synthetic_control.py +202 -0
  446. puremacro/teaching/__init__.py +0 -0
  447. puremacro/teaching/bartik.py +54 -0
  448. puremacro/teaching/bartik_importance.py +199 -0
  449. puremacro/teaching/bartik_research.py +66 -0
  450. puremacro/teaching/bq_canonical.py +115 -0
  451. puremacro/teaching/data.py +73 -0
  452. puremacro/teaching/data_long.py +197 -0
  453. puremacro/teaching/data_states.py +121 -0
  454. puremacro/teaching/experiment.py +181 -0
  455. puremacro/teaching/garch_arch.py +20 -0
  456. puremacro/teaching/garch_iv.py +493 -0
  457. puremacro/teaching/integration_checks.py +173 -0
  458. puremacro/teaching/irf_scale.py +114 -0
  459. puremacro/teaching/lp_sm.py +47 -0
  460. puremacro/teaching/panel_lm.py +74 -0
  461. puremacro/teaching/plot.py +70 -0
  462. puremacro/teaching/recipes.py +142 -0
  463. puremacro/teaching/svar_panel.py +592 -0
  464. puremacro/teaching/var_sm.py +195 -0
  465. puremacro/tests/__init__.py +0 -0
  466. puremacro/tests/breaks.py +217 -0
  467. puremacro/tests/test_var/__init__.py +0 -0
  468. puremacro/tests/test_var/test_proxy_axis.py +35 -0
  469. puremacro/tests/unit_root.py +313 -0
  470. puremacro/uncertainty/__init__.py +49 -0
  471. puremacro/uncertainty/composite.py +332 -0
  472. puremacro/uncertainty/decomposition.py +111 -0
  473. puremacro/uncertainty/diagnostics.py +121 -0
  474. puremacro/uncertainty/lp_long.py +181 -0
  475. puremacro/uncertainty/regimes.py +125 -0
  476. puremacro/validation/__init__.py +23 -0
  477. puremacro/validation/_fixtures.py +19 -0
  478. puremacro/validation/_goldens.py +29 -0
  479. puremacro/validation/_model.py +143 -0
  480. puremacro/validation/cases_dsge.py +350 -0
  481. puremacro/validation/cases_dynpanel.py +298 -0
  482. puremacro/validation/cases_forecast.py +234 -0
  483. puremacro/validation/cases_garch.py +221 -0
  484. puremacro/validation/cases_inference.py +361 -0
  485. puremacro/validation/cases_lp.py +244 -0
  486. puremacro/validation/cases_narrative.py +284 -0
  487. puremacro/validation/cases_spectral.py +342 -0
  488. puremacro/validation/cases_state_space.py +272 -0
  489. puremacro/validation/cases_var.py +78 -0
  490. puremacro/validation/cases_vfi.py +297 -0
  491. puremacro/validation/goldens/__init__.py +0 -0
  492. puremacro/validation/goldens/_t.json +4 -0
  493. puremacro/validation/goldens/garch.json +4818 -0
  494. puremacro/validation/goldens/inference.json +22 -0
  495. puremacro/validation/goldens/lp.json +40 -0
  496. puremacro/validation/goldens/state_space.json +239 -0
  497. puremacro/validation/goldens/var.json +142 -0
  498. puremacro/validation/runner.py +55 -0
  499. puremacro/var/__init__.py +15 -0
  500. puremacro/var/_results.py +60 -0
  501. puremacro/var/bootstrap.py +180 -0
  502. puremacro/var/bvar.py +476 -0
  503. puremacro/var/diagnostics.py +132 -0
  504. puremacro/var/estimate.py +82 -0
  505. puremacro/var/identify/__init__.py +37 -0
  506. puremacro/var/identify/_results.py +613 -0
  507. puremacro/var/identify/bq.py +128 -0
  508. puremacro/var/identify/cholesky.py +197 -0
  509. puremacro/var/identify/hetero.py +229 -0
  510. puremacro/var/identify/magmav.py +394 -0
  511. puremacro/var/identify/maxshare.py +409 -0
  512. puremacro/var/identify/narrative_sign.py +568 -0
  513. puremacro/var/identify/non_gaussian.py +230 -0
  514. puremacro/var/identify/panel.py +125 -0
  515. puremacro/var/identify/proxy.py +117 -0
  516. puremacro/var/identify/sign.py +77 -0
  517. puremacro/var/identify/sign_robust.py +212 -0
  518. puremacro/var/identify/sign_zero.py +167 -0
  519. puremacro/var/irf.py +153 -0
  520. puremacro/var/panel.py +250 -0
  521. puremacro/var/peak.py +121 -0
  522. puremacro/var/regime/__init__.py +13 -0
  523. puremacro/var/regime/girf.py +511 -0
  524. puremacro/var/regime/ms_var.py +250 -0
  525. puremacro/var/regime/threshold.py +137 -0
  526. puremacro/var/regime/tvecm.py +179 -0
  527. puremacro/var/tvp.py +471 -0
  528. puremacro/var/vecm.py +279 -0
  529. puremacro/vfi/__init__.py +98 -0
  530. puremacro/vfi/aggregate.py +111 -0
  531. puremacro/vfi/case2.py +128 -0
  532. puremacro/vfi/compat.py +57 -0
  533. puremacro/vfi/discretize.py +197 -0
  534. puremacro/vfi/distribution.py +140 -0
  535. puremacro/vfi/egm.py +83 -0
  536. puremacro/vfi/epstein_zin.py +110 -0
  537. puremacro/vfi/equilibrium.py +113 -0
  538. puremacro/vfi/estimate.py +86 -0
  539. puremacro/vfi/examples.py +379 -0
  540. puremacro/vfi/finite_horizon.py +227 -0
  541. puremacro/vfi/firm_dynamics.py +113 -0
  542. puremacro/vfi/kernels.py +28 -0
  543. puremacro/vfi/kernels_numba.py +174 -0
  544. puremacro/vfi/krusell_smith.py +261 -0
  545. puremacro/vfi/olg.py +124 -0
  546. puremacro/vfi/permanent_types.py +96 -0
  547. puremacro/vfi/problem.py +205 -0
  548. puremacro/vfi/returnfn.py +71 -0
  549. puremacro/vfi/simulate.py +77 -0
  550. puremacro/vfi/solve.py +60 -0
  551. puremacro/vfi/transition.py +91 -0
  552. puremacro/vintages.py +303 -0
  553. puremacro/volatility/__init__.py +32 -0
  554. puremacro/volatility/diagnostics.py +94 -0
  555. puremacro/volatility/har.py +79 -0
  556. puremacro/volatility/multivariate.py +212 -0
  557. puremacro/volatility/range.py +129 -0
  558. puremacro/volatility/sigma.py +262 -0
  559. puremacro/wavelet.py +251 -0
  560. puremacro-0.92.0.dist-info/METADATA +363 -0
  561. puremacro-0.92.0.dist-info/RECORD +564 -0
  562. puremacro-0.92.0.dist-info/WHEEL +5 -0
  563. puremacro-0.92.0.dist-info/licenses/LICENSE +21 -0
  564. puremacro-0.92.0.dist-info/top_level.txt +1 -0
puremacro/__init__.py ADDED
@@ -0,0 +1,8 @@
1
+ """puremacro — Pyodide-compatible empirical macro toolbox.
2
+
3
+ See README.md for the iPad / juno.sh quickstart and module-by-module
4
+ overview. Public API is curated in submodules; nothing is exported at
5
+ the top level beyond the version string.
6
+ """
7
+ __version__ = "0.92.0"
8
+ __all__ = ["__version__"]
puremacro/_backend.py ADDED
@@ -0,0 +1,92 @@
1
+ """Shared pluggable compute backends for puremacro's compute-heavy modules.
2
+
3
+ Four tiers:
4
+ - "numpy" : reference implementation and correctness ORACLE. Always
5
+ available; keeps puremacro's Pyodide-compatible core intact.
6
+ - "numba" : CPU JIT (+ prange). Compiled kernels, NOT an array namespace.
7
+ - "mlx" : Apple-Silicon GPU. numpy-like namespace (mlx.core); xp-generic
8
+ kernels run on it unchanged. float32-only (Metal has no float64).
9
+ - "cupy" : NVIDIA GPU. numpy-like namespace (cupy); xp-generic kernels run
10
+ unchanged. Optional [cuda] extra; cannot be installed/run without CUDA.
11
+
12
+ numba/mlx/cupy are optional extras detected via importlib.util.find_spec and
13
+ skipped, never hard-failed. (Promoted from models/nested_dmp/backend.py so the
14
+ nested DMP model and the general vfi engine share one source of truth.)
15
+ """
16
+ from __future__ import annotations
17
+
18
+ import importlib.util
19
+
20
+ import numpy as np
21
+
22
+ SUPPORTED = ("numpy", "numba", "mlx", "cupy")
23
+
24
+
25
+ def backend_available(name: str) -> bool:
26
+ """True if backend ``name`` can be used in this environment."""
27
+ if name == "numpy":
28
+ return True
29
+ if name in ("numba", "mlx", "cupy"):
30
+ return importlib.util.find_spec(name) is not None
31
+ raise ValueError(f"Unknown backend {name!r}; supported: {SUPPORTED}")
32
+
33
+
34
+ def available_backends() -> tuple[str, ...]:
35
+ """Tuple of installed backends, numpy first."""
36
+ return tuple(b for b in SUPPORTED if backend_available(b))
37
+
38
+
39
+ def get_array_namespace(name: str):
40
+ """Return a numpy-like array module for namespace-style backends.
41
+
42
+ "numpy"->numpy; "mlx"->mlx.core; "cupy"->cupy. "numba" is not a namespace.
43
+ """
44
+ if name == "numpy":
45
+ return np
46
+ if name == "mlx":
47
+ if not backend_available("mlx"):
48
+ raise ImportError(
49
+ "mlx not installed; on Apple Silicon run "
50
+ "`pip install puremacro[backend]`."
51
+ )
52
+ import mlx.core as mx
53
+
54
+ return mx
55
+ if name == "cupy":
56
+ if not backend_available("cupy"):
57
+ raise ImportError(
58
+ "cupy not installed; on an NVIDIA host run "
59
+ "`pip install puremacro[cuda]` (choose the wheel matching your "
60
+ "CUDA toolkit, e.g. cupy-cuda12x)."
61
+ )
62
+ import cupy as cp
63
+
64
+ return cp
65
+ if name == "numba":
66
+ raise ValueError(
67
+ "numba backend uses compiled kernels, not an array namespace; "
68
+ "call the kernels_numba.* functions directly."
69
+ )
70
+ raise ValueError(f"Unknown backend {name!r}; supported: {SUPPORTED}")
71
+
72
+
73
+ def to_numpy(x) -> np.ndarray:
74
+ """Bring an array from any backend back to numpy.
75
+
76
+ cupy blocks implicit conversion, so route it through cp.asnumpy; numpy and
77
+ mlx (which forces evaluation) go through np.asarray.
78
+ """
79
+ if type(x).__module__.split(".")[0] == "cupy":
80
+ import cupy as cp
81
+
82
+ return cp.asnumpy(x)
83
+ return np.asarray(x)
84
+
85
+
86
+ __all__ = [
87
+ "SUPPORTED",
88
+ "backend_available",
89
+ "available_backends",
90
+ "get_array_namespace",
91
+ "to_numpy",
92
+ ]
puremacro/_cache_db.py ADDED
@@ -0,0 +1,216 @@
1
+ """SQLite connection manager + schema bootstrap for puremacro's data
2
+ infrastructure.
3
+
4
+ A single SQLite file at ``~/.cache/puremacro/cache.db`` (overridable
5
+ via ``$PUREMACRO_HTTP_CACHE_DIR``) hosts three tables:
6
+
7
+ - ``http_cache`` — replaces the flat-file HTTP cache; backs
8
+ ``puremacro._http_cache.cache_read``/``cache_write``.
9
+ - ``alfred_vintages`` — persistent FRED-ALFRED vintage panel; backs
10
+ ``puremacro.vintages.AlfredVintageStore``.
11
+ - ``schema_version`` — registry for future migrations.
12
+
13
+ WAL journal mode is enabled so multiple notebooks against the same DB
14
+ do not block each other on writes. One ``sqlite3.Connection`` per
15
+ process is kept alive in a module-level singleton.
16
+ """
17
+ from __future__ import annotations
18
+
19
+ import os
20
+ import sqlite3
21
+ from pathlib import Path
22
+
23
+
24
+ _DDL_HTTP_CACHE = """
25
+ CREATE TABLE IF NOT EXISTS http_cache (
26
+ key TEXT PRIMARY KEY,
27
+ url TEXT NOT NULL,
28
+ fetched_at INTEGER NOT NULL,
29
+ content_type TEXT,
30
+ body BLOB NOT NULL
31
+ );
32
+ """
33
+
34
+ _DDL_HTTP_CACHE_IDX = (
35
+ "CREATE INDEX IF NOT EXISTS http_cache_fetched_at_idx "
36
+ "ON http_cache(fetched_at);"
37
+ )
38
+
39
+ _DDL_ALFRED_VINTAGES = """
40
+ CREATE TABLE IF NOT EXISTS alfred_vintages (
41
+ series_id TEXT NOT NULL,
42
+ observation_date TEXT NOT NULL,
43
+ vintage_date TEXT NOT NULL,
44
+ value REAL,
45
+ PRIMARY KEY (series_id, observation_date, vintage_date)
46
+ );
47
+ """
48
+
49
+ _DDL_ALFRED_VINTAGES_IDX = (
50
+ "CREATE INDEX IF NOT EXISTS alfred_vintages_series_vintage_idx "
51
+ "ON alfred_vintages(series_id, vintage_date);"
52
+ )
53
+
54
+ _DDL_CONNECTOR_EVENTS = """
55
+ CREATE TABLE IF NOT EXISTS connector_events (
56
+ ts INTEGER NOT NULL,
57
+ source TEXT NOT NULL,
58
+ outcome TEXT NOT NULL,
59
+ fallback_used TEXT NOT NULL
60
+ );
61
+ """
62
+
63
+ _DDL_CONNECTOR_EVENTS_IDX = (
64
+ "CREATE INDEX IF NOT EXISTS connector_events_ts_source_idx "
65
+ "ON connector_events(ts, source);"
66
+ )
67
+
68
+ _DDL_SCHEMA_VERSION = """
69
+ CREATE TABLE IF NOT EXISTS schema_version (
70
+ component TEXT PRIMARY KEY,
71
+ version INTEGER NOT NULL
72
+ );
73
+ """
74
+
75
+ _SCHEMA_SEED = [("http_cache", 1), ("alfred_vintages", 1),
76
+ ("connector_events", 1)]
77
+
78
+
79
+ def default_db_path() -> Path:
80
+ """Resolve the canonical cache DB path.
81
+
82
+ ``$PUREMACRO_HTTP_CACHE_DIR`` overrides everything:
83
+ - if it ends with ``.db``, return that path verbatim;
84
+ - otherwise treat as a directory and return ``<dir>/cache.db``.
85
+ Default: ``~/.cache/puremacro/cache.db``.
86
+ """
87
+ env = os.environ.get("PUREMACRO_HTTP_CACHE_DIR")
88
+ if env:
89
+ p = Path(env)
90
+ if p.suffix == ".db":
91
+ return p
92
+ return p / "cache.db"
93
+ return Path.home() / ".cache" / "puremacro" / "cache.db"
94
+
95
+
96
+ def bootstrap_schema(conn: sqlite3.Connection) -> None:
97
+ """Idempotent: create tables + seed schema_version rows if missing."""
98
+ cur = conn.cursor()
99
+ cur.execute(_DDL_HTTP_CACHE)
100
+ cur.execute(_DDL_HTTP_CACHE_IDX)
101
+ cur.execute(_DDL_ALFRED_VINTAGES)
102
+ cur.execute(_DDL_ALFRED_VINTAGES_IDX)
103
+ cur.execute(_DDL_CONNECTOR_EVENTS)
104
+ cur.execute(_DDL_CONNECTOR_EVENTS_IDX)
105
+ cur.execute(_DDL_SCHEMA_VERSION)
106
+ for component, version in _SCHEMA_SEED:
107
+ cur.execute(
108
+ "INSERT OR IGNORE INTO schema_version (component, version) "
109
+ "VALUES (?, ?)",
110
+ (component, version),
111
+ )
112
+ conn.commit()
113
+
114
+
115
+ _CONN: sqlite3.Connection | None = None
116
+ _CONN_PATH: Path | None = None
117
+
118
+
119
+ def get_conn(db_path: Path | None = None) -> sqlite3.Connection:
120
+ """Return the module-level singleton SQLite connection.
121
+
122
+ Lazily opens on first call. Enables WAL mode and bootstraps the
123
+ schema. If called twice with different ``db_path`` arguments
124
+ (e.g., in tests), closes the previous connection and opens a new one.
125
+ """
126
+ global _CONN, _CONN_PATH
127
+ target = db_path or default_db_path()
128
+ if _CONN is not None and _CONN_PATH == target:
129
+ return _CONN
130
+ if _CONN is not None:
131
+ _CONN.close()
132
+ target.parent.mkdir(parents=True, exist_ok=True)
133
+ conn = sqlite3.connect(
134
+ target, timeout=30.0, isolation_level=None, check_same_thread=False,
135
+ )
136
+ conn.execute("PRAGMA journal_mode=WAL")
137
+ conn.execute("PRAGMA synchronous=NORMAL")
138
+ conn.execute("PRAGMA foreign_keys=ON")
139
+ bootstrap_schema(conn)
140
+ _CONN = conn
141
+ _CONN_PATH = target
142
+ return _CONN
143
+
144
+
145
+ def close_conn() -> None:
146
+ """Close the singleton connection (used by tests)."""
147
+ global _CONN, _CONN_PATH
148
+ if _CONN is not None:
149
+ try:
150
+ _CONN.close()
151
+ finally:
152
+ _CONN = None
153
+ _CONN_PATH = None
154
+
155
+
156
+ def migrate_from_flat_files(
157
+ conn: sqlite3.Connection,
158
+ flat_cache_dir: Path,
159
+ *,
160
+ remove: bool = False,
161
+ ) -> int:
162
+ """Walk ``flat_cache_dir`` for ``*.bin`` + ``*.json`` sidecar pairs.
163
+
164
+ Inserts each into ``http_cache`` with ``INSERT OR IGNORE`` semantics
165
+ (idempotent). If ``remove=True``, unlinks each migrated file pair
166
+ after successful insert. Returns the count actually migrated.
167
+
168
+ Failures on a per-file basis are warned-and-skipped, not raised —
169
+ migration is opportunistic.
170
+ """
171
+ import json
172
+ import warnings
173
+
174
+ if not flat_cache_dir.exists():
175
+ return 0
176
+ migrated = 0
177
+ for bin_path in flat_cache_dir.glob("*.bin"):
178
+ sidecar = bin_path.with_suffix(".json")
179
+ if not sidecar.exists():
180
+ continue
181
+ try:
182
+ meta = json.loads(sidecar.read_text())
183
+ url = meta["url"]
184
+ fetched_at = int(float(meta["fetched_at"]))
185
+ content_type = meta.get("content_type")
186
+ body = bin_path.read_bytes()
187
+ key = bin_path.stem
188
+ cur = conn.execute(
189
+ "INSERT OR IGNORE INTO http_cache "
190
+ "(key, url, fetched_at, content_type, body) "
191
+ "VALUES (?, ?, ?, ?, ?)",
192
+ (key, url, fetched_at, content_type, body),
193
+ )
194
+ if cur.rowcount > 0:
195
+ migrated += 1
196
+ if remove:
197
+ bin_path.unlink(missing_ok=True)
198
+ sidecar.unlink(missing_ok=True)
199
+ except (OSError, ValueError, KeyError) as e:
200
+ warnings.warn(
201
+ f"puremacro._cache_db: skipping {bin_path.name}: {e}",
202
+ UserWarning,
203
+ stacklevel=2,
204
+ )
205
+ continue
206
+ conn.commit()
207
+ return migrated
208
+
209
+
210
+ __all__ = [
211
+ "default_db_path",
212
+ "bootstrap_schema",
213
+ "get_conn",
214
+ "close_conn",
215
+ "migrate_from_flat_files",
216
+ ]
puremacro/_codes.py ADDED
@@ -0,0 +1,31 @@
1
+ """Country / aggregate code canonicalization.
2
+
3
+ Used everywhere the panel might receive aggregate codes from upstream
4
+ fetchers (OECD `EA20`, `EU27`, `OECD`; WUI / GPR `WLD`; etc.) so the
5
+ panel only ever contains pure-country observations.
6
+ """
7
+ from __future__ import annotations
8
+
9
+ import pandas as pd
10
+
11
+ IS_AGGREGATE: frozenset[str] = frozenset({
12
+ "EA", "EA12", "EA17", "EA19", "EA20",
13
+ "EU", "EU27", "EU27_2020", "EU28",
14
+ "OECD", "OECDE", "OECDA",
15
+ "G7", "G20", "BRICS",
16
+ "WLD", "WORLD", "ADV", "EME",
17
+ "LATAM", "ASIA", "AFRICA", "MENA",
18
+ })
19
+
20
+
21
+ def is_country(code: object) -> bool:
22
+ """True iff *code* is a length-3 alpha string and not in IS_AGGREGATE."""
23
+ if not isinstance(code, str):
24
+ return False
25
+ return len(code) == 3 and code.isalpha() and code.upper() not in IS_AGGREGATE
26
+
27
+
28
+ def drop_aggregates(df: pd.DataFrame, code_col: str = "code") -> pd.DataFrame:
29
+ """Return a copy of *df* with rows whose *code_col* fails ``is_country`` removed."""
30
+ mask = df[code_col].apply(is_country)
31
+ return df.loc[mask].reset_index(drop=True)
puremacro/_http.py ADDED
@@ -0,0 +1,198 @@
1
+ """Shared HTTP helpers for puremacro fetchers and connectors.
2
+
3
+ This module is the single canonical home of ``safe_get_bytes``,
4
+ ``safe_get_text``, ``safe_get_json`` and the supporting ``USER_AGENT``
5
+ + ``DEFAULT_TIMEOUT`` constants. Promoted from
6
+ ``puremacro.narrative.sources._http`` in 0.6.0 so all fetchers
7
+ (narrative connectors, ``puremacro.fetch.*``, instrument loaders)
8
+ share one hardened path.
9
+
10
+ See ``puremacro/narrative/sources/RETRY_POLICY.md`` for the
11
+ contract every consumer adheres to: 30s default timeout, one-shot
12
+ SSL fallback for older endpoints with stale CA bundles, optional
13
+ keyword-only ``user_agent=`` override (added in 0.4.1) for endpoints
14
+ behind a WAF that blocks the default agent string.
15
+ """
16
+ from __future__ import annotations
17
+
18
+ import json
19
+ import ssl
20
+ import urllib.error
21
+ import urllib.request
22
+
23
+
24
+ USER_AGENT = "Mozilla/5.0 (puremacro/narrative)"
25
+ DEFAULT_TIMEOUT = 30.0
26
+
27
+
28
+ def _request(url: str, timeout: float, user_agent: str | None = None) -> bytes:
29
+ ua = user_agent or USER_AGENT
30
+ req = urllib.request.Request(url, headers={"User-Agent": ua})
31
+ try:
32
+ with urllib.request.urlopen(req, timeout=timeout) as resp:
33
+ return resp.read()
34
+ except (urllib.error.URLError, ssl.SSLError):
35
+ # One-shot fallback: some public endpoints (older OECD / IMF /
36
+ # ministry sites) ship certificates that Python's bundled CA
37
+ # store does not validate. Retry once with verification off.
38
+ # See RETRY_POLICY.md §3 for why we do not loop further.
39
+ ctx = ssl._create_unverified_context()
40
+ with urllib.request.urlopen(req, timeout=timeout, context=ctx) as resp:
41
+ return resp.read()
42
+
43
+
44
+ def safe_get_bytes(url: str, timeout: float = DEFAULT_TIMEOUT,
45
+ *, user_agent: str | None = None) -> bytes:
46
+ """Fetch ``url`` and return raw bytes. SSL fallback applied once.
47
+
48
+ ``user_agent`` overrides the default ``Mozilla/5.0 (puremacro/narrative)``
49
+ UA — needed for endpoints behind a WAF that blocks scripted clients.
50
+ """
51
+ return _request(url, timeout, user_agent=user_agent)
52
+
53
+
54
+ def safe_get_text(url: str, timeout: float = DEFAULT_TIMEOUT,
55
+ *, user_agent: str | None = None) -> str:
56
+ """Fetch ``url`` and return UTF-8 text (decode errors ignored).
57
+
58
+ See ``safe_get_bytes`` for the ``user_agent=`` semantics and
59
+ ``RETRY_POLICY.md §7`` for the WAF-bypass pattern.
60
+ """
61
+ return _request(url, timeout, user_agent=user_agent).decode(
62
+ "utf-8", errors="ignore",
63
+ )
64
+
65
+
66
+ def safe_get_json(url: str, timeout: float = DEFAULT_TIMEOUT,
67
+ *, user_agent: str | None = None) -> dict:
68
+ """Fetch ``url`` and return decoded JSON.
69
+
70
+ Empty / whitespace-only bodies return ``{}`` rather than raising,
71
+ matching the existing API-connector behaviour (e.g. GDELT v2 rate
72
+ limits sometimes return blank pages).
73
+
74
+ See ``safe_get_bytes`` for the ``user_agent=`` semantics and
75
+ ``RETRY_POLICY.md §7`` for the WAF-bypass pattern.
76
+ """
77
+ text = safe_get_text(url, timeout, user_agent=user_agent)
78
+ if not text.strip():
79
+ return {}
80
+ return json.loads(text)
81
+
82
+
83
+ def post_json(url: str, payload: dict, *, timeout: float = DEFAULT_TIMEOUT,
84
+ headers: dict | None = None) -> dict:
85
+ """POST ``payload`` as JSON and return the decoded JSON response.
86
+
87
+ urllib-only (Pyodide-safe). HTTP errors propagate (not retried); a
88
+ transport/SSL error retries once with verification off, matching
89
+ ``_request``. Used by the local-LLM HTTP engine (Ollama / OpenAI-compatible).
90
+ """
91
+ data = json.dumps(payload).encode("utf-8")
92
+ hdrs = {"Content-Type": "application/json", "User-Agent": USER_AGENT}
93
+ if headers:
94
+ hdrs.update(headers)
95
+ req = urllib.request.Request(url, data=data, headers=hdrs, method="POST")
96
+ try:
97
+ with urllib.request.urlopen(req, timeout=timeout) as resp:
98
+ return json.loads(resp.read().decode("utf-8"))
99
+ except urllib.error.HTTPError:
100
+ raise
101
+ except (urllib.error.URLError, ssl.SSLError):
102
+ ctx = ssl._create_unverified_context()
103
+ with urllib.request.urlopen(req, timeout=timeout, context=ctx) as resp:
104
+ return json.loads(resp.read().decode("utf-8"))
105
+
106
+
107
+ # ---------------------------------------------------------------------------
108
+ # Opt-in cached + rate-limited variants (0.60.0)
109
+ #
110
+ # Callers who want caching switch from ``safe_get_text`` to
111
+ # ``safe_get_text_cached``. The existing helpers are unchanged.
112
+ # ---------------------------------------------------------------------------
113
+
114
+ import os
115
+ import time
116
+ import urllib.parse
117
+
118
+ from ._http_cache import default_cache_dir, cache_read, cache_write
119
+
120
+
121
+ # Per-host last-fetch timestamps (monotonic seconds).
122
+ _HOST_LAST_FETCH: dict[str, float] = {}
123
+
124
+
125
+ def _reset_throttle_state() -> None:
126
+ """Clear per-host throttle state. Test helper."""
127
+ _HOST_LAST_FETCH.clear()
128
+
129
+
130
+ def _throttle(host: str, rate_limit_seconds: float) -> None:
131
+ """Sleep until ``rate_limit_seconds`` have elapsed since the last
132
+ fetch to ``host``. No-op on first call to a host."""
133
+ if rate_limit_seconds <= 0:
134
+ return
135
+ now = time.monotonic()
136
+ last = _HOST_LAST_FETCH.get(host)
137
+ if last is not None:
138
+ wait = rate_limit_seconds - (now - last)
139
+ if wait > 0:
140
+ time.sleep(wait)
141
+ _HOST_LAST_FETCH[host] = time.monotonic()
142
+
143
+
144
+ def _host_of(url: str) -> str:
145
+ return urllib.parse.urlparse(url).netloc.lower()
146
+
147
+
148
+ def safe_get_bytes_cached(
149
+ url: str,
150
+ timeout: float = DEFAULT_TIMEOUT,
151
+ *,
152
+ user_agent: str | None = None,
153
+ ttl_seconds: int = 30 * 24 * 3600,
154
+ rate_limit_seconds: float = 0.5,
155
+ ) -> bytes:
156
+ """Like ``safe_get_bytes`` but cached on disk.
157
+
158
+ Cache root: ``$PUREMACRO_HTTP_CACHE_DIR`` or ``~/.cache/puremacro/http``.
159
+ Bypass entirely with ``PUREMACRO_HTTP_NO_CACHE=1``.
160
+ """
161
+ bypass = os.environ.get("PUREMACRO_HTTP_NO_CACHE") == "1"
162
+ cache_dir = default_cache_dir()
163
+ if not bypass:
164
+ hit = cache_read(cache_dir, url, ttl_seconds=ttl_seconds)
165
+ if hit is not None:
166
+ return hit
167
+ _throttle(_host_of(url), rate_limit_seconds)
168
+ body = _request(url, timeout, user_agent=user_agent)
169
+ if not bypass:
170
+ cache_write(cache_dir, url, body)
171
+ return body
172
+
173
+
174
+ def safe_get_text_cached(
175
+ url: str,
176
+ timeout: float = DEFAULT_TIMEOUT,
177
+ *,
178
+ user_agent: str | None = None,
179
+ ttl_seconds: int = 30 * 24 * 3600,
180
+ rate_limit_seconds: float = 0.5,
181
+ ) -> str:
182
+ """Like ``safe_get_text`` but cached on disk. See
183
+ ``safe_get_bytes_cached`` for env-var semantics.
184
+ """
185
+ return safe_get_bytes_cached(
186
+ url, timeout,
187
+ user_agent=user_agent,
188
+ ttl_seconds=ttl_seconds,
189
+ rate_limit_seconds=rate_limit_seconds,
190
+ ).decode("utf-8", errors="ignore")
191
+
192
+
193
+ __all__ = [
194
+ "USER_AGENT", "DEFAULT_TIMEOUT",
195
+ "safe_get_bytes", "safe_get_text", "safe_get_json",
196
+ "post_json",
197
+ "safe_get_bytes_cached", "safe_get_text_cached",
198
+ ]