diff --git a/.editorconfig b/.editorconfig new file mode 100644 index 0000000..f941f5f --- /dev/null +++ b/.editorconfig @@ -0,0 +1,24 @@ +# Jednotné kódování a konce řádků napříč editory. +# Vzor: aiscr-management (quality_baseline/foundations/editorconfig.ini) +root = true + +[*] +charset = utf-8 +end_of_line = lf +insert_final_newline = true +trim_trailing_whitespace = true + +# Zdrojáky bez BOM – pyqgis4-checker na BOM spadne (viz AGENTS.md) +[*.py] +indent_style = space +indent_size = 4 + +[*.md] +trim_trailing_whitespace = false + +# Heslář zapisuje plugin s BOM a CRLF (kvůli Excelu) – needitovat ručně +[amcr_viewer/codelists/heslar.csv] +charset = utf-8-bom +end_of_line = crlf +insert_final_newline = unset +trim_trailing_whitespace = unset diff --git a/.gitattributes b/.gitattributes new file mode 100644 index 0000000..30ca8a4 --- /dev/null +++ b/.gitattributes @@ -0,0 +1,10 @@ +# Textové soubory v repozitáři s LF; vzor: aiscr-management +# (quality_baseline/foundations/gitattributes.fragment) +* text=auto eol=lf + +# Heslář generuje plugin (BOM + CRLF kvůli Excelu) – ukládat bajt po bajtu +amcr_viewer/codelists/heslar.csv -text + +# Binární soubory +*.png binary +*.zip binary diff --git a/.github/dependabot.yml b/.github/dependabot.yml new file mode 100644 index 0000000..30a8535 --- /dev/null +++ b/.github/dependabot.yml @@ -0,0 +1,16 @@ +# Dependabot hlídá jen GitHub Actions: akce jsou ve workflow připnuté +# na SHA s komentářem verze a Dependabot umí obojí povýšit naráz. +# Plugin nemá pip/npm manifest a verze nástrojů (ruff, flake8 …) se +# v workflow drží napevno vědomě – viz AGENTS.md. +# Vzor: aiscr-management/.github/dependabot.yml + +version: 2 +updates: + - package-ecosystem: "github-actions" + directory: "/" + schedule: + interval: "weekly" + open-pull-requests-limit: 5 + target-branch: main + commit-message: + prefix: "ci" diff --git a/.github/pull_request_template.md b/.github/pull_request_template.md index 08dd3d2..2483ef7 100644 --- a/.github/pull_request_template.md +++ b/.github/pull_request_template.md @@ -25,7 +25,7 @@ Nepotřebné body můžeš smazat. Komentáře () se v PR nezobrazuj ## Kontrolní seznam - [ ] Změny jsou v souladu se stylem projektu (viz `AGENTS.md`) -- [ ] Při změně funkcí povýšena verze v `amcr_viewer/metadata.txt` a doplněn `changelog` +- [ ] Při změně funkcí povýšena verze v `amcr_viewer/metadata.txt` (+ `changelog`) a v `CITATION.cff` (`version`, `date-released`) - [ ] Otestováno v QGIS (min. podporovaná verze 3.44) - [ ] PR míří do správné cílové větve - [ ] Větev odpovídá konvenci (`feat/ fix/ docs/ chore/`, AI `agents//`) diff --git a/.github/workflows/code_quality.yml b/.github/workflows/code_quality.yml index dfd7e9a..7cec011 100644 --- a/.github/workflows/code_quality.yml +++ b/.github/workflows/code_quality.yml @@ -28,6 +28,7 @@ env: DETECT_SECRETS: detect-secrets==1.5.0 FLAKE8: flake8==7.3.0 RUFF: ruff==0.16.5 + OPENSPEC: 1.14.0 jobs: # -------------------------------------------------------------------- @@ -74,10 +75,10 @@ jobs: print('detect-secrets: bez nálezů') " - # Na plugins.qgis.org je informativní, tady blokuje – konfigurace - # v amcr_viewer/.flake8 je stejná pro obě místa. + # Na plugins.qgis.org je informativní, tady blokuje. Bez konfigurace, + # tj. se stejnými výchozími pravidly jako scanner. - name: Flake8 - run: flake8 --config amcr_viewer/.flake8 amcr_viewer/ + run: flake8 --isolated amcr_viewer/ # Nad rámec plugins.qgis.org; konfigurace v pyproject.toml - name: Ruff @@ -110,6 +111,24 @@ jobs: exit 1 fi + # -------------------------------------------------------------------- + # OpenSpec – artefakty změn v openspec/ musí projít validací + # -------------------------------------------------------------------- + openspec: + name: OpenSpec + runs-on: ubuntu-latest + + steps: + - name: Checkout code + uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3 + + # Verze CLI napevno – formát validace se mezi verzemi mění. + # Node je v ubuntu-latest předinstalovaný. + - name: openspec validate + run: | + npx --yes @fission-ai/openspec@${{ env.OPENSPEC }} \ + validate --all --strict --no-interactive + # -------------------------------------------------------------------- # 3. Načtení pluginu ve skutečném QGIS, v obou podporovaných verzích # -------------------------------------------------------------------- @@ -147,14 +166,27 @@ jobs: - name: Zip plugin run: zip -r amcr_viewer.zip amcr_viewer -x "*.git*" - # Kontrola obsahu ZIPu. Config soubory pro scanner musí být uvnitř - # vedle metadata.txt, jinak je plugins.qgis.org nenajde – a některé - # nástroje skryté soubory tiše vynechávají. + # Verze v CITATION.cff se povyšuje ručně spolu s metadata.txt a snadno + # se zapomene – tag by pak nesl v citaci jinou verzi než plugin + - name: Verify CITATION.cff version + run: | + plugin=$(sed -n 's/^version=//p' amcr_viewer/metadata.txt \ + | tr -d "\r\"' ") + citace=$(sed -n 's/^version://p' CITATION.cff \ + | tr -d "\r\"' ") + echo "metadata.txt: '$plugin', CITATION.cff: '$citace'" + if [ -z "$plugin" ] || [ "$plugin" != "$citace" ]; then + echo "::error file=CITATION.cff::verze '$citace' neodpovídá" \ + "metadata.txt ('$plugin')" + exit 1 + fi + + # Kontrola obsahu ZIPu: povinné soubory jsou uvnitř, git soubory ne. - name: Verify archive contents run: | unzip -l amcr_viewer.zip for soubor in amcr_viewer/metadata.txt amcr_viewer/__init__.py \ - amcr_viewer/.flake8; do + amcr_viewer/LICENSE; do unzip -l amcr_viewer.zip | grep -qF " $soubor" \ || { echo "::error::v ZIPu chybí $soubor"; exit 1; } done diff --git a/AGENTS.md b/AGENTS.md index fa12dcd..12f4953 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -17,9 +17,36 @@ Zdroj dat: https://digiarchiv.aiscr.cz/ · Nápověda: https://amcr-help.aiscr.c Tento repozitář je jedním ze **sourozeneckých repozitářů** ekosystému AIS CR. Centrální governance a AI konfigurace spravuje hub **`aiscr-management`**; konvence v tomto souboru jsou s tímto vzorem sladěné a zjednodušené pro potřeby jednoho -QGIS pluginu. Těžkou mašinerii hubu (složka `.agents/`, OpenSpec, sync skripty, -multi-assistant generování) tento repozitář **záměrně nepřebírá**. Při širších -otázkách governance má přednost vzor z `aiscr-management`. +QGIS pluginu. Z hubu přebírá **OpenSpec** ve stupni `change-tracked` (viz +níže). Ostatní mašinerii hubu (složka `.agents/`, sync skripty, vlastní +schémata OpenSpec, multi-assistant generování) tento repozitář **záměrně +nepřebírá**. Při širších otázkách governance má přednost vzor +z `aiscr-management`. + +## OpenSpec + +Repozitář používá OpenSpec ve stupni **`change-tracked`**: plánovací +artefakty změn (`proposal.md`, delta spec, `design.md`, `tasks.md`) žijí +v `openspec/changes//`, trvalé specifikace v `openspec/specs/` se +**neudržují**. Stupeň a kontext pro agenty jsou v `openspec/config.yaml`; +změna stupně se dělá vědomě společně s hubem, ne v rámci rozpracované práce. + +- **Kdy založit změnu:** práce, která mění chování (co uživatel vidí, + atributy vrstev, kontrakt s API digiarchivu, uložená nastavení), zasahuje + víc repozitářů nebo mění pravidla / AI konfiguraci / CI. +- **Kdy ne:** překlepy a formátování, bump závislostí či pinů nástrojů bez + změny chování, přegenerování odvozených souborů. +- **Postup:** `openspec new change ` → artefakty → `openspec validate + --strict` → implementace (až na výslovný pokyn) → po merge + `openspec archive --skip-specs` (archiv + `openspec/changes/archive/RRRR-MM-DD-/`). +- Artefakty změny jdou **ve stejném PR** jako implementace; v popisu PR + odkaž na adresář změny. +- Používá se vestavěné schéma `spec-driven`; vlastní schémata hubu se sem + nepřenášejí. CLI: `npx @fission-ai/openspec@1.14.0` (nebo lokálně + nainstalované `openspec`); bez CLI lze artefakty psát i ručně. +- Asistentské povrchy (`.claude/`, `.github/prompts/` …) doručuje sync + z hubu; v tomto repozitáři se ručně nezakládají ani necommitují. ## Struktura repozitáře @@ -33,7 +60,8 @@ amcr_viewer/ # vlastní kód pluginu (toto se balí do releasu) metadata.txt # metadata pluginu + verze + changelog i18n/ # překlady (.ts) *.png # ikony -.github/workflows/ # CI – release pluginu +.github/workflows/ # CI – kontroly kvality a release pluginu +openspec/ # OpenSpec – konfigurace a plánovací artefakty změn README.md # uživatelská dokumentace (anglicky) ``` @@ -140,6 +168,9 @@ flatpak run --command=sh org.qgis.qgis -c \ - Verze pluginu žije v **`amcr_viewer/metadata.txt`** (`version=`). - **Při každé změně chování / nové funkci** povyš verzi a doplň položku do `changelog=` v `metadata.txt` (formát `vX.Y.Z (RRRR-MM-DD)` + odrážky). +- Současně povyš i **`CITATION.cff`** v kořeni repozitáře: `version:` na + stejnou verzi jako v `metadata.txt` a `date-released:` na datum releasu. + Oba soubory musí mít stejnou verzi, než se založí tag. - Datum v changelogu ber z **deterministického zdroje**, ne z paměti, např. `python -c "import datetime; print(datetime.date.today().isoformat())"`. - Release se spouští **pushnutím tagu `vX.Y.Z`**, ne publikací releasu @@ -160,6 +191,7 @@ flatpak run --command=sh org.qgis.qgis -c \ - PR musí mířit do správné `version/v2.x.y` větve. - Před požádáním o review projdi kontrolní seznam v šabloně (zejména bump verze v `metadata.txt`, pokud měníš chování). +- Mění-li PR chování, obsahuje i odpovídající změnu v `openspec/changes/`. - V popisu PR uveď **podíl AI** (např. „text navržen AI, ručně zkontrolováno") a odkaz na související issue, pokud existuje. @@ -189,7 +221,7 @@ tohle: | **Lint a bezpečnost** | `check_sources.py`, bandit, detect-secrets, flake8, ruff | | **Kompatibilita s Qt6** | `pyqgis4-checker` v dockeru | | **Smoke test** | `smoke_test.py` v `qgis/qgis:ltr` i `qgis/qgis:stable` | -| **Balíček pluginu** | sestaví ZIP, ověří obsah, přiloží jako artefakt | +| **Balíček pluginu** | ověří shodu verze v `CITATION.cff` a `metadata.txt`, sestaví ZIP, ověří obsah, přiloží jako artefakt | Smoke test běží v obou podporovaných řadách: `ltr` je QGIS 3.44 na Qt5, `stable` je QGIS 4.x na Qt6. @@ -205,7 +237,7 @@ pip install bandit detect-secrets flake8 ruff python3 tests/check_sources.py bandit -r amcr_viewer/ detect-secrets scan --all-files amcr_viewer/ -flake8 --config amcr_viewer/.flake8 amcr_viewer/ +flake8 --isolated amcr_viewer/ ruff check . # smoke test v obou verzích QGIS (docker, bez instalace čehokoli) @@ -222,11 +254,12 @@ Na co si dát pozor: v logu. Workflow proto kontroluje, že log obsahuje jen hlavičku. - **`detect-secrets` bez `--all-files` prohledá jen soubory sledované gitem** a o nesledovaném souboru mlčí. Vypadá to jako čistý výsledek. -- **Konfigurace lintů je rozdělená schválně.** `amcr_viewer/.flake8` leží - vedle `metadata.txt`, protože scanner na plugins.qgis.org hledá config - soubory jen v kořeni balíčku uvnitř ZIPu; díky tomu platí stejná pravidla - v CI, lokálně i při uploadu. Konfigurace ruffu je naopak v kořenovém - `pyproject.toml` – ruff se do balíčku pluginu nedistribuuje. +- **Flake8 běží bez konfigurace** (`--isolated`), tedy se stejnými + výchozími pravidly jako scanner na plugins.qgis.org. Do balíčku nepatří + `.flake8`, `.bandit` ani `.secrets.baseline`: scanner by plugin označil + jako „Validated (configured)“ a nález je lepší opravit v kódu. + Konfigurace ruffu je v kořenovém `pyproject.toml` – ruff se do balíčku + pluginu nedistribuuje. Viz https://plugins.qgis.org/docs/security-scanning/config-files - **Verze nástrojů jsou v workflow napevno.** Výchozí sada pravidel ruffu se mezi verzemi mění, takže bez pinu by CI začalo padat samo od sebe. diff --git a/CITATION.cff b/CITATION.cff index 561102b..1bc87e4 100644 --- a/CITATION.cff +++ b/CITATION.cff @@ -25,5 +25,5 @@ abstract: >- the Digital archive of the Archaeological Map of the Czech Republic (https://digiarchiv.aiscr.cz/). license: GPL-3.0 -version: '2.1.2' -date-released: '2026-09-01' +version: '2.1.4' +date-released: '2026-10-01' diff --git a/README.md b/README.md index 5ad6262..15c0267 100644 --- a/README.md +++ b/README.md @@ -333,9 +333,7 @@ amcr_viewer/ the plugin package (this is what gets zipped) codelists/heslar.csv cached controlled vocabularies i18n/ Qt translation files *.png toolbar and menu icons - resources.py generated by pyrcc, currently unused metadata.txt plugin metadata and changelog - .flake8 lint config, read by the plugins.qgis.org scanner tests/ check_sources.py source hygiene checks (no QGIS needed) smoke_test.py loads the plugin in a real, headless QGIS @@ -404,7 +402,7 @@ Reproducing them locally: ```bash python3 tests/check_sources.py ruff check . -flake8 --config amcr_viewer/.flake8 amcr_viewer/ +flake8 --isolated amcr_viewer/ bandit -r amcr_viewer/ docker run --rm -v "$PWD:/work:ro" -w /work --user "$(id -u):$(id -g)" \ -e HOME=/tmp qgis/qgis:stable python3 tests/smoke_test.py diff --git a/amcr_viewer/.flake8 b/amcr_viewer/.flake8 deleted file mode 100644 index 4ea6cc1..0000000 --- a/amcr_viewer/.flake8 +++ /dev/null @@ -1,12 +0,0 @@ -# Konfigurace flake8 pro plugin AMČR Viewer. -# -# Soubor leží vedle metadata.txt schválně: scanner na plugins.qgis.org -# hledá .flake8 pouze v kořeni balíčku uvnitř ZIPu, takže stejná pravidla -# platí v CI, lokálně i při uploadu. -# https://plugins.qgis.org/docs/security-scanning/config-files -[flake8] -# resources.py je vygenerovaný výstup pyrcc ("All changes made in this -# file will be lost"), není nikde importovaný a zdrojový .qrc v repu není. -# Ručně se neformátuje. -per-file-ignores = - *resources.py: E302,E305,E501 diff --git a/amcr_viewer/amcr_codelists.py b/amcr_viewer/amcr_codelists.py index 8646b38..4001e5e 100644 --- a/amcr_viewer/amcr_codelists.py +++ b/amcr_viewer/amcr_codelists.py @@ -104,6 +104,21 @@ def load_all_data(): return categorized_data +def _facet_name(item): + """ + Returns the value of one facet item from the Digiarchive API. + + Digiarchive v4.1.0 (Solr 10, json.nl=arrarr) returns facet items as + ["value", count] pairs; older versions returned {"name": "value", ...} + objects. Both shapes are accepted so the plugin works against either. + """ + if isinstance(item, dict): + return item.get("name") + if isinstance(item, (list, tuple)) and item: + return item[0] + return None + + def fetch_set(base_url, internal_name, api_set, task=None): dataset = [] params_amcr = { @@ -206,7 +221,9 @@ def fetch_set(base_url, internal_name, api_set, task=None): for r in records: - nazev = r["name"] + nazev = _facet_name(r) + if not nazev: + continue dataset.append({ 'Název': nazev, @@ -217,17 +234,48 @@ def fetch_set(base_url, internal_name, api_set, task=None): break except Exception as e: + # A partial set (e.g. pagination interrupted halfway) would + # silently drop codes – report the whole set as failed instead + # and let the caller keep the previous values QgsMessageLog.logMessage( f"Chyba u setu {api_set}: {e}", "AMČR", Qgis.MessageLevel.Warning) - break + return [] return dataset -def download_heslare(task=None): - """Fetches the codelists from the AMČR API and saves it to a CSV file.""" +def _read_existing_rows(): + """ + Returns the rows of the current heslar.csv grouped by category, so a set + that fails to download can keep its previous values. + """ + rows = {} + if not os.path.exists(OUTPUT_FILE): + return rows + try: + with open(OUTPUT_FILE, encoding='utf-8-sig', newline='') as f: + for row in csv.DictReader(f, delimiter=';'): + cat = (row.get('Kategorie') or '').strip() + if cat: + rows.setdefault(cat, []).append(row) + except Exception as e: + QgsMessageLog.logMessage( + f"Nelze načíst stávající hesláře: {e}", + "AMČR", Qgis.MessageLevel.Warning) + return rows + + +def download_heslare(task=None, failed=None): + """ + Fetches the codelists from the AMČR API and saves it to a CSV file. + + A set that fails or comes back empty keeps its rows from the current + heslar.csv instead of being wiped; its name is appended to ``failed`` + (if given) so the caller can warn the user. + """ ensure_codelists_dir() + existing = _read_existing_rows() all_data = [] total_sets = len(slovnicek) # index, (interni, api_nazev) @@ -251,6 +299,18 @@ def download_heslare(task=None): if data is None: return False # Cancelled mid-download + if not data: + # Never replace a working codelist with nothing – an API change + # would otherwise silently empty the filter in the dialog + old = existing.get(interni, []) + QgsMessageLog.logMessage( + f"Heslář '{interni}' se nepodařilo stáhnout, " + f"ponechávám předchozí hodnoty ({len(old)} položek).", + "AMČR", Qgis.MessageLevel.Warning) + if failed is not None: + failed.append(interni) + data = old + all_data.extend(data) # Report progress (0-100) @@ -261,7 +321,8 @@ def download_heslare(task=None): # Save to CSV with open(OUTPUT_FILE, 'w', newline='', encoding='utf-8-sig') as f: fieldnames = ['Název', 'Kód', 'Kategorie'] - writer = csv.DictWriter(f, fieldnames=fieldnames, delimiter=';') + writer = csv.DictWriter(f, fieldnames=fieldnames, delimiter=';', + extrasaction='ignore') writer.writeheader() writer.writerows(all_data) diff --git a/amcr_viewer/amcr_dialog.py b/amcr_viewer/amcr_dialog.py index df29ccd..c873d0a 100644 --- a/amcr_viewer/amcr_dialog.py +++ b/amcr_viewer/amcr_dialog.py @@ -75,12 +75,15 @@ class UpdateCodelistsTask(QgsTask): super().__init__(description, QgsTask.Flag.CanCancel) self.success = False self.exception = None + # Codelists that failed to download and kept their previous values + self.failed_sets = [] def run(self): """Runs in a background thread.""" try: # Call the download function with the task reference - self.success = download_heslare(task=self) + self.success = download_heslare( + task=self, failed=self.failed_sets) return self.success except Exception as e: self.exception = e @@ -91,10 +94,17 @@ class UpdateCodelistsTask(QgsTask): if result: # Safely update the global variables in the main thread refresh_globals() - QgsMessageLog.logMessage( - "Hesláře AMČR byly úspěšně aktualizovány.", - "AMČR", Qgis.MessageLevel.Info - ) + if self.failed_sets: + QgsMessageLog.logMessage( + "Hesláře AMČR aktualizovány částečně, beze změny " + f"zůstaly: {', '.join(self.failed_sets)}", + "AMČR", Qgis.MessageLevel.Warning + ) + else: + QgsMessageLog.logMessage( + "Hesláře AMČR byly úspěšně aktualizovány.", + "AMČR", Qgis.MessageLevel.Info + ) else: if self.isCanceled(): QgsMessageLog.logMessage( @@ -629,6 +639,16 @@ class AmcrFilterDialog(QDialog): def on_completed(): _cleanup() + if task.failed_sets: + QMessageBox.warning( + parent_win, + "Hesláře aktualizovány částečně", + "Některé hesláře se nepodařilo stáhnout, " + "ponechány byly jejich předchozí hodnoty:\n" + + "\n".join(f"• {name}" for name in task.failed_sets) + + "\n\nPodrobnosti jsou v panelu Zprávy, záložka AMČR." + ) + return QMessageBox.information( parent_win, "Hotovo", diff --git a/amcr_viewer/metadata.txt b/amcr_viewer/metadata.txt index b85afc9..f9a8940 100644 --- a/amcr_viewer/metadata.txt +++ b/amcr_viewer/metadata.txt @@ -28,6 +28,11 @@ changelog= * Filter labels unified: "Specifikace nálezu" renamed to "Materiál" to match the attribute alias * README rewritten to match the current state of the code * Tables for Akce and Lokality contain a field with feature weight, when Komponenty rendering is enabled + v2.1.4 (2026-10-01) + * Removed unused generated resources.py and the bundled flake8 config, so the plugin passes the plugins.qgis.org scan without custom configuration + v2.1.3 (2026-10-01) + * Fixed empty person codelists (excavation leaders, finders) after updating codelists against Digiarchive v4.1.0 + * A codelist that fails to download keeps its previous values and the user is warned v2.1.2 (2026-09-01) * Qt6 compatibility * Code clean-up diff --git a/amcr_viewer/resources.py b/amcr_viewer/resources.py deleted file mode 100644 index 2d64ca9..0000000 --- a/amcr_viewer/resources.py +++ /dev/null @@ -1,128 +0,0 @@ -# -*- coding: utf-8 -*- - -# Resource object code -# -# Created by: The Resource Compiler for PyQt5 (Qt v5.15.13) -# -# WARNING! All changes made in this file will be lost! - -from qgis.PyQt import QtCore - -qt_resource_data = b"\ -\x00\x00\x04\x0a\ -\x89\ -\x50\x4e\x47\x0d\x0a\x1a\x0a\x00\x00\x00\x0d\x49\x48\x44\x52\x00\ -\x00\x00\x17\x00\x00\x00\x18\x08\x06\x00\x00\x00\x11\x7c\x66\x75\ -\x00\x00\x00\x01\x73\x52\x47\x42\x00\xae\xce\x1c\xe9\x00\x00\x00\ -\x06\x62\x4b\x47\x44\x00\xff\x00\xff\x00\xff\xa0\xbd\xa7\x93\x00\ -\x00\x00\x09\x70\x48\x59\x73\x00\x00\x0b\x13\x00\x00\x0b\x13\x01\ -\x00\x9a\x9c\x18\x00\x00\x00\x07\x74\x49\x4d\x45\x07\xd9\x02\x15\ -\x16\x11\x2c\x9d\x48\x83\xbb\x00\x00\x03\x8a\x49\x44\x41\x54\x48\ -\xc7\xad\x95\x4b\x68\x5c\x55\x18\xc7\x7f\xe7\xdc\x7b\x67\xe6\xce\ -\x4c\x66\x26\x49\xd3\x24\x26\xa6\xc6\xf8\x40\x21\xa5\x04\xb3\x28\ -\xda\x98\x20\xa5\x0b\xad\x55\xa8\x2b\xc5\x50\x1f\xa0\x6e\x34\x2b\ -\x45\x30\x14\x02\xba\x52\x69\x15\x17\x66\x63\x45\x97\x95\xa0\xad\ -\x0b\xfb\xc0\x06\x25\xb6\x71\x61\x12\x41\x50\xdb\x2a\x21\xd1\xe2\ -\x24\xf3\x9e\xc9\xcc\xbd\xe7\x1c\x17\x35\x43\x1e\x33\x21\xb6\xfd\ -\x56\x87\xf3\x9d\xfb\xfb\x1e\xf7\xff\x9d\x23\x8c\x31\x43\x95\xf4\ -\x85\x1e\x3f\x3b\x35\xac\xfd\xcc\x43\xdc\xa4\x49\x3b\xfe\x9d\x1d\ -\xdb\x7b\x22\x90\x78\xf8\xb2\x28\xa7\xbe\x7d\xc1\x4b\x9d\x79\xdf\ -\x18\x15\xe5\x16\x99\x10\x56\xde\x69\xdc\x3f\x22\xfd\xec\xd4\xf0\ -\xad\x04\x03\x18\xa3\xa2\x7e\x76\x6a\x58\xde\x68\x2b\xb4\x36\xf8\ -\xbe\xc6\x18\x53\xdb\xef\xe7\xfa\xec\xed\x67\x63\x10\x42\x00\xf0\ -\xfb\xd5\x65\x2a\x15\x45\xc7\x6d\x0d\x00\xc4\xa2\xc1\xaa\x6f\x0d\ -\x3e\x6c\xab\xc2\x1c\x56\xa4\x77\x4b\xb0\xf2\x35\x15\x5f\x21\x85\ -\xe0\xc8\x6b\x5f\x92\x2d\x37\x33\x39\xf9\x03\x27\x8e\x1f\xa2\xf7\ -\xbe\x9d\x04\x1c\x0b\x37\xe4\xac\xff\xa6\x30\x87\xbd\xba\x00\x6a\ -\x06\x79\xe5\xf5\xaf\x89\xd9\x92\xc5\xcc\x0a\xd9\x7c\x19\xcf\xe9\ -\xe2\xe4\xa9\x2f\x78\x7c\xff\x01\x72\x85\x0a\x2b\x65\x1f\xa5\x4c\ -\xb5\xb2\x55\x16\x80\xbd\x31\xda\xda\x20\x1f\x7d\x3e\xcd\xc2\xfd\ -\x59\xa6\x93\x39\x92\xd1\x22\xea\x9b\x16\xce\x9d\x3f\xce\xe0\x83\ -\x03\x24\x82\x59\x3a\xdb\x7b\x88\xc7\x82\x68\x63\x58\xc9\xcc\x62\ -\x8c\x21\x18\xb0\x6a\xc3\x37\x06\x49\x16\xff\x24\x6b\xa5\x49\xbb\ -\x25\xbc\xa2\xa6\x21\xbb\x40\x7f\xdf\x00\x83\xbd\x01\x8e\x3c\xd5\ -\x45\xd7\x8e\x6b\x9c\x9c\x98\x25\x1a\xb6\xe8\xbe\x3d\xc2\xdd\x77\ -\x44\x48\xc4\x1c\x22\xe1\xeb\x58\x59\xaf\xcf\xd3\x33\x29\x2e\x34\ -\x2d\x91\x93\x3e\xbe\x34\x78\x01\xc5\xe2\x61\xc5\xae\x72\x8e\x70\ -\xc8\xc2\x0d\x5a\xbc\xf5\xee\x2f\x9c\xfa\x3e\x86\x69\x7a\x8e\xcf\ -\x26\xe6\xf9\x63\xa1\x44\xa1\xa4\xd0\xda\x6c\x0d\x2f\x15\x7c\xb4\ -\x67\x28\x59\x0a\xcf\xd6\x54\xe2\x06\x13\x87\x2b\x6f\x68\xa6\x27\ -\xaf\x31\x32\x36\xc7\xb2\x7f\x17\xef\x7d\x7c\x8c\x33\x67\xcf\x12\ -\x70\x24\x4a\x69\xd6\x6a\x46\xd6\xd3\x70\x72\xa9\x82\x67\x34\x45\ -\xad\x28\xdb\x1a\x15\x34\x98\xff\x46\xed\xef\x37\x0d\x99\xbf\x4a\ -\x3c\x30\x38\xc0\xc8\x4b\xaf\x92\x5a\x9c\xe2\xe0\x23\x6d\x74\xb4\ -\xba\x84\x5d\x0b\x29\x45\x7d\xb8\x94\x82\x96\xb6\x10\xf3\xc5\x12\ -\x2a\xef\x53\x11\x1a\x63\xad\x3f\x93\x19\x85\xf1\xb1\x77\x58\x5a\ -\xf8\x99\x97\x9f\xe9\xa6\x75\x47\x90\xc6\xb8\x43\xd8\xb5\xb6\xce\ -\xfc\xfa\xfd\x00\xfb\x3e\xf4\xc8\x05\x35\xba\x5e\xeb\x46\x21\xf9\ -\xcf\x0a\xa9\x8c\x87\xe3\x48\xdc\x90\xb5\x6e\x98\x6a\xaa\x65\xf2\ -\x52\x92\x43\x2f\x5e\xc2\x8c\x02\x1a\x10\xf5\x07\xac\xc3\x75\x70\ -\x83\x92\x80\xb3\xf9\xd0\x26\xf8\x8f\xb3\x29\xc6\x3e\xb8\x8c\x19\ -\x35\x75\x6b\x7b\x7e\x3c\xca\x45\x0c\x7e\x49\x31\xf4\x58\x3b\xf7\ -\xf6\x34\x90\x88\x39\x04\x1c\x59\x1f\xfe\xdb\xd5\x3c\x5f\x9d\x4b\ -\x32\xfd\x44\xb2\xba\xd7\xfa\xb6\x60\xcf\xde\x16\xdc\x90\x45\x4c\ -\x4a\x2a\x9e\x62\xfe\x4e\xc5\xc8\xc1\x4e\xda\x76\x86\xe8\xe9\x0a\ -\xe3\xd8\x92\x58\xd4\xc6\xb2\x44\x6d\x78\x2a\x53\xe1\xca\x7c\x99\ -\x63\x5d\xbf\x56\x9d\xbd\x9f\x44\x18\x7a\xba\x95\x27\x0f\xb4\xd3\ -\xdc\x18\xc0\xf3\x0d\x52\x40\xd8\xb5\xb0\xa4\x20\x14\xb2\x70\x6c\ -\x81\x63\xcb\xaa\x42\xd6\xfd\xb7\xf4\xec\xa3\x06\xa0\x50\x52\xd8\ -\x4e\x1b\x7e\x4a\xd3\x31\xf9\x29\xcf\xfe\xd4\x49\x7f\x5f\x13\xfb\ -\xfa\x9b\x71\x43\x92\x58\xd4\x21\x18\x90\xac\xde\xb0\x42\x50\x13\ -\x58\x33\xf3\x88\x6b\xa1\xfd\x65\x96\xf2\x79\xc6\x43\x7b\xd8\x75\ -\x38\xcc\x3d\xdd\xd1\xaa\xcf\x71\xe4\xff\x7f\x91\x56\x33\xaf\xea\ -\x37\xe7\xa1\x94\x21\x16\xb5\xd1\x06\x2c\x29\x36\xf5\x72\x9b\x96\ -\x95\xc0\xc4\xda\x9d\x78\x83\x43\x53\x22\x80\x65\x09\x1c\xfb\x86\ -\xc1\x00\xe7\x25\x70\x14\x48\x6f\x1e\x22\x51\xe3\x75\xd9\xb6\xa5\ -\x81\xa3\x32\xb1\xfb\xf4\x0c\x30\xb8\xb1\x82\x9b\xb0\x09\x60\x30\ -\xb1\xfb\xf4\xcc\xbf\xa0\xe9\x6e\xae\x5a\xdf\x4b\x81\x00\x00\x00\ -\x00\x49\x45\x4e\x44\xae\x42\x60\x82\ -" - -qt_resource_name = b"\ -\x00\x07\ -\x07\x3b\xe0\xb3\ -\x00\x70\ -\x00\x6c\x00\x75\x00\x67\x00\x69\x00\x6e\x00\x73\ -\x00\x0b\ -\x06\x1f\xb8\xc2\ -\x00\x61\ -\x00\x6d\x00\x63\x00\x72\x00\x5f\x00\x76\x00\x69\x00\x65\x00\x77\x00\x65\x00\x72\ -\x00\x08\ -\x0a\x61\x5a\xa7\ -\x00\x69\ -\x00\x63\x00\x6f\x00\x6e\x00\x2e\x00\x70\x00\x6e\x00\x67\ -" - -qt_resource_struct_v1 = b"\ -\x00\x00\x00\x00\x00\x02\x00\x00\x00\x01\x00\x00\x00\x01\ -\x00\x00\x00\x00\x00\x02\x00\x00\x00\x01\x00\x00\x00\x02\ -\x00\x00\x00\x14\x00\x02\x00\x00\x00\x01\x00\x00\x00\x03\ -\x00\x00\x00\x30\x00\x00\x00\x00\x00\x01\x00\x00\x00\x00\ -" - -qt_resource_struct_v2 = b"\ -\x00\x00\x00\x00\x00\x02\x00\x00\x00\x01\x00\x00\x00\x01\ -\x00\x00\x00\x00\x00\x00\x00\x00\ -\x00\x00\x00\x00\x00\x02\x00\x00\x00\x01\x00\x00\x00\x02\ -\x00\x00\x00\x00\x00\x00\x00\x00\ -\x00\x00\x00\x14\x00\x02\x00\x00\x00\x01\x00\x00\x00\x03\ -\x00\x00\x00\x00\x00\x00\x00\x00\ -\x00\x00\x00\x30\x00\x00\x00\x00\x00\x01\x00\x00\x00\x00\ -\x00\x00\x01\x9c\x23\xfd\x16\x70\ -" - -qt_version = [int(v) for v in QtCore.qVersion().split('.')] -if qt_version < [5, 8, 0]: - rcc_version = 1 - qt_resource_struct = qt_resource_struct_v1 -else: - rcc_version = 2 - qt_resource_struct = qt_resource_struct_v2 - -def qInitResources(): - QtCore.qRegisterResourceData(rcc_version, qt_resource_struct, qt_resource_name, qt_resource_data) - -def qCleanupResources(): - QtCore.qUnregisterResourceData(rcc_version, qt_resource_struct, qt_resource_name, qt_resource_data) - -qInitResources() diff --git a/openspec/changes/archive/.gitkeep b/openspec/changes/archive/.gitkeep new file mode 100644 index 0000000..e69de29 diff --git a/openspec/config.yaml b/openspec/config.yaml new file mode 100644 index 0000000..c62161b --- /dev/null +++ b/openspec/config.yaml @@ -0,0 +1,65 @@ +schema: spec-driven + +# Seeded from aiscr-management +# .agents/canonical_configs/templates/openspec/config_seed.yaml; +# from now on the content is owned by this repository. + +context: | + Repository: aiscr-qgis-amcr-viewer — QGIS plugin (AMČR Viewer) for + downloading and visualising data from the AMČR Digital Archive + (digiarchiv.aiscr.cz). + + OpenSpec posture: change-tracked. + This is the local reading of a posture declared and owned by the + management hub (aiscr-management, .agents/sync/repos.toml): + - change-tracked — change-scoped planning artifacts live under + `openspec/changes/`; no durable capability specs are maintained here. + Do not edit this line to unblock work in progress. Changing posture is + a decision taken deliberately with the hub and then applied here. + + When OpenSpec fires: behaviour-changing work (user-visible behaviour, + layer attributes, the digiarchiv API contract the plugin relies on, + stored settings), cross-repo work, and governance-touching work. Not + typo and formatting fixes, dependency or tool-pin bumps that change no + behaviour, or refreshing a generated surface from its source. + + Repository conventions: `AGENTS.md` is the single source of truth + (Qt5/Qt6 rules, versioning, branches, checks). Plugin code lives in + `amcr_viewer/` (entry point `amcr_viewer.py`, API and layers in + `amcr_tools.py`, dialogs in `amcr_dialog.py`, codelists in + `amcr_codelists.py`). User documentation is `README.md` (English); + planning artifacts, commits and PRs are in Czech or English as the + author prefers, code and identifiers in English. + +rules: + proposal: + - State what changes for a user of the plugin, not only in the code + - Name the affected modules under amcr_viewer/ explicitly + - Say when the change depends on or affects the digiarchiv API or + another AIS CR repository, and where + specs: + - Use RFC 2119 keywords (SHALL/MUST/SHOULD/MAY) + - Use Given/When/Then scenarios for testable contracts + - Describe behaviour the plugin guarantees, not how the code does it + design: + - Record the alternatives considered and why the chosen one won + - Respect the QGIS 3.44 minimum and Qt5/Qt6 rules from AGENTS.md + - Name the verification that will show the change worked + tasks: + - Order tasks so each one is independently verifiable + - Name the command or check that proves each group is done + - Include the version bump (metadata.txt + CITATION.cff) when + behaviour changes + - Include a final verification task that runs the checks from + AGENTS.md (check_sources, bandit, detect-secrets, flake8, ruff, + pyqgis4-checker, smoke test in qgis/qgis:ltr and :stable) + +operations: + apply: + guidance: + - Completed artifacts are not approval to implement; wait for an + explicit request to apply the change + archive: + guidance: + - Posture is change-tracked, so archive with --skip-specs (no + openspec/specs/ tree is maintained) diff --git a/pyproject.toml b/pyproject.toml index d60d6e9..31cda19 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -5,15 +5,14 @@ # jen k tomu, aby ruff choval stejně v CI, lokálně i za rok. Bez explicitní # konfigurace se výchozí sada pravidel mezi verzemi ruffu mění. # -# Konfigurace flake8 je záměrně jinde: v amcr_viewer/.flake8, protože ji -# musí najít i scanner na plugins.qgis.org. +# Flake8 záměrně žádnou konfiguraci nemá a běží s výchozími pravidly – stejně +# jako scanner na plugins.qgis.org. Config soubor v balíčku by plugin +# označil jako „Validated (configured)“. [tool.ruff] line-length = 79 # QGIS 3.44 běží na Pythonu 3.9 a novějším target-version = "py39" -# Generovaný výstup pyrcc, "All changes made in this file will be lost" -extend-exclude = ["amcr_viewer/resources.py"] [tool.ruff.lint] select = [ diff --git a/tests/check_sources.py b/tests/check_sources.py index fb960a2..f840a55 100644 --- a/tests/check_sources.py +++ b/tests/check_sources.py @@ -23,10 +23,6 @@ import sys ROOT = os.path.dirname(os.path.dirname(os.path.abspath(__file__))) BALICEK = os.path.join(ROOT, "amcr_viewer") -# Files that belong in the plugin package even though the upload scanner -# would otherwise call them hidden -POVOLENE_SKRYTE = {".flake8", ".bandit", ".secrets.baseline"} - # Extensions that have no business inside a plugin package PODEZRELE = {".exe", ".dll", ".so", ".dylib", ".sh", ".bat", ".cmd", ".pyc", ".pyd", ".jar", ".bin"} @@ -72,7 +68,9 @@ for cesta in vsechny_soubory(): if rezim & (stat.S_IXUSR | stat.S_IXGRP | stat.S_IXOTH): nalezy.append(f"{zkratka(cesta)}: spustitelná práva " f"({stat.filemode(rezim)})") - if jmeno.startswith(".") and jmeno not in POVOLENE_SKRYTE: + # No exceptions: scanner config files (.flake8, .bandit, + # .secrets.baseline) would mark the upload "Validated (configured)" + if jmeno.startswith("."): nalezy.append(f"{zkratka(cesta)}: skrytý soubor v balíčku pluginu") if os.path.splitext(jmeno)[1].lower() in PODEZRELE: nalezy.append(f"{zkratka(cesta)}: podezřelý typ souboru")