station-studio 1.5.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 (120) hide show
  1. api/__init__.py +0 -0
  2. api/deps.py +117 -0
  3. api/job_runner.py +674 -0
  4. api/main.py +333 -0
  5. api/routers/__init__.py +0 -0
  6. api/routers/editor.py +1633 -0
  7. api/routers/generate.py +691 -0
  8. api/routers/jobs.py +616 -0
  9. api/routers/meta.py +41 -0
  10. api/routers/rules.py +164 -0
  11. api/routers/sources.py +184 -0
  12. api/routers/stations.py +540 -0
  13. api/schemas.py +921 -0
  14. api/timefmt.py +20 -0
  15. engine/__init__.py +7 -0
  16. engine/artist_match.py +203 -0
  17. engine/database.py +33 -0
  18. engine/divergence.py +336 -0
  19. engine/editor_metadata.py +1280 -0
  20. engine/exporter.py +718 -0
  21. engine/generator.py +1161 -0
  22. engine/importer.py +217 -0
  23. engine/models.py +451 -0
  24. engine/persistence.py +988 -0
  25. engine/prior_seed.py +166 -0
  26. engine/rules.py +529 -0
  27. engine/seeding.py +526 -0
  28. engine/sidecar_types.py +406 -0
  29. engine/strategies/__init__.py +26 -0
  30. engine/strategies/archive_ref.py +63 -0
  31. engine/strategies/jingle.py +51 -0
  32. engine/strategies/mix.py +79 -0
  33. engine/strategies/pick.py +147 -0
  34. engine/strategies/prev_ref.py +94 -0
  35. engine/strategies/weighted.py +458 -0
  36. engine/strategies/window.py +100 -0
  37. engine/template.py +1009 -0
  38. engine/template_importer.py +347 -0
  39. engine/trace.py +80 -0
  40. engine/trace_diff.py +129 -0
  41. station_studio/__init__.py +9 -0
  42. station_studio/_bundled/alembic.ini +150 -0
  43. station_studio/_bundled/migrations/README +1 -0
  44. station_studio/_bundled/migrations/env.py +100 -0
  45. station_studio/_bundled/migrations/script.py.mako +28 -0
  46. station_studio/_bundled/migrations/versions/08c84df31980_add_station_sidecar_error_column_16_2_5_t7.py +54 -0
  47. station_studio/_bundled/migrations/versions/0f76cfb45e8d_baseline.py +177 -0
  48. station_studio/_bundled/migrations/versions/18b360df9e02_add_playlist_version_table_16_4.py +162 -0
  49. station_studio/_bundled/migrations/versions/1cafcc971152_slot_type.py +44 -0
  50. station_studio/_bundled/migrations/versions/54570900ed97_rule.py +59 -0
  51. station_studio/_bundled/migrations/versions/6db55f2a13af_artist_alias.py +45 -0
  52. station_studio/_bundled/migrations/versions/8353f29ac95e_mix_usage.py +66 -0
  53. station_studio/_bundled/migrations/versions/be9b9713c5a4_add_playlist_version_tags_column_16_4_t5.py +49 -0
  54. station_studio/_bundled/migrations/versions/c7bb27708788_add_source_playlist_role_column_14_35.py +50 -0
  55. station_studio/_bundled/migrations/versions/e695658879de_add_station_sidecar_column_16_2_5.py +59 -0
  56. station_studio/_bundled/stations/README.md +997 -0
  57. station_studio/_bundled/stations/rebirth-classics.editor.json +17 -0
  58. station_studio/_bundled/stations/rebirth-classics.json +24 -0
  59. station_studio/_bundled/stations/rebirth-radio.editor.json +654 -0
  60. station_studio/_bundled/stations/rebirth-radio.json +12284 -0
  61. station_studio/_bundled/stations/rebirth-radio.pool-manifest.json +12 -0
  62. station_studio/_bundled/stations/starter-content.itunes.xml +1189 -0
  63. station_studio/_bundled/stations/starter-jingles.itunes.xml +154 -0
  64. station_studio/_bundled/stations/starter-reference.itunes.xml +315 -0
  65. station_studio/_bundled/stations/starter.editor.json +395 -0
  66. station_studio/_bundled/stations/starter.json +194 -0
  67. station_studio/_bundled/stations/stateful-starter-content.itunes.xml +7767 -0
  68. station_studio/_bundled/stations/stateful-starter.editor.json +545 -0
  69. station_studio/_bundled/stations/stateful-starter.json +2071 -0
  70. station_studio/_bundled/web/_app/env.js +1 -0
  71. station_studio/_bundled/web/_app/immutable/assets/0.CHcXy38b.css +1 -0
  72. station_studio/_bundled/web/_app/immutable/assets/10.D59qnJwu.css +1 -0
  73. station_studio/_bundled/web/_app/immutable/assets/11.DJRzRnJ2.css +1 -0
  74. station_studio/_bundled/web/_app/immutable/assets/2.BxRsQkmK.css +1 -0
  75. station_studio/_bundled/web/_app/immutable/assets/3.DRUlMdK3.css +1 -0
  76. station_studio/_bundled/web/_app/immutable/assets/4.52O9OOIP.css +1 -0
  77. station_studio/_bundled/web/_app/immutable/assets/5.Csn0hZD_.css +1 -0
  78. station_studio/_bundled/web/_app/immutable/assets/6.DxvG4bd-.css +1 -0
  79. station_studio/_bundled/web/_app/immutable/assets/7.CfcTqcRr.css +1 -0
  80. station_studio/_bundled/web/_app/immutable/assets/8.Bwsy3jBn.css +1 -0
  81. station_studio/_bundled/web/_app/immutable/assets/9.B31kdeU4.css +1 -0
  82. station_studio/_bundled/web/_app/immutable/assets/ChipMultiInput.DULmv1MR.css +1 -0
  83. station_studio/_bundled/web/_app/immutable/assets/ErrorBanner.B9DMjHa5.css +1 -0
  84. station_studio/_bundled/web/_app/immutable/assets/PoolImportPanel.B6YfNFeB.css +1 -0
  85. station_studio/_bundled/web/_app/immutable/assets/StationForm.DZPAT17X.css +1 -0
  86. station_studio/_bundled/web/_app/immutable/chunks/B4EyEyxP.js +2 -0
  87. station_studio/_bundled/web/_app/immutable/chunks/BU8rCeKM.js +1 -0
  88. station_studio/_bundled/web/_app/immutable/chunks/BUAoT0-3.js +1 -0
  89. station_studio/_bundled/web/_app/immutable/chunks/Be1Id7BW.js +1 -0
  90. station_studio/_bundled/web/_app/immutable/chunks/Bpisc4ul.js +1 -0
  91. station_studio/_bundled/web/_app/immutable/chunks/DK2tf20W.js +1 -0
  92. station_studio/_bundled/web/_app/immutable/chunks/QXSCjADz.js +1 -0
  93. station_studio/_bundled/web/_app/immutable/chunks/SHg1fOfW.js +1 -0
  94. station_studio/_bundled/web/_app/immutable/chunks/juDB7fyi.js +9 -0
  95. station_studio/_bundled/web/_app/immutable/chunks/uXzXLYFb.js +1 -0
  96. station_studio/_bundled/web/_app/immutable/chunks/y_vJ_g9l.js +13 -0
  97. station_studio/_bundled/web/_app/immutable/entry/app.CFeZQhV_.js +2 -0
  98. station_studio/_bundled/web/_app/immutable/entry/start.CrH9QAQm.js +1 -0
  99. station_studio/_bundled/web/_app/immutable/nodes/0.CxFOD7tO.js +1 -0
  100. station_studio/_bundled/web/_app/immutable/nodes/1.DtZs9QID.js +1 -0
  101. station_studio/_bundled/web/_app/immutable/nodes/10.BjcAQg9i.js +42 -0
  102. station_studio/_bundled/web/_app/immutable/nodes/11.h8B3q9So.js +2 -0
  103. station_studio/_bundled/web/_app/immutable/nodes/2.C-gVionp.js +3 -0
  104. station_studio/_bundled/web/_app/immutable/nodes/3.CsEYWMXp.js +25 -0
  105. station_studio/_bundled/web/_app/immutable/nodes/4.BZtHkw90.js +25 -0
  106. station_studio/_bundled/web/_app/immutable/nodes/5.Crod0gUe.js +3 -0
  107. station_studio/_bundled/web/_app/immutable/nodes/6.CFblxVlM.js +12 -0
  108. station_studio/_bundled/web/_app/immutable/nodes/7.BHhoSAi2.js +27 -0
  109. station_studio/_bundled/web/_app/immutable/nodes/8.BqTnHUU6.js +16 -0
  110. station_studio/_bundled/web/_app/immutable/nodes/9.DtY1oETd.js +4 -0
  111. station_studio/_bundled/web/_app/version.json +1 -0
  112. station_studio/_bundled/web/index.html +38 -0
  113. station_studio/bootstrap.py +448 -0
  114. station_studio/cli.py +575 -0
  115. station_studio/home.py +72 -0
  116. station_studio-1.5.0.dist-info/METADATA +359 -0
  117. station_studio-1.5.0.dist-info/RECORD +120 -0
  118. station_studio-1.5.0.dist-info/WHEEL +4 -0
  119. station_studio-1.5.0.dist-info/entry_points.txt +2 -0
  120. station_studio-1.5.0.dist-info/licenses/LICENSE +21 -0
api/__init__.py ADDED
File without changes
api/deps.py ADDED
@@ -0,0 +1,117 @@
1
+ """FastAPI dependency providers."""
2
+ from __future__ import annotations
3
+
4
+ import os
5
+ from pathlib import Path
6
+
7
+ from engine.database import make_engine, make_sessionmaker
8
+ from engine.models import Base
9
+
10
+
11
+ _engine = make_engine()
12
+ _SessionLocal = make_sessionmaker(_engine)
13
+
14
+ # Ensure schema exists (development default; real migrations later if needed)
15
+ Base.metadata.create_all(_engine)
16
+
17
+
18
+ # §16.2.2 c1: sidecar files live at `stations/{slug}.editor.json` next
19
+ # to the canonical `stations/{slug}.json`. Same directory the seeder +
20
+ # bootstrap scripts read from. Path is resolved relative to the repo
21
+ # root (api/deps.py → api/ → repo root) so it works both from `make
22
+ # dev` (cwd = repo root) and from arbitrary cwds.
23
+ _REPO_ROOT = Path(__file__).resolve().parent.parent
24
+ _STATIONS_DIR = _REPO_ROOT / "stations"
25
+
26
+ # §16.2.2.t4 (post-c5 sweep) — fail-fast against silent brittleness.
27
+ # If api/deps.py ever moves deeper into the package, `parent.parent`
28
+ # would resolve to the wrong directory. The sibling `engine/` dir is
29
+ # a load-bearing presence at the repo root — the whole codebase
30
+ # imports from it — so verifying it lives next to `_STATIONS_DIR`
31
+ # catches the misconfiguration at import time, with a clear stack
32
+ # trace, instead of failing later with "stations directory not
33
+ # found" once the editor endpoint is hit.
34
+ #
35
+ # Repo-name-agnostic on purpose: someone forking the repo into
36
+ # `my-station-fork/` should still pass. The checks verify
37
+ # STRUCTURE (engine/ sibling, stations/ exists), not naming.
38
+ #
39
+ # `if ... raise` (not `assert`) because `python -O` strips `assert`
40
+ # — and the whole point of the guard is to fail loudly when the
41
+ # runtime layout is wrong; `-O` would silently disable it.
42
+
43
+ def _verify_repo_layout(repo_root: Path, stations_dir: Path) -> None:
44
+ """Raise RuntimeError if the path-derivation produced a layout
45
+ that won't work. Extracted so tests can drive failure cases
46
+ deterministically (tests/test_deps_layout.py).
47
+
48
+ §16.13 c4: when `STATION_STUDIO_STATIONS_DIR` is set (the
49
+ installed-wheel path — `station_studio.bootstrap` populates it
50
+ before uvicorn imports this module), the stations-dir check is
51
+ SKIPPED. The env-override is the authoritative source of truth in
52
+ that mode, and the wheel's site-packages layout has no `stations/`
53
+ sibling. The engine-sibling check stays — it's about Python module
54
+ resolution, which IS load-bearing in both modes.
55
+ """
56
+ if not (repo_root / "engine").is_dir():
57
+ raise RuntimeError(
58
+ f"api/deps.py: repo_root={repo_root} doesn't contain an "
59
+ f"`engine/` sibling. Did api/deps.py move deeper into the package?"
60
+ )
61
+ # If the env override is set, defer the stations check to
62
+ # `get_stations_dir()` (which `mkdir(parents=True, exist_ok=True)`s
63
+ # the override path at call time anyway). No need to assert the
64
+ # repo-relative stations/ exists when the operator told us to use
65
+ # a different dir.
66
+ if os.environ.get("STATION_STUDIO_STATIONS_DIR", "").strip():
67
+ return
68
+ if not stations_dir.is_dir():
69
+ raise RuntimeError(
70
+ f"api/deps.py: stations_dir={stations_dir} doesn't exist. "
71
+ f"The `stations/` directory should live at the repo root."
72
+ )
73
+
74
+
75
+ _verify_repo_layout(_REPO_ROOT, _STATIONS_DIR)
76
+
77
+
78
+ def get_db():
79
+ db = _SessionLocal()
80
+ try:
81
+ yield db
82
+ finally:
83
+ db.close()
84
+
85
+
86
+ def get_session_factory():
87
+ """Return the SessionLocal factory. Endpoints that spawn background
88
+ work (§16.11 c3 job runner) need to open fresh sessions INSIDE the
89
+ worker thread — the request's `db` from `get_db()` is closed when
90
+ the endpoint returns. Tests override this dep alongside `get_db` so
91
+ the worker uses the throwaway-tmp test DB, not the dev DB.
92
+ """
93
+ return _SessionLocal
94
+
95
+
96
+ def get_stations_dir() -> Path:
97
+ """Return the directory holding the committed station JSON +
98
+ sidecar files (`stations/{slug}.json`, `stations/{slug}.editor.json`).
99
+
100
+ §16.2.2 c1: the editor endpoint reads sidecars from this directory.
101
+ Pytest overrides this dep to a tmp_path so fixtures drop in
102
+ isolation without polluting the repo's committed sidecars.
103
+
104
+ §16.2.3 c3: `STATION_STUDIO_STATIONS_DIR` env override. The export
105
+ endpoint WRITES `stations/{slug}.json` here — in a real server
106
+ process (Playwright e2e) there's no dependency-override, so without
107
+ this the e2e would write stray files into the repo's `stations/`.
108
+ The Playwright webServer points it at a throwaway dir. Read at call
109
+ time (not import) so the env applies per-process; the dir is
110
+ created if missing so the first export doesn't 500.
111
+ """
112
+ override = os.environ.get("STATION_STUDIO_STATIONS_DIR", "").strip()
113
+ if override:
114
+ d = Path(override)
115
+ d.mkdir(parents=True, exist_ok=True)
116
+ return d
117
+ return _STATIONS_DIR