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 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
@@ -3,6 +3,10 @@ class PumException(Exception):
3
3
  """Base class for all exceptions raised by PUM."""
4
4
 
5
5
 
6
+ class PumDependencyError(PumException):
7
+ """Exception when dependency are not resolved"""
8
+
9
+
6
10
  # --- Configuration and Validation Errors ---
7
11
 
8
12
 
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
- PUM_VERSION = packaging.version.Version("0.0.0")
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__(self, base_path: str | Path, validate: bool = True, **kwargs: dict) -> None:
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(cls, file_path: str | Path, *, validate: bool = True) -> "PumConfig":
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(base_path=base_path, validate=validate, **data)
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 chanbgelogs and hooks."""
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.0.0
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=IG1g3LMuqSxlW8GC1-H5XdnPX3KF-Irq7MuMEbCYCUM,684
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=GcxBiQM_S4CDG1v1Gy9xj0noaf-GdeJFi8jc2yfVjLU,13956
5
- pum/config_model.py,sha256=G8FsVGsYzMEQKrVIc3SltycbJvwmgfhu6uNduAL-D8E,5107
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=HYgC0kLk6Gel7RtT9AjxHdtkmZ4BtjKdB5BHYL67LVs,1042
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=WopogLoPEJkvuKEdpWq56YZaZc3mgK-pnII0TbYtIlQ,8454
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.0.0.dist-info/licenses/LICENSE,sha256=2ylvL381vKOhdO-w6zkrOxe9lLNBhRQpo9_0EbHC_HM,18046
18
- pum-1.0.0.dist-info/METADATA,sha256=HVBChQBJ0xnjyYYxnYyU7EAhU4CLs0V8SIJhM09j9iE,3146
19
- pum-1.0.0.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
20
- pum-1.0.0.dist-info/entry_points.txt,sha256=U6dmxSpKs1Pe9vWiR29VPhJMDjrmZeJCSxvfLGR8BD4,36
21
- pum-1.0.0.dist-info/top_level.txt,sha256=ddiI4HLBhY6ql-NNm0Ez0JhoOHdWDIzrHeCdHmmagcc,4
22
- pum-1.0.0.dist-info/RECORD,,
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