ml4t-specs 0.1.0__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- ml4t_specs-0.1.0/.gitignore +6 -0
- ml4t_specs-0.1.0/LICENSE +21 -0
- ml4t_specs-0.1.0/PKG-INFO +149 -0
- ml4t_specs-0.1.0/README.md +122 -0
- ml4t_specs-0.1.0/pyproject.toml +96 -0
- ml4t_specs-0.1.0/src/ml4t/specs/__init__.py +36 -0
- ml4t_specs-0.1.0/src/ml4t/specs/base.py +131 -0
- ml4t_specs-0.1.0/src/ml4t/specs/io.py +75 -0
- ml4t_specs-0.1.0/src/ml4t/specs/market_data.py +361 -0
- ml4t_specs-0.1.0/src/ml4t/specs/py.typed +0 -0
- ml4t_specs-0.1.0/tests/test_market_data.py +326 -0
- ml4t_specs-0.1.0/tests/test_spec_io.py +149 -0
ml4t_specs-0.1.0/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Stefan Jansen
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
|
@@ -0,0 +1,149 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: ml4t-specs
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: Shared artifact and schema specifications for ML4T libraries
|
|
5
|
+
Project-URL: Homepage, https://github.com/ml4t/specs
|
|
6
|
+
Project-URL: Repository, https://github.com/ml4t/specs
|
|
7
|
+
Project-URL: Issues, https://github.com/ml4t/specs/issues
|
|
8
|
+
Project-URL: Changelog, https://github.com/ml4t/specs/releases
|
|
9
|
+
License: MIT
|
|
10
|
+
License-File: LICENSE
|
|
11
|
+
Classifier: Development Status :: 5 - Production/Stable
|
|
12
|
+
Classifier: Intended Audience :: Developers
|
|
13
|
+
Classifier: Intended Audience :: Financial and Insurance Industry
|
|
14
|
+
Classifier: Intended Audience :: Science/Research
|
|
15
|
+
Classifier: License :: OSI Approved :: MIT License
|
|
16
|
+
Classifier: Operating System :: OS Independent
|
|
17
|
+
Classifier: Programming Language :: Python :: 3
|
|
18
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
19
|
+
Classifier: Programming Language :: Python :: 3.13
|
|
20
|
+
Classifier: Programming Language :: Python :: 3.14
|
|
21
|
+
Classifier: Topic :: Office/Business :: Financial :: Investment
|
|
22
|
+
Classifier: Topic :: Scientific/Engineering :: Information Analysis
|
|
23
|
+
Classifier: Typing :: Typed
|
|
24
|
+
Requires-Python: >=3.12
|
|
25
|
+
Requires-Dist: pyyaml>=6.0
|
|
26
|
+
Description-Content-Type: text/markdown
|
|
27
|
+
|
|
28
|
+
# ml4t-specs
|
|
29
|
+
|
|
30
|
+
[](https://www.python.org/downloads/)
|
|
31
|
+
[](https://pypi.org/project/ml4t-specs/)
|
|
32
|
+
[](https://opensource.org/licenses/MIT)
|
|
33
|
+
|
|
34
|
+
Shared schema and artifact contracts for the ML4T library ecosystem.
|
|
35
|
+
|
|
36
|
+
The stable support matrix is CPython 3.12 through 3.14 on Linux, macOS, and Windows.
|
|
37
|
+
CPython 3.15 prereleases are tested on all three operating systems but are not advertised as
|
|
38
|
+
stable until Python 3.15 is final.
|
|
39
|
+
|
|
40
|
+
## What This Package Does
|
|
41
|
+
|
|
42
|
+
`ml4t-specs` provides the small set of shared types that multiple ML4T libraries use to
|
|
43
|
+
describe:
|
|
44
|
+
|
|
45
|
+
- market data column mappings and feed semantics
|
|
46
|
+
- artifact metadata and storage conventions
|
|
47
|
+
- lightweight YAML/JSON spec payloads
|
|
48
|
+
|
|
49
|
+
It exists so the higher-level libraries can exchange consistent contracts without re-defining
|
|
50
|
+
the same dataclasses in multiple repos.
|
|
51
|
+
|
|
52
|
+
Today it is used by:
|
|
53
|
+
|
|
54
|
+
- `ml4t-backtest` for `FeedSpec` and market-data execution semantics
|
|
55
|
+
- `ml4t-engineer` for artifact metadata
|
|
56
|
+
- `ml4t-diagnostic` for artifact and backtest-result integration
|
|
57
|
+
- `ml4t-models` as an optional integration bridge when `ml4t-specs` is installed
|
|
58
|
+
|
|
59
|
+
## Installation
|
|
60
|
+
|
|
61
|
+
```bash
|
|
62
|
+
pip install ml4t-specs
|
|
63
|
+
```
|
|
64
|
+
|
|
65
|
+
## Main Types
|
|
66
|
+
|
|
67
|
+
### FeedSpec
|
|
68
|
+
|
|
69
|
+
`FeedSpec` defines how downstream libraries should interpret a tradable price table:
|
|
70
|
+
|
|
71
|
+
- timestamp column
|
|
72
|
+
- entity column
|
|
73
|
+
- price / OHLCV columns
|
|
74
|
+
- quote columns
|
|
75
|
+
- calendar and timezone
|
|
76
|
+
- data frequency and timestamp semantics
|
|
77
|
+
|
|
78
|
+
```python
|
|
79
|
+
from ml4t.specs import FeedSpec
|
|
80
|
+
|
|
81
|
+
feed = FeedSpec(
|
|
82
|
+
timestamp_col="date",
|
|
83
|
+
entity_col="ticker",
|
|
84
|
+
close_col="settle",
|
|
85
|
+
price_col="settle",
|
|
86
|
+
calendar="NYSE",
|
|
87
|
+
timezone="America/New_York",
|
|
88
|
+
data_frequency="daily",
|
|
89
|
+
)
|
|
90
|
+
```
|
|
91
|
+
|
|
92
|
+
### MarketDataSpec
|
|
93
|
+
|
|
94
|
+
`MarketDataSpec` bundles schema, semantics, and artifact metadata into one serializable object.
|
|
95
|
+
|
|
96
|
+
```python
|
|
97
|
+
from ml4t.specs import ArtifactStorage, MarketDataSchema, MarketDataSemantics, MarketDataSpec
|
|
98
|
+
|
|
99
|
+
spec = MarketDataSpec(
|
|
100
|
+
artifact_id="us_equities_daily",
|
|
101
|
+
schema=MarketDataSchema(timestamp_col="date", entity_col="ticker", close_col="close"),
|
|
102
|
+
semantics=MarketDataSemantics(calendar="NYSE", data_frequency="daily"),
|
|
103
|
+
storage=ArtifactStorage(path="data/us_equities_daily.parquet"),
|
|
104
|
+
)
|
|
105
|
+
```
|
|
106
|
+
|
|
107
|
+
### Artifact Contracts
|
|
108
|
+
|
|
109
|
+
The base artifact layer gives ML4T libraries a shared way to talk about persisted outputs:
|
|
110
|
+
|
|
111
|
+
- `ArtifactKind`
|
|
112
|
+
- `ArtifactStorage`
|
|
113
|
+
- `ArtifactProvenance`
|
|
114
|
+
- `ArtifactSpec`
|
|
115
|
+
|
|
116
|
+
## Read And Write Spec Payloads
|
|
117
|
+
|
|
118
|
+
```python
|
|
119
|
+
from ml4t.specs import read_spec_payload, write_spec_payload
|
|
120
|
+
|
|
121
|
+
write_spec_payload(spec, "market_data.yaml")
|
|
122
|
+
loaded = read_spec_payload("market_data.yaml")
|
|
123
|
+
```
|
|
124
|
+
|
|
125
|
+
## Why This Exists
|
|
126
|
+
|
|
127
|
+
The public ML4T libraries share a few contract types at their boundaries. Keeping them here:
|
|
128
|
+
|
|
129
|
+
- reduces duplication
|
|
130
|
+
- keeps cross-library serialization consistent
|
|
131
|
+
- gives backtest, modeling, engineering, and diagnostics code one shared contract vocabulary
|
|
132
|
+
|
|
133
|
+
This package is intentionally small. It is a support layer, not a full end-user workflow library.
|
|
134
|
+
|
|
135
|
+
## Development
|
|
136
|
+
|
|
137
|
+
```bash
|
|
138
|
+
git clone https://github.com/ml4t/specs.git
|
|
139
|
+
cd ml4t-specs
|
|
140
|
+
uv sync --dev
|
|
141
|
+
uv run ruff check src/ tests/
|
|
142
|
+
uv run ty check
|
|
143
|
+
uv run pytest tests/ -q
|
|
144
|
+
uv build
|
|
145
|
+
```
|
|
146
|
+
|
|
147
|
+
## License
|
|
148
|
+
|
|
149
|
+
MIT
|
|
@@ -0,0 +1,122 @@
|
|
|
1
|
+
# ml4t-specs
|
|
2
|
+
|
|
3
|
+
[](https://www.python.org/downloads/)
|
|
4
|
+
[](https://pypi.org/project/ml4t-specs/)
|
|
5
|
+
[](https://opensource.org/licenses/MIT)
|
|
6
|
+
|
|
7
|
+
Shared schema and artifact contracts for the ML4T library ecosystem.
|
|
8
|
+
|
|
9
|
+
The stable support matrix is CPython 3.12 through 3.14 on Linux, macOS, and Windows.
|
|
10
|
+
CPython 3.15 prereleases are tested on all three operating systems but are not advertised as
|
|
11
|
+
stable until Python 3.15 is final.
|
|
12
|
+
|
|
13
|
+
## What This Package Does
|
|
14
|
+
|
|
15
|
+
`ml4t-specs` provides the small set of shared types that multiple ML4T libraries use to
|
|
16
|
+
describe:
|
|
17
|
+
|
|
18
|
+
- market data column mappings and feed semantics
|
|
19
|
+
- artifact metadata and storage conventions
|
|
20
|
+
- lightweight YAML/JSON spec payloads
|
|
21
|
+
|
|
22
|
+
It exists so the higher-level libraries can exchange consistent contracts without re-defining
|
|
23
|
+
the same dataclasses in multiple repos.
|
|
24
|
+
|
|
25
|
+
Today it is used by:
|
|
26
|
+
|
|
27
|
+
- `ml4t-backtest` for `FeedSpec` and market-data execution semantics
|
|
28
|
+
- `ml4t-engineer` for artifact metadata
|
|
29
|
+
- `ml4t-diagnostic` for artifact and backtest-result integration
|
|
30
|
+
- `ml4t-models` as an optional integration bridge when `ml4t-specs` is installed
|
|
31
|
+
|
|
32
|
+
## Installation
|
|
33
|
+
|
|
34
|
+
```bash
|
|
35
|
+
pip install ml4t-specs
|
|
36
|
+
```
|
|
37
|
+
|
|
38
|
+
## Main Types
|
|
39
|
+
|
|
40
|
+
### FeedSpec
|
|
41
|
+
|
|
42
|
+
`FeedSpec` defines how downstream libraries should interpret a tradable price table:
|
|
43
|
+
|
|
44
|
+
- timestamp column
|
|
45
|
+
- entity column
|
|
46
|
+
- price / OHLCV columns
|
|
47
|
+
- quote columns
|
|
48
|
+
- calendar and timezone
|
|
49
|
+
- data frequency and timestamp semantics
|
|
50
|
+
|
|
51
|
+
```python
|
|
52
|
+
from ml4t.specs import FeedSpec
|
|
53
|
+
|
|
54
|
+
feed = FeedSpec(
|
|
55
|
+
timestamp_col="date",
|
|
56
|
+
entity_col="ticker",
|
|
57
|
+
close_col="settle",
|
|
58
|
+
price_col="settle",
|
|
59
|
+
calendar="NYSE",
|
|
60
|
+
timezone="America/New_York",
|
|
61
|
+
data_frequency="daily",
|
|
62
|
+
)
|
|
63
|
+
```
|
|
64
|
+
|
|
65
|
+
### MarketDataSpec
|
|
66
|
+
|
|
67
|
+
`MarketDataSpec` bundles schema, semantics, and artifact metadata into one serializable object.
|
|
68
|
+
|
|
69
|
+
```python
|
|
70
|
+
from ml4t.specs import ArtifactStorage, MarketDataSchema, MarketDataSemantics, MarketDataSpec
|
|
71
|
+
|
|
72
|
+
spec = MarketDataSpec(
|
|
73
|
+
artifact_id="us_equities_daily",
|
|
74
|
+
schema=MarketDataSchema(timestamp_col="date", entity_col="ticker", close_col="close"),
|
|
75
|
+
semantics=MarketDataSemantics(calendar="NYSE", data_frequency="daily"),
|
|
76
|
+
storage=ArtifactStorage(path="data/us_equities_daily.parquet"),
|
|
77
|
+
)
|
|
78
|
+
```
|
|
79
|
+
|
|
80
|
+
### Artifact Contracts
|
|
81
|
+
|
|
82
|
+
The base artifact layer gives ML4T libraries a shared way to talk about persisted outputs:
|
|
83
|
+
|
|
84
|
+
- `ArtifactKind`
|
|
85
|
+
- `ArtifactStorage`
|
|
86
|
+
- `ArtifactProvenance`
|
|
87
|
+
- `ArtifactSpec`
|
|
88
|
+
|
|
89
|
+
## Read And Write Spec Payloads
|
|
90
|
+
|
|
91
|
+
```python
|
|
92
|
+
from ml4t.specs import read_spec_payload, write_spec_payload
|
|
93
|
+
|
|
94
|
+
write_spec_payload(spec, "market_data.yaml")
|
|
95
|
+
loaded = read_spec_payload("market_data.yaml")
|
|
96
|
+
```
|
|
97
|
+
|
|
98
|
+
## Why This Exists
|
|
99
|
+
|
|
100
|
+
The public ML4T libraries share a few contract types at their boundaries. Keeping them here:
|
|
101
|
+
|
|
102
|
+
- reduces duplication
|
|
103
|
+
- keeps cross-library serialization consistent
|
|
104
|
+
- gives backtest, modeling, engineering, and diagnostics code one shared contract vocabulary
|
|
105
|
+
|
|
106
|
+
This package is intentionally small. It is a support layer, not a full end-user workflow library.
|
|
107
|
+
|
|
108
|
+
## Development
|
|
109
|
+
|
|
110
|
+
```bash
|
|
111
|
+
git clone https://github.com/ml4t/specs.git
|
|
112
|
+
cd ml4t-specs
|
|
113
|
+
uv sync --dev
|
|
114
|
+
uv run ruff check src/ tests/
|
|
115
|
+
uv run ty check
|
|
116
|
+
uv run pytest tests/ -q
|
|
117
|
+
uv build
|
|
118
|
+
```
|
|
119
|
+
|
|
120
|
+
## License
|
|
121
|
+
|
|
122
|
+
MIT
|
|
@@ -0,0 +1,96 @@
|
|
|
1
|
+
[build-system]
|
|
2
|
+
requires = ["hatchling", "hatch-vcs"]
|
|
3
|
+
build-backend = "hatchling.build"
|
|
4
|
+
|
|
5
|
+
[tool.hatch.version]
|
|
6
|
+
path = "src/ml4t/specs/__init__.py"
|
|
7
|
+
|
|
8
|
+
[tool.hatch.build.targets.wheel]
|
|
9
|
+
packages = ["src/ml4t"]
|
|
10
|
+
namespaces = true
|
|
11
|
+
|
|
12
|
+
[tool.hatch.build.targets.sdist]
|
|
13
|
+
include = [
|
|
14
|
+
"/src",
|
|
15
|
+
"/tests",
|
|
16
|
+
"/README.md",
|
|
17
|
+
"/LICENSE",
|
|
18
|
+
]
|
|
19
|
+
|
|
20
|
+
[project]
|
|
21
|
+
name = "ml4t-specs"
|
|
22
|
+
dynamic = ["version"]
|
|
23
|
+
description = "Shared artifact and schema specifications for ML4T libraries"
|
|
24
|
+
readme = "README.md"
|
|
25
|
+
license = { text = "MIT" }
|
|
26
|
+
requires-python = ">=3.12"
|
|
27
|
+
dependencies = [
|
|
28
|
+
"pyyaml>=6.0",
|
|
29
|
+
]
|
|
30
|
+
classifiers = [
|
|
31
|
+
"Development Status :: 5 - Production/Stable",
|
|
32
|
+
"Intended Audience :: Developers",
|
|
33
|
+
"Intended Audience :: Financial and Insurance Industry",
|
|
34
|
+
"Intended Audience :: Science/Research",
|
|
35
|
+
"License :: OSI Approved :: MIT License",
|
|
36
|
+
"Operating System :: OS Independent",
|
|
37
|
+
"Programming Language :: Python :: 3",
|
|
38
|
+
"Programming Language :: Python :: 3.12",
|
|
39
|
+
"Programming Language :: Python :: 3.13",
|
|
40
|
+
"Programming Language :: Python :: 3.14",
|
|
41
|
+
"Topic :: Office/Business :: Financial :: Investment",
|
|
42
|
+
"Topic :: Scientific/Engineering :: Information Analysis",
|
|
43
|
+
"Typing :: Typed",
|
|
44
|
+
]
|
|
45
|
+
|
|
46
|
+
[project.urls]
|
|
47
|
+
Homepage = "https://github.com/ml4t/specs"
|
|
48
|
+
Repository = "https://github.com/ml4t/specs"
|
|
49
|
+
Issues = "https://github.com/ml4t/specs/issues"
|
|
50
|
+
Changelog = "https://github.com/ml4t/specs/releases"
|
|
51
|
+
|
|
52
|
+
[dependency-groups]
|
|
53
|
+
dev = [
|
|
54
|
+
"pip-audit>=2.10.0",
|
|
55
|
+
"pytest>=8.0.0",
|
|
56
|
+
"pytest-cov>=6.0",
|
|
57
|
+
"ruff>=0.8.0",
|
|
58
|
+
"twine>=6.2.0",
|
|
59
|
+
"ty",
|
|
60
|
+
]
|
|
61
|
+
|
|
62
|
+
[tool.pytest.ini_options]
|
|
63
|
+
testpaths = ["tests"]
|
|
64
|
+
python_files = ["test_*.py"]
|
|
65
|
+
python_classes = ["Test*"]
|
|
66
|
+
python_functions = ["test_*"]
|
|
67
|
+
addopts = [
|
|
68
|
+
"--cov=ml4t.specs",
|
|
69
|
+
"--cov-branch",
|
|
70
|
+
"--cov-fail-under=100",
|
|
71
|
+
]
|
|
72
|
+
|
|
73
|
+
[tool.ruff]
|
|
74
|
+
line-length = 100
|
|
75
|
+
target-version = "py312"
|
|
76
|
+
fix = true
|
|
77
|
+
|
|
78
|
+
[tool.ruff.lint]
|
|
79
|
+
select = [
|
|
80
|
+
"E",
|
|
81
|
+
"W",
|
|
82
|
+
"F",
|
|
83
|
+
"I",
|
|
84
|
+
"B",
|
|
85
|
+
"C4",
|
|
86
|
+
"UP",
|
|
87
|
+
]
|
|
88
|
+
ignore = [
|
|
89
|
+
"E501",
|
|
90
|
+
"B008",
|
|
91
|
+
"B905",
|
|
92
|
+
]
|
|
93
|
+
|
|
94
|
+
[tool.ty.environment]
|
|
95
|
+
python-version = "3.12"
|
|
96
|
+
root = ["src"]
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
"""Shared artifact and schema specifications for ML4T libraries."""
|
|
2
|
+
|
|
3
|
+
from .base import (
|
|
4
|
+
ArtifactKind,
|
|
5
|
+
ArtifactProvenance,
|
|
6
|
+
ArtifactSpec,
|
|
7
|
+
ArtifactStorage,
|
|
8
|
+
optional_str,
|
|
9
|
+
serialize_artifact_value,
|
|
10
|
+
)
|
|
11
|
+
from .io import read_spec_payload, write_spec_payload
|
|
12
|
+
from .market_data import (
|
|
13
|
+
FeedSpec,
|
|
14
|
+
MarketDataSchema,
|
|
15
|
+
MarketDataSemantics,
|
|
16
|
+
MarketDataSpec,
|
|
17
|
+
TimestampSemantics,
|
|
18
|
+
)
|
|
19
|
+
|
|
20
|
+
__version__ = "0.1.0"
|
|
21
|
+
|
|
22
|
+
__all__ = [
|
|
23
|
+
"ArtifactKind",
|
|
24
|
+
"ArtifactProvenance",
|
|
25
|
+
"ArtifactSpec",
|
|
26
|
+
"ArtifactStorage",
|
|
27
|
+
"FeedSpec",
|
|
28
|
+
"MarketDataSchema",
|
|
29
|
+
"MarketDataSemantics",
|
|
30
|
+
"MarketDataSpec",
|
|
31
|
+
"TimestampSemantics",
|
|
32
|
+
"optional_str",
|
|
33
|
+
"read_spec_payload",
|
|
34
|
+
"serialize_artifact_value",
|
|
35
|
+
"write_spec_payload",
|
|
36
|
+
]
|
|
@@ -0,0 +1,131 @@
|
|
|
1
|
+
"""Shared base specifications for persisted ML4T artifacts."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from collections.abc import Mapping, Sequence
|
|
6
|
+
from dataclasses import asdict, dataclass, field
|
|
7
|
+
from enum import Enum, StrEnum
|
|
8
|
+
from pathlib import Path
|
|
9
|
+
from typing import Any
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
class ArtifactKind(StrEnum):
|
|
13
|
+
"""Kinds of persisted artifacts shared across ML4T workflows."""
|
|
14
|
+
|
|
15
|
+
MARKET_DATA = "market_data"
|
|
16
|
+
LABELS = "labels"
|
|
17
|
+
FEATURES = "features"
|
|
18
|
+
PREDICTIONS = "predictions"
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
@dataclass(frozen=True, slots=True)
|
|
22
|
+
class ArtifactStorage:
|
|
23
|
+
"""Storage location and serialization hints for an artifact."""
|
|
24
|
+
|
|
25
|
+
path: str | Path = ""
|
|
26
|
+
format: str = "parquet"
|
|
27
|
+
partition_by: tuple[str, ...] = ()
|
|
28
|
+
|
|
29
|
+
@classmethod
|
|
30
|
+
def from_mapping(cls, mapping: Mapping[str, Any] | None) -> ArtifactStorage:
|
|
31
|
+
if mapping is None:
|
|
32
|
+
return cls()
|
|
33
|
+
if not isinstance(mapping, Mapping):
|
|
34
|
+
raise TypeError("storage must be a mapping or None")
|
|
35
|
+
partition_by = mapping.get("partition_by", ())
|
|
36
|
+
if isinstance(partition_by, str):
|
|
37
|
+
partition_by = (partition_by,)
|
|
38
|
+
elif not isinstance(partition_by, Sequence):
|
|
39
|
+
raise TypeError("storage partition_by must be a string or sequence")
|
|
40
|
+
return cls(
|
|
41
|
+
path=mapping.get("path", ""),
|
|
42
|
+
format=str(mapping.get("format", "parquet")),
|
|
43
|
+
partition_by=tuple(str(item) for item in partition_by),
|
|
44
|
+
)
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
@dataclass(frozen=True, slots=True)
|
|
48
|
+
class ArtifactProvenance:
|
|
49
|
+
"""Upstream lineage and content fingerprinting for an artifact."""
|
|
50
|
+
|
|
51
|
+
source_artifacts: tuple[str, ...] = ()
|
|
52
|
+
content_hash: str | None = None
|
|
53
|
+
created_by: str | None = None
|
|
54
|
+
|
|
55
|
+
@classmethod
|
|
56
|
+
def from_mapping(cls, mapping: Mapping[str, Any] | None) -> ArtifactProvenance:
|
|
57
|
+
if mapping is None:
|
|
58
|
+
return cls()
|
|
59
|
+
if not isinstance(mapping, Mapping):
|
|
60
|
+
raise TypeError("provenance must be a mapping or None")
|
|
61
|
+
source_artifacts = mapping.get("source_artifacts", ())
|
|
62
|
+
if isinstance(source_artifacts, str):
|
|
63
|
+
source_artifacts = (source_artifacts,)
|
|
64
|
+
elif not isinstance(source_artifacts, Sequence):
|
|
65
|
+
raise TypeError("provenance source_artifacts must be a string or sequence")
|
|
66
|
+
return cls(
|
|
67
|
+
source_artifacts=tuple(str(item) for item in source_artifacts),
|
|
68
|
+
content_hash=_optional_str(mapping.get("content_hash")),
|
|
69
|
+
created_by=_optional_str(mapping.get("created_by")),
|
|
70
|
+
)
|
|
71
|
+
|
|
72
|
+
|
|
73
|
+
@dataclass(frozen=True, slots=True)
|
|
74
|
+
class ArtifactSpec:
|
|
75
|
+
"""Base metadata shared by all persisted artifact specifications."""
|
|
76
|
+
|
|
77
|
+
artifact_id: str
|
|
78
|
+
kind: ArtifactKind
|
|
79
|
+
version: int = 1
|
|
80
|
+
storage: ArtifactStorage = field(default_factory=ArtifactStorage)
|
|
81
|
+
provenance: ArtifactProvenance = field(default_factory=ArtifactProvenance)
|
|
82
|
+
|
|
83
|
+
def __post_init__(self) -> None:
|
|
84
|
+
if not isinstance(self.artifact_id, str) or not self.artifact_id.strip():
|
|
85
|
+
raise ValueError("artifact_id must be a non-empty string")
|
|
86
|
+
if isinstance(self.version, bool) or not isinstance(self.version, int) or self.version < 1:
|
|
87
|
+
raise ValueError("version must be a positive integer")
|
|
88
|
+
|
|
89
|
+
def to_dict(self) -> dict[str, Any]:
|
|
90
|
+
return _serialize(asdict(self))
|
|
91
|
+
|
|
92
|
+
|
|
93
|
+
def optional_str(value: Any) -> str | None:
|
|
94
|
+
"""Coerce a value to a non-empty string."""
|
|
95
|
+
return _optional_str(value)
|
|
96
|
+
|
|
97
|
+
|
|
98
|
+
def serialize_artifact_value(value: Any) -> Any:
|
|
99
|
+
"""Serialize enums and pathlib objects for YAML/JSON output."""
|
|
100
|
+
return _serialize(value)
|
|
101
|
+
|
|
102
|
+
|
|
103
|
+
def _optional_str(value: Any) -> str | None:
|
|
104
|
+
if value is None:
|
|
105
|
+
return None
|
|
106
|
+
text = str(value)
|
|
107
|
+
return text if text else None
|
|
108
|
+
|
|
109
|
+
|
|
110
|
+
def _serialize(value: Any) -> Any:
|
|
111
|
+
if isinstance(value, Enum):
|
|
112
|
+
return value.value
|
|
113
|
+
if isinstance(value, Path):
|
|
114
|
+
return str(value)
|
|
115
|
+
if isinstance(value, dict):
|
|
116
|
+
return {key: _serialize(item) for key, item in value.items()}
|
|
117
|
+
if isinstance(value, tuple):
|
|
118
|
+
return [_serialize(item) for item in value]
|
|
119
|
+
if isinstance(value, list):
|
|
120
|
+
return [_serialize(item) for item in value]
|
|
121
|
+
return value
|
|
122
|
+
|
|
123
|
+
|
|
124
|
+
__all__ = [
|
|
125
|
+
"ArtifactKind",
|
|
126
|
+
"ArtifactProvenance",
|
|
127
|
+
"ArtifactSpec",
|
|
128
|
+
"ArtifactStorage",
|
|
129
|
+
"optional_str",
|
|
130
|
+
"serialize_artifact_value",
|
|
131
|
+
]
|
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
"""Low-level payload I/O for ML4T artifact specifications."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import json
|
|
6
|
+
from collections.abc import Mapping
|
|
7
|
+
from pathlib import Path
|
|
8
|
+
from tempfile import NamedTemporaryFile
|
|
9
|
+
from typing import Any
|
|
10
|
+
|
|
11
|
+
import yaml
|
|
12
|
+
|
|
13
|
+
from .base import ArtifactSpec
|
|
14
|
+
|
|
15
|
+
_SUPPORTED_SUFFIXES = frozenset({".json", ".yaml", ".yml"})
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
def _validated_suffix(path: Path) -> str:
|
|
19
|
+
suffix = path.suffix.lower()
|
|
20
|
+
if suffix not in _SUPPORTED_SUFFIXES:
|
|
21
|
+
choices = ", ".join(sorted(_SUPPORTED_SUFFIXES))
|
|
22
|
+
raise ValueError(f"Spec path extension must be one of: {choices}")
|
|
23
|
+
return suffix
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
def _normalize_mapping(data: Any) -> dict[str, Any]:
|
|
27
|
+
if not isinstance(data, Mapping):
|
|
28
|
+
raise ValueError("Spec payload must be a mapping")
|
|
29
|
+
return {str(key): value for key, value in data.items()}
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
def read_spec_payload(path_or_mapping: str | Path | Mapping[Any, Any]) -> dict[str, Any]:
|
|
33
|
+
"""Load a spec payload from YAML/JSON or return a copied mapping."""
|
|
34
|
+
if isinstance(path_or_mapping, Mapping):
|
|
35
|
+
return _normalize_mapping(path_or_mapping)
|
|
36
|
+
|
|
37
|
+
path = Path(path_or_mapping)
|
|
38
|
+
suffix = _validated_suffix(path)
|
|
39
|
+
with path.open(encoding="utf-8") as f:
|
|
40
|
+
data = json.load(f) if suffix == ".json" else yaml.safe_load(f)
|
|
41
|
+
return _normalize_mapping({} if data is None else data)
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
def write_spec_payload(payload: Mapping[Any, Any] | ArtifactSpec, path: str | Path) -> Path:
|
|
45
|
+
"""Write a spec payload to YAML or JSON."""
|
|
46
|
+
dest = Path(path)
|
|
47
|
+
suffix = _validated_suffix(dest)
|
|
48
|
+
normalized = (
|
|
49
|
+
payload.to_dict() if isinstance(payload, ArtifactSpec) else _normalize_mapping(payload)
|
|
50
|
+
)
|
|
51
|
+
dest.parent.mkdir(parents=True, exist_ok=True)
|
|
52
|
+
temporary_path: Path | None = None
|
|
53
|
+
try:
|
|
54
|
+
with NamedTemporaryFile(
|
|
55
|
+
"w",
|
|
56
|
+
encoding="utf-8",
|
|
57
|
+
dir=dest.parent,
|
|
58
|
+
prefix=f".{dest.name}.",
|
|
59
|
+
delete=False,
|
|
60
|
+
) as temporary:
|
|
61
|
+
temporary_path = Path(temporary.name)
|
|
62
|
+
if suffix == ".json":
|
|
63
|
+
json.dump(normalized, temporary, indent=2)
|
|
64
|
+
temporary.write("\n")
|
|
65
|
+
else:
|
|
66
|
+
yaml.safe_dump(normalized, temporary, sort_keys=False)
|
|
67
|
+
temporary_path.replace(dest)
|
|
68
|
+
except Exception:
|
|
69
|
+
if temporary_path is not None:
|
|
70
|
+
temporary_path.unlink(missing_ok=True)
|
|
71
|
+
raise
|
|
72
|
+
return dest
|
|
73
|
+
|
|
74
|
+
|
|
75
|
+
__all__ = ["read_spec_payload", "write_spec_payload"]
|