Skip to content

Python API Reference

This page documents the supported Python API. Internal modules are not a supported interface. To extend Portolan, follow the plugin guide and use the APIs on this page.

Top-level exports

Catalog

A Portolan catalog backed by a .portolan directory.

The Catalog class provides the Python API for all catalog operations. The CLI commands are thin wrappers around these methods.

Note: This is the legacy v1 API. New code should use init_catalog() which creates the v2 file structure with catalog.json at root level.

Attributes:

Name Type Description
root

The root directory containing the .portolan folder.

portolan_path property

Path to the .portolan directory.

catalog_file property

Path to the catalog.json file (at root, not inside .portolan).

__init__(root)

Initialize a Catalog instance.

Parameters:

Name Type Description Default
root Path

The root directory containing the .portolan folder.

required

init(root, *, license_id=None) classmethod

Initialize a new Portolan catalog.

Creates the catalog using the v2 file structure via init_catalog().

Parameters:

Name Type Description Default
root Path

The directory where the catalog should be created.

required
license_id str | None

SPDX identifier to seed into metadata.yaml. Left None, the catalog starts unlicensed and add will ask for a license before it writes a collection (issue #686).

None

Returns:

Type Description
Self

A Catalog instance for the newly created catalog.

Raises:

Type Description
CatalogExistsError

If a .portolan directory already exists.

CatalogExistsError

Bases: Exception

Raised when attempting to initialize a catalog that already exists.

Legacy exception kept for backward compatibility. New code should use CatalogAlreadyExistsError from portolan_cli.errors.

cli

Portolan CLI - Command-line interface for managing cloud-native geospatial data.

The CLI is a thin wrapper around the Python API (see catalog.py). All business logic lives in the library; the CLI handles user interaction.

click_writer(prefix, message, *, fg, dim, file, nl)

Write an output message through click.echo.

click.echo strips color from a pipe, follows the CliRunner color setting, and wraps a Windows console. The library writer does not.

show_warning(message, category, filename, lineno, file=None, line=None)

Print a PluginWarning as an output warning. Pass other warnings on.

A plugin warning names the plugin and the problem. The source path and line that Python adds point into Portolan, which does not help the user.

should_output_json(ctx, json_flag=False)

Determine if JSON output should be used.

Checks both the global --format option and per-command --json flags. Global --format=json takes precedence, but per-command flags also work for backward compatibility.

Parameters:

Name Type Description Default
ctx Context

Click context containing the format preference.

required
json_flag bool

Per-command --json flag value.

False

Returns:

Type Description
bool

True if JSON output should be used, False for text output.

output_json_envelope(envelope)

Output a JSON envelope to stdout.

Parameters:

Name Type Description Default
envelope Any

OutputEnvelope instance to output.

required

load_dotenv_and_warn_sensitive(catalog_path)

Load .env from catalog and warn if config.yaml has sensitive settings.

This should be called after the actual catalog path is resolved (not in the global CLI callback) to ensure the correct .env is loaded when --catalog/--portolan-dir overrides the default.

Parameters:

Name Type Description Default
catalog_path Path

Resolved catalog root path.

required

require_catalog_root(use_json=False, command_name='command')

Find and validate catalog root, or exit with git-style error.

Git-style behavior: walks up from cwd to find .portolan/config.yaml. Commands that need to operate on the entire catalog use this to find the root regardless of which subdirectory they're run from.

Parameters:

Name Type Description Default
use_json bool

If True, output error as JSON envelope.

False
command_name str

Name of the command for error messages.

'command'

Returns:

Type Description
Path

Path to catalog root.

Raises:

Type Description
SystemExit

If not inside a catalog.

resolve_remote(destination, catalog_path, collection=None)

Resolve remote destination with precedence: CLI > env var > config.

Parameters:

Name Type Description Default
destination str | None

CLI-provided destination value (None if not specified).

required
catalog_path Path | None

Path to catalog root for config lookup.

required
collection str | None

Optional collection name for collection-level config.

None

Returns:

Type Description
str | None

Resolved destination URL or None if not configured.

resolve_aws_profile(profile, catalog_path, collection=None)

Resolve the AWS profile: CLI, then env var, then config.

A Portolan setting wins, because a user writes it for this tool. None means that nothing here names a profile. The sync layer then reads AWS_PROFILE, and falls back to the default profile, as boto3 and the AWS CLI do.

Parameters:

Name Type Description Default
profile str | None

CLI-provided profile value (None if not specified).

required
catalog_path Path | None

Path to catalog root for config lookup.

required
collection str | None

Optional collection name for collection-level config.

None

Returns:

Type Description
str | None

The profile name, or None when nothing here names one.

resolve_aws_region(region, catalog_path, collection=None)

Resolve AWS region with precedence: CLI > env var > config.

Parameters:

Name Type Description Default
region str | None

CLI-provided region value (None if not specified).

required
catalog_path Path | None

Path to catalog root for config lookup.

required
collection str | None

Optional collection name for collection-level config.

None

Returns:

Type Description
str | None

Resolved region name or None if not configured.

cli(ctx, output_format)

Portolan - Publish and manage cloud-native geospatial data catalogs.

init(ctx, path, json_output, auto_mode, catalog_id, title, description, backend, license_id, license_url, logo, logo_title)

Initialize a new Portolan catalog.

Creates a catalog.json at the root level and a .portolan directory with management files (config.yaml, metadata.yaml). Also creates versions.json at the root.

Derives the catalog ID from the directory name. Pass --id to set it yourself. The ID is the catalog's public STAC identity, so a directory named for a step in your workflow makes a poor one.

PATH is the directory where the catalog should be created (default: current directory).

A license is required. Every collection inherits it from the catalog's metadata.yaml, and 'portolan add' refuses to write a collection without one. Pass an SPDX identifier, or 'other' with --license-url when the license has no identifier. Without --auto or --json you are prompted for it.

Use --auto to skip all prompts and use default values. Use --id, --title, and --description to set catalog metadata directly.

A logo is optional. Pass --logo with a local image to copy it into _assets/ and publish it as a rel="icon" link, or add one later with 'portolan logo'.

\b Examples: portolan init # Prompts for the license portolan init --auto --license CC-BY-4.0 # Skip prompts portolan init --title "My Catalog" --license MIT portolan init --id phl-housing --license CC-BY-4.0 portolan init --license other --license-url https://x.org/terms portolan init --backend iceberg --license CC0-1.0 portolan init --auto --license CC-BY-4.0 --logo brand.png

list_cmd(ctx, collection, catalog_path, json_output, tracked_only, untracked_only)

List all files in the catalog with tracking status.

Git-style behavior: automatically finds the catalog root by walking up from the current directory. Works from any subdirectory within a catalog. Use --catalog to override and specify an explicit path.

Shows all files organized by collection in a hierarchical tree view. Each file shows its tracking status, format type, and file size.

\b Status indicators: + = tracked (in versions.json, unchanged) + = untracked (on disk, not in versions.json) ~ = modified (in versions.json, checksum changed) ! = deleted (in versions.json, missing from disk)

\b Example output: censo-2010/ data/ (3 tracked, 2 untracked) + census-data.parquet (GeoParquet, 4.5MB) + metadata.parquet (GeoParquet, 1.2MB) + README.md (2KB) + style.json (1KB)

\b Examples: portolan list # List all files with status portolan list --collection demographics # Filter by collection portolan list --tracked-only # Show only tracked files portolan list --untracked-only # Show only untracked files portolan list --json # JSON output

status_cmd(ctx, collection, catalog_path, offline, json_output)

Show local vs remote version state for collections.

Git-style status showing version sync state, modified files, and untracked files for each collection in the catalog.

\b Status information: Local version Current version in local versions.json Remote version Current version on remote (unless --offline) Sync state in_sync, ahead, behind, or unknown Modified Files changed since last version Untracked Files on disk not in versions.json Deleted Files in versions.json but missing from disk

\b Examples: portolan status # Status for all collections portolan status -c demographics # Status for one collection portolan status --offline # Skip remote check portolan status --json # JSON output for agents

info_cmd(ctx, target, catalog_path, json_output)

Show information about a file, collection, or catalog.

TARGET can be: - A file path (e.g., demographics/census.parquet) - shows file metadata - A collection directory (e.g., demographics/) - shows collection metadata - Omitted - shows catalog-level metadata

The output format for files is

Format: GeoParquet CRS: EPSG:4326 Bbox: [-122.5, 37.7, -122.3, 37.9] Features: 4,231 Version: v1.2.0

\b Examples: portolan info demographics/census.parquet # File info portolan info demographics/ # Collection info portolan info # Catalog info portolan info demographics/census.parquet --json # JSON output

check(ctx, path, json_output, verbose, fix, dry_run, remove_legacy, force, workers, metadata, geo_assets, strict, live, public_url, no_data, data_scope, no_structural, schema)

Validate a Portolan catalog against the Portolan spec.

Validation runs on rashid, which reports PTL-* rule ids citing the spec requirements they enforce. By default it checks metadata, STAC 1.1.0 structure, and the asset bytes themselves. The metadata and structural passes are offline. The data pass reads asset bytes, including remote https assets over HTTP range requests. Use --data-scope local to read only the assets inside the catalog tree. Use --no-data to skip the pass. With --fix, applies the mechanical repairs and re-checks.

The --live pass probes the published host for HTTP Range support and CORS headers. It also requests every link target under the base URL, and reports a linked file the host does not serve. It skips an upstream host only when it knows the base URL, from --url or publish.public_url. Without a base URL it probes every absolute href as declared, which can report CORS failures against a host you do not control.

PATH is the directory to check (default: current directory).

Use --metadata or --geo-assets to limit scope: - --metadata: Only validate the catalog (its own metadata, structure, data) - --geo-assets: Only check source files on disk for cloud-native status - Neither: Both (default)

Examples:

portolan check # Validate all (metadata + geo-assets)

portolan check --metadata # Validate the catalog only

portolan check --geo-assets # Check source files only

portolan check --data-scope local # Read only assets inside the catalog tree

portolan check --no-data # Skip reading asset bytes (faster)

portolan check --live # Also probe the published host

portolan check --fix # Fix what can be fixed, then re-check

portolan check --fix --dry-run # Preview all fixes

scan(ctx, path, json_output, no_recursive, max_depth, include_hidden, follow_symlinks, show_all, show_tree, suggest_collections, manual_only, fix, dry_run, strict)

Scan a directory for geospatial files and potential issues.

Discovers files by extension, validates shapefile completeness, and reports issues that may cause problems during import.

PATH is the directory to scan (default: current directory).

\b Fix Mode: Use --fix to auto-rename files with: - Invalid characters (spaces, parentheses, non-ASCII) - Windows reserved names (CON, PRN, AUX, etc.) - Long paths (> 200 characters)

Use --dry-run to preview changes without applying.

Examples:

portolan scan # Scan current directory

portolan scan --json # JSON output in current directory

portolan scan /data/geospatial

portolan scan /large/tree --max-depth=2

portolan scan /data --no-recursive

portolan scan /data --fix --dry-run

portolan scan /data --fix

add_cmd(ctx, json_output, paths, verbose, item_id, catalog_path, item_datetime, workers, generate_parquet, generate_pmtiles, force_pmtiles, generate_thumbnails, force_thumbnails, force, reconvert, merge_strategy)

Track files in the catalog.

Accepts multiple paths like git add. Each path is processed independently with automatic collection inference based on directory structure.

Works like git: run from anywhere inside a catalog and it auto-detects the catalog root. Use --portolan-dir to override.

\b Item ID derivation: By default, the item ID is derived from the parent directory name. For example, adding 'census/2020/data.parquet' creates an item named '2020'. Use --item-id to override this automatic derivation. All other files in the item directory are tracked as companion assets.

\b Datetime handling: --datetime applies to ALL items added in this command. For items with different acquisition dates, run separate add commands:

    portolan add census/2020/ --datetime 2020-04-01
    portolan add census/2023/ --datetime 2023-04-01

If --datetime is omitted, items have a null temporal extent and carry
the sentinel start/end range. Run 'portolan check' to find items
needing dates.

\b Examples: portolan add demographics/census.parquet portolan add file1.geojson file2.geojson # Add multiple files portolan add imagery/ # Add all files in directory portolan add . # Add all files in catalog portolan add data.geojson --item-id my-id # Override item ID (single file only) portolan add sat.tif --datetime 2024-06-15 # Explicit acquisition date

Smart behavior: - Unchanged files are silently skipped (use --verbose to see them) - Changed files are re-extracted with new metadata - Sidecar files (.dbf, .shx, .prj for shapefiles) are auto-detected - All files in the item directory are tracked, not just geo files

\b Large file partitioning: GeoParquet files exceeding 2GB are automatically partitioned into spatial chunks using KD-tree partitioning. In interactive mode, you'll be prompted before partitioning. Configure via:

    partitioning.enabled: true/false (default: true)
    partitioning.prompt: true/false (default: true)
    partitioning.threshold_gb: size in GB (default: 2.0)

add_external_cmd(ctx, json_output, url, collection_id, title, description, media_type, license_id, license_url, via_url, bbox, catalog_path, force)

Register remote data as a collection WITHOUT downloading or converting it.

The external counterpart to 'portolan add'. Some valuable data sources are already published cloud-natively at a remote location and should be referenced in place rather than copied. This creates a collection.json whose collection-level 'data' asset href is the remote URL (kept as-is, carrying the 'external' role) plus a rel:via provenance link.

The remote asset is never fetched, and 'portolan check' will not try to convert it (the metadata scanner skips scheme-qualified hrefs).

A license is required. Pass an SPDX identifier, or 'other' with --license-url when the license has no identifier. Referencing someone else's data in place does not make its license unknowable, and a collection without one is an error under 'portolan check'.

\b Examples: # Overture Maps places — planet-scale GeoParquet on Overture's S3 portolan add-external \ "s3://overturemaps-us-west-2/release/2024-09-18.0/theme=places/type=place/*" \ --collection overture-places \ --title "Overture Maps — Places" \ --license ODbL-1.0 \ --via "https://docs.overturemaps.org/guides/places/"

portolan add-external "https://example.org/data/buildings.parquet" \\
    --license other --license-url "https://example.org/terms"

rm_cmd(ctx, json_output, path, keep, force, dry_run, verbose, catalog_path)

Remove files from tracking.

By default, removes the file from disk AND untracks it from the catalog. Requires --force for destructive operations (deleting files).

Works like git: run from anywhere inside a catalog and it auto-detects the catalog root. Use --portolan-dir to override.

A file that is already gone from disk is still untracked, so you can drop a tracked asset that you deleted by hand. The removal is recorded as a new version.

\b Safety flags: - --keep: Untrack file but preserve it on disk (safe, no --force needed) - --force: Required for destructive rm (when not using --keep) - --dry-run: Preview what would be removed without actually removing

\b Examples: portolan rm --keep imagery/old_data.tif # Safe: untrack only portolan rm --dry-run vectors/ # Preview what would be removed portolan rm -f demographics/census.parquet # Force delete and untrack portolan rm -f vectors/ # Force remove entire directory

push(ctx, json_output, verbose, destination, collection, force, dry_run, profile, catalog_path, workers, concurrency, chunk_concurrency, max_connections, adaptive)

Push local catalog changes to cloud object storage.

Git-style behavior: automatically finds the catalog root by walking up from the current directory. Works from any subdirectory within a catalog. Use --catalog to override and specify an explicit path.

Syncs collection(s) to a remote destination (S3, GCS, Azure). Uses optimistic locking to detect concurrent modifications.

DESTINATION is the object store URL (e.g., s3://mybucket/my-catalog). If not provided, uses 'remote' from PORTOLAN_REMOTE env var or .env file.

If --collection is specified, pushes that collection only. If --collection is omitted, pushes all collections in the catalog.

\b Examples: # Push a single collection portolan push s3://mybucket/catalog --collection demographics portolan push gs://mybucket/catalog -c imagery --dry-run

# Push all collections
portolan push s3://mybucket/catalog
portolan push --dry-run  # Uses configured remote

pull_command(ctx, json_output, remote_url, collection, catalog_path, force, dry_run, restore, profile, workers, concurrency)

Pull updates from a remote catalog.

Git-style behavior: automatically finds the catalog root by walking up from the current directory. Works from any subdirectory within a catalog. Use --catalog to override and specify an explicit path.

Fetches changes from a remote catalog and downloads updated files. Similar to git pull, this checks for uncommitted local changes before overwriting.

REMOTE_URL is the remote catalog URL (e.g., s3://bucket/catalog).

If --collection is specified, pulls that collection only. If --collection is omitted, pulls all collections in the catalog.

\b Examples: # Pull a single collection portolan pull s3://mybucket/my-catalog --collection demographics portolan pull s3://mybucket/catalog -c imagery --dry-run

# Pull all collections
portolan pull s3://mybucket/catalog
portolan pull s3://mybucket/catalog --workers 4

sync(ctx, json_output, destination, collection, force, dry_run, fix, profile, catalog_path)

Sync local catalog with remote storage (pull + push).

Orchestrates a full sync workflow: Pull -> Init -> Scan -> Check -> Push. This is the recommended way to keep a local catalog in sync with remote.

DESTINATION is the object store URL (e.g., s3://mybucket/my-catalog).

\b Examples: portolan sync s3://mybucket/catalog --collection demographics portolan sync s3://mybucket/catalog -c imagery --dry-run portolan sync s3://mybucket/catalog -c data --fix --force portolan sync s3://mybucket/catalog -c data --profile prod portolan sync --collection demographics # Uses configured remote

clone(ctx, json_output, remote_url, local_path, collection, profile)

Clone a remote catalog to a local directory.

This is essentially "pull to an empty directory" with guardrails. Creates the target directory and pulls collections from remote storage.

REMOTE_URL is the object store URL (e.g., s3://mybucket/my-catalog).

LOCAL_PATH is optional - if not provided, it will be inferred from the catalog name in the URL (git clone style).

--collection is optional - if not provided, all collections in the remote catalog will be cloned.

\b Examples: # Infer directory from URL, clone all collections portolan clone s3://mybucket/my-catalog

# Clone to current directory (must be empty)
portolan clone s3://mybucket/my-catalog.

# Clone specific collection
portolan clone s3://mybucket/catalog -c demographics

# Clone all collections to specific directory
portolan clone s3://mybucket/catalog ./local-copy

# Clone specific collection with profile
portolan clone s3://mybucket/catalog ./data -c imagery --profile prod

config()

Manage catalog configuration.

Configuration is stored in .portolan/config.yaml and follows this precedence:

\b 1. CLI argument (highest) 2. Environment variable (PORTOLAN_) or .env file 3. Collection-level config 4. Catalog-level config 5. Built-in default (lowest)

Note: Sensitive settings (remote, profile, region) must use env vars or .env.

\b Examples: portolan config set backend iceberg portolan config get remote portolan config list

config_set(ctx, json_output, key, value, collection)

Set a configuration value.

KEY is the setting name (e.g., backend, statistics.enabled). VALUE is the value to set.

Note: Sensitive settings (remote, profile, region) cannot be stored in config.yaml. Use environment variables or .env files instead.

\b Examples: portolan config set backend iceberg portolan config set statistics.enabled true portolan config set pmtiles.enabled false --collection demographics

config_get(ctx, json_output, key, collection)

Get a configuration value.

Shows the resolved value and its source (env, catalog, collection, or not set).

KEY is the setting name (e.g., remote, aws_profile).

\b Examples: portolan config get remote portolan config get aws_profile --collection restricted

config_list(ctx, json_output, collection)

List all configuration settings.

Shows all settings with their values and sources.

\b Examples: portolan config list portolan config list --collection demographics

config_unset(ctx, json_output, key, collection)

Remove a configuration value.

Removes the setting from the config file. Does not affect environment variables.

KEY is the setting name to remove.

\b Examples: portolan config unset remote portolan config unset aws_profile --collection restricted

clean(ctx, json_output, dry_run)

Remove all Portolan metadata while preserving data files.

Removes catalog.json, collection.json, item.json (STAC metadata), versions.json, and the .portolan/ directory. Preserves all data files (.parquet, .tif, .gpkg, .geojson, etc.).

Use --dry-run to preview what would be removed without deleting anything.

\b Examples: portolan clean # Remove all metadata portolan clean --dry-run # Preview what would be removed

metadata()

Manage catalog metadata for README generation.

metadata.yaml files supplement STAC with human-enrichable fields like titles, descriptions, contact info, and citations. These files can exist at any level in the catalog hierarchy (catalog, subcatalog, collection).

\b Examples: portolan metadata init # Create template at catalog root portolan metadata init demographics # Create template for collection portolan metadata validate # Validate metadata.yaml

metadata_init(ctx, json_output, path, force, no_recursive)

Generate a metadata.yaml template.

Creates .portolan/metadata.yaml files at all STAC levels (catalogs, subcatalogs, collections) by default. Skips items (item.json directories) and preserves existing files unless --force is used.

If PATH is provided, starts from that directory. Otherwise, starts at the catalog root.

\b Examples: portolan metadata init # All levels in catalog portolan metadata init climate # All levels under climate/ portolan metadata init --force # Overwrite existing portolan metadata init --no-recursive # Only at catalog root portolan metadata init demographics --no-recursive # Only for collection

metadata_validate(ctx, json_output, path, no_recursive)

Validate metadata.yaml against schema.

Validates all .portolan/metadata.yaml files in the catalog tree by default. Checks for: - Required fields: contact (name + email), license - Format validation: email, SPDX license identifier, DOI

Uses hierarchical resolution: child metadata.yaml files inherit from parent levels and override specific fields.

\b Examples: portolan metadata validate # Validate all levels portolan metadata validate climate # Validate under climate/ portolan metadata validate --no-recursive # Only at catalog root portolan metadata validate demographics --no-recursive # Only for collection

readme(ctx, json_output, path, stdout, check, no_recursive, verbose)

Generate README.md from STAC metadata and metadata.yaml.

Generates READMEs for the catalog and all collections by default. The README is a pure output - always generated from STAC (machine-extracted metadata) plus .portolan/metadata.yaml (human enrichment). Never hand-edit the README; edit metadata.yaml instead and regenerate.

Use --check in CI to verify READMEs are up-to-date:

\b Examples: portolan readme # Generate for catalog and all collections portolan readme climate # Generate under climate/ portolan readme --check # CI mode: exit 1 if any stale portolan readme --no-recursive # Only at catalog root portolan readme demographics --no-recursive # Only for collection portolan readme --stdout --no-recursive # Print single README

logo_cmd(ctx, source, title, json_output)

Publish SOURCE as the catalog logo.

Copies the image to _assets/ beside the root catalog.json and writes a rel="icon" link pointing at it. A registry lists many catalogs side by side, and the logo is what makes yours recognizable.

SOURCE must be a local file in one of the seven permitted formats: APNG, AVIF, GIF, JPEG, PNG, SVG, or WebP. A URL is rejected — download the image first. SVG conforms but STAC Browser will not render it.

Re-run to replace the logo: the previous link and image are removed, so the catalog always carries exactly one.

\b Examples: portolan logo brand.png # Title from the catalog portolan logo brand.png --title "Acme GIS" # Explicit title

extract()

Extract data from external sources into Portolan catalogs.

Convert data from ArcGIS services, APIs, or other sources into well-structured Portolan catalogs with STAC metadata.

\b Examples: portolan extract arcgis https://services.arcgis.com/.../FeatureServer ./output portolan extract arcgis URL --layers "Census" --dry-run portolan extract arcgis URL --filter "sdn_" --resume

partition(ctx, input_file, output_dir, strategy, target_rows, preview, verbose)

Partition a large GeoParquet file for better query performance.

Splits a GeoParquet file into spatially-organized partitions using geoparquet-io. Per OGC best practices, files over 2GB should be partitioned.

\b Output structure (Hive-style): output_dir/ ├── kdtree_cell=001/ │ └── data.parquet ├── kdtree_cell=002/ │ └── data.parquet └── ...

\b Examples: # Preview partition strategy portolan partition buildings.parquet --preview

# Partition with default settings (kdtree, 120k rows/partition)
portolan partition buildings.parquet output/

# Custom target rows
portolan partition buildings.parquet output/ --target-rows 50000

version()

Version management commands.

Works with any versioning backend (file, iceberg). Backend is auto-detected from catalog configuration.

\b Subcommands: current Show current version of a collection list List all versions of a collection rollback Rollback to a previous version (iceberg only) prune Remove old versions (iceberg only)

current(ctx, collection, catalog_path, json_output)

Show the current version of a collection.

Works with any versioning backend (auto-detected from config).

\b Examples: portolan version current boundaries portolan version current boundaries --json

version_list_cmd(ctx, collection, catalog_path, json_output)

List all versions of a collection.

Works with any versioning backend (auto-detected from config).

\b Examples: portolan version list boundaries portolan version list boundaries --json

bump(ctx, collection, new_version, notes, breaking, yes, catalog_path, json_output)

Create a new version from current file state.

Detects modified files by comparing checksums, computes new checksums, and creates a new version entry in versions.json.

NEW_VERSION must be an explicit semver string (e.g., "1.2.0").

\b Examples: portolan version bump demographics 1.4.0 -m "Updated source data" portolan version bump demographics 2.0.0 --breaking -m "Schema change" portolan version bump demographics 1.4.0 -y # Skip confirmation

stac_geoparquet(ctx, collection, catalog_path, dry_run, json_output)

Generate items.parquet for efficient STAC queries.

Creates a GeoParquet file containing all items in a collection, enabling fast spatial/temporal queries without N HTTP requests.

This is optional but recommended for collections with >100 items. The parquet file is added as a link in collection.json.

If --collection is omitted, generates for ALL collections in the catalog.

\b Examples: portolan stac-geoparquet # Generate for ALL collections portolan stac-geoparquet -c landsat # Generate for landsat collection portolan stac-geoparquet -c imagery --dry-run # Preview without creating portolan stac-geoparquet --json # JSON output for all collections

skills()

List and view AI skills for Portolan workflows.

Skills have moved to: https://github.com/portolan-sdi/portolan-skills

skills_list_cmd(ctx, json_output)

List available skills.

skills_show_cmd(ctx, name, json_output)

View a skill.

detect_format(path, catalog_root=None)

Return the format family of a file, asking the plugin format handlers first.

Parameters:

Name Type Description Default
path Path

Path to the file or directory to detect.

required
catalog_root Path | None

Catalog root, for a conversion.handlers pin.

None

Returns:

Type Description
str | None

The family of the handler that claims the file, for example

str | None

vector or raster. None when no handler claims it.

Raises:

Type Description
FileNotFoundError

If the file does not exist.

IsADirectoryError

If the path is a non-geospatial directory.

Plugin API

Import these names from portolan_cli.api. The module follows semantic versioning.

Hooks

hookimpl = pluggy.HookimplMarker(PROJECT_NAME) module-attribute

EnrichContext dataclass

Where the STAC document that a hook enriches lives.

Attributes:

Name Type Description
catalog_root Path

Directory that holds catalog.json.

collection_id str

Collection identifier, nested ids joined with /.

document_path Path

Path of the item or collection JSON file.

item_id str | None

Item identifier. None for a collection.

Format handlers

FormatHandler

Bases: Protocol

Convert one file format to a cloud-native file and read its metadata.

Attributes:

Name Type Description
name str

Unique handler name, used by conversion.handlers.

family str

Free text that groups formats, for example vector.

display_name str

Name for messages, for example GeoParquet.

extensions frozenset[str]

Lowercase source extensions with a dot, for example .xyz.

media_type str

Media type of the converted file.

asset_level str

collection when a single file in a collection directory is a collection-level asset, item when each file is an item.

name property

Unique handler name.

family property

Format family.

display_name property

Name for messages.

extensions property

Source extensions.

media_type property

Media type of the converted file.

asset_level property

Either collection or item.

claims(path)

Return True when this handler reads path. Read file contents if needed.

output_path(source, output_dir)

Return where convert writes the cloud-native file for source.

add --force keeps an output that exists at this path.

convert(source, output_dir, catalog_root)

Write the cloud-native file into output_dir and return its path.

Validate the result and raise InvalidOutputError when it fails. Write a thumbnail here when the format has one. add and check --fix both call this.

read_metadata(path)

Read the metadata of a converted file.

describe_item(item, output, metadata, context)

Add format fields to a new STAC item before Portolan saves it.

item is a pystac.Item. Change it in place: properties, assets, and stac_extensions.

describe_collection(collection, metadata, context)

Add format fields to a collection after an add.

collection is a pystac.Collection. metadata holds what read_metadata returned for each file this add converted.

inspect(path, catalog_root)

Return a FileInfo for portolan info. Raise ValueError if unsupported.

FormatMetadata

Bases: Protocol

What a handler reads from a converted file.

Attributes:

Name Type Description
bbox Any

[min_x, min_y, max_x, max_y] in the file CRS.

crs Any

The file CRS as an authority string, a PROJJSON dict, or None.

bbox property

Bounding box in the file CRS.

crs property

File CRS.

to_stac_properties()

Return the STAC item properties this format contributes.

FormatContext dataclass

Where a file sits, for the handler methods that describe STAC.

Attributes:

Name Type Description
catalog_root Path

Directory that holds catalog.json.

collection_dir Path

Directory that holds collection.json.

source Path | None

The file the operator added. None for a collection.

defaults dict[str, Any]

The defaults section of the merged metadata.yaml.

merge_strategy Any

How add merges detected values into existing ones.

Versioning backends

VersioningBackend

Bases: Protocol

Protocol for version storage and management backends.

This protocol defines the interface that all versioning backends must implement. The MVP uses JsonFileBackend (versions.json), while enterprise deployments can use IcebergBackend via the portolake plugin for ACID transactions.

All methods take a collection parameter identifying which collection to operate on. The collection path is relative to the catalog root.

Thread Safety

This protocol assumes single-writer access. Implementations are NOT required to be thread-safe. Use drift checking to detect concurrent modifications. For true concurrent access, use a backend with distributed locking (e.g., IcebergBackend via portolake).

get_current_version(collection)

Get the current (latest) version of a collection.

Parameters:

Name Type Description Default
collection str

Collection identifier/path.

required

Returns:

Type Description
Version

The current Version object.

Raises:

Type Description
FileNotFoundError

If the collection has no versions.

list_versions(collection)

List all versions of a collection, oldest first.

Parameters:

Name Type Description Default
collection str

Collection identifier/path.

required

Returns:

Type Description
list[Version]

List of Version objects, ordered oldest to newest.

publish(collection, assets, schema, breaking, message, removed=None, version=None)

Publish a new version of a collection.

Parameters:

Name Type Description Default
collection str

Collection identifier/path.

required
assets dict[str, str]

Mapping of asset names to asset paths/URIs.

required
schema SchemaFingerprint

Schema fingerprint for change detection.

required
breaking bool

Whether this is a breaking change.

required
message str

Human-readable description of the change.

required
removed set[str] | None

Set of asset keys to remove from the version.

None
version str | None

Explicit version string. If None, auto-compute next version.

None

Returns:

Type Description
Version

The newly created Version object.

rollback(collection, target_version)

Rollback to a previous version.

This creates a NEW version with the contents of the target version, preserving full history. It does not delete intermediate versions.

Parameters:

Name Type Description Default
collection str

Collection identifier/path.

required
target_version str

Semantic version string to roll back to.

required

Returns:

Type Description
Version

The newly created Version object (representing the rollback).

Raises:

Type Description
ValueError

If target_version doesn't exist.

prune(collection, keep, dry_run)

Remove old versions, keeping the N most recent.

Parameters:

Name Type Description Default
collection str

Collection identifier/path.

required
keep int

Number of recent versions to keep.

required
dry_run bool

If True, don't actually delete, just report what would be deleted.

required

Returns:

Type Description
list[Version]

List of Version objects that were (or would be) deleted.

check_drift(collection)

Check for drift between local and remote state.

Detects if another process has modified the remote versions.json without going through this backend. Used to catch accidental concurrent access in the MVP single-writer model.

Parameters:

Name Type Description Default
collection str

Collection identifier/path.

required

Returns:

Type Description
DriftReport

DriftReport with drift status and details.

DriftReport

Bases: TypedDict

Report from drift detection between local and remote state.

Attributes:

Name Type Description
has_drift bool

True if local and remote versions differ.

local_version str | None

Local version string, or None if no local version exists.

remote_version str | None

Remote version string, or None if no remote version exists.

message str

Human-readable description of the drift status.

SchemaFingerprint

Bases: TypedDict

Schema fingerprint for detecting breaking changes between versions.

Attributes:

Name Type Description
columns list[str]

Ordered list of column names.

types dict[str, str]

Mapping of column names to their data types (e.g., "int64", "geometry").

hash str

Hash of the schema for quick comparison.

PostAddContext

Bases: TypedDict

Context passed to a backend's optional on_post_add() hook.

After add() completes local processing, portolan-cli checks hasattr(backend, "on_post_add") and, if present, calls it with this context. Backends that do not implement on_post_add() are silently skipped.

Optional backend methods (checked via hasattr, NOT part of VersioningBackend): on_post_add(context: PostAddContext) -> None pull(remote_url: str, local_root: Path, collection: str, *, dry_run: bool) -> PullResult supports_push() -> bool push_blocked_message(remote: str | None) -> str

Note

The collection field is typed as Any to avoid a hard dependency on pystac. At runtime, this will be a pystac.Collection instance.

PostAddItemContext

Bases: TypedDict

Per-item context within PostAddContext.items list.

Attributes:

Name Type Description
item_id str

The STAC item identifier.

item_dir Path

Path to the item directory containing assets.

asset_files dict[str, tuple[Path, str, int]]

Dict mapping filename to (path, checksum, size) tuples.

Version dataclass

A single version entry in the versions history.

Attributes:

Name Type Description
version str

Semantic version string (e.g., "1.0.0").

created datetime

UTC timestamp when this version was created.

breaking bool

Whether this version has breaking changes.

assets dict[str, Asset]

Mapping of filename to Asset metadata.

changes list[str]

List of filenames that changed in this version.

schema SchemaInfo | None

Optional schema fingerprint for breaking change detection.

message str | None

Optional human-readable description of the change.

Asset dataclass

A single asset (file) within a version.

Attributes:

Name Type Description
sha256 str

SHA-256 checksum of the file content.

size_bytes int

File size in bytes.

href str

Path to the asset, relative to catalog root (e.g., "collection_id/item_id/filename.parquet"). push.py and pull.py resolve this via catalog_root / href.

source_path str | None

Optional relative path to the original source file (e.g., the GeoJSON that was converted to this GeoParquet).

source_mtime float | None

Optional Unix timestamp of the source file when conversion occurred. Used to detect when source has changed.

mtime float | None

Optional Unix timestamp of the asset file itself. Used for fast-path change detection.

feature_count int | None

Optional feature/row count (pixel count for rasters) captured when the asset was tracked. Lets a touched-but-identical asset read FRESH instead of a spurious STALE (heuristics).

schema_fingerprint str | None

Optional hash of the asset schema captured when the asset was tracked. Used to detect breaking schema changes.

SchemaInfo dataclass

Schema information for breaking change detection.

Attributes:

Name Type Description
type str

Schema type identifier (e.g., "geoparquet", "cog").

fingerprint dict[str, Any]

Type-specific schema fingerprint for change detection.

parse_version(version_str)

Parse a semantic version string into (major, minor, patch) tuple.

Handles standard semver, pre-release versions (CRITICAL #3), and build metadata. Returns (0, 0, 0) for invalid versions instead of raising.

Examples:

>>> parse_version("1.2.3")
(1, 2, 3)
>>> parse_version("1.0.0-beta")  # Pre-release stripped
(1, 0, 0)
>>> parse_version("1.0.0+build.123")  # Build metadata stripped
(1, 0, 0)
>>> parse_version("invalid")
(0, 0, 0)

Parameters:

Name Type Description Default
version_str str

Semantic version string (e.g., "1.2.3", "1.0.0-beta").

required

Returns:

Type Description
tuple[int, int, int]

Tuple of (major, minor, patch) integers. Returns (0, 0, 0) if parsing fails.

get_backend(name='file', catalog_root=None)

Get a versioning backend by name.

Note: Creates a NEW instance on each call. This function does not implement singleton semantics; each call returns a fresh backend instance.

Parameters:

Name Type Description Default
name str

Backend name. "file" for built-in, "iceberg" for Iceberg (requires [iceberg] extra), or a name a plugin registers.

'file'
catalog_root Path | None

Catalog root the backend reads and writes. None lets the backend resolve the root itself.

None

Returns:

Type Description
VersioningBackend

VersioningBackend instance.

Raises:

Type Description
ValueError

If the backend is not found, its factory fails, or it does not implement VersioningBackend. A factory's own ValueError, such as the missing iceberg extra, passes through unchanged.

Example

backend = get_backend() # Default file backend backend = get_backend("file") # Explicit file backend backend = get_backend("iceberg") # Iceberg backend (requires [iceberg] extra)

Catalogs

find_catalog_root(start_path=None, *, require_operational=True)

Find the catalog root by walking up from the given path.

Searches for a managed Portolan catalog starting from start_path (or cwd if None) and walking up parent directories. This provides git-style behavior where commands work from any subdirectory within a catalog.

issue #290, this uses .portolan/config.yaml as the sole sentinel, unifying detection across all CLI commands. By default (require_operational=True), it also requires catalog.json to exist to avoid detecting half-initialized repos.

Security: Limited to MAX_CATALOG_SEARCH_DEPTH levels to prevent traversing to filesystem root where a malicious .portolan might exist.

Parameters:

Name Type Description Default
start_path Path | None

Starting directory for search (defaults to cwd).

None
require_operational bool

If True (default), require .portolan/config.yaml AND catalog.json to exist. Set to False during init_catalog() when creating a new catalog where config.yaml is written before catalog.json.

True

Returns:

Type Description
Path | None

Path to catalog root if found, None otherwise.

Examples:

>>> find_catalog_root(Path("/my-catalog/collection/item"))
PosixPath('/my-catalog')
>>> find_catalog_root(Path("/no-catalog-here"))
None
>>> find_catalog_root()  # Uses current working directory
PosixPath('/my-catalog')
>>> # During init, check for config.yaml only (catalog.json not yet written)
>>> find_catalog_root(start_path, require_operational=False)

get_setting(key, cli_value=None, catalog_path=None, collection=None, collection_path=None)

Resolve a setting with full precedence.

Precedence (highest to lowest): 1. CLI argument (cli_value) 2. Environment variable (PORTOLAN_) 3. Hierarchical .portolan/ config (if collection_path specified) 4. Legacy collection-level config (collections: section, if collection specified) 5. Catalog-level config 6. Built-in default (None)

Parameters:

Name Type Description Default
key str

Setting key (e.g., "remote", "aws_profile")

required
cli_value Any | None

Value passed via CLI argument (highest precedence)

None
catalog_path Path | None

Path to catalog root for loading config file

None
collection str | None

Optional collection name for legacy collection-level config

None
collection_path Path | None

Optional path to collection directory for hierarchical lookup

None

Returns:

Type Description
Any | None

Resolved value, or None if not found at any level.

write_json_atomic(path, data)

Serialize data to path as JSON, atomically.

Writes to a temporary file in the destination's own directory and then os.replaces it into position, so readers observe either the previous content or the complete new content — never a truncated file. The temporary file is removed if serialization or writing fails, and path is left untouched in that case.

Output format (identical at every call site): json.dumps(data, indent=2, ensure_ascii=False) plus a trailing newline, encoded UTF-8. ensure_ascii=False keeps accented titles ("Córdoba") literal instead of \\u00f3 escapes.

Parameters:

Name Type Description Default
path Path

Destination file. Parent directories are created if needed.

required
data Any

Any JSON-serializable object.

required

Raises:

Type Description
TypeError

If data is not JSON-serializable (nothing is written).

OSError

If the temporary file cannot be written or renamed.

Extractors

Extractor

Bases: Protocol

A source of data that Portolan can extract into a catalog.

Attributes:

Name Type Description
name str

The subcommand name, portolan extract <name>.

summary str

One line that describes the source.

extract(source, output_dir, **options)

Extract source into a catalog in output_dir.

Parameters:

Name Type Description Default
source str

Service URL or other source locator.

required
output_dir Path

Directory of the catalog to write.

required
**options Any

Source-specific options.

{}

Returns:

Type Description
ExtractionReport

The extraction report.

cli_command()

Return the Click command for portolan extract <name>.

Import the command module inside this method, so portolan --help does not pay for it.

ExtractionReport dataclass

Complete extraction report for a service.

This is the top-level report that gets written to .portolan/extraction-report.json.

Attributes:

Name Type Description
extraction_date str

ISO 8601 timestamp of extraction.

source_url str

The service URL that was extracted.

portolan_version str

Version of Portolan CLI used.

gpio_version str

Version of geoparquet-io used.

metadata_extracted MetadataExtracted

Harvested service metadata.

layers list[LayerResult]

Results for each layer extraction attempt.

summary ExtractionSummary

Aggregate statistics.

folder_coverage FolderCoverage | None

Optional coverage data from recursive folder traversal.

to_dict()

Convert to JSON-serializable dict.

from_dict(data) classmethod

Create ExtractionReport from dict.

Layer sources

LayerSource

Bases: Protocol

A data source that serves named layers.

Attributes:

Name Type Description
name str

The subcommand name, portolan extract <name>. It is also the default source_type in seeded metadata.

summary str

One line that describes the source.

discover(source, options)

Find the layers at source and the catalog-level metadata.

Do the work that happens once per run here, such as protocol negotiation or a walk through a services root. Put what fetch needs for a layer into LayerRef.data.

fetch(source, layer, dest)

Write layer to dest as Parquet. Return the row count.

Raise EmptyLayerError for a layer with no features. Raise any other exception for a failure. The driver retries both.

layer_metadata(source, layer)

Return the metadata for the layer's collection, or None to skip it.

styles(source, layer, collection_dir)

Write style or legend files next to the layer's data, or do nothing.

Return the via link to the original layer, or None.

Discovery dataclass

What discover found at a source.

Attributes:

Name Type Description
source_url str

URL that the report records and that the other methods receive as source.

layers list[LayerRef]

Layers in discovery order.

metadata MetadataExtracted | None

Service metadata for the report. None records the URL only.

catalog_seed ExtractedMetadata | None

Metadata that seeds the catalog metadata.yaml.

license_url str | None

License URL that the source publishes, if any.

catalog_title str | None

Title for a new catalog, or None for the default.

catalog_description str | None

Description for a new catalog, or None.

root_metadata tuple[str | None, str | None] | None

(title, description) written to catalog.json after a new catalog is created, or None to leave it.

folder_coverage FolderCoverage | None

Folders that a services-root traversal visited.

LayerRef dataclass

One layer that a source serves.

Attributes:

Name Type Description
id int

Layer id as the source knows it. The report records it.

name str

Layer name. Filters and resume match on it.

path str | None

Collection directory relative to the catalog, /-separated. The data file is <path>/<last segment>.parquet. None uses a slug of the name.

data Any

The source's own data for the layer, for fetch and the other methods. The driver does not read it.

notes tuple[str, ...]

Warnings that a dry run reports for the layer.

LayerMetadata dataclass

Descriptive metadata for one layer's collection.

Attributes:

Name Type Description
title str | None

Collection title.

description str | None

Collection description.

keywords list[str] | None

Discovery keywords.

layer_name str | None

Layer name for the processing notes. None uses the name.

source_type str | None

source_type in metadata.yaml. None uses the source's name.

source_url str | None

Layer URL for provenance. None uses the source link.

record ExtractedMetadata | None

A full metadata record, such as an ISO 19139 record. It replaces the fields above in metadata.yaml, and title and description go to collection.json.

record_label str

Name of the record kind for the progress message.

A provenance via link from a collection to the original layer.

LayerSourceOptions dataclass

Options for run_layer_extraction.

Attributes:

Name Type Description
layer_filter list[str] | None

Glob patterns of layer names to include. None includes all.

layer_exclude list[str] | None

Glob patterns of layer names to exclude.

resume bool

Skip layers that succeeded or were empty in the previous report.

dry_run bool

Report the layers without writing anything.

raw bool

Write the layer files and the report, but no catalog.

retries int

Attempts per layer.

retry_delay float

Seconds before the first retry. Later retries double it.

workers int

Layers to fetch at the same time.

layer_timeout float | None

With more than one worker, seconds a layer may run before it is recorded as failed. None sets no limit.

catalog_id str | None

Catalog id. None derives it from the output directory.

license str | None

SPDX license identifier, or other with license_url.

license_url str | None

URL of the license text.

EmptyLayerError

Bases: Exception

Raised by fetch when the layer has no features.

The driver records the layer as empty, not failed. A resumed run does not try it again.

ExtractedMetadata dataclass

Source-agnostic metadata extracted from a data source.

This class holds metadata that can be extracted from various sources (ArcGIS services, GDAL files, etc.) and used to seed metadata.yaml.

All fields except source_type may be None if not available from the source. The seeding process will convert None values to TODO placeholders for required fields.

Attributes:

Name Type Description
source_type str

Identifier for the extraction source (e.g., "arcgis_featureserver").

source_url str | None

URL of the original data source.

attribution str | None

Copyright or attribution text.

description str | None

Human-readable description of the data.

keywords list[str] | None

List of discovery keywords/tags.

contact_name str | None

Name of the data maintainer/author.

contact_email str | None

Email of the data maintainer (rarely available).

license_raw str | None

Raw license text (not SPDX identifier).

license_url str | None

URL of the license text, read out of license_raw.

processing_notes str | None

Notes about data processing or updates.

known_issues str | None

Known limitations or caveats.

source_type instance-attribute

Identifier for the extraction source (e.g., 'arcgis_featureserver').

source_url = None class-attribute instance-attribute

URL of the original data source.

attribution = None class-attribute instance-attribute

Copyright or attribution text.

description = None class-attribute instance-attribute

Human-readable description of the data.

keywords = None class-attribute instance-attribute

List of discovery keywords/tags.

contact_name = None class-attribute instance-attribute

Name of the data maintainer/author.

contact_email = None class-attribute instance-attribute

Email of the data maintainer (rarely available from extraction).

license_url = None class-attribute instance-attribute

Raw license text (not SPDX identifier, for human review).

processing_notes = None class-attribute instance-attribute

Notes about data processing or update frequency.

known_issues = None class-attribute instance-attribute

Known limitations, caveats, or access restrictions.

to_dict()

Convert to a dictionary, excluding None values.

Returns:

Type Description
dict[str, object]

Dictionary with non-None fields.

run_layer_extraction(layer_source, source, output_dir, options=None, *, on_progress=None)

Extract the layers of source into a Portolan catalog.

Steps: discover, filter, check the license, resume, fetch with retry, save the report to .portolan/extraction-report.json, then build the catalog unless options.raw is set.

Parameters:

Name Type Description Default
layer_source LayerSource

The source implementation.

required
source str

Service URL or other source locator.

required
output_dir Path

Catalog directory.

required
options LayerSourceOptions | None

Extraction options. None uses the defaults.

None
on_progress Callable[[ExtractionProgress], None] | None

Called with progress for each layer.

None

Returns:

Type Description
ExtractionReport

The extraction report.

Raises:

Type Description
MissingLicenseError

Before any download, when no license can be resolved and options.raw is not set.

InputValidationError

Before any write, when a layer's path could leave the catalog.

extractor_from(layer_source)

Wrap a LayerSource as an Extractor for portolan_extractors.

Parameters:

Name Type Description Default
layer_source LayerSource

The source implementation.

required

Returns:

Type Description
LayerSourceExtractor

An extractor with the source's name and summary.

Remote storage

upload_file(source, destination, *, profile=None, dry_run=False, s3_endpoint=None, s3_region=None, s3_use_ssl=None, chunk_concurrency=4)

Upload a single file to S3/GCS/Azure.

Parameters:

Name Type Description Default
source Path

Local file path

required
destination str

Object store URL (e.g., s3://bucket/key)

required
profile str | None

AWS profile name (for S3 only)

None
dry_run bool

If True, show what would be uploaded without actually uploading

False
s3_endpoint str | None

Custom S3-compatible endpoint (e.g., "minio.example.com:9000")

None
s3_region str | None

S3 region (default: auto-detected)

None
s3_use_ssl bool | None

Whether to use HTTPS for S3 endpoint (default: True)

None
chunk_concurrency int

Max concurrent chunks per file (default: 4)

4

Returns:

Type Description
UploadResult

UploadResult with upload statistics

Raises:

Type Description
FileNotFoundError

If source file does not exist

Example

result = upload_file(Path("data.parquet"), "s3://bucket/data.parquet") if result.success: ... print(f"Uploaded {result.files_uploaded} file(s)")

download_file(source, destination, *, profile=None, dry_run=False, overwrite=True, s3_endpoint=None, s3_region=None, s3_use_ssl=None, chunk_concurrency=4)

Download a single file from S3/GCS/Azure.

Parameters:

Name Type Description Default
source str

Object store URL (e.g., s3://bucket/key)

required
destination Path

Local file path or directory

required
profile str | None

AWS profile name (for S3 only)

None
dry_run bool

If True, show what would be downloaded without actually downloading

False
overwrite bool

If False, skip existing files (default: True)

True
s3_endpoint str | None

Custom S3-compatible endpoint (e.g., "minio.example.com:9000")

None
s3_region str | None

S3 region (default: auto-detected)

None
s3_use_ssl bool | None

Whether to use HTTPS for S3 endpoint (default: True)

None
chunk_concurrency int

Max concurrent chunks per file (default: 4)

4

Returns:

Type Description
DownloadResult

DownloadResult with download statistics

Raises:

Type Description
ValueError

If source or destination is empty/invalid

Example

result = download_file("s3://bucket/data.parquet", Path("data.parquet")) if result.success: ... print(f"Downloaded {result.files_downloaded} file(s)")

PullResult dataclass

Result of a pull operation.

Attributes:

Name Type Description
success bool

True if pull completed successfully.

files_downloaded int

Number of files downloaded.

files_skipped int

Number of files skipped (already up to date).

local_version str | None

Local version before pull.

remote_version str | None

Remote version (None in dry-run mode since no network check).

uncommitted_changes list[str]

List of files with uncommitted changes that blocked pull.

up_to_date bool

True if already at remote version (nothing to pull).

dry_run bool

True if this was a dry-run operation (no network calls made).

files_restored int

Number of files restored (re-downloaded because missing locally).

Terminal output

success(message, *, file=None, nl=True, dry_run=False)

Print a success message with green checkmark.

Parameters:

Name Type Description Default
message str

The message to display.

required
file TextIO | None

File to write to (default: stdout).

None
nl bool

Whether to print a newline after the message.

True
dry_run bool

If True, prefix with [DRY RUN] to indicate simulation mode.

False
Example

success("Wrote output.parquet (1.2 MB)") ✓ Wrote output.parquet (1.2 MB)

success("Would write file", dry_run=True) ✓ [DRY RUN] Would write file

info(message, *, file=None, nl=True, dry_run=False)

Print an info message with blue arrow.

Parameters:

Name Type Description Default
message str

The message to display.

required
file TextIO | None

File to write to (default: stdout).

None
nl bool

Whether to print a newline after the message.

True
dry_run bool

If True, prefix with [DRY RUN] to indicate simulation mode.

False
Example

info("Reading data.shp (4,231 features)") → Reading data.shp (4,231 features)

info("Would read file", dry_run=True) → [DRY RUN] Would read file

warn(message, *, file=None, nl=True, dry_run=False)

Print a warning message with yellow warning symbol.

Parameters:

Name Type Description Default
message str

The message to display.

required
file TextIO | None

File to write to (default: stderr).

None
nl bool

Whether to print a newline after the message.

True
dry_run bool

If True, prefix with [DRY RUN] to indicate simulation mode.

False
Example

warn("Missing thumbnail (recommended)") âš  Missing thumbnail (recommended)

warn("Would skip validation", dry_run=True) âš  [DRY RUN] Would skip validation

error(message, *, file=None, nl=True, dry_run=False)

Print an error message with red X.

Parameters:

Name Type Description Default
message str

The message to display.

required
file TextIO | None

File to write to (default: stderr).

None
nl bool

Whether to print a newline after the message.

True
dry_run bool

If True, prefix with [DRY RUN] to indicate simulation mode.

False
Example

error("No geometry column (required)") ✗ No geometry column (required)

error("Would fail validation", dry_run=True) ✗ [DRY RUN] Would fail validation

detail(message, *, file=None, nl=True, dry_run=False)

Print a detail/progress message in dimmed text.

Parameters:

Name Type Description Default
message str

The message to display.

required
file TextIO | None

File to write to (default: stdout).

None
nl bool

Whether to print a newline after the message.

True
dry_run bool

If True, prefix with [DRY RUN] to indicate simulation mode.

False
Example

detail("Processing chunk 3/10...") Processing chunk 3/10...

detail("Would process chunk", dry_run=True) [DRY RUN] Would process chunk

JSON output for commands

success_envelope(command, data)

Create a success envelope with the given command and data.

Parameters:

Name Type Description Default
command str

Name of the command (e.g., "scan", "init", "check")

required
data dict[str, Any]

Command-specific payload

required

Returns:

Type Description
OutputEnvelope

OutputEnvelope with success=True and errors=None

error_envelope(command, errors, *, data=None)

Create an error envelope with the given command and errors.

Parameters:

Name Type Description Default
command str

Name of the command (e.g., "scan", "init", "check")

required
errors list[ErrorDetail]

List of ErrorDetail objects describing the errors

required
data dict[str, Any] | None

Optional partial data to include (default: empty dict)

None

Returns:

Type Description
OutputEnvelope

OutputEnvelope with success=False and the provided errors

ErrorDetail dataclass

Structure for individual error entries in the errors array.

Attributes:

Name Type Description
type str

Error class name (e.g., "CatalogExistsError", "FileNotFoundError")

message str

Human-readable error description

code str | None

Optional structured error code (e.g., "PRTLN-CAT001")

to_dict()

Convert to dictionary for JSON serialization.

Warnings

PluginWarning

Bases: UserWarning

A plugin misbehaved. The CLI keeps running without its contribution.

Format handler helpers

FileInfo dataclass

Metadata extracted from a geospatial file.

Combines format-specific metadata from upstream libraries with version tracking from versions.json.

Attributes:

Name Type Description
path Path

Path to the file.

format str

File format (GeoParquet, COG, etc.).

crs str | None

Coordinate reference system (EPSG code or WKT).

bbox list[float] | None

Bounding box as [minx, miny, maxx, maxy].

feature_count int | None

Number of features (vector files only).

width int | None

Image width in pixels (raster files only).

height int | None

Image height in pixels (raster files only).

band_count int | None

Number of bands (raster files only).

dtype str | None

Data type (raster files only).

version str | None

Version string from versions.json (if tracked).

geometry_type str | None

Geometry type (vector files only).

to_dict()

Convert to JSON-serializable dictionary.

format_human()

Format for human-readable output.

Returns:

Type Description
list[str]

List of output lines in the format: Format: GeoParquet CRS: EPSG:4326 Bbox: [-122.5, 37.7, -122.3, 37.9] Features: 4,231 Version: v1.2.0

InvalidOutputError

Bases: ConversionError

Raised when a format handler writes a file that fails its own validation.

Error code: PRTLN-CNV008

RASTER_EXTENSIONS = _reg.extensions_where(routes_as='raster') module-attribute

VECTOR_EXTENSIONS = _reg.extensions_where(routes_as='vector') module-attribute

aggregate_table_extension(collection, metadata, context)

Aggregate table:* metadata from tracked parquet on disk (issues #304/#447).

Runs only when the batch holds at least one NEW GeoParquet file. Recomputes aggregates from ALL tracked parquet files on disk instead of carrying forward stale counts. This prevents double-counting on re-add and stale counts when files are replaced. The built-in GeoParquet handler calls this from describe_collection.

Parameters:

Name Type Description Default
collection Collection

The collection in memory.

required
metadata list[Any]

What the handler read from each new file.

required
context FormatContext

Where the collection sits, and the merge strategy.

required

convert_raster(source, dest_dir, catalog_root=None, *, reoptimize=False)

Convert a raster to a COG. add and check --fix both use this (#947).

A source that is already a COG is kept, unless reoptimize asks to rewrite it with the current settings (issue #530). Otherwise convert.write_cog writes the COG with the catalog's conversion.cog settings: DEFLATE, 512x512 tiles, and a predictor and overview resampling derived from the source dtype by default (issue #690).

Parameters:

Name Type Description Default
source Path

Source raster file.

required
dest_dir Path

Destination directory.

required
catalog_root Path | None

Catalog root, for the COG settings. None uses the defaults.

None
reoptimize bool

Rewrite a source that is already a COG.

False

Returns:

Type Description
Path

Path to the output COG file.

convert_vector(source, dest_dir, catalog_root=None, *, reconvert=False, settings=None, partition_dir=None)

Convert a vector file to GeoParquet. add and check --fix both use this (#947).

Applies the catalog's conversion.vector settings, which sort rows and add a bbox covering column by default (issue #805). A .parquet source is copied unless :func:_needs_spatial_rewrite says it must be rewritten.

add writes one file. A configured spatial index becomes a column of that file, whatever partition says. check --fix passes partition_dir instead, and the table is written as Hive partitions by that index. Each source gets its own partition_dir, because two sources in one directory share cells, and geoparquet-io does not write over a cell that exists.

Parameters:

Name Type Description Default
source Path

Source vector file.

required
dest_dir Path

Destination directory.

required
catalog_root Path | None

Catalog root, for reading conversion.vector settings. None uses the built-in defaults.

None
reconvert bool

True when the operator asked to re-convert from source, which rewrites a Parquet file whatever its footer already declares.

False
settings VectorSettings | None

Explicit vector settings. None reads them from the catalog.

None
partition_dir Path | None

Directory for Hive partitions by the configured spatial index. It applies only to a non-Parquet source, when the settings set partition and a spatial index.

None

Returns:

Type Description
Path

Path to the output GeoParquet file, or partition_dir when the

Path

table was written as partitions.

describe_raster_item(item, output, metadata, context)

Add the raster fields to a new item: render, band statistics, nodata, extension.

The built-in COG handler calls this from describe_item (#947).

Parameters:

Name Type Description Default
item Item

The new item.

required
output Path

The COG.

required
metadata Any

What extract_cog_metadata returned.

required
context FormatContext

Where the file sits.

required

describe_vector_item(item, output, metadata, context)

Add the vector fields to a new item: column statistics and the vector extension.

The built-in GeoParquet handler calls this from describe_item (#947).

Parameters:

Name Type Description Default
item Item

The new item.

required
output Path

The GeoParquet file.

required
metadata Any

What extract_geoparquet_metadata returned.

required
context FormatContext

Where the file sits.

required

extract_cog_metadata(path)

Extract metadata from a COG file.

Uses rasterio to read file metadata without loading pixel data.

Parameters:

Name Type Description Default
path Path

Path to COG file.

required

Returns:

Type Description
COGMetadata

COGMetadata with extracted information.

Raises:

Type Description
FileNotFoundError

If file doesn't exist.

RasterioIOError

If file is not valid raster.

extract_flatgeobuf_metadata(path)

Extract metadata from a FlatGeobuf file.

Uses pyogrio to read header metadata without loading all data.

Parameters:

Name Type Description Default
path Path

Path to FlatGeobuf file.

required

Returns:

Type Description
FlatGeobufMetadata

FlatGeobufMetadata with extracted information.

Raises:

Type Description
FileNotFoundError

If file doesn't exist.

ValueError

If file is not a valid FlatGeobuf file.

extract_geoparquet_metadata(path)

Extract metadata from a GeoParquet file.

Uses pyarrow to read file metadata without loading all data. Parses GeoParquet geo metadata from file's custom metadata.

Parameters:

Name Type Description Default
path Path

Path to GeoParquet file.

required

Returns:

Type Description
GeoParquetMetadata

GeoParquetMetadata with extracted information.

Raises:

Type Description
FileNotFoundError

If file doesn't exist.

Note

For Parquet files without GeoParquet metadata, returns metadata with None/empty fields for geo-specific properties (bbox, crs, geometry_type).

extract_pmtiles_metadata(path)

Extract metadata from a PMTiles file.

Uses the pmtiles package to read header metadata without loading tiles.

Parameters:

Name Type Description Default
path Path

Path to PMTiles file.

required

Returns:

Type Description
PMTilesMetadata

PMTilesMetadata with extracted information.

Raises:

Type Description
FileNotFoundError

If file doesn't exist.

ValueError

If file is not a valid PMTiles file.

ImportError

If pmtiles package is not installed.

inspect_cog(path, *, catalog_root=None)

Extract metadata from a COG file.

Parameters:

Name Type Description Default
path Path

Path to the COG file.

required
catalog_root Path | None

Optional catalog root for version lookup.

None

Returns:

Type Description
FileInfo

FileInfo with COG metadata.

inspect_geoparquet(path, *, catalog_root=None)

Extract metadata from a GeoParquet file.

Parameters:

Name Type Description Default
path Path

Path to the GeoParquet file.

required
catalog_root Path | None

Optional catalog root for version lookup.

None

Returns:

Type Description
FileInfo

FileInfo with GeoParquet metadata.

is_geojson(path)

Check if a .json file is GeoJSON.

Uses prefix reading (first 8KB) to avoid OOM on large files. Searches for GeoJSON type tokens without full JSON parsing.

STAC items/collections/catalogs are NOT GeoJSON even though they have "type": "Feature". They are identified by "stac_version".

Parameters:

Name Type Description Default
path Path

Path to JSON file.

required

Returns:

Type Description
bool

True if the prefix looks like GeoJSON.

is_geoparquet(path)

Check if a Parquet file has GeoParquet metadata.

GeoParquet files have a 'geo' key in their schema metadata that contains geospatial column information.

Parameters:

Name Type Description Default
path Path

Path to the Parquet file.

required

Returns:

Name Type Description
bool

True if the file is a valid GeoParquet, False otherwise.

Note bool

Returns False for both non-geo Parquet AND invalid/corrupted files.

bool

Use is_valid_parquet() first if you need to distinguish these cases.

partition_vector_file(path, catalog_root)

Partition a converted GeoParquet file when partition_strategy says so.

The partitions go next to the file in Hive directories, through partition_geoparquet. The file is removed only when the step wrote at least one partition file.

The directory must hold no partitions for the strategy. geoparquet-io does not write over a cell that exists, so a second source, or a re-add, would lose the rows in each shared cell.

Parameters:

Name Type Description Default
path Path

The converted GeoParquet file.

required
catalog_root Path | None

Catalog root, for the settings.

required

Returns:

Type Description
list[Path]

The partition files, or an empty list when the file stays whole.

Raises:

Type Description
PartitionCollisionError

The directory already holds partitions.

validate_cog(path)

Validate that the output file is a valid COG.

Parameters:

Name Type Description Default
path Path

Path to the TIFF file to validate.

required

Returns:

Type Description
str | None

Error message if validation failed, None if valid.

validate_geoparquet(path)

Validate that the output is valid GeoParquet (file or partitioned directory).

Parameters:

Name Type Description Default
path Path

Path to the parquet file or hive-partitioned directory.

required

Returns:

Type Description
str | None

Error message if validation failed, None if valid.

write_raster_thumbnail(cog_path, catalog_root)

Write <stem>.thumb.jpg next to a COG when the COG settings ask for it.

A thumbnail that exists stays, so a re-add keeps a hand-made thumbnail. Portolan cannot tell a hand-made thumbnail from one it wrote, so an old thumbnail also stays after the imagery changes. Delete <stem>.thumb.jpg to make the next conversion write a new one. A thumbnail is optional, so a failure only warns.

Parameters:

Name Type Description Default
cog_path Path

The COG.

required
catalog_root Path | None

Catalog root, for the COG settings.

required

Plugin hooks

Use @hookimpl to implement these hooks from portolan_cli.hookspecs.

portolan_backends()

Return versioning backends by name.

Each value is called as factory(catalog_root=path) and must return a VersioningBackend. A class satisfies this. Import heavy dependencies inside the factory, so a catalog that does not use the backend does not pay for them. A catalog selects a backend with the backend setting.

Returns:

Type Description
dict[str, BackendFactory]

Mapping of backend name to factory.

portolan_formats()

Return format handlers.

A handler converts a source format to a cloud-native file and reads its metadata. See portolan_cli.api.FormatHandler. For one file, a conversion.handlers pin wins, then a plugin handler, then a built-in handler.

Returns:

Type Description
list[FormatHandler]

Format handler objects.

portolan_enrich_item(item, context)

Add STAC fields to an item before Portolan publishes it.

The hook receives the item JSON as a dict and changes it in place. It may add properties, stac_extensions entries, and asset fields. Portolan restores id, type, stac_version, and links after the hook.

The hook runs every time portolan add writes the item. It must give the same result when it runs twice.

Portolan calls each implementation on its own. A hook wrapper (wrapper=True or hookwrapper=True) is skipped with a warning.

Parameters:

Name Type Description Default
item dict[str, Any]

STAC item JSON.

required
context EnrichContext

Where the item lives.

required

portolan_enrich_collection(collection, context)

Add STAC fields to a collection before Portolan publishes it.

The rules of portolan_enrich_item apply.

Parameters:

Name Type Description Default
collection dict[str, Any]

STAC collection JSON.

required
context EnrichContext

Where the collection lives.

required

portolan_extractors()

Return data sources for portolan extract.

Each extractor becomes portolan extract <name>. portolan extract --help imports the hook module, so keep it light. The extractor imports its Click command inside cli_command().

A layer-based service can implement LayerSource and wrap it with portolan_cli.api.extractor_from. It then gets filters, resume, retry, the report, and catalog setup from Portolan.

Returns:

Type Description
list[Extractor]

Extractors. A name that a built-in extractor uses is ignored.

Plugin testing

Call these checks from portolan_cli.testing in your plugin tests.

ConformanceError

Bases: AssertionError

A plugin object does not meet its protocol.

check_backend(factory, catalog_root)

Check a versioning backend factory against VersioningBackend.

The check creates the backend in catalog_root, publishes two versions of one collection, and reads them back. rollback and prune may raise NotImplementedError, as the built-in file backend does.

Parameters:

Name Type Description Default
factory Callable[..., Any]

What portolan_backends returns for the backend name.

required
catalog_root Path

An empty directory the backend may write to.

required

Raises:

Type Description
ConformanceError

With one line per problem found.

check_extractor(extractor, source=None, output_dir=None, **options)

Check an extractor against Extractor.

The check reads name and summary and builds the Click command. With source and output_dir, it also runs extract() and checks the report.

Parameters:

Name Type Description Default
extractor Any

What portolan_extractors returns.

required
source str | None

A source to extract, or None to skip the run.

None
output_dir Path | None

An empty directory for the run.

None
**options Any

Options for extract(), such as license.

{}

Raises:

Type Description
ConformanceError

With one line per problem found.

check_format_handler(handler, sample, output_dir)

Check a format handler against FormatHandler with a sample file.

The check reads the handler attributes, converts sample into output_dir, and reads the metadata of the result.

Parameters:

Name Type Description Default
handler Any

What portolan_formats returns.

required
sample Path

A small source file in a format the handler claims.

required
output_dir Path

An empty directory the handler may write to.

required

Raises:

Type Description
ConformanceError

With one line per problem found.