mostlyrightmd 0.1.0__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (147) hide show
  1. mostlyrightmd-0.1.0/.gitignore +68 -0
  2. mostlyrightmd-0.1.0/PKG-INFO +48 -0
  3. mostlyrightmd-0.1.0/README.md +17 -0
  4. mostlyrightmd-0.1.0/pyproject.toml +76 -0
  5. mostlyrightmd-0.1.0/src/mostlyright/__init__.py +46 -0
  6. mostlyrightmd-0.1.0/src/mostlyright/_compose.py +338 -0
  7. mostlyrightmd-0.1.0/src/mostlyright/_exact_fetch.py +162 -0
  8. mostlyrightmd-0.1.0/src/mostlyright/_internal/__init__.py +31 -0
  9. mostlyrightmd-0.1.0/src/mostlyright/_internal/_bounds.py +167 -0
  10. mostlyrightmd-0.1.0/src/mostlyright/_internal/_cache_dir.py +90 -0
  11. mostlyrightmd-0.1.0/src/mostlyright/_internal/_capabilities.py +274 -0
  12. mostlyrightmd-0.1.0/src/mostlyright/_internal/_convert.py +241 -0
  13. mostlyrightmd-0.1.0/src/mostlyright/_internal/_http.py +71 -0
  14. mostlyrightmd-0.1.0/src/mostlyright/_internal/_pairs.py +522 -0
  15. mostlyrightmd-0.1.0/src/mostlyright/_internal/_pandas_compat.py +75 -0
  16. mostlyrightmd-0.1.0/src/mostlyright/_internal/_stations.py +692 -0
  17. mostlyrightmd-0.1.0/src/mostlyright/_internal/_toon.py +350 -0
  18. mostlyrightmd-0.1.0/src/mostlyright/_internal/exceptions.py +69 -0
  19. mostlyrightmd-0.1.0/src/mostlyright/_internal/merge/__init__.py +29 -0
  20. mostlyrightmd-0.1.0/src/mostlyright/_internal/merge/_schemas.py +120 -0
  21. mostlyrightmd-0.1.0/src/mostlyright/_internal/merge/climate.py +68 -0
  22. mostlyrightmd-0.1.0/src/mostlyright/_internal/merge/observations.py +43 -0
  23. mostlyrightmd-0.1.0/src/mostlyright/_internal/models/__init__.py +7 -0
  24. mostlyrightmd-0.1.0/src/mostlyright/_internal/models/_base.py +49 -0
  25. mostlyrightmd-0.1.0/src/mostlyright/_internal/models/availability.py +69 -0
  26. mostlyrightmd-0.1.0/src/mostlyright/_internal/models/observation.py +129 -0
  27. mostlyrightmd-0.1.0/src/mostlyright/_internal/models/station.py +115 -0
  28. mostlyrightmd-0.1.0/src/mostlyright/_internal/specs/book_snapshot.json +81 -0
  29. mostlyrightmd-0.1.0/src/mostlyright/_internal/specs/brackets.json +63 -0
  30. mostlyrightmd-0.1.0/src/mostlyright/_internal/specs/candle.json +66 -0
  31. mostlyrightmd-0.1.0/src/mostlyright/_internal/specs/climate.json +60 -0
  32. mostlyrightmd-0.1.0/src/mostlyright/_internal/specs/daily_extreme.json +56 -0
  33. mostlyrightmd-0.1.0/src/mostlyright/_internal/specs/data_version.json +38 -0
  34. mostlyrightmd-0.1.0/src/mostlyright/_internal/specs/event.json +62 -0
  35. mostlyrightmd-0.1.0/src/mostlyright/_internal/specs/forecast.json +177 -0
  36. mostlyrightmd-0.1.0/src/mostlyright/_internal/specs/forecast_series.json +128 -0
  37. mostlyrightmd-0.1.0/src/mostlyright/_internal/specs/market.json +63 -0
  38. mostlyrightmd-0.1.0/src/mostlyright/_internal/specs/market_unified.json +92 -0
  39. mostlyrightmd-0.1.0/src/mostlyright/_internal/specs/observation.json +144 -0
  40. mostlyrightmd-0.1.0/src/mostlyright/_internal/specs/observation_ledger.json +50 -0
  41. mostlyrightmd-0.1.0/src/mostlyright/_internal/specs/observation_qc.json +24 -0
  42. mostlyrightmd-0.1.0/src/mostlyright/_internal/specs/omo.json +78 -0
  43. mostlyrightmd-0.1.0/src/mostlyright/_internal/specs/series.json +58 -0
  44. mostlyrightmd-0.1.0/src/mostlyright/_internal/specs/settlement-join.json +76 -0
  45. mostlyrightmd-0.1.0/src/mostlyright/_internal/specs/settlement_record.json +56 -0
  46. mostlyrightmd-0.1.0/src/mostlyright/_internal/specs/snapshot.json +72 -0
  47. mostlyrightmd-0.1.0/src/mostlyright/_internal/specs/synoptic_extremes.json +75 -0
  48. mostlyrightmd-0.1.0/src/mostlyright/_internal/versioning.py +132 -0
  49. mostlyrightmd-0.1.0/src/mostlyright/core/__init__.py +62 -0
  50. mostlyrightmd-0.1.0/src/mostlyright/core/_backend_dispatch.py +174 -0
  51. mostlyrightmd-0.1.0/src/mostlyright/core/_json_safe.py +177 -0
  52. mostlyrightmd-0.1.0/src/mostlyright/core/_narwhals_compat.py +132 -0
  53. mostlyrightmd-0.1.0/src/mostlyright/core/_polars_compat.py +55 -0
  54. mostlyrightmd-0.1.0/src/mostlyright/core/exceptions.py +704 -0
  55. mostlyrightmd-0.1.0/src/mostlyright/core/formats/__init__.py +42 -0
  56. mostlyrightmd-0.1.0/src/mostlyright/core/formats/_toon.py +344 -0
  57. mostlyrightmd-0.1.0/src/mostlyright/core/formats/_toon_list_codec.py +213 -0
  58. mostlyrightmd-0.1.0/src/mostlyright/core/formats/csv.py +57 -0
  59. mostlyrightmd-0.1.0/src/mostlyright/core/formats/dataframe.py +34 -0
  60. mostlyrightmd-0.1.0/src/mostlyright/core/formats/json.py +83 -0
  61. mostlyrightmd-0.1.0/src/mostlyright/core/formats/parquet.py +56 -0
  62. mostlyrightmd-0.1.0/src/mostlyright/core/formats/toon.py +434 -0
  63. mostlyrightmd-0.1.0/src/mostlyright/core/merge.py +129 -0
  64. mostlyrightmd-0.1.0/src/mostlyright/core/result.py +192 -0
  65. mostlyrightmd-0.1.0/src/mostlyright/core/schema.py +334 -0
  66. mostlyrightmd-0.1.0/src/mostlyright/core/schemas/__init__.py +39 -0
  67. mostlyrightmd-0.1.0/src/mostlyright/core/schemas/forecast.py +122 -0
  68. mostlyrightmd-0.1.0/src/mostlyright/core/schemas/forecast_nwp.py +192 -0
  69. mostlyrightmd-0.1.0/src/mostlyright/core/schemas/observation.py +201 -0
  70. mostlyrightmd-0.1.0/src/mostlyright/core/schemas/observation_ledger.py +117 -0
  71. mostlyrightmd-0.1.0/src/mostlyright/core/schemas/observation_qc.py +75 -0
  72. mostlyrightmd-0.1.0/src/mostlyright/core/schemas/settlement.py +164 -0
  73. mostlyrightmd-0.1.0/src/mostlyright/core/temporal/__init__.py +18 -0
  74. mostlyrightmd-0.1.0/src/mostlyright/core/temporal/knowledge_view.py +109 -0
  75. mostlyrightmd-0.1.0/src/mostlyright/core/temporal/leakage.py +147 -0
  76. mostlyrightmd-0.1.0/src/mostlyright/core/temporal/timepoint.py +253 -0
  77. mostlyrightmd-0.1.0/src/mostlyright/core/validator.py +465 -0
  78. mostlyrightmd-0.1.0/src/mostlyright/discover.py +100 -0
  79. mostlyrightmd-0.1.0/src/mostlyright/discovery.py +273 -0
  80. mostlyrightmd-0.1.0/src/mostlyright/forecasts.py +267 -0
  81. mostlyrightmd-0.1.0/src/mostlyright/international.py +423 -0
  82. mostlyrightmd-0.1.0/src/mostlyright/live/__init__.py +39 -0
  83. mostlyrightmd-0.1.0/src/mostlyright/live/_latest.py +194 -0
  84. mostlyrightmd-0.1.0/src/mostlyright/live/_sources.py +106 -0
  85. mostlyrightmd-0.1.0/src/mostlyright/live/_stream.py +108 -0
  86. mostlyrightmd-0.1.0/src/mostlyright/mode2.py +235 -0
  87. mostlyrightmd-0.1.0/src/mostlyright/preprocessing.py +173 -0
  88. mostlyrightmd-0.1.0/src/mostlyright/qc.py +240 -0
  89. mostlyrightmd-0.1.0/src/mostlyright/research.py +1669 -0
  90. mostlyrightmd-0.1.0/src/mostlyright/snapshot.py +504 -0
  91. mostlyrightmd-0.1.0/src/mostlyright/transforms.py +201 -0
  92. mostlyrightmd-0.1.0/tests/_internal/__init__.py +0 -0
  93. mostlyrightmd-0.1.0/tests/_internal/merge/__init__.py +0 -0
  94. mostlyrightmd-0.1.0/tests/_internal/merge/test_awc_gap_filled_by_iem.py +122 -0
  95. mostlyrightmd-0.1.0/tests/_internal/merge/test_climate.py +325 -0
  96. mostlyrightmd-0.1.0/tests/_internal/merge/test_observations.py +233 -0
  97. mostlyrightmd-0.1.0/tests/_internal/models/__init__.py +0 -0
  98. mostlyrightmd-0.1.0/tests/_internal/models/test_availability.py +100 -0
  99. mostlyrightmd-0.1.0/tests/_internal/models/test_base.py +88 -0
  100. mostlyrightmd-0.1.0/tests/_internal/models/test_observation.py +402 -0
  101. mostlyrightmd-0.1.0/tests/_internal/models/test_station.py +173 -0
  102. mostlyrightmd-0.1.0/tests/_internal/test_bounds.py +368 -0
  103. mostlyrightmd-0.1.0/tests/_internal/test_capabilities.py +196 -0
  104. mostlyrightmd-0.1.0/tests/_internal/test_convert.py +475 -0
  105. mostlyrightmd-0.1.0/tests/_internal/test_exceptions.py +118 -0
  106. mostlyrightmd-0.1.0/tests/_internal/test_http.py +164 -0
  107. mostlyrightmd-0.1.0/tests/_internal/test_pairs.py +643 -0
  108. mostlyrightmd-0.1.0/tests/_internal/test_stations.py +218 -0
  109. mostlyrightmd-0.1.0/tests/_internal/test_versioning.py +222 -0
  110. mostlyrightmd-0.1.0/tests/core/__init__.py +0 -0
  111. mostlyrightmd-0.1.0/tests/core/temporal/__init__.py +0 -0
  112. mostlyrightmd-0.1.0/tests/core/temporal/test_knowledge_view.py +210 -0
  113. mostlyrightmd-0.1.0/tests/core/temporal/test_leakage.py +185 -0
  114. mostlyrightmd-0.1.0/tests/core/test_exceptions.py +384 -0
  115. mostlyrightmd-0.1.0/tests/core/test_formats.py +748 -0
  116. mostlyrightmd-0.1.0/tests/core/test_json_safe.py +422 -0
  117. mostlyrightmd-0.1.0/tests/core/test_merge.py +233 -0
  118. mostlyrightmd-0.1.0/tests/core/test_result.py +259 -0
  119. mostlyrightmd-0.1.0/tests/core/test_schema.py +401 -0
  120. mostlyrightmd-0.1.0/tests/core/test_schemas/__init__.py +0 -0
  121. mostlyrightmd-0.1.0/tests/core/test_schemas/test_forecast.py +104 -0
  122. mostlyrightmd-0.1.0/tests/core/test_schemas/test_observation.py +172 -0
  123. mostlyrightmd-0.1.0/tests/core/test_schemas/test_settlement.py +129 -0
  124. mostlyrightmd-0.1.0/tests/core/test_timepoint.py +580 -0
  125. mostlyrightmd-0.1.0/tests/core/test_validator.py +404 -0
  126. mostlyrightmd-0.1.0/tests/test_backend_dispatch.py +178 -0
  127. mostlyrightmd-0.1.0/tests/test_cache_env_back_compat.py +50 -0
  128. mostlyrightmd-0.1.0/tests/test_compose.py +371 -0
  129. mostlyrightmd-0.1.0/tests/test_discover.py +97 -0
  130. mostlyrightmd-0.1.0/tests/test_discovery_real.py +289 -0
  131. mostlyrightmd-0.1.0/tests/test_exact_fetch.py +194 -0
  132. mostlyrightmd-0.1.0/tests/test_exceptions_phase17.py +81 -0
  133. mostlyrightmd-0.1.0/tests/test_exceptions_phase17_plan06.py +78 -0
  134. mostlyrightmd-0.1.0/tests/test_forecast_nwp_schema_phase17.py +128 -0
  135. mostlyrightmd-0.1.0/tests/test_international.py +429 -0
  136. mostlyrightmd-0.1.0/tests/test_live_latest.py +332 -0
  137. mostlyrightmd-0.1.0/tests/test_live_stream.py +422 -0
  138. mostlyrightmd-0.1.0/tests/test_mode2.py +182 -0
  139. mostlyrightmd-0.1.0/tests/test_namespace.py +39 -0
  140. mostlyrightmd-0.1.0/tests/test_phase_3x.py +389 -0
  141. mostlyrightmd-0.1.0/tests/test_polars_cross_backend.py +223 -0
  142. mostlyrightmd-0.1.0/tests/test_polars_invariants.py +155 -0
  143. mostlyrightmd-0.1.0/tests/test_qc_wired.py +433 -0
  144. mostlyrightmd-0.1.0/tests/test_research.py +1173 -0
  145. mostlyrightmd-0.1.0/tests/test_research_prefetch.py +377 -0
  146. mostlyrightmd-0.1.0/tests/test_snapshot.py +512 -0
  147. mostlyrightmd-0.1.0/tests/test_transforms_preprocessing.py +291 -0
@@ -0,0 +1,68 @@
1
+ # Python
2
+ __pycache__/
3
+ *.py[cod]
4
+ *$py.class
5
+ *.so
6
+ *.egg
7
+ *.egg-info/
8
+ build/
9
+ dist/
10
+ .eggs/
11
+
12
+ # Virtual environments
13
+ .venv/
14
+ venv/
15
+ ENV/
16
+
17
+ # Testing / linting
18
+ .pytest_cache/
19
+ .ruff_cache/
20
+ .coverage
21
+ .coverage.*
22
+ htmlcov/
23
+ .tox/
24
+
25
+ # Local cache (tradewinds runtime cache lives in $HOME/.tradewinds/; this catches any test-time fixtures that leak)
26
+ .tradewinds-cache/
27
+
28
+ # Phase 4 CI-05 — drift watchdog populates tests/fixtures/drift/ from the
29
+ # weekly cron. The scripts + README are tracked; the captured parquets
30
+ # and the drift-report are not (rotation is the whole point — git history
31
+ # isn't the storage layer for drift fixtures).
32
+ tests/fixtures/drift/case_*.parquet
33
+ tests/fixtures/drift/drift-report.md
34
+
35
+ # uv — uv.lock IS committed for dev reproducibility across Lane F + Lane V (decision: lockfile-tracked for dev parity)
36
+
37
+ # IDE
38
+ .vscode/
39
+ .idea/
40
+ *.swp
41
+ *.swo
42
+
43
+ # OS
44
+ .DS_Store
45
+ Thumbs.db
46
+ .claude/scheduled_tasks.lock
47
+
48
+ # Planning artifacts — local GSD planning workspace; NOT for public consumption.
49
+ # Untracked since the repo went public (Phase 13 W0 cleanup commit, 2026-05-25).
50
+ # Operators keep these on disk for ongoing GSD workflow; nothing in `.planning/`
51
+ # should be required to build or run the SDK.
52
+ .planning/
53
+
54
+ # Dev-only TODO scratchpad (replaced by GSD TODOS in .planning/)
55
+ TODOS.md
56
+
57
+ # TypeScript / pnpm workspace (packages-ts/)
58
+ node_modules/
59
+ coverage/
60
+ .tsbuildinfo
61
+ packages-ts/*/dist/
62
+ packages-ts/*/coverage/
63
+ # Note: top-level `dist/` and `build/` already covered by Python section above.
64
+
65
+ # TS-W2 Plan 08 drift watchdog — output files are transient, not committed.
66
+ packages-ts/meta/tests/parity/drift/*.json
67
+ packages-ts/meta/tests/parity/drift-report.md
68
+ .claude/
@@ -0,0 +1,48 @@
1
+ Metadata-Version: 2.4
2
+ Name: mostlyrightmd
3
+ Version: 0.1.0
4
+ Summary: Local-first SDK for quants researching prediction-market weather settlements. Meta package: research() join + shared utilities. Python module: `import mostlyright`.
5
+ Author-email: Robert Tarabcak <tarabcakr@gmail.com>
6
+ License-Expression: MIT
7
+ Keywords: kalshi,metar,polymarket,quantitative-finance,weather
8
+ Classifier: Development Status :: 2 - Pre-Alpha
9
+ Classifier: Intended Audience :: Financial and Insurance Industry
10
+ Classifier: License :: OSI Approved :: MIT License
11
+ Classifier: Programming Language :: Python :: 3.11
12
+ Classifier: Programming Language :: Python :: 3.12
13
+ Classifier: Programming Language :: Python :: 3.13
14
+ Requires-Python: >=3.11
15
+ Requires-Dist: httpx>=0.27
16
+ Requires-Dist: jsonschema>=4.21
17
+ Requires-Dist: tzdata; sys_platform == 'win32'
18
+ Provides-Extra: parquet
19
+ Requires-Dist: pandas<4.0,>=2.2; extra == 'parquet'
20
+ Requires-Dist: pyarrow<24.0,>=17.0; extra == 'parquet'
21
+ Provides-Extra: polars
22
+ Requires-Dist: narwhals<2.0,>=1.20; extra == 'polars'
23
+ Requires-Dist: pandas<4.0,>=2.2; extra == 'polars'
24
+ Requires-Dist: polars<2.0,>=1.0; extra == 'polars'
25
+ Requires-Dist: pyarrow<24.0,>=17.0; extra == 'polars'
26
+ Provides-Extra: research
27
+ Requires-Dist: mostlyrightmd-weather<0.2,>=0.1.0; extra == 'research'
28
+ Requires-Dist: pandas<4.0,>=2.2; extra == 'research'
29
+ Requires-Dist: pyarrow<24.0,>=17.0; extra == 'research'
30
+ Description-Content-Type: text/markdown
31
+
32
+ # mostlyrightmd
33
+
34
+ Local-first SDK for quants researching prediction-market weather settlements.
35
+
36
+ Meta package: exposes `mostlyright.research()` (the observation × climate join) and re-exports common utilities.
37
+
38
+ ## Install
39
+
40
+ ```bash
41
+ pip install mostlyrightmd # core only (helpers + snapshot primitives)
42
+ pip install mostlyrightmd[research] # core + weather, enables `mostlyright.research()`
43
+ pip install mostlyrightmd-weather # weather data sources (transitively brings core)
44
+ ```
45
+
46
+ `mostlyrightmd` and `mostlyrightmd-weather` share the `mostlyright.*` Python namespace but ship as separate PyPI distributions so users who need only the helpers can skip the heavier weather deps. `research()` lazy-imports `mostlyright.weather` and raises a clear error if the weather package is not installed.
47
+
48
+ See the workspace [README](../../README.md) and [CLAUDE.md](../../CLAUDE.md) for project rules.
@@ -0,0 +1,17 @@
1
+ # mostlyrightmd
2
+
3
+ Local-first SDK for quants researching prediction-market weather settlements.
4
+
5
+ Meta package: exposes `mostlyright.research()` (the observation × climate join) and re-exports common utilities.
6
+
7
+ ## Install
8
+
9
+ ```bash
10
+ pip install mostlyrightmd # core only (helpers + snapshot primitives)
11
+ pip install mostlyrightmd[research] # core + weather, enables `mostlyright.research()`
12
+ pip install mostlyrightmd-weather # weather data sources (transitively brings core)
13
+ ```
14
+
15
+ `mostlyrightmd` and `mostlyrightmd-weather` share the `mostlyright.*` Python namespace but ship as separate PyPI distributions so users who need only the helpers can skip the heavier weather deps. `research()` lazy-imports `mostlyright.weather` and raises a clear error if the weather package is not installed.
16
+
17
+ See the workspace [README](../../README.md) and [CLAUDE.md](../../CLAUDE.md) for project rules.
@@ -0,0 +1,76 @@
1
+ [project]
2
+ name = "mostlyrightmd"
3
+ version = "0.1.0"
4
+ description = "Local-first SDK for quants researching prediction-market weather settlements. Meta package: research() join + shared utilities. Python module: `import mostlyright`."
5
+ readme = "README.md"
6
+ license = "MIT"
7
+ authors = [{ name = "Robert Tarabcak", email = "tarabcakr@gmail.com" }]
8
+ requires-python = ">=3.11"
9
+ keywords = ["weather", "quantitative-finance", "kalshi", "polymarket", "metar"]
10
+ classifiers = [
11
+ "Development Status :: 2 - Pre-Alpha",
12
+ "Intended Audience :: Financial and Insurance Industry",
13
+ "License :: OSI Approved :: MIT License",
14
+ "Programming Language :: Python :: 3.11",
15
+ "Programming Language :: Python :: 3.12",
16
+ "Programming Language :: Python :: 3.13",
17
+ ]
18
+ dependencies = [
19
+ # Pinned to v0.14.1 mostlyright deps for inherited test coverage (per TODOS.md item 1)
20
+ "httpx>=0.27",
21
+ "jsonschema>=4.21",
22
+ "tzdata; sys_platform == 'win32'",
23
+ # NOTE: `mostlyrightmd-weather` is INTENTIONALLY NOT in runtime deps.
24
+ # Listing it here creates a runtime cycle (core <-> weather) that defeats
25
+ # the three-package split — see .planning/REQUIREMENTS.md RESEARCH-05.
26
+ # Users who want `mostlyright.research()` install `mostlyrightmd[research]`
27
+ # (or `mostlyrightmd-weather` directly, which depends on core transitively).
28
+ ]
29
+
30
+ [project.optional-dependencies]
31
+ # DataFrame return + parquet cache. Matches v0.14.1's [parquet] extra.
32
+ # Phase 6 (PANDAS3-02): the pandas<3.0 cap is dropped. Pandas 3 byte-
33
+ # equivalence is enforced via the dual-pandas CI matrix (W1-T4) +
34
+ # tests/fixtures/parity/coerce_pd3.py invertible bridge (W1-T5) +
35
+ # ulp_drift_pd3.json measurement artifact, NOT a recapture-on-3.x cap.
36
+ # CLAUDE.md "Data + parity rules" updated alongside this drop.
37
+ parquet = [
38
+ "pyarrow>=17.0,<24.0",
39
+ "pandas>=2.2,<4.0",
40
+ ]
41
+ # Pull in the sibling weather package + pandas/pyarrow so the default
42
+ # `mostlyright.research(...)` call returns a DataFrame without `ImportError`.
43
+ # Codex iter-1 P2 fix: previously `research` listed only `mostlyrightmd-weather`,
44
+ # leaving the soft `import pandas` in `pairs_to_dataframe` to blow up at call
45
+ # time because `as_dataframe=True` is the default. Phase 6 (PANDAS3-02):
46
+ # pandas upper bound aligned with `parquet` extra at <4.0; both backends
47
+ # are exercised by the dual-pandas CI matrix.
48
+ research = [
49
+ "mostlyrightmd-weather>=0.1.0,<0.2",
50
+ "pyarrow>=17.0,<24.0",
51
+ "pandas>=2.2,<4.0",
52
+ ]
53
+ # Phase 6 (POLARS-05): opt-in polars backend. Default install never
54
+ # pulls polars; calling `backend="polars"` without the extra raises
55
+ # SourceUnavailableError with an install hint (mirrors the [nwp] pattern).
56
+ # narwhals is the cross-backend abstraction layer the cleanly-portable
57
+ # modules go through. pandas is required because the boundary shim
58
+ # (core/_narwhals_compat.py) converts polars↔pandas via `pl.from_pandas`
59
+ # and `pl.DataFrame.to_pandas()` — codex iter-3 P2 fix; a clean
60
+ # `pip install mostlyrightmd[polars]` would otherwise fail at first
61
+ # `transforms.lag(pl_df, ...)` call with ImportError. pyarrow is
62
+ # required by pl.from_pandas/to_pandas for non-trivial dtypes
63
+ # (codex iter-5 P1 fix).
64
+ polars = [
65
+ "polars>=1.0,<2.0",
66
+ "narwhals>=1.20,<2.0",
67
+ "pandas>=2.2,<4.0",
68
+ "pyarrow>=17.0,<24.0",
69
+ ]
70
+
71
+ [build-system]
72
+ requires = ["hatchling"]
73
+ build-backend = "hatchling.build"
74
+
75
+ [tool.hatch.build.targets.wheel]
76
+ packages = ["src/mostlyright"]
@@ -0,0 +1,46 @@
1
+ """mostlyright — local-first SDK for prediction-market weather settlement research.
2
+
3
+ Sprint 0 v0.1.0 ships:
4
+ - ``mostlyright.research(station, from_date, to_date, ...)`` — the v0.14.1 ``pairs()`` join,
5
+ lifted from monorepo-v0.14.1, calling AWC + IEM + GHCNh + NWS CLI directly.
6
+ - ``mostlyright.snapshot`` — settlement-window math (LST, market_close_utc).
7
+
8
+ Adjacent surfaces:
9
+ - ``mostlyright.weather`` — observations + climate + forecasts (sibling package ``mostlyrightmd-weather``).
10
+ - ``mostlyright.markets`` — Kalshi + Polymarket metadata (sibling package ``mostlyrightmd-markets``,
11
+ ships v0.1.0 in Sprint 0.5).
12
+
13
+ Namespace note: ``mostlyright`` is a split-distribution namespace package. Core owns this
14
+ ``__init__.py``; sibling distributions ``mostlyrightmd-weather`` and ``mostlyrightmd-markets`` ship
15
+ subdirectories (``mostlyright/weather/``, ``mostlyright/markets/``) WITHOUT their own
16
+ namespace-root ``__init__.py``. The pkgutil declaration below extends ``__path__`` so Python's
17
+ import machinery finds those subpackages from whichever site-packages location installed them.
18
+ """
19
+
20
+ # Split-distribution namespace: extend __path__ to discover sibling packages' contributions.
21
+ __path__ = __import__("pkgutil").extend_path(__path__, __name__)
22
+
23
+ __version__ = "0.1.0rc1"
24
+
25
+ from mostlyright.discover import discover
26
+ from mostlyright.research import research
27
+
28
+ __all__ = ["__version__", "discover", "live", "research"]
29
+
30
+
31
+ # Lazy `mostlyright.live` access (Phase 11). Both `discover` and `research`
32
+ # above already eagerly import `mostlyright.core` (which pulls pandas via
33
+ # `core.validator`), so the eager-pandas path is pre-existing and NOT a
34
+ # Phase 11 regression. Even so, we expose `live` through a module-level
35
+ # `__getattr__` hook so `import mostlyright` doesn't pull in
36
+ # `mostlyright.weather` (via the live module's deferred fetcher imports
37
+ # that fire on first use, not first attribute access). First access via
38
+ # `mostlyright.live.stream(...)` resolves and caches the submodule.
39
+ def __getattr__(name: str):
40
+ if name == "live":
41
+ import mostlyright.live as _live
42
+
43
+ # Cache on the module so subsequent accesses skip __getattr__.
44
+ globals()["live"] = _live
45
+ return _live
46
+ raise AttributeError(f"module {__name__!r} has no attribute {name!r}")
@@ -0,0 +1,338 @@
1
+ """Phase 10 — composable ``research()`` dispatcher.
2
+
3
+ Translates the new selectors (``city=``, ``contract=``, ``contracts=``)
4
+ into resolution metadata + station tuples that the existing
5
+ station-based ``research()`` machinery consumes. Cross-issuer annotation
6
+ (``settles_for``) is computed here so the dispatch layer is the single
7
+ source of truth for "which markets settle against which stations."
8
+
9
+ The dispatcher is intentionally pure (no I/O, no DataFrame
10
+ construction) so unit tests run instantly and the same logic can be
11
+ reused by ``discover()`` and the TS counterpart.
12
+ """
13
+
14
+ from __future__ import annotations
15
+
16
+ import warnings
17
+ from typing import Any
18
+
19
+ #: The valid selector kwarg names. Exactly one must be provided on each
20
+ #: ``research()`` invocation; passing zero or >1 raises ``ValueError``.
21
+ _SELECTOR_NAMES: tuple[str, ...] = ("station", "city", "contract", "contracts")
22
+
23
+
24
+ #: Kalshi short-ticker → canonical city slug. Real Kalshi tickers use
25
+ #: variable-length city suffixes: ``KXHIGHNY-...`` (NY → NYC),
26
+ #: ``KXHIGHCHI-...`` (CHI → CHI), ``KXHIGHLAX-...`` (LAX → LAX). The
27
+ #: ``KALSHI_SETTLEMENT_STATIONS`` catalog is keyed by the canonical
28
+ #: 3-letter city slug; this alias table normalizes the variable-length
29
+ #: Kalshi suffix to the catalog key before lookup. Phase 10 iter-1 codex
30
+ #: HIGH: without this, ``kalshi:KXHIGHNY-25MAY26-T79`` (the actual
31
+ #: ROADMAP example) would fail to resolve.
32
+ _KALSHI_TICKER_ALIASES: dict[str, str] = {
33
+ "NY": "NYC",
34
+ # All other Kalshi cities use the canonical 3-letter slug as their
35
+ # ticker suffix verbatim (identity mapping is implicit).
36
+ }
37
+
38
+
39
+ #: Kalshi-short ↔ Polymarket-long city slug alias. Architect iter-1 HIGH:
40
+ #: ``resolve_city`` and ``annotate_settles_for`` need to recognize BOTH
41
+ #: forms so a single call with EITHER input surfaces the cross-issuer
42
+ #: settlement neighborhood. Without this, ``resolve_city("LAX")`` would
43
+ #: miss Polymarket's KLAX entry (Polymarket keys it as ``los_angeles``);
44
+ #: ``resolve_city("chicago")`` would miss Kalshi's KMDW (Kalshi keys it
45
+ #: as ``CHI``). Bi-directional table — looked up either way.
46
+ _CITY_SLUG_ALIASES: dict[str, tuple[str, str]] = {
47
+ # short_kalshi: (long_polymarket, canonical_kalshi_upper)
48
+ "nyc": ("nyc", "NYC"),
49
+ "chi": ("chicago", "CHI"),
50
+ "lax": ("los_angeles", "LAX"),
51
+ "mia": ("miami", "MIA"),
52
+ "den": ("denver", "DEN"),
53
+ "bos": ("boston", "BOS"),
54
+ "aus": ("austin", "AUS"),
55
+ "dca": ("washington_dc", "DCA"),
56
+ "phl": ("philadelphia", "PHL"),
57
+ "sfo": ("san_francisco", "SFO"),
58
+ "sea": ("seattle", "SEA"),
59
+ "atl": ("atlanta", "ATL"),
60
+ "hou": ("houston", "HOU"),
61
+ "dal": ("dallas", "DAL"),
62
+ "phx": ("phoenix", "PHX"),
63
+ "msp": ("minneapolis", "MSP"),
64
+ "dtw": ("detroit", "DTW"),
65
+ }
66
+
67
+ # Build reverse lookup so passing the Polymarket long form also surfaces
68
+ # the Kalshi short form.
69
+ _CITY_SLUG_ALIASES_REVERSE: dict[str, tuple[str, str]] = {
70
+ long_poly: (short_kalshi, kalshi_upper)
71
+ for short_kalshi, (long_poly, kalshi_upper) in _CITY_SLUG_ALIASES.items()
72
+ }
73
+
74
+
75
+ def _normalize_city_slugs(city: str) -> tuple[str, str]:
76
+ """Return ``(polymarket_slug_lower, kalshi_slug_upper)`` for ``city``.
77
+
78
+ Accepts either form (``"nyc"`` or ``"NYC"``, ``"chicago"`` or ``"CHI"``)
79
+ and returns both canonical forms so callers can probe either catalog.
80
+
81
+ Falls back to ``(city.lower(), city.upper())`` for cities not in the
82
+ alias table (international cities the user might pass).
83
+ """
84
+ lower = city.lower()
85
+ upper = city.upper()
86
+ if lower in _CITY_SLUG_ALIASES:
87
+ long_poly, kalshi_upper = _CITY_SLUG_ALIASES[lower]
88
+ return long_poly, kalshi_upper
89
+ if lower in _CITY_SLUG_ALIASES_REVERSE:
90
+ short_kalshi, kalshi_upper = _CITY_SLUG_ALIASES_REVERSE[lower]
91
+ return lower, kalshi_upper
92
+ return lower, upper
93
+
94
+
95
+ class StationOverrideWarning(UserWarning):
96
+ """Emitted when ``station_override=`` deliberately mismatches the
97
+ contract's canonical settlement station.
98
+
99
+ The output row carries ``settlement_mismatch=True`` so downstream
100
+ backtest code can filter / flag these silently-divergent rows.
101
+ """
102
+
103
+
104
+ def validate_selectors(
105
+ *,
106
+ station: str | None = None,
107
+ city: str | None = None,
108
+ contract: str | None = None,
109
+ contracts: list[str] | tuple[str, ...] | None = None,
110
+ ) -> str:
111
+ """Validate that exactly one selector is provided; return the active name.
112
+
113
+ Args:
114
+ station, city, contract, contracts: the four mutually-exclusive
115
+ selectors. Exactly one must be non-None / non-empty.
116
+
117
+ Returns:
118
+ The name of the active selector (``"station"`` / ``"city"`` /
119
+ ``"contract"`` / ``"contracts"``).
120
+
121
+ Raises:
122
+ ValueError: zero or >1 selectors provided.
123
+ """
124
+ provided: list[str] = []
125
+ if station is not None and station != "":
126
+ provided.append("station")
127
+ if city is not None and city != "":
128
+ provided.append("city")
129
+ if contract is not None and contract != "":
130
+ provided.append("contract")
131
+ if contracts is not None and len(contracts) > 0:
132
+ provided.append("contracts")
133
+ if not provided:
134
+ raise ValueError(
135
+ "research(): exactly one of station=, city=, contract=, contracts= must be provided"
136
+ )
137
+ if len(provided) > 1:
138
+ raise ValueError(f"research(): selectors are mutually exclusive; got {provided!r}")
139
+ return provided[0]
140
+
141
+
142
+ def resolve_contract(contract_id: str) -> tuple[str, str]:
143
+ """Resolve a ``"<issuer>:<id>"`` string to ``(station, issuer)``.
144
+
145
+ Supported issuers:
146
+ - ``kalshi:`` — ``KHIGH*``/``KXHIGH*``/``KLOW*``/``KXLOW*`` city tickers.
147
+ - ``polymarket:`` — event/market ids. v0.2 raises NotImplementedError
148
+ with an actionable message (the resolver lives in
149
+ :mod:`mostlyright.markets._per_event_station` but requires a fetched
150
+ event payload to identify the city; Phase 10 v0.2 surfaces this as
151
+ a clear error and defers the integration to v0.3).
152
+
153
+ Args:
154
+ contract_id: ``"<issuer>:<id>"`` string (e.g.
155
+ ``"kalshi:KXHIGHNYC"`` or ``"polymarket:0x..."``).
156
+
157
+ Returns:
158
+ Tuple of ``(station_icao, issuer_name)``.
159
+
160
+ Raises:
161
+ ValueError: malformed contract id or unknown issuer.
162
+ NotImplementedError: Polymarket contract resolution (deferred).
163
+ """
164
+ if not isinstance(contract_id, str) or ":" not in contract_id:
165
+ raise ValueError(f"contract id must be `<issuer>:<id>`; got {contract_id!r}")
166
+ issuer, raw = contract_id.split(":", 1)
167
+ issuer = issuer.lower()
168
+ raw_upper = raw.upper()
169
+ if issuer == "kalshi":
170
+ from datetime import date as _date
171
+
172
+ from mostlyright.markets.catalog import kalshi_nhigh, kalshi_nlow
173
+
174
+ # Kalshi tickers come in two prefix families:
175
+ # KHIGH<CITY>* / KXHIGH<CITY>* → NHIGH (daily-high)
176
+ # KLOW<CITY>* / KXLOW<CITY>* → NLOW (daily-low)
177
+ # The existing kalshi_nhigh / kalshi_nlow resolvers were built for
178
+ # the legacy KHIGH<CITY> / KLOW<CITY> shape. Modern Kalshi market
179
+ # tickers use the KX-prefix exchange convention
180
+ # (KXHIGH<CITY>-<DATE>-<STRIKE>); strip the `KX` to feed the
181
+ # resolver and pass the bare city portion. The resolver's own
182
+ # validation (`startswith("KHIGH")` / `startswith("KLOW")` +
183
+ # length>5) does the city-ticker validity check.
184
+ # Strip just the 'X' from the KX exchange prefix so KXHIGH<CITY>
185
+ # becomes KHIGH<CITY> (the legacy resolver's expected format).
186
+ # KX = position [0..1] but the literal 'K' is kept; drop position [1].
187
+ normalized = raw_upper
188
+ if normalized.startswith("KX"):
189
+ normalized = "K" + normalized[2:] # KXHIGHNYC → KHIGHNYC
190
+ # Many full Kalshi tickers carry a trailing -DATE-STRIKE suffix
191
+ # (e.g. KXHIGHNYC-25MAY26-T79 → KXHIGHNYC). Pull the city portion
192
+ # by trimming at the first '-'.
193
+ city_only = normalized.split("-", 1)[0]
194
+ # Extract the variable-length city suffix and normalize via the
195
+ # Kalshi-ticker alias table so KXHIGHNY → NY → NYC (the canonical
196
+ # catalog key). Iter-1 codex HIGH.
197
+ if city_only.startswith("KHIGH") and len(city_only) > 5:
198
+ short = city_only[5:]
199
+ canonical = _KALSHI_TICKER_ALIASES.get(short, short)
200
+ r = kalshi_nhigh.resolve(f"KHIGH{canonical}", _date.today())
201
+ return r.settlement_station, "kalshi"
202
+ if city_only.startswith("KLOW") and len(city_only) > 4:
203
+ short = city_only[4:]
204
+ canonical = _KALSHI_TICKER_ALIASES.get(short, short)
205
+ r = kalshi_nlow.resolve(f"KLOW{canonical}", _date.today())
206
+ return r.settlement_station, "kalshi"
207
+ raise ValueError(
208
+ f"unsupported kalshi contract format: {raw!r}; "
209
+ "expected KHIGH<CITY>* / KXHIGH<CITY>* / KLOW<CITY>* / "
210
+ "KXLOW<CITY>* prefix"
211
+ )
212
+ if issuer == "polymarket":
213
+ raise NotImplementedError(
214
+ "polymarket contract resolution requires event_id → station lookup "
215
+ "via polymarket_discover() or polymarket_settle(); Phase 10 v0.2 "
216
+ "defers this integration to v0.3. Use `city='nyc'` or pass the "
217
+ "station explicitly via `station_override=` until then."
218
+ )
219
+ raise ValueError(f"unknown issuer prefix: {issuer!r}; expected kalshi or polymarket")
220
+
221
+
222
+ def resolve_city(city: str) -> tuple[str, ...]:
223
+ """Resolve a city slug to all stations any issuer settles against.
224
+
225
+ Returns a deduplicated tuple in stable order:
226
+ 1. Kalshi's settlement station (if the city is in the Kalshi catalog).
227
+ 2. Polymarket's default + high + low stations (if in Polymarket catalog).
228
+ 3. Polymarket per-city denylist entries (forbidden-but-known stations
229
+ surfaced so quants can SEE the full neighborhood for explicit
230
+ ``station_override=``).
231
+
232
+ For ``"NYC"`` returns (``"KNYC"``, ``"KLGA"``, ``"KJFK"``, ``"KEWR"``)
233
+ — KNYC is Kalshi's, KLGA is Polymarket's, KJFK + KEWR are the
234
+ denylist backstops Polymarket forbids.
235
+
236
+ Args:
237
+ city: city slug. Accepts ``"NYC"`` (Kalshi upper) or ``"nyc"``
238
+ (Polymarket lower); both are normalized.
239
+
240
+ Returns:
241
+ Tuple of station ICAOs.
242
+
243
+ Raises:
244
+ ValueError: city not in either catalog.
245
+ """
246
+ if not isinstance(city, str) or not city:
247
+ raise ValueError(f"city must be a non-empty str; got {city!r}")
248
+
249
+ from mostlyright.markets._per_event_station import load_polymarket_city_stations
250
+ from mostlyright.markets.catalog.kalshi_stations import (
251
+ KALSHI_SETTLEMENT_STATIONS,
252
+ )
253
+ from mostlyright.markets.polymarket import KNOWN_WRONG_STATIONS as POLY_WRONG
254
+
255
+ # Iter-1 python-architect HIGH: normalize via the cross-issuer slug
256
+ # alias table so a single call (with either "NYC" or "nyc", "CHI" or
257
+ # "chicago", "LAX" or "los_angeles") surfaces the full cross-issuer
258
+ # settlement neighborhood from BOTH catalogs.
259
+ poly_slug, kalshi_slug = _normalize_city_slugs(city)
260
+ out: list[str] = []
261
+ if kalshi_slug in KALSHI_SETTLEMENT_STATIONS:
262
+ out.append(KALSHI_SETTLEMENT_STATIONS[kalshi_slug].station)
263
+ poly = load_polymarket_city_stations()
264
+ if poly_slug in poly:
265
+ # Preserve insertion order across the measure keys.
266
+ for measure in ("default", "high", "low"):
267
+ st = poly[poly_slug].get(measure)
268
+ if st and st not in out:
269
+ out.append(st)
270
+ for st in sorted(POLY_WRONG.get(poly_slug, frozenset())):
271
+ if st not in out:
272
+ out.append(st)
273
+ if not out:
274
+ raise ValueError(f"unknown city {city!r}; not in kalshi or polymarket catalogs")
275
+ return tuple(out)
276
+
277
+
278
+ def annotate_settles_for(station: str, city: str | None) -> list[str]:
279
+ """Return the list of ``"<issuer>:<ticker>"`` markers that settle
280
+ against ``station`` for ``city``.
281
+
282
+ Empty list means no known issuer settles against this station for
283
+ this city (typically a denylist entry surfaced by
284
+ :func:`resolve_city` for the caller's awareness).
285
+
286
+ Args:
287
+ station: 4-char K-prefix ICAO.
288
+ city: city slug (optional; when None, returns empty list).
289
+
290
+ Returns:
291
+ Sorted list of ``"kalshi:CITY"`` / ``"polymarket:city"`` markers.
292
+ """
293
+ out: list[str] = []
294
+ if city is None:
295
+ return out
296
+ from mostlyright.markets._per_event_station import load_polymarket_city_stations
297
+ from mostlyright.markets.catalog.kalshi_stations import (
298
+ KALSHI_SETTLEMENT_STATIONS,
299
+ )
300
+
301
+ # Iter-1 python-architect HIGH: use cross-issuer slug alias so the
302
+ # annotation works regardless of which slug-form the caller passed.
303
+ poly_slug, kalshi_slug = _normalize_city_slugs(city)
304
+ if (
305
+ kalshi_slug in KALSHI_SETTLEMENT_STATIONS
306
+ and KALSHI_SETTLEMENT_STATIONS[kalshi_slug].station == station
307
+ ):
308
+ out.append(f"kalshi:{kalshi_slug}")
309
+ poly = load_polymarket_city_stations()
310
+ if poly_slug in poly and station in poly[poly_slug].values():
311
+ out.append(f"polymarket:{poly_slug}")
312
+ return sorted(out)
313
+
314
+
315
+ def emit_override_warning(contract_station: str, override_station: str) -> None:
316
+ """Helper: emit :class:`StationOverrideWarning` for a deliberate mismatch."""
317
+ warnings.warn(
318
+ f"station_override={override_station!r} differs from contract's "
319
+ f"canonical settlement station {contract_station!r}; output row will "
320
+ f"carry settlement_mismatch=True",
321
+ StationOverrideWarning,
322
+ stacklevel=3,
323
+ )
324
+
325
+
326
+ __all__ = [
327
+ "StationOverrideWarning",
328
+ "annotate_settles_for",
329
+ "emit_override_warning",
330
+ "resolve_city",
331
+ "resolve_contract",
332
+ "validate_selectors",
333
+ ]
334
+
335
+
336
+ # Silence the `Any` import warning — kept for ruff future-proofing if/when
337
+ # the dispatch layer needs to type DataFrame returns.
338
+ _ = Any