portolan-cli 0.4.2__tar.gz → 0.4.4__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (447) hide show
  1. {portolan_cli-0.4.2 → portolan_cli-0.4.4}/.pre-commit-config.yaml +10 -0
  2. {portolan_cli-0.4.2 → portolan_cli-0.4.4}/CHANGELOG.md +41 -0
  3. {portolan_cli-0.4.2 → portolan_cli-0.4.4}/CLAUDE.md +2 -1
  4. {portolan_cli-0.4.2 → portolan_cli-0.4.4}/PKG-INFO +1 -1
  5. {portolan_cli-0.4.2 → portolan_cli-0.4.4}/SKILL.md +13 -3
  6. portolan_cli-0.4.4/context/shared/adr/0029-unified-catalog-root-detection.md +125 -0
  7. {portolan_cli-0.4.2 → portolan_cli-0.4.4}/portolan_cli/catalog.py +81 -0
  8. portolan_cli-0.4.4/portolan_cli/clean.py +210 -0
  9. {portolan_cli-0.4.2 → portolan_cli-0.4.4}/portolan_cli/cli.py +736 -302
  10. {portolan_cli-0.4.2 → portolan_cli-0.4.4}/portolan_cli/constants.py +6 -1
  11. {portolan_cli-0.4.2 → portolan_cli-0.4.4}/portolan_cli/dataset.py +429 -138
  12. {portolan_cli-0.4.2 → portolan_cli-0.4.4}/portolan_cli/errors.py +19 -0
  13. {portolan_cli-0.4.2 → portolan_cli-0.4.4}/portolan_cli/formats.py +4 -1
  14. {portolan_cli-0.4.2 → portolan_cli-0.4.4}/portolan_cli/pull.py +18 -1
  15. {portolan_cli-0.4.2 → portolan_cli-0.4.4}/portolan_cli/push.py +67 -1
  16. {portolan_cli-0.4.2 → portolan_cli-0.4.4}/portolan_cli/scan.py +15 -4
  17. {portolan_cli-0.4.2 → portolan_cli-0.4.4}/portolan_cli/scan_classify.py +4 -2
  18. portolan_cli-0.4.4/portolan_cli/status.py +528 -0
  19. {portolan_cli-0.4.2 → portolan_cli-0.4.4}/portolan_cli/validation/rules.py +1 -1
  20. {portolan_cli-0.4.2 → portolan_cli-0.4.4}/pyproject.toml +2 -2
  21. {portolan_cli-0.4.2 → portolan_cli-0.4.4}/tests/conftest.py +104 -0
  22. portolan_cli-0.4.4/tests/integration/test_add_catalog_root_integration.py +416 -0
  23. {portolan_cli-0.4.2 → portolan_cli-0.4.4}/tests/integration/test_add_rm_integration.py +223 -3
  24. portolan_cli-0.4.4/tests/integration/test_catalog_root_consistency.py +269 -0
  25. {portolan_cli-0.4.2 → portolan_cli-0.4.4}/tests/integration/test_check_command.py +124 -0
  26. portolan_cli-0.4.4/tests/integration/test_clean_integration.py +594 -0
  27. {portolan_cli-0.4.2 → portolan_cli-0.4.4}/tests/integration/test_dataset_integration.py +94 -30
  28. portolan_cli-0.4.4/tests/integration/test_list_cli.py +304 -0
  29. {portolan_cli-0.4.2 → portolan_cli-0.4.4}/tests/integration/test_push_integration.py +21 -11
  30. portolan_cli-0.4.4/tests/integration/test_scan_batching.py +141 -0
  31. {portolan_cli-0.4.2 → portolan_cli-0.4.4}/tests/integration/test_scan_cli.py +172 -5
  32. portolan_cli-0.4.4/tests/integration/test_scan_unrecognized_files.py +457 -0
  33. portolan_cli-0.4.4/tests/integration/test_status_integration.py +499 -0
  34. portolan_cli-0.4.4/tests/unit/test_add_catalog_root.py +362 -0
  35. portolan_cli-0.4.4/tests/unit/test_add_continue_on_errors.py +570 -0
  36. portolan_cli-0.4.4/tests/unit/test_add_failures_batching.py +288 -0
  37. {portolan_cli-0.4.2 → portolan_cli-0.4.4}/tests/unit/test_add_rm_hypothesis.py +645 -24
  38. portolan_cli-0.4.4/tests/unit/test_clean.py +551 -0
  39. {portolan_cli-0.4.2 → portolan_cli-0.4.4}/tests/unit/test_cli_add_rm.py +416 -20
  40. portolan_cli-0.4.4/tests/unit/test_cli_check.py +804 -0
  41. {portolan_cli-0.4.2 → portolan_cli-0.4.4}/tests/unit/test_cli_dataset.py +108 -6
  42. {portolan_cli-0.4.2 → portolan_cli-0.4.4}/tests/unit/test_csv_skip.py +68 -65
  43. {portolan_cli-0.4.2 → portolan_cli-0.4.4}/tests/unit/test_dataset.py +308 -55
  44. {portolan_cli-0.4.2 → portolan_cli-0.4.4}/tests/unit/test_dataset_directory.py +107 -46
  45. portolan_cli-0.4.4/tests/unit/test_find_catalog_root.py +400 -0
  46. {portolan_cli-0.4.2 → portolan_cli-0.4.4}/tests/unit/test_formats.py +49 -0
  47. {portolan_cli-0.4.2 → portolan_cli-0.4.4}/tests/unit/test_formats_property.py +121 -1
  48. portolan_cli-0.4.4/tests/unit/test_list_output.py +523 -0
  49. {portolan_cli-0.4.2 → portolan_cli-0.4.4}/tests/unit/test_pull.py +145 -20
  50. {portolan_cli-0.4.2 → portolan_cli-0.4.4}/tests/unit/test_push.py +172 -0
  51. {portolan_cli-0.4.2 → portolan_cli-0.4.4}/tests/unit/test_scan.py +85 -17
  52. {portolan_cli-0.4.2 → portolan_cli-0.4.4}/tests/unit/test_scan_classify.py +42 -2
  53. portolan_cli-0.4.4/tests/unit/test_scan_output_batching.py +385 -0
  54. portolan_cli-0.4.4/tests/unit/test_scan_unrecognized_files_property.py +192 -0
  55. portolan_cli-0.4.4/tests/unit/test_status.py +1807 -0
  56. portolan_cli-0.4.4/tests/unit/test_status_filegdb.py +634 -0
  57. {portolan_cli-0.4.2 → portolan_cli-0.4.4}/tests/unit/test_sync.py +134 -0
  58. portolan_cli-0.4.4/tests/unit/test_tabular_parquet_skip.py +646 -0
  59. {portolan_cli-0.4.2 → portolan_cli-0.4.4}/tests/unit/test_validation_rules.py +1 -1
  60. {portolan_cli-0.4.2 → portolan_cli-0.4.4}/uv.lock +1 -1
  61. portolan_cli-0.4.2/portolan_cli/status.py +0 -234
  62. portolan_cli-0.4.2/tests/unit/test_cli_check.py +0 -1112
  63. portolan_cli-0.4.2/tests/unit/test_status.py +0 -700
  64. {portolan_cli-0.4.2 → portolan_cli-0.4.4}/.claude/commands/speckit.analyze.md +0 -0
  65. {portolan_cli-0.4.2 → portolan_cli-0.4.4}/.claude/commands/speckit.checklist.md +0 -0
  66. {portolan_cli-0.4.2 → portolan_cli-0.4.4}/.claude/commands/speckit.clarify.md +0 -0
  67. {portolan_cli-0.4.2 → portolan_cli-0.4.4}/.claude/commands/speckit.constitution.md +0 -0
  68. {portolan_cli-0.4.2 → portolan_cli-0.4.4}/.claude/commands/speckit.implement.md +0 -0
  69. {portolan_cli-0.4.2 → portolan_cli-0.4.4}/.claude/commands/speckit.plan.md +0 -0
  70. {portolan_cli-0.4.2 → portolan_cli-0.4.4}/.claude/commands/speckit.specify.md +0 -0
  71. {portolan_cli-0.4.2 → portolan_cli-0.4.4}/.claude/commands/speckit.tasks.md +0 -0
  72. {portolan_cli-0.4.2 → portolan_cli-0.4.4}/.claude/commands/speckit.taskstoissues.md +0 -0
  73. {portolan_cli-0.4.2 → portolan_cli-0.4.4}/.claude/hooks/post-bash-remind.sh +0 -0
  74. {portolan_cli-0.4.2 → portolan_cli-0.4.4}/.claude/hooks/post-read-inject-docs.sh +0 -0
  75. {portolan_cli-0.4.2 → portolan_cli-0.4.4}/.claude/hooks/pre-read-check.sh +0 -0
  76. {portolan_cli-0.4.2 → portolan_cli-0.4.4}/.claude/hooks/prompt-inject.sh +0 -0
  77. {portolan_cli-0.4.2 → portolan_cli-0.4.4}/.claude/settings.json +0 -0
  78. {portolan_cli-0.4.2 → portolan_cli-0.4.4}/.coderabbit.yaml +0 -0
  79. {portolan_cli-0.4.2 → portolan_cli-0.4.4}/.github/CODEOWNERS +0 -0
  80. {portolan_cli-0.4.2 → portolan_cli-0.4.4}/.github/SOCIAL_PREVIEW.md +0 -0
  81. {portolan_cli-0.4.2 → portolan_cli-0.4.4}/.github/dependabot.yml +0 -0
  82. {portolan_cli-0.4.2 → portolan_cli-0.4.4}/.github/pull_request_template.md +0 -0
  83. {portolan_cli-0.4.2 → portolan_cli-0.4.4}/.github/workflows/ci.yml +0 -0
  84. {portolan_cli-0.4.2 → portolan_cli-0.4.4}/.github/workflows/docs.yml +0 -0
  85. {portolan_cli-0.4.2 → portolan_cli-0.4.4}/.github/workflows/nightly.yml +0 -0
  86. {portolan_cli-0.4.2 → portolan_cli-0.4.4}/.github/workflows/release.yml +0 -0
  87. {portolan_cli-0.4.2 → portolan_cli-0.4.4}/.gitignore +0 -0
  88. {portolan_cli-0.4.2 → portolan_cli-0.4.4}/.grepaiignore +0 -0
  89. {portolan_cli-0.4.2 → portolan_cli-0.4.4}/.mcp.json +0 -0
  90. {portolan_cli-0.4.2 → portolan_cli-0.4.4}/.python-version +0 -0
  91. {portolan_cli-0.4.2 → portolan_cli-0.4.4}/.specify/memory/constitution.md +0 -0
  92. {portolan_cli-0.4.2 → portolan_cli-0.4.4}/.specify/scripts/bash/check-prerequisites.sh +0 -0
  93. {portolan_cli-0.4.2 → portolan_cli-0.4.4}/.specify/scripts/bash/common.sh +0 -0
  94. {portolan_cli-0.4.2 → portolan_cli-0.4.4}/.specify/scripts/bash/create-new-feature.sh +0 -0
  95. {portolan_cli-0.4.2 → portolan_cli-0.4.4}/.specify/scripts/bash/setup-plan.sh +0 -0
  96. {portolan_cli-0.4.2 → portolan_cli-0.4.4}/.specify/scripts/bash/update-agent-context.sh +0 -0
  97. {portolan_cli-0.4.2 → portolan_cli-0.4.4}/.specify/templates/agent-file-template.md +0 -0
  98. {portolan_cli-0.4.2 → portolan_cli-0.4.4}/.specify/templates/checklist-template.md +0 -0
  99. {portolan_cli-0.4.2 → portolan_cli-0.4.4}/.specify/templates/plan-template.md +0 -0
  100. {portolan_cli-0.4.2 → portolan_cli-0.4.4}/.specify/templates/spec-template.md +0 -0
  101. {portolan_cli-0.4.2 → portolan_cli-0.4.4}/.specify/templates/tasks-template.md +0 -0
  102. {portolan_cli-0.4.2 → portolan_cli-0.4.4}/LICENSE +0 -0
  103. {portolan_cli-0.4.2 → portolan_cli-0.4.4}/README.md +0 -0
  104. {portolan_cli-0.4.2 → portolan_cli-0.4.4}/SECURITY.md +0 -0
  105. {portolan_cli-0.4.2 → portolan_cli-0.4.4}/codecov.yml +0 -0
  106. {portolan_cli-0.4.2 → portolan_cli-0.4.4}/context/shared/adr/0000-template.md +0 -0
  107. {portolan_cli-0.4.2 → portolan_cli-0.4.4}/context/shared/adr/0001-agentic-first-development.md +0 -0
  108. {portolan_cli-0.4.2 → portolan_cli-0.4.4}/context/shared/adr/0002-click-for-cli.md +0 -0
  109. {portolan_cli-0.4.2 → portolan_cli-0.4.4}/context/shared/adr/0003-plugin-architecture.md +0 -0
  110. {portolan_cli-0.4.2 → portolan_cli-0.4.4}/context/shared/adr/0004-iceberg-as-plugin.md +0 -0
  111. {portolan_cli-0.4.2 → portolan_cli-0.4.4}/context/shared/adr/0005-versions-json-source-of-truth.md +0 -0
  112. {portolan_cli-0.4.2 → portolan_cli-0.4.4}/context/shared/adr/0006-remote-ownership-model.md +0 -0
  113. {portolan_cli-0.4.2 → portolan_cli-0.4.4}/context/shared/adr/0007-cli-wraps-api.md +0 -0
  114. {portolan_cli-0.4.2 → portolan_cli-0.4.4}/context/shared/adr/0008-pipx-for-installation.md +0 -0
  115. {portolan_cli-0.4.2 → portolan_cli-0.4.4}/context/shared/adr/0009-output-dry-run-and-verbose-modes.md +0 -0
  116. {portolan_cli-0.4.2 → portolan_cli-0.4.4}/context/shared/adr/0010-delegate-conversion-validation.md +0 -0
  117. {portolan_cli-0.4.2 → portolan_cli-0.4.4}/context/shared/adr/0011-mvp-validation-framework.md +0 -0
  118. {portolan_cli-0.4.2 → portolan_cli-0.4.4}/context/shared/adr/0012-flat-catalog-hierarchy.md +0 -0
  119. {portolan_cli-0.4.2 → portolan_cli-0.4.4}/context/shared/adr/0013-gitingest-auto-fetch.md +0 -0
  120. {portolan_cli-0.4.2 → portolan_cli-0.4.4}/context/shared/adr/0014-accept-non-cloud-native-formats.md +0 -0
  121. {portolan_cli-0.4.2 → portolan_cli-0.4.4}/context/shared/adr/0015-two-tier-versioning-architecture.md +0 -0
  122. {portolan_cli-0.4.2 → portolan_cli-0.4.4}/context/shared/adr/0016-scan-before-import.md +0 -0
  123. {portolan_cli-0.4.2 → portolan_cli-0.4.4}/context/shared/adr/0017-mtime-heuristics-change-detection.md +0 -0
  124. {portolan_cli-0.4.2 → portolan_cli-0.4.4}/context/shared/adr/0018-metadata-generation-tiers.md +0 -0
  125. {portolan_cli-0.4.2 → portolan_cli-0.4.4}/context/shared/adr/0019-cog-optimization-defaults.md +0 -0
  126. {portolan_cli-0.4.2 → portolan_cli-0.4.4}/context/shared/adr/0020-conversion-output-location.md +0 -0
  127. {portolan_cli-0.4.2 → portolan_cli-0.4.4}/context/shared/adr/0021-catalog-json-root-level.md +0 -0
  128. {portolan_cli-0.4.2 → portolan_cli-0.4.4}/context/shared/adr/0022-git-style-implicit-tracking.md +0 -0
  129. {portolan_cli-0.4.2 → portolan_cli-0.4.4}/context/shared/adr/0023-stac-structure-separation.md +0 -0
  130. {portolan_cli-0.4.2 → portolan_cli-0.4.4}/context/shared/adr/0024-hierarchical-config-system.md +0 -0
  131. {portolan_cli-0.4.2 → portolan_cli-0.4.4}/context/shared/adr/0025-architecture-as-code.md +0 -0
  132. {portolan_cli-0.4.2 → portolan_cli-0.4.4}/context/shared/adr/0026-conversion-config-design.md +0 -0
  133. {portolan_cli-0.4.2 → portolan_cli-0.4.4}/context/shared/adr/0027-unified-config-yaml-sentinel.md +0 -0
  134. {portolan_cli-0.4.2 → portolan_cli-0.4.4}/context/shared/adr/0028-all-files-as-assets.md +0 -0
  135. {portolan_cli-0.4.2 → portolan_cli-0.4.4}/context/shared/documentation/ci.md +0 -0
  136. {portolan_cli-0.4.2 → portolan_cli-0.4.4}/context/shared/documentation/context-guide.md +0 -0
  137. {portolan_cli-0.4.2 → portolan_cli-0.4.4}/context/shared/documentation/distill-mcp.md +0 -0
  138. {portolan_cli-0.4.2 → portolan_cli-0.4.4}/context/shared/documentation/gitingest-setup.md +0 -0
  139. {portolan_cli-0.4.2 → portolan_cli-0.4.4}/context/shared/documentation/test-fixtures.md +0 -0
  140. {portolan_cli-0.4.2 → portolan_cli-0.4.4}/context/shared/known-issues/example.md +0 -0
  141. {portolan_cli-0.4.2 → portolan_cli-0.4.4}/context/shared/known-issues/geoparquet-io-windows-segfault.md +0 -0
  142. {portolan_cli-0.4.2 → portolan_cli-0.4.4}/context/shared/known-issues/pyarrow-abseil-abi.md +0 -0
  143. {portolan_cli-0.4.2 → portolan_cli-0.4.4}/context/shared/known-issues/pystac-absolute-paths.md +0 -0
  144. {portolan_cli-0.4.2 → portolan_cli-0.4.4}/context/shared/plans/2026-03-05-fix-versions-snapshot-model.md +0 -0
  145. {portolan_cli-0.4.2 → portolan_cli-0.4.4}/docs/BRANDING.md +0 -0
  146. {portolan_cli-0.4.2 → portolan_cli-0.4.4}/docs/assets/images/cover.png +0 -0
  147. {portolan_cli-0.4.2 → portolan_cli-0.4.4}/docs/assets/images/icon-white.svg +0 -0
  148. {portolan_cli-0.4.2 → portolan_cli-0.4.4}/docs/assets/images/icon.svg +0 -0
  149. {portolan_cli-0.4.2 → portolan_cli-0.4.4}/docs/assets/images/logo.png +0 -0
  150. {portolan_cli-0.4.2 → portolan_cli-0.4.4}/docs/assets/images/logo.svg +0 -0
  151. {portolan_cli-0.4.2 → portolan_cli-0.4.4}/docs/assets/images/social-card.png +0 -0
  152. {portolan_cli-0.4.2 → portolan_cli-0.4.4}/docs/assets/stylesheets/extra.css +0 -0
  153. {portolan_cli-0.4.2 → portolan_cli-0.4.4}/docs/changelog.md +0 -0
  154. {portolan_cli-0.4.2 → portolan_cli-0.4.4}/docs/contributing.md +0 -0
  155. {portolan_cli-0.4.2 → portolan_cli-0.4.4}/docs/index.md +0 -0
  156. {portolan_cli-0.4.2 → portolan_cli-0.4.4}/docs/reference/cli.md +0 -0
  157. {portolan_cli-0.4.2 → portolan_cli-0.4.4}/docs/reference/configuration.md +0 -0
  158. {portolan_cli-0.4.2 → portolan_cli-0.4.4}/mkdocs.yml +0 -0
  159. {portolan_cli-0.4.2 → portolan_cli-0.4.4}/portolan_cli/__init__.py +0 -0
  160. {portolan_cli-0.4.2 → portolan_cli-0.4.4}/portolan_cli/backends/__init__.py +0 -0
  161. {portolan_cli-0.4.2 → portolan_cli-0.4.4}/portolan_cli/backends/json_file.py +0 -0
  162. {portolan_cli-0.4.2 → portolan_cli-0.4.4}/portolan_cli/backends/protocol.py +0 -0
  163. {portolan_cli-0.4.2 → portolan_cli-0.4.4}/portolan_cli/catalog_versions.py +0 -0
  164. {portolan_cli-0.4.2 → portolan_cli-0.4.4}/portolan_cli/check.py +0 -0
  165. {portolan_cli-0.4.2 → portolan_cli-0.4.4}/portolan_cli/collection.py +0 -0
  166. {portolan_cli-0.4.2 → portolan_cli-0.4.4}/portolan_cli/collection_id.py +0 -0
  167. {portolan_cli-0.4.2 → portolan_cli-0.4.4}/portolan_cli/config.py +0 -0
  168. {portolan_cli-0.4.2 → portolan_cli-0.4.4}/portolan_cli/conversion_config.py +0 -0
  169. {portolan_cli-0.4.2 → portolan_cli-0.4.4}/portolan_cli/convert.py +0 -0
  170. {portolan_cli-0.4.2 → portolan_cli-0.4.4}/portolan_cli/download.py +0 -0
  171. {portolan_cli-0.4.2 → portolan_cli-0.4.4}/portolan_cli/inspect.py +0 -0
  172. {portolan_cli-0.4.2 → portolan_cli-0.4.4}/portolan_cli/item.py +0 -0
  173. {portolan_cli-0.4.2 → portolan_cli-0.4.4}/portolan_cli/json_output.py +0 -0
  174. {portolan_cli-0.4.2 → portolan_cli-0.4.4}/portolan_cli/metadata/__init__.py +0 -0
  175. {portolan_cli-0.4.2 → portolan_cli-0.4.4}/portolan_cli/metadata/cog.py +0 -0
  176. {portolan_cli-0.4.2 → portolan_cli-0.4.4}/portolan_cli/metadata/detection.py +0 -0
  177. {portolan_cli-0.4.2 → portolan_cli-0.4.4}/portolan_cli/metadata/fix.py +0 -0
  178. {portolan_cli-0.4.2 → portolan_cli-0.4.4}/portolan_cli/metadata/geoparquet.py +0 -0
  179. {portolan_cli-0.4.2 → portolan_cli-0.4.4}/portolan_cli/metadata/models.py +0 -0
  180. {portolan_cli-0.4.2 → portolan_cli-0.4.4}/portolan_cli/metadata/update.py +0 -0
  181. {portolan_cli-0.4.2 → portolan_cli-0.4.4}/portolan_cli/metadata/validation.py +0 -0
  182. {portolan_cli-0.4.2 → portolan_cli-0.4.4}/portolan_cli/models/__init__.py +0 -0
  183. {portolan_cli-0.4.2 → portolan_cli-0.4.4}/portolan_cli/models/catalog.py +0 -0
  184. {portolan_cli-0.4.2 → portolan_cli-0.4.4}/portolan_cli/models/collection.py +0 -0
  185. {portolan_cli-0.4.2 → portolan_cli-0.4.4}/portolan_cli/models/item.py +0 -0
  186. {portolan_cli-0.4.2 → portolan_cli-0.4.4}/portolan_cli/models/schema.py +0 -0
  187. {portolan_cli-0.4.2 → portolan_cli-0.4.4}/portolan_cli/models/version.py +0 -0
  188. {portolan_cli-0.4.2 → portolan_cli-0.4.4}/portolan_cli/output.py +0 -0
  189. {portolan_cli-0.4.2 → portolan_cli-0.4.4}/portolan_cli/scan_detect.py +0 -0
  190. {portolan_cli-0.4.2 → portolan_cli-0.4.4}/portolan_cli/scan_fix.py +0 -0
  191. {portolan_cli-0.4.2 → portolan_cli-0.4.4}/portolan_cli/scan_infer.py +0 -0
  192. {portolan_cli-0.4.2 → portolan_cli-0.4.4}/portolan_cli/scan_output.py +0 -0
  193. {portolan_cli-0.4.2 → portolan_cli-0.4.4}/portolan_cli/schema/__init__.py +0 -0
  194. {portolan_cli-0.4.2 → portolan_cli-0.4.4}/portolan_cli/schema/breaking.py +0 -0
  195. {portolan_cli-0.4.2 → portolan_cli-0.4.4}/portolan_cli/schema/export.py +0 -0
  196. {portolan_cli-0.4.2 → portolan_cli-0.4.4}/portolan_cli/schema/import_.py +0 -0
  197. {portolan_cli-0.4.2 → portolan_cli-0.4.4}/portolan_cli/stac.py +0 -0
  198. {portolan_cli-0.4.2 → portolan_cli-0.4.4}/portolan_cli/sync.py +0 -0
  199. {portolan_cli-0.4.2 → portolan_cli-0.4.4}/portolan_cli/upload.py +0 -0
  200. {portolan_cli-0.4.2 → portolan_cli-0.4.4}/portolan_cli/validation/__init__.py +0 -0
  201. {portolan_cli-0.4.2 → portolan_cli-0.4.4}/portolan_cli/validation/results.py +0 -0
  202. {portolan_cli-0.4.2 → portolan_cli-0.4.4}/portolan_cli/validation/runner.py +0 -0
  203. {portolan_cli-0.4.2 → portolan_cli-0.4.4}/portolan_cli/versions.py +0 -0
  204. {portolan_cli-0.4.2 → portolan_cli-0.4.4}/scripts/__init__.py +0 -0
  205. {portolan_cli-0.4.2 → portolan_cli-0.4.4}/scripts/fetch_lib_docs.py +0 -0
  206. {portolan_cli-0.4.2 → portolan_cli-0.4.4}/scripts/generate_claude_md_sections.py +0 -0
  207. {portolan_cli-0.4.2 → portolan_cli-0.4.4}/scripts/generate_skill_md.py +0 -0
  208. {portolan_cli-0.4.2 → portolan_cli-0.4.4}/scripts/orchestrate-issue.sh +0 -0
  209. {portolan_cli-0.4.2 → portolan_cli-0.4.4}/scripts/update_freshness.py +0 -0
  210. {portolan_cli-0.4.2 → portolan_cli-0.4.4}/scripts/validate_claude_md.py +0 -0
  211. {portolan_cli-0.4.2 → portolan_cli-0.4.4}/scripts/validate_skill_md.py +0 -0
  212. {portolan_cli-0.4.2 → portolan_cli-0.4.4}/tests/benchmark/__init__.py +0 -0
  213. {portolan_cli-0.4.2 → portolan_cli-0.4.4}/tests/benchmark/test_catalog_benchmark.py +0 -0
  214. {portolan_cli-0.4.2 → portolan_cli-0.4.4}/tests/benchmark/test_conversion_benchmark.py +0 -0
  215. {portolan_cli-0.4.2 → portolan_cli-0.4.4}/tests/benchmark/test_format_detection_benchmark.py +0 -0
  216. {portolan_cli-0.4.2 → portolan_cli-0.4.4}/tests/benchmark/test_metadata_benchmark.py +0 -0
  217. {portolan_cli-0.4.2 → portolan_cli-0.4.4}/tests/benchmark/test_scan_performance.py +0 -0
  218. {portolan_cli-0.4.2 → portolan_cli-0.4.4}/tests/benchmark/test_validation_benchmark.py +0 -0
  219. {portolan_cli-0.4.2 → portolan_cli-0.4.4}/tests/docs/__init__.py +0 -0
  220. {portolan_cli-0.4.2 → portolan_cli-0.4.4}/tests/docs/conftest.py +0 -0
  221. {portolan_cli-0.4.2 → portolan_cli-0.4.4}/tests/docs/test_readme_examples.py +0 -0
  222. {portolan_cli-0.4.2 → portolan_cli-0.4.4}/tests/fixtures/README.md +0 -0
  223. {portolan_cli-0.4.2 → portolan_cli-0.4.4}/tests/fixtures/edge/antimeridian.geojson +0 -0
  224. {portolan_cli-0.4.2 → portolan_cli-0.4.4}/tests/fixtures/edge/special_filename spaces.geojson +0 -0
  225. {portolan_cli-0.4.2 → portolan_cli-0.4.4}/tests/fixtures/edge/unicode_properties.geojson +0 -0
  226. {portolan_cli-0.4.2 → portolan_cli-0.4.4}/tests/fixtures/generate.py +0 -0
  227. {portolan_cli-0.4.2 → portolan_cli-0.4.4}/tests/fixtures/metadata/stac/invalid/catalog_missing_id.json +0 -0
  228. {portolan_cli-0.4.2 → portolan_cli-0.4.4}/tests/fixtures/metadata/stac/invalid/collection_bad_extent.json +0 -0
  229. {portolan_cli-0.4.2 → portolan_cli-0.4.4}/tests/fixtures/metadata/stac/invalid/item_no_geometry.json +0 -0
  230. {portolan_cli-0.4.2 → portolan_cli-0.4.4}/tests/fixtures/metadata/stac/invalid/item_wrong_type.json +0 -0
  231. {portolan_cli-0.4.2 → portolan_cli-0.4.4}/tests/fixtures/metadata/stac/valid/catalog_full.json +0 -0
  232. {portolan_cli-0.4.2 → portolan_cli-0.4.4}/tests/fixtures/metadata/stac/valid/catalog_minimal.json +0 -0
  233. {portolan_cli-0.4.2 → portolan_cli-0.4.4}/tests/fixtures/metadata/stac/valid/collection_raster.json +0 -0
  234. {portolan_cli-0.4.2 → portolan_cli-0.4.4}/tests/fixtures/metadata/stac/valid/collection_vector.json +0 -0
  235. {portolan_cli-0.4.2 → portolan_cli-0.4.4}/tests/fixtures/metadata/stac/valid/item_cog.json +0 -0
  236. {portolan_cli-0.4.2 → portolan_cli-0.4.4}/tests/fixtures/metadata/stac/valid/item_geoparquet.json +0 -0
  237. {portolan_cli-0.4.2 → portolan_cli-0.4.4}/tests/fixtures/metadata/versions/invalid/versions_bad_checksum.json +0 -0
  238. {portolan_cli-0.4.2 → portolan_cli-0.4.4}/tests/fixtures/metadata/versions/invalid/versions_duplicate.json +0 -0
  239. {portolan_cli-0.4.2 → portolan_cli-0.4.4}/tests/fixtures/metadata/versions/invalid/versions_missing_current.json +0 -0
  240. {portolan_cli-0.4.2 → portolan_cli-0.4.4}/tests/fixtures/metadata/versions/valid/versions_breaking.json +0 -0
  241. {portolan_cli-0.4.2 → portolan_cli-0.4.4}/tests/fixtures/metadata/versions/valid/versions_partial_sync.json +0 -0
  242. {portolan_cli-0.4.2 → portolan_cli-0.4.4}/tests/fixtures/metadata/versions/valid/versions_v1.json +0 -0
  243. {portolan_cli-0.4.2 → portolan_cli-0.4.4}/tests/fixtures/metadata/versions/valid/versions_v3.json +0 -0
  244. {portolan_cli-0.4.2 → portolan_cli-0.4.4}/tests/fixtures/raster/invalid/not_georeferenced.tif +0 -0
  245. {portolan_cli-0.4.2 → portolan_cli-0.4.4}/tests/fixtures/raster/invalid/truncated.tif +0 -0
  246. {portolan_cli-0.4.2 → portolan_cli-0.4.4}/tests/fixtures/raster/natural-earth-non-cog.tif +0 -0
  247. {portolan_cli-0.4.2 → portolan_cli-0.4.4}/tests/fixtures/raster/valid/float32.tif +0 -0
  248. {portolan_cli-0.4.2 → portolan_cli-0.4.4}/tests/fixtures/raster/valid/nodata.tif +0 -0
  249. {portolan_cli-0.4.2 → portolan_cli-0.4.4}/tests/fixtures/raster/valid/rgb.tif +0 -0
  250. {portolan_cli-0.4.2 → portolan_cli-0.4.4}/tests/fixtures/raster/valid/singleband.tif +0 -0
  251. {portolan_cli-0.4.2 → portolan_cli-0.4.4}/tests/fixtures/realdata/README.md +0 -0
  252. {portolan_cli-0.4.2 → portolan_cli-0.4.4}/tests/fixtures/realdata/fieldmaps-boundaries.parquet +0 -0
  253. {portolan_cli-0.4.2 → portolan_cli-0.4.4}/tests/fixtures/realdata/filegdb/field_alias.gdb/a00000001.TablesByName.atx +0 -0
  254. {portolan_cli-0.4.2 → portolan_cli-0.4.4}/tests/fixtures/realdata/filegdb/field_alias.gdb/a00000001.gdbindexes +0 -0
  255. {portolan_cli-0.4.2 → portolan_cli-0.4.4}/tests/fixtures/realdata/filegdb/field_alias.gdb/a00000001.gdbtable +0 -0
  256. {portolan_cli-0.4.2 → portolan_cli-0.4.4}/tests/fixtures/realdata/filegdb/field_alias.gdb/a00000001.gdbtablx +0 -0
  257. {portolan_cli-0.4.2 → portolan_cli-0.4.4}/tests/fixtures/realdata/filegdb/field_alias.gdb/a00000002.gdbtable +0 -0
  258. {portolan_cli-0.4.2 → portolan_cli-0.4.4}/tests/fixtures/realdata/filegdb/field_alias.gdb/a00000002.gdbtablx +0 -0
  259. {portolan_cli-0.4.2 → portolan_cli-0.4.4}/tests/fixtures/realdata/filegdb/field_alias.gdb/a00000003.gdbindexes +0 -0
  260. {portolan_cli-0.4.2 → portolan_cli-0.4.4}/tests/fixtures/realdata/filegdb/field_alias.gdb/a00000003.gdbtable +0 -0
  261. {portolan_cli-0.4.2 → portolan_cli-0.4.4}/tests/fixtures/realdata/filegdb/field_alias.gdb/a00000003.gdbtablx +0 -0
  262. {portolan_cli-0.4.2 → portolan_cli-0.4.4}/tests/fixtures/realdata/filegdb/field_alias.gdb/a00000004.CatItemsByPhysicalName.atx +0 -0
  263. {portolan_cli-0.4.2 → portolan_cli-0.4.4}/tests/fixtures/realdata/filegdb/field_alias.gdb/a00000004.CatItemsByType.atx +0 -0
  264. {portolan_cli-0.4.2 → portolan_cli-0.4.4}/tests/fixtures/realdata/filegdb/field_alias.gdb/a00000004.FDO_UUID.atx +0 -0
  265. {portolan_cli-0.4.2 → portolan_cli-0.4.4}/tests/fixtures/realdata/filegdb/field_alias.gdb/a00000004.gdbindexes +0 -0
  266. {portolan_cli-0.4.2 → portolan_cli-0.4.4}/tests/fixtures/realdata/filegdb/field_alias.gdb/a00000004.gdbtable +0 -0
  267. {portolan_cli-0.4.2 → portolan_cli-0.4.4}/tests/fixtures/realdata/filegdb/field_alias.gdb/a00000004.gdbtablx +0 -0
  268. {portolan_cli-0.4.2 → portolan_cli-0.4.4}/tests/fixtures/realdata/filegdb/field_alias.gdb/a00000004.spx +0 -0
  269. {portolan_cli-0.4.2 → portolan_cli-0.4.4}/tests/fixtures/realdata/filegdb/field_alias.gdb/a00000005.CatItemTypesByName.atx +0 -0
  270. {portolan_cli-0.4.2 → portolan_cli-0.4.4}/tests/fixtures/realdata/filegdb/field_alias.gdb/a00000005.CatItemTypesByParentTypeID.atx +0 -0
  271. {portolan_cli-0.4.2 → portolan_cli-0.4.4}/tests/fixtures/realdata/filegdb/field_alias.gdb/a00000005.CatItemTypesByUUID.atx +0 -0
  272. {portolan_cli-0.4.2 → portolan_cli-0.4.4}/tests/fixtures/realdata/filegdb/field_alias.gdb/a00000005.gdbindexes +0 -0
  273. {portolan_cli-0.4.2 → portolan_cli-0.4.4}/tests/fixtures/realdata/filegdb/field_alias.gdb/a00000005.gdbtable +0 -0
  274. {portolan_cli-0.4.2 → portolan_cli-0.4.4}/tests/fixtures/realdata/filegdb/field_alias.gdb/a00000005.gdbtablx +0 -0
  275. {portolan_cli-0.4.2 → portolan_cli-0.4.4}/tests/fixtures/realdata/filegdb/field_alias.gdb/a00000006.CatRelsByDestinationID.atx +0 -0
  276. {portolan_cli-0.4.2 → portolan_cli-0.4.4}/tests/fixtures/realdata/filegdb/field_alias.gdb/a00000006.CatRelsByOriginID.atx +0 -0
  277. {portolan_cli-0.4.2 → portolan_cli-0.4.4}/tests/fixtures/realdata/filegdb/field_alias.gdb/a00000006.CatRelsByType.atx +0 -0
  278. {portolan_cli-0.4.2 → portolan_cli-0.4.4}/tests/fixtures/realdata/filegdb/field_alias.gdb/a00000006.FDO_UUID.atx +0 -0
  279. {portolan_cli-0.4.2 → portolan_cli-0.4.4}/tests/fixtures/realdata/filegdb/field_alias.gdb/a00000006.gdbindexes +0 -0
  280. {portolan_cli-0.4.2 → portolan_cli-0.4.4}/tests/fixtures/realdata/filegdb/field_alias.gdb/a00000006.gdbtable +0 -0
  281. {portolan_cli-0.4.2 → portolan_cli-0.4.4}/tests/fixtures/realdata/filegdb/field_alias.gdb/a00000006.gdbtablx +0 -0
  282. {portolan_cli-0.4.2 → portolan_cli-0.4.4}/tests/fixtures/realdata/filegdb/field_alias.gdb/a00000007.CatRelTypesByBackwardLabel.atx +0 -0
  283. {portolan_cli-0.4.2 → portolan_cli-0.4.4}/tests/fixtures/realdata/filegdb/field_alias.gdb/a00000007.CatRelTypesByDestItemTypeID.atx +0 -0
  284. {portolan_cli-0.4.2 → portolan_cli-0.4.4}/tests/fixtures/realdata/filegdb/field_alias.gdb/a00000007.CatRelTypesByForwardLabel.atx +0 -0
  285. {portolan_cli-0.4.2 → portolan_cli-0.4.4}/tests/fixtures/realdata/filegdb/field_alias.gdb/a00000007.CatRelTypesByName.atx +0 -0
  286. {portolan_cli-0.4.2 → portolan_cli-0.4.4}/tests/fixtures/realdata/filegdb/field_alias.gdb/a00000007.CatRelTypesByOriginItemTypeID.atx +0 -0
  287. {portolan_cli-0.4.2 → portolan_cli-0.4.4}/tests/fixtures/realdata/filegdb/field_alias.gdb/a00000007.CatRelTypesByUUID.atx +0 -0
  288. {portolan_cli-0.4.2 → portolan_cli-0.4.4}/tests/fixtures/realdata/filegdb/field_alias.gdb/a00000007.gdbindexes +0 -0
  289. {portolan_cli-0.4.2 → portolan_cli-0.4.4}/tests/fixtures/realdata/filegdb/field_alias.gdb/a00000007.gdbtable +0 -0
  290. {portolan_cli-0.4.2 → portolan_cli-0.4.4}/tests/fixtures/realdata/filegdb/field_alias.gdb/a00000007.gdbtablx +0 -0
  291. {portolan_cli-0.4.2 → portolan_cli-0.4.4}/tests/fixtures/realdata/filegdb/field_alias.gdb/a00000009.gdbindexes +0 -0
  292. {portolan_cli-0.4.2 → portolan_cli-0.4.4}/tests/fixtures/realdata/filegdb/field_alias.gdb/a00000009.gdbtable +0 -0
  293. {portolan_cli-0.4.2 → portolan_cli-0.4.4}/tests/fixtures/realdata/filegdb/field_alias.gdb/a00000009.gdbtablx +0 -0
  294. {portolan_cli-0.4.2 → portolan_cli-0.4.4}/tests/fixtures/realdata/filegdb/field_alias.gdb/a00000009.spx +0 -0
  295. {portolan_cli-0.4.2 → portolan_cli-0.4.4}/tests/fixtures/realdata/filegdb/field_alias.gdb/a0000000a.gdbindexes +0 -0
  296. {portolan_cli-0.4.2 → portolan_cli-0.4.4}/tests/fixtures/realdata/filegdb/field_alias.gdb/a0000000a.gdbtable +0 -0
  297. {portolan_cli-0.4.2 → portolan_cli-0.4.4}/tests/fixtures/realdata/filegdb/field_alias.gdb/a0000000a.gdbtablx +0 -0
  298. {portolan_cli-0.4.2 → portolan_cli-0.4.4}/tests/fixtures/realdata/filegdb/field_alias.gdb/a0000000b.gdbindexes +0 -0
  299. {portolan_cli-0.4.2 → portolan_cli-0.4.4}/tests/fixtures/realdata/filegdb/field_alias.gdb/a0000000b.gdbtable +0 -0
  300. {portolan_cli-0.4.2 → portolan_cli-0.4.4}/tests/fixtures/realdata/filegdb/field_alias.gdb/a0000000b.gdbtablx +0 -0
  301. {portolan_cli-0.4.2 → portolan_cli-0.4.4}/tests/fixtures/realdata/filegdb/field_alias.gdb/a0000000c.band_index.atx +0 -0
  302. {portolan_cli-0.4.2 → portolan_cli-0.4.4}/tests/fixtures/realdata/filegdb/field_alias.gdb/a0000000c.blk_key_index.atx +0 -0
  303. {portolan_cli-0.4.2 → portolan_cli-0.4.4}/tests/fixtures/realdata/filegdb/field_alias.gdb/a0000000c.col_index.atx +0 -0
  304. {portolan_cli-0.4.2 → portolan_cli-0.4.4}/tests/fixtures/realdata/filegdb/field_alias.gdb/a0000000c.gdbindexes +0 -0
  305. {portolan_cli-0.4.2 → portolan_cli-0.4.4}/tests/fixtures/realdata/filegdb/field_alias.gdb/a0000000c.gdbtable +0 -0
  306. {portolan_cli-0.4.2 → portolan_cli-0.4.4}/tests/fixtures/realdata/filegdb/field_alias.gdb/a0000000c.gdbtablx +0 -0
  307. {portolan_cli-0.4.2 → portolan_cli-0.4.4}/tests/fixtures/realdata/filegdb/field_alias.gdb/a0000000c.row_index.atx +0 -0
  308. {portolan_cli-0.4.2 → portolan_cli-0.4.4}/tests/fixtures/realdata/filegdb/field_alias.gdb/a0000000d.gdbindexes +0 -0
  309. {portolan_cli-0.4.2 → portolan_cli-0.4.4}/tests/fixtures/realdata/filegdb/field_alias.gdb/a0000000d.gdbtable +0 -0
  310. {portolan_cli-0.4.2 → portolan_cli-0.4.4}/tests/fixtures/realdata/filegdb/field_alias.gdb/a0000000d.gdbtablx +0 -0
  311. {portolan_cli-0.4.2 → portolan_cli-0.4.4}/tests/fixtures/realdata/filegdb/field_alias.gdb/gdb +0 -0
  312. {portolan_cli-0.4.2 → portolan_cli-0.4.4}/tests/fixtures/realdata/filegdb/field_alias.gdb/timestamps +0 -0
  313. {portolan_cli-0.4.2 → portolan_cli-0.4.4}/tests/fixtures/realdata/nwi-wetlands.parquet +0 -0
  314. {portolan_cli-0.4.2 → portolan_cli-0.4.4}/tests/fixtures/realdata/open-buildings.parquet +0 -0
  315. {portolan_cli-0.4.2 → portolan_cli-0.4.4}/tests/fixtures/realdata/rapidai4eo-sample.tif +0 -0
  316. {portolan_cli-0.4.2 → portolan_cli-0.4.4}/tests/fixtures/realdata/road-detections.parquet +0 -0
  317. {portolan_cli-0.4.2 → portolan_cli-0.4.4}/tests/fixtures/scan/README.md +0 -0
  318. {portolan_cli-0.4.2 → portolan_cli-0.4.4}/tests/fixtures/scan/clean_flat/argentina.geojson +0 -0
  319. {portolan_cli-0.4.2 → portolan_cli-0.4.4}/tests/fixtures/scan/clean_flat/example.parquet +0 -0
  320. {portolan_cli-0.4.2 → portolan_cli-0.4.4}/tests/fixtures/scan/clean_flat/seaLevelDistanceGeoJSON.geojson +0 -0
  321. {portolan_cli-0.4.2 → portolan_cli-0.4.4}/tests/fixtures/scan/complete_shapefile/radios_sample.dbf +0 -0
  322. {portolan_cli-0.4.2 → portolan_cli-0.4.4}/tests/fixtures/scan/complete_shapefile/radios_sample.prj +0 -0
  323. {portolan_cli-0.4.2 → portolan_cli-0.4.4}/tests/fixtures/scan/complete_shapefile/radios_sample.shp +0 -0
  324. {portolan_cli-0.4.2 → portolan_cli-0.4.4}/tests/fixtures/scan/complete_shapefile/radios_sample.shx +0 -0
  325. {portolan_cli-0.4.2 → portolan_cli-0.4.4}/tests/fixtures/scan/duplicate_basenames/dir_a/argentina.geojson +0 -0
  326. {portolan_cli-0.4.2 → portolan_cli-0.4.4}/tests/fixtures/scan/duplicate_basenames/dir_b/argentina.geojson +0 -0
  327. {portolan_cli-0.4.2 → portolan_cli-0.4.4}/tests/fixtures/scan/filegdb/sample.gdb/a00000001.gdbtable +0 -0
  328. {portolan_cli-0.4.2 → portolan_cli-0.4.4}/tests/fixtures/scan/filegdb/sample.gdb/a00000001.gdbtablx +0 -0
  329. {portolan_cli-0.4.2 → portolan_cli-0.4.4}/tests/fixtures/scan/filegdb/sample.gdb/gdb +0 -0
  330. {portolan_cli-0.4.2 → portolan_cli-0.4.4}/tests/fixtures/scan/filegdb/sample.gdb.zip +0 -0
  331. {portolan_cli-0.4.2 → portolan_cli-0.4.4}/tests/fixtures/scan/incomplete_shapefile/radios_sample.prj +0 -0
  332. {portolan_cli-0.4.2 → portolan_cli-0.4.4}/tests/fixtures/scan/incomplete_shapefile/radios_sample.shp +0 -0
  333. {portolan_cli-0.4.2 → portolan_cli-0.4.4}/tests/fixtures/scan/incomplete_shapefile/radios_sample.shx +0 -0
  334. {portolan_cli-0.4.2 → portolan_cli-0.4.4}/tests/fixtures/scan/invalid_chars/data (copy).parquet +0 -0
  335. {portolan_cli-0.4.2 → portolan_cli-0.4.4}/tests/fixtures/scan/invalid_chars/donn/303/251es.geojson" +0 -0
  336. {portolan_cli-0.4.2 → portolan_cli-0.4.4}/tests/fixtures/scan/mixed_formats/ID1_N80_W170_RP10_depth_reclass.tif +0 -0
  337. {portolan_cli-0.4.2 → portolan_cli-0.4.4}/tests/fixtures/scan/mixed_formats/example.parquet +0 -0
  338. {portolan_cli-0.4.2 → portolan_cli-0.4.4}/tests/fixtures/scan/multiple_primaries/la_plata_L1.geojson +0 -0
  339. {portolan_cli-0.4.2 → portolan_cli-0.4.4}/tests/fixtures/scan/multiple_primaries/la_plata_L2.geojson +0 -0
  340. {portolan_cli-0.4.2 → portolan_cli-0.4.4}/tests/fixtures/scan/multiple_primaries/la_plata_L3.geojson +0 -0
  341. {portolan_cli-0.4.2 → portolan_cli-0.4.4}/tests/fixtures/scan/nested/census/2020/boundaries.geojson +0 -0
  342. {portolan_cli-0.4.2 → portolan_cli-0.4.4}/tests/fixtures/scan/nested/census/2022/boundaries.geojson +0 -0
  343. {portolan_cli-0.4.2 → portolan_cli-0.4.4}/tests/fixtures/scan/nested/imagery/2024/flood_depth.tif +0 -0
  344. {portolan_cli-0.4.2 → portolan_cli-0.4.4}/tests/fixtures/scan/unsupported/argentina.geojson +0 -0
  345. {portolan_cli-0.4.2 → portolan_cli-0.4.4}/tests/fixtures/scan/unsupported/metadata.csv +0 -0
  346. {portolan_cli-0.4.2 → portolan_cli-0.4.4}/tests/fixtures/scan/unsupported/project.mxd +0 -0
  347. {portolan_cli-0.4.2 → portolan_cli-0.4.4}/tests/fixtures/validation/pmtiles/README.md +0 -0
  348. {portolan_cli-0.4.2 → portolan_cli-0.4.4}/tests/fixtures/validation/pmtiles/sample.parquet +0 -0
  349. {portolan_cli-0.4.2 → portolan_cli-0.4.4}/tests/fixtures/validation/pmtiles/sample.pmtiles +0 -0
  350. {portolan_cli-0.4.2 → portolan_cli-0.4.4}/tests/fixtures/vector/invalid/empty.geojson +0 -0
  351. {portolan_cli-0.4.2 → portolan_cli-0.4.4}/tests/fixtures/vector/invalid/malformed.geojson +0 -0
  352. {portolan_cli-0.4.2 → portolan_cli-0.4.4}/tests/fixtures/vector/invalid/no_geometry.json +0 -0
  353. {portolan_cli-0.4.2 → portolan_cli-0.4.4}/tests/fixtures/vector/invalid/null_geometries.geojson +0 -0
  354. {portolan_cli-0.4.2 → portolan_cli-0.4.4}/tests/fixtures/vector/open-buildings-utm31n.parquet +0 -0
  355. {portolan_cli-0.4.2 → portolan_cli-0.4.4}/tests/fixtures/vector/valid/large_properties.geojson +0 -0
  356. {portolan_cli-0.4.2 → portolan_cli-0.4.4}/tests/fixtures/vector/valid/lines.geojson +0 -0
  357. {portolan_cli-0.4.2 → portolan_cli-0.4.4}/tests/fixtures/vector/valid/multigeom.geojson +0 -0
  358. {portolan_cli-0.4.2 → portolan_cli-0.4.4}/tests/fixtures/vector/valid/points.geojson +0 -0
  359. {portolan_cli-0.4.2 → portolan_cli-0.4.4}/tests/fixtures/vector/valid/polygons.geojson +0 -0
  360. {portolan_cli-0.4.2 → portolan_cli-0.4.4}/tests/integration/test_check_conversion_config.py +0 -0
  361. {portolan_cli-0.4.2 → portolan_cli-0.4.4}/tests/integration/test_cli_json.py +0 -0
  362. {portolan_cli-0.4.2 → portolan_cli-0.4.4}/tests/integration/test_clone_ergonomics_integration.py +0 -0
  363. {portolan_cli-0.4.2 → portolan_cli-0.4.4}/tests/integration/test_config_command.py +0 -0
  364. {portolan_cli-0.4.2 → portolan_cli-0.4.4}/tests/integration/test_convert_workflow.py +0 -0
  365. {portolan_cli-0.4.2 → portolan_cli-0.4.4}/tests/integration/test_dataset_warnings.py +0 -0
  366. {portolan_cli-0.4.2 → portolan_cli-0.4.4}/tests/integration/test_download_integration.py +0 -0
  367. {portolan_cli-0.4.2 → portolan_cli-0.4.4}/tests/integration/test_fetch_lib_docs_integration.py +0 -0
  368. {portolan_cli-0.4.2 → portolan_cli-0.4.4}/tests/integration/test_gpio_integration.py +0 -0
  369. {portolan_cli-0.4.2 → portolan_cli-0.4.4}/tests/integration/test_init_command.py +0 -0
  370. {portolan_cli-0.4.2 → portolan_cli-0.4.4}/tests/integration/test_init_v2.py +0 -0
  371. {portolan_cli-0.4.2 → portolan_cli-0.4.4}/tests/integration/test_pull_integration.py +0 -0
  372. {portolan_cli-0.4.2 → portolan_cli-0.4.4}/tests/integration/test_realdata_metadata.py +0 -0
  373. {portolan_cli-0.4.2 → portolan_cli-0.4.4}/tests/integration/test_riocogeo_integration.py +0 -0
  374. {portolan_cli-0.4.2 → portolan_cli-0.4.4}/tests/integration/test_schema_roundtrip.py +0 -0
  375. {portolan_cli-0.4.2 → portolan_cli-0.4.4}/tests/integration/test_sync_integration.py +0 -0
  376. {portolan_cli-0.4.2 → portolan_cli-0.4.4}/tests/integration/test_upload_integration.py +0 -0
  377. {portolan_cli-0.4.2 → portolan_cli-0.4.4}/tests/network/__init__.py +0 -0
  378. {portolan_cli-0.4.2 → portolan_cli-0.4.4}/tests/network/test_network_placeholder.py +0 -0
  379. {portolan_cli-0.4.2 → portolan_cli-0.4.4}/tests/test_placeholder.py +0 -0
  380. {portolan_cli-0.4.2 → portolan_cli-0.4.4}/tests/unit/__init__.py +0 -0
  381. {portolan_cli-0.4.2 → portolan_cli-0.4.4}/tests/unit/backends/__init__.py +0 -0
  382. {portolan_cli-0.4.2 → portolan_cli-0.4.4}/tests/unit/backends/test_backends.py +0 -0
  383. {portolan_cli-0.4.2 → portolan_cli-0.4.4}/tests/unit/metadata/__init__.py +0 -0
  384. {portolan_cli-0.4.2 → portolan_cli-0.4.4}/tests/unit/models/__init__.py +0 -0
  385. {portolan_cli-0.4.2 → portolan_cli-0.4.4}/tests/unit/models/test_catalog.py +0 -0
  386. {portolan_cli-0.4.2 → portolan_cli-0.4.4}/tests/unit/models/test_collection.py +0 -0
  387. {portolan_cli-0.4.2 → portolan_cli-0.4.4}/tests/unit/models/test_item.py +0 -0
  388. {portolan_cli-0.4.2 → portolan_cli-0.4.4}/tests/unit/models/test_schema.py +0 -0
  389. {portolan_cli-0.4.2 → portolan_cli-0.4.4}/tests/unit/models/test_version.py +0 -0
  390. {portolan_cli-0.4.2 → portolan_cli-0.4.4}/tests/unit/schema/__init__.py +0 -0
  391. {portolan_cli-0.4.2 → portolan_cli-0.4.4}/tests/unit/schema/test_breaking.py +0 -0
  392. {portolan_cli-0.4.2 → portolan_cli-0.4.4}/tests/unit/schema/test_export.py +0 -0
  393. {portolan_cli-0.4.2 → portolan_cli-0.4.4}/tests/unit/schema/test_import.py +0 -0
  394. {portolan_cli-0.4.2 → portolan_cli-0.4.4}/tests/unit/test_catalog_init.py +0 -0
  395. {portolan_cli-0.4.2 → portolan_cli-0.4.4}/tests/unit/test_catalog_model_init.py +0 -0
  396. {portolan_cli-0.4.2 → portolan_cli-0.4.4}/tests/unit/test_catalog_versions_collections.py +0 -0
  397. {portolan_cli-0.4.2 → portolan_cli-0.4.4}/tests/unit/test_cli_config.py +0 -0
  398. {portolan_cli-0.4.2 → portolan_cli-0.4.4}/tests/unit/test_cli_info.py +0 -0
  399. {portolan_cli-0.4.2 → portolan_cli-0.4.4}/tests/unit/test_cli_init.py +0 -0
  400. {portolan_cli-0.4.2 → portolan_cli-0.4.4}/tests/unit/test_clone_ergonomics.py +0 -0
  401. {portolan_cli-0.4.2 → portolan_cli-0.4.4}/tests/unit/test_collection_creation.py +0 -0
  402. {portolan_cli-0.4.2 → portolan_cli-0.4.4}/tests/unit/test_collection_id.py +0 -0
  403. {portolan_cli-0.4.2 → portolan_cli-0.4.4}/tests/unit/test_config.py +0 -0
  404. {portolan_cli-0.4.2 → portolan_cli-0.4.4}/tests/unit/test_conversion_config.py +0 -0
  405. {portolan_cli-0.4.2 → portolan_cli-0.4.4}/tests/unit/test_convert.py +0 -0
  406. {portolan_cli-0.4.2 → portolan_cli-0.4.4}/tests/unit/test_download.py +0 -0
  407. {portolan_cli-0.4.2 → portolan_cli-0.4.4}/tests/unit/test_errors.py +0 -0
  408. {portolan_cli-0.4.2 → portolan_cli-0.4.4}/tests/unit/test_fetch_lib_docs.py +0 -0
  409. {portolan_cli-0.4.2 → portolan_cli-0.4.4}/tests/unit/test_filegdb_add_support.py +0 -0
  410. {portolan_cli-0.4.2 → portolan_cli-0.4.4}/tests/unit/test_fixtures.py +0 -0
  411. {portolan_cli-0.4.2 → portolan_cli-0.4.4}/tests/unit/test_formats_cloud_native.py +0 -0
  412. {portolan_cli-0.4.2 → portolan_cli-0.4.4}/tests/unit/test_formats_convertible.py +0 -0
  413. {portolan_cli-0.4.2 → portolan_cli-0.4.4}/tests/unit/test_formats_edge_cases.py +0 -0
  414. {portolan_cli-0.4.2 → portolan_cli-0.4.4}/tests/unit/test_formats_effective_status.py +0 -0
  415. {portolan_cli-0.4.2 → portolan_cli-0.4.4}/tests/unit/test_formats_status.py +0 -0
  416. {portolan_cli-0.4.2 → portolan_cli-0.4.4}/tests/unit/test_formats_unsupported.py +0 -0
  417. {portolan_cli-0.4.2 → portolan_cli-0.4.4}/tests/unit/test_inspect.py +0 -0
  418. {portolan_cli-0.4.2 → portolan_cli-0.4.4}/tests/unit/test_item_creation.py +0 -0
  419. {portolan_cli-0.4.2 → portolan_cli-0.4.4}/tests/unit/test_json_output.py +0 -0
  420. {portolan_cli-0.4.2 → portolan_cli-0.4.4}/tests/unit/test_metadata_cog.py +0 -0
  421. {portolan_cli-0.4.2 → portolan_cli-0.4.4}/tests/unit/test_metadata_detection.py +0 -0
  422. {portolan_cli-0.4.2 → portolan_cli-0.4.4}/tests/unit/test_metadata_fix.py +0 -0
  423. {portolan_cli-0.4.2 → portolan_cli-0.4.4}/tests/unit/test_metadata_geoparquet.py +0 -0
  424. {portolan_cli-0.4.2 → portolan_cli-0.4.4}/tests/unit/test_metadata_models.py +0 -0
  425. {portolan_cli-0.4.2 → portolan_cli-0.4.4}/tests/unit/test_metadata_update.py +0 -0
  426. {portolan_cli-0.4.2 → portolan_cli-0.4.4}/tests/unit/test_metadata_validation.py +0 -0
  427. {portolan_cli-0.4.2 → portolan_cli-0.4.4}/tests/unit/test_output.py +0 -0
  428. {portolan_cli-0.4.2 → portolan_cli-0.4.4}/tests/unit/test_push_default_remote.py +0 -0
  429. {portolan_cli-0.4.2 → portolan_cli-0.4.4}/tests/unit/test_scan_coverage.py +0 -0
  430. {portolan_cli-0.4.2 → portolan_cli-0.4.4}/tests/unit/test_scan_detect.py +0 -0
  431. {portolan_cli-0.4.2 → portolan_cli-0.4.4}/tests/unit/test_scan_filegdb_discovery.py +0 -0
  432. {portolan_cli-0.4.2 → portolan_cli-0.4.4}/tests/unit/test_scan_fix.py +0 -0
  433. {portolan_cli-0.4.2 → portolan_cli-0.4.4}/tests/unit/test_scan_fix_coverage.py +0 -0
  434. {portolan_cli-0.4.2 → portolan_cli-0.4.4}/tests/unit/test_scan_infer.py +0 -0
  435. {portolan_cli-0.4.2 → portolan_cli-0.4.4}/tests/unit/test_scan_infer_coverage.py +0 -0
  436. {portolan_cli-0.4.2 → portolan_cli-0.4.4}/tests/unit/test_scan_output.py +0 -0
  437. {portolan_cli-0.4.2 → portolan_cli-0.4.4}/tests/unit/test_schema_extraction.py +0 -0
  438. {portolan_cli-0.4.2 → portolan_cli-0.4.4}/tests/unit/test_scripts.py +0 -0
  439. {portolan_cli-0.4.2 → portolan_cli-0.4.4}/tests/unit/test_stac.py +0 -0
  440. {portolan_cli-0.4.2 → portolan_cli-0.4.4}/tests/unit/test_stac_fixtures.py +0 -0
  441. {portolan_cli-0.4.2 → portolan_cli-0.4.4}/tests/unit/test_state_detection.py +0 -0
  442. {portolan_cli-0.4.2 → portolan_cli-0.4.4}/tests/unit/test_upload.py +0 -0
  443. {portolan_cli-0.4.2 → portolan_cli-0.4.4}/tests/unit/test_validation_results.py +0 -0
  444. {portolan_cli-0.4.2 → portolan_cli-0.4.4}/tests/unit/test_validation_runner.py +0 -0
  445. {portolan_cli-0.4.2 → portolan_cli-0.4.4}/tests/unit/test_versions.py +0 -0
  446. {portolan_cli-0.4.2 → portolan_cli-0.4.4}/tests/unit/test_versions_fixtures.py +0 -0
  447. {portolan_cli-0.4.2 → portolan_cli-0.4.4}/vulture_whitelist.py +0 -0
@@ -43,6 +43,16 @@ repos:
43
43
 
44
44
  - repo: local
45
45
  hooks:
46
+ # === SAFETY HOOKS ===
47
+ # Prevent accidental commits of temporary/generated content
48
+
49
+ - id: forbid-worktrees
50
+ name: forbid worktree files
51
+ entry: "bash -c 'echo ERROR - Worktree files should not be committed. Remove .claude/worktrees/ from staging. && exit 1'"
52
+ language: system
53
+ files: ^\.claude/worktrees/
54
+ pass_filenames: false
55
+
46
56
  # === AUTO-UPDATE HOOKS ===
47
57
  # These regenerate content from code - keeps docs in sync automatically
48
58
 
@@ -1,3 +1,44 @@
1
+ ## v0.4.4 (2026-03-07)
2
+
3
+ ### Feat
4
+
5
+ - **scan**: batch and summarize repeated warnings (#194)
6
+ - **add**: accept multiple paths like git add (#189)
7
+ - **add**: support tabular parquet as auxiliary assets (#190)
8
+
9
+ ### Fix
10
+
11
+ - **list**: show all assets grouped by item (#196) (#204)
12
+ - **add**: batch repetitive failures by error message (#199) (#205)
13
+ - **add**: accept PMTiles as cloud-native primary asset (#201)
14
+ - **status**: treat FileGDB directories as single assets (#187)
15
+ - **status**: detect and skip symlink cycles (#188)
16
+ - **add**: continue on errors and report all failures at end (#191)
17
+ - **scan**: list specific unrecognized files instead of just count (#185)
18
+ - **scan**: make PATH argument optional, default to current directory (#184)
19
+ - **scan**: remove non-existent --bundle flag reference from suggestion (#183)
20
+
21
+ ## v0.4.3 (2026-03-07)
22
+
23
+ ### BREAKING CHANGE
24
+
25
+ - Removes --fix-metadata flag; use --metadata --fix instead.
26
+
27
+ ### Feat
28
+
29
+ - **cli**: add portolan clean command to remove metadata (#172)
30
+ - **cli**: add --item-id flag to portolan add command (#171)
31
+ - **check**: redesign --fix to work orthogonally with --metadata/--geo-assets (#164)
32
+
33
+ ### Fix
34
+
35
+ - **dataset**: make add_dataset atomic and track files in-place (#170)
36
+ - **catalog**: unify catalog root detection (#162) (#169)
37
+ - **status**: detect untracked files in uninitialized collections (#167)
38
+ - **dry-run**: prevent network calls in dry-run mode (#168)
39
+ - **add**: support recursive add at catalog root (#166)
40
+ - **list**: add guidance when no items found (#165)
41
+
1
42
  ## v0.4.2 (2026-03-06)
2
43
 
3
44
  ### Feat
@@ -100,6 +100,7 @@ AI agents will write most of the code. Human review does not scale to match AI o
100
100
  | [0026](context/shared/adr/0026-conversion-config-design.md) | Conversion config: extension/path overrides, precedence rules |
101
101
  | [0027](context/shared/adr/0027-unified-config-yaml-sentinel.md) | Unified config.yaml as sentinel and user config (eliminates config.json) |
102
102
  | [0028](context/shared/adr/0028-all-files-as-assets.md) | Track ALL files in item directories as assets |
103
+ | [0029](context/shared/adr/0029-unified-catalog-root-detection.md) | Unified catalog root detection via .portolan/config.yaml |
103
104
 
104
105
  ## Common Commands
105
106
 
@@ -375,7 +376,7 @@ error("No geometry column (required)") # ✗ Red X
375
376
  detail("Processing chunk 3/10...") # Dimmed text
376
377
  ```
377
378
 
378
- <!-- freshness: last-verified: 2026-03-06 -->
379
+ <!-- freshness: last-verified: 2026-03-07 -->
379
380
  ## Design Principles
380
381
 
381
382
  | Principle | Meaning | ADR |
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: portolan-cli
3
- Version: 0.4.2
3
+ Version: 0.4.4
4
4
  Summary: A CLI tool for managing cloud-native geospatial data
5
5
  Project-URL: Homepage, https://github.com/portolan-sdi/portolan-cli
6
6
  Project-URL: Bug Tracker, https://github.com/portolan-sdi/portolan-cli/issues
@@ -23,6 +23,8 @@ Portolan is a CLI for publishing and managing **cloud-native geospatial data cat
23
23
  Track files in the catalog.
24
24
 
25
25
  ```bash
26
+ portolan add demographics/census.parquet
27
+ portolan add file1.geojson file2.geojson # Add multiple files
26
28
  portolan add imagery/ # Add all files in directory
27
29
  portolan add . # Add all files in catalog
28
30
  ```
@@ -34,7 +36,15 @@ Validate a Portolan catalog or check files for cloud-native status.
34
36
  portolan check # Validate all (metadata + geo-assets)
35
37
  portolan check --metadata # Validate metadata only
36
38
  portolan check --geo-assets # Check geo-assets only
37
- portolan check /data --fix # Convert files to cloud-native
39
+ portolan check --fix # Fix both metadata and geo-assets
40
+ ```
41
+
42
+ ### `portolan clean`
43
+ Remove all Portolan metadata while preserving data files.
44
+
45
+ ```bash
46
+ portolan clean # Remove all metadata
47
+ portolan clean --dry-run # Preview what would be removed
38
48
  ```
39
49
 
40
50
  ### `portolan clone`
@@ -123,10 +133,10 @@ portolan rm -f vectors/ # Force remove entire directory
123
133
  Scan a directory for geospatial files and potential issues.
124
134
 
125
135
  ```bash
136
+ portolan scan # Scan current directory
137
+ portolan scan --json # JSON output in current directory
126
138
  portolan scan /data/geospatial
127
- portolan scan . --json
128
139
  portolan scan /large/tree --max-depth=2
129
- portolan scan /data --no-recursive
130
140
  ```
131
141
 
132
142
  ### `portolan status`
@@ -0,0 +1,125 @@
1
+ # ADR-0029: Unified Catalog Root Detection
2
+
3
+ ## Status
4
+
5
+ Accepted
6
+
7
+ ## Context
8
+
9
+ Portolan CLI has inconsistent catalog root detection across commands, causing confusing behavior where users can add files successfully but then can't see them with `list`.
10
+
11
+ ### Current State (Pre-fix)
12
+
13
+ Two competing functions detect catalog roots using different criteria:
14
+
15
+ | Function | Location | Sentinel | Used By |
16
+ |----------|----------|----------|---------|
17
+ | `find_catalog_root()` | `dataset.py` | `catalog.json` | `status`, `add`, `rm` |
18
+ | `_find_catalog_root()` | `cli.py` | `.portolan/` directory | `config get/set/list/unset` |
19
+
20
+ Additionally, `check` only looks at the current directory without walking up.
21
+
22
+ ### Problems
23
+
24
+ 1. **Shadow catalogs:** `add` auto-creates structure, but `list` looks for parent catalog, so added items are invisible
25
+ 2. **Inconsistent behavior:** `status` shows sibling directories from parent catalog when user is in subdirectory
26
+ 3. **Trust erosion:** Commands report contradictory states
27
+
28
+ ### Related Issues
29
+
30
+ - GitHub Issue #162: Inconsistent catalog root detection across CLI commands
31
+ - GitHub Issue #137: Multiple related bugs that may be symptoms of this root cause
32
+
33
+ ## Decision
34
+
35
+ **Use `.portolan/config.yaml` as the single sentinel for catalog root detection.**
36
+
37
+ All commands will use a unified `find_catalog_root()` function in `catalog.py` that:
38
+
39
+ 1. Walks up from the current directory (or specified path)
40
+ 2. Looks for `.portolan/config.yaml` (not `catalog.json`, not bare `.portolan/`)
41
+ 3. Returns the catalog root path, or `None` if not found
42
+ 4. Enforces a depth limit (`MAX_CATALOG_SEARCH_DEPTH = 20`) for security
43
+
44
+ This aligns with ADR-0027, which established `config.yaml` as the unified sentinel file for managed catalogs.
45
+
46
+ ### Behavior Changes
47
+
48
+ | Command | Old Behavior | New Behavior |
49
+ |---------|--------------|--------------|
50
+ | `add` | Auto-creates catalog structure if missing | Fails with "run `portolan init`" message |
51
+ | `list` | Uses explicit `--catalog` argument (default `.`) | *Unchanged* - uses `--catalog` arg, not `find_catalog_root()` |
52
+ | `status` | Finds parent catalog via `catalog.json` | Finds parent catalog via `.portolan/config.yaml` |
53
+ | `rm` | Finds parent catalog via `catalog.json` | Finds parent catalog via `.portolan/config.yaml` |
54
+ | `config *` | Finds catalog via `.portolan/` directory | Finds catalog via `.portolan/config.yaml` |
55
+
56
+ > **Note:** `list` is excluded from the unified `find_catalog_root()` treatment because it takes `--catalog` as an explicit argument. This is a separate issue to address in a future PR.
57
+
58
+ ### UNMANAGED_STAC Impact
59
+
60
+ Directories with only `catalog.json` (no `.portolan/`) will no longer be detected as catalogs. Users must run `portolan init` or `portolan adopt` to manage existing STAC catalogs.
61
+
62
+ This is acceptable because:
63
+ - UNMANAGED_STAC support was always incomplete (some commands worked, others didn't)
64
+ - Clear error messages guide users to the fix
65
+ - Pre-v1.0, so breaking changes are acceptable
66
+
67
+ ## Implementation
68
+
69
+ ### Single Function Location
70
+
71
+ The unified `find_catalog_root()` lives in `portolan_cli/catalog.py` alongside `detect_state()`, since both deal with catalog detection logic.
72
+
73
+ ```python
74
+ def find_catalog_root(start_path: Path | None = None) -> Path | None:
75
+ """Find catalog root by walking up to find .portolan/config.yaml.
76
+
77
+ Per ADR-0029, uses .portolan/config.yaml as the single sentinel,
78
+ unifying detection across all CLI commands.
79
+
80
+ Security: Limited to MAX_CATALOG_SEARCH_DEPTH (20) levels.
81
+ """
82
+ ```
83
+
84
+ ### Deleted Code
85
+
86
+ - `find_catalog_root()` from `dataset.py` (looked for `catalog.json`)
87
+ - `_find_catalog_root()` from `cli.py` (looked for `.portolan/` directory)
88
+
89
+ ## Consequences
90
+
91
+ ### Benefits
92
+
93
+ - **Consistent behavior:** All commands agree on catalog root
94
+ - **No shadow catalogs:** `add` requires `init` first, eliminating invisible items
95
+ - **Simpler mental model:** One sentinel, one function, one behavior
96
+ - **Aligns with ADR-0027:** Uses the established `config.yaml` sentinel
97
+
98
+ ### Trade-offs
99
+
100
+ - **UNMANAGED_STAC no longer auto-detected:** Users must explicitly init/adopt
101
+ - **`add` no longer auto-creates:** Requires explicit `init` first (git-style)
102
+
103
+ ### Migration
104
+
105
+ None required. Pre-v1.0 with minimal users. Existing managed catalogs already have `.portolan/config.yaml`.
106
+
107
+ ## Alternatives Considered
108
+
109
+ ### Option A: Use `catalog.json` as sentinel everywhere
110
+
111
+ **Rejected:** Would still allow "unmanaged" catalogs that only partially work. The root cause is supporting two modes (managed/unmanaged) inconsistently.
112
+
113
+ ### Option B: Current-directory-only (no walking up)
114
+
115
+ **Rejected:** Breaks git-style UX that users expect. Commands should work from subdirectories.
116
+
117
+ ### Option C: Distinguish managed/unmanaged with two-tier behavior
118
+
119
+ **Rejected:** More complex, doesn't eliminate the inconsistency problem, and UNMANAGED_STAC was never fully supported anyway.
120
+
121
+ ## References
122
+
123
+ - [ADR-0027: Unified config.yaml as Sentinel](0027-unified-config-yaml-sentinel.md)
124
+ - [ADR-0023: STAC Structure Separation](0023-stac-structure-separation.md)
125
+ - [GitHub Issue #162](https://github.com/portolan-sdi/portolan-cli/issues/162)
@@ -95,6 +95,87 @@ def detect_state(path: Path) -> CatalogState:
95
95
  return CatalogState.FRESH
96
96
 
97
97
 
98
+ def find_catalog_root(
99
+ start_path: Path | None = None,
100
+ *,
101
+ require_operational: bool = True,
102
+ ) -> Path | None:
103
+ """Find the catalog root by walking up from the given path.
104
+
105
+ Searches for a managed Portolan catalog starting from start_path (or cwd if None)
106
+ and walking up parent directories. This provides git-style behavior
107
+ where commands work from any subdirectory within a catalog.
108
+
109
+ Per ADR-0029, this uses .portolan/config.yaml as the primary sentinel,
110
+ unifying detection across all CLI commands. By default (require_operational=True),
111
+ it also requires at least one operational file (catalog.json or state.json)
112
+ to avoid detecting half-initialized repos.
113
+
114
+ Security: Limited to MAX_CATALOG_SEARCH_DEPTH levels to prevent
115
+ traversing to filesystem root where a malicious .portolan might exist.
116
+
117
+ Args:
118
+ start_path: Starting directory for search (defaults to cwd).
119
+ require_operational: If True (default), require .portolan/config.yaml
120
+ AND at least one of (catalog.json, .portolan/state.json) to exist.
121
+ Set to False during init_catalog() when creating a new catalog
122
+ where config.yaml is written before catalog.json.
123
+
124
+ Returns:
125
+ Path to catalog root if found, None otherwise.
126
+
127
+ Examples:
128
+ >>> find_catalog_root(Path("/my-catalog/collection/item"))
129
+ PosixPath('/my-catalog')
130
+
131
+ >>> find_catalog_root(Path("/no-catalog-here"))
132
+ None
133
+
134
+ >>> find_catalog_root() # Uses current working directory
135
+ PosixPath('/my-catalog')
136
+
137
+ >>> # During init, check for config.yaml only (catalog.json not yet written)
138
+ >>> find_catalog_root(start_path, require_operational=False)
139
+ """
140
+ from portolan_cli.constants import MAX_CATALOG_SEARCH_DEPTH
141
+
142
+ def _is_catalog_root(path: Path) -> bool:
143
+ """Check if path is a valid catalog root."""
144
+ config_yaml = path / ".portolan" / "config.yaml"
145
+ if not config_yaml.exists():
146
+ return False
147
+
148
+ if not require_operational:
149
+ # During init, config.yaml alone is sufficient
150
+ return True
151
+
152
+ # Require operational file: catalog.json at root OR state.json in .portolan
153
+ catalog_json = path / "catalog.json"
154
+ state_json = path / ".portolan" / "state.json"
155
+ return catalog_json.exists() or state_json.exists()
156
+
157
+ # Handle non-existent paths gracefully
158
+ if start_path is not None and not start_path.exists():
159
+ return None
160
+
161
+ current = (start_path or Path.cwd()).resolve()
162
+ depth = 0
163
+
164
+ # Walk up until we find a valid catalog root, hit filesystem root, or exceed depth
165
+ while current != current.parent and depth < MAX_CATALOG_SEARCH_DEPTH:
166
+ if _is_catalog_root(current):
167
+ return current
168
+ current = current.parent
169
+ depth += 1
170
+
171
+ # Check the root directory itself (only if within depth limit)
172
+ if depth < MAX_CATALOG_SEARCH_DEPTH:
173
+ if _is_catalog_root(current):
174
+ return current
175
+
176
+ return None
177
+
178
+
98
179
  # Keep legacy exception for backward compatibility
99
180
  class CatalogExistsError(Exception):
100
181
  """Raised when attempting to initialize a catalog that already exists.
@@ -0,0 +1,210 @@
1
+ """Clean command for removing Portolan metadata while preserving data files.
2
+
3
+ This module provides functionality to remove all Portolan-generated metadata
4
+ from a catalog while preserving user data files. Essential for testing
5
+ workflows where you want to reset a catalog to a "clean slate" state.
6
+
7
+ Files removed (metadata only):
8
+ - .portolan/ directory (entire thing)
9
+ - catalog.json (with "type": "Catalog")
10
+ - */collection.json (with "type": "Collection")
11
+ - */*/item.json or similar with "type": "Feature" (STAC items)
12
+ - */versions.json (Portolan-specific schema)
13
+
14
+ Files preserved (all data):
15
+ - .parquet, .tif, .gpkg, .geojson, .shp, etc.
16
+ - ANY file that isn't STAC metadata
17
+ - Non-STAC JSON files (checked by "type" field)
18
+ """
19
+
20
+ from __future__ import annotations
21
+
22
+ import json
23
+ import shutil
24
+ from pathlib import Path
25
+
26
+ from portolan_cli.constants import PORTOLAN_DIR
27
+
28
+
29
+ def is_stac_metadata(path: Path) -> bool:
30
+ """Check if a JSON file is STAC metadata (safe to remove).
31
+
32
+ STAC metadata files have a "type" field with one of:
33
+ - "Catalog" (root catalog.json)
34
+ - "Collection" (collection.json)
35
+ - "Feature" (item.json - STAC items are GeoJSON Features)
36
+
37
+ Args:
38
+ path: Path to the file to check.
39
+
40
+ Returns:
41
+ True if the file is STAC metadata, False otherwise.
42
+
43
+ Examples:
44
+ >>> is_stac_metadata(Path("catalog.json")) # has type=Catalog
45
+ True
46
+ >>> is_stac_metadata(Path("style.json")) # non-STAC JSON
47
+ False
48
+ >>> is_stac_metadata(Path("data.parquet"))
49
+ False
50
+ """
51
+ # Only check JSON files
52
+ if path.suffix.lower() != ".json":
53
+ return False
54
+
55
+ try:
56
+ data = json.loads(path.read_text())
57
+ if not isinstance(data, dict):
58
+ return False
59
+ stac_type = data.get("type")
60
+ return stac_type in ("Catalog", "Collection", "Feature")
61
+ except (json.JSONDecodeError, OSError):
62
+ return False
63
+
64
+
65
+ def is_versions_json(path: Path) -> bool:
66
+ """Check if a file is versions.json (Portolan-specific, safe to remove).
67
+
68
+ Args:
69
+ path: Path to the file to check.
70
+
71
+ Returns:
72
+ True if the file is named versions.json, False otherwise.
73
+ """
74
+ return path.name == "versions.json"
75
+
76
+
77
+ def collect_files_to_remove(catalog_root: Path) -> tuple[list[Path], list[Path]]:
78
+ """Collect all metadata files and directories to remove from a catalog.
79
+
80
+ Walks the catalog structure and identifies:
81
+ 1. .portolan/ directory (always removed entirely)
82
+ 2. STAC metadata files (catalog.json, collection.json, item.json with proper type)
83
+ 3. versions.json files (Portolan-specific)
84
+
85
+ Args:
86
+ catalog_root: Path to the catalog root directory.
87
+
88
+ Returns:
89
+ Tuple of (files_to_remove, directories_to_remove).
90
+ Files are sorted for deterministic output.
91
+ """
92
+ files_to_remove: list[Path] = []
93
+ directories_to_remove: list[Path] = []
94
+
95
+ # Always remove .portolan directory if it exists
96
+ portolan_dir = catalog_root / PORTOLAN_DIR
97
+ if portolan_dir.exists() and portolan_dir.is_dir():
98
+ directories_to_remove.append(portolan_dir)
99
+
100
+ # Walk the catalog and collect files
101
+ for path in catalog_root.rglob("*"):
102
+ # Skip .portolan directory contents (we're removing the whole dir)
103
+ if PORTOLAN_DIR in path.parts:
104
+ continue
105
+
106
+ if path.is_file():
107
+ # Check for STAC metadata
108
+ if is_stac_metadata(path):
109
+ files_to_remove.append(path)
110
+ # Check for versions.json
111
+ elif is_versions_json(path):
112
+ files_to_remove.append(path)
113
+
114
+ # Sort files for deterministic output
115
+ files_to_remove.sort()
116
+
117
+ return files_to_remove, directories_to_remove
118
+
119
+
120
+ def remove_empty_directories(catalog_root: Path, removed_files: list[Path]) -> list[Path]:
121
+ """Remove directories that became empty after file removal.
122
+
123
+ Walks bottom-up from the deepest directories and removes any that are empty.
124
+ Stops at the catalog root.
125
+
126
+ Args:
127
+ catalog_root: Path to the catalog root directory.
128
+ removed_files: List of files that were removed.
129
+
130
+ Returns:
131
+ List of directories that were removed.
132
+ """
133
+ directories_removed: list[Path] = []
134
+
135
+ # Get unique parent directories of removed files, sorted deepest first
136
+ parent_dirs = set()
137
+ for file_path in removed_files:
138
+ parent = file_path.parent
139
+ while parent != catalog_root and parent != catalog_root.parent:
140
+ parent_dirs.add(parent)
141
+ parent = parent.parent
142
+
143
+ # Sort by depth (deepest first)
144
+ sorted_dirs = sorted(parent_dirs, key=lambda p: len(p.parts), reverse=True)
145
+
146
+ for dir_path in sorted_dirs:
147
+ if dir_path.exists() and dir_path.is_dir():
148
+ # Check if directory is empty using lazy iteration (no list allocation)
149
+ if next(dir_path.iterdir(), None) is None:
150
+ try:
151
+ dir_path.rmdir()
152
+ except OSError:
153
+ # On Windows, rmdir fails if the directory is the process CWD.
154
+ # This is a best-effort cleanup, so skip and continue.
155
+ continue
156
+ directories_removed.append(dir_path)
157
+
158
+ return directories_removed
159
+
160
+
161
+ def clean_catalog(
162
+ catalog_root: Path,
163
+ *,
164
+ dry_run: bool = False,
165
+ ) -> tuple[list[Path], list[Path], int]:
166
+ """Remove all Portolan metadata from a catalog while preserving data.
167
+
168
+ Args:
169
+ catalog_root: Path to the catalog root directory.
170
+ dry_run: If True, only report what would be removed without deleting.
171
+
172
+ Returns:
173
+ Tuple of (files_removed, directories_removed, data_files_preserved).
174
+
175
+ Raises:
176
+ OSError: If file deletion fails (permission errors, etc.).
177
+ """
178
+ files_to_remove, directories_to_remove = collect_files_to_remove(catalog_root)
179
+
180
+ # Count data files (files that are NOT being removed)
181
+ # Use a set for O(1) membership checks instead of O(n) list scan
182
+ files_to_remove_set = set(files_to_remove)
183
+ data_files = 0
184
+ for path in catalog_root.rglob("*"):
185
+ if path.is_file() and PORTOLAN_DIR not in path.parts:
186
+ if path not in files_to_remove_set:
187
+ data_files += 1
188
+
189
+ if dry_run:
190
+ return files_to_remove, directories_to_remove, data_files
191
+
192
+ # Actually remove files
193
+ files_removed: list[Path] = []
194
+ for file_path in files_to_remove:
195
+ if file_path.exists():
196
+ file_path.unlink()
197
+ files_removed.append(file_path)
198
+
199
+ # Remove .portolan directory
200
+ directories_removed: list[Path] = []
201
+ for dir_path in directories_to_remove:
202
+ if dir_path.exists():
203
+ shutil.rmtree(dir_path)
204
+ directories_removed.append(dir_path)
205
+
206
+ # Clean up empty directories
207
+ empty_dirs_removed = remove_empty_directories(catalog_root, files_removed)
208
+ directories_removed.extend(empty_dirs_removed)
209
+
210
+ return files_removed, directories_removed, data_files