pum 1.0.0__tar.gz → 1.1.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.
Files changed (158) hide show
  1. {pum-1.0.0 → pum-1.1.0}/.github/workflows/tests.yml +0 -1
  2. {pum-1.0.0 → pum-1.1.0}/.github/workflows/windows-tests.yml +0 -1
  3. {pum-1.0.0/pum.egg-info → pum-1.1.0}/PKG-INFO +1 -1
  4. pum-1.1.0/docs/docs/configuration/dependency_model.md +3 -0
  5. {pum-1.0.0 → pum-1.1.0}/docs/mkdocs.yml +1 -0
  6. {pum-1.0.0 → pum-1.1.0}/pum/__init__.py +2 -0
  7. {pum-1.0.0 → pum-1.1.0}/pum/config_model.py +31 -0
  8. pum-1.1.0/pum/dependency_handler.py +95 -0
  9. {pum-1.0.0 → pum-1.1.0}/pum/exceptions.py +4 -0
  10. {pum-1.0.0 → pum-1.1.0}/pum/pum_config.py +72 -8
  11. {pum-1.0.0 → pum-1.1.0/pum.egg-info}/PKG-INFO +1 -1
  12. {pum-1.0.0 → pum-1.1.0}/pum.egg-info/SOURCES.txt +9 -1
  13. pum-1.1.0/test/data/dependencies/.pum.yaml +17 -0
  14. {pum-1.0.0/test/data/pre_post_python_parameters → pum-1.1.0/test/data/dependencies}/post/create_view.py +0 -2
  15. {pum-1.0.0 → pum-1.1.0}/test/data/pre_post_python/post/create_view.py +3 -9
  16. {pum-1.0.0 → pum-1.1.0}/test/data/pre_post_python_local_import/post/create_view.py +1 -7
  17. {pum-1.0.0 → pum-1.1.0}/test/data/pre_post_python_local_import/post/folder/my_module.py +2 -2
  18. {pum-1.0.0 → pum-1.1.0}/test/data/pre_post_python_parameters/.pum.yaml +0 -1
  19. pum-1.1.0/test/data/pre_post_python_parameters/post/create_view.py +18 -0
  20. pum-1.1.0/test/data/pre_post_python_parameters_broken/post/create_schema.sql +2 -0
  21. {pum-1.0.0 → pum-1.1.0}/test/data/pre_post_python_parameters_broken/post/create_view.py +6 -11
  22. pum-1.1.0/test/data/pre_post_sql_files/changelogs/1.2.4/rename_created_date.sql +2 -0
  23. pum-1.1.0/test/data/pre_post_sql_files/pre/drop_view.sql +3 -0
  24. pum-1.1.0/test/data/single_changelog/changelogs/1.2.3/single_changelog.sql +9 -0
  25. {pum-1.0.0 → pum-1.1.0}/test/test_upgrader.py +15 -2
  26. {pum-1.0.0 → pum-1.1.0}/.ci/setup_db.sh +0 -0
  27. {pum-1.0.0 → pum-1.1.0}/.github/dependabot.yml +0 -0
  28. {pum-1.0.0 → pum-1.1.0}/.github/workflows/deploy-to-pypi.yml +0 -0
  29. {pum-1.0.0 → pum-1.1.0}/.github/workflows/docs.yml +0 -0
  30. {pum-1.0.0 → pum-1.1.0}/.github/workflows/wheel.yml +0 -0
  31. {pum-1.0.0 → pum-1.1.0}/.gitignore +0 -0
  32. {pum-1.0.0 → pum-1.1.0}/.pre-commit-config.yaml +0 -0
  33. {pum-1.0.0 → pum-1.1.0}/LICENSE +0 -0
  34. {pum-1.0.0 → pum-1.1.0}/README.md +0 -0
  35. {pum-1.0.0 → pum-1.1.0}/docs/docs/api/api.md +0 -0
  36. {pum-1.0.0 → pum-1.1.0}/docs/docs/api/changelog.md +0 -0
  37. {pum-1.0.0 → pum-1.1.0}/docs/docs/api/dump_format.md +0 -0
  38. {pum-1.0.0 → pum-1.1.0}/docs/docs/api/dumper.md +0 -0
  39. {pum-1.0.0 → pum-1.1.0}/docs/docs/api/exceptions.md +0 -0
  40. {pum-1.0.0 → pum-1.1.0}/docs/docs/api/hook_base.md +0 -0
  41. {pum-1.0.0 → pum-1.1.0}/docs/docs/api/hook_handler.md +0 -0
  42. {pum-1.0.0 → pum-1.1.0}/docs/docs/api/parameter_definition.md +0 -0
  43. {pum-1.0.0 → pum-1.1.0}/docs/docs/api/parameter_type.md +0 -0
  44. {pum-1.0.0 → pum-1.1.0}/docs/docs/api/permission.md +0 -0
  45. {pum-1.0.0 → pum-1.1.0}/docs/docs/api/permission_type.md +0 -0
  46. {pum-1.0.0 → pum-1.1.0}/docs/docs/api/pum_config.md +0 -0
  47. {pum-1.0.0 → pum-1.1.0}/docs/docs/api/role.md +0 -0
  48. {pum-1.0.0 → pum-1.1.0}/docs/docs/api/role_manager.md +0 -0
  49. {pum-1.0.0 → pum-1.1.0}/docs/docs/api/schema_migrations.md +0 -0
  50. {pum-1.0.0 → pum-1.1.0}/docs/docs/api/sql_content.md +0 -0
  51. {pum-1.0.0 → pum-1.1.0}/docs/docs/api/upgrader.md +0 -0
  52. {pum-1.0.0 → pum-1.1.0}/docs/docs/assets/images/favicon.ico +0 -0
  53. {pum-1.0.0 → pum-1.1.0}/docs/docs/assets/images/pum-darkmode.png +0 -0
  54. {pum-1.0.0 → pum-1.1.0}/docs/docs/assets/images/pum.png +0 -0
  55. {pum-1.0.0 → pum-1.1.0}/docs/docs/cli/baseline.md +0 -0
  56. {pum-1.0.0 → pum-1.1.0}/docs/docs/cli/check.md +0 -0
  57. {pum-1.0.0 → pum-1.1.0}/docs/docs/cli/dump.md +0 -0
  58. {pum-1.0.0 → pum-1.1.0}/docs/docs/cli/info.md +0 -0
  59. {pum-1.0.0 → pum-1.1.0}/docs/docs/cli/install.md +0 -0
  60. {pum-1.0.0 → pum-1.1.0}/docs/docs/cli/restore.md +0 -0
  61. {pum-1.0.0 → pum-1.1.0}/docs/docs/cli/role.md +0 -0
  62. {pum-1.0.0 → pum-1.1.0}/docs/docs/cli/upgrade.md +0 -0
  63. {pum-1.0.0 → pum-1.1.0}/docs/docs/cli.md +0 -0
  64. {pum-1.0.0 → pum-1.1.0}/docs/docs/configuration/config_model.md +0 -0
  65. {pum-1.0.0 → pum-1.1.0}/docs/docs/configuration/configuration.md +0 -0
  66. {pum-1.0.0 → pum-1.1.0}/docs/docs/configuration/demo_data_model.md +0 -0
  67. {pum-1.0.0 → pum-1.1.0}/docs/docs/configuration/hook_model.md +0 -0
  68. {pum-1.0.0 → pum-1.1.0}/docs/docs/configuration/migration_hooks_model.md +0 -0
  69. {pum-1.0.0 → pum-1.1.0}/docs/docs/configuration/parameter_definition_model.md +0 -0
  70. {pum-1.0.0 → pum-1.1.0}/docs/docs/configuration/permission_model.md +0 -0
  71. {pum-1.0.0 → pum-1.1.0}/docs/docs/configuration/pum_model.md +0 -0
  72. {pum-1.0.0 → pum-1.1.0}/docs/docs/configuration/role_model.md +0 -0
  73. {pum-1.0.0 → pum-1.1.0}/docs/docs/getting_started.md +0 -0
  74. {pum-1.0.0 → pum-1.1.0}/docs/docs/hooks.md +0 -0
  75. {pum-1.0.0 → pum-1.1.0}/docs/docs/index.md +0 -0
  76. {pum-1.0.0 → pum-1.1.0}/docs/docs/roles.md +0 -0
  77. {pum-1.0.0 → pum-1.1.0}/docs/requirements.txt +0 -0
  78. {pum-1.0.0 → pum-1.1.0}/docs/roles.md +0 -0
  79. /pum-1.0.0/docs/create_cli_help.py → /pum-1.1.0/docs/update_cli_docs.py +0 -0
  80. {pum-1.0.0 → pum-1.1.0}/pum/changelog.py +0 -0
  81. {pum-1.0.0 → pum-1.1.0}/pum/checker.py +0 -0
  82. {pum-1.0.0 → pum-1.1.0}/pum/cli.py +0 -0
  83. {pum-1.0.0 → pum-1.1.0}/pum/conf/pum_config_example.yaml +0 -0
  84. {pum-1.0.0 → pum-1.1.0}/pum/dumper.py +0 -0
  85. {pum-1.0.0 → pum-1.1.0}/pum/hook.py +0 -0
  86. {pum-1.0.0 → pum-1.1.0}/pum/info.py +0 -0
  87. {pum-1.0.0 → pum-1.1.0}/pum/parameter.py +0 -0
  88. {pum-1.0.0 → pum-1.1.0}/pum/role_manager.py +0 -0
  89. {pum-1.0.0 → pum-1.1.0}/pum/schema_migrations.py +0 -0
  90. {pum-1.0.0 → pum-1.1.0}/pum/sql_content.py +0 -0
  91. {pum-1.0.0 → pum-1.1.0}/pum/upgrader.py +0 -0
  92. {pum-1.0.0 → pum-1.1.0}/pum.egg-info/dependency_links.txt +0 -0
  93. {pum-1.0.0 → pum-1.1.0}/pum.egg-info/entry_points.txt +0 -0
  94. {pum-1.0.0 → pum-1.1.0}/pum.egg-info/requires.txt +0 -0
  95. {pum-1.0.0 → pum-1.1.0}/pum.egg-info/top_level.txt +0 -0
  96. {pum-1.0.0 → pum-1.1.0}/pyproject.toml +0 -0
  97. {pum-1.0.0 → pum-1.1.0}/requirements/base.txt +0 -0
  98. {pum-1.0.0 → pum-1.1.0}/requirements/development.txt +0 -0
  99. {pum-1.0.0 → pum-1.1.0}/scripts/run_postgis.sh +0 -0
  100. {pum-1.0.0 → pum-1.1.0}/setup.cfg +0 -0
  101. {pum-1.0.0 → pum-1.1.0}/test/__init__.py +0 -0
  102. {pum-1.0.0 → pum-1.1.0}/test/data/complex_files_content/changelogs/1.2.3/complex_files_content.sql +0 -0
  103. {pum-1.0.0 → pum-1.1.0}/test/data/custom_directory/.pum.yaml +0 -0
  104. {pum-1.0.0 → pum-1.1.0}/test/data/custom_directory/my_delta_directory/1.2.3/my_delta_directory.sql +0 -0
  105. {pum-1.0.0 → pum-1.1.0}/test/data/custom_migration_schema/.pum.yaml +0 -0
  106. {pum-1.0.0 → pum-1.1.0}/test/data/custom_migration_schema/changelogs/1.2.3/custom_migration_schema.sql +0 -0
  107. {pum-1.0.0 → pum-1.1.0}/test/data/demo_data/.pum.yaml +0 -0
  108. {pum-1.0.0 → pum-1.1.0}/test/data/demo_data/changelogs/1.2.3/single_changelog.sql +0 -0
  109. {pum-1.0.0 → pum-1.1.0}/test/data/demo_data/demo_data/demo_data.sql +0 -0
  110. {pum-1.0.0/test/data/pre_post_python_parameters → pum-1.1.0/test/data/dependencies}/changelogs/1.2.3/pre_post_python_parameters.sql +0 -0
  111. {pum-1.0.0/test/data/multiple_changelogs → pum-1.1.0/test/data/dependencies}/changelogs/1.2.4/rename_created_date.sql +0 -0
  112. {pum-1.0.0/test/data/pre_post_python → pum-1.1.0/test/data/dependencies}/post/create_schema.sql +0 -0
  113. {pum-1.0.0/test/data/pre_post_python → pum-1.1.0/test/data/dependencies}/pre/drop_view.sql +0 -0
  114. {pum-1.0.0 → pum-1.1.0}/test/data/invalid_changelog_commit/changelogs/1.2.3/invalid_changelog.sql +0 -0
  115. {pum-1.0.0 → pum-1.1.0}/test/data/invalid_changelog_search_path/changelogs/1.2.3/invalid_changelog.sql +0 -0
  116. {pum-1.0.0 → pum-1.1.0}/test/data/min_version/.pum.yaml +0 -0
  117. {pum-1.0.0 → pum-1.1.0}/test/data/min_version/changelogs/1.2.3/roles.sql +0 -0
  118. {pum-1.0.0 → pum-1.1.0}/test/data/multiple_changelogs/changelogs/1.2.3/multiple_changelogs.sql +0 -0
  119. {pum-1.0.0/test/data/pre_post_python → pum-1.1.0/test/data/multiple_changelogs}/changelogs/1.2.4/rename_created_date.sql +0 -0
  120. {pum-1.0.0 → pum-1.1.0}/test/data/multiple_changelogs/changelogs/1.3.0/add_created_by_column.sql +0 -0
  121. {pum-1.0.0 → pum-1.1.0}/test/data/multiple_changelogs/changelogs/2.0.0/create_second_table.sql +0 -0
  122. {pum-1.0.0 → pum-1.1.0}/test/data/multiple_changelogs/changelogs/2.0.0/create_third_table.sql +0 -0
  123. {pum-1.0.0 → pum-1.1.0}/test/data/parameters/.pum.yaml +0 -0
  124. {pum-1.0.0 → pum-1.1.0}/test/data/parameters/changelogs/1.2.3/parameters.sql +0 -0
  125. {pum-1.0.0 → pum-1.1.0}/test/data/pre_post_python/.pum.yaml +0 -0
  126. {pum-1.0.0 → pum-1.1.0}/test/data/pre_post_python/changelogs/1.2.3/pre_post_python.sql +0 -0
  127. {pum-1.0.0/test/data/pre_post_python_parameters → pum-1.1.0/test/data/pre_post_python}/changelogs/1.2.4/rename_created_date.sql +0 -0
  128. {pum-1.0.0/test/data/pre_post_python_local_import → pum-1.1.0/test/data/pre_post_python}/post/create_schema.sql +0 -0
  129. {pum-1.0.0/test/data/pre_post_python_local_import → pum-1.1.0/test/data/pre_post_python}/pre/drop_view.sql +0 -0
  130. {pum-1.0.0 → pum-1.1.0}/test/data/pre_post_python_local_import/.pum.yaml +0 -0
  131. {pum-1.0.0 → pum-1.1.0}/test/data/pre_post_python_local_import/changelogs/1.2.3/pre_post_python_local_import.sql +0 -0
  132. {pum-1.0.0/test/data/pre_post_python_parameters → pum-1.1.0/test/data/pre_post_python_local_import}/post/create_schema.sql +0 -0
  133. {pum-1.0.0/test/data/pre_post_python_parameters → pum-1.1.0/test/data/pre_post_python_local_import}/pre/drop_view.sql +0 -0
  134. /pum-1.0.0/test/data/pre_post_python_parameters_broken/changelogs/1.2.3/pre_post_python_parameters_broken.sql → /pum-1.1.0/test/data/pre_post_python_parameters/changelogs/1.2.3/pre_post_python_parameters.sql +0 -0
  135. {pum-1.0.0/test/data/pre_post_python_parameters_broken → pum-1.1.0/test/data/pre_post_python_parameters}/changelogs/1.2.4/rename_created_date.sql +0 -0
  136. {pum-1.0.0/test/data/pre_post_python_parameters_broken → pum-1.1.0/test/data/pre_post_python_parameters}/post/create_schema.sql +0 -0
  137. {pum-1.0.0/test/data/pre_post_python_parameters_broken → pum-1.1.0/test/data/pre_post_python_parameters}/pre/drop_view.sql +0 -0
  138. {pum-1.0.0 → pum-1.1.0}/test/data/pre_post_python_parameters_broken/.pum.yaml +0 -0
  139. /pum-1.0.0/test/data/pre_post_sql_code/changelogs/1.2.3/pre_post_sql_code.sql → /pum-1.1.0/test/data/pre_post_python_parameters_broken/changelogs/1.2.3/pre_post_python_parameters_broken.sql +0 -0
  140. {pum-1.0.0/test/data/pre_post_sql_code → pum-1.1.0/test/data/pre_post_python_parameters_broken}/changelogs/1.2.4/rename_created_date.sql +0 -0
  141. {pum-1.0.0/test/data/pre_post_sql_files → pum-1.1.0/test/data/pre_post_python_parameters_broken}/pre/drop_view.sql +0 -0
  142. {pum-1.0.0 → pum-1.1.0}/test/data/pre_post_sql_code/.pum.yaml +0 -0
  143. /pum-1.0.0/test/data/pre_post_sql_files/changelogs/1.2.3/pre_post_sql_files.sql → /pum-1.1.0/test/data/pre_post_sql_code/changelogs/1.2.3/pre_post_sql_code.sql +0 -0
  144. {pum-1.0.0/test/data/pre_post_sql_files → pum-1.1.0/test/data/pre_post_sql_code}/changelogs/1.2.4/rename_created_date.sql +0 -0
  145. {pum-1.0.0 → pum-1.1.0}/test/data/pre_post_sql_code/post/create_view.sql +0 -0
  146. {pum-1.0.0 → pum-1.1.0}/test/data/pre_post_sql_code/pre/drop_view.sql +0 -0
  147. {pum-1.0.0 → pum-1.1.0}/test/data/pre_post_sql_files/.pum.yaml +0 -0
  148. /pum-1.0.0/test/data/single_changelog/changelogs/1.2.3/single_changelog.sql → /pum-1.1.0/test/data/pre_post_sql_files/changelogs/1.2.3/pre_post_sql_files.sql +0 -0
  149. {pum-1.0.0 → pum-1.1.0}/test/data/pre_post_sql_files/post/create_view.sql +0 -0
  150. {pum-1.0.0 → pum-1.1.0}/test/data/roles/.pum.yaml +0 -0
  151. {pum-1.0.0 → pum-1.1.0}/test/data/roles/changelogs/1.2.3/roles.sql +0 -0
  152. {pum-1.0.0 → pum-1.1.0}/test/test_changelog.py +0 -0
  153. {pum-1.0.0 → pum-1.1.0}/test/test_checker.py.disabled +0 -0
  154. {pum-1.0.0 → pum-1.1.0}/test/test_config.py +0 -0
  155. {pum-1.0.0 → pum-1.1.0}/test/test_dumper.py +0 -0
  156. {pum-1.0.0 → pum-1.1.0}/test/test_pum.sh +0 -0
  157. {pum-1.0.0 → pum-1.1.0}/test/test_roles.py +0 -0
  158. {pum-1.0.0 → pum-1.1.0}/test/test_schema_migrations.py +0 -0
@@ -56,7 +56,6 @@ jobs:
56
56
  run: |
57
57
  python -m pip install -r requirements/base.txt
58
58
  python -m pip install -r requirements/development.txt
59
- python -m pip install git+https://github.com/opengisch/pirogue.git@v3#egg=pirogue
60
59
 
61
60
  - name: Install pum
62
61
  run: python -m pip install .
@@ -49,7 +49,6 @@ jobs:
49
49
  run: |
50
50
  python -m pip install -r requirements/base.txt
51
51
  python -m pip install -r requirements/development.txt
52
- python -m pip install git+https://github.com/opengisch/pirogue.git@v3#egg=pirogue
53
52
 
54
53
  - name: Install pum
55
54
  run: python -m pip install .
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: pum
3
- Version: 1.0.0
3
+ Version: 1.1.0
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
@@ -0,0 +1,3 @@
1
+ # DependencyModel
2
+
3
+ ::: pum.config_model.DependencyModel
@@ -70,6 +70,7 @@ nav:
70
70
  - Configuration:
71
71
  - Overview: configuration/configuration.md
72
72
  - ConfigModel: configuration/config_model.md
73
+ - DependencyModel: configuration/dependency_model.md
73
74
  - HookModel: configuration/hook_model.md
74
75
  - MigrationHooksModel: configuration/migration_hooks_model.md
75
76
  - ParameterDefinitionModel: configuration/parameter_definition_model.md
@@ -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",
@@ -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
@@ -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
 
@@ -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.0
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
@@ -10,10 +10,10 @@ pyproject.toml
10
10
  .github/workflows/tests.yml
11
11
  .github/workflows/wheel.yml
12
12
  .github/workflows/windows-tests.yml
13
- docs/create_cli_help.py
14
13
  docs/mkdocs.yml
15
14
  docs/requirements.txt
16
15
  docs/roles.md
16
+ docs/update_cli_docs.py
17
17
  docs/docs/cli.md
18
18
  docs/docs/getting_started.md
19
19
  docs/docs/hooks.md
@@ -50,6 +50,7 @@ docs/docs/cli/upgrade.md
50
50
  docs/docs/configuration/config_model.md
51
51
  docs/docs/configuration/configuration.md
52
52
  docs/docs/configuration/demo_data_model.md
53
+ docs/docs/configuration/dependency_model.md
53
54
  docs/docs/configuration/hook_model.md
54
55
  docs/docs/configuration/migration_hooks_model.md
55
56
  docs/docs/configuration/parameter_definition_model.md
@@ -61,6 +62,7 @@ pum/changelog.py
61
62
  pum/checker.py
62
63
  pum/cli.py
63
64
  pum/config_model.py
65
+ pum/dependency_handler.py
64
66
  pum/dumper.py
65
67
  pum/exceptions.py
66
68
  pum/hook.py
@@ -98,6 +100,12 @@ test/data/custom_migration_schema/changelogs/1.2.3/custom_migration_schema.sql
98
100
  test/data/demo_data/.pum.yaml
99
101
  test/data/demo_data/changelogs/1.2.3/single_changelog.sql
100
102
  test/data/demo_data/demo_data/demo_data.sql
103
+ test/data/dependencies/.pum.yaml
104
+ test/data/dependencies/changelogs/1.2.3/pre_post_python_parameters.sql
105
+ test/data/dependencies/changelogs/1.2.4/rename_created_date.sql
106
+ test/data/dependencies/post/create_schema.sql
107
+ test/data/dependencies/post/create_view.py
108
+ test/data/dependencies/pre/drop_view.sql
101
109
  test/data/invalid_changelog_commit/changelogs/1.2.3/invalid_changelog.sql
102
110
  test/data/invalid_changelog_search_path/changelogs/1.2.3/invalid_changelog.sql
103
111
  test/data/min_version/.pum.yaml
@@ -0,0 +1,17 @@
1
+
2
+ parameters:
3
+ - name: my_comment
4
+ type: text
5
+ default: "This is a comment"
6
+
7
+ dependencies:
8
+ - name: pirogue
9
+ minimum_version: 3.0.0
10
+
11
+ migration_hooks:
12
+ pre:
13
+ - file: pre/drop_view.sql
14
+
15
+ post:
16
+ - file: post/create_schema.sql
17
+ - file: post/create_view.py
@@ -17,8 +17,6 @@ class Hook(HookBase):
17
17
  SELECT {columns}
18
18
  FROM pum_test_data.some_table
19
19
  WHERE is_active = TRUE;
20
-
21
- COMMENT ON VIEW pum_test_app.some_view IS {{my_comment}};
22
20
  """
23
21
 
24
22
  self.execute(sql=sql_code)
@@ -1,4 +1,3 @@
1
- from pirogue.utils import select_columns
2
1
  import psycopg
3
2
 
4
3
  from pum import HookBase
@@ -7,15 +6,10 @@ from pum import HookBase
7
6
  class Hook(HookBase):
8
7
  def run_hook(self, connection: psycopg.Connection) -> None:
9
8
  """Run the migration hook to create a view."""
10
- columns = select_columns(
11
- connection=connection,
12
- table_schema="pum_test_data",
13
- table_name="some_table",
14
- )
15
- sql_code = f"""
9
+ sql_code = """
16
10
  CREATE OR REPLACE VIEW pum_test_app.some_view AS
17
- SELECT {columns}
11
+ SELECT id, name, created
18
12
  FROM pum_test_data.some_table
19
13
  WHERE is_active = TRUE;
20
- """ # noqa: S608
14
+ """
21
15
  self.execute(sql=sql_code)
@@ -1,4 +1,3 @@
1
- from pirogue.utils import select_columns
2
1
  import psycopg
3
2
 
4
3
  from folder.my_module import produce_sql_code
@@ -9,10 +8,5 @@ from pum import HookBase
9
8
  class Hook(HookBase):
10
9
  def run_hook(self, connection: psycopg.Connection) -> None:
11
10
  """Run the migration hook to create a view."""
12
- columns = select_columns(
13
- connection=connection,
14
- table_schema="pum_test_data",
15
- table_name="some_table",
16
- )
17
- sql_code = produce_sql_code(columns)
11
+ sql_code = produce_sql_code(["id", "name", "created_date"])
18
12
  self.execute(sql=sql_code)
@@ -1,8 +1,8 @@
1
- def produce_sql_code(columns: str) -> str:
1
+ def produce_sql_code(columns: list[str]) -> str:
2
2
  """Produce SQL code."""
3
3
  return f"""
4
4
  CREATE OR REPLACE VIEW pum_test_app.some_view AS
5
- SELECT {columns}
5
+ SELECT {", ".join(columns)}
6
6
  FROM pum_test_data.some_table
7
7
  WHERE is_active = TRUE;
8
8
  """ # noqa: S608
@@ -4,7 +4,6 @@ parameters:
4
4
  type: text
5
5
  default: "This is a comment"
6
6
 
7
-
8
7
  migration_hooks:
9
8
  pre:
10
9
  - file: pre/drop_view.sql
@@ -0,0 +1,18 @@
1
+ import psycopg
2
+
3
+ from pum import HookBase
4
+
5
+
6
+ class Hook(HookBase):
7
+ def run_hook(self, connection: psycopg.Connection) -> None:
8
+ """Run the migration hook to create a view."""
9
+ sql_code = """
10
+ CREATE OR REPLACE VIEW pum_test_app.some_view AS
11
+ SELECT id, name, created_date -- ! changelog 1.2.4 is not called in test
12
+ FROM pum_test_data.some_table
13
+ WHERE is_active = TRUE;
14
+
15
+ COMMENT ON VIEW pum_test_app.some_view IS {my_comment};
16
+ """
17
+
18
+ self.execute(sql=sql_code)
@@ -0,0 +1,2 @@
1
+
2
+ CREATE SCHEMA IF NOT EXISTS pum_test_app;
@@ -1,4 +1,3 @@
1
- from pirogue.utils import select_columns
2
1
  import psycopg
3
2
 
4
3
  from pum import HookBase
@@ -13,17 +12,13 @@ class Hook(HookBase):
13
12
  my_comment (str): The comment to be added to the view.
14
13
 
15
14
  """
16
- columns = select_columns(
17
- pg_cur=connection.cursor(),
18
- table_schema="pum_test_data",
19
- table_name="some_table",
20
- )
21
- sql_code = psycopg.sql.SQL(f"""
15
+
16
+ sql_code = """
22
17
  CREATE OR REPLACE VIEW pum_test_app.some_view AS
23
- SELECT {columns}
18
+ SELECT id, name, created
24
19
  FROM pum_test_data.some_table
25
20
  WHERE is_active = TRUE;
26
21
 
27
- COMMENT ON VIEW pum_test_app.some_view IS {{my_comment}};
28
- """)
29
- self.execute(sql_code=sql_code)
22
+ COMMENT ON VIEW pum_test_app.some_view IS {my_comment};
23
+ """
24
+ self.execute(sql=sql_code)
@@ -0,0 +1,2 @@
1
+
2
+ ALTER TABLE pum_test_data.some_table RENAME COLUMN created_date TO created;
@@ -0,0 +1,3 @@
1
+
2
+
3
+ DROP VIEW IF EXISTS pum_test_app.some_view;
@@ -0,0 +1,9 @@
1
+ CREATE SCHEMA IF NOT EXISTS pum_test_data;
2
+
3
+ CREATE TABLE pum_test_data.some_table (
4
+ id INT PRIMARY KEY,
5
+ name VARCHAR(100) NOT NULL,
6
+ created_date DATE DEFAULT CURRENT_DATE,
7
+ is_active BOOLEAN DEFAULT TRUE,
8
+ amount NUMERIC(10,2)
9
+ );
@@ -7,7 +7,7 @@ from pathlib import Path
7
7
  import psycopg
8
8
 
9
9
  from pum.pum_config import PumConfig
10
- from pum.exceptions import PumException, PumHookError
10
+ from pum.exceptions import PumDependencyError, PumException, PumHookError
11
11
  from pum.parameter import ParameterDefinition
12
12
  from pum.schema_migrations import SchemaMigrations
13
13
  from pum.upgrader import Upgrader
@@ -317,7 +317,7 @@ class TestUpgrader(unittest.TestCase):
317
317
  with psycopg.connect(f"service={self.pg_service}") as conn:
318
318
  self.assertFalse(sm.exists(conn))
319
319
  upgrader = Upgrader(config=cfg)
320
- upgrader.install(connection=conn, max_version="1.2.3")
320
+ upgrader.install(connection=conn)
321
321
  self.assertTrue(sm.exists(conn))
322
322
  cursor = conn.cursor()
323
323
  cursor.execute(
@@ -390,6 +390,19 @@ class TestUpgrader(unittest.TestCase):
390
390
  count = cursor.fetchone()[0]
391
391
  self.assertEqual(count, 4)
392
392
 
393
+ def test_dependencies(self) -> None:
394
+ """Test the installation of dependencies."""
395
+ test_dir = Path("test") / "data" / "dependencies"
396
+ with self.assertRaises(PumDependencyError):
397
+ PumConfig.from_yaml(test_dir / ".pum.yaml")
398
+ cfg = PumConfig.from_yaml(test_dir / ".pum.yaml", install_dependencies=True)
399
+ sm = SchemaMigrations(cfg)
400
+ with psycopg.connect(f"service={self.pg_service}") as conn:
401
+ self.assertFalse(sm.exists(conn))
402
+ upgrader = Upgrader(config=cfg)
403
+ upgrader.install(connection=conn)
404
+ self.assertTrue(sm.exists(conn))
405
+
393
406
 
394
407
  if __name__ == "__main__":
395
408
  unittest.main()
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes