Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 3 additions & 3 deletions portolan_cli/add.py
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,6 @@
import click
import pystac

from portolan_cli.checksums import compute_checksum
from portolan_cli.collection import (
_compute_union_bbox,
_get_metadata_yaml_bbox,
Expand Down Expand Up @@ -151,7 +150,8 @@
create_collection,
update_collection_file_statistics,
)
from portolan_cli.style import enrich_cog_assets
from portolan_cli.sync.checksums import compute_checksum
from portolan_cli.viz.style import enrich_cog_assets

logger = logging.getLogger(__name__)

Expand Down Expand Up @@ -629,7 +629,7 @@ def _update_versions(
# ─────────────────────────────────────────────────────────────────────────────

# Note: GEOSPATIAL_EXTENSIONS imported from portolan_cli.constants
# Note: is_filegdb is imported from portolan_cli.scan_detect (canonical implementation).
# Note: is_filegdb is imported from portolan_cli.scan.detect (canonical implementation).
# scan_detect.is_filegdb accepts either .gdbtable files OR a 'gdb' marker file, which
# matches the full FileGDB spec. Do not reimplement here.

Expand Down
6 changes: 3 additions & 3 deletions portolan_cli/backends/iceberg/backend.py
Original file line number Diff line number Diff line change
Expand Up @@ -411,7 +411,7 @@ def _upload_stac_metadata(
Only uploads STAC metadata (item JSON, collection JSON, catalog.json).
Data files are NOT uploaded — they live in the Iceberg warehouse.
"""
from portolan_cli.upload import upload_file
from portolan_cli.sync.upload import upload_file

remote = remote.rstrip("/")

Expand Down Expand Up @@ -444,9 +444,9 @@ def pull(
Queries get_current_version() for asset info, then downloads each
asset from {remote_url}/{href} to {local_root}/{href}.
"""
from portolan_cli.download import download_file
from portolan_cli.output import detail, error, info, success
from portolan_cli.pull import PullResult
from portolan_cli.sync.download import download_file
from portolan_cli.sync.pull import PullResult

remote_url = remote_url.rstrip("/")

Expand Down
2 changes: 1 addition & 1 deletion portolan_cli/backends/json_file.py
Original file line number Diff line number Diff line change
Expand Up @@ -145,7 +145,7 @@ def publish(
Returns:
The newly created Version object.
"""
from portolan_cli.checksums import compute_checksum
from portolan_cli.sync.checksums import compute_checksum
from portolan_cli.versions import SPEC_VERSION

versions_path = self._versions_path(collection)
Expand Down
40 changes: 20 additions & 20 deletions portolan_cli/cli.py
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@
from portolan_cli.backends.protocol import VersioningBackend
from portolan_cli.extract.arcgis.report import ExtractionReport
from portolan_cli.extract.arcgis.url_parser import ParsedArcGISURL
from portolan_cli.pull import PullResult
from portolan_cli.sync.pull import PullResult

import click

Expand All @@ -31,7 +31,6 @@
CatalogListResult,
list_catalog_contents,
)
from portolan_cli.check import check_directory
from portolan_cli.collection_id import resolve_collection_id
from portolan_cli.constants import PORTOLAN_SPEC_VERSION
from portolan_cli.convert import ConversionResult
Expand All @@ -43,19 +42,20 @@
from portolan_cli.output import info as info_output
from portolan_cli.query import ItemInfo
from portolan_cli.remove import remove_files
from portolan_cli.scan import (
from portolan_cli.scan.check import check_directory
from portolan_cli.scan.core import (
IssueType,
ScanIssue,
ScanOptions,
ScanResult,
scan_directory,
)
from portolan_cli.scan import (
from portolan_cli.scan.core import (
Severity as ScanSeverity,
)
from portolan_cli.scan_fix import ProposedFix, apply_safe_fixes
from portolan_cli.scan_infer import infer_collections
from portolan_cli.scan_output import (
from portolan_cli.scan.fix import ProposedFix, apply_safe_fixes
from portolan_cli.scan.infer import infer_collections
from portolan_cli.scan.output import (
format_collection_suggestion,
format_fix_commands_json,
generate_next_steps,
Expand All @@ -64,7 +64,7 @@
group_skipped_files,
render_tree_view,
)
from portolan_cli.scan_progress import ScanProgressReporter, count_directories
from portolan_cli.scan.progress import ScanProgressReporter, count_directories
from portolan_cli.stac import MergeStrategy
from portolan_cli.status import CollectionStatus, get_collection_status
from portolan_cli.temporal import FLEXIBLE_DATETIME
Expand Down Expand Up @@ -789,7 +789,7 @@ def status_cmd(
remote_url = resolve_remote(None, catalog_path, collection)

# Discover collections
from portolan_cli.push import discover_collections
from portolan_cli.sync.push import discover_collections

if collection:
collections = [collection]
Expand Down Expand Up @@ -1400,7 +1400,7 @@ def _execute_check_workflow(
- Without --fix: run validation and report issues
- With --fix: run validation AND apply fixes for the selected scope
"""
from portolan_cli.check import build_check_rules
from portolan_cli.scan.check import build_check_rules

# Build rules (respects config severity overrides + strict flag)
rules = build_check_rules(path, strict=strict)
Expand Down Expand Up @@ -1465,7 +1465,7 @@ def _run_fix_workflow(
force: Re-optimize already-valid COGs (raster-scoped, issue #530).
workers: Parallel worker processes for conversion.
"""
from portolan_cli.check import run_fix_workflow
from portolan_cli.scan.check import run_fix_workflow

# Progress callback for conversion (skip for JSON mode, per ADR-0040: per-file only in verbose)
def show_conversion_progress(result: ConversionResult) -> None:
Expand Down Expand Up @@ -1962,7 +1962,7 @@ def _output_scan_results(
if use_json:
_output_scan_json(result, strict=strict, has_strict_failure=has_strict_failure)
elif manual_only:
from portolan_cli.scan_output import format_scan_output
from portolan_cli.scan.output import format_scan_output

output = format_scan_output(result, manual_only=True)
click.echo(output)
Expand Down Expand Up @@ -2272,7 +2272,7 @@ def _print_skipped_by_category(result: ScanResult, *, show_all: bool = False) ->
return

# Check if any files are truly unknown
from portolan_cli.scan_classify import FileCategory
from portolan_cli.scan.classify import FileCategory

unknown_files = grouped.get(FileCategory.UNKNOWN, [])
unknown_count = len(unknown_files)
Expand Down Expand Up @@ -2951,7 +2951,7 @@ def on_file_progress(file_path: Path) -> None:
)

# Handle PMTiles generation BEFORE output (so JSON reflects final state)
from portolan_cli.pmtiles import generate_or_suggest_pmtiles
from portolan_cli.viz.pmtiles import generate_or_suggest_pmtiles

generate_or_suggest_pmtiles(
catalog_root,
Expand Down Expand Up @@ -3469,7 +3469,7 @@ def push(
"""
import asyncio

from portolan_cli.push import PushConflictError, push_all_collections, push_async
from portolan_cli.sync.push import PushConflictError, push_all_collections, push_async

use_json = should_output_json(ctx, json_output)

Expand Down Expand Up @@ -3801,8 +3801,8 @@ def pull_command(
portolan pull s3://mybucket/catalog
portolan pull s3://mybucket/catalog --workers 4
"""
from portolan_cli.pull import pull as pull_fn
from portolan_cli.pull import pull_all_collections
from portolan_cli.sync.pull import pull as pull_fn
from portolan_cli.sync.pull import pull_all_collections

use_json = should_output_json(ctx, json_output)

Expand Down Expand Up @@ -3993,7 +3993,7 @@ def sync(
portolan sync s3://mybucket/catalog -c data --profile prod
portolan sync --collection demographics # Uses configured remote
"""
from portolan_cli.sync import sync as sync_fn
from portolan_cli.sync.core import sync as sync_fn

use_json = should_output_json(ctx, json_output)

Expand Down Expand Up @@ -4142,8 +4142,8 @@ def clone(
# Clone specific collection with profile
portolan clone s3://mybucket/catalog ./data -c imagery --profile prod
"""
from portolan_cli.sync import clone as clone_fn
from portolan_cli.sync import infer_local_path_from_url
from portolan_cli.sync.core import clone as clone_fn
from portolan_cli.sync.core import infer_local_path_from_url

use_json = should_output_json(ctx, json_output)

Expand Down
4 changes: 2 additions & 2 deletions portolan_cli/collection_id.py
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@
from pathlib import Path

from portolan_cli.formats import FormatType, detect_format
from portolan_cli.scan_detect import is_filegdb
from portolan_cli.scan.detect import is_filegdb

# Pattern for valid collection IDs (supports path syntax per ADR-0032):
# - Start with lowercase letter or number (year-based organization like 2020/)
Expand Down Expand Up @@ -253,7 +253,7 @@ def infer_nested_collection_id(path: Path, catalog_root: Path) -> str:
ValueError: If path is not inside catalog root, at root level, or
if raster data lacks required item subdirectory structure.
"""
from portolan_cli.scan_detect import is_hive_partition_dir
from portolan_cli.scan.detect import is_hive_partition_dir

# Get path relative to catalog root
try:
Expand Down
2 changes: 1 addition & 1 deletion portolan_cli/convert.py
Original file line number Diff line number Diff line change
Expand Up @@ -39,7 +39,7 @@
detect_format,
get_cloud_native_status,
)
from portolan_cli.thumbnail import (
from portolan_cli.viz.thumbnail import (
ThumbnailConfig,
generate_vector_thumbnail,
get_thumbnail_config,
Expand Down
2 changes: 1 addition & 1 deletion portolan_cli/discovery.py
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@
GEOSPATIAL_EXTENSIONS,
SIDECAR_PATTERNS,
)
from portolan_cli.scan_detect import is_filegdb
from portolan_cli.scan.detect import is_filegdb

logger = logging.getLogger(__name__)

Expand Down
2 changes: 1 addition & 1 deletion portolan_cli/extract/common/orchestrator_base.py
Original file line number Diff line number Diff line change
Expand Up @@ -120,7 +120,7 @@ def register_collection_styles(
report: The extraction report.
include_legends: Also discover and register legend sidecars (WFS/WMS).
"""
from portolan_cli.style import (
from portolan_cli.viz.style import (
discover_legends,
discover_styles,
register_legend_assets,
Expand Down
4 changes: 2 additions & 2 deletions portolan_cli/metadata/fix.py
Original file line number Diff line number Diff line change
Expand Up @@ -295,11 +295,11 @@ def repair_pmtiles_links(catalog_root: Path, *, dry_run: bool = False) -> list[F

def _repair_pmtiles_collection(collection_json: Path, *, dry_run: bool) -> FixResult | None:
"""Backfill PMTiles links / extension for one collection (see repair_pmtiles_links)."""
from portolan_cli.pmtiles import (
from portolan_cli.viz.pmtiles import (
add_pmtiles_link_to_collection,
ensure_web_map_links_extension,
)
from portolan_cli.pmtiles_links import (
from portolan_cli.viz.pmtiles_links import (
WEB_MAP_LINKS_EXTENSION,
pmtiles_asset_hrefs,
pmtiles_link_hrefs,
Expand Down
2 changes: 1 addition & 1 deletion portolan_cli/metadata/scan.py
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,6 @@
from pathlib import Path
from typing import Any

from portolan_cli.checksums import compute_checksum
from portolan_cli.metadata.detection import (
check_file_metadata,
detect_changes,
Expand All @@ -36,6 +35,7 @@
MetadataReport,
MetadataStatus,
)
from portolan_cli.sync.checksums import compute_checksum

_DATA_EXTENSIONS = frozenset(
{
Expand Down
8 changes: 4 additions & 4 deletions portolan_cli/preparation.py
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,6 @@
import pystac

from portolan_cli import extension_registry as _reg
from portolan_cli.checksums import compute_checksum, compute_dir_checksum, compute_dir_size
from portolan_cli.collection_id import normalize_collection_id, validate_collection_id
from portolan_cli.config import get_setting, load_merged_metadata
from portolan_cli.crs import transform_bbox_to_wgs84
Expand All @@ -49,14 +48,15 @@
apply_temporal_defaults,
validate_metadata,
)
from portolan_cli.scan_detect import is_filegdb
from portolan_cli.scan.detect import is_filegdb
from portolan_cli.stac import (
add_projection_extension,
add_raster_extension,
add_vector_extension,
create_item,
)
from portolan_cli.style import enrich_cog_assets
from portolan_cli.sync.checksums import compute_checksum, compute_dir_checksum, compute_dir_size
from portolan_cli.viz.style import enrich_cog_assets

logger = logging.getLogger(__name__)

Expand Down Expand Up @@ -443,7 +443,7 @@ def _derive_item_id_and_asset_level(
collection-level assets; other formats derive unique IDs from the
partition values.
"""
from portolan_cli.scan_detect import is_hive_partition_dir
from portolan_cli.scan.detect import is_hive_partition_dir

# If item_id is explicitly provided, treat as item-level (not collection-level)
# This ensures --item-id creates a subdirectory structure
Expand Down
2 changes: 1 addition & 1 deletion portolan_cli/query.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,13 +8,13 @@
from datetime import datetime
from pathlib import Path

from portolan_cli.checksums import compute_checksum, compute_dir_checksum
from portolan_cli.constants import (
MTIME_TOLERANCE_SECONDS,
)
from portolan_cli.formats import (
FormatType,
)
from portolan_cli.sync.checksums import compute_checksum, compute_dir_checksum
from portolan_cli.versions import (
read_versions,
)
Expand Down
8 changes: 8 additions & 0 deletions portolan_cli/scan/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
"""Directory scanning, classification, inference, and catalog ``check``.
The orchestrator lives in :mod:`portolan_cli.scan.core`; import its public API
as ``from portolan_cli.scan.core import scan_directory`` (etc.). This ``__init__``
is intentionally import-free: importing a leaf submodule (e.g. ``scan.detect``)
must not eagerly pull in the heavy orchestrator, which would reintroduce the
``collection_id`` <-> ``scan`` import cycle (#625).
"""
2 changes: 1 addition & 1 deletion portolan_cli/check.py → portolan_cli/scan/check.py
Original file line number Diff line number Diff line change
Expand Up @@ -37,7 +37,7 @@
get_effective_status,
is_geoparquet,
)
from portolan_cli.scan_detect import is_filegdb
from portolan_cli.scan.detect import is_filegdb

if TYPE_CHECKING:
from portolan_cli.metadata.fix import FixReport
Expand Down
File renamed without changes.
12 changes: 6 additions & 6 deletions portolan_cli/scan.py → portolan_cli/scan/core.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@
import (like ruff check vs ruff format). See ADR-0016 for rationale.

Example:
>>> from portolan_cli.scan import scan_directory, ScanOptions
>>> from portolan_cli.scan.core import scan_directory, ScanOptions
>>> result = scan_directory(Path("/data/geospatial"))
>>> print(f"Found {len(result.ready)} files ready to import")
>>> if result.has_errors:
Expand Down Expand Up @@ -55,22 +55,22 @@
is_geoparquet,
is_valid_parquet,
)
from portolan_cli.scan_classify import (
from portolan_cli.scan.classify import (
STAC_FILENAMES,
FileCategory,
SkippedFile,
SkipReasonType,
classify_file,
)
from portolan_cli.scan_detect import (
from portolan_cli.scan.detect import (
FILEGDB_LOCK_PATTERNS,
DualFormatPair,
SpecialFormat,
is_filegdb,
is_hive_partition_dir,
)
from portolan_cli.scan_fix import ProposedFix
from portolan_cli.scan_infer import CollectionSuggestion
from portolan_cli.scan.fix import ProposedFix
from portolan_cli.scan.infer import CollectionSuggestion

# =============================================================================
# Constants
Expand Down Expand Up @@ -1387,7 +1387,7 @@ def scan_directory(
NotADirectoryError: If path is not a directory.

Example:
>>> from portolan_cli.scan import scan_directory, ScanOptions
>>> from portolan_cli.scan.core import scan_directory, ScanOptions
>>> result = scan_directory(Path("/data/geospatial"))
>>> print(f"Found {len(result.ready)} files ready to import")
>>> if result.has_errors:
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,7 @@
from typing import TYPE_CHECKING, Any

if TYPE_CHECKING:
from portolan_cli.scan import ScannedFile
from portolan_cli.scan.core import ScannedFile

# Hive partition pattern: key=value where key starts with letter/underscore
HIVE_PARTITION_PATTERN = re.compile(r"^([a-zA-Z_][a-zA-Z0-9_]*)=(.+)$")
Expand Down
Loading
Loading