Skip to content

Commit 744c807

Browse files
committed
extend compatibility to pydantic v1 and v2
1 parent 161930d commit 744c807

File tree

9 files changed

+311
-144
lines changed

9 files changed

+311
-144
lines changed

CHANGELOG.md

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,12 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
1010
Anything MAY change at any time. The public API SHOULD NOT be considered stable.").
1111
While in this phase, we will denote breaking changes with a minor increase.
1212

13+
## 0.3.2
14+
15+
### Changed
16+
17+
* Extend compatibility to pydantic v1. Now `dac` works with both v1 and v2 of pydantic
18+
1319
## 0.3.1
1420

1521
### Changed

pyproject.toml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -23,7 +23,7 @@ classifiers = [
2323

2424
dependencies = [
2525
"build~=0.9",
26-
"pydantic~=2.1",
26+
"pydantic>=1.10,<3.0",
2727
"toml~=0.10",
2828
"typer[all]~=0.7",
2929
"wheel~=0.38"
Lines changed: 110 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,110 @@
1+
import importlib
2+
import inspect
3+
import sys
4+
from pathlib import Path
5+
from typing import Dict, Optional
6+
7+
import pandera as pa
8+
from pydantic import BaseModel, root_validator, validator
9+
10+
from dac._file_helper import temporarily_copied_file
11+
from dac._input.pyproject import PyProjectConfig
12+
13+
14+
class PackConfig(BaseModel):
15+
data_path: Optional[Path] = None
16+
load_path: Path
17+
schema_path: Path
18+
wheel_dir: Path
19+
pyproject: PyProjectConfig
20+
21+
@validator("data_path", "load_path", "schema_path", "wheel_dir")
22+
def path_exists(cls, path: Path) -> Path: # pylint: disable=no-self-argument,no-self-use
23+
if path is not None and not path.exists():
24+
raise ValueError((f"Path {path.as_posix()} is not valid"))
25+
return path
26+
27+
@validator("load_path")
28+
def load_contains_expected_function(cls, path: Path) -> Path: # pylint: disable=no-self-argument,no-self-use
29+
try:
30+
sys.path.append(path.parent.as_posix())
31+
pkg = importlib.import_module(name=path.stem)
32+
except Exception as e:
33+
raise ValueError(
34+
(
35+
f"{path.as_posix()} is not a path to a python module that can be imported, "
36+
"because of the following error:"
37+
"\n"
38+
f"{e}"
39+
)
40+
) from e
41+
42+
try:
43+
signature = inspect.getfullargspec(pkg.load)
44+
assert signature.args == []
45+
except Exception as e:
46+
raise ValueError((f"{path.as_posix()} does not contain the required `def load()`")) from e
47+
return path
48+
49+
@validator("schema_path")
50+
def schema_contains_expected_class(cls, path: Path) -> Path: # pylint: disable=no-self-argument,no-self-use
51+
try:
52+
sys.path.append(path.parent.as_posix())
53+
pkg = importlib.import_module(name=path.stem)
54+
except Exception as e:
55+
raise ValueError(
56+
(
57+
f"{path.as_posix()} is not a path to a python module that can be imported, "
58+
"because of the following error:"
59+
"\n"
60+
f"{e}"
61+
)
62+
) from e
63+
64+
try:
65+
issubclass(pkg.Schema, pa.SchemaModel)
66+
except Exception as e:
67+
raise ValueError((f"{path.as_posix()} does not contain the required `class Schema(pa.SchemaModel)`")) from e
68+
return path
69+
70+
@root_validator
71+
def schema_match_data( # pylint: disable=no-self-argument,no-self-use
72+
cls, values: Dict[str, Path]
73+
) -> Dict[str, Path]:
74+
try:
75+
sys.path.append(values["load_path"].parent.as_posix())
76+
load_module = importlib.import_module(name=values["load_path"].stem)
77+
except Exception as e:
78+
raise ValueError(
79+
"Validation of the schema against the data has failed because the load module could not be imported"
80+
) from e
81+
82+
try:
83+
if values.get("data_path", None) is not None:
84+
with temporarily_copied_file(
85+
src=values["data_path"], dst=values["load_path"].parent / values["data_path"].name
86+
):
87+
data = load_module.load()
88+
else:
89+
data = load_module.load()
90+
except Exception as e:
91+
raise ValueError("`load()` failed due to the following error:" "\n" f"{e}") from e
92+
93+
try:
94+
sys.path.append(values["schema_path"].parent.as_posix())
95+
schema_module = importlib.import_module(name=values["schema_path"].stem)
96+
except Exception as e:
97+
raise ValueError(
98+
"Validation of the schema against the data has failed because the schema module could not be imported"
99+
) from e
100+
101+
try:
102+
schema_module.Schema.validate(data, lazy=True)
103+
except pa.errors.SchemaErrors as e:
104+
raise ValueError("Validation of the schema against the data has failed:" "\n" f"{e.failure_cases}") from e
105+
except Exception as e:
106+
raise ValueError(
107+
"Validation of the schema against the data has failed for unexpected reasons:" "\n" f"{e}"
108+
) from e
109+
110+
return values
Lines changed: 109 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,109 @@
1+
import importlib
2+
import inspect
3+
import sys
4+
from pathlib import Path
5+
from typing import Optional
6+
7+
import pandera as pa
8+
from pydantic import BaseModel, field_validator, model_validator
9+
10+
from dac._file_helper import temporarily_copied_file
11+
from dac._input.pyproject import PyProjectConfig
12+
13+
14+
class PackConfig(BaseModel):
15+
data_path: Optional[Path] = None
16+
load_path: Path
17+
schema_path: Path
18+
wheel_dir: Path
19+
pyproject: PyProjectConfig
20+
21+
@field_validator("data_path", "load_path", "schema_path", "wheel_dir")
22+
@classmethod
23+
def path_exists(cls, path: Path) -> Path: # pylint: disable=no-self-argument,no-self-use
24+
if path is not None and not path.exists():
25+
raise ValueError((f"Path {path.as_posix()} is not valid"))
26+
return path
27+
28+
@field_validator("load_path")
29+
@classmethod
30+
def load_contains_expected_function(cls, path: Path) -> Path: # pylint: disable=no-self-argument,no-self-use
31+
try:
32+
sys.path.append(path.parent.as_posix())
33+
pkg = importlib.import_module(name=path.stem)
34+
except Exception as e:
35+
raise ValueError(
36+
(
37+
f"{path.as_posix()} is not a path to a python module that can be imported, "
38+
"because of the following error:"
39+
"\n"
40+
f"{e}"
41+
)
42+
) from e
43+
44+
try:
45+
signature = inspect.getfullargspec(pkg.load)
46+
assert signature.args == []
47+
except Exception as e:
48+
raise ValueError((f"{path.as_posix()} does not contain the required `def load()`")) from e
49+
return path
50+
51+
@field_validator("schema_path")
52+
@classmethod
53+
def schema_contains_expected_class(cls, path: Path) -> Path: # pylint: disable=no-self-argument,no-self-use
54+
try:
55+
sys.path.append(path.parent.as_posix())
56+
pkg = importlib.import_module(name=path.stem)
57+
except Exception as e:
58+
raise ValueError(
59+
(
60+
f"{path.as_posix()} is not a path to a python module that can be imported, "
61+
"because of the following error:"
62+
"\n"
63+
f"{e}"
64+
)
65+
) from e
66+
67+
try:
68+
issubclass(pkg.Schema, pa.SchemaModel)
69+
except Exception as e:
70+
raise ValueError((f"{path.as_posix()} does not contain the required `class Schema(pa.SchemaModel)`")) from e
71+
return path
72+
73+
@model_validator(mode="after")
74+
def schema_match_data(self) -> "PackConfig":
75+
try:
76+
sys.path.append(self.load_path.parent.as_posix())
77+
load_module = importlib.import_module(name=self.load_path.stem)
78+
except Exception as e:
79+
raise ValueError(
80+
"Validation of the schema against the data has failed because the load module could not be imported"
81+
) from e
82+
83+
try:
84+
if self.data_path is not None:
85+
with temporarily_copied_file(src=self.data_path, dst=self.load_path.parent / self.data_path.name):
86+
data = load_module.load()
87+
else:
88+
data = load_module.load()
89+
except Exception as e:
90+
raise ValueError("`load()` failed due to the following error:" "\n" f"{e}") from e
91+
92+
try:
93+
sys.path.append(self.schema_path.parent.as_posix())
94+
schema_module = importlib.import_module(name=self.schema_path.stem)
95+
except Exception as e:
96+
raise ValueError(
97+
"Validation of the schema against the data has failed because the schema module could not be imported"
98+
) from e
99+
100+
try:
101+
schema_module.Schema.validate(data, lazy=True)
102+
except pa.errors.SchemaErrors as e:
103+
raise ValueError("Validation of the schema against the data has failed:" "\n" f"{e.failure_cases}") from e
104+
except Exception as e:
105+
raise ValueError(
106+
"Validation of the schema against the data has failed for unexpected reasons:" "\n" f"{e}"
107+
) from e
108+
109+
return self
Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,32 @@
1+
from typing import List
2+
3+
import toml # type: ignore
4+
from pydantic import BaseModel, validator
5+
6+
7+
class PyProjectConfig(BaseModel):
8+
project_name: str
9+
project_version: str
10+
project_dependencies: str
11+
12+
@validator("project_name")
13+
def valid_project_name(cls, name: str) -> str: # pylint: disable=no-self-argument,no-self-use
14+
if not name.isidentifier() or "\xb7" in name:
15+
raise ValueError(f"Invalid project name: {name} (hint: only '_' are allowed, no '-')")
16+
return name
17+
18+
def generate_pyproject_toml(self) -> str:
19+
return toml.dumps(
20+
{
21+
"project": {
22+
"name": self.project_name,
23+
"version": self.project_version,
24+
"dependencies": self._get_list_of_project_dependencies(),
25+
}
26+
}
27+
)
28+
29+
def _get_list_of_project_dependencies(self) -> List[str]:
30+
splitted_by_newline = self.project_dependencies.splitlines()
31+
splitted_by_newline_or_comma = [s for ss in splitted_by_newline for s in ss.split(";")]
32+
return sorted(map(lambda x: x.strip(), splitted_by_newline_or_comma))
Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,33 @@
1+
from typing import List
2+
3+
import toml # type: ignore
4+
from pydantic import BaseModel, field_validator
5+
6+
7+
class PyProjectConfig(BaseModel):
8+
project_name: str
9+
project_version: str
10+
project_dependencies: str
11+
12+
@field_validator("project_name")
13+
@classmethod
14+
def valid_project_name(cls, name: str) -> str: # pylint: disable=no-self-argument,no-self-use
15+
if not name.isidentifier() or "\xb7" in name:
16+
raise ValueError(f"Invalid project name: {name} (hint: only '_' are allowed, no '-')")
17+
return name
18+
19+
def generate_pyproject_toml(self) -> str:
20+
return toml.dumps(
21+
{
22+
"project": {
23+
"name": self.project_name,
24+
"version": self.project_version,
25+
"dependencies": self._get_list_of_project_dependencies(),
26+
}
27+
}
28+
)
29+
30+
def _get_list_of_project_dependencies(self) -> List[str]:
31+
splitted_by_newline = self.project_dependencies.splitlines()
32+
splitted_by_newline_or_comma = [s for ss in splitted_by_newline for s in ss.split(";")]
33+
return sorted(map(lambda x: x.strip(), splitted_by_newline_or_comma))

0 commit comments

Comments
 (0)