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.
- algoriq/__init__.py +71 -0
- algoriq/core/__init__.py +154 -0
- algoriq/core/service_topology.py +256 -0
- algoriq/indicators/__init__.py +250 -0
- algoriq/indicators/bars/__init__.py +37 -0
- algoriq/indicators/bars/_emit.py +52 -0
- algoriq/indicators/bars/_validate.py +88 -0
- algoriq/indicators/bars/dollar_bar.py +49 -0
- algoriq/indicators/bars/factory.py +80 -0
- algoriq/indicators/bars/imbalance_bar.py +178 -0
- algoriq/indicators/bars/protocol.py +56 -0
- algoriq/indicators/bars/quality_diagnostics.py +542 -0
- algoriq/indicators/bars/time_bar.py +63 -0
- algoriq/indicators/bars/volume_bar.py +75 -0
- algoriq/indicators/ffd/__init__.py +46 -0
- algoriq/indicators/ffd/frac_diff.py +118 -0
- algoriq/indicators/ffd/search.py +345 -0
- algoriq/indicators/ffd/weights.py +68 -0
- algoriq/indicators/ma/__init__.py +44 -0
- algoriq/indicators/ma/_validate.py +160 -0
- algoriq/indicators/ma/dema.py +40 -0
- algoriq/indicators/ma/ema.py +56 -0
- algoriq/indicators/ma/factory.py +84 -0
- algoriq/indicators/ma/hma.py +42 -0
- algoriq/indicators/ma/kama.py +114 -0
- algoriq/indicators/ma/protocol.py +45 -0
- algoriq/indicators/ma/rma.py +56 -0
- algoriq/indicators/ma/sma.py +42 -0
- algoriq/indicators/ma/tema.py +42 -0
- algoriq/indicators/ma/vwma.py +60 -0
- algoriq/indicators/ma/wma.py +49 -0
- algoriq/indicators/momentum/__init__.py +41 -0
- algoriq/indicators/momentum/_validate.py +120 -0
- algoriq/indicators/momentum/cci.py +70 -0
- algoriq/indicators/momentum/factory.py +122 -0
- algoriq/indicators/momentum/macd.py +71 -0
- algoriq/indicators/momentum/protocol.py +84 -0
- algoriq/indicators/momentum/roc.py +36 -0
- algoriq/indicators/momentum/rsi.py +82 -0
- algoriq/indicators/momentum/stochastic.py +89 -0
- algoriq/indicators/momentum/williams_r.py +50 -0
- algoriq/indicators/registry.py +303 -0
- algoriq/indicators/trend/__init__.py +38 -0
- algoriq/indicators/trend/_validate.py +62 -0
- algoriq/indicators/trend/adx.py +103 -0
- algoriq/indicators/trend/aroon.py +79 -0
- algoriq/indicators/trend/factory.py +59 -0
- algoriq/indicators/trend/ichimoku.py +120 -0
- algoriq/indicators/trend/parabolic_sar.py +184 -0
- algoriq/indicators/trend/protocol.py +41 -0
- algoriq/indicators/trend/supertrend.py +158 -0
- algoriq/indicators/volatility/__init__.py +47 -0
- algoriq/indicators/volatility/_validate.py +73 -0
- algoriq/indicators/volatility/atr.py +92 -0
- algoriq/indicators/volatility/bollinger_bands.py +66 -0
- algoriq/indicators/volatility/factory.py +72 -0
- algoriq/indicators/volatility/garman_klass.py +83 -0
- algoriq/indicators/volatility/keltner_channel.py +69 -0
- algoriq/indicators/volatility/parkinson.py +56 -0
- algoriq/indicators/volatility/protocol.py +42 -0
- algoriq/indicators/volatility/squeeze_momentum.py +111 -0
- algoriq/indicators/volatility/std_dev.py +53 -0
- algoriq/indicators/volatility/yang_zhang.py +107 -0
- algoriq/indicators/volume/__init__.py +32 -0
- algoriq/indicators/volume/_validate.py +95 -0
- algoriq/indicators/volume/ad_line.py +54 -0
- algoriq/indicators/volume/factory.py +56 -0
- algoriq/indicators/volume/mfi.py +61 -0
- algoriq/indicators/volume/obv.py +51 -0
- algoriq/indicators/volume/protocol.py +37 -0
- algoriq/indicators/volume/vwap.py +91 -0
- algoriq/labeling/__init__.py +74 -0
- algoriq/labeling/cusum.py +250 -0
- algoriq/labeling/factory.py +225 -0
- algoriq/labeling/fixed_time.py +227 -0
- algoriq/labeling/meta.py +206 -0
- algoriq/labeling/side_policy.py +94 -0
- algoriq/labeling/triple_barrier.py +514 -0
- algoriq/labeling/volatility.py +64 -0
- algoriq/ml/__init__.py +876 -0
- algoriq/ml/ale.py +489 -0
- algoriq/ml/bayesian/__init__.py +46 -0
- algoriq/ml/bayesian/bayesian_gate.py +128 -0
- algoriq/ml/bayesian/calibration.py +129 -0
- algoriq/ml/bayesian/config.py +259 -0
- algoriq/ml/bayesian/ensemble.py +138 -0
- algoriq/ml/bayesian/posterior.py +205 -0
- algoriq/ml/bet_sizing/__init__.py +48 -0
- algoriq/ml/bet_sizing/base.py +342 -0
- algoriq/ml/bet_sizing/kelly_sizing.py +44 -0
- algoriq/ml/bet_sizing/meta_sizing.py +24 -0
- algoriq/ml/bet_sizing/sigmoid_sizing.py +43 -0
- algoriq/ml/bet_sizing/threshold_sizing.py +35 -0
- algoriq/ml/causal/__init__.py +77 -0
- algoriq/ml/causal/analyzer.py +811 -0
- algoriq/ml/causal/confounders.py +162 -0
- algoriq/ml/causal/dml.py +470 -0
- algoriq/ml/causal/refutation.py +421 -0
- algoriq/ml/causal/treatment.py +193 -0
- algoriq/ml/clustering.py +498 -0
- algoriq/ml/constraint_enforcer/__init__.py +36 -0
- algoriq/ml/constraint_enforcer/base.py +196 -0
- algoriq/ml/constraint_enforcer/max_leverage.py +63 -0
- algoriq/ml/constraint_enforcer/max_turnover.py +105 -0
- algoriq/ml/constraint_enforcer/max_weight.py +67 -0
- algoriq/ml/constraint_enforcer/min_holding.py +122 -0
- algoriq/ml/covariance.py +547 -0
- algoriq/ml/cross_sectional.py +250 -0
- algoriq/ml/cross_validation.py +1222 -0
- algoriq/ml/dedup.py +450 -0
- algoriq/ml/device.py +27 -0
- algoriq/ml/entropy.py +719 -0
- algoriq/ml/foundation/__init__.py +22 -0
- algoriq/ml/foundation/embedder.py +202 -0
- algoriq/ml/fusion.py +718 -0
- algoriq/ml/ic_decay.py +255 -0
- algoriq/ml/importance.py +1069 -0
- algoriq/ml/intraday.py +345 -0
- algoriq/ml/matrix_clustering.py +229 -0
- algoriq/ml/metrics.py +371 -0
- algoriq/ml/microstructure.py +556 -0
- algoriq/ml/model_type.py +184 -0
- algoriq/ml/quantile_scoring.py +506 -0
- algoriq/ml/regime/__init__.py +20 -0
- algoriq/ml/regime/hmm_regime_classifier.py +412 -0
- algoriq/ml/regime/regime_classifier_service.py +199 -0
- algoriq/ml/regime_base_rate.py +322 -0
- algoriq/ml/regime_ic.py +295 -0
- algoriq/ml/regression_target.py +263 -0
- algoriq/ml/risk_metrics.py +909 -0
- algoriq/ml/sample_weights.py +915 -0
- algoriq/ml/screening_budget.py +266 -0
- algoriq/ml/shared_models/__init__.py +0 -0
- algoriq/ml/shared_models/cross_sectional_net.py +225 -0
- algoriq/ml/shared_models/custom_objectives.py +246 -0
- algoriq/ml/shared_models/e2e_portfolio.py +253 -0
- algoriq/ml/shared_models/e2e_trainer.py +747 -0
- algoriq/ml/shared_models/ml_models.py +3767 -0
- algoriq/ml/shared_models/temporal_alpha_net.py +339 -0
- algoriq/ml/shared_models/wavelet_alpha.py +333 -0
- algoriq/ml/sharpe_inference.py +736 -0
- algoriq/ml/strategy_view.py +488 -0
- algoriq/ml/structural_break.py +893 -0
- algoriq/ml/symbolic/__init__.py +25 -0
- algoriq/ml/symbolic/discoverer.py +466 -0
- algoriq/ml/timeframe_params.py +251 -0
- algoriq/ml/turnover.py +105 -0
- algoriq/ml/vrp.py +377 -0
- algoriq/ml/weights.py +354 -0
- algoriq/schemas/__init__.py +42 -0
- algoriq/schemas/_base/__init__.py +5 -0
- algoriq/schemas/_base/enum.py +1 -0
- algoriq/schemas/_base/model.py +54 -0
- algoriq/schemas/artifacts/__init__.py +102 -0
- algoriq/schemas/artifacts/api.py +120 -0
- algoriq/schemas/artifacts/dataset.py +955 -0
- algoriq/schemas/artifacts/ingredient.py +758 -0
- algoriq/schemas/artifacts/manifest.py +348 -0
- algoriq/schemas/constants.py +129 -0
- algoriq/schemas/curation/__init__.py +120 -0
- algoriq/schemas/curation/event_pit.py +70 -0
- algoriq/schemas/curation/exogenous_level.py +71 -0
- algoriq/schemas/curation/instrument_contract.py +87 -0
- algoriq/schemas/curation/load_feasibility.py +400 -0
- algoriq/schemas/curation/pit_integrity.py +360 -0
- algoriq/schemas/curation/pit_ohlcv.py +467 -0
- algoriq/schemas/curation/quality.py +166 -0
- algoriq/schemas/curation/quality_rollup.py +273 -0
- algoriq/schemas/curation/spec.py +812 -0
- algoriq/schemas/data_axis/__init__.py +69 -0
- algoriq/schemas/data_axis/access.py +96 -0
- algoriq/schemas/data_axis/adapter.py +132 -0
- algoriq/schemas/data_axis/instrument.py +59 -0
- algoriq/schemas/data_axis/pit.py +65 -0
- algoriq/schemas/data_axis/provider.py +195 -0
- algoriq/schemas/data_axis/serving.py +42 -0
- algoriq/schemas/data_axis/shape.py +43 -0
- algoriq/schemas/data_axis/source.py +53 -0
- algoriq/schemas/market_data/__init__.py +106 -0
- algoriq/schemas/market_data/company_profile.py +99 -0
- algoriq/schemas/market_data/corp_action.py +191 -0
- algoriq/schemas/market_data/crypto_derivatives.py +90 -0
- algoriq/schemas/market_data/daily_metrics.py +71 -0
- algoriq/schemas/market_data/fundamental.py +199 -0
- algoriq/schemas/market_data/index_constituent.py +93 -0
- algoriq/schemas/market_data/institutional_holdings.py +164 -0
- algoriq/schemas/market_data/ohlcv.py +95 -0
- algoriq/schemas/market_data/onchain_metrics.py +52 -0
- algoriq/schemas/model_registry.py +1374 -0
- algoriq/schemas/research/__init__.py +1 -0
- algoriq/schemas/research/intake.py +329 -0
- algoriq/schemas/research/observation.py +215 -0
- algoriq/schemas/research/publication.py +49 -0
- algoriq/schemas/research/screening.py +195 -0
- algoriq/schemas/research/snapshot.py +106 -0
- algoriq/schemas/shared/__init__.py +33 -0
- algoriq/schemas/shared/api.py +27 -0
- algoriq/schemas/shared/artifact.py +52 -0
- algoriq/schemas/shared/forge.py +96 -0
- algoriq/schemas/shared/identity.py +10 -0
- algoriq/schemas/shared/lineage.py +83 -0
- algoriq/schemas/shared/quality.py +120 -0
- algoriq/schemas/shared/raw.py +46 -0
- algoriq/schemas/shared/run.py +424 -0
- algoriq/schemas/shared/time.py +58 -0
- algoriq/schemas/strategy/__init__.py +421 -0
- algoriq/schemas/strategy/_models.py +698 -0
- algoriq/schemas/strategy/api.py +335 -0
- algoriq/schemas/strategy/artifacts.py +71 -0
- algoriq/schemas/strategy/bundle.py +130 -0
- algoriq/schemas/strategy/cv.py +17 -0
- algoriq/schemas/strategy/diagnostics.py +30 -0
- algoriq/schemas/strategy/dsl.py +49 -0
- algoriq/schemas/strategy/dsl_api.py +316 -0
- algoriq/schemas/strategy/dsl_vocabulary.py +133 -0
- algoriq/schemas/strategy/enums.py +152 -0
- algoriq/schemas/strategy/evaluation.py +122 -0
- algoriq/schemas/strategy/execution_reeval.py +177 -0
- algoriq/schemas/strategy/experiment.py +79 -0
- algoriq/schemas/strategy/family.py +835 -0
- algoriq/schemas/strategy/family_feature.py +85 -0
- algoriq/schemas/strategy/family_validation.py +84 -0
- algoriq/schemas/strategy/fiir.py +49 -0
- algoriq/schemas/strategy/hpo.py +212 -0
- algoriq/schemas/strategy/meta_hpo.py +49 -0
- algoriq/schemas/strategy/promotion.py +82 -0
- algoriq/schemas/strategy/quick_backtest_api.py +83 -0
- algoriq/schemas/strategy/quick_backtest_emission.py +224 -0
- algoriq/schemas/strategy/regime.py +111 -0
- algoriq/schemas/strategy/run.py +410 -0
- algoriq/schemas/strategy/session.py +151 -0
- algoriq/schemas/strategy/strategy_config.py +98 -0
- algoriq/schemas/strategy/tba.py +32 -0
- algoriq/schemas/strategy/train_api.py +327 -0
- algoriq/schemas/strategy/trained_model.py +77 -0
- algoriq/schemas/strategy/trial_ledger.py +744 -0
- algoriq/schemas/strategy/trial_tracker_api.py +71 -0
- algoriq/schemas/strategy/walk_forward.py +83 -0
- algoriq/schemas/validation/__init__.py +294 -0
- algoriq/schemas/validation/_models.py +492 -0
- algoriq/schemas/validation/api.py +409 -0
- algoriq/schemas/validation/cpcv.py +66 -0
- algoriq/schemas/validation/enums.py +53 -0
- algoriq/schemas/validation/kpi.py +256 -0
- algoriq/schemas/validation/kpi_gates_artifact.py +78 -0
- algoriq/schemas/validation/leakage.py +180 -0
- algoriq/schemas/validation/regime_ic.py +182 -0
- algoriq/schemas/validation/returns_audit.py +224 -0
- algoriq/schemas/validation/run.py +180 -0
- algoriq/schemas/validation/scenario.py +30 -0
- algoriq/schemas/validation/seven_sins.py +50 -0
- algoriq/schemas/validation/signal_diagnostics.py +97 -0
- algoriq/schemas/validation/status.py +259 -0
- algoriq/schemas/validation/status_artifact.py +162 -0
- algoriq/schemas/validation/tier_zero.py +361 -0
- algoriq/schemas/validation/walk_forward.py +434 -0
- algoriq-0.2.0.dist-info/METADATA +193 -0
- algoriq-0.2.0.dist-info/RECORD +267 -0
- algoriq-0.2.0.dist-info/WHEEL +4 -0
- algoriq_research/__init__.py +237 -0
- algoriq_research/_probes.py +372 -0
- algoriq_research/capabilities.py +826 -0
- algoriq_research/capability_status.py +59 -0
- algoriq_research/client.py +591 -0
- algoriq_research/errors.py +136 -0
- algoriq_research/plane.py +188 -0
- 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()))
|
algoriq/core/__init__.py
ADDED
|
@@ -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
|
+
]
|