Skip to main content

py-containerio

Shared Azure Blob Storage handler for internal and collaboration projects of Alliance SwissPass. Provides a unified interface for reading and writing data to both local filesystem and Azure Blob Storage, allowing seamless switching between local development and cloud environments.

Features

  • Unified API: Same code works for local files and Azure Blob Storage
  • Flexible configuration: Load Azure config from a dotenv file, environment variables, strings, or a container-config.toml with direnv-populated secrets (see TOML + direnv configuration)
  • Polars integration: Native support for Polars DataFrames and LazyFrames
  • Multiple auth methods: Device code flow (default), client credentials (CI/CD), or SAS tokens
  • CLI tools: Unified io command for listing, downloading, uploading, deleting blobs and generating SAS tokens
  • Memory-efficient: Streaming support with scan_* and sink_* methods

Install containerio

From PyPI

The package is available on PyPI: https://pypi.org/project/containerio/

pip install containerio

Or with uv:

uv add containerio
uv sync

Use containerio as a package dependency

Add to your project's pyproject.toml:

[project]
dependencies = [
    "containerio",
]

Then run:

uv sync

From Git

For development or testing, install directly from the repository:

[project]
dependencies = [
    "containerio",
]

[tool.uv.sources]
containerio = { git = "https://codefloe.com/Alliance-SwissPass/py-containerio.git", tag = "v1.0.0" }

Usage in Other Projects

When installed as a dependency, containerio integrates seamlessly with your project:

  • CLI tools are available - Run uv run io directly from your project
  • Dotenv is the default convention - The CLI best-effort-loads .env.<name> matching --env <name> from your current working directory.
  • Native TOML + direnv support - Pass --container-config to resolve --container as a logical key from a container-config.toml with direnv-populated secrets — see TOML + direnv configuration.
  • Other formats via a small wrapper script - See Providing config to the CLI.
  • No configuration in containerio needed - Just add the dependency and configure in your project

Example workflow in your project:

# Your project directory
cd my-project

# Create your .env.staging file here
cp /path/to/.env.example .env.staging
# Edit .env.staging with your credentials

# List blobs in both containers
uv run io ls --env staging

# List blobs from the local filesystem (./data/ directory)
uv run io ls

# Generate SAS tokens (updates .env.staging)
uv run io generate-sas --env staging --update-env

Example Usage

The storage backend is chosen by what you pass into StorageHandler. Pass nothing (or config=None) for the local filesystem. Pass an AzureStorageConfig for cloud storage.

from containerio import AzureStorageConfig, StorageHandler

# Local filesystem (./data/ directory) — no config needed
handler = StorageHandler()

# Cloud storage — build an AzureStorageConfig explicitly, then pass it in.
# Option 1: read from os.environ (caller is responsible for populating it).
config = AzureStorageConfig.from_env()

# Option 2: load from a specific dotenv file (any path, relative or absolute).
config = AzureStorageConfig.from_env_file(path=".env.staging")
config = AzureStorageConfig.from_env_file(path="/etc/myapp/staging.env")

# Option 3: construct directly from strings (e.g. values loaded from a TOML file).
config = AzureStorageConfig(
    storage_account="<azure_storage_account>",
    tenant_id="<azure_tenant_id>",
)

# Open a handler against a named container. Permissions (read, write,
# delete) are enforced by Azure RBAC on your identity — the handler
# itself does not gate operations.
handler = StorageHandler(container="raw-data", config=config)

# Read files
df = handler.read_csv_blob("path/to/file.csv")
df = handler.read_excel_blob("path/to/file.xlsx")
df, metadata = handler.read_parquet_blob("path/to/file.parquet")

# Lazy scan (memory-efficient for large files)
lf, metadata = handler.scan_parquet_blob("large_file.parquet")
lf = handler.scan_csv_blob("large_file.csv")

# Write to a different container (re-use the same config)
curated = StorageHandler(container="curated", config=config)
curated.write_csv_blob("output.csv", df)
curated.write_parquet_blob("output.parquet", df)
curated.write_excel_blob("output.xlsx", df)

# Stream LazyFrame directly to storage
curated.sink_parquet_blob("output.parquet", lf)

# Upload any file (JSON, YAML, images, etc.)
curated.upload_blob("config.json", "./local/config.json")

# List, download, delete, move
blobs = handler.list_blobs()    # Returns List[BlobInfo]
handler.download_blob("file")   # Download to local ./data/
curated.delete_blob("file")     # Delete a blob or folder
curated.move_blob("old.csv", "archive/old.csv")  # Move/rename

Configuration

Environment Variables

Set STORAGE_ENV (or pass --env <name> to the CLI) to control the storage backend. The table below describes the CLI's best-effort dotenv preload — the library itself reads os.environ, and it is up to you how those vars get there (dotenv, TOML wrapper, direnv, exports, …).

Value Behavior
local (default) Uses local filesystem (./data/ directory)
prod CLI preloads .env (if present), then reads os.environ
any other name CLI preloads .env.<name> (if present), then reads os.environ — e.g. staging → .env.staging, dev → .env.dev, eu-prod → .env.eu-prod

Only local and prod are special. Any other value is treated as an environment name and the corresponding .env.<name> file is preloaded — add as many as you need.

Azure Configuration

For Azure storage, create a .env file (for prod) or .env.staging (for staging).

Important: Restrict permissions on your .env file so only you can access it:

chmod 600 .env

Example .env (production)

AZURE_STORAGE_ACCOUNT=<azure_storage_account>
AZURE_TENANT_ID=<azure_tenant_id>
AZURE_CLIENT_ID=<azure_client_id>

# For CI/CD with client-credentials auth
AZURE_CLIENT_ID_CONTAINER=<container_app_client_id>
AZURE_CLIENT_SECRET_CONTAINER=<container_app_client_secret>

# SAS tokens (optional, only if using SAS auth).
# Suffix is the container name, uppercased with dashes → underscores.
AZURE_STORAGE_SAS_TOKEN_RAW_DATA=<sas_token_for_raw_data>
AZURE_STORAGE_SAS_TOKEN_CURATED=<sas_token_for_curated>

Example .env.staging

Same shape as .env; the file's name is what makes it the staging config.

AZURE_STORAGE_ACCOUNT=<azure_storage_account>
AZURE_TENANT_ID=<azure_tenant_id>
AZURE_CLIENT_ID=<azure_client_id>
AZURE_CLIENT_ID_CONTAINER=<container_app_client_id>
AZURE_CLIENT_SECRET_CONTAINER=<container_app_client_secret>
AZURE_STORAGE_SAS_TOKEN_RAW_DATA_STG=<sas_token_for_staging_container>

Note: There is no registry of containers — you simply pass --container <name> (or container="..." in code) and the call is forwarded to Azure. Whether the container exists and what you can do with it is decided by Azure RBAC on your identity.

TOML + direnv configuration

For projects with several logical containers across environments, py-containerio supports a combined TOML + direnv approach (the Python-native equivalent of r-containerio's containerio_config()):

  • Non-sensitive fields (storage account, container names, and identity IDs) live in a project-local container-config.toml, which is safe to commit.
  • Sensitive values (client secrets) never appear in the TOML: client_secret_env names the environment variable that holds the secret, resolved from os.environ at load time.
  • tenant_id, client_id, and device_code_client_id are resolved environment-first: their *_env keys name environment variables that win when set at runtime, and the TOML value is only the fallback.
  • The environment is populated through direnv: a commit-safe .envrc that pulls values at runtime, e.g. from Azure Key Vault via the authenticated az CLI. direnv only fills gaps (${VAR:-...}), so a value injected by CI (Crow from_secret becomes a plain environment variable) or an existing shell export stays authoritative.

Copy container-config_example.toml from this repository to container-config.toml and adapt it. Each container maps an environment (staging, prod) to a concrete container name and an Azure auth configuration. Auth can be referenced in two ways:

  • Tag indirection (shared auth across containers): azure_auth.<env> is a string referencing a shared [azure_auth.<tag>] section.
  • Inline nested auth (per-container): azure_auth.<env> is a nested table defining the auth fields directly under [containers.<key>.azure_auth.<env>].

If the resolved auth spec defines no storage_account, the full configuration is read from environment variables via AzureStorageConfig.from_env() instead.

Example .envrc (direnv)

The .envrc is committed; the secrets it pulls are not. Secrets are fetched at shell-entry time with the already-authenticated az CLI:

# Load the base tool environment (optional, e.g. uv/mise).
use flake

# Fetch a Key Vault secret into an env var at direnv load time.
# Requires a logged-in 'az' CLI with access to the vault.
azure_kv_secret() {
  az keyvault secret show \
    --vault-name "$1" \
    --name "$2" \
    --query value \
    --output tsv
}

# Identity values live in a distinct vault from the application secrets.
# Env-first: only fetch when the variable is not already set, so CI
# (Crow from_secret -> plain env var) and existing exports win.
export AZURE_TENANT_ID="${AZURE_TENANT_ID:-$(azure_kv_secret <identity-vault-name> <tenant-id-secret-name>)}"
export AZURE_CLIENT_ID_CONTAINER="${AZURE_CLIENT_ID_CONTAINER:-$(azure_kv_secret <identity-vault-name> <client-id-secret-name>)}"
export AZURE_CLIENT_ID="${AZURE_CLIENT_ID:-$(azure_kv_secret <identity-vault-name> <device-code-client-id-secret-name>)}"

export AZURE_CLIENT_SECRET_CONTAINER_RAW_STAGING="${AZURE_CLIENT_SECRET_CONTAINER_RAW_STAGING:-$(azure_kv_secret <app-vault-name> <secret-name-staging>)}"
export AZURE_CLIENT_SECRET_CONTAINER_RAW_PROD="${AZURE_CLIENT_SECRET_CONTAINER_RAW_PROD:-$(azure_kv_secret <app-vault-name> <secret-name-prod>)}"

# SAS tokens for containers are read by the handler from env vars named
# AZURE_STORAGE_SAS_TOKEN_<CONTAINER> (dashes -> underscores) and can be
# populated the same way:
export AZURE_STORAGE_SAS_TOKEN_RAW_DATA="${AZURE_STORAGE_SAS_TOKEN_RAW_DATA:-$(azure_kv_secret <app-vault-name> <sas-secret-name>)}"

# ARM_SUBSCRIPTION_ID is not used by py-containerio; never hardcode it here.

Allow the file once with direnv allow. See examples/toml-direnv-demo/envrc.example for a full template that fetches several values in parallel, classifies failures, and skips variables that are already set.

Library usage

from containerio import resolve_container_config, StorageHandler

# Resolve a logical key for an environment.
# Returns (container_name, AzureStorageConfig); both None for env="local".
container, config = resolve_container_config(
    container_key="input1",
    env="staging",
)

handler = StorageHandler(container=container, config=config)

Errors mirror the R wrapper: unknown container key, missing auth for the selected env, unknown auth tag, missing container name, and a malformed name/azure_auth table all raise ValueError. A missing TOML file raises FileNotFoundError. An unset client_secret_env does not raise at resolve time: the secret resolves to None and only client-credentials auth reports it, when that method is actually used, so device-code login still works.

CLI usage

The io command consumes the combined TOML + direnv config directly. Pass --container-config (optionally with a path, default container-config.toml in the current directory) and select the container by its logical key. --env is required and selects the name.<env> / azure_auth.<env> entries:

# List blobs in the container mapped from logical key 'input1' for staging
uv run io ls --env staging --container input1 --container-config

# Explicit TOML path
uv run io ls --env prod --container input1 --container-config path/to/container-config.toml

# All other commands work the same way, e.g.
uv run io download data/file.csv --env prod --container output --container-config
uv run io auth --env staging --container input1 --container-config

With --container-config, the --container value is always interpreted as a logical key under [containers]. The io session command stores both the logical key and the TOML path, so a saved session keeps resolving through the TOML on later runs.

Note: When using --container-config, pass --env explicitly.

The dotenv preload of the default CLI flow is unaffected and keeps working; secrets reach the process through direnv or plain exports.

Authentication

Use io auth to manage authentication explicitly. Device-code login and status auto-discover the environment from your .env / .env.staging file, so --env is optional. SAS token and client credentials login require explicit --env:

# Device code login — auto-discovers env, verifies against a container
uv run io auth --env staging --container raw-data

# Check auth status across all methods
uv run io auth status

# Check status against a specific container (verifies methods end-to-end)
uv run io auth status --env staging --container raw-data

# SAS token login — requires --env and --container
uv run io auth login --method sas-token --env staging --container raw-data

# Client credentials — for CI/CD pipelines
uv run io auth login --method client-credentials --env prod --container raw-data

# Clear cached device code tokens
uv run io auth clear

# Force re-authentication (clears cache first)
uv run io auth login --env staging --container raw-data --force

The chosen auth method is saved to the session. Subsequent commands (ls, download, etc.) automatically use it.

Authentication Methods

  1. Device Code Flow (recommended for development)

    • Interactive browser-based authentication
    • Tokens are cached locally for 90 days
    • No credentials stored in files
  2. Client Credentials

    • For CI/CD pipelines
    • Use io auth login --method client-credentials --env <env> --container <name> to authenticate
    • Requires (in the env file for that environment):
      • AZURE_TENANT_ID: Service principal's tenant ID
      • AZURE_CLIENT_ID_CONTAINER: Service principal's client ID
      • AZURE_CLIENT_SECRET_CONTAINER: Service principal's client secret
  3. SAS Token

    • Use pre-generated tokens with limited validity
    • Good for sharing temporary access

CLI Tools

The package provides a unified io command with subcommands for all storage operations. All commands accept --env prod|staging (selects the env file) and --container <name> (selects the Azure container). --container-config resolves --container as a logical key from a container-config.toml (see TOML + direnv configuration).

Command Usage Description
auth uv run io auth [login|status|clear] [--method M] [--force] Manage authentication
ls uv run io ls --env <env> --container <name> List blobs in a container
download uv run io download <blob_name> --env <env> --container <name> Download a blob to local data/ directory
download-dir uv run io download-dir [<prefix>] [--local-dir <path>] --env <env> --container <name> Recursively download all blobs under a prefix to a local directory
upload uv run io upload <blob_name> <file_path> --env <env> --container <name> Upload a local file to a container
upload-dir uv run io upload-dir <local_dir> [--blob-prefix <prefix>] --env <env> --container <name> Upload all files from a local directory to storage
rm uv run io rm <path> --env <env> --container <name> Delete a blob or folder from a container
mv uv run io mv <source> <dest> [--overwrite] --env <env> --container <name> Move (rename) a blob within a container
generate-sas uv run io generate-sas --env <env> --container <name> [--days N] [--update-env] [--write] Generate an Azure Storage SAS token for a container (read+list by default; add --write for write+delete)

Examples

# Authenticate with device code flow against a container
uv run io auth --env staging --container raw-data

# Check authentication status
uv run io auth status

# List blobs from the local filesystem (./data/) — no --env / --container needed
uv run io ls

# List blobs in an Azure container
uv run io ls --env staging --container raw-data

# Download a blob to the local data/ directory
uv run io download path/to/file.csv --env staging --container raw-data

# Recursively download all blobs under a prefix to a local directory
uv run io download-dir archive/ --env staging --container raw-data
uv run io download-dir archive/ --local-dir ./downloads --env staging --container raw-data

# Recursively download the blobs of entire container (no prefix, from root)
uv run io download-dir --env staging --container raw-data

# Upload a local file
uv run io upload path/to/blob.csv ./local/file.csv --env staging --container curated

# Upload a local directory recursively to the container root directory
uv run io upload-dir ./local/data --env staging --container curated
# Upload a local directory recursively to a prefix in the container
uv run io upload-dir ./local/data --blob-prefix archive/2025 --env staging --container curated

# Delete a blob or folder
uv run io rm path/to/blob.csv --env staging --container curated
uv run io rm old-folder/ --env staging --container curated

# Move a blob within a container
uv run io mv data/old.csv archive/old.csv --env staging --container curated

# Overwrite an existing destination
uv run io mv data/old.csv data/existing.csv --env staging --container curated --overwrite

# Generate a read+list SAS token for a container and update the .env.staging file
uv run io generate-sas --env staging --container raw-data --update-env

# Generate a read+write+delete+list SAS token (opt-in via --write)
uv run io generate-sas --env staging --container curated --update-env --write

# Print a SAS token for a prod container valid for 3 days
uv run io generate-sas --env prod --container raw-data --days 3

Session

When doing multiple operations against the same container, use session to save defaults so you don't have to repeat --env and --container on every command. Both --env and --container are required to start a session.

# Start a session — saves env and container
uv run io session --env staging --container raw-data

# Show active session
uv run io session

# Now these use the saved defaults
uv run io ls
uv run io rm old-folder/
uv run io mv data/old.csv archive/old.csv

# Explicit flags still override the session
uv run io ls --container curated

# End the session
uv run io session clear

The session is stored in .containerio/session.json in the current directory. It is project-local and git-ignored.

The session is also available programmatically via the Session class:

from containerio.session import Session

session = Session()
print(session.auth_method)      # e.g. 'client-credentials'
print(session.env)              # e.g. 'staging'
print(session.container)        # e.g. 'raw-data'
print(session.container_config) # e.g. 'container-config.toml', or None
print(session.is_active)        # True if any session data exists

CLI options:

  • --env <name>: selects the environment. The CLI best-effort-preloads .env.<name> (no-op if missing) and uses the name as a label in prints, sessions, and SAS file write-back. Any name works (prod, staging, dev, eu-prod, …); local means the local filesystem.
  • --container: the Azure container name to target. With --container-config, this is a logical key under [containers] in the TOML file.
  • --container-config [path]: resolve --container as a logical key from a container-config.toml (default: container-config.toml in the current directory). Secrets come from the environment (e.g. via direnv). See TOML + direnv configuration.
  • --days: SAS token validity in days, max 7 (default: 1, generate-sas only)
  • --update-env: write SAS token to env file instead of printing (generate-sas only)
  • --write: opt in to write+delete permissions (generate-sas only). Default is read+list.

Note: SAS tokens are generated using a user delegation key which has a maximum validity of 7 days (Azure limitation). See Microsoft documentation for details.

The token's effective permissions are the intersection of the permissions requested here and the Azure RBAC permissions held by your identity (the user-delegation-key requester) at generation time. This means requesting --write only produces a write-capable token if your RBAC role allows it. See Microsoft documentation on user delegation SAS for details.

API Reference

StorageHandler

Main class for storage operations.

StorageHandler(
    container: Optional[str] = None,
    config: Optional[AzureStorageConfig] = None,
    auth_type: Optional[str] = None,
    session: Optional[Session] = None,
)
  • container: The container name (required for cloud storage, ignored for the local filesystem). Whether the container exists and what you can do with it is decided by Azure RBAC on your identity.
  • config=None: Local filesystem mode (uses ./data/ directory).
  • config=<AzureStorageConfig>: Cloud storage mode. Build via AzureStorageConfig(...) or AzureStorageConfig.from_env_file(path=...).
  • auth_type=None: Falls back to session auth method, then device code flow
  • auth_type="device-code": Device code flow (interactive). Uses config.device_code_client_id.
  • auth_type="sas-token": SAS token authentication (reads AZURE_STORAGE_SAS_TOKEN_<UPPER_CONTAINER_NAME>)
  • auth_type="client-credentials": Client credentials (service principal). Uses config.client_id and config.client_secret.
  • session=None: Creates a new Session (skipped for the local filesystem). Pass an existing Session instance for dependency injection.

Read Methods

Method Description
read_parquet_blob(blob_name) Read parquet file, returns (DataFrame, metadata)
read_csv_blob(blob_name, **kwargs) Read CSV file into DataFrame
read_excel_blob(blob_name, **kwargs) Read Excel file into DataFrame
read_json_blob(blob_name, **kwargs) Read JSON file into DataFrame
scan_parquet_blob(blob_name) Lazy scan parquet, returns (LazyFrame, metadata)
scan_csv_blob(blob_name, **kwargs) Lazy scan CSV file

Write Methods

Method Description
write_parquet_blob(blob_name, df) Write DataFrame to parquet
write_csv_blob(blob_name, df, **kwargs) Write DataFrame to CSV
write_excel_blob(blob_name, df, **kwargs) Write DataFrame to Excel
write_json_blob(blob_name, df, **kwargs) Write DataFrame to JSON
sink_parquet_blob(blob_name, lf) Stream LazyFrame to parquet

Utility Methods

Method Description
upload_blob(blob_name, file_path, progress_hook) Upload a local file to storage
delete_blob(blob_name) Delete a blob, file, or folder recursively. Returns List[str] of deleted paths
move_blob(source, destination, overwrite) Move (rename) a blob within the same container
download_blob(blob_name, progress_hook) Download blob to local data/ directory. Returns Optional[Path]
list_blobs() List all blobs, returns List[BlobInfo]

AzureStorageConfig

Configuration dataclass for Azure storage settings. Fields: storage_account, tenant_id (optional), dotenv_path (optional), client_id (optional), client_secret (optional), device_code_client_id (optional). dotenv_path is the file the config was loaded from; it is useful in logs and repr. storage_endpoint is a derived property — https://<storage_account>.blob.core.windows.net. The config is the single carrier of account and identity data: the handler reads these fields and never reads environment variables for them itself. from_env() is the one seam that maps env vars onto the config (AZURE_CLIENT_ID_CONTAINER → client_id, AZURE_CLIENT_SECRET_CONTAINER → client_secret, AZURE_CLIENT_ID → device_code_client_id). The lone exception is the SAS token, which is container-scoped and therefore resolved by the handler from AZURE_STORAGE_SAS_TOKEN_<NAME> at use time.

client_id and client_secret apply to client-credentials auth; device_code_client_id applies to device-code auth. Setting client_id/client_secret per config lets you drive multiple containers backed by different service principals in the same process:

raw = AzureStorageConfig(
    storage_account="myaccount",
    tenant_id="<tenant>",
    client_id="<raw_sp_client_id>",
    client_secret="<raw_sp_secret>",
)
curated = AzureStorageConfig(
    storage_account="myaccount",
    tenant_id="<tenant>",
    client_id="<curated_sp_client_id>",
    client_secret="<curated_sp_secret>",
)

raw_handler = StorageHandler(container="raw-data", config=raw, auth_type="client-credentials")
curated_handler = StorageHandler(container="curated", config=curated, auth_type="client-credentials")

The env name ("staging", "prod", …) is intentionally not a field on the config. That concept lives at the CLI and Session layer — see --env, Session.env, STORAGE_ENV. The config carries data, not an environment label.

Three ways to build a config:

from containerio import AzureStorageConfig

# 1. Read from os.environ (no file involved).
#    Vars must already be set (dotenv preload, direnv, exports, etc.).
config = AzureStorageConfig.from_env()

# 2. Load a specific dotenv file (any path, relative or absolute).
config = AzureStorageConfig.from_env_file(path=".env.staging")
config = AzureStorageConfig.from_env_file(path="/etc/myapp/staging.env")

# 3. Construct directly from strings (e.g. values loaded from a TOML file).
config = AzureStorageConfig(
    storage_account="myaccount",
    tenant_id="<tenant>",
)

# Access configuration
print(config.storage_account)   # "myaccount"
print(config.storage_endpoint)  # "https://myaccount.blob.core.windows.net"
print(config.dotenv_path)       # PosixPath('.env.staging') for option 2; None otherwise

Errors:

  • from_env() raises ValueError if AZURE_STORAGE_ACCOUNT is not in os.environ.
  • from_env_file(path) raises FileNotFoundError if the path does not exist, then ValueError if AZURE_STORAGE_ACCOUNT is still missing after loading.

Providing config to the CLI

The CLI reads required Azure variables from os.environ — by default it does not parse any specific config format itself, keeping it format-agnostic so each project can choose how to manage config. For the native combined TOML + direnv approach, pass --container-config (see TOML + direnv configuration); the rest of this section covers the default dotenv / env-var flow.

Dotenv is the default. If a .env.<name> file matching --env <name> exists, the CLI auto-loads it via python-dotenv before reading os.environ. No extra setup — drop a .env.staging next to your code and io ls --env staging works.

If you don't use dotenv, you populate os.environ yourself before invoking the CLI. Any of these work:

  • Wrapper script that reads your config format and exports vars (see TOML example below).
  • direnv that exports vars automatically on cd.
  • CI / container injection — the platform sets vars in the environment before the CLI runs.
  • Plain shell exports: export AZURE_STORAGE_ACCOUNT=... && io ls --env staging --container raw-data.

The CLI flow is the same in every case:

  1. main() tries load_dotenv(".env.<name>") — silently no-ops if the file isn't there.
  2. _build_config() calls AzureStorageConfig.from_env(), which reads whatever is now in os.environ.
  3. If no vars are set anywhere, from_env() raises a clear AZURE_STORAGE_ACCOUNT not set error.

Example: TOML config via a wrapper script

Your project has a containerio.toml (or any name) with per-env sections:

[staging]
storage_account = "myacct-staging"
tenant_id = "tid-staging"

[prod]
storage_account = "myacct-prod"
tenant_id = "tid-prod"

Add a small wrapper that reads the section selected by --env and populates os.environ, then hands off to the CLI:

# scripts/io.py
import argparse
import os
import tomllib

parser = argparse.ArgumentParser(add_help=False)
parser.add_argument("--env", default=None)
args, _ = parser.parse_known_args()

if args.env and args.env != "local":
    with open("containerio.toml", "rb") as toml_file:
        section = tomllib.load(toml_file)[args.env]
    os.environ["AZURE_STORAGE_ACCOUNT"] = section["storage_account"]
    os.environ["AZURE_TENANT_ID"] = section["tenant_id"]

from containerio.tools.cli import main

main()

Invoke it the same way you would the bare CLI:

# read containerio.toml's [staging] section, then run ls
uv run python scripts/io.py ls --env staging --container raw-data

The wrapper is the only TOML-aware piece. containerio itself stays format-agnostic. The same pattern works for YAML, JSON, Vault lookups, or anything else — read your source, set os.environ, call main().

Environment helpers

Small helpers for working with environment names.

from containerio.azure_config import env_file, is_local

is_local("local")      # True
is_local("prod")       # False
is_local(None)         # True (defaults to local)

env_file("prod")       # ".env"
env_file("staging")    # ".env.staging"
env_file("dev")        # ".env.dev"
env_file("local")      # raises ValueError

Development

# Clone and install
git clone ssh://git@codefloe.com/Alliance-SwissPass/py-containerio.git
cd py-containerio
uv sync --group dev

# Run tests
uv run pytest

# Lint and format
uv run ruff check
uv run ruff format

Version Management with git-sv

This project uses git-sv for semantic versioning and changelog generation.

The .gitsv/config.yaml file defines how versions are bumped based on commit messages.

  1. Commit messages: Follow conventional commits format
  2. Version bump: The CI pipeline automatically runs git-sv bump based on commit history
  3. Changelog: The CI pipeline generates the changelog using git-sv changelog and pins it as a Git issue
  4. Release: The CI pipeline automatically handles tagging and publishing

The CI pipeline will automatically:

  • Bump the version based on commit history
  • Generate and pin the changelog as a Git issue
  • Build and publish the package to Forgejo
  • Create a release with the changelog notes

Publishing

The repository includes a Crow CI pipeline that automatically builds, tests, and publishes the package when a git tag is pushed:

  1. Merge all branches you want to have part of a new release into main
  2. git sv rn: check tag for upcoming semantic release version and associated changelog.
  3. If not already added, add your private key (whose public key is associated with your Codefloe Git user) to the SSH agent. ssh-add ~/.ssh/<your-private-key>
  4. git sv tag: generate tag with version and push it to remote in one go, based on git commit parsing.

The pipeline will:

  • Run linting and tests
  • Update the version in pyproject.toml automatically
  • Generate changelog from git history using git-sv
  • Build and publish the package to Forgejo and PyPI
  • Create a release with changelog notes
  • Push version updates back to the repository

Note: For CI publishing, ensure the UV_PUBLISH_TOKEN secret is configured in your Crow CI pipeline settings.

Security Best Practices

  1. Never commit credentials: Always add .env* to your .gitignore file
  2. Restrict file permissions: Use chmod 600 .env to limit access to your credentials
  3. Use short-lived tokens: SAS tokens have a maximum validity of 7 days
  4. Rotate secrets regularly: Especially for production environments and CI/CD pipelines
  5. Use device code flow: For development to avoid storing credentials in files
  6. Environment isolation: Use separate credentials for staging and production
  7. Limit token scope: When generating SAS tokens, specify the minimum required permissions

References

License

Alliance SwissPass https://allianceswisspass.ch

Metadata

Release files for containerio 2.5.0

For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.

Source distribution (sdist)

Source distribution for containerio 2.5.0
File Size Uploaded Provenance
containerio-2.5.0.tar.gz 45.0 kB Without provenance Details

Built distribution (wheel)

Table of built distributions (wheels) for containerio 2.5.0
File Interpreter ABI Platform
containerio-2.5.0-py3-none-any.whl Python 3 none any Details

Total release size: 83.3 kB

Release files / containerio-2.5.0.tar.gz

Download URL containerio-2.5.0.tar.gz
Size 45.0 kB
Tags Source
SHA-256 checksum
How to use checksums
462eb0a859af17cdf2b816b68a7b8b0a9e867f841761ae50ee06fbbd8741f354
BLAKE2b-256 checksum
How to use checksums
aee94c5466f852ee0ff0af47719e304b0a76363fcc0e0b0034e78dbb71f6e8c7
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
No
Uploaded via uv/0.13.3 {"installer":{"name":"uv","version":"0.13.3","subcommand":["publish"]},"python":null,"implementation":{"name":null,"version":null},"distro":{"name":"Alpine Linux","version":"3.23.6","id":null,"libc":null},"system":{"name":null,"release":null},"cpu":null,"openssl_version":null,"setuptools_version":null,"rustc_version":null,"ci":true}

Release files / containerio-2.5.0-py3-none-any.whl

Download URL containerio-2.5.0-py3-none-any.whl
Size 38.2 kB
Tags Python 3
SHA-256 checksum
How to use checksums
00725acac8474f2a252105839e0590d36cd8703a628cdf6e2421ae46ff975d37
BLAKE2b-256 checksum
How to use checksums
fa0af00abdf75f38d540a25d0cac33fa7f4dd75778bc346e056e422c1a1b2822
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
No
Uploaded via uv/0.13.3 {"installer":{"name":"uv","version":"0.13.3","subcommand":["publish"]},"python":null,"implementation":{"name":null,"version":null},"distro":{"name":"Alpine Linux","version":"3.23.6","id":null,"libc":null},"system":{"name":null,"release":null},"cpu":null,"openssl_version":null,"setuptools_version":null,"rustc_version":null,"ci":true}

Security for containerio 2.5.0

Provenance

Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.

No provenance found

None of the files in this release were uploaded with provenance metadata.

A note on package security

PyPI is a repository for distributing packages, not a service for auditing them. While third-party security researchers and our staff do their best to detect and remove malicious packages, PyPI cannot guarantee the quality, security or suitability of any given package.

Anthropic, PBC Visionary sponsor Bloomberg Visionary sponsor Hudson River Trading Visionary sponsor Meta Visionary sponsor NVIDIA Visionary sponsor Microsoft Sustainability sponsor Depot Continuous Integration AWS Cloud computing and Security Sponsor Datadog Monitoring Fastly CDN Google Download Analytics Sentry Error logging StatusPage Status page