laco-pydantic 1.0.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.
- laco_pydantic-1.0.0/PKG-INFO +29 -0
- laco_pydantic-1.0.0/README.md +19 -0
- laco_pydantic-1.0.0/pyproject.toml +19 -0
- laco_pydantic-1.0.0/setup.cfg +4 -0
- laco_pydantic-1.0.0/sources/laco/integrations/pydantic/__init__.py +25 -0
- laco_pydantic-1.0.0/sources/laco/integrations/pydantic/_core.py +116 -0
- laco_pydantic-1.0.0/sources/laco_pydantic.egg-info/PKG-INFO +29 -0
- laco_pydantic-1.0.0/sources/laco_pydantic.egg-info/SOURCES.txt +10 -0
- laco_pydantic-1.0.0/sources/laco_pydantic.egg-info/dependency_links.txt +1 -0
- laco_pydantic-1.0.0/sources/laco_pydantic.egg-info/requires.txt +2 -0
- laco_pydantic-1.0.0/sources/laco_pydantic.egg-info/top_level.txt +1 -0
- laco_pydantic-1.0.0/tests/test_laco_pydantic.py +99 -0
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: laco-pydantic
|
|
3
|
+
Version: 1.0.0
|
|
4
|
+
Summary: Pydantic v2 bridge for laco structured configs.
|
|
5
|
+
Author-email: Kurt Stolle <kurt@khws.io>
|
|
6
|
+
Requires-Python: >=3.13
|
|
7
|
+
Description-Content-Type: text/markdown
|
|
8
|
+
Requires-Dist: laco>=1.0.0
|
|
9
|
+
Requires-Dist: pydantic>=2.0
|
|
10
|
+
|
|
11
|
+
# Laco-Pydantic
|
|
12
|
+
|
|
13
|
+
Pydantic v2 bridge for laco structured configs.
|
|
14
|
+
|
|
15
|
+
Part of the [laco](https://github.com/khwstolle/laco) project — see the [root README](../../README.md) for an overview.
|
|
16
|
+
|
|
17
|
+
## Installation
|
|
18
|
+
|
|
19
|
+
```bash
|
|
20
|
+
pip install laco-pydantic
|
|
21
|
+
```
|
|
22
|
+
|
|
23
|
+
## Features
|
|
24
|
+
|
|
25
|
+
`validate()`, `to_config()`, `json_schema()`, `model_from_params()`
|
|
26
|
+
|
|
27
|
+
## Usage
|
|
28
|
+
|
|
29
|
+
See [`docs/index.md`](docs/index.md) for the full guide.
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
# Laco-Pydantic
|
|
2
|
+
|
|
3
|
+
Pydantic v2 bridge for laco structured configs.
|
|
4
|
+
|
|
5
|
+
Part of the [laco](https://github.com/khwstolle/laco) project — see the [root README](../../README.md) for an overview.
|
|
6
|
+
|
|
7
|
+
## Installation
|
|
8
|
+
|
|
9
|
+
```bash
|
|
10
|
+
pip install laco-pydantic
|
|
11
|
+
```
|
|
12
|
+
|
|
13
|
+
## Features
|
|
14
|
+
|
|
15
|
+
`validate()`, `to_config()`, `json_schema()`, `model_from_params()`
|
|
16
|
+
|
|
17
|
+
## Usage
|
|
18
|
+
|
|
19
|
+
See [`docs/index.md`](docs/index.md) for the full guide.
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
[build-system]
|
|
2
|
+
requires = ["setuptools>=75", "wheel"]
|
|
3
|
+
build-backend = "setuptools.build_meta"
|
|
4
|
+
|
|
5
|
+
[project]
|
|
6
|
+
name = "laco-pydantic"
|
|
7
|
+
version = "1.0.0"
|
|
8
|
+
description = "Pydantic v2 bridge for laco structured configs."
|
|
9
|
+
readme = "README.md"
|
|
10
|
+
requires-python = ">=3.13"
|
|
11
|
+
authors = [{ name = "Kurt Stolle", email = "kurt@khws.io" }]
|
|
12
|
+
dependencies = ["laco>=1.0.0", "pydantic>=2.0"]
|
|
13
|
+
|
|
14
|
+
[tool.setuptools.packages.find]
|
|
15
|
+
where = ["sources"]
|
|
16
|
+
namespaces = true
|
|
17
|
+
|
|
18
|
+
[tool.uv.sources]
|
|
19
|
+
laco = { workspace = true }
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
"""Pydantic v2 bridge for laco structured configs.
|
|
2
|
+
|
|
3
|
+
Examples
|
|
4
|
+
--------
|
|
5
|
+
::
|
|
6
|
+
|
|
7
|
+
import laco
|
|
8
|
+
import laco.integrations.pydantic as laco_pydantic
|
|
9
|
+
from pydantic import BaseModel
|
|
10
|
+
|
|
11
|
+
class TrainConfig(BaseModel):
|
|
12
|
+
lr: float = 1e-3
|
|
13
|
+
epochs: int = 10
|
|
14
|
+
|
|
15
|
+
cfg = laco.load("configs/train.py")
|
|
16
|
+
validated = laco_pydantic.validate(cfg, TrainConfig)
|
|
17
|
+
print(validated.lr)
|
|
18
|
+
"""
|
|
19
|
+
|
|
20
|
+
from laco.integrations.pydantic._core import json_schema as json_schema
|
|
21
|
+
from laco.integrations.pydantic._core import model_from_params as model_from_params
|
|
22
|
+
from laco.integrations.pydantic._core import to_config as to_config
|
|
23
|
+
from laco.integrations.pydantic._core import validate as validate
|
|
24
|
+
|
|
25
|
+
__all__ = ["json_schema", "model_from_params", "to_config", "validate"]
|
|
@@ -0,0 +1,116 @@
|
|
|
1
|
+
"""Implementation for laco-pydantic."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import typing
|
|
6
|
+
|
|
7
|
+
if typing.TYPE_CHECKING:
|
|
8
|
+
import pydantic
|
|
9
|
+
from omegaconf import DictConfig
|
|
10
|
+
|
|
11
|
+
_ModelT = typing.TypeVar("_ModelT", bound="pydantic.BaseModel")
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
def validate(cfg: DictConfig, model: type[_ModelT]) -> _ModelT:
|
|
15
|
+
"""Validate a laco DictConfig against a Pydantic model.
|
|
16
|
+
|
|
17
|
+
Resolves OmegaConf interpolations before validation so ``${...}``
|
|
18
|
+
references are expanded to their concrete values.
|
|
19
|
+
|
|
20
|
+
Parameters
|
|
21
|
+
----------
|
|
22
|
+
cfg : DictConfig
|
|
23
|
+
Resolved (or partially resolved) laco DictConfig.
|
|
24
|
+
model : type[BaseModel]
|
|
25
|
+
Pydantic v2 ``BaseModel`` subclass to validate against.
|
|
26
|
+
|
|
27
|
+
Returns
|
|
28
|
+
-------
|
|
29
|
+
_ModelT
|
|
30
|
+
A validated instance of *model*.
|
|
31
|
+
|
|
32
|
+
Raises
|
|
33
|
+
------
|
|
34
|
+
pydantic.ValidationError
|
|
35
|
+
If the config does not match the model schema.
|
|
36
|
+
"""
|
|
37
|
+
from omegaconf import OmegaConf
|
|
38
|
+
|
|
39
|
+
raw = OmegaConf.to_container(cfg, resolve=True, throw_on_missing=False)
|
|
40
|
+
return model.model_validate(raw)
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
def to_config(instance: pydantic.BaseModel) -> DictConfig:
|
|
44
|
+
"""Convert a Pydantic model instance to a laco DictConfig.
|
|
45
|
+
|
|
46
|
+
Parameters
|
|
47
|
+
----------
|
|
48
|
+
instance : BaseModel
|
|
49
|
+
A validated Pydantic v2 model instance.
|
|
50
|
+
|
|
51
|
+
Returns
|
|
52
|
+
-------
|
|
53
|
+
DictConfig
|
|
54
|
+
OmegaConf ``DictConfig`` with the same fields and values.
|
|
55
|
+
"""
|
|
56
|
+
from omegaconf import OmegaConf
|
|
57
|
+
|
|
58
|
+
data = instance.model_dump()
|
|
59
|
+
return OmegaConf.create(data)
|
|
60
|
+
|
|
61
|
+
|
|
62
|
+
def json_schema(model: type[pydantic.BaseModel]) -> dict[str, typing.Any]:
|
|
63
|
+
"""Return the JSON Schema for a Pydantic model.
|
|
64
|
+
|
|
65
|
+
Parameters
|
|
66
|
+
----------
|
|
67
|
+
model : type[BaseModel]
|
|
68
|
+
Pydantic v2 ``BaseModel`` subclass.
|
|
69
|
+
|
|
70
|
+
Returns
|
|
71
|
+
-------
|
|
72
|
+
dict
|
|
73
|
+
JSON Schema as produced by ``model.model_json_schema()``.
|
|
74
|
+
"""
|
|
75
|
+
return model.model_json_schema()
|
|
76
|
+
|
|
77
|
+
|
|
78
|
+
def model_from_params(
|
|
79
|
+
wrapper: typing.Any,
|
|
80
|
+
*,
|
|
81
|
+
model_name: str = "LacoModel",
|
|
82
|
+
) -> type[pydantic.BaseModel]:
|
|
83
|
+
"""Dynamically build a Pydantic model from a laco ``ParamsWrapper``.
|
|
84
|
+
|
|
85
|
+
Parameters
|
|
86
|
+
----------
|
|
87
|
+
wrapper : ParamsWrapper
|
|
88
|
+
A ``ParamsWrapper`` instance (the result of ``@L.params``).
|
|
89
|
+
model_name : str
|
|
90
|
+
Class name for the generated Pydantic model. Default: ``"LacoModel"``.
|
|
91
|
+
|
|
92
|
+
Returns
|
|
93
|
+
-------
|
|
94
|
+
type[BaseModel]
|
|
95
|
+
Pydantic ``BaseModel`` subclass with fields matching the params block.
|
|
96
|
+
"""
|
|
97
|
+
import pydantic
|
|
98
|
+
from laco.language import LacoParam
|
|
99
|
+
|
|
100
|
+
try:
|
|
101
|
+
annotations = typing.get_type_hints(wrapper._cls) if hasattr(wrapper, "_cls") else {}
|
|
102
|
+
except Exception: # noqa: BLE001
|
|
103
|
+
annotations = getattr(wrapper._cls, "__annotations__", {}) if hasattr(wrapper, "_cls") else {}
|
|
104
|
+
fields: dict[str, typing.Any] = {}
|
|
105
|
+
for field_name, raw_val in wrapper._dict.items():
|
|
106
|
+
laco_p = raw_val if isinstance(raw_val, LacoParam) else None
|
|
107
|
+
default = laco_p.default if laco_p else raw_val
|
|
108
|
+
ann = annotations.get(field_name, type(default) if default is not None else typing.Any)
|
|
109
|
+
fields[field_name] = (
|
|
110
|
+
ann,
|
|
111
|
+
pydantic.Field(
|
|
112
|
+
default=default, description=laco_p.help if laco_p else None
|
|
113
|
+
),
|
|
114
|
+
)
|
|
115
|
+
|
|
116
|
+
return pydantic.create_model(model_name, **fields)
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: laco-pydantic
|
|
3
|
+
Version: 1.0.0
|
|
4
|
+
Summary: Pydantic v2 bridge for laco structured configs.
|
|
5
|
+
Author-email: Kurt Stolle <kurt@khws.io>
|
|
6
|
+
Requires-Python: >=3.13
|
|
7
|
+
Description-Content-Type: text/markdown
|
|
8
|
+
Requires-Dist: laco>=1.0.0
|
|
9
|
+
Requires-Dist: pydantic>=2.0
|
|
10
|
+
|
|
11
|
+
# Laco-Pydantic
|
|
12
|
+
|
|
13
|
+
Pydantic v2 bridge for laco structured configs.
|
|
14
|
+
|
|
15
|
+
Part of the [laco](https://github.com/khwstolle/laco) project — see the [root README](../../README.md) for an overview.
|
|
16
|
+
|
|
17
|
+
## Installation
|
|
18
|
+
|
|
19
|
+
```bash
|
|
20
|
+
pip install laco-pydantic
|
|
21
|
+
```
|
|
22
|
+
|
|
23
|
+
## Features
|
|
24
|
+
|
|
25
|
+
`validate()`, `to_config()`, `json_schema()`, `model_from_params()`
|
|
26
|
+
|
|
27
|
+
## Usage
|
|
28
|
+
|
|
29
|
+
See [`docs/index.md`](docs/index.md) for the full guide.
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
README.md
|
|
2
|
+
pyproject.toml
|
|
3
|
+
sources/laco/integrations/pydantic/__init__.py
|
|
4
|
+
sources/laco/integrations/pydantic/_core.py
|
|
5
|
+
sources/laco_pydantic.egg-info/PKG-INFO
|
|
6
|
+
sources/laco_pydantic.egg-info/SOURCES.txt
|
|
7
|
+
sources/laco_pydantic.egg-info/dependency_links.txt
|
|
8
|
+
sources/laco_pydantic.egg-info/requires.txt
|
|
9
|
+
sources/laco_pydantic.egg-info/top_level.txt
|
|
10
|
+
tests/test_laco_pydantic.py
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
laco
|
|
@@ -0,0 +1,99 @@
|
|
|
1
|
+
"""Tests for laco-pydantic."""
|
|
2
|
+
|
|
3
|
+
import pytest
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
def test_import():
|
|
7
|
+
import laco.integrations.pydantic # noqa: F401
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
def test_public_api():
|
|
11
|
+
import laco.integrations.pydantic as laco_pydantic
|
|
12
|
+
|
|
13
|
+
assert set(laco_pydantic.__all__) == {
|
|
14
|
+
"json_schema",
|
|
15
|
+
"model_from_params",
|
|
16
|
+
"to_config",
|
|
17
|
+
"validate",
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
def test_validate_basic():
|
|
22
|
+
import laco.integrations.pydantic as laco_pydantic
|
|
23
|
+
from omegaconf import OmegaConf
|
|
24
|
+
from pydantic import BaseModel
|
|
25
|
+
|
|
26
|
+
class Config(BaseModel):
|
|
27
|
+
lr: float = 1e-3
|
|
28
|
+
epochs: int = 10
|
|
29
|
+
|
|
30
|
+
cfg = OmegaConf.create({"lr": 0.01, "epochs": 5})
|
|
31
|
+
result = laco_pydantic.validate(cfg, Config)
|
|
32
|
+
|
|
33
|
+
assert isinstance(result, Config)
|
|
34
|
+
assert result.lr == pytest.approx(0.01)
|
|
35
|
+
assert result.epochs == 5
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
def test_validate_type_coercion():
|
|
39
|
+
import laco.integrations.pydantic as laco_pydantic
|
|
40
|
+
from omegaconf import OmegaConf
|
|
41
|
+
from pydantic import BaseModel
|
|
42
|
+
|
|
43
|
+
class Config(BaseModel):
|
|
44
|
+
value: float
|
|
45
|
+
|
|
46
|
+
cfg = OmegaConf.create({"value": 1})
|
|
47
|
+
result = laco_pydantic.validate(cfg, Config)
|
|
48
|
+
|
|
49
|
+
assert result.value == pytest.approx(1.0)
|
|
50
|
+
assert isinstance(result.value, float)
|
|
51
|
+
|
|
52
|
+
|
|
53
|
+
def test_validate_raises_on_invalid():
|
|
54
|
+
import laco.integrations.pydantic as laco_pydantic
|
|
55
|
+
from omegaconf import OmegaConf
|
|
56
|
+
from pydantic import BaseModel, ValidationError
|
|
57
|
+
|
|
58
|
+
class Config(BaseModel):
|
|
59
|
+
count: int
|
|
60
|
+
|
|
61
|
+
cfg = OmegaConf.create({"count": "not-a-number"})
|
|
62
|
+
with pytest.raises(ValidationError):
|
|
63
|
+
laco_pydantic.validate(cfg, Config)
|
|
64
|
+
|
|
65
|
+
|
|
66
|
+
def test_to_config_roundtrip():
|
|
67
|
+
import laco.integrations.pydantic as laco_pydantic
|
|
68
|
+
from omegaconf import OmegaConf
|
|
69
|
+
from pydantic import BaseModel
|
|
70
|
+
|
|
71
|
+
class Config(BaseModel):
|
|
72
|
+
lr: float = 1e-3
|
|
73
|
+
epochs: int = 10
|
|
74
|
+
|
|
75
|
+
original = OmegaConf.create({"lr": 0.001, "epochs": 10})
|
|
76
|
+
model_instance = laco_pydantic.validate(original, Config)
|
|
77
|
+
recovered = laco_pydantic.to_config(model_instance)
|
|
78
|
+
|
|
79
|
+
assert OmegaConf.to_container(recovered) == {
|
|
80
|
+
"lr": pytest.approx(0.001),
|
|
81
|
+
"epochs": 10,
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
|
|
85
|
+
def test_json_schema_has_properties():
|
|
86
|
+
import laco.integrations.pydantic as laco_pydantic
|
|
87
|
+
from pydantic import BaseModel
|
|
88
|
+
|
|
89
|
+
class Config(BaseModel):
|
|
90
|
+
lr: float = 1e-3
|
|
91
|
+
epochs: int = 10
|
|
92
|
+
|
|
93
|
+
schema = laco_pydantic.json_schema(Config)
|
|
94
|
+
|
|
95
|
+
assert "properties" in schema
|
|
96
|
+
assert "lr" in schema["properties"]
|
|
97
|
+
assert "epochs" in schema["properties"]
|
|
98
|
+
assert schema["properties"]["lr"]["type"] == "number"
|
|
99
|
+
assert schema["properties"]["epochs"]["type"] == "integer"
|