pum 1.0.0__py3-none-any.whl → 1.1.1__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.
- pum/__init__.py +2 -0
- pum/cli.py +2 -2
- pum/config_model.py +31 -0
- pum/dependency_handler.py +95 -0
- pum/exceptions.py +4 -0
- pum/pum_config.py +72 -8
- {pum-1.0.0.dist-info → pum-1.1.1.dist-info}/METADATA +1 -1
- {pum-1.0.0.dist-info → pum-1.1.1.dist-info}/RECORD +12 -11
- {pum-1.0.0.dist-info → pum-1.1.1.dist-info}/WHEEL +0 -0
- {pum-1.0.0.dist-info → pum-1.1.1.dist-info}/entry_points.txt +0 -0
- {pum-1.0.0.dist-info → pum-1.1.1.dist-info}/licenses/LICENSE +0 -0
- {pum-1.0.0.dist-info → pum-1.1.1.dist-info}/top_level.txt +0 -0
pum/__init__.py
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
from .changelog import Changelog
|
|
2
|
+
from .dependency_handler import DependencyHandler
|
|
2
3
|
from .dumper import Dumper, DumpFormat
|
|
3
4
|
from .pum_config import PumConfig
|
|
4
5
|
from .hook import HookHandler, HookBase
|
|
@@ -10,6 +11,7 @@ from .upgrader import Upgrader
|
|
|
10
11
|
|
|
11
12
|
__all__ = [
|
|
12
13
|
"Changelog",
|
|
14
|
+
"DependencyHandler",
|
|
13
15
|
"Dumper",
|
|
14
16
|
"DumpFormat",
|
|
15
17
|
"HookBase",
|
pum/cli.py
CHANGED
|
@@ -303,9 +303,9 @@ def cli() -> int: # noqa: PLR0912
|
|
|
303
303
|
parser.exit()
|
|
304
304
|
|
|
305
305
|
if args.config_file:
|
|
306
|
-
config = PumConfig.from_yaml(args.config_file)
|
|
306
|
+
config = PumConfig.from_yaml(args.config_file, install_dependencies=True)
|
|
307
307
|
else:
|
|
308
|
-
config = PumConfig.from_yaml(Path(args.dir) / ".pum.yaml")
|
|
308
|
+
config = PumConfig.from_yaml(Path(args.dir) / ".pum.yaml", install_dependencies=True)
|
|
309
309
|
|
|
310
310
|
with psycopg.connect(f"service={args.pg_service}") as conn:
|
|
311
311
|
# Check if the connection is successful
|
pum/config_model.py
CHANGED
|
@@ -132,6 +132,36 @@ class DemoDataModel(PumCustomBaseModel):
|
|
|
132
132
|
file: str = Field(..., description="Path to the demo data file.")
|
|
133
133
|
|
|
134
134
|
|
|
135
|
+
class DependencyModel(PumCustomBaseModel):
|
|
136
|
+
"""
|
|
137
|
+
DependencyModel represents a Python dependency for PUM.
|
|
138
|
+
|
|
139
|
+
Attributes:
|
|
140
|
+
name: Name of the Python dependency.
|
|
141
|
+
version: Version of the dependency.
|
|
142
|
+
"""
|
|
143
|
+
|
|
144
|
+
model_config = {"arbitrary_types_allowed": True}
|
|
145
|
+
|
|
146
|
+
name: str = Field(..., description="Name of the Python dependency.")
|
|
147
|
+
minimum_version: Optional[packaging.version.Version] = Field(
|
|
148
|
+
default=None,
|
|
149
|
+
description="Specific minimum required version of the package.",
|
|
150
|
+
)
|
|
151
|
+
maximum_version: Optional[packaging.version.Version] = Field(
|
|
152
|
+
default=None,
|
|
153
|
+
description="Specific maximum required version of the package.",
|
|
154
|
+
)
|
|
155
|
+
|
|
156
|
+
@model_validator(mode="before")
|
|
157
|
+
def parse_version(cls, values):
|
|
158
|
+
for value in ("minimum_version", "maximum_version"):
|
|
159
|
+
ver = values.get(value)
|
|
160
|
+
if isinstance(ver, str):
|
|
161
|
+
values[value] = packaging.version.Version(ver)
|
|
162
|
+
return values
|
|
163
|
+
|
|
164
|
+
|
|
135
165
|
class ConfigModel(PumCustomBaseModel):
|
|
136
166
|
"""
|
|
137
167
|
ConfigModel represents the main configuration schema for the application.
|
|
@@ -150,3 +180,4 @@ class ConfigModel(PumCustomBaseModel):
|
|
|
150
180
|
changelogs_directory: Optional[str] = "changelogs"
|
|
151
181
|
roles: Optional[List[RoleModel]] = []
|
|
152
182
|
demo_data: Optional[List[DemoDataModel]] = []
|
|
183
|
+
dependencies: Optional[List[DependencyModel]] = []
|
|
@@ -0,0 +1,95 @@
|
|
|
1
|
+
import logging
|
|
2
|
+
import packaging
|
|
3
|
+
import packaging.version
|
|
4
|
+
import os
|
|
5
|
+
import sys
|
|
6
|
+
import importlib.metadata
|
|
7
|
+
import subprocess
|
|
8
|
+
|
|
9
|
+
from .exceptions import PumDependencyError
|
|
10
|
+
|
|
11
|
+
logger = logging.getLogger(__name__)
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
class DependencyHandler:
|
|
15
|
+
def __init__(
|
|
16
|
+
self,
|
|
17
|
+
name: str,
|
|
18
|
+
*,
|
|
19
|
+
minimum_version: packaging.version.Version | None,
|
|
20
|
+
maximum_version: packaging.version.Version | None,
|
|
21
|
+
):
|
|
22
|
+
"""
|
|
23
|
+
Initialize the DependencyHandler with a dependency name and version.
|
|
24
|
+
Args:
|
|
25
|
+
name (str): The name of the dependency.
|
|
26
|
+
version (packaging.version.Version | None): The version of the dependency, or None if not specified.
|
|
27
|
+
"""
|
|
28
|
+
self.name = name
|
|
29
|
+
self.minimum_version = minimum_version
|
|
30
|
+
self.maximum_version = maximum_version
|
|
31
|
+
|
|
32
|
+
def resolve(self, install_dependencies: bool = False, install_path: str | None = None):
|
|
33
|
+
"""
|
|
34
|
+
Resolve the dependency by checking if it is installed and compatible with the current PUM version.
|
|
35
|
+
|
|
36
|
+
Args:
|
|
37
|
+
install_dependencies: If True, the dependency will be locally installed.
|
|
38
|
+
Raises:
|
|
39
|
+
PumConfigError: If the dependency is not installed or is incompatible.
|
|
40
|
+
"""
|
|
41
|
+
try:
|
|
42
|
+
importlib.metadata.version(self.name)
|
|
43
|
+
|
|
44
|
+
installed_version = packaging.version.Version(importlib.metadata.version(self.name))
|
|
45
|
+
if self.minimum_version and installed_version < self.minimum_version:
|
|
46
|
+
raise PumDependencyError(
|
|
47
|
+
f"Installed version of `{self.name}` ({installed_version}) is lower than the minimum required ({self.minimum_version})."
|
|
48
|
+
)
|
|
49
|
+
if self.maximum_version and installed_version > self.maximum_version:
|
|
50
|
+
raise PumDependencyError(
|
|
51
|
+
f"Installed version of `{self.name}` ({installed_version}) is higher than the maximum allowed ({self.maximum_version})."
|
|
52
|
+
)
|
|
53
|
+
|
|
54
|
+
logger.info(f"Dependency {self.name} is satisfied.")
|
|
55
|
+
|
|
56
|
+
except importlib.metadata.PackageNotFoundError as e:
|
|
57
|
+
if not install_dependencies:
|
|
58
|
+
raise PumDependencyError(
|
|
59
|
+
f"Dependency `{self.name}` is not installed. You can activate the installation."
|
|
60
|
+
) from e
|
|
61
|
+
else:
|
|
62
|
+
logger.warning(f"Dependency {self.name} is not installed: {e}")
|
|
63
|
+
self.pip_install(install_path=install_path)
|
|
64
|
+
|
|
65
|
+
logger.info(f"Dependency {self.name} is now installed in {install_path}")
|
|
66
|
+
|
|
67
|
+
def pip_install(self, install_path: str):
|
|
68
|
+
"""
|
|
69
|
+
Installs given reqs with pip
|
|
70
|
+
Code copied from qpip plugin
|
|
71
|
+
"""
|
|
72
|
+
|
|
73
|
+
req = self.name
|
|
74
|
+
if self.minimum_version and self.maximum_version:
|
|
75
|
+
req += f">={self.minimum_version},<={self.maximum_version}"
|
|
76
|
+
elif self.minimum_version:
|
|
77
|
+
req += f">={self.minimum_version}"
|
|
78
|
+
elif self.maximum_version:
|
|
79
|
+
req += f"<={self.maximum_version}"
|
|
80
|
+
|
|
81
|
+
command = [self.python_command(), "-m", "pip", "install", req, "--target", install_path]
|
|
82
|
+
|
|
83
|
+
try:
|
|
84
|
+
output = subprocess.run(command, capture_output=True, text=True, check=False)
|
|
85
|
+
if output.returncode != 0:
|
|
86
|
+
logger.error("pip installed failed: %s", output.stderr)
|
|
87
|
+
raise PumDependencyError(output.stderr)
|
|
88
|
+
except TypeError:
|
|
89
|
+
logger.error("Invalid command: %s", " ".join(command))
|
|
90
|
+
raise PumDependencyError("invalid command: {}".format(" ".join(filter(None, command))))
|
|
91
|
+
|
|
92
|
+
def python_command(self):
|
|
93
|
+
# python is normally found at sys.executable, but there is an issue on windows qgis so use 'python' instead
|
|
94
|
+
# https://github.com/qgis/QGIS/issues/45646
|
|
95
|
+
return "python" if os.name == "nt" else sys.executable
|
pum/exceptions.py
CHANGED
pum/pum_config.py
CHANGED
|
@@ -5,20 +5,42 @@ import packaging
|
|
|
5
5
|
from pydantic import ValidationError
|
|
6
6
|
import logging
|
|
7
7
|
import importlib.metadata
|
|
8
|
+
import glob
|
|
9
|
+
import os
|
|
8
10
|
|
|
9
11
|
|
|
10
12
|
from .changelog import Changelog
|
|
13
|
+
from .dependency_handler import DependencyHandler
|
|
11
14
|
from .exceptions import PumConfigError, PumException, PumHookError, PumInvalidChangelog, PumSqlError
|
|
12
15
|
from .parameter import ParameterDefinition
|
|
13
16
|
from .role_manager import RoleManager
|
|
14
17
|
from .config_model import ConfigModel
|
|
15
18
|
from .hook import HookHandler
|
|
19
|
+
import tempfile
|
|
20
|
+
import sys
|
|
21
|
+
import atexit
|
|
16
22
|
|
|
17
23
|
|
|
18
24
|
try:
|
|
19
25
|
PUM_VERSION = packaging.version.Version(importlib.metadata.version("pum"))
|
|
20
26
|
except importlib.metadata.PackageNotFoundError:
|
|
21
|
-
|
|
27
|
+
# Fallback: try to read from pum-*.dist-info/METADATA
|
|
28
|
+
dist_info_dirs = glob.glob(os.path.join(os.path.dirname(__file__), "..", "pum-*.dist-info"))
|
|
29
|
+
version = None
|
|
30
|
+
for dist_info in dist_info_dirs:
|
|
31
|
+
metadata_path = os.path.join(dist_info, "METADATA")
|
|
32
|
+
if os.path.isfile(metadata_path):
|
|
33
|
+
with open(metadata_path) as f:
|
|
34
|
+
for line in f:
|
|
35
|
+
if line.startswith("Version:"):
|
|
36
|
+
version = line.split(":", 1)[1].strip()
|
|
37
|
+
break
|
|
38
|
+
if version:
|
|
39
|
+
break
|
|
40
|
+
if version:
|
|
41
|
+
PUM_VERSION = packaging.version.Version(version)
|
|
42
|
+
else:
|
|
43
|
+
PUM_VERSION = packaging.version.Version("0.0.0")
|
|
22
44
|
|
|
23
45
|
|
|
24
46
|
logger = logging.getLogger(__name__)
|
|
@@ -27,12 +49,20 @@ logger = logging.getLogger(__name__)
|
|
|
27
49
|
class PumConfig:
|
|
28
50
|
"""A class to hold configuration settings."""
|
|
29
51
|
|
|
30
|
-
def __init__(
|
|
52
|
+
def __init__(
|
|
53
|
+
self,
|
|
54
|
+
base_path: str | Path,
|
|
55
|
+
*,
|
|
56
|
+
validate: bool = True,
|
|
57
|
+
install_dependencies: bool = False,
|
|
58
|
+
**kwargs: dict,
|
|
59
|
+
) -> None:
|
|
31
60
|
"""Initialize the configuration with key-value pairs.
|
|
32
61
|
|
|
33
62
|
Args:
|
|
34
63
|
base_path: The directory where the changelogs are located.
|
|
35
|
-
validate: Whether to validate the changelogs and hooks.
|
|
64
|
+
validate: Whether to validate the changelogs and hooks and resolve dependencies. Defaults to True.
|
|
65
|
+
install_dependencies: Whether to temporarily install dependencies.
|
|
36
66
|
**kwargs: Key-value pairs representing configuration settings.
|
|
37
67
|
|
|
38
68
|
Raises:
|
|
@@ -46,6 +76,8 @@ class PumConfig:
|
|
|
46
76
|
raise PumConfigError(f"Directory `{base_path}` does not exist.")
|
|
47
77
|
self._base_path = base_path
|
|
48
78
|
|
|
79
|
+
self.dependency_path = None
|
|
80
|
+
|
|
49
81
|
try:
|
|
50
82
|
self.config = ConfigModel(**kwargs)
|
|
51
83
|
except ValidationError as e:
|
|
@@ -58,19 +90,26 @@ class PumConfig:
|
|
|
58
90
|
f"Minimum required version of pum is {self.config.pum.minimum_version}, but the current version is {PUM_VERSION}. Please upgrade pum."
|
|
59
91
|
)
|
|
60
92
|
try:
|
|
61
|
-
self.validate()
|
|
93
|
+
self.validate(install_dependencies=install_dependencies)
|
|
62
94
|
except (PumInvalidChangelog, PumHookError) as e:
|
|
63
95
|
raise PumConfigError(
|
|
64
96
|
f"Configuration is invalid: {e}. You can disable the validation when constructing the config."
|
|
65
97
|
) from e
|
|
66
98
|
|
|
67
99
|
@classmethod
|
|
68
|
-
def from_yaml(
|
|
100
|
+
def from_yaml(
|
|
101
|
+
cls,
|
|
102
|
+
file_path: str | Path,
|
|
103
|
+
*,
|
|
104
|
+
validate: bool = True,
|
|
105
|
+
install_dependencies: bool = False,
|
|
106
|
+
) -> "PumConfig":
|
|
69
107
|
"""Create a PumConfig instance from a YAML file.
|
|
70
108
|
|
|
71
109
|
Args:
|
|
72
110
|
file_path: The path to the YAML file.
|
|
73
111
|
validate: Whether to validate the changelogs and hooks.
|
|
112
|
+
install_dependencies: Wheter to temporarily install dependencies.
|
|
74
113
|
|
|
75
114
|
Returns:
|
|
76
115
|
PumConfig: An instance of the PumConfig class.
|
|
@@ -87,7 +126,12 @@ class PumConfig:
|
|
|
87
126
|
raise PumConfigError("base_path not allowed in configuration instead.")
|
|
88
127
|
|
|
89
128
|
base_path = Path(file_path).parent
|
|
90
|
-
return cls(
|
|
129
|
+
return cls(
|
|
130
|
+
base_path=base_path,
|
|
131
|
+
validate=validate,
|
|
132
|
+
install_dependencies=install_dependencies,
|
|
133
|
+
**data,
|
|
134
|
+
)
|
|
91
135
|
|
|
92
136
|
@property
|
|
93
137
|
def base_path(self) -> Path:
|
|
@@ -206,13 +250,33 @@ class PumConfig:
|
|
|
206
250
|
"""Return a dictionary of demo data files defined in the configuration."""
|
|
207
251
|
return {dm.name: dm.file for dm in self.config.demo_data}
|
|
208
252
|
|
|
209
|
-
def validate(self) -> None:
|
|
210
|
-
"""Validate the
|
|
253
|
+
def validate(self, install_dependencies: bool = False) -> None:
|
|
254
|
+
"""Validate the changelogs and hooks.
|
|
255
|
+
|
|
256
|
+
Args:
|
|
257
|
+
install_dependencies (bool): Whether to temporarily install dependencies.
|
|
258
|
+
"""
|
|
259
|
+
|
|
260
|
+
if install_dependencies and self.config.dependencies:
|
|
261
|
+
temp_dir = tempfile.TemporaryDirectory()
|
|
262
|
+
self.dependency_path = Path(temp_dir.name)
|
|
263
|
+
sys.path.insert(0, str(self.dependency_path))
|
|
264
|
+
|
|
265
|
+
def cleanup():
|
|
266
|
+
sys.path = [p for p in sys.path if p != str(self.dependency_path)]
|
|
267
|
+
temp_dir.cleanup()
|
|
268
|
+
|
|
269
|
+
atexit.register(cleanup)
|
|
211
270
|
|
|
212
271
|
parameter_defaults = {}
|
|
213
272
|
for parameter in self.config.parameters:
|
|
214
273
|
parameter_defaults[parameter.name] = psycopg.sql.Literal(parameter.default)
|
|
215
274
|
|
|
275
|
+
for dependency in self.config.dependencies:
|
|
276
|
+
DependencyHandler(**dependency.model_dump()).resolve(
|
|
277
|
+
install_dependencies=install_dependencies, install_path=self.dependency_path
|
|
278
|
+
)
|
|
279
|
+
|
|
216
280
|
for changelog in self.changelogs():
|
|
217
281
|
try:
|
|
218
282
|
changelog.validate(parameters=parameter_defaults)
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: pum
|
|
3
|
-
Version: 1.
|
|
3
|
+
Version: 1.1.1
|
|
4
4
|
Summary: Pum stands for "Postgres Upgrades Manager". It is a Database migration management tool very similar to flyway-db or Liquibase, based on metadata tables.
|
|
5
5
|
Author-email: Denis Rouzaud <denis@opengis.ch>
|
|
6
6
|
License-Expression: GPL-2.0-or-later
|
|
@@ -1,22 +1,23 @@
|
|
|
1
|
-
pum/__init__.py,sha256=
|
|
1
|
+
pum/__init__.py,sha256=P-NHd6_SYpk9aypefLI62QCZ3f5APOMCwSzrFFCKAew,759
|
|
2
2
|
pum/changelog.py,sha256=yDc5swmMd5gb2vCEAlenoq5gs-ZEGc4uXicBtiGxkOk,3692
|
|
3
3
|
pum/checker.py,sha256=GT2v7793HP1g94dv0mL6CHtQfblQwAyeFHEWCy44lkc,14379
|
|
4
|
-
pum/cli.py,sha256
|
|
5
|
-
pum/config_model.py,sha256=
|
|
4
|
+
pum/cli.py,sha256=-GRjvGlBx-ok05CKb9-84MmYs44azn4off71nqs3NdY,14010
|
|
5
|
+
pum/config_model.py,sha256=8FREAJpnm6-4Mby_udKpNo4WPbgn-4ZL1e-02a2FUFY,6147
|
|
6
|
+
pum/dependency_handler.py,sha256=34wvDGWlI-vsMFm1z0XcSDN2cnL_VFAh61GWmeYEzk4,3841
|
|
6
7
|
pum/dumper.py,sha256=EJZ8T44JM0GKgdqw1ENOfhZ-RI89OQ4DNdoTZKtLdEw,3404
|
|
7
|
-
pum/exceptions.py,sha256=
|
|
8
|
+
pum/exceptions.py,sha256=xyzzY4ht1nKfrVt59Giulflpmu83nJhxoTygrqiqPlw,1137
|
|
8
9
|
pum/hook.py,sha256=L4Cnr34zrgPzxso9CdsUYWmtuOXRmFccQZ9Lp4IYCBM,9326
|
|
9
10
|
pum/info.py,sha256=VSCUZJJ_ae-khKaudwbgqszZXBMKB_yskuQo5Mc1PgY,1024
|
|
10
11
|
pum/parameter.py,sha256=e9f80kMZpART9laeImW_YECeTvwDyDSmZlTeJGvpS_8,2449
|
|
11
|
-
pum/pum_config.py,sha256=
|
|
12
|
+
pum/pum_config.py,sha256=7rGPaLreOOLl3kuBK_FzYxW02PiWHT4ayenWzOiJWJA,10566
|
|
12
13
|
pum/role_manager.py,sha256=yr-fmytflGqANY3IZIpgJBoMOK98ynTWfemIBhAy79A,10131
|
|
13
14
|
pum/schema_migrations.py,sha256=FiaqAbhFX7vd3Rk_R43kd7-QWfil-Q5587EU8xSLBkA,10504
|
|
14
15
|
pum/sql_content.py,sha256=gwgvcdXOXxNz3RvLtL8Bqr5WO3KKq3sluhbj4OAEnQs,9756
|
|
15
16
|
pum/upgrader.py,sha256=jvl6vmpgxGyYiw8rrWC_bDC7Zd4wHJqGLXCK8EMt9wY,7109
|
|
16
17
|
pum/conf/pum_config_example.yaml,sha256=_nwV_7z6S_Se-mejh_My0JFLY-A0Q4nigeLGPZAfcqg,424
|
|
17
|
-
pum-1.
|
|
18
|
-
pum-1.
|
|
19
|
-
pum-1.
|
|
20
|
-
pum-1.
|
|
21
|
-
pum-1.
|
|
22
|
-
pum-1.
|
|
18
|
+
pum-1.1.1.dist-info/licenses/LICENSE,sha256=2ylvL381vKOhdO-w6zkrOxe9lLNBhRQpo9_0EbHC_HM,18046
|
|
19
|
+
pum-1.1.1.dist-info/METADATA,sha256=1YYn57IUQU69qAwyguFJGizQNG5mtu96NMj9t-PgfWI,3146
|
|
20
|
+
pum-1.1.1.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
|
|
21
|
+
pum-1.1.1.dist-info/entry_points.txt,sha256=U6dmxSpKs1Pe9vWiR29VPhJMDjrmZeJCSxvfLGR8BD4,36
|
|
22
|
+
pum-1.1.1.dist-info/top_level.txt,sha256=ddiI4HLBhY6ql-NNm0Ez0JhoOHdWDIzrHeCdHmmagcc,4
|
|
23
|
+
pum-1.1.1.dist-info/RECORD,,
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|