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.
@@ -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,4 @@
1
+ [egg_info]
2
+ tag_build =
3
+ tag_date = 0
4
+
@@ -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,2 @@
1
+ laco>=1.0.0
2
+ pydantic>=2.0
@@ -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"