c2cgeoportal-geoportal 2.3.5.80__py3-none-any.whl → 2.9rc45__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.
Files changed (198) hide show
  1. c2cgeoportal_geoportal/__init__.py +960 -0
  2. c2cgeoportal_geoportal/lib/__init__.py +256 -0
  3. c2cgeoportal_geoportal/lib/authentication.py +250 -0
  4. c2cgeoportal_geoportal/lib/bashcolor.py +46 -0
  5. c2cgeoportal_geoportal/lib/cacheversion.py +77 -0
  6. c2cgeoportal_geoportal/lib/caching.py +176 -0
  7. c2cgeoportal_geoportal/lib/check_collector.py +80 -0
  8. c2cgeoportal_geoportal/lib/checker.py +295 -0
  9. c2cgeoportal_geoportal/lib/common_headers.py +172 -0
  10. c2cgeoportal_geoportal/lib/dbreflection.py +266 -0
  11. c2cgeoportal_geoportal/lib/filter_capabilities.py +360 -0
  12. c2cgeoportal_geoportal/lib/fulltextsearch.py +50 -0
  13. c2cgeoportal_geoportal/lib/functionality.py +166 -0
  14. c2cgeoportal_geoportal/lib/headers.py +62 -0
  15. c2cgeoportal_geoportal/lib/i18n.py +38 -0
  16. c2cgeoportal_geoportal/lib/layers.py +132 -0
  17. c2cgeoportal_geoportal/lib/lingva_extractor.py +937 -0
  18. c2cgeoportal_geoportal/lib/loader.py +57 -0
  19. c2cgeoportal_geoportal/lib/metrics.py +117 -0
  20. c2cgeoportal_geoportal/lib/oauth2.py +1186 -0
  21. c2cgeoportal_geoportal/lib/oidc.py +304 -0
  22. c2cgeoportal_geoportal/lib/wmstparsing.py +353 -0
  23. c2cgeoportal_geoportal/lib/xsd.py +166 -0
  24. c2cgeoportal_geoportal/py.typed +0 -0
  25. c2cgeoportal_geoportal/resources.py +49 -0
  26. c2cgeoportal_geoportal/scaffolds/advance_create/ci/config.yaml +26 -0
  27. c2cgeoportal_geoportal/scaffolds/advance_create/cookiecutter.json +18 -0
  28. c2cgeoportal_geoportal/scaffolds/advance_create/{{cookiecutter.project}}/geoportal/.dockerignore +6 -0
  29. c2cgeoportal_geoportal/scaffolds/advance_create/{{cookiecutter.project}}/geoportal/.eslintrc.yaml +19 -0
  30. c2cgeoportal_geoportal/scaffolds/advance_create/{{cookiecutter.project}}/geoportal/.prospector.yaml +30 -0
  31. c2cgeoportal_geoportal/scaffolds/advance_create/{{cookiecutter.project}}/geoportal/Dockerfile +75 -0
  32. c2cgeoportal_geoportal/scaffolds/advance_create/{{cookiecutter.project}}/geoportal/Makefile +6 -0
  33. c2cgeoportal_geoportal/scaffolds/advance_create/{{cookiecutter.project}}/geoportal/alembic.ini +58 -0
  34. c2cgeoportal_geoportal/scaffolds/advance_create/{{cookiecutter.project}}/geoportal/alembic.yaml +19 -0
  35. c2cgeoportal_geoportal/scaffolds/advance_create/{{cookiecutter.project}}/geoportal/development.ini +121 -0
  36. c2cgeoportal_geoportal/scaffolds/advance_create/{{cookiecutter.project}}/geoportal/gunicorn.conf.py +139 -0
  37. c2cgeoportal_geoportal/scaffolds/advance_create/{{cookiecutter.project}}/geoportal/language_mapping +3 -0
  38. c2cgeoportal_geoportal/scaffolds/advance_create/{{cookiecutter.project}}/geoportal/lingva-client.cfg +5 -0
  39. c2cgeoportal_geoportal/scaffolds/advance_create/{{cookiecutter.project}}/geoportal/lingva-server.cfg +6 -0
  40. c2cgeoportal_geoportal/scaffolds/advance_create/{{cookiecutter.project}}/geoportal/production.ini +38 -0
  41. c2cgeoportal_geoportal/scaffolds/advance_create/{{cookiecutter.project}}/geoportal/requirements.txt +2 -0
  42. c2cgeoportal_geoportal/scaffolds/advance_create/{{cookiecutter.project}}/geoportal/setup.py +25 -0
  43. c2cgeoportal_geoportal/scaffolds/advance_create/{{cookiecutter.project}}/geoportal/webpack.api.js +41 -0
  44. c2cgeoportal_geoportal/scaffolds/advance_create/{{cookiecutter.project}}/geoportal/webpack.apps.js +64 -0
  45. c2cgeoportal_geoportal/scaffolds/advance_create/{{cookiecutter.project}}/geoportal/webpack.commons.js +11 -0
  46. c2cgeoportal_geoportal/scaffolds/advance_create/{{cookiecutter.project}}/geoportal/webpack.config.js +22 -0
  47. c2cgeoportal_geoportal/scaffolds/advance_create/{{cookiecutter.project}}/geoportal/{{cookiecutter.package}}_geoportal/__init__.py +42 -0
  48. c2cgeoportal_geoportal/scaffolds/advance_create/{{cookiecutter.project}}/geoportal/{{cookiecutter.package}}_geoportal/authentication.py +10 -0
  49. c2cgeoportal_geoportal/scaffolds/advance_create/{{cookiecutter.project}}/geoportal/{{cookiecutter.package}}_geoportal/dev.py +14 -0
  50. c2cgeoportal_geoportal/scaffolds/advance_create/{{cookiecutter.project}}/geoportal/{{cookiecutter.package}}_geoportal/models.py +8 -0
  51. c2cgeoportal_geoportal/scaffolds/advance_create/{{cookiecutter.project}}/geoportal/{{cookiecutter.package}}_geoportal/multi_organization.py +7 -0
  52. c2cgeoportal_geoportal/scaffolds/advance_create/{{cookiecutter.project}}/geoportal/{{cookiecutter.package}}_geoportal/resources.py +11 -0
  53. c2cgeoportal_geoportal/scaffolds/advance_create/{{cookiecutter.project}}/geoportal/{{cookiecutter.package}}_geoportal/static-ngeo/api/index.js +12 -0
  54. c2cgeoportal_geoportal/scaffolds/advance_create/{{cookiecutter.project}}/geoportal/{{cookiecutter.package}}_geoportal/static-ngeo/js/{{cookiecutter.package}}module.js +25 -0
  55. c2cgeoportal_geoportal/scaffolds/advance_create/{{cookiecutter.project}}/geoportal/{{cookiecutter.package}}_geoportal/subscribers.py +39 -0
  56. c2cgeoportal_geoportal/scaffolds/advance_create/{{cookiecutter.project}}/geoportal/{{cookiecutter.package}}_geoportal/views/__init__.py +0 -0
  57. c2cgeoportal_geoportal/scaffolds/advance_update/cookiecutter.json +18 -0
  58. c2cgeoportal_geoportal/scaffolds/advance_update/{{cookiecutter.project}}/geoportal/CONST_Makefile +121 -0
  59. c2cgeoportal_geoportal/scaffolds/create/cookiecutter.json +18 -0
  60. c2cgeoportal_geoportal/scaffolds/create/{{cookiecutter.project}}/.dockerignore +14 -0
  61. c2cgeoportal_geoportal/scaffolds/create/{{cookiecutter.project}}/.editorconfig +17 -0
  62. c2cgeoportal_geoportal/scaffolds/create/{{cookiecutter.project}}/.github/workflows/main.yaml +73 -0
  63. c2cgeoportal_geoportal/scaffolds/create/{{cookiecutter.project}}/.github/workflows/rebuild.yaml +50 -0
  64. c2cgeoportal_geoportal/scaffolds/create/{{cookiecutter.project}}/.github/workflows/update_l10n.yaml +66 -0
  65. c2cgeoportal_geoportal/scaffolds/create/{{cookiecutter.project}}/.gitignore +16 -0
  66. c2cgeoportal_geoportal/scaffolds/create/{{cookiecutter.project}}/.pre-commit-config.yaml +35 -0
  67. c2cgeoportal_geoportal/scaffolds/create/{{cookiecutter.project}}/.prettierignore +1 -0
  68. c2cgeoportal_geoportal/scaffolds/create/{{cookiecutter.project}}/.prettierrc.yaml +2 -0
  69. c2cgeoportal_geoportal/scaffolds/create/{{cookiecutter.project}}/Dockerfile +75 -0
  70. c2cgeoportal_geoportal/scaffolds/create/{{cookiecutter.project}}/Makefile +70 -0
  71. c2cgeoportal_geoportal/scaffolds/create/{{cookiecutter.project}}/README.rst +29 -0
  72. c2cgeoportal_geoportal/scaffolds/create/{{cookiecutter.project}}/build +179 -0
  73. c2cgeoportal_geoportal/scaffolds/create/{{cookiecutter.project}}/ci/config.yaml +22 -0
  74. c2cgeoportal_geoportal/scaffolds/create/{{cookiecutter.project}}/ci/docker-compose-check +25 -0
  75. c2cgeoportal_geoportal/scaffolds/create/{{cookiecutter.project}}/ci/requirements.txt +2 -0
  76. c2cgeoportal_geoportal/scaffolds/create/{{cookiecutter.project}}/docker-compose-db.yaml +24 -0
  77. c2cgeoportal_geoportal/scaffolds/create/{{cookiecutter.project}}/docker-compose-lib.yaml +513 -0
  78. c2cgeoportal_geoportal/scaffolds/create/{{cookiecutter.project}}/docker-compose-qgis.yaml +21 -0
  79. c2cgeoportal_geoportal/scaffolds/create/{{cookiecutter.project}}/docker-compose.override.sample.yaml +65 -0
  80. c2cgeoportal_geoportal/scaffolds/create/{{cookiecutter.project}}/docker-compose.yaml +121 -0
  81. c2cgeoportal_geoportal/scaffolds/create/{{cookiecutter.project}}/env.default +102 -0
  82. c2cgeoportal_geoportal/scaffolds/create/{{cookiecutter.project}}/env.project +69 -0
  83. c2cgeoportal_geoportal/scaffolds/create/{{cookiecutter.project}}/geoportal/vars.yaml +430 -0
  84. c2cgeoportal_geoportal/scaffolds/create/{{cookiecutter.project}}/geoportal/{{cookiecutter.package}}_geoportal/locale/en/LC_MESSAGES/{{cookiecutter.package}}_geoportal-client.po +6 -0
  85. c2cgeoportal_geoportal/scaffolds/create/{{cookiecutter.project}}/geoportal/{{cookiecutter.package}}_geoportal/static/css/desktop.css +0 -0
  86. c2cgeoportal_geoportal/scaffolds/create/{{cookiecutter.project}}/geoportal/{{cookiecutter.package}}_geoportal/static/css/iframe_api.css +0 -0
  87. c2cgeoportal_geoportal/scaffolds/create/{{cookiecutter.project}}/geoportal/{{cookiecutter.package}}_geoportal/static/css/mobile.css +0 -0
  88. c2cgeoportal_geoportal/scaffolds/create/{{cookiecutter.project}}/geoportal/{{cookiecutter.package}}_geoportal/static/images/banner_left.png +0 -0
  89. c2cgeoportal_geoportal/scaffolds/create/{{cookiecutter.project}}/geoportal/{{cookiecutter.package}}_geoportal/static/images/banner_right.png +0 -0
  90. c2cgeoportal_geoportal/scaffolds/create/{{cookiecutter.project}}/geoportal/{{cookiecutter.package}}_geoportal/static/images/blank.png +0 -0
  91. c2cgeoportal_geoportal/scaffolds/create/{{cookiecutter.project}}/geoportal/{{cookiecutter.package}}_geoportal/static/images/markers/marker-blue.png +0 -0
  92. c2cgeoportal_geoportal/scaffolds/create/{{cookiecutter.project}}/geoportal/{{cookiecutter.package}}_geoportal/static/images/markers/marker-gold.png +0 -0
  93. c2cgeoportal_geoportal/scaffolds/create/{{cookiecutter.project}}/geoportal/{{cookiecutter.package}}_geoportal/static/images/markers/marker-green.png +0 -0
  94. c2cgeoportal_geoportal/scaffolds/create/{{cookiecutter.project}}/geoportal/{{cookiecutter.package}}_geoportal/static/images/markers/marker.png +0 -0
  95. c2cgeoportal_geoportal/scaffolds/create/{{cookiecutter.project}}/geoportal/{{cookiecutter.package}}_geoportal/static/robot.txt.tmpl +3 -0
  96. c2cgeoportal_geoportal/scaffolds/create/{{cookiecutter.project}}/mapserver/data/Readme.txt +69 -0
  97. c2cgeoportal_geoportal/scaffolds/create/{{cookiecutter.project}}/mapserver/data/TM_EUROPE_BORDERS-0.3.sql +70 -0
  98. c2cgeoportal_geoportal/scaffolds/create/{{cookiecutter.project}}/mapserver/demo.map.tmpl +224 -0
  99. c2cgeoportal_geoportal/scaffolds/create/{{cookiecutter.project}}/mapserver/fonts/Arial.ttf +0 -0
  100. c2cgeoportal_geoportal/scaffolds/create/{{cookiecutter.project}}/mapserver/fonts/Arialbd.ttf +0 -0
  101. c2cgeoportal_geoportal/scaffolds/create/{{cookiecutter.project}}/mapserver/fonts/Arialbi.ttf +0 -0
  102. c2cgeoportal_geoportal/scaffolds/create/{{cookiecutter.project}}/mapserver/fonts/Ariali.ttf +0 -0
  103. c2cgeoportal_geoportal/scaffolds/create/{{cookiecutter.project}}/mapserver/fonts/NotoSans-Bold.ttf +0 -0
  104. c2cgeoportal_geoportal/scaffolds/create/{{cookiecutter.project}}/mapserver/fonts/NotoSans-BoldItalic.ttf +0 -0
  105. c2cgeoportal_geoportal/scaffolds/create/{{cookiecutter.project}}/mapserver/fonts/NotoSans-Italic.ttf +0 -0
  106. c2cgeoportal_geoportal/scaffolds/create/{{cookiecutter.project}}/mapserver/fonts/NotoSans-Regular.ttf +0 -0
  107. c2cgeoportal_geoportal/scaffolds/create/{{cookiecutter.project}}/mapserver/fonts/Verdana.ttf +0 -0
  108. c2cgeoportal_geoportal/scaffolds/create/{{cookiecutter.project}}/mapserver/fonts/Verdanab.ttf +0 -0
  109. c2cgeoportal_geoportal/scaffolds/create/{{cookiecutter.project}}/mapserver/fonts/Verdanai.ttf +0 -0
  110. c2cgeoportal_geoportal/scaffolds/create/{{cookiecutter.project}}/mapserver/fonts/Verdanaz.ttf +0 -0
  111. c2cgeoportal_geoportal/scaffolds/create/{{cookiecutter.project}}/mapserver/fonts.conf +12 -0
  112. c2cgeoportal_geoportal/scaffolds/create/{{cookiecutter.project}}/mapserver/mapserver.conf +16 -0
  113. c2cgeoportal_geoportal/scaffolds/create/{{cookiecutter.project}}/mapserver/mapserver.map.tmpl +87 -0
  114. c2cgeoportal_geoportal/scaffolds/create/{{cookiecutter.project}}/mapserver/tinyows.xml.tmpl +36 -0
  115. c2cgeoportal_geoportal/scaffolds/create/{{cookiecutter.project}}/print/print-apps/{{cookiecutter.package}}/A3_Landscape.jrxml +207 -0
  116. c2cgeoportal_geoportal/scaffolds/create/{{cookiecutter.project}}/print/print-apps/{{cookiecutter.package}}/A3_Portrait.jrxml +185 -0
  117. c2cgeoportal_geoportal/scaffolds/create/{{cookiecutter.project}}/print/print-apps/{{cookiecutter.package}}/A4_Landscape.jrxml +200 -0
  118. c2cgeoportal_geoportal/scaffolds/create/{{cookiecutter.project}}/print/print-apps/{{cookiecutter.package}}/A4_Portrait.jrxml +170 -0
  119. c2cgeoportal_geoportal/scaffolds/create/{{cookiecutter.project}}/print/print-apps/{{cookiecutter.package}}/config.yaml.tmpl +175 -0
  120. c2cgeoportal_geoportal/scaffolds/create/{{cookiecutter.project}}/print/print-apps/{{cookiecutter.package}}/legend.jrxml +109 -0
  121. c2cgeoportal_geoportal/scaffolds/create/{{cookiecutter.project}}/print/print-apps/{{cookiecutter.package}}/localisation.properties +4 -0
  122. c2cgeoportal_geoportal/scaffolds/create/{{cookiecutter.project}}/print/print-apps/{{cookiecutter.package}}/localisation_fr.properties +4 -0
  123. c2cgeoportal_geoportal/scaffolds/create/{{cookiecutter.project}}/print/print-apps/{{cookiecutter.package}}/logo.png +0 -0
  124. c2cgeoportal_geoportal/scaffolds/create/{{cookiecutter.project}}/print/print-apps/{{cookiecutter.package}}/north.svg +93 -0
  125. c2cgeoportal_geoportal/scaffolds/create/{{cookiecutter.project}}/print/print-apps/{{cookiecutter.package}}/results.jrxml +25 -0
  126. c2cgeoportal_geoportal/scaffolds/create/{{cookiecutter.project}}/project.yaml +18 -0
  127. c2cgeoportal_geoportal/scaffolds/create/{{cookiecutter.project}}/pyproject.toml +7 -0
  128. c2cgeoportal_geoportal/scaffolds/create/{{cookiecutter.project}}/qgisserver/pg_service.conf.tmpl +15 -0
  129. c2cgeoportal_geoportal/scaffolds/create/{{cookiecutter.project}}/run_alembic.sh +11 -0
  130. c2cgeoportal_geoportal/scaffolds/create/{{cookiecutter.project}}/scripts/db-backup +126 -0
  131. c2cgeoportal_geoportal/scaffolds/create/{{cookiecutter.project}}/scripts/db-restore +132 -0
  132. c2cgeoportal_geoportal/scaffolds/create/{{cookiecutter.project}}/setup.cfg +7 -0
  133. c2cgeoportal_geoportal/scaffolds/create/{{cookiecutter.project}}/spell-ignore-words.txt +5 -0
  134. c2cgeoportal_geoportal/scaffolds/create/{{cookiecutter.project}}/tests/__init__.py +0 -0
  135. c2cgeoportal_geoportal/scaffolds/create/{{cookiecutter.project}}/tests/test_app.py +78 -0
  136. c2cgeoportal_geoportal/scaffolds/create/{{cookiecutter.project}}/tilegeneration/config.yaml.tmpl +195 -0
  137. c2cgeoportal_geoportal/scaffolds/update/cookiecutter.json +18 -0
  138. c2cgeoportal_geoportal/scaffolds/update/{{cookiecutter.project}}/.upgrade.yaml +67 -0
  139. c2cgeoportal_geoportal/scaffolds/update/{{cookiecutter.project}}/CONST_CHANGELOG.txt +304 -0
  140. c2cgeoportal_geoportal/scaffolds/update/{{cookiecutter.project}}/CONST_create_template/tests/test_testapp.py +48 -0
  141. c2cgeoportal_geoportal/scaffolds/update/{{cookiecutter.project}}/geoportal/.CONST_vars.yaml.swp +0 -0
  142. c2cgeoportal_geoportal/scaffolds/update/{{cookiecutter.project}}/geoportal/CONST_config-schema.yaml +927 -0
  143. c2cgeoportal_geoportal/scaffolds/update/{{cookiecutter.project}}/geoportal/CONST_vars.yaml +1503 -0
  144. c2cgeoportal_geoportal/scripts/__init__.py +64 -0
  145. c2cgeoportal_geoportal/scripts/c2cupgrade.py +879 -0
  146. c2cgeoportal_geoportal/scripts/create_demo_theme.py +83 -0
  147. c2cgeoportal_geoportal/scripts/manage_users.py +140 -0
  148. c2cgeoportal_geoportal/scripts/pcreate.py +296 -0
  149. c2cgeoportal_geoportal/scripts/theme2fts.py +347 -0
  150. c2cgeoportal_geoportal/scripts/urllogin.py +81 -0
  151. c2cgeoportal_geoportal/templates/login.html +90 -0
  152. c2cgeoportal_geoportal/templates/notlogin.html +62 -0
  153. c2cgeoportal_geoportal/templates/testi18n.html +12 -0
  154. c2cgeoportal_geoportal/views/__init__.py +59 -0
  155. c2cgeoportal_geoportal/views/dev.py +57 -0
  156. c2cgeoportal_geoportal/views/dynamic.py +209 -0
  157. c2cgeoportal_geoportal/views/entry.py +174 -0
  158. c2cgeoportal_geoportal/views/fulltextsearch.py +189 -0
  159. c2cgeoportal_geoportal/views/geometry_processing.py +75 -0
  160. c2cgeoportal_geoportal/views/i18n.py +129 -0
  161. c2cgeoportal_geoportal/views/layers.py +713 -0
  162. c2cgeoportal_geoportal/views/login.py +684 -0
  163. c2cgeoportal_geoportal/views/mapserverproxy.py +234 -0
  164. c2cgeoportal_geoportal/views/memory.py +90 -0
  165. c2cgeoportal_geoportal/views/ogcproxy.py +120 -0
  166. c2cgeoportal_geoportal/views/pdfreport.py +245 -0
  167. c2cgeoportal_geoportal/views/printproxy.py +143 -0
  168. c2cgeoportal_geoportal/views/profile.py +192 -0
  169. c2cgeoportal_geoportal/views/proxy.py +261 -0
  170. c2cgeoportal_geoportal/views/raster.py +233 -0
  171. c2cgeoportal_geoportal/views/resourceproxy.py +73 -0
  172. c2cgeoportal_geoportal/views/shortener.py +152 -0
  173. c2cgeoportal_geoportal/views/theme.py +1322 -0
  174. c2cgeoportal_geoportal/views/tinyowsproxy.py +189 -0
  175. c2cgeoportal_geoportal/views/vector_tiles.py +83 -0
  176. {c2cgeoportal_geoportal-2.3.5.80.dist-info → c2cgeoportal_geoportal-2.9rc45.dist-info}/METADATA +21 -24
  177. c2cgeoportal_geoportal-2.9rc45.dist-info/RECORD +193 -0
  178. {c2cgeoportal_geoportal-2.3.5.80.dist-info → c2cgeoportal_geoportal-2.9rc45.dist-info}/WHEEL +1 -1
  179. c2cgeoportal_geoportal-2.9rc45.dist-info/entry_points.txt +28 -0
  180. c2cgeoportal_geoportal-2.9rc45.dist-info/top_level.txt +2 -0
  181. tests/__init__.py +100 -0
  182. tests/test_cachebuster.py +71 -0
  183. tests/test_caching.py +275 -0
  184. tests/test_checker.py +85 -0
  185. tests/test_decimaljson.py +47 -0
  186. tests/test_headerstween.py +64 -0
  187. tests/test_i18n.py +31 -0
  188. tests/test_init.py +193 -0
  189. tests/test_locale_negociator.py +69 -0
  190. tests/test_mapserverproxy_route_predicate.py +64 -0
  191. tests/test_raster.py +267 -0
  192. tests/test_wmstparsing.py +238 -0
  193. tests/xmlstr.py +103 -0
  194. c2cgeoportal_geoportal-2.3.5.80.dist-info/DESCRIPTION.rst +0 -8
  195. c2cgeoportal_geoportal-2.3.5.80.dist-info/RECORD +0 -7
  196. c2cgeoportal_geoportal-2.3.5.80.dist-info/entry_points.txt +0 -22
  197. c2cgeoportal_geoportal-2.3.5.80.dist-info/metadata.json +0 -1
  198. c2cgeoportal_geoportal-2.3.5.80.dist-info/top_level.txt +0 -1
@@ -0,0 +1,879 @@
1
+ # Copyright (c) 2014-2024, Camptocamp SA
2
+ # All rights reserved.
3
+
4
+ # Redistribution and use in source and binary forms, with or without
5
+ # modification, are permitted provided that the following conditions are met:
6
+
7
+ # 1. Redistributions of source code must retain the above copyright notice, this
8
+ # list of conditions and the following disclaimer.
9
+ # 2. Redistributions in binary form must reproduce the above copyright notice,
10
+ # this list of conditions and the following disclaimer in the documentation
11
+ # and/or other materials provided with the distribution.
12
+
13
+ # THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND
14
+ # ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
15
+ # WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
16
+ # DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR
17
+ # ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
18
+ # (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
19
+ # LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND
20
+ # ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
21
+ # (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
22
+ # SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
23
+
24
+ # The views and conclusions contained in the software and documentation are those
25
+ # of the authors and should not be interpreted as representing official policies,
26
+ # either expressed or implied, of the FreeBSD Project.
27
+
28
+
29
+ import argparse
30
+ import atexit
31
+ import filecmp
32
+ import os
33
+ import re
34
+ import shutil
35
+ import subprocess
36
+ import sys
37
+ from argparse import ArgumentParser, Namespace
38
+ from collections.abc import Callable
39
+ from json.decoder import JSONDecodeError
40
+ from subprocess import call, check_call, check_output
41
+ from typing import Any, Union, cast
42
+
43
+ import pkg_resources
44
+ import requests
45
+ import yaml
46
+
47
+ from c2cgeoportal_geoportal.lib.bashcolor import Color, colorize
48
+
49
+ REQUIRED_TEMPLATE_KEYS = ["package", "srid", "extent"]
50
+ TEMPLATE_EXAMPLE = {"package": "${package}", "srid": "${srid}", "extent": "489246, 78873, 837119, 296543"}
51
+ DIFF_NOTICE = (
52
+ "You should apply the changes shown in the diff file on `CONST_create_template/<file>` "
53
+ "on your project's `<file>`.\n"
54
+ "Some advice to be more efficient: if the changes on a file concern a file that you never customize, "
55
+ "you can simply copy the new file from `CONST_create_template` "
56
+ "(`cp CONST_create_template/<file> <file>`)."
57
+ "You can furthermore add this file to the `unmanaged_files` section of the `project.yaml` file, "
58
+ "to avoid its contents appearing in the diff file for the next upgrade."
59
+ )
60
+
61
+
62
+ def fix_style() -> None:
63
+ """Fix the style of all the project files using isort, Black and Prettier."""
64
+
65
+ file_to_clean = []
66
+ for filename, content in (
67
+ (".prettierignore", "*.min.js\n"),
68
+ ("pyproject.toml", "[tool.black]\nline-length = 110\ntarget-version = ['py39']\n"),
69
+ (".prettierrc.yaml", "bracketSpacing: false\nquoteProps: preserve\n"),
70
+ (
71
+ ".editorconfig",
72
+ """root = true
73
+ [*]
74
+ max_line_length = 110
75
+ """,
76
+ ),
77
+ ):
78
+ if not os.path.exists(filename):
79
+ file_to_clean.append(filename)
80
+ if os.path.exists(os.path.join("CONST_create_template", filename)):
81
+ shutil.copyfile(os.path.join("CONST_create_template", filename), filename)
82
+ else:
83
+ with open(filename, "w", encoding="utf8") as file_:
84
+ file_.write(content)
85
+
86
+ if os.path.exists("ci/config.yaml"):
87
+ os.rename("ci/config.yaml", "ci/config.yaml_")
88
+ if os.path.exists(".pre-commit-config.yaml"):
89
+ print("Run pre-commit to fix the style.")
90
+ sys.stdout.flush()
91
+ subprocess.run(["pre-commit", "run", "--all-files"]) # pylint: disable=subprocess-run-check
92
+ else:
93
+ print("Run c2cciutils-checks to fix the style.")
94
+ sys.stdout.flush()
95
+ subprocess.run( # pylint: disable=subprocess-run-check
96
+ ["c2cciutils-checks", "--fix", "--check=isort"]
97
+ )
98
+ subprocess.run( # pylint: disable=subprocess-run-check
99
+ ["c2cciutils-checks", "--fix", "--check=black"]
100
+ )
101
+ subprocess.run( # pylint: disable=subprocess-run-check
102
+ ["c2cciutils-checks", "--fix", "--check=prettier"]
103
+ )
104
+ if os.path.exists("ci/config.yaml_"):
105
+ os.rename("ci/config.yaml_", "ci/config.yaml")
106
+
107
+ for filename in file_to_clean:
108
+ os.remove(filename)
109
+
110
+
111
+ def main() -> None:
112
+ """Tool used to do the application upgrade."""
113
+ parser = _fill_arguments()
114
+ options = parser.parse_args()
115
+
116
+ c2cupgradetool = C2cUpgradeTool(options)
117
+ c2cupgradetool.upgrade()
118
+
119
+
120
+ def _fill_arguments() -> ArgumentParser:
121
+ parser = ArgumentParser()
122
+ parser.add_argument(
123
+ "--git-remote", metavar="GITREMOTE", help="Specify the remote branch", default="origin"
124
+ )
125
+ parser.add_argument("--step", type=int, help=argparse.SUPPRESS, default=0)
126
+
127
+ return parser
128
+
129
+
130
+ class InteruptedException(Exception):
131
+ """The interrupted exception."""
132
+
133
+
134
+ _CURRENT_STEP_NUMBER = 0
135
+
136
+
137
+ class Step:
138
+ """Decorator used for en upgrade step."""
139
+
140
+ def __init__(self, step_number: int, file_marker: bool = True):
141
+ global _CURRENT_STEP_NUMBER # pylint: disable=global-statement
142
+ _CURRENT_STEP_NUMBER = step_number
143
+ self.step_number = step_number
144
+ self.file_marker = file_marker
145
+
146
+ def __call__(
147
+ self, current_step: Callable[["C2cUpgradeTool", int], None]
148
+ ) -> Callable[["C2cUpgradeTool"], None]:
149
+ def decorate(c2cupgradetool: "C2cUpgradeTool") -> None:
150
+ try:
151
+ if os.path.isfile(f".UPGRADE{self.step_number - 1}"):
152
+ os.unlink(f".UPGRADE{self.step_number - 1}")
153
+ if self.file_marker:
154
+ with open(f".UPGRADE{self.step_number}", "w", encoding="utf8"):
155
+ pass
156
+ print(f"Start step {self.step_number}.")
157
+ sys.stdout.flush()
158
+ current_step(c2cupgradetool, self.step_number)
159
+ except subprocess.CalledProcessError as exception:
160
+ command = " ".join([f"'{exception}'" for exception in exception.cmd])
161
+ c2cupgradetool.print_step(
162
+ self.step_number,
163
+ error=True,
164
+ message=f"The command `{command}` returns the error code {exception.returncode}.",
165
+ prompt="Fix the error and run the step again:",
166
+ )
167
+ sys.exit(1)
168
+ except InteruptedException as exception:
169
+ c2cupgradetool.print_step(
170
+ self.step_number,
171
+ error=True,
172
+ message=f"There was an error: {exception}.",
173
+ prompt="Fix the error and run the step again:",
174
+ )
175
+ sys.exit(1)
176
+ except Exception as exception:
177
+ catch_exception = exception
178
+
179
+ if self.step_number == _CURRENT_STEP_NUMBER:
180
+
181
+ def message() -> None:
182
+ c2cupgradetool.print_step(
183
+ self.step_number,
184
+ error=True,
185
+ message=f"The step had the error '{catch_exception}'.",
186
+ prompt="Fix the error and run the step again:",
187
+ )
188
+
189
+ atexit.register(message)
190
+ raise
191
+
192
+ return decorate
193
+
194
+
195
+ class C2cUpgradeTool:
196
+ """The tool used to upgrade the application."""
197
+
198
+ color_bar = colorize("================================================================", Color.GREEN)
199
+
200
+ def __init__(self, options: Namespace):
201
+ self.options = options
202
+ self.project = self.get_project()
203
+
204
+ @staticmethod
205
+ def get_project() -> dict[str, Any]:
206
+ if not os.path.isfile("project.yaml"):
207
+ print(colorize("Unable to find the required 'project.yaml' file.", Color.RED))
208
+ sys.exit(1)
209
+
210
+ with open("project.yaml", encoding="utf8") as project_file:
211
+ return cast(dict[str, Any], yaml.safe_load(project_file))
212
+
213
+ @staticmethod
214
+ def get_upgrade(section: str) -> list[Any] | dict[str, Any]:
215
+ if not os.path.isfile(".upgrade.yaml"):
216
+ print(colorize("Unable to find the required '.upgrade.yaml' file.", Color.RED))
217
+ sys.exit(1)
218
+
219
+ with open(".upgrade.yaml", encoding="utf8") as project_file:
220
+ return cast(Union[list[Any], dict[str, Any]], yaml.safe_load(project_file)[section])
221
+
222
+ def print_step(
223
+ self,
224
+ step: int,
225
+ error: bool = False,
226
+ message: str | None = None,
227
+ prompt: str = "To continue, type:",
228
+ ) -> None:
229
+ with open(".UPGRADE_INSTRUCTIONS", "w", encoding="utf8") as instructions:
230
+ print("")
231
+ print(self.color_bar)
232
+ if message is not None:
233
+ print(colorize(message, Color.RED if error else Color.YELLOW))
234
+ instructions.write(f"{message}\n")
235
+ if step >= 0:
236
+ print(colorize(prompt, Color.GREEN))
237
+ instructions.write(f"{prompt}\n")
238
+ cmd = ["./upgrade", os.environ["VERSION"]]
239
+ if step != 0:
240
+ cmd.append(f"{step}")
241
+ print(colorize(" ".join(cmd), Color.GREEN))
242
+ instructions.write(f"{' '.join(cmd)}\n")
243
+
244
+ def run_step(self, step: int) -> None:
245
+ getattr(self, f"step{step}")()
246
+
247
+ def test_checkers(self) -> tuple[bool, str | None]:
248
+ headers = " ".join(
249
+ [f"--header {i[0]}={i[1]}" for i in self.project.get("checker_headers", {}).items()]
250
+ )
251
+ run_curl = f"Run `curl --insecure {headers} '{self.project['checker_url']}'` for more information."
252
+ try:
253
+ requests.packages.urllib3.disable_warnings() # type: ignore
254
+ resp = requests.get(
255
+ self.project["checker_url"],
256
+ headers=self.project.get("checker_headers"),
257
+ verify=False, # nosec
258
+ timeout=120,
259
+ )
260
+ except requests.exceptions.ConnectionError as exception:
261
+ return False, "\n".join([f"Connection error: {exception}", run_curl])
262
+ except ConnectionRefusedError as exception:
263
+ return False, "\n".join([f"Connection refused: {exception}", run_curl])
264
+ if resp.status_code < 200 or resp.status_code >= 300:
265
+ print(colorize("=============", Color.RED))
266
+ print(colorize("Checker error", Color.RED))
267
+ try:
268
+ for name, value in resp.json()["failures"].items():
269
+ print(colorize(f"Test '{name}' failed with result:", Color.YELLOW))
270
+ del value["level"]
271
+ del value["timing"]
272
+
273
+ print(yaml.dump(value) if value != {} else "No result")
274
+ except JSONDecodeError:
275
+ print(
276
+ colorize(
277
+ f"Response is not a JSON '{resp.text}', {resp.reason} {resp.status_code}",
278
+ Color.RED,
279
+ )
280
+ )
281
+
282
+ return False, "\n".join(["Checker error:", run_curl])
283
+
284
+ return True, None
285
+
286
+ def upgrade(self) -> None:
287
+ self.run_step(self.options.step)
288
+
289
+ @Step(0, file_marker=False)
290
+ def step0(self, step: int) -> None:
291
+ project_template_keys = list(cast(dict[str, Any], self.project.get("template_vars")).keys())
292
+ messages = []
293
+ for required in REQUIRED_TEMPLATE_KEYS:
294
+ if required not in project_template_keys:
295
+ messages.append(
296
+ "The element '{required}' is missing in the `template_vars` of "
297
+ "the file 'project.yaml', you should have for example: {required}: {template}.".format(
298
+ required=required, template=TEMPLATE_EXAMPLE.get("required", "")
299
+ )
300
+ )
301
+ if self.project.get("managed_files") is None:
302
+ messages.append(
303
+ "The element `managed_files` is missing in the file 'project.yaml', "
304
+ "you must define this element with a list of regular expressions or with an empty array. "
305
+ "See upgrade documentation for more information."
306
+ )
307
+ if messages:
308
+ self.print_step(
309
+ step, error=True, message="\n".join(messages), prompt="Fix it and run again the upgrade:"
310
+ )
311
+ sys.exit(1)
312
+
313
+ if check_git_status_output() == "":
314
+ self.run_step(step + 1)
315
+ else:
316
+ check_call(["git", "status"])
317
+ self.print_step(
318
+ step + 1,
319
+ message="Here is the output of 'git status'. Please make sure to commit all your "
320
+ "changes before going further. All uncommitted changes will be lost.\n"
321
+ "Note that for debugging purpose it is possible to pass directly to step 2 "
322
+ f"e.-g.: ./upgrade --debug=../c2cgeoportal {os.environ['VERSION']} 2",
323
+ )
324
+
325
+ @Step(1)
326
+ def step1(self, step: int) -> None:
327
+ shutil.copyfile("project.yaml", "/tmp/project.yaml")
328
+ try:
329
+ check_call(["git", "reset", "--hard"])
330
+ check_call(["git", "clean", "--force", "-d"])
331
+ finally:
332
+ shutil.copyfile("/tmp/project.yaml", "project.yaml")
333
+
334
+ self.run_step(step + 1)
335
+
336
+ @Step(2)
337
+ def step2(self, step: int) -> None:
338
+ fix_style()
339
+ subprocess.run(["git", "add", "-A"]) # pylint: disable=subprocess-run-check
340
+ subprocess.run(["git", "commit", "--message=Run code style"]) # pylint: disable=subprocess-run-check
341
+
342
+ self.run_step(step + 1)
343
+
344
+ @Step(3)
345
+ def step3(self, step: int) -> None:
346
+ project_path = os.path.join("/tmp", self.project["project_folder"])
347
+ os.mkdir(project_path)
348
+ shutil.copyfile("/src/project.yaml", os.path.join(project_path, "project.yaml"))
349
+ check_call(
350
+ [
351
+ "pcreate",
352
+ "--overwrite",
353
+ "--scaffold=update",
354
+ project_path,
355
+ ]
356
+ )
357
+ if self.get_project().get("advance", False):
358
+ check_call(
359
+ [
360
+ "pcreate",
361
+ "--overwrite",
362
+ "--scaffold=advance_update",
363
+ project_path,
364
+ ]
365
+ )
366
+
367
+ shutil.copyfile(os.path.join(project_path, ".upgrade.yaml"), ".upgrade.yaml")
368
+ for upgrade_file in cast(list[dict[str, Any]], self.get_upgrade("upgrade_files")):
369
+ action = upgrade_file["action"]
370
+ if action == "remove":
371
+ self.files_to_remove(upgrade_file, prefix="CONST_create_template", force=True)
372
+ if action == "move":
373
+ self.files_to_move(upgrade_file, prefix="CONST_create_template", force=True)
374
+
375
+ shutil.rmtree(project_path)
376
+ os.remove(".upgrade.yaml")
377
+
378
+ check_call(["git", "add", "--all", "--force", "CONST_create_template/"])
379
+ call(["git", "commit", "--message=Perform the move into the CONST_create_template folder"])
380
+
381
+ self.run_step(step + 1)
382
+
383
+ @Step(4)
384
+ def step4(self, step: int) -> None:
385
+ if os.path.exists("CONST_create_template"):
386
+ check_call(["git", "rm", "-r", "--force", "CONST_create_template/"])
387
+
388
+ project_path = os.path.join("/tmp", self.project["project_folder"])
389
+ check_call(["ln", "-s", "/src", project_path])
390
+ check_call(
391
+ [
392
+ "pcreate",
393
+ "--overwrite",
394
+ "--scaffold=update",
395
+ project_path,
396
+ ]
397
+ )
398
+ if self.get_project().get("advance", False):
399
+ check_call(
400
+ [
401
+ "pcreate",
402
+ "--overwrite",
403
+ "--scaffold=advance_update",
404
+ project_path,
405
+ ]
406
+ )
407
+ os.remove(project_path)
408
+
409
+ check_call(["git", "add", "--all", "CONST_create_template/"])
410
+
411
+ def changed_files() -> list[str]:
412
+ try:
413
+ status = [
414
+ [s for s in status.strip().split(" ", maxsplit=1) if s]
415
+ for status in check_git_status_output().strip().split("\n")
416
+ if status
417
+ ]
418
+ return [
419
+ file.strip().split(" ")[-1]
420
+ for state, file in status
421
+ if state == "M" and not file.strip().startswith("CONST_")
422
+ ]
423
+ except: # pylint: disable=bare-except
424
+ self.print_step(
425
+ step,
426
+ error=True,
427
+ message=f"Error while getting changed files:\n{check_git_status_output()}",
428
+ prompt="Fix the error and run the step again:",
429
+ )
430
+ sys.exit(1)
431
+
432
+ changed_before_style = changed_files()
433
+
434
+ fix_style()
435
+
436
+ # Revert code style changes in the project otherwise we get an error: does not match index
437
+ # on git apply.
438
+ changed_after_style = changed_files()
439
+ to_checkout = [file for file in changed_after_style if file not in changed_before_style]
440
+ if to_checkout:
441
+ subprocess.run(["git", "checkout"] + to_checkout, check=True)
442
+
443
+ check_call(["git", "add", "--all", "CONST_create_template/"])
444
+ check_call(["git", "clean", "-Xf", "CONST_create_template/"])
445
+ self.run_step(step + 1)
446
+
447
+ @Step(5)
448
+ def step5(self, step: int) -> None:
449
+ if "managed_files" not in self.project:
450
+ unmanaged_files = "\n".join(["- " + e for e in self.project.get("unmanaged_files", [])])
451
+ self.print_step(
452
+ step,
453
+ message="In the new version, we will also manage almost all the create template files.\n"
454
+ "By default, files conforming to the following regex pattern will not be replaced:\n"
455
+ f"{unmanaged_files} Therefore, you should fill the 'managed_files' in you 'project.yaml' "
456
+ "file with at least `[]`.",
457
+ prompt="Fill it and run the step again:",
458
+ )
459
+ else:
460
+ self.run_step(step + 1)
461
+
462
+ @Step(6)
463
+ def step6(self, step: int) -> None:
464
+ task_to_do = False
465
+ for upgrade_file in cast(list[dict[str, Any]], self.get_upgrade("upgrade_files")):
466
+ action = upgrade_file["action"]
467
+ if action == "remove":
468
+ task_to_do |= self.files_to_remove(upgrade_file)
469
+ elif action == "move":
470
+ task_to_do |= self.files_to_move(upgrade_file)
471
+
472
+ if task_to_do:
473
+ self.print_step(
474
+ step + 1,
475
+ message="""Some `managed_files` or `unmanaged_files` should be updated,
476
+ see message above red and yellow messages to know what should be changed.
477
+ If there is some false positive you should manually revert the changes and
478
+ in the (un)managed files replace the pattern by:
479
+
480
+ - pattern: <pattern>
481
+ no_touch: True
482
+ """,
483
+ )
484
+ else:
485
+ self.run_step(step + 1)
486
+
487
+ def files_to_remove(self, element: dict[str, Any], prefix: str = "", force: bool = False) -> bool:
488
+ task_to_do = False
489
+ for path in element["paths"]:
490
+ file_ = os.path.join(prefix, path.format(package=self.project["project_package"]))
491
+ if os.path.exists(file_):
492
+ managed = False
493
+ if not force:
494
+ for files in self.project["managed_files"]:
495
+ if isinstance(files, str):
496
+ pattern = files
497
+ no_touch = False
498
+ else:
499
+ pattern = files["pattern"]
500
+ no_touch = files.get("no_touch", False)
501
+ if re.match(pattern + "$", file_):
502
+ if no_touch:
503
+ managed = True
504
+ else:
505
+ print(
506
+ colorize(
507
+ f"The file '{file_}' has been removed but he is in the "
508
+ f"`managed_files` as '{pattern}'.",
509
+ Color.RED,
510
+ )
511
+ )
512
+ task_to_do = True
513
+ for pattern in self.project.get("unmanaged_files", []):
514
+ if re.match(pattern + "$", file_):
515
+ print(
516
+ colorize(
517
+ f"The file '{file_}' has been removed but he is in the "
518
+ f"`unmanaged_files` as '{pattern}'.",
519
+ Color.YELLOW,
520
+ )
521
+ )
522
+ task_to_do = True
523
+ if not managed:
524
+ print(f"The file '{file_}' is removed.")
525
+ if "version" in element and "from" in element:
526
+ print(
527
+ f"Was used in version {element['from']}, to be removed from version "
528
+ f"{element['version']}."
529
+ )
530
+ if os.path.isdir(file_):
531
+ shutil.rmtree(file_)
532
+ else:
533
+ os.remove(file_)
534
+ return task_to_do
535
+
536
+ def files_to_move(self, element: dict[str, Any], prefix: str = "", force: bool = False) -> bool:
537
+ task_to_do = False
538
+ src = os.path.join(prefix, element["from"].format(package=self.project["project_package"]))
539
+ dst = os.path.join(prefix, element["to"].format(package=self.project["project_package"]))
540
+ if os.path.exists(src):
541
+ managed = False
542
+ type_ = "directory" if os.path.isdir(src) else "file"
543
+ if not force:
544
+ for files in self.project["managed_files"]:
545
+ if isinstance(files, str):
546
+ pattern = files
547
+ no_touch = False
548
+ else:
549
+ pattern = files["pattern"]
550
+ no_touch = files.get("no_touch", False)
551
+ if re.match(pattern + "$", src):
552
+ if no_touch:
553
+ managed = True
554
+ else:
555
+ print(
556
+ colorize(
557
+ f"The {type_} '{src}' is present in the `managed_files` as '{pattern}', "
558
+ f"but it has been moved to '{dst}'.",
559
+ Color.RED,
560
+ )
561
+ )
562
+ task_to_do = True
563
+ if re.match(pattern + "$", dst):
564
+ print(
565
+ colorize(
566
+ f"The {type_} '{dst}' is present in the `managed_files` as '{pattern}', "
567
+ f"but a file have been moved on it from '{src}'.",
568
+ Color.RED,
569
+ )
570
+ )
571
+ task_to_do = True
572
+ for pattern in self.project["unmanaged_files"]:
573
+ if re.match(pattern + "$", src):
574
+ print(
575
+ colorize(
576
+ f"The {type_} '{src}' is present in the `unmanaged_files` as '{pattern}', "
577
+ f"but it has been moved to '{dst}'.",
578
+ Color.YELLOW,
579
+ )
580
+ )
581
+ task_to_do = True
582
+ if re.match(pattern + "$", dst):
583
+ print(
584
+ colorize(
585
+ f"The {type_} '{dst}' is present in the `unmanaged_files` as '{pattern}', "
586
+ f"but a file have been moved on it from '{src}'.",
587
+ Color.YELLOW,
588
+ )
589
+ )
590
+ task_to_do = True
591
+ if not managed and os.path.exists(dst) and not element.get("override", False):
592
+ print(colorize(f"The destination '{dst}' already exists, ignoring.", Color.YELLOW))
593
+ elif not managed:
594
+ print(f"Move the {type_} '{src}' to '{dst}'.")
595
+ if "version" in element:
596
+ print(f"Needed from version {element['version']}.")
597
+ if os.path.dirname(dst) != "":
598
+ os.makedirs(os.path.dirname(dst), exist_ok=True)
599
+ try:
600
+ check_call(["git", "mv", src, dst])
601
+ except Exception as exception: # pylint: disable=broad-exception-caught
602
+ print(f"[Warning] Git move error: {exception}.")
603
+ os.rename(src, dst)
604
+ return task_to_do
605
+
606
+ @Step(7)
607
+ def step7(self, step: int) -> None:
608
+ self.files_to_get(step)
609
+ self.run_step(step + 1)
610
+
611
+ def is_managed(self, file_: str, files_to_get: bool = False) -> bool:
612
+ # Dictionary with:
613
+ # include: list of include regular expression
614
+ # exclude: list of exclude regular expression
615
+ default_project_file = cast(dict[str, list[str]], self.get_upgrade("default_project_file"))
616
+
617
+ # Managed means managed by the application owner, not the c2cupgrade
618
+ managed = False
619
+ if (
620
+ not files_to_get
621
+ or os.path.exists(file_)
622
+ or not check_git_status_output(["CONST_create_template/" + file_]).startswith("A ")
623
+ ):
624
+ for pattern in default_project_file["include"]:
625
+ if re.match(pattern + "$", file_):
626
+ print(f"File '{file_}' included by migration config pattern '{pattern}'.")
627
+ managed = True
628
+ break
629
+ if managed:
630
+ for pattern in default_project_file["exclude"]:
631
+ if re.match(pattern + "$", file_):
632
+ print(f"File '{file_}' excluded by migration config pattern '{pattern}'.")
633
+ print("managed", file_, pattern)
634
+ managed = False
635
+ break
636
+ else:
637
+ print(f"New file '{file_}'.")
638
+
639
+ if not managed and not os.path.exists(file_):
640
+ for pattern in self.get_upgrade("extra"):
641
+ if re.match(pattern + "$", file_):
642
+ print(f"File '{file_}' is an extra by migration config pattern '{pattern}'.")
643
+ managed = True
644
+
645
+ if not managed:
646
+ for files in self.project["managed_files"]:
647
+ if isinstance(files, str):
648
+ pattern = files
649
+ else:
650
+ pattern = files["pattern"]
651
+ if re.match(pattern + "$", file_):
652
+ print(f"File '{file_}' included by project config pattern `managed_files` '{pattern}'.")
653
+ print("managed", file_, pattern)
654
+ managed = True
655
+ break
656
+ if managed:
657
+ for pattern in self.project.get("unmanaged_files", []):
658
+ if re.match(pattern + "$", file_):
659
+ print(f"File '{file_}' excluded by project config pattern `unmanaged_files` '{pattern}'.")
660
+ managed = False
661
+ break
662
+
663
+ return managed
664
+
665
+ def files_to_get(self, step: int, pre: bool = False) -> bool:
666
+ error = False
667
+ for root, _, files in os.walk("CONST_create_template"):
668
+ root = root[len("CONST_create_template/") :]
669
+ for file_ in files:
670
+ destination = os.path.join(root, file_)
671
+ managed = self.is_managed(destination, True)
672
+ source = os.path.join("CONST_create_template", destination)
673
+ if not managed and (not os.path.exists(destination) or not filecmp.cmp(source, destination)):
674
+ print(colorize(f"Get the file '{destination}' from the create template.", Color.GREEN))
675
+ if not pre:
676
+ if os.path.dirname(destination) != "":
677
+ os.makedirs(os.path.dirname(destination), exist_ok=True)
678
+ try:
679
+ shutil.copyfile(source, destination)
680
+ shutil.copymode(source, destination)
681
+ except PermissionError as exception:
682
+ self.print_step(
683
+ step,
684
+ error=True,
685
+ message=(
686
+ "All your project files should be owned by your user, "
687
+ "current error:\n" + str(exception)
688
+ ),
689
+ prompt="Fix it and run the upgrade again:",
690
+ )
691
+ sys.exit(1)
692
+ elif managed:
693
+ print(f"The file '{destination}' is managed by the project.")
694
+ elif os.path.exists(destination) and filecmp.cmp(source, destination):
695
+ print(f"The file '{destination}' does not change.")
696
+ else:
697
+ print(f"Unknown stat for the file '{destination}'.")
698
+ sys.exit(2)
699
+ return error
700
+
701
+ @Step(8)
702
+ def step8(self, step: int) -> None:
703
+ with open("changelog.diff", "w", encoding="utf8") as diff_file:
704
+ check_call(["git", "diff", "--", "CONST_CHANGELOG.txt"], stdout=diff_file)
705
+
706
+ from210 = False
707
+ try:
708
+ check_call(["grep", "--", "-Version 2.1.0", "changelog.diff"])
709
+ from210 = True
710
+ except subprocess.CalledProcessError:
711
+ pass
712
+ if from210:
713
+ check_call(["cp", "CONST_CHANGELOG.txt", "changelog.diff"])
714
+
715
+ if os.path.getsize("changelog.diff") == 0:
716
+ self.run_step(step + 1)
717
+ else:
718
+ self.print_step(
719
+ step + 1,
720
+ message="Apply the manual migration steps based on what is in the CONST_CHANGELOG.txt "
721
+ "file (listed in the `changelog.diff` file).",
722
+ )
723
+
724
+ def get_modified(self, status_path: str) -> list[str]:
725
+ status = check_git_status_output([status_path]).split("\n")
726
+ status = [s for s in status if len(s) > 3]
727
+ status = [s[3:] for s in status if s[:3].strip() == "M"]
728
+ for pattern in self.get_upgrade("no_diff"):
729
+ matcher = re.compile(f"CONST_create_template/{pattern}$")
730
+ status = [s for s in status if not matcher.match(s)]
731
+ status = [s for s in status if os.path.exists(s[len("CONST_create_template/") :])]
732
+ status = [s for s in status if not filecmp.cmp(s, s[len("CONST_create_template/") :])]
733
+ return status
734
+
735
+ @Step(9)
736
+ def step9(self, step: int) -> None:
737
+ if os.path.isfile("changelog.diff"):
738
+ os.unlink("changelog.diff")
739
+
740
+ status = self.get_modified(
741
+ f"CONST_create_template/geoportal/{self.project['project_package']}_geoportal/static-ngeo"
742
+ )
743
+ status += ["CONST_create_template/geoportal/vars.yaml"]
744
+
745
+ with open("ngeo.diff", "w", encoding="utf8") as diff_file:
746
+ if status:
747
+ check_call(
748
+ ["git", "diff", "--relative=CONST_create_template", "--staged", "--"] + status,
749
+ stdout=diff_file,
750
+ )
751
+
752
+ if os.path.getsize("ngeo.diff") == 0:
753
+ self.run_step(step + 1)
754
+ else:
755
+ self.print_step(
756
+ step + 1,
757
+ message="Manually apply the ngeo application changes as shown in the `ngeo.diff` file.\n"
758
+ + DIFF_NOTICE
759
+ + "\nNote that you can also apply them using: git apply --3way ngeo.diff",
760
+ )
761
+
762
+ @Step(10)
763
+ def step10(self, step: int) -> None:
764
+ if os.path.isfile("ngeo.diff"):
765
+ os.unlink("ngeo.diff")
766
+
767
+ status = self.get_modified("CONST_create_template")
768
+ status = [
769
+ s
770
+ for s in status
771
+ if not s.startswith(
772
+ f"CONST_create_template/geoportal/{self.project['project_package']}_geoportal/static-ngeo/"
773
+ )
774
+ ]
775
+ status = [s for s in status if s != "CONST_create_template/geoportal/vars.yaml"]
776
+
777
+ if status:
778
+ with open("create.diff", "w", encoding="utf8") as diff_file:
779
+ if status:
780
+ check_call(
781
+ ["git", "diff", "--relative=CONST_create_template", "--staged", "--"] + status,
782
+ stdout=diff_file,
783
+ )
784
+
785
+ if os.path.getsize("create.diff") == 0:
786
+ self.run_step(step + 1)
787
+ else:
788
+ self.print_step(
789
+ step + 1,
790
+ message="The `create.diff` file is a recommendation of the changes that you "
791
+ "should apply to your project.\n"
792
+ + DIFF_NOTICE
793
+ + "\nNote that you can also apply them using: git apply --3way create.diff",
794
+ )
795
+ else:
796
+ self.run_step(step + 1)
797
+
798
+ @Step(11)
799
+ def step11(self, step: int) -> None:
800
+ if os.path.isfile("create.diff"):
801
+ os.unlink("create.diff")
802
+
803
+ fix_style()
804
+
805
+ message = [
806
+ "The upgrade is nearly done, now you should:",
807
+ "- Build your application with `./upgrade --finalize [build arguments]`",
808
+ f"- Test your application on '{self.project.get('application_url', '... missing ...')}'.",
809
+ ]
810
+
811
+ if os.path.isfile(".upgrade.yaml"):
812
+ os.unlink(".upgrade.yaml")
813
+ with open(".UPGRADE_SUCCESS", "w", encoding="utf8"):
814
+ pass
815
+ self.print_step(step + 1, message="\n".join(message))
816
+
817
+ @Step(12, file_marker=False)
818
+ def step12(self, step: int) -> None:
819
+ if os.path.isfile(".UPGRADE_SUCCESS"):
820
+ os.unlink(".UPGRADE_SUCCESS")
821
+ good, message = self.test_checkers()
822
+ if good:
823
+ self.run_step(step + 1)
824
+ else:
825
+ self.print_step(
826
+ step,
827
+ error=True,
828
+ message=message,
829
+ prompt="Correct the checker, then run the step again "
830
+ "(If you want to fix it later you can pass to the next step:int):",
831
+ )
832
+ sys.exit(1)
833
+
834
+ @Step(13, file_marker=False)
835
+ def step13(self, step: int) -> None:
836
+ # Required to remove from the Git stage the ignored file when we lunch the step again
837
+ check_call(["git", "reset", "--mixed"])
838
+
839
+ check_call(["git", "add", "--all"])
840
+ check_call(["git", "status"])
841
+
842
+ self.print_step(
843
+ step + 1,
844
+ message="We will commit all the above files!\n"
845
+ "If there are some files which should not be committed, then you should "
846
+ f"add them into the `.gitignore` file and launch upgrade {step} again.",
847
+ prompt="Then to commit your changes type:",
848
+ )
849
+
850
+ @Step(14, file_marker=False)
851
+ def step14(self, _: int) -> None:
852
+ if os.path.isfile(".UPGRADE_INSTRUCTIONS"):
853
+ os.unlink(".UPGRADE_INSTRUCTIONS")
854
+ check_call(
855
+ [
856
+ "git",
857
+ "commit",
858
+ "--message=Upgrade to GeoMapFish "
859
+ f"{pkg_resources.get_distribution('c2cgeoportal_commons').version}",
860
+ ]
861
+ )
862
+
863
+ print("")
864
+ print(self.color_bar)
865
+ print("")
866
+ print(colorize("Congratulations, your upgrade was successful.", Color.GREEN))
867
+ print("")
868
+ branch = check_output(["git", "rev-parse", "--abbrev-ref", "HEAD"]).decode("utf-8").strip()
869
+ print("Now all your files are committed; you should do a git push:")
870
+ print(f"git push {self.options.git_remote} {branch}.")
871
+
872
+
873
+ def check_git_status_output(args: list[str] | None = None) -> str:
874
+ """Check if there is something that's not committed."""
875
+ return check_output(["git", "status", "--short"] + (args if args is not None else [])).decode("utf-8")
876
+
877
+
878
+ if __name__ == "__main__":
879
+ main()