ins-pricing 0.1.6__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.
- ins_pricing/README.md +60 -0
- ins_pricing/__init__.py +102 -0
- ins_pricing/governance/README.md +18 -0
- ins_pricing/governance/__init__.py +20 -0
- ins_pricing/governance/approval.py +93 -0
- ins_pricing/governance/audit.py +37 -0
- ins_pricing/governance/registry.py +99 -0
- ins_pricing/governance/release.py +159 -0
- ins_pricing/modelling/BayesOpt.py +146 -0
- ins_pricing/modelling/BayesOpt_USAGE.md +925 -0
- ins_pricing/modelling/BayesOpt_entry.py +575 -0
- ins_pricing/modelling/BayesOpt_incremental.py +731 -0
- ins_pricing/modelling/Explain_Run.py +36 -0
- ins_pricing/modelling/Explain_entry.py +539 -0
- ins_pricing/modelling/Pricing_Run.py +36 -0
- ins_pricing/modelling/README.md +33 -0
- ins_pricing/modelling/__init__.py +44 -0
- ins_pricing/modelling/bayesopt/__init__.py +98 -0
- ins_pricing/modelling/bayesopt/config_preprocess.py +303 -0
- ins_pricing/modelling/bayesopt/core.py +1476 -0
- ins_pricing/modelling/bayesopt/models.py +2196 -0
- ins_pricing/modelling/bayesopt/trainers.py +2446 -0
- ins_pricing/modelling/bayesopt/utils.py +1021 -0
- ins_pricing/modelling/cli_common.py +136 -0
- ins_pricing/modelling/explain/__init__.py +55 -0
- ins_pricing/modelling/explain/gradients.py +334 -0
- ins_pricing/modelling/explain/metrics.py +176 -0
- ins_pricing/modelling/explain/permutation.py +155 -0
- ins_pricing/modelling/explain/shap_utils.py +146 -0
- ins_pricing/modelling/notebook_utils.py +284 -0
- ins_pricing/modelling/plotting/__init__.py +45 -0
- ins_pricing/modelling/plotting/common.py +63 -0
- ins_pricing/modelling/plotting/curves.py +572 -0
- ins_pricing/modelling/plotting/diagnostics.py +139 -0
- ins_pricing/modelling/plotting/geo.py +362 -0
- ins_pricing/modelling/plotting/importance.py +121 -0
- ins_pricing/modelling/run_logging.py +133 -0
- ins_pricing/modelling/tests/conftest.py +8 -0
- ins_pricing/modelling/tests/test_cross_val_generic.py +66 -0
- ins_pricing/modelling/tests/test_distributed_utils.py +18 -0
- ins_pricing/modelling/tests/test_explain.py +56 -0
- ins_pricing/modelling/tests/test_geo_tokens_split.py +49 -0
- ins_pricing/modelling/tests/test_graph_cache.py +33 -0
- ins_pricing/modelling/tests/test_plotting.py +63 -0
- ins_pricing/modelling/tests/test_plotting_library.py +150 -0
- ins_pricing/modelling/tests/test_preprocessor.py +48 -0
- ins_pricing/modelling/watchdog_run.py +211 -0
- ins_pricing/pricing/README.md +44 -0
- ins_pricing/pricing/__init__.py +27 -0
- ins_pricing/pricing/calibration.py +39 -0
- ins_pricing/pricing/data_quality.py +117 -0
- ins_pricing/pricing/exposure.py +85 -0
- ins_pricing/pricing/factors.py +91 -0
- ins_pricing/pricing/monitoring.py +99 -0
- ins_pricing/pricing/rate_table.py +78 -0
- ins_pricing/production/__init__.py +21 -0
- ins_pricing/production/drift.py +30 -0
- ins_pricing/production/monitoring.py +143 -0
- ins_pricing/production/scoring.py +40 -0
- ins_pricing/reporting/README.md +20 -0
- ins_pricing/reporting/__init__.py +11 -0
- ins_pricing/reporting/report_builder.py +72 -0
- ins_pricing/reporting/scheduler.py +45 -0
- ins_pricing/setup.py +41 -0
- ins_pricing v2/__init__.py +23 -0
- ins_pricing v2/governance/__init__.py +20 -0
- ins_pricing v2/governance/approval.py +93 -0
- ins_pricing v2/governance/audit.py +37 -0
- ins_pricing v2/governance/registry.py +99 -0
- ins_pricing v2/governance/release.py +159 -0
- ins_pricing v2/modelling/Explain_Run.py +36 -0
- ins_pricing v2/modelling/Pricing_Run.py +36 -0
- ins_pricing v2/modelling/__init__.py +151 -0
- ins_pricing v2/modelling/cli_common.py +141 -0
- ins_pricing v2/modelling/config.py +249 -0
- ins_pricing v2/modelling/config_preprocess.py +254 -0
- ins_pricing v2/modelling/core.py +741 -0
- ins_pricing v2/modelling/data_container.py +42 -0
- ins_pricing v2/modelling/explain/__init__.py +55 -0
- ins_pricing v2/modelling/explain/gradients.py +334 -0
- ins_pricing v2/modelling/explain/metrics.py +176 -0
- ins_pricing v2/modelling/explain/permutation.py +155 -0
- ins_pricing v2/modelling/explain/shap_utils.py +146 -0
- ins_pricing v2/modelling/features.py +215 -0
- ins_pricing v2/modelling/model_manager.py +148 -0
- ins_pricing v2/modelling/model_plotting.py +463 -0
- ins_pricing v2/modelling/models.py +2203 -0
- ins_pricing v2/modelling/notebook_utils.py +294 -0
- ins_pricing v2/modelling/plotting/__init__.py +45 -0
- ins_pricing v2/modelling/plotting/common.py +63 -0
- ins_pricing v2/modelling/plotting/curves.py +572 -0
- ins_pricing v2/modelling/plotting/diagnostics.py +139 -0
- ins_pricing v2/modelling/plotting/geo.py +362 -0
- ins_pricing v2/modelling/plotting/importance.py +121 -0
- ins_pricing v2/modelling/run_logging.py +133 -0
- ins_pricing v2/modelling/tests/conftest.py +8 -0
- ins_pricing v2/modelling/tests/test_cross_val_generic.py +66 -0
- ins_pricing v2/modelling/tests/test_distributed_utils.py +18 -0
- ins_pricing v2/modelling/tests/test_explain.py +56 -0
- ins_pricing v2/modelling/tests/test_geo_tokens_split.py +49 -0
- ins_pricing v2/modelling/tests/test_graph_cache.py +33 -0
- ins_pricing v2/modelling/tests/test_plotting.py +63 -0
- ins_pricing v2/modelling/tests/test_plotting_library.py +150 -0
- ins_pricing v2/modelling/tests/test_preprocessor.py +48 -0
- ins_pricing v2/modelling/trainers.py +2447 -0
- ins_pricing v2/modelling/utils.py +1020 -0
- ins_pricing v2/modelling/watchdog_run.py +211 -0
- ins_pricing v2/pricing/__init__.py +27 -0
- ins_pricing v2/pricing/calibration.py +39 -0
- ins_pricing v2/pricing/data_quality.py +117 -0
- ins_pricing v2/pricing/exposure.py +85 -0
- ins_pricing v2/pricing/factors.py +91 -0
- ins_pricing v2/pricing/monitoring.py +99 -0
- ins_pricing v2/pricing/rate_table.py +78 -0
- ins_pricing v2/production/__init__.py +21 -0
- ins_pricing v2/production/drift.py +30 -0
- ins_pricing v2/production/monitoring.py +143 -0
- ins_pricing v2/production/scoring.py +40 -0
- ins_pricing v2/reporting/__init__.py +11 -0
- ins_pricing v2/reporting/report_builder.py +72 -0
- ins_pricing v2/reporting/scheduler.py +45 -0
- ins_pricing v2/scripts/BayesOpt_incremental.py +722 -0
- ins_pricing v2/scripts/Explain_entry.py +545 -0
- ins_pricing v2/scripts/__init__.py +1 -0
- ins_pricing v2/scripts/train.py +568 -0
- ins_pricing v2/setup.py +55 -0
- ins_pricing v2/smoke_test.py +28 -0
- ins_pricing-0.1.6.dist-info/METADATA +78 -0
- ins_pricing-0.1.6.dist-info/RECORD +169 -0
- ins_pricing-0.1.6.dist-info/WHEEL +5 -0
- ins_pricing-0.1.6.dist-info/top_level.txt +4 -0
- user_packages/__init__.py +105 -0
- user_packages legacy/BayesOpt.py +5659 -0
- user_packages legacy/BayesOpt_entry.py +513 -0
- user_packages legacy/BayesOpt_incremental.py +685 -0
- user_packages legacy/Pricing_Run.py +36 -0
- user_packages legacy/Try/BayesOpt Legacy251213.py +3719 -0
- user_packages legacy/Try/BayesOpt Legacy251215.py +3758 -0
- user_packages legacy/Try/BayesOpt lagecy251201.py +3506 -0
- user_packages legacy/Try/BayesOpt lagecy251218.py +3992 -0
- user_packages legacy/Try/BayesOpt legacy.py +3280 -0
- user_packages legacy/Try/BayesOpt.py +838 -0
- user_packages legacy/Try/BayesOptAll.py +1569 -0
- user_packages legacy/Try/BayesOptAllPlatform.py +909 -0
- user_packages legacy/Try/BayesOptCPUGPU.py +1877 -0
- user_packages legacy/Try/BayesOptSearch.py +830 -0
- user_packages legacy/Try/BayesOptSearchOrigin.py +829 -0
- user_packages legacy/Try/BayesOptV1.py +1911 -0
- user_packages legacy/Try/BayesOptV10.py +2973 -0
- user_packages legacy/Try/BayesOptV11.py +3001 -0
- user_packages legacy/Try/BayesOptV12.py +3001 -0
- user_packages legacy/Try/BayesOptV2.py +2065 -0
- user_packages legacy/Try/BayesOptV3.py +2209 -0
- user_packages legacy/Try/BayesOptV4.py +2342 -0
- user_packages legacy/Try/BayesOptV5.py +2372 -0
- user_packages legacy/Try/BayesOptV6.py +2759 -0
- user_packages legacy/Try/BayesOptV7.py +2832 -0
- user_packages legacy/Try/BayesOptV8Codex.py +2731 -0
- user_packages legacy/Try/BayesOptV8Gemini.py +2614 -0
- user_packages legacy/Try/BayesOptV9.py +2927 -0
- user_packages legacy/Try/BayesOpt_entry legacy.py +313 -0
- user_packages legacy/Try/ModelBayesOptSearch.py +359 -0
- user_packages legacy/Try/ResNetBayesOptSearch.py +249 -0
- user_packages legacy/Try/XgbBayesOptSearch.py +121 -0
- user_packages legacy/Try/xgbbayesopt.py +523 -0
- user_packages legacy/__init__.py +19 -0
- user_packages legacy/cli_common.py +124 -0
- user_packages legacy/notebook_utils.py +228 -0
- user_packages legacy/watchdog_run.py +202 -0
ins_pricing/README.md
ADDED
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
# Ins-Pricing
|
|
2
|
+
|
|
3
|
+
Distribution name: Ins-Pricing (import package is `ins_pricing`, legacy alias `user_packages` still works).
|
|
4
|
+
|
|
5
|
+
Reusable modelling and pricing utilities organized as a small toolbox with clear boundaries
|
|
6
|
+
between modelling, production, governance, and reporting.
|
|
7
|
+
|
|
8
|
+
## Architecture
|
|
9
|
+
|
|
10
|
+
- `modelling/`
|
|
11
|
+
- `bayesopt/`: BayesOpt training core (GLM / XGB / ResNet / FT / GNN).
|
|
12
|
+
- `plotting/`: model-agnostic curves and geo visualizations.
|
|
13
|
+
- `explain/`: permutation, gradients, and SHAP helpers.
|
|
14
|
+
- `BayesOpt_entry.py`: CLI runner for batch training.
|
|
15
|
+
- `BayesOpt_incremental.py`: CLI for incremental runs.
|
|
16
|
+
- `Pricing_Run.py`: lightweight pricing orchestration.
|
|
17
|
+
- `pricing/`: factor tables, calibration, exposure, monitoring.
|
|
18
|
+
- `production/`: scoring, metrics, drift/PSI.
|
|
19
|
+
- `governance/`: registry, release, audit, approval workflow.
|
|
20
|
+
- `reporting/`: report builder + scheduler.
|
|
21
|
+
|
|
22
|
+
## Call flow (typical)
|
|
23
|
+
|
|
24
|
+
1. Model training
|
|
25
|
+
- Python API: `from ins_pricing.modelling import BayesOptModel`
|
|
26
|
+
- CLI: `python ins_pricing/modelling/BayesOpt_entry.py --config-json ...`
|
|
27
|
+
2. Evaluation & visualization
|
|
28
|
+
- Curves: `from ins_pricing.plotting import curves`
|
|
29
|
+
- Importance: `from ins_pricing.plotting import importance`
|
|
30
|
+
- Geo: `from ins_pricing.plotting import geo`
|
|
31
|
+
3. Explainability
|
|
32
|
+
- `from ins_pricing.explain import permutation_importance, integrated_gradients_torch`
|
|
33
|
+
4. Pricing loop
|
|
34
|
+
- `from ins_pricing.pricing import build_factor_table, rate_premium`
|
|
35
|
+
5. Production & governance
|
|
36
|
+
- `from ins_pricing.production import batch_score, psi_report`
|
|
37
|
+
- `from ins_pricing.governance import ModelRegistry, ReleaseManager`
|
|
38
|
+
6. Reporting
|
|
39
|
+
- `from ins_pricing.reporting import build_report, write_report, schedule_daily`
|
|
40
|
+
|
|
41
|
+
## Import notes
|
|
42
|
+
|
|
43
|
+
- `ins_pricing` exposes lightweight lazy imports so that `pricing/production/governance`
|
|
44
|
+
can be used without installing heavy ML dependencies.
|
|
45
|
+
- Demo notebooks/configs live in the repo under `ins_pricing/modelling/demo/` and are not shipped in the PyPI package.
|
|
46
|
+
- Heavy dependencies are only required when you import or use the related modules:
|
|
47
|
+
- BayesOpt: `torch`, `optuna`, `xgboost`, etc.
|
|
48
|
+
- Explain: `torch` (gradients), `shap` (SHAP).
|
|
49
|
+
- Geo plotting on basemap: `contextily`.
|
|
50
|
+
- Plotting: `matplotlib`.
|
|
51
|
+
|
|
52
|
+
## Backward-compatible imports
|
|
53
|
+
|
|
54
|
+
Legacy import paths continue to work:
|
|
55
|
+
|
|
56
|
+
- `import user_packages`
|
|
57
|
+
- `import user_packages.bayesopt`
|
|
58
|
+
- `import user_packages.plotting`
|
|
59
|
+
- `import user_packages.explain`
|
|
60
|
+
- `import user_packages.BayesOpt`
|
ins_pricing/__init__.py
ADDED
|
@@ -0,0 +1,102 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from importlib import import_module
|
|
4
|
+
from pathlib import Path
|
|
5
|
+
import sys
|
|
6
|
+
import types
|
|
7
|
+
|
|
8
|
+
_ROOT_SUBPACKAGES = {
|
|
9
|
+
"modelling": "ins_pricing.modelling",
|
|
10
|
+
"pricing": "ins_pricing.pricing",
|
|
11
|
+
"production": "ins_pricing.production",
|
|
12
|
+
"governance": "ins_pricing.governance",
|
|
13
|
+
"reporting": "ins_pricing.reporting",
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
_MODELLING_EXPORTS = {
|
|
17
|
+
"BayesOptConfig",
|
|
18
|
+
"BayesOptModel",
|
|
19
|
+
"IOUtils",
|
|
20
|
+
"TrainingUtils",
|
|
21
|
+
"free_cuda",
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
_LAZY_SUBMODULES = {
|
|
25
|
+
"bayesopt": "ins_pricing.modelling.bayesopt",
|
|
26
|
+
"plotting": "ins_pricing.modelling.plotting",
|
|
27
|
+
"explain": "ins_pricing.modelling.explain",
|
|
28
|
+
"BayesOpt": "ins_pricing.modelling.BayesOpt",
|
|
29
|
+
"BayesOpt_entry": "ins_pricing.modelling.BayesOpt_entry",
|
|
30
|
+
"BayesOpt_incremental": "ins_pricing.modelling.BayesOpt_incremental",
|
|
31
|
+
"Explain_entry": "ins_pricing.modelling.Explain_entry",
|
|
32
|
+
"Explain_Run": "ins_pricing.modelling.Explain_Run",
|
|
33
|
+
"Pricing_Run": "ins_pricing.modelling.Pricing_Run",
|
|
34
|
+
"cli_common": "ins_pricing.modelling.cli_common",
|
|
35
|
+
"notebook_utils": "ins_pricing.modelling.notebook_utils",
|
|
36
|
+
"watchdog_run": "ins_pricing.modelling.watchdog_run",
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
_PACKAGE_PATHS = {
|
|
40
|
+
"bayesopt": Path(__file__).resolve().parent / "modelling" / "bayesopt",
|
|
41
|
+
"plotting": Path(__file__).resolve().parent / "modelling" / "plotting",
|
|
42
|
+
"explain": Path(__file__).resolve().parent / "modelling" / "explain",
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
__all__ = sorted(
|
|
46
|
+
set(_ROOT_SUBPACKAGES)
|
|
47
|
+
| set(_MODELLING_EXPORTS)
|
|
48
|
+
| set(_LAZY_SUBMODULES)
|
|
49
|
+
)
|
|
50
|
+
|
|
51
|
+
|
|
52
|
+
def _lazy_module(name: str, target: str, package_path: Path | None = None) -> types.ModuleType:
|
|
53
|
+
proxy = types.ModuleType(name)
|
|
54
|
+
if package_path is not None:
|
|
55
|
+
proxy.__path__ = [str(package_path)]
|
|
56
|
+
|
|
57
|
+
def _load():
|
|
58
|
+
module = import_module(target)
|
|
59
|
+
sys.modules[name] = module
|
|
60
|
+
return module
|
|
61
|
+
|
|
62
|
+
def __getattr__(attr: str):
|
|
63
|
+
module = _load()
|
|
64
|
+
return getattr(module, attr)
|
|
65
|
+
|
|
66
|
+
def __dir__() -> list[str]:
|
|
67
|
+
module = _load()
|
|
68
|
+
return sorted(set(dir(module)))
|
|
69
|
+
|
|
70
|
+
proxy.__getattr__ = __getattr__ # type: ignore[attr-defined]
|
|
71
|
+
proxy.__dir__ = __dir__ # type: ignore[attr-defined]
|
|
72
|
+
return proxy
|
|
73
|
+
|
|
74
|
+
|
|
75
|
+
def _install_proxy(alias: str, target: str) -> None:
|
|
76
|
+
module_name = f"{__name__}.{alias}"
|
|
77
|
+
if module_name in sys.modules:
|
|
78
|
+
return
|
|
79
|
+
proxy = _lazy_module(module_name, target, _PACKAGE_PATHS.get(alias))
|
|
80
|
+
sys.modules[module_name] = proxy
|
|
81
|
+
globals()[alias] = proxy
|
|
82
|
+
|
|
83
|
+
|
|
84
|
+
for _alias, _target in _LAZY_SUBMODULES.items():
|
|
85
|
+
_install_proxy(_alias, _target)
|
|
86
|
+
|
|
87
|
+
|
|
88
|
+
def __getattr__(name: str):
|
|
89
|
+
if name in _ROOT_SUBPACKAGES:
|
|
90
|
+
module = import_module(_ROOT_SUBPACKAGES[name])
|
|
91
|
+
globals()[name] = module
|
|
92
|
+
return module
|
|
93
|
+
if name in _MODELLING_EXPORTS:
|
|
94
|
+
module = import_module("ins_pricing.modelling")
|
|
95
|
+
value = getattr(module, name)
|
|
96
|
+
globals()[name] = value
|
|
97
|
+
return value
|
|
98
|
+
raise AttributeError(f"module {__name__!r} has no attribute {name!r}")
|
|
99
|
+
|
|
100
|
+
|
|
101
|
+
def __dir__() -> list[str]:
|
|
102
|
+
return sorted(set(__all__) | set(globals().keys()))
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
# governance
|
|
2
|
+
|
|
3
|
+
Lightweight model registry, approval, audit log, and release management.
|
|
4
|
+
|
|
5
|
+
Example (deploy + rollback):
|
|
6
|
+
|
|
7
|
+
```python
|
|
8
|
+
from ins_pricing.governance import ModelRegistry, ReleaseManager
|
|
9
|
+
|
|
10
|
+
registry = ModelRegistry("Registry/models.json")
|
|
11
|
+
release = ReleaseManager("Registry/deployments", registry=registry)
|
|
12
|
+
|
|
13
|
+
registry.register("pricing_ft", "v1", metrics={"rmse": 0.12})
|
|
14
|
+
release.deploy("prod", "pricing_ft", "v1", actor="ops")
|
|
15
|
+
|
|
16
|
+
# rollback to previous active version
|
|
17
|
+
release.rollback("prod", actor="ops")
|
|
18
|
+
```
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from .approval import ApprovalAction, ApprovalRequest, ApprovalStore
|
|
4
|
+
from .audit import AuditEvent, AuditLogger
|
|
5
|
+
from .registry import ModelArtifact, ModelRegistry, ModelVersion
|
|
6
|
+
from .release import DeploymentState, ModelRef, ReleaseManager
|
|
7
|
+
|
|
8
|
+
__all__ = [
|
|
9
|
+
"ApprovalAction",
|
|
10
|
+
"ApprovalRequest",
|
|
11
|
+
"ApprovalStore",
|
|
12
|
+
"AuditEvent",
|
|
13
|
+
"AuditLogger",
|
|
14
|
+
"ModelArtifact",
|
|
15
|
+
"ModelRegistry",
|
|
16
|
+
"ModelVersion",
|
|
17
|
+
"DeploymentState",
|
|
18
|
+
"ModelRef",
|
|
19
|
+
"ReleaseManager",
|
|
20
|
+
]
|
|
@@ -0,0 +1,93 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import json
|
|
4
|
+
from dataclasses import asdict, dataclass, field
|
|
5
|
+
from datetime import datetime
|
|
6
|
+
from pathlib import Path
|
|
7
|
+
from typing import List, Optional
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
@dataclass
|
|
11
|
+
class ApprovalAction:
|
|
12
|
+
actor: str
|
|
13
|
+
decision: str
|
|
14
|
+
timestamp: str
|
|
15
|
+
comment: Optional[str] = None
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
@dataclass
|
|
19
|
+
class ApprovalRequest:
|
|
20
|
+
model_name: str
|
|
21
|
+
model_version: str
|
|
22
|
+
requested_by: str
|
|
23
|
+
requested_at: str
|
|
24
|
+
status: str = "pending"
|
|
25
|
+
actions: List[ApprovalAction] = field(default_factory=list)
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
class ApprovalStore:
|
|
29
|
+
"""Simple approval workflow stored as JSON."""
|
|
30
|
+
|
|
31
|
+
def __init__(self, store_path: str | Path):
|
|
32
|
+
self.store_path = Path(store_path)
|
|
33
|
+
self.store_path.parent.mkdir(parents=True, exist_ok=True)
|
|
34
|
+
|
|
35
|
+
def _load(self) -> List[dict]:
|
|
36
|
+
if not self.store_path.exists():
|
|
37
|
+
return []
|
|
38
|
+
with self.store_path.open("r", encoding="utf-8") as fh:
|
|
39
|
+
return json.load(fh)
|
|
40
|
+
|
|
41
|
+
def _save(self, payload: List[dict]) -> None:
|
|
42
|
+
with self.store_path.open("w", encoding="utf-8") as fh:
|
|
43
|
+
json.dump(payload, fh, indent=2, ensure_ascii=True)
|
|
44
|
+
|
|
45
|
+
def request(self, model_name: str, model_version: str, requested_by: str) -> ApprovalRequest:
|
|
46
|
+
payload = self._load()
|
|
47
|
+
req = ApprovalRequest(
|
|
48
|
+
model_name=model_name,
|
|
49
|
+
model_version=model_version,
|
|
50
|
+
requested_by=requested_by,
|
|
51
|
+
requested_at=datetime.utcnow().isoformat(),
|
|
52
|
+
)
|
|
53
|
+
payload.append(asdict(req))
|
|
54
|
+
self._save(payload)
|
|
55
|
+
return req
|
|
56
|
+
|
|
57
|
+
def list_requests(self, model_name: Optional[str] = None) -> List[ApprovalRequest]:
|
|
58
|
+
payload = self._load()
|
|
59
|
+
requests = [ApprovalRequest(**entry) for entry in payload]
|
|
60
|
+
if model_name is None:
|
|
61
|
+
return requests
|
|
62
|
+
return [req for req in requests if req.model_name == model_name]
|
|
63
|
+
|
|
64
|
+
def act(
|
|
65
|
+
self,
|
|
66
|
+
model_name: str,
|
|
67
|
+
model_version: str,
|
|
68
|
+
*,
|
|
69
|
+
actor: str,
|
|
70
|
+
decision: str,
|
|
71
|
+
comment: Optional[str] = None,
|
|
72
|
+
) -> ApprovalRequest:
|
|
73
|
+
payload = self._load()
|
|
74
|
+
found = None
|
|
75
|
+
for entry in payload:
|
|
76
|
+
if entry["model_name"] == model_name and entry["model_version"] == model_version:
|
|
77
|
+
found = entry
|
|
78
|
+
break
|
|
79
|
+
if found is None:
|
|
80
|
+
raise ValueError("Approval request not found.")
|
|
81
|
+
action = ApprovalAction(
|
|
82
|
+
actor=actor,
|
|
83
|
+
decision=decision,
|
|
84
|
+
timestamp=datetime.utcnow().isoformat(),
|
|
85
|
+
comment=comment,
|
|
86
|
+
)
|
|
87
|
+
found["actions"].append(asdict(action))
|
|
88
|
+
if decision.lower() in {"approve", "approved"}:
|
|
89
|
+
found["status"] = "approved"
|
|
90
|
+
elif decision.lower() in {"reject", "rejected"}:
|
|
91
|
+
found["status"] = "rejected"
|
|
92
|
+
self._save(payload)
|
|
93
|
+
return ApprovalRequest(**found)
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import json
|
|
4
|
+
from dataclasses import asdict, dataclass
|
|
5
|
+
from datetime import datetime
|
|
6
|
+
from pathlib import Path
|
|
7
|
+
from typing import Any, Dict, Optional
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
@dataclass
|
|
11
|
+
class AuditEvent:
|
|
12
|
+
action: str
|
|
13
|
+
actor: str
|
|
14
|
+
timestamp: str
|
|
15
|
+
metadata: Dict[str, Any]
|
|
16
|
+
note: Optional[str] = None
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
class AuditLogger:
|
|
20
|
+
"""Append-only JSONL audit log."""
|
|
21
|
+
|
|
22
|
+
def __init__(self, log_path: str | Path):
|
|
23
|
+
self.log_path = Path(log_path)
|
|
24
|
+
self.log_path.parent.mkdir(parents=True, exist_ok=True)
|
|
25
|
+
|
|
26
|
+
def log(self, action: str, actor: str, *, metadata: Optional[Dict[str, Any]] = None,
|
|
27
|
+
note: Optional[str] = None) -> AuditEvent:
|
|
28
|
+
event = AuditEvent(
|
|
29
|
+
action=action,
|
|
30
|
+
actor=actor,
|
|
31
|
+
timestamp=datetime.utcnow().isoformat(),
|
|
32
|
+
metadata=metadata or {},
|
|
33
|
+
note=note,
|
|
34
|
+
)
|
|
35
|
+
with self.log_path.open("a", encoding="utf-8") as fh:
|
|
36
|
+
fh.write(json.dumps(asdict(event), ensure_ascii=True) + "\n")
|
|
37
|
+
return event
|
|
@@ -0,0 +1,99 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import json
|
|
4
|
+
from dataclasses import asdict, dataclass, field
|
|
5
|
+
from datetime import datetime
|
|
6
|
+
from pathlib import Path
|
|
7
|
+
from typing import Dict, List, Optional
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
@dataclass
|
|
11
|
+
class ModelArtifact:
|
|
12
|
+
path: str
|
|
13
|
+
description: Optional[str] = None
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
@dataclass
|
|
17
|
+
class ModelVersion:
|
|
18
|
+
name: str
|
|
19
|
+
version: str
|
|
20
|
+
created_at: str
|
|
21
|
+
metrics: Dict[str, float] = field(default_factory=dict)
|
|
22
|
+
tags: Dict[str, str] = field(default_factory=dict)
|
|
23
|
+
artifacts: List[ModelArtifact] = field(default_factory=list)
|
|
24
|
+
status: str = "candidate"
|
|
25
|
+
notes: Optional[str] = None
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
class ModelRegistry:
|
|
29
|
+
"""Lightweight JSON-based model registry."""
|
|
30
|
+
|
|
31
|
+
def __init__(self, registry_path: str | Path):
|
|
32
|
+
self.registry_path = Path(registry_path)
|
|
33
|
+
self.registry_path.parent.mkdir(parents=True, exist_ok=True)
|
|
34
|
+
|
|
35
|
+
def _load(self) -> Dict[str, List[dict]]:
|
|
36
|
+
if not self.registry_path.exists():
|
|
37
|
+
return {}
|
|
38
|
+
with self.registry_path.open("r", encoding="utf-8") as fh:
|
|
39
|
+
return json.load(fh)
|
|
40
|
+
|
|
41
|
+
def _save(self, payload: Dict[str, List[dict]]) -> None:
|
|
42
|
+
with self.registry_path.open("w", encoding="utf-8") as fh:
|
|
43
|
+
json.dump(payload, fh, indent=2, ensure_ascii=True)
|
|
44
|
+
|
|
45
|
+
def register(
|
|
46
|
+
self,
|
|
47
|
+
name: str,
|
|
48
|
+
version: str,
|
|
49
|
+
*,
|
|
50
|
+
metrics: Optional[Dict[str, float]] = None,
|
|
51
|
+
tags: Optional[Dict[str, str]] = None,
|
|
52
|
+
artifacts: Optional[List[ModelArtifact]] = None,
|
|
53
|
+
status: str = "candidate",
|
|
54
|
+
notes: Optional[str] = None,
|
|
55
|
+
) -> ModelVersion:
|
|
56
|
+
payload = self._load()
|
|
57
|
+
created_at = datetime.utcnow().isoformat()
|
|
58
|
+
entry = ModelVersion(
|
|
59
|
+
name=name,
|
|
60
|
+
version=version,
|
|
61
|
+
created_at=created_at,
|
|
62
|
+
metrics=metrics or {},
|
|
63
|
+
tags=tags or {},
|
|
64
|
+
artifacts=artifacts or [],
|
|
65
|
+
status=status,
|
|
66
|
+
notes=notes,
|
|
67
|
+
)
|
|
68
|
+
payload.setdefault(name, []).append(asdict(entry))
|
|
69
|
+
self._save(payload)
|
|
70
|
+
return entry
|
|
71
|
+
|
|
72
|
+
def list_versions(self, name: str) -> List[ModelVersion]:
|
|
73
|
+
payload = self._load()
|
|
74
|
+
versions = payload.get(name, [])
|
|
75
|
+
return [ModelVersion(**v) for v in versions]
|
|
76
|
+
|
|
77
|
+
def get_version(self, name: str, version: str) -> Optional[ModelVersion]:
|
|
78
|
+
for entry in self.list_versions(name):
|
|
79
|
+
if entry.version == version:
|
|
80
|
+
return entry
|
|
81
|
+
return None
|
|
82
|
+
|
|
83
|
+
def promote(
|
|
84
|
+
self, name: str, version: str, *, new_status: str = "production"
|
|
85
|
+
) -> None:
|
|
86
|
+
payload = self._load()
|
|
87
|
+
if name not in payload:
|
|
88
|
+
raise ValueError("Model not found in registry.")
|
|
89
|
+
updated = False
|
|
90
|
+
for entry in payload[name]:
|
|
91
|
+
if entry["version"] == version:
|
|
92
|
+
entry["status"] = new_status
|
|
93
|
+
updated = True
|
|
94
|
+
elif new_status == "production":
|
|
95
|
+
if entry.get("status") == "production":
|
|
96
|
+
entry["status"] = "archived"
|
|
97
|
+
if not updated:
|
|
98
|
+
raise ValueError("Version not found in registry.")
|
|
99
|
+
self._save(payload)
|
|
@@ -0,0 +1,159 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import json
|
|
4
|
+
from dataclasses import asdict, dataclass, field
|
|
5
|
+
from datetime import datetime
|
|
6
|
+
from pathlib import Path
|
|
7
|
+
from typing import List, Optional
|
|
8
|
+
|
|
9
|
+
from .audit import AuditLogger
|
|
10
|
+
from .registry import ModelRegistry
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
@dataclass
|
|
14
|
+
class ModelRef:
|
|
15
|
+
name: str
|
|
16
|
+
version: str
|
|
17
|
+
activated_at: str
|
|
18
|
+
actor: Optional[str] = None
|
|
19
|
+
note: Optional[str] = None
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
@dataclass
|
|
23
|
+
class DeploymentState:
|
|
24
|
+
env: str
|
|
25
|
+
active: Optional[ModelRef] = None
|
|
26
|
+
history: List[ModelRef] = field(default_factory=list)
|
|
27
|
+
updated_at: Optional[str] = None
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
class ReleaseManager:
|
|
31
|
+
"""Environment release manager with rollback support."""
|
|
32
|
+
|
|
33
|
+
def __init__(
|
|
34
|
+
self,
|
|
35
|
+
state_dir: str | Path,
|
|
36
|
+
*,
|
|
37
|
+
registry: Optional[ModelRegistry] = None,
|
|
38
|
+
audit_logger: Optional[AuditLogger] = None,
|
|
39
|
+
):
|
|
40
|
+
self.state_dir = Path(state_dir)
|
|
41
|
+
self.state_dir.mkdir(parents=True, exist_ok=True)
|
|
42
|
+
self.registry = registry
|
|
43
|
+
self.audit_logger = audit_logger
|
|
44
|
+
|
|
45
|
+
def _state_path(self, env: str) -> Path:
|
|
46
|
+
return self.state_dir / f"{env}.json"
|
|
47
|
+
|
|
48
|
+
def _load(self, env: str) -> DeploymentState:
|
|
49
|
+
path = self._state_path(env)
|
|
50
|
+
if not path.exists():
|
|
51
|
+
return DeploymentState(env=env)
|
|
52
|
+
with path.open("r", encoding="utf-8") as fh:
|
|
53
|
+
payload = json.load(fh)
|
|
54
|
+
active = payload.get("active")
|
|
55
|
+
history = payload.get("history", [])
|
|
56
|
+
return DeploymentState(
|
|
57
|
+
env=payload.get("env", env),
|
|
58
|
+
active=ModelRef(**active) if active else None,
|
|
59
|
+
history=[ModelRef(**item) for item in history],
|
|
60
|
+
updated_at=payload.get("updated_at"),
|
|
61
|
+
)
|
|
62
|
+
|
|
63
|
+
def _save(self, state: DeploymentState) -> None:
|
|
64
|
+
payload = {
|
|
65
|
+
"env": state.env,
|
|
66
|
+
"active": asdict(state.active) if state.active else None,
|
|
67
|
+
"history": [asdict(item) for item in state.history],
|
|
68
|
+
"updated_at": state.updated_at,
|
|
69
|
+
}
|
|
70
|
+
path = self._state_path(state.env)
|
|
71
|
+
with path.open("w", encoding="utf-8") as fh:
|
|
72
|
+
json.dump(payload, fh, indent=2, ensure_ascii=True)
|
|
73
|
+
|
|
74
|
+
def get_active(self, env: str) -> Optional[ModelRef]:
|
|
75
|
+
state = self._load(env)
|
|
76
|
+
return state.active
|
|
77
|
+
|
|
78
|
+
def list_history(self, env: str) -> List[ModelRef]:
|
|
79
|
+
return self._load(env).history
|
|
80
|
+
|
|
81
|
+
def deploy(
|
|
82
|
+
self,
|
|
83
|
+
env: str,
|
|
84
|
+
name: str,
|
|
85
|
+
version: str,
|
|
86
|
+
*,
|
|
87
|
+
actor: Optional[str] = None,
|
|
88
|
+
note: Optional[str] = None,
|
|
89
|
+
update_registry_status: bool = True,
|
|
90
|
+
registry_status: str = "production",
|
|
91
|
+
) -> DeploymentState:
|
|
92
|
+
state = self._load(env)
|
|
93
|
+
if state.active and state.active.name == name and state.active.version == version:
|
|
94
|
+
return state
|
|
95
|
+
|
|
96
|
+
if state.active is not None:
|
|
97
|
+
state.history.append(state.active)
|
|
98
|
+
|
|
99
|
+
now = datetime.utcnow().isoformat()
|
|
100
|
+
state.active = ModelRef(
|
|
101
|
+
name=name,
|
|
102
|
+
version=version,
|
|
103
|
+
activated_at=now,
|
|
104
|
+
actor=actor,
|
|
105
|
+
note=note,
|
|
106
|
+
)
|
|
107
|
+
state.updated_at = now
|
|
108
|
+
self._save(state)
|
|
109
|
+
|
|
110
|
+
if self.registry and update_registry_status:
|
|
111
|
+
self.registry.promote(name, version, new_status=registry_status)
|
|
112
|
+
|
|
113
|
+
if self.audit_logger:
|
|
114
|
+
self.audit_logger.log(
|
|
115
|
+
"deploy",
|
|
116
|
+
actor or "unknown",
|
|
117
|
+
metadata={"env": env, "name": name, "version": version},
|
|
118
|
+
note=note,
|
|
119
|
+
)
|
|
120
|
+
|
|
121
|
+
return state
|
|
122
|
+
|
|
123
|
+
def rollback(
|
|
124
|
+
self,
|
|
125
|
+
env: str,
|
|
126
|
+
*,
|
|
127
|
+
actor: Optional[str] = None,
|
|
128
|
+
note: Optional[str] = None,
|
|
129
|
+
update_registry_status: bool = False,
|
|
130
|
+
registry_status: str = "production",
|
|
131
|
+
) -> DeploymentState:
|
|
132
|
+
state = self._load(env)
|
|
133
|
+
if not state.history:
|
|
134
|
+
raise ValueError("No history available to rollback.")
|
|
135
|
+
|
|
136
|
+
previous = state.history.pop()
|
|
137
|
+
now = datetime.utcnow().isoformat()
|
|
138
|
+
state.active = ModelRef(
|
|
139
|
+
name=previous.name,
|
|
140
|
+
version=previous.version,
|
|
141
|
+
activated_at=now,
|
|
142
|
+
actor=actor or previous.actor,
|
|
143
|
+
note=note or previous.note,
|
|
144
|
+
)
|
|
145
|
+
state.updated_at = now
|
|
146
|
+
self._save(state)
|
|
147
|
+
|
|
148
|
+
if self.registry and update_registry_status:
|
|
149
|
+
self.registry.promote(previous.name, previous.version, new_status=registry_status)
|
|
150
|
+
|
|
151
|
+
if self.audit_logger:
|
|
152
|
+
self.audit_logger.log(
|
|
153
|
+
"rollback",
|
|
154
|
+
actor or "unknown",
|
|
155
|
+
metadata={"env": env, "name": previous.name, "version": previous.version},
|
|
156
|
+
note=note,
|
|
157
|
+
)
|
|
158
|
+
|
|
159
|
+
return state
|