algoriq 0.2.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 (267) hide show
  1. algoriq/__init__.py +71 -0
  2. algoriq/core/__init__.py +154 -0
  3. algoriq/core/service_topology.py +256 -0
  4. algoriq/indicators/__init__.py +250 -0
  5. algoriq/indicators/bars/__init__.py +37 -0
  6. algoriq/indicators/bars/_emit.py +52 -0
  7. algoriq/indicators/bars/_validate.py +88 -0
  8. algoriq/indicators/bars/dollar_bar.py +49 -0
  9. algoriq/indicators/bars/factory.py +80 -0
  10. algoriq/indicators/bars/imbalance_bar.py +178 -0
  11. algoriq/indicators/bars/protocol.py +56 -0
  12. algoriq/indicators/bars/quality_diagnostics.py +542 -0
  13. algoriq/indicators/bars/time_bar.py +63 -0
  14. algoriq/indicators/bars/volume_bar.py +75 -0
  15. algoriq/indicators/ffd/__init__.py +46 -0
  16. algoriq/indicators/ffd/frac_diff.py +118 -0
  17. algoriq/indicators/ffd/search.py +345 -0
  18. algoriq/indicators/ffd/weights.py +68 -0
  19. algoriq/indicators/ma/__init__.py +44 -0
  20. algoriq/indicators/ma/_validate.py +160 -0
  21. algoriq/indicators/ma/dema.py +40 -0
  22. algoriq/indicators/ma/ema.py +56 -0
  23. algoriq/indicators/ma/factory.py +84 -0
  24. algoriq/indicators/ma/hma.py +42 -0
  25. algoriq/indicators/ma/kama.py +114 -0
  26. algoriq/indicators/ma/protocol.py +45 -0
  27. algoriq/indicators/ma/rma.py +56 -0
  28. algoriq/indicators/ma/sma.py +42 -0
  29. algoriq/indicators/ma/tema.py +42 -0
  30. algoriq/indicators/ma/vwma.py +60 -0
  31. algoriq/indicators/ma/wma.py +49 -0
  32. algoriq/indicators/momentum/__init__.py +41 -0
  33. algoriq/indicators/momentum/_validate.py +120 -0
  34. algoriq/indicators/momentum/cci.py +70 -0
  35. algoriq/indicators/momentum/factory.py +122 -0
  36. algoriq/indicators/momentum/macd.py +71 -0
  37. algoriq/indicators/momentum/protocol.py +84 -0
  38. algoriq/indicators/momentum/roc.py +36 -0
  39. algoriq/indicators/momentum/rsi.py +82 -0
  40. algoriq/indicators/momentum/stochastic.py +89 -0
  41. algoriq/indicators/momentum/williams_r.py +50 -0
  42. algoriq/indicators/registry.py +303 -0
  43. algoriq/indicators/trend/__init__.py +38 -0
  44. algoriq/indicators/trend/_validate.py +62 -0
  45. algoriq/indicators/trend/adx.py +103 -0
  46. algoriq/indicators/trend/aroon.py +79 -0
  47. algoriq/indicators/trend/factory.py +59 -0
  48. algoriq/indicators/trend/ichimoku.py +120 -0
  49. algoriq/indicators/trend/parabolic_sar.py +184 -0
  50. algoriq/indicators/trend/protocol.py +41 -0
  51. algoriq/indicators/trend/supertrend.py +158 -0
  52. algoriq/indicators/volatility/__init__.py +47 -0
  53. algoriq/indicators/volatility/_validate.py +73 -0
  54. algoriq/indicators/volatility/atr.py +92 -0
  55. algoriq/indicators/volatility/bollinger_bands.py +66 -0
  56. algoriq/indicators/volatility/factory.py +72 -0
  57. algoriq/indicators/volatility/garman_klass.py +83 -0
  58. algoriq/indicators/volatility/keltner_channel.py +69 -0
  59. algoriq/indicators/volatility/parkinson.py +56 -0
  60. algoriq/indicators/volatility/protocol.py +42 -0
  61. algoriq/indicators/volatility/squeeze_momentum.py +111 -0
  62. algoriq/indicators/volatility/std_dev.py +53 -0
  63. algoriq/indicators/volatility/yang_zhang.py +107 -0
  64. algoriq/indicators/volume/__init__.py +32 -0
  65. algoriq/indicators/volume/_validate.py +95 -0
  66. algoriq/indicators/volume/ad_line.py +54 -0
  67. algoriq/indicators/volume/factory.py +56 -0
  68. algoriq/indicators/volume/mfi.py +61 -0
  69. algoriq/indicators/volume/obv.py +51 -0
  70. algoriq/indicators/volume/protocol.py +37 -0
  71. algoriq/indicators/volume/vwap.py +91 -0
  72. algoriq/labeling/__init__.py +74 -0
  73. algoriq/labeling/cusum.py +250 -0
  74. algoriq/labeling/factory.py +225 -0
  75. algoriq/labeling/fixed_time.py +227 -0
  76. algoriq/labeling/meta.py +206 -0
  77. algoriq/labeling/side_policy.py +94 -0
  78. algoriq/labeling/triple_barrier.py +514 -0
  79. algoriq/labeling/volatility.py +64 -0
  80. algoriq/ml/__init__.py +876 -0
  81. algoriq/ml/ale.py +489 -0
  82. algoriq/ml/bayesian/__init__.py +46 -0
  83. algoriq/ml/bayesian/bayesian_gate.py +128 -0
  84. algoriq/ml/bayesian/calibration.py +129 -0
  85. algoriq/ml/bayesian/config.py +259 -0
  86. algoriq/ml/bayesian/ensemble.py +138 -0
  87. algoriq/ml/bayesian/posterior.py +205 -0
  88. algoriq/ml/bet_sizing/__init__.py +48 -0
  89. algoriq/ml/bet_sizing/base.py +342 -0
  90. algoriq/ml/bet_sizing/kelly_sizing.py +44 -0
  91. algoriq/ml/bet_sizing/meta_sizing.py +24 -0
  92. algoriq/ml/bet_sizing/sigmoid_sizing.py +43 -0
  93. algoriq/ml/bet_sizing/threshold_sizing.py +35 -0
  94. algoriq/ml/causal/__init__.py +77 -0
  95. algoriq/ml/causal/analyzer.py +811 -0
  96. algoriq/ml/causal/confounders.py +162 -0
  97. algoriq/ml/causal/dml.py +470 -0
  98. algoriq/ml/causal/refutation.py +421 -0
  99. algoriq/ml/causal/treatment.py +193 -0
  100. algoriq/ml/clustering.py +498 -0
  101. algoriq/ml/constraint_enforcer/__init__.py +36 -0
  102. algoriq/ml/constraint_enforcer/base.py +196 -0
  103. algoriq/ml/constraint_enforcer/max_leverage.py +63 -0
  104. algoriq/ml/constraint_enforcer/max_turnover.py +105 -0
  105. algoriq/ml/constraint_enforcer/max_weight.py +67 -0
  106. algoriq/ml/constraint_enforcer/min_holding.py +122 -0
  107. algoriq/ml/covariance.py +547 -0
  108. algoriq/ml/cross_sectional.py +250 -0
  109. algoriq/ml/cross_validation.py +1222 -0
  110. algoriq/ml/dedup.py +450 -0
  111. algoriq/ml/device.py +27 -0
  112. algoriq/ml/entropy.py +719 -0
  113. algoriq/ml/foundation/__init__.py +22 -0
  114. algoriq/ml/foundation/embedder.py +202 -0
  115. algoriq/ml/fusion.py +718 -0
  116. algoriq/ml/ic_decay.py +255 -0
  117. algoriq/ml/importance.py +1069 -0
  118. algoriq/ml/intraday.py +345 -0
  119. algoriq/ml/matrix_clustering.py +229 -0
  120. algoriq/ml/metrics.py +371 -0
  121. algoriq/ml/microstructure.py +556 -0
  122. algoriq/ml/model_type.py +184 -0
  123. algoriq/ml/quantile_scoring.py +506 -0
  124. algoriq/ml/regime/__init__.py +20 -0
  125. algoriq/ml/regime/hmm_regime_classifier.py +412 -0
  126. algoriq/ml/regime/regime_classifier_service.py +199 -0
  127. algoriq/ml/regime_base_rate.py +322 -0
  128. algoriq/ml/regime_ic.py +295 -0
  129. algoriq/ml/regression_target.py +263 -0
  130. algoriq/ml/risk_metrics.py +909 -0
  131. algoriq/ml/sample_weights.py +915 -0
  132. algoriq/ml/screening_budget.py +266 -0
  133. algoriq/ml/shared_models/__init__.py +0 -0
  134. algoriq/ml/shared_models/cross_sectional_net.py +225 -0
  135. algoriq/ml/shared_models/custom_objectives.py +246 -0
  136. algoriq/ml/shared_models/e2e_portfolio.py +253 -0
  137. algoriq/ml/shared_models/e2e_trainer.py +747 -0
  138. algoriq/ml/shared_models/ml_models.py +3767 -0
  139. algoriq/ml/shared_models/temporal_alpha_net.py +339 -0
  140. algoriq/ml/shared_models/wavelet_alpha.py +333 -0
  141. algoriq/ml/sharpe_inference.py +736 -0
  142. algoriq/ml/strategy_view.py +488 -0
  143. algoriq/ml/structural_break.py +893 -0
  144. algoriq/ml/symbolic/__init__.py +25 -0
  145. algoriq/ml/symbolic/discoverer.py +466 -0
  146. algoriq/ml/timeframe_params.py +251 -0
  147. algoriq/ml/turnover.py +105 -0
  148. algoriq/ml/vrp.py +377 -0
  149. algoriq/ml/weights.py +354 -0
  150. algoriq/schemas/__init__.py +42 -0
  151. algoriq/schemas/_base/__init__.py +5 -0
  152. algoriq/schemas/_base/enum.py +1 -0
  153. algoriq/schemas/_base/model.py +54 -0
  154. algoriq/schemas/artifacts/__init__.py +102 -0
  155. algoriq/schemas/artifacts/api.py +120 -0
  156. algoriq/schemas/artifacts/dataset.py +955 -0
  157. algoriq/schemas/artifacts/ingredient.py +758 -0
  158. algoriq/schemas/artifacts/manifest.py +348 -0
  159. algoriq/schemas/constants.py +129 -0
  160. algoriq/schemas/curation/__init__.py +120 -0
  161. algoriq/schemas/curation/event_pit.py +70 -0
  162. algoriq/schemas/curation/exogenous_level.py +71 -0
  163. algoriq/schemas/curation/instrument_contract.py +87 -0
  164. algoriq/schemas/curation/load_feasibility.py +400 -0
  165. algoriq/schemas/curation/pit_integrity.py +360 -0
  166. algoriq/schemas/curation/pit_ohlcv.py +467 -0
  167. algoriq/schemas/curation/quality.py +166 -0
  168. algoriq/schemas/curation/quality_rollup.py +273 -0
  169. algoriq/schemas/curation/spec.py +812 -0
  170. algoriq/schemas/data_axis/__init__.py +69 -0
  171. algoriq/schemas/data_axis/access.py +96 -0
  172. algoriq/schemas/data_axis/adapter.py +132 -0
  173. algoriq/schemas/data_axis/instrument.py +59 -0
  174. algoriq/schemas/data_axis/pit.py +65 -0
  175. algoriq/schemas/data_axis/provider.py +195 -0
  176. algoriq/schemas/data_axis/serving.py +42 -0
  177. algoriq/schemas/data_axis/shape.py +43 -0
  178. algoriq/schemas/data_axis/source.py +53 -0
  179. algoriq/schemas/market_data/__init__.py +106 -0
  180. algoriq/schemas/market_data/company_profile.py +99 -0
  181. algoriq/schemas/market_data/corp_action.py +191 -0
  182. algoriq/schemas/market_data/crypto_derivatives.py +90 -0
  183. algoriq/schemas/market_data/daily_metrics.py +71 -0
  184. algoriq/schemas/market_data/fundamental.py +199 -0
  185. algoriq/schemas/market_data/index_constituent.py +93 -0
  186. algoriq/schemas/market_data/institutional_holdings.py +164 -0
  187. algoriq/schemas/market_data/ohlcv.py +95 -0
  188. algoriq/schemas/market_data/onchain_metrics.py +52 -0
  189. algoriq/schemas/model_registry.py +1374 -0
  190. algoriq/schemas/research/__init__.py +1 -0
  191. algoriq/schemas/research/intake.py +329 -0
  192. algoriq/schemas/research/observation.py +215 -0
  193. algoriq/schemas/research/publication.py +49 -0
  194. algoriq/schemas/research/screening.py +195 -0
  195. algoriq/schemas/research/snapshot.py +106 -0
  196. algoriq/schemas/shared/__init__.py +33 -0
  197. algoriq/schemas/shared/api.py +27 -0
  198. algoriq/schemas/shared/artifact.py +52 -0
  199. algoriq/schemas/shared/forge.py +96 -0
  200. algoriq/schemas/shared/identity.py +10 -0
  201. algoriq/schemas/shared/lineage.py +83 -0
  202. algoriq/schemas/shared/quality.py +120 -0
  203. algoriq/schemas/shared/raw.py +46 -0
  204. algoriq/schemas/shared/run.py +424 -0
  205. algoriq/schemas/shared/time.py +58 -0
  206. algoriq/schemas/strategy/__init__.py +421 -0
  207. algoriq/schemas/strategy/_models.py +698 -0
  208. algoriq/schemas/strategy/api.py +335 -0
  209. algoriq/schemas/strategy/artifacts.py +71 -0
  210. algoriq/schemas/strategy/bundle.py +130 -0
  211. algoriq/schemas/strategy/cv.py +17 -0
  212. algoriq/schemas/strategy/diagnostics.py +30 -0
  213. algoriq/schemas/strategy/dsl.py +49 -0
  214. algoriq/schemas/strategy/dsl_api.py +316 -0
  215. algoriq/schemas/strategy/dsl_vocabulary.py +133 -0
  216. algoriq/schemas/strategy/enums.py +152 -0
  217. algoriq/schemas/strategy/evaluation.py +122 -0
  218. algoriq/schemas/strategy/execution_reeval.py +177 -0
  219. algoriq/schemas/strategy/experiment.py +79 -0
  220. algoriq/schemas/strategy/family.py +835 -0
  221. algoriq/schemas/strategy/family_feature.py +85 -0
  222. algoriq/schemas/strategy/family_validation.py +84 -0
  223. algoriq/schemas/strategy/fiir.py +49 -0
  224. algoriq/schemas/strategy/hpo.py +212 -0
  225. algoriq/schemas/strategy/meta_hpo.py +49 -0
  226. algoriq/schemas/strategy/promotion.py +82 -0
  227. algoriq/schemas/strategy/quick_backtest_api.py +83 -0
  228. algoriq/schemas/strategy/quick_backtest_emission.py +224 -0
  229. algoriq/schemas/strategy/regime.py +111 -0
  230. algoriq/schemas/strategy/run.py +410 -0
  231. algoriq/schemas/strategy/session.py +151 -0
  232. algoriq/schemas/strategy/strategy_config.py +98 -0
  233. algoriq/schemas/strategy/tba.py +32 -0
  234. algoriq/schemas/strategy/train_api.py +327 -0
  235. algoriq/schemas/strategy/trained_model.py +77 -0
  236. algoriq/schemas/strategy/trial_ledger.py +744 -0
  237. algoriq/schemas/strategy/trial_tracker_api.py +71 -0
  238. algoriq/schemas/strategy/walk_forward.py +83 -0
  239. algoriq/schemas/validation/__init__.py +294 -0
  240. algoriq/schemas/validation/_models.py +492 -0
  241. algoriq/schemas/validation/api.py +409 -0
  242. algoriq/schemas/validation/cpcv.py +66 -0
  243. algoriq/schemas/validation/enums.py +53 -0
  244. algoriq/schemas/validation/kpi.py +256 -0
  245. algoriq/schemas/validation/kpi_gates_artifact.py +78 -0
  246. algoriq/schemas/validation/leakage.py +180 -0
  247. algoriq/schemas/validation/regime_ic.py +182 -0
  248. algoriq/schemas/validation/returns_audit.py +224 -0
  249. algoriq/schemas/validation/run.py +180 -0
  250. algoriq/schemas/validation/scenario.py +30 -0
  251. algoriq/schemas/validation/seven_sins.py +50 -0
  252. algoriq/schemas/validation/signal_diagnostics.py +97 -0
  253. algoriq/schemas/validation/status.py +259 -0
  254. algoriq/schemas/validation/status_artifact.py +162 -0
  255. algoriq/schemas/validation/tier_zero.py +361 -0
  256. algoriq/schemas/validation/walk_forward.py +434 -0
  257. algoriq-0.2.0.dist-info/METADATA +193 -0
  258. algoriq-0.2.0.dist-info/RECORD +267 -0
  259. algoriq-0.2.0.dist-info/WHEEL +4 -0
  260. algoriq_research/__init__.py +237 -0
  261. algoriq_research/_probes.py +372 -0
  262. algoriq_research/capabilities.py +826 -0
  263. algoriq_research/capability_status.py +59 -0
  264. algoriq_research/client.py +591 -0
  265. algoriq_research/errors.py +136 -0
  266. algoriq_research/plane.py +188 -0
  267. algoriq_research/runtime_guard.py +353 -0
algoriq/__init__.py ADDED
@@ -0,0 +1,71 @@
1
+ """``import algoriq as aq`` — 공개 배포물의 진입점.
2
+
3
+ 이 파일은 **wheel 에만 실린다.** 소스 트리의 ``packages/algoriq/src/algoriq/`` 에는
4
+ ``__init__.py`` 가 없고(PEP 420 암묵 네임스페이스), 그 부재는 12 서비스가 의존하는 성질이라
5
+ 건드리지 않는다. 그래서 여기 따로 저작해 빌드 훅이 아카이브 경로 ``algoriq/__init__.py`` 로
6
+ force-include 한다 — 레포 안에서는 존재하지 않고 사용자 디스크에서만 존재한다.
7
+
8
+ ## 왜 있나
9
+
10
+ ``pip install algoriq`` 를 한 사람이 다음 줄에 쓰는 것이 ``import algoriq`` 다. 그것이
11
+ 계산을 주지 않으면 배포물 이름과 임포트 이름이 갈려 있다는 뜻이고, 그 갈림은 설치 직후
12
+ 첫 줄에서 드러난다. 실제 API 는 ``algoriq_research`` 에 살고 앞으로도 거기 산다(그 이름이
13
+ SDK 의 임포트 루트이고, 배포물명만 인덱스에서 이름을 다툰다) — 여기서는 그리로 위임한다.
14
+
15
+ ## 왜 지연인가
16
+
17
+ ``algoriq_research`` 를 여기서 즉시 임포트하면 ``import algoriq.schemas.strategy`` 한 줄이
18
+ numpy·pandas·scipy 를 끌고 온다. PEP 562 ``__getattr__`` 는 **이름을 실제로 집을 때까지**
19
+ 그 비용을 미룬다 — ``aq.deflated_sharpe_ratio`` 를 쓰는 사람만 그 값을 낸다.
20
+
21
+ ## ``pkgutil.extend_path`` 가 하는 일
22
+
23
+ ``__init__.py`` 가 생기는 순간 ``algoriq`` 는 암묵 네임스페이스에서 **정규 패키지**가 되고,
24
+ 정규 패키지의 ``__path__`` 는 자기 디렉터리 하나다. 두 배포물이 같은 ``site-packages`` 로
25
+ 풀리는 보통의 설치에서는 그 하나에 모든 파일이 있어 아무 문제가 없다(독립 측정 3회). 갈리는
26
+ 것은 **서로 다른 ``sys.path`` 엔트리에 앉을 때**뿐이고 — editable 과 wheel 이 섞인 이 레포의
27
+ 개발 환경이 정확히 그 형상이다 — ``extend_path`` 가 ``sys.path`` 를 훑어 같은 이름의
28
+ 디렉터리를 ``__path__`` 에 합쳐 그 경우까지 덮는다. 없어도 대부분 동작한다는 것이 이 줄을
29
+ 빼면 안 되는 이유다: 깨지는 환경이 드물어서 **깨진 채로 오래 간다.**
30
+ """
31
+
32
+ from __future__ import annotations
33
+
34
+ import pkgutil
35
+ from typing import Any
36
+
37
+ #: 같은 이름의 다른 ``sys.path`` 엔트리를 합친다. 모듈 docstring 참조.
38
+ __path__ = pkgutil.extend_path(__path__, __name__)
39
+
40
+ #: 위임 대상. 문자열로 두어 이 모듈이 임포트될 때 따라 들어오지 않게 한다.
41
+ _DELEGATE = "algoriq_research"
42
+
43
+
44
+ def __getattr__(name: str) -> Any:
45
+ """``algoriq_research`` 의 공개 이름을 여기서도 집게 한다.
46
+
47
+ Args:
48
+ name: 집으려는 속성 이름.
49
+
50
+ Returns:
51
+ ``algoriq_research`` 가 그 이름으로 내놓는 객체.
52
+
53
+ Raises:
54
+ AttributeError: 그쪽에도 없는 이름일 때. ⛔ 메시지를 ``algoriq_research`` 것으로
55
+ 두지 않고 다시 쓴다 — 사용자는 ``algoriq`` 를 임포트했고, 자기가 쓴 적 없는
56
+ 이름이 오류에 나오면 그것부터 찾아 나선다.
57
+ """
58
+ import importlib
59
+
60
+ delegate = importlib.import_module(_DELEGATE)
61
+ try:
62
+ return getattr(delegate, name)
63
+ except AttributeError:
64
+ raise AttributeError(f"module 'algoriq' has no attribute {name!r}") from None
65
+
66
+
67
+ def __dir__() -> list[str]:
68
+ """``dir(algoriq)`` 와 탭 완성이 실제 표면을 보이게 한다."""
69
+ import importlib
70
+
71
+ return sorted(set(dir(importlib.import_module(_DELEGATE))) | set(globals()))
@@ -0,0 +1,154 @@
1
+ """algoriq.core — Cross-service infrastructure SSOT for AlgoriQ backend services.
2
+
3
+ Package structure:
4
+ algoriq.core.logging — structlog-based structured logging (setup_logging)
5
+ algoriq.core.internal_auth — InternalAuthMiddleware + HMAC helpers
6
+ algoriq.core.correlation — cross-hop correlation id (header, contextvar, ASGI)
7
+ algoriq.core.base_config — BaseAlgoriqSettings (pydantic-settings)
8
+ algoriq.core.service_topology — the one service-coordinate registry
9
+
10
+ Why every top-level re-export is lazy (PEP 562)
11
+ -----------------------------------------------
12
+ The names below are the canonical public API: ``from algoriq.core import
13
+ setup_logging`` and ``from algoriq.core.logging import setup_logging`` are
14
+ interchangeable, and that stays true. What changed is *when* each submodule is
15
+ executed.
16
+
17
+ Importing **any** ``algoriq.core`` submodule runs this file first. When this
18
+ file imported its submodules eagerly, that made the whole server stack a
19
+ transitive dependency of a pure data table: ``algoriq.schemas.constants`` reads
20
+ ``algoriq.core.service_topology`` for one coordinate registry, and so
21
+ ``import algoriq.ml.covariance`` loaded ``fastapi`` (via ``core.dependencies``),
22
+ ``starlette`` (via ``core.celery_observability`` → ``core.correlation``),
23
+ ``pymongo`` (via ``core.platform_events``) and 2,399 modules in total — for a
24
+ covariance estimator that needs numpy, pandas, scipy and sklearn.
25
+
26
+ That blocks the notebook SDK, whose compute modules ship to Colab where no web
27
+ framework and no Mongo driver exist. A deferral limited to "the submodules that
28
+ look server-ish today" would encode a classification that decays the first time
29
+ a quiet submodule grows a heavy dependency, so the deferral is uniform: this
30
+ file executes no submodule at import time at all.
31
+
32
+ The guard against silent regression is ``scripts/measure_sdk_import_boundary.py``,
33
+ which imports each notebook compute module in its own interpreter and fails when
34
+ a server-only distribution appears. Neither pyright nor ruff can see this
35
+ invariant — an eager ``from algoriq.core.platform_events import …`` added here
36
+ would type-check and lint clean while re-coupling the SDK to the server stack.
37
+
38
+ Resolution failure raises. A missing submodule surfaces as the real
39
+ ``ImportError`` and an unknown name as ``AttributeError``; there is no default
40
+ value and no swallowed import error.
41
+ """
42
+
43
+ from typing import TYPE_CHECKING
44
+
45
+ if TYPE_CHECKING:
46
+ # Static-only bindings for the names the PEP 562 ``__getattr__`` below
47
+ # resolves at runtime. Type checkers and IDEs keep seeing the real objects
48
+ # (no ``Any`` leak, no stub class); the interpreter never runs this block.
49
+ from algoriq.core.base_config import BaseAlgoriqSettings
50
+ from algoriq.core.celery_observability import (
51
+ CELERY_SHUTDOWN_CONF,
52
+ TaskFailureEvent,
53
+ install_correlation_propagation,
54
+ install_task_failure_logging,
55
+ install_worker_shutdown_contract,
56
+ platform_event_sink,
57
+ revoke_run_task,
58
+ termination_requested_during_task,
59
+ )
60
+ from algoriq.core.cpu_budget import CpuBudget, activate_cpu_budget, resolve_cpu_budget
61
+ from algoriq.core.dependencies import get_user_tier, require_user_id
62
+ from algoriq.core.logging import CELERY_LOGGING_CONF, setup_logging
63
+ from algoriq.core.platform_events import emit_platform_event_sync, upsert_platform_event_sync
64
+ from algoriq.core.sweeper_watchdog import HeartbeatVerdict, check_sweeper_heartbeat
65
+ from algoriq.core.urls import build_mongodb_uri, build_postgres_uri, build_redis_url
66
+
67
+ __version__ = "0.1.0"
68
+
69
+ #: Public name → owning submodule. This is the whole re-export surface; a name
70
+ #: absent here is absent from the package, which is what keeps ``__all__`` and
71
+ #: the runtime hook from drifting apart (``test_core_lazy_exports.py`` pins it).
72
+ _LAZY_EXPORTS: dict[str, str] = {
73
+ "BaseAlgoriqSettings": "algoriq.core.base_config",
74
+ "CELERY_SHUTDOWN_CONF": "algoriq.core.celery_observability",
75
+ "TaskFailureEvent": "algoriq.core.celery_observability",
76
+ "install_correlation_propagation": "algoriq.core.celery_observability",
77
+ "install_task_failure_logging": "algoriq.core.celery_observability",
78
+ "install_worker_shutdown_contract": "algoriq.core.celery_observability",
79
+ "platform_event_sink": "algoriq.core.celery_observability",
80
+ "revoke_run_task": "algoriq.core.celery_observability",
81
+ "termination_requested_during_task": "algoriq.core.celery_observability",
82
+ "CpuBudget": "algoriq.core.cpu_budget",
83
+ "activate_cpu_budget": "algoriq.core.cpu_budget",
84
+ "resolve_cpu_budget": "algoriq.core.cpu_budget",
85
+ "get_user_tier": "algoriq.core.dependencies",
86
+ "require_user_id": "algoriq.core.dependencies",
87
+ "CELERY_LOGGING_CONF": "algoriq.core.logging",
88
+ "setup_logging": "algoriq.core.logging",
89
+ "emit_platform_event_sync": "algoriq.core.platform_events",
90
+ "upsert_platform_event_sync": "algoriq.core.platform_events",
91
+ "HeartbeatVerdict": "algoriq.core.sweeper_watchdog",
92
+ "check_sweeper_heartbeat": "algoriq.core.sweeper_watchdog",
93
+ "build_mongodb_uri": "algoriq.core.urls",
94
+ "build_postgres_uri": "algoriq.core.urls",
95
+ "build_redis_url": "algoriq.core.urls",
96
+ }
97
+
98
+ __all__ = [
99
+ "CELERY_LOGGING_CONF",
100
+ "CELERY_SHUTDOWN_CONF",
101
+ "BaseAlgoriqSettings",
102
+ "CpuBudget",
103
+ "HeartbeatVerdict",
104
+ "TaskFailureEvent",
105
+ "activate_cpu_budget",
106
+ "build_mongodb_uri",
107
+ "build_postgres_uri",
108
+ "build_redis_url",
109
+ "check_sweeper_heartbeat",
110
+ "emit_platform_event_sync",
111
+ "get_user_tier",
112
+ "install_correlation_propagation",
113
+ "install_task_failure_logging",
114
+ "install_worker_shutdown_contract",
115
+ "platform_event_sink",
116
+ "require_user_id",
117
+ "resolve_cpu_budget",
118
+ "revoke_run_task",
119
+ "setup_logging",
120
+ "termination_requested_during_task",
121
+ "upsert_platform_event_sync",
122
+ ]
123
+
124
+
125
+ def __getattr__(name: str) -> object:
126
+ """Resolve a lazily re-exported ``algoriq.core`` name (PEP 562).
127
+
128
+ Args:
129
+ name: Attribute requested on the ``algoriq.core`` package.
130
+
131
+ Returns:
132
+ The resolved object, also cached into the module globals so subsequent
133
+ lookups skip this hook entirely.
134
+
135
+ Raises:
136
+ AttributeError: If ``name`` is not part of the re-export surface —
137
+ standard package behaviour for an unknown attribute.
138
+ ImportError: Propagated unchanged when the owning submodule fails to
139
+ import. A deferred import that cannot be satisfied is a real
140
+ failure, not a missing attribute.
141
+ """
142
+ module_path = _LAZY_EXPORTS.get(name)
143
+ if module_path is None:
144
+ raise AttributeError(f"module {__name__!r} has no attribute {name!r}")
145
+ import importlib
146
+
147
+ value = getattr(importlib.import_module(module_path), name)
148
+ globals()[name] = value
149
+ return value
150
+
151
+
152
+ def __dir__() -> list[str]:
153
+ """Keep the lazily re-exported names visible to ``dir()`` / tab completion."""
154
+ return sorted(set(globals()) | set(_LAZY_EXPORTS))
@@ -0,0 +1,256 @@
1
+ """service_topology — one coordinate registry for the pipeline services (#2034 S2 · D5).
2
+
3
+ Why one table instead of many maps
4
+ ----------------------------------
5
+ A map keyed on service and *named after its key* says nothing about its
6
+ *codomain*, so ``grep`` cannot tell the MinIO object-key token from the HTTP path
7
+ token — and the two genuinely differ (``strategy`` is ``"strategy"`` on storage
8
+ but ``"strategies"`` on HTTP). That shape also duplicates silently: nine such
9
+ maps sat in six modules (``SERVICE_SEGMENT_MAP``, ``SERVICE_PREFIX_MAP``,
10
+ ``SERVICE_RUN_ID_FIELD``, ``_SERVICE_TO_CLIENT_CLS``, ``_SERVICE_TO_ENV_KEYS``,
11
+ ``_DEFAULT_SERVICE_URLS``, ``_SEGMENTS``, ``MCP_SERVICE_TYPE_TO_CLIENT``,
12
+ ``_DETAIL_IDENTITY_KEY``), two of them byte-identical copies of the same
13
+ client map, and the ninth was found only because the enforcement gate below was
14
+ written.
15
+
16
+ The fix is not a naming rule — a rule cannot stop a tenth map. Every coordinate
17
+ of a service is one field on one record here, and the historical maps become
18
+ one-line comprehensions over this table. A new coordinate is a new *field*,
19
+ which is a declaration everyone reviewing the record sees, and
20
+ ``packages/algoriq/tests/core/test_service_topology_gate.py`` fails on any
21
+ service-keyed dict literal declared outside this module.
22
+
23
+ Why the values are not derived from the service name
24
+ ----------------------------------------------------
25
+ They are not derivable. ``feature``'s object-key segment is ``"features"``
26
+ while ``curation``'s is ``"curation"``; #1834 is the live evidence of what
27
+ guessing costs (a lineage walk that resolved to a prefix no object ever had).
28
+ The table is explicit on purpose and its values are frozen: a field here is a
29
+ coordinate that some other layer already stamped into storage or a URL.
30
+
31
+ Axis boundaries
32
+ ---------------
33
+ * :data:`SERVICE_TOPOLOGY` — the five services on the run chain. Every field is
34
+ populated for every row; a partial row would put ``None`` handling into
35
+ every derived view.
36
+ * :data:`NON_PIPELINE_ARTIFACT_NAMESPACES` — oracle. It owns an artifact-id
37
+ prefix (``or_``) and a storage segment, and lineage walks resolve it, but it
38
+ mints no chain run and has no per-service run client, so it is not a
39
+ :data:`ServiceId`. Its prefix is **not** ``None``: ``ORACLE_ARTIFACT_ID_PREFIX``
40
+ is live in ``services/oracle/src/pipeline/oracle_report_producer.py`` and that
41
+ module asserts the round-trip against the manifest-chain SSOT at produce time.
42
+ * :data:`NON_SERVICE_RUN_ID_NAMESPACES` — ``opt``, the oracle optimisation
43
+ job-id namespace. It mints ids through ``generate_run_id`` but is not a
44
+ service: it can be neither a ``manifest.service`` value nor a path segment.
45
+ Declaring it as its own namespace lets the type say that, instead of a bare
46
+ entry in a service-keyed map that a comment has to explain away.
47
+ """
48
+
49
+ from __future__ import annotations
50
+
51
+ from typing import Literal, NamedTuple, get_args
52
+
53
+ #: The five services that own a run surface, in pipeline order.
54
+ #:
55
+ #: The name is ``ServiceId`` rather than a *type* name because the value set is the
56
+ #: set of services and the service name already says which service it is.
57
+ #: ``apps/web/src/lib/serviceRouter.ts``
58
+ #: ``SERVICE_IDS`` is the cross-language mirror and
59
+ #: ``packages/algoriq/tests/tools/test_service_id_ssot.py`` locks the two together.
60
+ ServiceId = Literal["curation", "feature", "strategy", "validation", "portfolio"]
61
+
62
+
63
+ class ServiceCoords(NamedTuple):
64
+ """Every coordinate a caller can need for one pipeline service.
65
+
66
+ Attributes:
67
+ run_id_prefix: Run-ID prefix without the underscore (``"cur"`` →
68
+ ``cur_0001``). ``generate_run_id`` mints it; ``resolve_service_from_run_id``
69
+ routes on it.
70
+ object_key_segment: MinIO object-key segment —
71
+ ``{owner}/{segment}/{artifact_id}/``. **Not** the HTTP segment.
72
+ http_path_segment: Plural URL segment of the service API
73
+ (``/api/v1/{segment}/...``). **Not** the storage segment.
74
+ run_id_field: The key a run **list item** carries its own run id under.
75
+ The services never agreed on one name: curation uses the generic
76
+ ``run_id``, everyone else prefixes it. Reading the wrong key returns
77
+ ``""`` rather than raising (#1961).
78
+ detail_identity_key: The key a run **detail** body carries its identity
79
+ under. Deliberately a separate coordinate from ``run_id_field``: the
80
+ two disagree for four of the five services (a strategy detail body
81
+ says ``run_id`` while its list item says ``strategy_run_id``), and
82
+ reading the wrong one yields ``None`` — the exact absence the MCP
83
+ read path must not swallow, which is why it is resolved per service
84
+ rather than ``or``-chained across both spellings.
85
+ client_env_keys: Environment variables holding this service's base URL,
86
+ in resolution order. advisor and mcp-gateway historically spelled
87
+ some of these differently, hence a tuple rather than one name.
88
+ default_base_url: Compose-network fallback when no env var is set.
89
+ model_type_detail_path: Key path, inside the run **detail** body, to the
90
+ model type the run trained. Empty for every service whose run spec
91
+ declares no ``model_type`` field — an absence of the axis, not a
92
+ lookup that fails, so a reader branches on the empty path instead of
93
+ probing and interpreting a miss. Only ``strategy`` declares one.
94
+
95
+ The strategy path is nested because a *top-level* ``model_type``
96
+ cannot exist on ``strategyRuns``: ``StrategyRunDocument`` is
97
+ ``extra="forbid"`` with a derived DB-level ``$jsonSchema`` validator,
98
+ so the collection has no room for the key and reading one is answered
99
+ by absence on every document (#2287 D-6). ``spec.model_type`` is
100
+ required by ``StrategyRunSpec`` and is written when the run document
101
+ is created, before any work runs — a run that failed in its first
102
+ 120 ms still carries it.
103
+
104
+ ⚠️ The detail body also carries ``lineage.model_type``. That is a
105
+ different axis (lineage metadata, populated by the lineage
106
+ publisher), and reading it here is how a model identity silently
107
+ becomes ``null``. This path is the one the owning service reads.
108
+ """
109
+
110
+ run_id_prefix: str
111
+ object_key_segment: str
112
+ http_path_segment: str
113
+ run_id_field: str
114
+ detail_identity_key: str
115
+ client_env_keys: tuple[str, ...]
116
+ default_base_url: str
117
+ model_type_detail_path: tuple[str, ...]
118
+
119
+
120
+ SERVICE_TOPOLOGY: dict[ServiceId, ServiceCoords] = {
121
+ "curation": ServiceCoords(
122
+ run_id_prefix="cur",
123
+ object_key_segment="curation",
124
+ http_path_segment="curations",
125
+ run_id_field="run_id",
126
+ detail_identity_key="run_id",
127
+ client_env_keys=("CURATION_API_URL",),
128
+ default_base_url="http://svc-curation:8000",
129
+ model_type_detail_path=(),
130
+ ),
131
+ "feature": ServiceCoords(
132
+ run_id_prefix="feat",
133
+ # Non-identity with the service name, and deliberately so: prod holds 149
134
+ # objects under ``features/``. #1834 fixed the walker to read this table
135
+ # instead of re-deriving the segment from the name; identity-ising it here
136
+ # would revert that fix.
137
+ object_key_segment="features",
138
+ http_path_segment="features",
139
+ run_id_field="feature_run_id",
140
+ detail_identity_key="run_id",
141
+ client_env_keys=("FEATURE_API_URL",),
142
+ default_base_url="http://svc-feature:8000",
143
+ model_type_detail_path=(),
144
+ ),
145
+ "strategy": ServiceCoords(
146
+ run_id_prefix="strat",
147
+ object_key_segment="strategy",
148
+ http_path_segment="strategies",
149
+ run_id_field="strategy_run_id",
150
+ detail_identity_key="run_id",
151
+ client_env_keys=("STRATEGY_API_URL",),
152
+ default_base_url="http://svc-strategy:8000",
153
+ model_type_detail_path=("spec", "model_type"),
154
+ ),
155
+ "validation": ServiceCoords(
156
+ run_id_prefix="val",
157
+ object_key_segment="validation",
158
+ http_path_segment="validations",
159
+ run_id_field="validation_run_id",
160
+ detail_identity_key="run_id",
161
+ client_env_keys=("VALIDATION_API_URL",),
162
+ default_base_url="http://svc-validation:8000",
163
+ model_type_detail_path=(),
164
+ ),
165
+ "portfolio": ServiceCoords(
166
+ run_id_prefix="port",
167
+ object_key_segment="portfolio",
168
+ http_path_segment="portfolios",
169
+ run_id_field="portfolio_run_id",
170
+ detail_identity_key="portfolio_run_id",
171
+ client_env_keys=("PORTFOLIO_API_URL",),
172
+ default_base_url="http://svc-portfolio:8000",
173
+ model_type_detail_path=(),
174
+ ),
175
+ }
176
+
177
+
178
+ class ArtifactNamespace(NamedTuple):
179
+ """A producer that owns artifact coordinates without being on the run chain.
180
+
181
+ Attributes:
182
+ artifact_id_prefix: Artifact-id prefix without the underscore.
183
+ object_key_segment: MinIO object-key segment for its artifacts.
184
+ """
185
+
186
+ artifact_id_prefix: str
187
+ object_key_segment: str
188
+
189
+
190
+ #: Producers with artifact coordinates but no chain run. Lineage walks resolve
191
+ #: their ids, so their prefix and segment must be declared here rather than
192
+ #: hardcoded at each walker (``manifest_chain`` carried a hardcoded ``"or_"``
193
+ #: entry directly under a comment recording the #1179 incident caused by exactly
194
+ #: that kind of hardcoding).
195
+ NON_PIPELINE_ARTIFACT_NAMESPACES: dict[str, ArtifactNamespace] = {
196
+ "oracle": ArtifactNamespace(artifact_id_prefix="or", object_key_segment="oracle"),
197
+ }
198
+
199
+ #: Run-ID namespaces that are not a service at all. ``opt`` is the oracle
200
+ #: optimisation job-id namespace: it mints ids but can never be a ``manifest.service``
201
+ #: value or a path segment, which is why it is declared apart from the two maps
202
+ #: above instead of sitting inside them behind a comment.
203
+ NON_SERVICE_RUN_ID_NAMESPACES: dict[str, str] = {"opt": "opt"}
204
+
205
+
206
+ # ---------------------------------------------------------------------------
207
+ # Import-time completeness guard
208
+ # ---------------------------------------------------------------------------
209
+ # Rewritten from the ``manifest_chain`` guard it replaces (#2034 D5-c): that
210
+ # guard checked two maps against each other, and those two maps are now one
211
+ # record, so the drift it watched for is structurally impossible. What is still
212
+ # possible is a row missing from this table, or a namespace colliding with a
213
+ # service — both of which would surface deep inside a write path, so they fail
214
+ # the import instead.
215
+
216
+ _MISSING_ROWS: list[str] = sorted(set(get_args(ServiceId)) - set(SERVICE_TOPOLOGY))
217
+ if _MISSING_ROWS: # pragma: no cover — import-time structural guard.
218
+ raise RuntimeError(
219
+ f"service_topology: ServiceId members with no SERVICE_TOPOLOGY row: {_MISSING_ROWS}. "
220
+ "Every service must declare all of its coordinates before any derived view can "
221
+ "be total."
222
+ )
223
+
224
+ _COLLIDING_NAMESPACES: list[str] = sorted(
225
+ (set(NON_PIPELINE_ARTIFACT_NAMESPACES) | set(NON_SERVICE_RUN_ID_NAMESPACES))
226
+ & set(SERVICE_TOPOLOGY)
227
+ )
228
+ if _COLLIDING_NAMESPACES: # pragma: no cover — import-time structural guard.
229
+ raise RuntimeError(
230
+ f"service_topology: namespaces collide with a ServiceId: {_COLLIDING_NAMESPACES}. "
231
+ "A colliding key would silently overwrite the service's entry in the derived "
232
+ "prefix/segment maps."
233
+ )
234
+
235
+ _ALL_PREFIXES: list[str] = [coords.run_id_prefix for coords in SERVICE_TOPOLOGY.values()] + [
236
+ namespace.artifact_id_prefix for namespace in NON_PIPELINE_ARTIFACT_NAMESPACES.values()
237
+ ]
238
+ _DUPLICATE_PREFIXES: list[str] = sorted(
239
+ {prefix for prefix in _ALL_PREFIXES if _ALL_PREFIXES.count(prefix) > 1}
240
+ )
241
+ if _DUPLICATE_PREFIXES: # pragma: no cover — import-time structural guard.
242
+ raise RuntimeError(
243
+ f"service_topology: duplicate run/artifact id prefixes: {_DUPLICATE_PREFIXES}. "
244
+ "Prefix resolution is first-match, so a duplicate routes one service's runs to "
245
+ "another service's storage."
246
+ )
247
+
248
+
249
+ __all__ = [
250
+ "NON_PIPELINE_ARTIFACT_NAMESPACES",
251
+ "NON_SERVICE_RUN_ID_NAMESPACES",
252
+ "SERVICE_TOPOLOGY",
253
+ "ArtifactNamespace",
254
+ "ServiceCoords",
255
+ "ServiceId",
256
+ ]