dicogis 4.0.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.
Files changed (88) hide show
  1. dicogis/__about__.py +55 -0
  2. dicogis/__init__.py +0 -0
  3. dicogis/bin/audio/notification_sound_microwave_bell.wav +0 -0
  4. dicogis/bin/img/DicoGIS.ico +0 -0
  5. dicogis/bin/img/DicoGIS_logo.gif +0 -0
  6. dicogis/bin/img/DicoGIS_logo_200px.png +0 -0
  7. dicogis/cli/__init__.py +0 -0
  8. dicogis/cli/cmd_inventory.py +500 -0
  9. dicogis/cli/cmd_publish.py +170 -0
  10. dicogis/cli/main.py +94 -0
  11. dicogis/constants.py +113 -0
  12. dicogis/export/__init__.py +0 -0
  13. dicogis/export/base_serializer.py +128 -0
  14. dicogis/export/output_path.py +100 -0
  15. dicogis/export/to_json.py +165 -0
  16. dicogis/export/to_xlsx.py +1046 -0
  17. dicogis/georeaders/__init__.py +0 -0
  18. dicogis/georeaders/base_georeader.py +433 -0
  19. dicogis/georeaders/gdal_exceptions_handler.py +68 -0
  20. dicogis/georeaders/process_files.py +529 -0
  21. dicogis/georeaders/read_dxf.py +44 -0
  22. dicogis/georeaders/read_postgis.py +316 -0
  23. dicogis/georeaders/read_raster.py +152 -0
  24. dicogis/georeaders/read_vector_flat_dataset.py +238 -0
  25. dicogis/georeaders/read_vector_flat_geodatabase.py +67 -0
  26. dicogis/listing/__init__.py +0 -0
  27. dicogis/listing/geodata_listing.py +412 -0
  28. dicogis/locale/lang_EN.xml +75 -0
  29. dicogis/locale/lang_ES.xml +75 -0
  30. dicogis/locale/lang_FR.xml +75 -0
  31. dicogis/models/__init__.py +0 -0
  32. dicogis/models/data_format.py +45 -0
  33. dicogis/models/database_connection.py +162 -0
  34. dicogis/models/feature_attributes.py +72 -0
  35. dicogis/models/metadataset.py +377 -0
  36. dicogis/publishing/__init__.py +0 -0
  37. dicogis/publishing/udata.py +284 -0
  38. dicogis/ui/__init__.py +7 -0
  39. dicogis/ui/dialogs/__init__.py +1 -0
  40. dicogis/ui/dialogs/dlg_database_connection.py +102 -0
  41. dicogis/ui/dialogs/dlg_database_connection.ui +97 -0
  42. dicogis/ui/i18n/dicogis_es.qm +0 -0
  43. dicogis/ui/i18n/dicogis_fr.qm +0 -0
  44. dicogis/ui/main.py +154 -0
  45. dicogis/ui/mw_dicogis.py +1079 -0
  46. dicogis/ui/mw_dicogis.ui +209 -0
  47. dicogis/ui/qt_compat/QtCore.py +13 -0
  48. dicogis/ui/qt_compat/QtGui.py +13 -0
  49. dicogis/ui/qt_compat/QtWidgets.py +13 -0
  50. dicogis/ui/qt_compat/__init__.py +95 -0
  51. dicogis/ui/qt_compat/uic.py +12 -0
  52. dicogis/ui/wdg_misc_buttons.py +124 -0
  53. dicogis/ui/wdg_misc_buttons.ui +69 -0
  54. dicogis/ui/wdg_scrollable_table.py +52 -0
  55. dicogis/ui/wdg_scrollable_table.ui +59 -0
  56. dicogis/ui/wdg_tab_credits.py +87 -0
  57. dicogis/ui/wdg_tab_credits.ui +132 -0
  58. dicogis/ui/wdg_tab_database.py +147 -0
  59. dicogis/ui/wdg_tab_database.ui +73 -0
  60. dicogis/ui/wdg_tab_files.py +242 -0
  61. dicogis/ui/wdg_tab_files.ui +212 -0
  62. dicogis/ui/wdg_tab_publish.py +212 -0
  63. dicogis/ui/wdg_tab_publish.ui +126 -0
  64. dicogis/ui/wdg_tab_settings.py +295 -0
  65. dicogis/ui/wdg_tab_settings.ui +303 -0
  66. dicogis/ui/workers.py +295 -0
  67. dicogis/utils/__init__.py +0 -0
  68. dicogis/utils/app_dir.py +45 -0
  69. dicogis/utils/check_path.py +324 -0
  70. dicogis/utils/checknorris.py +97 -0
  71. dicogis/utils/db_conf_reader.py +96 -0
  72. dicogis/utils/environment.py +222 -0
  73. dicogis/utils/formatters.py +75 -0
  74. dicogis/utils/journalizer.py +146 -0
  75. dicogis/utils/notifier.py +127 -0
  76. dicogis/utils/options.py +256 -0
  77. dicogis/utils/paths.py +33 -0
  78. dicogis/utils/progress.py +83 -0
  79. dicogis/utils/slugger.py +71 -0
  80. dicogis/utils/str2bool.py +65 -0
  81. dicogis/utils/texts.py +142 -0
  82. dicogis/utils/utils.py +66 -0
  83. dicogis-4.0.1.dist-info/METADATA +95 -0
  84. dicogis-4.0.1.dist-info/RECORD +88 -0
  85. dicogis-4.0.1.dist-info/WHEEL +5 -0
  86. dicogis-4.0.1.dist-info/entry_points.txt +5 -0
  87. dicogis-4.0.1.dist-info/licenses/LICENSE +201 -0
  88. dicogis-4.0.1.dist-info/top_level.txt +1 -0
dicogis/__about__.py ADDED
@@ -0,0 +1,55 @@
1
+ #! python3 # noqa: E265
2
+
3
+ """
4
+ Metadata about the package to easily retrieve informations about it.
5
+
6
+ See: https://packaging.python.org/guides/single-sourcing-package-version/
7
+ """
8
+
9
+ # standard lib
10
+ from datetime import date
11
+ from pathlib import Path
12
+
13
+
14
+ __all__ = [
15
+ "__author__",
16
+ "__copyright__",
17
+ "__email__",
18
+ "__executable_name__",
19
+ "__license__",
20
+ "__summary__",
21
+ "__title__",
22
+ "__title_clean__",
23
+ "__uri__",
24
+ "__version__",
25
+ "__version_info__",
26
+ ]
27
+
28
+ __author__ = "Julien Moura"
29
+ __copyright__ = f"2014 - {date.today().year}, {__author__}"
30
+ __email__ = "dev@ingeoveritas.com"
31
+ __executable_name__ = "DicoGIS.exe"
32
+ __package_name__ = "dicogis"
33
+ __icon_path__ = Path("bin/img/DicoGIS_logo_200px.png")
34
+ __keywords__ = ["GIS", "metadata", "INSPIRE", "GDAL", "OGR", "data management"]
35
+ __license__ = "Apache-2.0"
36
+ __notification_sound_path__ = Path("bin/audio/notification_sound_microwave_bell.wav")
37
+ __summary__ = (
38
+ "Create Excel spreadsheet describing geographical data from a PostGIS "
39
+ "Database or a file tree structure."
40
+ )
41
+ __title__ = "DicoGIS"
42
+ __title_clean__ = "".join(e for e in __title__ if e.isalnum())
43
+ __uri_homepage__ = "https://guts.github.io/DicoGIS/"
44
+ __uri_repository__ = "https://github.com/Guts/DicoGIS/"
45
+ __uri_tracker__ = f"{__uri_repository__}issues/"
46
+ __uri__ = __uri_repository__
47
+
48
+
49
+ __version__ = "4.0.1"
50
+ __version_info__ = tuple(
51
+ [
52
+ int(num) if num.isdigit() else num
53
+ for num in __version__.replace("-", ".", 1).split(".")
54
+ ]
55
+ )
dicogis/__init__.py ADDED
File without changes
Binary file
Binary file
Binary file
File without changes
@@ -0,0 +1,500 @@
1
+ #! python3 # noqa: E265
2
+
3
+ # ############################################################################
4
+ # ########## Libraries #############
5
+ # ##################################
6
+
7
+ # standard lib
8
+ import logging
9
+ from locale import getlocale
10
+ from pathlib import Path
11
+ from typing import TYPE_CHECKING, Annotated
12
+
13
+ # 3rd party
14
+ import typer
15
+ from rich import print as rich_print
16
+
17
+ # project
18
+ from dicogis.__about__ import __package_name__
19
+ from dicogis.constants import SUPPORTED_FORMATS, AvailableLocales, OutputFormats
20
+ from dicogis.export.base_serializer import MetadatasetSerializerBase
21
+ from dicogis.export.output_path import determine_output_path
22
+ from dicogis.listing.geodata_listing import check_usable_pg_services, find_geodata_files
23
+ from dicogis.utils.app_dir import get_app_dir
24
+ from dicogis.utils.environment import (
25
+ GDAL_IS_AVAILABLE,
26
+ get_available_gdal_drivers,
27
+ is_format_supported_by_gdal,
28
+ )
29
+ from dicogis.utils.journalizer import LogManager
30
+ from dicogis.utils.notifier import send_system_notify
31
+ from dicogis.utils.texts import TextsManager
32
+
33
+
34
+ # GDAL is an optional dependency (see pyproject.toml `gdal` extra): importing the
35
+ # georeaders modules here at module level would break `dicogis-cli` entirely
36
+ # (even --help/--version) when GDAL isn't installed, e.g. under pipx. So the
37
+ # imports below are deferred into `inventory()`, guarded by GDAL_IS_AVAILABLE.
38
+ # Type-checking-only imports are not executed at runtime, so they are safe here.
39
+ if TYPE_CHECKING:
40
+ from dicogis.georeaders.read_postgis import ReadPostGIS
41
+
42
+ # ############################################################################
43
+ # ########## Globals ###############
44
+ # ##################################
45
+
46
+ state = {"verbose": False}
47
+ logger = logging.getLogger(__name__)
48
+ default_formats = ",".join([f.name for f in SUPPORTED_FORMATS])
49
+ # ############################################################################
50
+ # ########## Functions #############
51
+ # ##################################
52
+
53
+
54
+ def serialize_pg_service_layers(
55
+ sgbd_reader: "ReadPostGIS",
56
+ output_serializer: MetadatasetSerializerBase,
57
+ ) -> int:
58
+ """Serialize the metadata of every layer exposed by a connected PostGIS service.
59
+
60
+ Args:
61
+ sgbd_reader: PostGIS reader with an already opened connection
62
+ output_serializer: serializer to write each layer's metadata into
63
+
64
+ Returns:
65
+ number of layers read from this service
66
+ """
67
+ layers_count: int = sgbd_reader.conn.GetLayerCount()
68
+ logger.info(f"{layers_count} tables found in PostGIS database.")
69
+
70
+ for idx_layer in range(layers_count):
71
+ layer = sgbd_reader.conn.GetLayerByIndex(idx_layer)
72
+ metadataset = sgbd_reader.infos_dataset(layer=layer)
73
+ logger.info(f"Table examined: {metadataset.name}")
74
+ output_serializer.serialize_metadaset(metadataset=metadataset)
75
+ logger.debug("Layer metadata stored into workbook.")
76
+
77
+ return layers_count
78
+
79
+
80
+ def inventory(
81
+ input_folder: Annotated[
82
+ Path | None,
83
+ typer.Option(
84
+ dir_okay=True,
85
+ envvar="DICOGIS_START_FOLDER",
86
+ file_okay=False,
87
+ help="Starting folder for listing files. "
88
+ "The folder must exist and be readable. "
89
+ "If None, files listing is ignored. Defaults to None.",
90
+ readable=True,
91
+ resolve_path=True,
92
+ ),
93
+ ] = None,
94
+ formats: Annotated[
95
+ str,
96
+ typer.Option(
97
+ envvar="DICOGIS_FORMATS_LIST",
98
+ help="List of files extensions to include into listing. "
99
+ "Must be part of supported formats. "
100
+ "Defaults to every supported formats.",
101
+ ),
102
+ ] = default_formats,
103
+ pg_services: Annotated[
104
+ list[str] | None,
105
+ typer.Option(
106
+ envvar="DICOGIS_POSTGRES_SERVICES",
107
+ help="name(s) of PostgreSQL services to use. It's a repeatable option. "
108
+ "If None, database listing is ignored. Defaults to None.",
109
+ ),
110
+ ] = None,
111
+ output_path: Annotated[
112
+ Path | None,
113
+ typer.Option(
114
+ dir_okay=True,
115
+ envvar="DICOGIS_OUTPUT_FILEPATH",
116
+ file_okay=True,
117
+ help="Output file (excel) or folder (json, udata). If not set, it's "
118
+ "created in the current working directory (cwd), named after the input "
119
+ "folder or PostgreSQL services and the date.",
120
+ resolve_path=True,
121
+ writable=True,
122
+ ),
123
+ ] = None,
124
+ output_format: Annotated[
125
+ OutputFormats,
126
+ typer.Option(
127
+ case_sensitive=False,
128
+ envvar="DICOGIS_OUTPUT_FORMAT",
129
+ help="Output format: excel for a Microsoft Excel workbook (.xlsx), json "
130
+ "for one JSON file per dataset, udata for JSON files shaped for a uData "
131
+ "catalog (see the publish command).",
132
+ ),
133
+ ] = "excel",
134
+ opt_notify_sound: Annotated[
135
+ bool,
136
+ typer.Option(
137
+ envvar="DICOGIS_ENABLE_NOTIFICATION_SOUND",
138
+ help="Enable/disable notification's sound at the end of processing.",
139
+ ),
140
+ ] = True,
141
+ opt_open_output: Annotated[
142
+ bool,
143
+ typer.Option(
144
+ envvar="DICOGIS_OPEN_OUTPUT",
145
+ help="Enable/disable auto opening output file when processing has finished.",
146
+ ),
147
+ ] = True,
148
+ opt_prettify_size: Annotated[
149
+ bool,
150
+ typer.Option(
151
+ envvar="DICOGIS_EXPORT_SIZE_PRETTIFY",
152
+ help="Enable prettified files size in output. "
153
+ "Example: 1 ko instead of 1024.",
154
+ ),
155
+ ] = False,
156
+ opt_quick_fail: Annotated[
157
+ bool,
158
+ typer.Option(
159
+ envvar="DICOGIS_QUICK_FAIL",
160
+ help="Enable quick fail instead of passing errors. Useful for debug.",
161
+ ),
162
+ ] = False,
163
+ opt_raw_path: Annotated[
164
+ bool,
165
+ typer.Option(
166
+ envvar="DICOGIS_EXPORT_RAW_PATH",
167
+ help="Enable raw path instead of hyperlink in formats which support it.",
168
+ ),
169
+ ] = False,
170
+ opt_parallel_scan: Annotated[
171
+ bool,
172
+ typer.Option(
173
+ envvar="DICOGIS_LISTING_PARALLEL_SCAN",
174
+ help="Scan top-level subfolders in parallel worker threads instead of "
175
+ "a single sequential walk. Only beneficial on high-latency storage "
176
+ "(e.g. network-mounted shares) with a deep folder tree - can be slower "
177
+ "on local/fast storage. Disabled by default.",
178
+ ),
179
+ ] = False,
180
+ listing_max_workers: Annotated[
181
+ int | None,
182
+ typer.Option(
183
+ envvar="DICOGIS_LISTING_MAX_WORKERS",
184
+ help="Maximum worker threads used when --opt-parallel-scan is enabled. "
185
+ "Ignored otherwise. Defaults to ThreadPoolExecutor's own default "
186
+ "(based on CPU count).",
187
+ ),
188
+ ] = None,
189
+ language: Annotated[
190
+ AvailableLocales | None,
191
+ typer.Option(
192
+ case_sensitive=False,
193
+ envvar="DICOGIS_DEFAULT_LANGUAGE",
194
+ help="Force language to use. If not set, the current default locale is used.",
195
+ ),
196
+ ] = None,
197
+ verbose: Annotated[
198
+ bool,
199
+ typer.Option(
200
+ envvar="DICOGIS_DEBUG",
201
+ help="Increase verbosity to show debug logs.",
202
+ ),
203
+ ] = False,
204
+ ):
205
+ """List, extract metadata from geospatial datasets (files or database) and store it
206
+ as files.
207
+
208
+ Make an inventory of geodata files starting from a folder and/or
209
+ databases using connection listed in pg_service.conf and store everything in an
210
+ output file.
211
+ \f
212
+ Args:
213
+ input_folder: starting folder for files. Defaults to None.
214
+ formats: List of files extensions to include into listing. Defaults to every
215
+ supported formats.
216
+ pg_services: name(s) of PostgreSQL services to use. Repeatable. If None,
217
+ database listing is ignored. Defaults to None.
218
+ language: language code to use. If not set, the current default locale is used.
219
+ Defaults to None.
220
+ verbose: enable verbose mode. Defaults to False.
221
+ """
222
+ app_dir = get_app_dir()
223
+ # start logging
224
+ if verbose:
225
+ state["verbose"] = True
226
+
227
+ logmngr = LogManager(
228
+ console_level=logging.DEBUG if verbose else logging.WARNING,
229
+ file_level=logging.DEBUG if verbose else logging.INFO,
230
+ label=f"{__package_name__}-cli-inventory",
231
+ folder=Path(app_dir).joinpath("logs"),
232
+ )
233
+ # add headers
234
+ logmngr.headers()
235
+ logger.debug(f"DicoGIS working folder: {app_dir}")
236
+ logger.debug(
237
+ f"CLI passed parameters: {input_folder=} - {formats=} - {pg_services=} - "
238
+ f"{verbose=} -{language=}"
239
+ )
240
+
241
+ # check minimal parameters
242
+ # note: pg_services defaults to [] not to None
243
+ if input_folder is None and not pg_services:
244
+ rich_print(
245
+ "[bold red]Error: You must provide at least one of the options: "
246
+ "input_folder or pg_services[/bold red]"
247
+ )
248
+ raise typer.Exit(code=1)
249
+
250
+ if not GDAL_IS_AVAILABLE:
251
+ rich_print(
252
+ "[bold red]Error: GDAL (and its Python bindings) is required to run an "
253
+ "inventory but is not installed.[/bold red]\n"
254
+ "See https://guts.github.io/DicoGIS/usage/installation.html for how to "
255
+ "install it, including with pipx."
256
+ )
257
+ raise typer.Exit(code=1)
258
+
259
+ # imported here rather than at module level: GDAL is an optional dependency
260
+ # (see pyproject.toml `gdal` extra), so importing it eagerly would break every
261
+ # dicogis-cli invocation, even --help/--version, when GDAL isn't installed
262
+ from dicogis.georeaders.process_files import ProcessingFiles
263
+ from dicogis.georeaders.read_postgis import ReadPostGIS
264
+
265
+ # drop requested formats the installed GDAL build can't actually read
266
+ # (some drivers, e.g. Geoconcept or ECW, are optional/plugin drivers not
267
+ # included in every GDAL packaging)
268
+ requested_formats = [fmt.strip() for fmt in formats.split(",") if fmt.strip()]
269
+ available_gdal_drivers = get_available_gdal_drivers()
270
+ unsupported_formats = [
271
+ fmt
272
+ for fmt in requested_formats
273
+ if not is_format_supported_by_gdal(fmt, available_gdal_drivers)
274
+ ]
275
+ if unsupported_formats:
276
+ rich_print(
277
+ "[bold yellow]Warning: the installed GDAL build does not support the "
278
+ f"following requested format(s), they will be ignored: "
279
+ f"{', '.join(unsupported_formats)}[/bold yellow]"
280
+ )
281
+ formats = ",".join(
282
+ fmt for fmt in requested_formats if fmt not in unsupported_formats
283
+ )
284
+
285
+ # i18n
286
+ if language is None:
287
+ language = getlocale()
288
+ localized_strings = TextsManager().load_texts(language_code=language)
289
+
290
+ # output format
291
+
292
+ # look for geographic data files
293
+ if input_folder is not None:
294
+ output_path = determine_output_path(
295
+ output_path=output_path,
296
+ output_format=output_format,
297
+ input_folder=input_folder,
298
+ )
299
+
300
+ output_serializer = MetadatasetSerializerBase.get_serializer_from_parameters(
301
+ format_or_serializer=output_format,
302
+ localized_strings=localized_strings,
303
+ output_path=output_path,
304
+ opt_prettify_size=opt_prettify_size,
305
+ opt_raw_path=opt_raw_path,
306
+ )
307
+
308
+ li_vectors = []
309
+ (
310
+ num_folders,
311
+ li_shapefiles,
312
+ li_mapinfo_tab,
313
+ li_kml,
314
+ li_gml,
315
+ li_geojson,
316
+ li_geotiff,
317
+ li_gxt,
318
+ li_raster,
319
+ li_file_database_esri,
320
+ li_dxf,
321
+ li_dwg,
322
+ li_dgn,
323
+ li_cdao,
324
+ li_file_databases,
325
+ li_file_database_spatialite,
326
+ li_file_database_geopackage,
327
+ ) = find_geodata_files(
328
+ start_folder=input_folder,
329
+ parallel_scan=opt_parallel_scan,
330
+ max_workers=listing_max_workers,
331
+ )
332
+
333
+ rich_print(
334
+ "Found: "
335
+ f"{len(li_shapefiles)} shapefiles - "
336
+ f"{len(li_mapinfo_tab)} tables (MapInfo) - "
337
+ f"{len(li_kml)} KML - "
338
+ f"{len(li_gml)} GML - "
339
+ f"{len(li_geojson)} GeoJSON - "
340
+ f"{len(li_gxt)} GXT - "
341
+ f"{len(li_raster)} rasters - "
342
+ f"{len(li_file_databases)} file databases - "
343
+ f"{len(li_cdao)} CAO/DAO - "
344
+ f"in {num_folders}{localized_strings.get('log_numfold')}"
345
+ )
346
+
347
+ # grouping vectors lists
348
+ li_vectors.extend(li_shapefiles)
349
+ li_vectors.extend(li_mapinfo_tab)
350
+ li_vectors.extend(li_kml)
351
+ li_vectors.extend(li_gml)
352
+ li_vectors.extend(li_geojson)
353
+ li_vectors.extend(li_gxt)
354
+
355
+ # check if there are some layers into the folder structure
356
+ if not (
357
+ len(li_vectors) + len(li_raster) + len(li_file_databases) + len(li_cdao)
358
+ ):
359
+ logger.error(localized_strings.get("nodata"))
360
+ raise typer.Exit(1)
361
+
362
+ # instanciate geofiles processor
363
+ geofiles_processor = ProcessingFiles(
364
+ serializer=output_serializer,
365
+ localized_strings=localized_strings,
366
+ # list by tabs
367
+ li_vectors=li_vectors,
368
+ li_rasters=li_raster,
369
+ li_file_databases=li_file_databases,
370
+ li_cdao=li_cdao,
371
+ # list by formats
372
+ li_dxf=li_dxf,
373
+ li_flat_geodatabase_esri_filegdb=li_file_database_esri,
374
+ li_flat_geodatabase_spatialite=li_file_database_spatialite,
375
+ li_flat_geodatabase_geopackage=li_file_database_geopackage,
376
+ li_geojson=li_geojson,
377
+ li_gml=li_gml,
378
+ li_gxt=li_gxt,
379
+ li_kml=li_kml,
380
+ li_mapinfo_tab=li_mapinfo_tab,
381
+ li_shapefiles=li_shapefiles,
382
+ # options
383
+ opt_analyze_cdao="dgn" in formats,
384
+ opt_analyze_esri_filegdb="file_geodatabase_esri" in formats,
385
+ opt_analyze_geopackage="file_geodatabase_geopackage" in formats,
386
+ opt_analyze_geojson="geojson" in formats,
387
+ opt_analyze_gml="gml" in formats,
388
+ opt_analyze_gxt="gxt" in formats,
389
+ opt_analyze_kml="kml" in formats,
390
+ opt_analyze_mapinfo_tab="mapinfo_tab" in formats,
391
+ opt_analyze_raster=any(
392
+ [fmt in formats for fmt in ("ecw", "geotiff", "jpeg")]
393
+ ),
394
+ opt_analyze_shapefiles="esri_shapefile" in formats,
395
+ opt_analyze_spatialite="file_geodatabase_spatialite" in formats,
396
+ # misc
397
+ opt_quick_fail=opt_quick_fail,
398
+ )
399
+
400
+ # sheets and progress bar
401
+ total_files = geofiles_processor.count_files_to_process()
402
+ rich_print(f"Start analyzing {total_files} files...")
403
+
404
+ geofiles_processor.process_datasets_in_queue()
405
+
406
+ send_system_notify(
407
+ notification_title="DicoGIS analysis ended",
408
+ notification_message=f"DicoGIS successfully processed {total_files} files.",
409
+ notification_sound=opt_notify_sound,
410
+ )
411
+ if opt_open_output:
412
+ typer.launch(url=f"{output_path.resolve()}")
413
+
414
+ # look for geographic database
415
+ if pg_services:
416
+ # check if at least one pg_service name is referenced
417
+ pg_services = check_usable_pg_services(requested_pg_services=pg_services)
418
+ if not len(pg_services):
419
+ logger.error("None of the specified pg_services is available.")
420
+ raise typer.Exit(1)
421
+
422
+ output_path = determine_output_path(
423
+ output_path=output_path,
424
+ output_format=output_format,
425
+ pg_services=pg_services,
426
+ )
427
+
428
+ output_serializer = MetadatasetSerializerBase.get_serializer_from_parameters(
429
+ format_or_serializer=output_format,
430
+ localized_strings=localized_strings,
431
+ output_path=output_path,
432
+ opt_prettify_size=opt_prettify_size,
433
+ opt_raw_path=opt_raw_path,
434
+ )
435
+
436
+ rich_print("Start looking for geographic table in PostGIS...")
437
+ # configure output workbook
438
+ output_serializer.pre_serializing(has_sgbd=True)
439
+
440
+ # counted across every service, not just the last one
441
+ total_layers: int = 0
442
+ failed_pg_services: list[str] = []
443
+
444
+ for pg_service in pg_services:
445
+ rich_print(f"Start processing using PostgreSQL service: {pg_service}")
446
+
447
+ # testing connection settings
448
+ sgbd_reader = ReadPostGIS(
449
+ service=pg_service, localized_strings=localized_strings
450
+ )
451
+ sgbd_reader.get_connection()
452
+
453
+ # check connection state
454
+ if sgbd_reader.conn is None:
455
+ fail_reason = sgbd_reader.db_connection.state_msg
456
+ logger.error(
457
+ f"Connection failed using pg_service {pg_service}. Trace: {fail_reason}."
458
+ )
459
+ # the CLI installs no console log handler, so a failed service
460
+ # would otherwise only show up in the log file
461
+ rich_print(
462
+ "[bold yellow]Warning: connection failed using PostgreSQL service "
463
+ f"{pg_service}, it is skipped. Trace: {fail_reason}[/bold yellow]"
464
+ )
465
+ failed_pg_services.append(pg_service)
466
+ continue
467
+
468
+ # show must go on: parse the layers of this service
469
+ total_layers += serialize_pg_service_layers(
470
+ sgbd_reader=sgbd_reader, output_serializer=output_serializer
471
+ )
472
+
473
+ # nothing could be read: fail instead of writing an empty inventory and
474
+ # reporting success, which a calling script would take at face value
475
+ if len(failed_pg_services) == len(pg_services):
476
+ logger.error(
477
+ "Connection failed for every requested PostgreSQL service: "
478
+ f"{', '.join(failed_pg_services)}."
479
+ )
480
+ rich_print(
481
+ "[bold red]Error: connection failed for every requested PostgreSQL "
482
+ f"service ({', '.join(failed_pg_services)}). No output written."
483
+ "[/bold red]"
484
+ )
485
+ raise typer.Exit(code=1)
486
+
487
+ output_serializer.post_serializing()
488
+
489
+ send_system_notify(
490
+ notification_title="DicoGIS analysis ended",
491
+ notification_message="DicoGIS successfully processed "
492
+ f"{total_layers} PostGIS tables. "
493
+ "\nOpen the application to save the workbook.",
494
+ notification_sound=opt_notify_sound,
495
+ )
496
+
497
+ if opt_open_output:
498
+ typer.launch(url=f"{output_path.resolve()}")
499
+
500
+ logger.info(f"Logs stored in {Path(app_dir).joinpath('logs')}")