This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository.
DicoGIS scans a folder tree (and/or PostGIS databases via pg_service.conf) for
geospatial datasets, extracts their metadata (CRS, geometry type, fields, extent,
size, etc.) using GDAL/OGR, and exports the result as an Excel workbook (.xlsx)
or JSON (optionally in "udata" flavor for publishing to a uData catalog). It ships
both a PyQt6 desktop GUI (dicogis-gui) and a Typer-based CLI (dicogis-cli),
distributed as PyInstaller executables for Windows/Linux.
GDAL system libraries must be installed before the Python gdal package (version
must match gdal-config --version). See docs/development/ubuntu.md /
windows.md for OS-specific system requirements (incl. python3-tk).
python -m pip install -U pip setuptools wheel
python -m pip install -U gdal=="$(gdal-config --version).*"
python -m pip install -U -e .[gdal,dev,gui,test]
pre-commit installdicogis-cli --help
dicogis-cli inventory --input-folder ./tests/fixtures --language EN
dicogis-guiTests need fixture data and a PostGIS instance (see docs/development/tests.md):
git clone --depth=1 https://github.com/qgis/QGIS-Training-Data.git ./tests/fixtures/qgisdata
python -m pip install -U gisdata -t ./tests/fixtures
docker compose -f "tests/container/docker-compose.dev.yml" up -d --buildRun the full suite (config lives in [tool.pytest.ini_options] in pyproject.toml;
coverage is enabled by default via addopts):
pytestRun a single test file or test:
pytest tests/test_utils_formatters.py
pytest tests/test_georeader_postgis.py::test_name -vtests/dev/ and tests/_wip/ are excluded from collection (dev scratch scripts,
not part of the suite). PostGIS-related tests expect
PGSERVICEFILE=./tests/fixtures/database/pg_service.conf.
Formatting/linting is enforced through pre-commit (ruff, ruff-format, black, isort, pyupgrade, flake8 syntax-only check). Run everything pre-commit would run:
pre-commit run --all-filesCI's dedicated lint job only checks for syntax errors/undefined names:
flake8 dicogis --select=E9,F63,F7,F82Line length: 88 (black/ruff-format/isort), docstrings are Google-style.
dicogis-cli (dicogis/cli/main.py, a Typer app with inventory and publish
subcommands) and dicogis-gui (dicogis/ui/main.py, PyQt6) are thin wrappers
around the same core pipeline in dicogis/georeaders, dicogis/listing, and
dicogis/export. When changing core processing logic, check both entry points
for how they call into it — the GUI additionally wires up progress bars/counters
and runs long-running work in QThread workers (dicogis/ui/workers.py) that
the CLI passes as None.
GDAL is an optional extra (pip install dicogis[gdal]), so dicogis-cli/
dicogis-gui must stay importable without it (e.g. installed via pipx on a
system without GDAL): dicogis/cli/cmd_inventory.py and dicogis/ui/main.py
guard the GDAL-dependent imports (ProcessingFiles, ReadPostGIS, DicoGIS)
behind GDAL_IS_AVAILABLE (dicogis/utils/environment.py) and defer them into
the function/entry point body rather than importing eagerly at module scope —
keep new GDAL-dependent imports deferred the same way. See
docs/usage/installation.md for the pipx/GDAL install story.
PyQt6 is likewise an optional dependency (the gui extra) — dicogis-cli
does not import anything from dicogis/ui, so it works fully without it.
dicogis-gui (dicogis/ui/main.py::dicogis_gui()) imports PyQt6.QtWidgets
lazily and exits with a clear message if it's missing; dicogis/ui/__init__.py
is kept empty so importing dicogis.ui/dicogis.ui.main doesn't pull in PyQt6
before that guard runs (see tests/test_ui_optional_pyqt6.py).
GUI widgets are defined as Qt Designer .ui files, loaded dynamically at
runtime with PyQt6.uic.loadUi() — not built up imperatively in Python. Each
widget/dialog module has a matching .ui file of the same stem
(dicogis/ui/wdg_tab_files.py + dicogis/ui/wdg_tab_files.ui), loaded via
Utilities().resolve_internal_path() (so it resolves both from source and
from a frozen PyInstaller build) rather than Path(__file__).parent. Widgets
named in the .ui file (objectName) become attributes on self after
loadUi() — dynamic content (translated strings, values only known at
runtime, nested custom widgets) is still applied/composed in Python after the
load call.
File naming follows a prefix convention:
dlg_—QDialogsubclasses (dicogis/ui/dialogs/dlg_database_connection.py)wdg_—QWidgetsubclasses, including notebook tab pages (wdg_tab_files.py,wdg_collapsible_frame.py,wdg_scrollable_table.py, …)mw_— theQMainWindow(dicogis/ui/mw_dicogis.py)
Plain, unprefixed modules (workers.py, main.py) hold non-widget code
(QThread workers, the app entry point) and have no .ui counterpart.
- Listing (
dicogis/listing/geodata_listing.py) —find_geodata_files()walks a folder tree and buckets file paths by format (shapefiles, MapInfo TAB, KML, GML, GeoJSON, GXT, rasters/GeoTIFF, CAD/DXF, file geodatabases: Esri FileGDB / SpatiaLite / GeoPackage).check_usable_pg_services()cross-checks requested PostgreSQL service names againstpg_service.confviapgserviceparser. - Reading (
dicogis/georeaders/) —GeoReaderBase(base_georeader.py) holds shared GDAL/OGR setup (error handling viaGdalErrorHandler, CRS/SRS introspection, extent, field listing, dependency-file discovery, dataset size). Format-specific readers (read_vector_flat_dataset.py,read_vector_flat_geodatabase.py,read_raster.py,read_dxf.py,read_postgis.py) subclass it and populate aMetaDataset(dicogis/models/metadataset.py) dataclass — the canonical in-memory representation of one dataset's metadata. - Orchestration (
dicogis/georeaders/process_files.py) —ProcessingFilesmaps each supported format to its reader class viaMATRIX_FORMAT_GEOREADER, iterates the file lists produced by the listing step, wraps each in aDatasetToProcess, invokes the matching reader, and feeds the resultingMetaDatasetinto a serializer.opt_analyze_*flags (derived from the--formats/GUI checkboxes) gate which formats actually get processed;opt_quick_failcontrols whether errors abort the run or get recorded per-dataset and skipped. - Serialization (
dicogis/export/) —MetadatasetSerializerBase(base_serializer.py) definespre_serializing()/serialize_metadaset()/post_serializing()and a factory,get_serializer_from_parameters(), that picksMetadatasetSerializerXlsx(to_xlsx.py) orMetadatasetSerializerJson(to_json.py, with aflavorof"dicogis"or"udata") based onOutputFormats. Add a new output format here plus indicogis/constants.py::OutputFormats.
Adding a new file format means: add it to dicogis/constants.py
(FormatsVector/FormatsRaster/SUPPORTED_FORMATS), extend
find_geodata_files() to detect it, register it (and its opt_analyze_* flag)
in ProcessingFiles.MATRIX_FORMAT_GEOREADER, and reuse or extend an existing
GeoReaderBase subclass.
Independent of the file pipeline: ReadPostGIS (read_postgis.py) connects using
a named pg_service (via GDAL's PG:service=... + GDAL_POSTGIS_OPEN_OPTIONS
from constants.py), iterates OGR layers, and serializes each with the same
serializer interface used by files. dicogis/models/database_connection.py holds
connection state/errors.
Two separate mechanisms cover two separate concerns, split by whether GUI (PyQt6) is involved:
-
CLI + shared processing pipeline (
dicogis/export,dicogis/georeaders, and anything reachable fromdicogis-cli):dicogis/utils/texts.py::TextsManagerloads strings fromdicogis/locale/lang_{EN,ES,FR}.xmlkeyed byAvailableLocales(dicogis/constants.py) into alocalized_stringsdict, passed down explicitly through the processing pipeline (e.g. toProcessingFiles,MetadatasetSerializerBase). This has no PyQt6 dependency, matching the CLI's own requirement to run without theguiextra installed. New pipeline-output strings (Excel/JSON field labels, error messages) should go through this mechanism rather than being hardcoded. -
GUI widget text (
dicogis/ui/*, including dialogs): plain Qt native i18n —self.tr("English source text")calls, extracted intodicogis/ui/i18n/dicogis_{en,fr,es}.tswithpylupdate6, translated in Qt Linguist (or by hand), compiled to.qmwithlrelease, and loaded via aQTranslatorinstalled on theQApplication(DicoGIS._install_qt_translator()inmw_dicogis.py, called fromretranslate_ui()before anyself.tr()call, including the one done at startup and on every language-dropdown change). New widget-visible text should useself.tr(...)— neverTextsManager— and the.tsfiles regenerated with:pylupdate6 dicogis/ui/*.py dicogis/ui/dialogs/*.py --ts dicogis/ui/i18n/dicogis_en.ts # repeat for _fr.ts / _es.ts, fill in <translation> entries, then compile: lrelease dicogis/ui/i18n/dicogis_fr.ts -qm dicogis/ui/i18n/dicogis_fr.qm lrelease dicogis/ui/i18n/dicogis_es.ts -qm dicogis/ui/i18n/dicogis_es.qm
dicogis_en.tsis kept as the source-of-truth reference (never compiled/loaded at runtime: English is the source language, so untranslatedself.tr()calls already return it with no.qminstalled). Only.qmfiles ship with the package/frozen executables (pyproject.tomlpackage-data, PyInstaller GUI builder scripts'--add-data), not the.tssources.
journalizer.py::LogManager— sets up console + rotating file logging under the platform app dir (typer.get_app_dir).options.py/db_conf_reader.py— read/write theoptions.iniconfig (template:options_TPL.ini) andpg_service.confentries.formatters.py,slugger.py,check_path.py,str2bound.py,checknorris.py— small pure helpers (size formatting, slugifying, path validation) used across readers/exporters; covered directly by unit tests namedtest_utils_*.py.notifier.py— cross-platform desktop notification (notify-py) at the end of a run.
builder/ contains the PyInstaller specs/scripts for Windows and Ubuntu, for
both the CLI and GUI executables, plus a Windows version-info templater. CI's
builder_releaser.yml workflow builds and publishes these. Version is
single-sourced from dicogis/__about__.py::__version__ (pyproject.toml reads
it via [tool.setuptools.dynamic]).
A root-level Dockerfile (multi-stage, based on the official
ghcr.io/osgeo/gdal:ubuntu-small-* image so GDAL's Python bindings are
already present and version-matched — no gdal extra to compile) packages
dicogis-cli only, entrypoint dicogis-cli. Runtime dependencies are
installed with exact == pins (the versions pyproject.toml's ranges
currently resolve to) before pip install --no-deps ., so the image is
reproducible instead of re-resolving ranges on every build — regenerate the
pins by rebuilding the image and copying the versions pip resolves. CI's
docker_builder.yml workflow builds it, smoke-tests --version/--help,
and pushes to ghcr.io/guts/dicogis (tags: X.Y.Z/X.Y on releases, edge
on master) via docker/build-push-action (pinned to a commit SHA, like the
other third-party actions it uses).
Full guidelines: CONTRIBUTING.md. What matters most for changes made here:
- Commit messages follow Conventional Commits:
<type>(<scope>): <description>(scope optional), e.g.fix(cli,gui): defer GDAL imports so dicogis-cli/gui work without it. Common types:feat,fix,docs,test,refactor,chore,ci,build. - Branch names are prefixed to drive auto-labeling and release-notes
categorization (
.github/labeler.yml/.github/release.yml):fix/…orhotfix/…(bug fix),feature/…orimprove/…(enhancement),docs/…,packaging/…,tooling/…,cli/…. - Pull requests should fill out
.github/PULL_REQUEST_TEMPLATE.md(auto-filled on GitHub) — including the "Type of change" checklist and confirmingpre-commit run --all-files/pytestwere run where relevant. - AI/LLM disclosure is required: any PR/commit produced with AI assistance
must disclose the transparency level (per
VisiData's scale, summarized as a
table in
CONTRIBUTING.md's "AI/LLM-assisted contributions" section), the model used, and a link to the session — in the PR description's "AI/LLM disclosure" section and/or as commit trailers (AI-Level:,AI-Model:,Co-Authored-By:,Claude-Session:— see recent commit history for the exact trailer format). A human must still have reviewed and tested the change themselves, whatever the level.
- Files start with the shebang comment
#! python3 # noqa: E265and use the# ###...section-banner comment style (Libraries / Globals / Classes / Functions) seen throughoutdicogis/; new files should follow the same layout for consistency. - Google-style docstrings.
- GDAL/OGR objects require
gdal.UseExceptions()/ogr.UseExceptions()(set up inGeoReaderBase.__init__) — don't re-open datasets without going through aGeoReaderBasesubclass, since error handling and open-flag/option logic lives there. - Prefer the standard library over new third-party dependencies where practical
(see
CONTRIBUTING.md"Security" section) given the project targets large-scale IT infrastructures. - GUI widgets:
.uifiles loaded withuic.loadUi(), not built up in Python — see "GUI widgets:.uifiles + naming convention" above.