containerio 2.4.1__tar.gz → 2.5.0__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.3
2
2
  Name: containerio
3
- Version: 2.4.1
3
+ Version: 2.5.0
4
4
  Summary: Azure Blob Storage handler with unified local/cloud interface
5
5
  Author: Alliance SwissPass
6
6
  Requires-Dist: polars>=1.30.0
@@ -20,7 +20,7 @@ Provides a unified interface for reading and writing data to both local filesyst
20
20
  ## Features
21
21
 
22
22
  - **Unified API**: Same code works for local files and Azure Blob Storage
23
- - **Flexible configuration**: Load Azure config from a dotenv file, from existing environment variables, or directly from strings (e.g. values from a TOML file)
23
+ - **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](#toml--direnv-configuration))
24
24
  - **[Polars](https://pola.rs/) integration**: Native support for Polars DataFrames and LazyFrames
25
25
  - **Multiple auth methods**: Device code flow (default), client credentials (CI/CD), or SAS tokens
26
26
  - **CLI tools**: Unified `io` command for listing, downloading, uploading, deleting blobs and generating SAS tokens
@@ -81,7 +81,9 @@ containerio = { git = "https://codefloe.com/Alliance-SwissPass/py-containerio.gi
81
81
  When installed as a dependency, containerio integrates seamlessly with your project:
82
82
 
83
83
  - **CLI tools are available** - Run `uv run io` directly from your project
84
- - **Dotenv is the default convention** - The CLI best-effort-loads `.env.<name>` matching `--env <name>` from your current working directory. Other formats are supported via a small wrapper script — see [Providing config to the CLI](#providing-config-to-the-cli).
84
+ - **Dotenv is the default convention** - The CLI best-effort-loads `.env.<name>` matching `--env <name>` from your current working directory.
85
+ - **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](#toml--direnv-configuration).
86
+ - **Other formats via a small wrapper script** - See [Providing config to the CLI](#providing-config-to-the-cli).
85
87
  - **No configuration in containerio needed** - Just add the dependency and configure in your project
86
88
 
87
89
  Example workflow in your project:
@@ -218,7 +220,109 @@ AZURE_CLIENT_SECRET_CONTAINER=<container_app_client_secret>
218
220
  AZURE_STORAGE_SAS_TOKEN_RAW_DATA_STG=<sas_token_for_staging_container>
219
221
  ```
220
222
 
221
- > **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.
223
+ > **Note:** There is no registry of containers — you simply pass `--container <name>` (or `container="..."` in code) and the call is forwarded to Azure.
224
+ > Whether the container exists and what you can do with it is decided by Azure RBAC on your identity.
225
+
226
+ ### TOML + direnv configuration
227
+
228
+ 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()`):
229
+
230
+ - Non-sensitive fields (storage account, container names, and identity IDs) live in a project-local `container-config.toml`, which is safe to commit.
231
+ - 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.
232
+ - `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.
233
+ - 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.
234
+ 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.
235
+
236
+ Copy `container-config_example.toml` from this repository to `container-config.toml` and adapt it.
237
+ Each container maps an environment (`staging`, `prod`) to a concrete container name and an Azure auth configuration. Auth can be referenced in two ways:
238
+
239
+ - **Tag indirection** (shared auth across containers): `azure_auth.<env>` is a string referencing a shared `[azure_auth.<tag>]` section.
240
+ - **Inline nested auth** (per-container): `azure_auth.<env>` is a nested table defining the auth fields directly under `[containers.<key>.azure_auth.<env>]`.
241
+
242
+ If the resolved auth spec defines no `storage_account`, the full configuration is read from environment variables via `AzureStorageConfig.from_env()` instead.
243
+
244
+ #### Example `.envrc` (direnv)
245
+
246
+ The `.envrc` is committed; the secrets it pulls are not. Secrets are fetched at shell-entry time with the already-authenticated `az` CLI:
247
+
248
+ ```bash
249
+ # Load the base tool environment (optional, e.g. uv/mise).
250
+ use flake
251
+
252
+ # Fetch a Key Vault secret into an env var at direnv load time.
253
+ # Requires a logged-in 'az' CLI with access to the vault.
254
+ azure_kv_secret() {
255
+ az keyvault secret show \
256
+ --vault-name "$1" \
257
+ --name "$2" \
258
+ --query value \
259
+ --output tsv
260
+ }
261
+
262
+ # Identity values live in a distinct vault from the application secrets.
263
+ # Env-first: only fetch when the variable is not already set, so CI
264
+ # (Crow from_secret -> plain env var) and existing exports win.
265
+ export AZURE_TENANT_ID="${AZURE_TENANT_ID:-$(azure_kv_secret <identity-vault-name> <tenant-id-secret-name>)}"
266
+ export AZURE_CLIENT_ID_CONTAINER="${AZURE_CLIENT_ID_CONTAINER:-$(azure_kv_secret <identity-vault-name> <client-id-secret-name>)}"
267
+ export AZURE_CLIENT_ID="${AZURE_CLIENT_ID:-$(azure_kv_secret <identity-vault-name> <device-code-client-id-secret-name>)}"
268
+
269
+ export AZURE_CLIENT_SECRET_CONTAINER_RAW_STAGING="${AZURE_CLIENT_SECRET_CONTAINER_RAW_STAGING:-$(azure_kv_secret <app-vault-name> <secret-name-staging>)}"
270
+ export AZURE_CLIENT_SECRET_CONTAINER_RAW_PROD="${AZURE_CLIENT_SECRET_CONTAINER_RAW_PROD:-$(azure_kv_secret <app-vault-name> <secret-name-prod>)}"
271
+
272
+ # SAS tokens for containers are read by the handler from env vars named
273
+ # AZURE_STORAGE_SAS_TOKEN_<CONTAINER> (dashes -> underscores) and can be
274
+ # populated the same way:
275
+ export AZURE_STORAGE_SAS_TOKEN_RAW_DATA="${AZURE_STORAGE_SAS_TOKEN_RAW_DATA:-$(azure_kv_secret <app-vault-name> <sas-secret-name>)}"
276
+
277
+ # ARM_SUBSCRIPTION_ID is not used by py-containerio; never hardcode it here.
278
+ ```
279
+
280
+ Allow the file once with `direnv allow`.
281
+ 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.
282
+
283
+ #### Library usage
284
+
285
+ ```python
286
+ from containerio import resolve_container_config, StorageHandler
287
+
288
+ # Resolve a logical key for an environment.
289
+ # Returns (container_name, AzureStorageConfig); both None for env="local".
290
+ container, config = resolve_container_config(
291
+ container_key="input1",
292
+ env="staging",
293
+ )
294
+
295
+ handler = StorageHandler(container=container, config=config)
296
+ ```
297
+
298
+ 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`.
299
+ A missing TOML file raises `FileNotFoundError`.
300
+ 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.
301
+
302
+ #### CLI usage
303
+
304
+ The `io` command consumes the combined TOML + direnv config directly.
305
+ Pass `--container-config` (optionally with a path, default `container-config.toml` in the current directory) and select the container by its logical key.
306
+ `--env` is required and selects the `name.<env>` / `azure_auth.<env>` entries:
307
+
308
+ ```bash
309
+ # List blobs in the container mapped from logical key 'input1' for staging
310
+ uv run io ls --env staging --container input1 --container-config
311
+
312
+ # Explicit TOML path
313
+ uv run io ls --env prod --container input1 --container-config path/to/container-config.toml
314
+
315
+ # All other commands work the same way, e.g.
316
+ uv run io download data/file.csv --env prod --container output --container-config
317
+ uv run io auth --env staging --container input1 --container-config
318
+ ```
319
+
320
+ With `--container-config`, the `--container` value is always interpreted as a logical key under `[containers]`.
321
+ 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.
322
+
323
+ > **Note:** When using `--container-config`, pass `--env` explicitly.
324
+ >
325
+ > The dotenv preload of the default CLI flow is unaffected and keeps working; secrets reach the process through direnv or plain exports.
222
326
 
223
327
  ### Authentication
224
328
 
@@ -272,7 +376,9 @@ The chosen auth method is saved to the session. Subsequent commands (`ls`, `down
272
376
 
273
377
  ## CLI Tools
274
378
 
275
- 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).
379
+ The package provides a unified `io` command with subcommands for all storage operations.
380
+ All commands accept `--env prod|staging` (selects the env file) and `--container <name>` (selects the Azure container).
381
+ `--container-config` resolves `--container` as a logical key from a `container-config.toml` (see [TOML + direnv configuration](#toml--direnv-configuration)).
276
382
 
277
383
  | Command | Usage | Description |
278
384
  |---------|-------|-------------|
@@ -371,23 +477,29 @@ The session is also available programmatically via the `Session` class:
371
477
  from containerio.session import Session
372
478
 
373
479
  session = Session()
374
- print(session.auth_method) # e.g. 'client-credentials'
375
- print(session.env) # e.g. 'staging'
376
- print(session.container) # e.g. 'raw-data'
377
- print(session.is_active) # True if any session data exists
480
+ print(session.auth_method) # e.g. 'client-credentials'
481
+ print(session.env) # e.g. 'staging'
482
+ print(session.container) # e.g. 'raw-data'
483
+ print(session.container_config) # e.g. 'container-config.toml', or None
484
+ print(session.is_active) # True if any session data exists
378
485
  ```
379
486
 
380
487
  CLI options:
381
488
 
382
- - `--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.
383
- - `--container`: the Azure container name to target
489
+ - `--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.
490
+ Any name works (`prod`, `staging`, `dev`, `eu-prod`, …); `local` means the local filesystem.
491
+ - `--container`: the Azure container name to target. With `--container-config`, this is a logical key under `[containers]` in the TOML file.
492
+ - `--container-config [path]`: resolve `--container` as a logical key from a `container-config.toml` (default: `container-config.toml` in the current directory).
493
+ Secrets come from the environment (e.g. via direnv). See [TOML + direnv configuration](#toml--direnv-configuration).
384
494
  - `--days`: SAS token validity in days, max 7 (default: 1, `generate-sas` only)
385
495
  - `--update-env`: write SAS token to env file instead of printing (`generate-sas` only)
386
496
  - `--write`: opt in to write+delete permissions (`generate-sas` only). Default is read+list.
387
497
 
388
498
  **Note:** SAS tokens are generated using a *user delegation key* which has a maximum validity of **7 days** (Azure limitation). See [Microsoft documentation](https://learn.microsoft.com/en-us/rest/api/storageservices/get-user-delegation-key#request-body) for details.
389
499
 
390
- 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 — so requesting `--write` only produces a write-capable token if your RBAC role allows it. See [Microsoft documentation on user delegation SAS](https://learn.microsoft.com/en-us/rest/api/storageservices/create-user-delegation-sas#permissions-for-a-user-delegation-sas) for details.
500
+ 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.
501
+ This means requesting `--write` only produces a write-capable token if your RBAC role allows it.
502
+ See [Microsoft documentation on user delegation SAS](https://learn.microsoft.com/en-us/rest/api/storageservices/create-user-delegation-sas#permissions-for-a-user-delegation-sas) for details.
391
503
 
392
504
  ## API Reference
393
505
 
@@ -447,9 +559,9 @@ StorageHandler(
447
559
  ### AzureStorageConfig
448
560
 
449
561
  Configuration dataclass for Azure storage settings.
450
- Fields: `storage_account`, `tenant_id` (optional), `dotenv_path` (optional — the file the config was loaded from; useful in logs and `repr`), `client_id` (optional), `client_secret` (optional) and `device_code_client_id` (optional).
562
+ Fields: `storage_account`, `tenant_id` (optional), `dotenv_path` (optional), `client_id` (optional), `client_secret` (optional), `device_code_client_id` (optional).
563
+ `dotenv_path` is the file the config was loaded from; it is useful in logs and `repr`.
451
564
  `storage_endpoint` is a derived property — `https://<storage_account>.blob.core.windows.net`.
452
-
453
565
  The config is the single carrier of account and identity data: the handler reads these fields and never reads environment variables for them itself.
454
566
  `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`).
455
567
  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.
@@ -511,8 +623,8 @@ Errors:
511
623
 
512
624
  ### Providing config to the CLI
513
625
 
514
- The CLI reads required Azure variables from `os.environ` — it does not parse any specific config format itself.
515
- This keeps the CLI format-agnostic and lets each project choose how to manage config.
626
+ 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.
627
+ For the native combined TOML + direnv approach, pass `--container-config` (see [TOML + direnv configuration](#toml--direnv-configuration)); the rest of this section covers the default dotenv / env-var flow.
516
628
 
517
629
  **Dotenv is the default.**
518
630
  If a `.env.<name>` file matching `--env <name>` exists, the CLI auto-loads it via `python-dotenv` before reading `os.environ`.
@@ -625,6 +737,7 @@ The `.gitsv/config.yaml` file defines how versions are bumped based on commit me
625
737
  4. **Release**: The CI pipeline automatically handles tagging and publishing
626
738
 
627
739
  The CI pipeline will automatically:
740
+
628
741
  - Bump the version based on commit history
629
742
  - Generate and pin the changelog as a Git issue
630
743
  - Build and publish the package to Forgejo
@@ -643,6 +756,7 @@ The repository includes a Crow CI pipeline that automatically builds, tests, and
643
756
  1. `git sv tag`: generate tag with version and push it to remote in one go, based on git commit parsing.
644
757
 
645
758
  The pipeline will:
759
+
646
760
  - Run linting and tests
647
761
  - Update the version in `pyproject.toml` automatically
648
762
  - Generate changelog from git history using `git-sv`
@@ -6,7 +6,7 @@ Provides a unified interface for reading and writing data to both local filesyst
6
6
  ## Features
7
7
 
8
8
  - **Unified API**: Same code works for local files and Azure Blob Storage
9
- - **Flexible configuration**: Load Azure config from a dotenv file, from existing environment variables, or directly from strings (e.g. values from a TOML file)
9
+ - **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](#toml--direnv-configuration))
10
10
  - **[Polars](https://pola.rs/) integration**: Native support for Polars DataFrames and LazyFrames
11
11
  - **Multiple auth methods**: Device code flow (default), client credentials (CI/CD), or SAS tokens
12
12
  - **CLI tools**: Unified `io` command for listing, downloading, uploading, deleting blobs and generating SAS tokens
@@ -67,7 +67,9 @@ containerio = { git = "https://codefloe.com/Alliance-SwissPass/py-containerio.gi
67
67
  When installed as a dependency, containerio integrates seamlessly with your project:
68
68
 
69
69
  - **CLI tools are available** - Run `uv run io` directly from your project
70
- - **Dotenv is the default convention** - The CLI best-effort-loads `.env.<name>` matching `--env <name>` from your current working directory. Other formats are supported via a small wrapper script — see [Providing config to the CLI](#providing-config-to-the-cli).
70
+ - **Dotenv is the default convention** - The CLI best-effort-loads `.env.<name>` matching `--env <name>` from your current working directory.
71
+ - **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](#toml--direnv-configuration).
72
+ - **Other formats via a small wrapper script** - See [Providing config to the CLI](#providing-config-to-the-cli).
71
73
  - **No configuration in containerio needed** - Just add the dependency and configure in your project
72
74
 
73
75
  Example workflow in your project:
@@ -204,7 +206,109 @@ AZURE_CLIENT_SECRET_CONTAINER=<container_app_client_secret>
204
206
  AZURE_STORAGE_SAS_TOKEN_RAW_DATA_STG=<sas_token_for_staging_container>
205
207
  ```
206
208
 
207
- > **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.
209
+ > **Note:** There is no registry of containers — you simply pass `--container <name>` (or `container="..."` in code) and the call is forwarded to Azure.
210
+ > Whether the container exists and what you can do with it is decided by Azure RBAC on your identity.
211
+
212
+ ### TOML + direnv configuration
213
+
214
+ 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()`):
215
+
216
+ - Non-sensitive fields (storage account, container names, and identity IDs) live in a project-local `container-config.toml`, which is safe to commit.
217
+ - 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.
218
+ - `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.
219
+ - 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.
220
+ 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.
221
+
222
+ Copy `container-config_example.toml` from this repository to `container-config.toml` and adapt it.
223
+ Each container maps an environment (`staging`, `prod`) to a concrete container name and an Azure auth configuration. Auth can be referenced in two ways:
224
+
225
+ - **Tag indirection** (shared auth across containers): `azure_auth.<env>` is a string referencing a shared `[azure_auth.<tag>]` section.
226
+ - **Inline nested auth** (per-container): `azure_auth.<env>` is a nested table defining the auth fields directly under `[containers.<key>.azure_auth.<env>]`.
227
+
228
+ If the resolved auth spec defines no `storage_account`, the full configuration is read from environment variables via `AzureStorageConfig.from_env()` instead.
229
+
230
+ #### Example `.envrc` (direnv)
231
+
232
+ The `.envrc` is committed; the secrets it pulls are not. Secrets are fetched at shell-entry time with the already-authenticated `az` CLI:
233
+
234
+ ```bash
235
+ # Load the base tool environment (optional, e.g. uv/mise).
236
+ use flake
237
+
238
+ # Fetch a Key Vault secret into an env var at direnv load time.
239
+ # Requires a logged-in 'az' CLI with access to the vault.
240
+ azure_kv_secret() {
241
+ az keyvault secret show \
242
+ --vault-name "$1" \
243
+ --name "$2" \
244
+ --query value \
245
+ --output tsv
246
+ }
247
+
248
+ # Identity values live in a distinct vault from the application secrets.
249
+ # Env-first: only fetch when the variable is not already set, so CI
250
+ # (Crow from_secret -> plain env var) and existing exports win.
251
+ export AZURE_TENANT_ID="${AZURE_TENANT_ID:-$(azure_kv_secret <identity-vault-name> <tenant-id-secret-name>)}"
252
+ export AZURE_CLIENT_ID_CONTAINER="${AZURE_CLIENT_ID_CONTAINER:-$(azure_kv_secret <identity-vault-name> <client-id-secret-name>)}"
253
+ export AZURE_CLIENT_ID="${AZURE_CLIENT_ID:-$(azure_kv_secret <identity-vault-name> <device-code-client-id-secret-name>)}"
254
+
255
+ export AZURE_CLIENT_SECRET_CONTAINER_RAW_STAGING="${AZURE_CLIENT_SECRET_CONTAINER_RAW_STAGING:-$(azure_kv_secret <app-vault-name> <secret-name-staging>)}"
256
+ export AZURE_CLIENT_SECRET_CONTAINER_RAW_PROD="${AZURE_CLIENT_SECRET_CONTAINER_RAW_PROD:-$(azure_kv_secret <app-vault-name> <secret-name-prod>)}"
257
+
258
+ # SAS tokens for containers are read by the handler from env vars named
259
+ # AZURE_STORAGE_SAS_TOKEN_<CONTAINER> (dashes -> underscores) and can be
260
+ # populated the same way:
261
+ export AZURE_STORAGE_SAS_TOKEN_RAW_DATA="${AZURE_STORAGE_SAS_TOKEN_RAW_DATA:-$(azure_kv_secret <app-vault-name> <sas-secret-name>)}"
262
+
263
+ # ARM_SUBSCRIPTION_ID is not used by py-containerio; never hardcode it here.
264
+ ```
265
+
266
+ Allow the file once with `direnv allow`.
267
+ 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.
268
+
269
+ #### Library usage
270
+
271
+ ```python
272
+ from containerio import resolve_container_config, StorageHandler
273
+
274
+ # Resolve a logical key for an environment.
275
+ # Returns (container_name, AzureStorageConfig); both None for env="local".
276
+ container, config = resolve_container_config(
277
+ container_key="input1",
278
+ env="staging",
279
+ )
280
+
281
+ handler = StorageHandler(container=container, config=config)
282
+ ```
283
+
284
+ 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`.
285
+ A missing TOML file raises `FileNotFoundError`.
286
+ 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.
287
+
288
+ #### CLI usage
289
+
290
+ The `io` command consumes the combined TOML + direnv config directly.
291
+ Pass `--container-config` (optionally with a path, default `container-config.toml` in the current directory) and select the container by its logical key.
292
+ `--env` is required and selects the `name.<env>` / `azure_auth.<env>` entries:
293
+
294
+ ```bash
295
+ # List blobs in the container mapped from logical key 'input1' for staging
296
+ uv run io ls --env staging --container input1 --container-config
297
+
298
+ # Explicit TOML path
299
+ uv run io ls --env prod --container input1 --container-config path/to/container-config.toml
300
+
301
+ # All other commands work the same way, e.g.
302
+ uv run io download data/file.csv --env prod --container output --container-config
303
+ uv run io auth --env staging --container input1 --container-config
304
+ ```
305
+
306
+ With `--container-config`, the `--container` value is always interpreted as a logical key under `[containers]`.
307
+ 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.
308
+
309
+ > **Note:** When using `--container-config`, pass `--env` explicitly.
310
+ >
311
+ > The dotenv preload of the default CLI flow is unaffected and keeps working; secrets reach the process through direnv or plain exports.
208
312
 
209
313
  ### Authentication
210
314
 
@@ -258,7 +362,9 @@ The chosen auth method is saved to the session. Subsequent commands (`ls`, `down
258
362
 
259
363
  ## CLI Tools
260
364
 
261
- 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).
365
+ The package provides a unified `io` command with subcommands for all storage operations.
366
+ All commands accept `--env prod|staging` (selects the env file) and `--container <name>` (selects the Azure container).
367
+ `--container-config` resolves `--container` as a logical key from a `container-config.toml` (see [TOML + direnv configuration](#toml--direnv-configuration)).
262
368
 
263
369
  | Command | Usage | Description |
264
370
  |---------|-------|-------------|
@@ -357,23 +463,29 @@ The session is also available programmatically via the `Session` class:
357
463
  from containerio.session import Session
358
464
 
359
465
  session = Session()
360
- print(session.auth_method) # e.g. 'client-credentials'
361
- print(session.env) # e.g. 'staging'
362
- print(session.container) # e.g. 'raw-data'
363
- print(session.is_active) # True if any session data exists
466
+ print(session.auth_method) # e.g. 'client-credentials'
467
+ print(session.env) # e.g. 'staging'
468
+ print(session.container) # e.g. 'raw-data'
469
+ print(session.container_config) # e.g. 'container-config.toml', or None
470
+ print(session.is_active) # True if any session data exists
364
471
  ```
365
472
 
366
473
  CLI options:
367
474
 
368
- - `--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.
369
- - `--container`: the Azure container name to target
475
+ - `--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.
476
+ Any name works (`prod`, `staging`, `dev`, `eu-prod`, …); `local` means the local filesystem.
477
+ - `--container`: the Azure container name to target. With `--container-config`, this is a logical key under `[containers]` in the TOML file.
478
+ - `--container-config [path]`: resolve `--container` as a logical key from a `container-config.toml` (default: `container-config.toml` in the current directory).
479
+ Secrets come from the environment (e.g. via direnv). See [TOML + direnv configuration](#toml--direnv-configuration).
370
480
  - `--days`: SAS token validity in days, max 7 (default: 1, `generate-sas` only)
371
481
  - `--update-env`: write SAS token to env file instead of printing (`generate-sas` only)
372
482
  - `--write`: opt in to write+delete permissions (`generate-sas` only). Default is read+list.
373
483
 
374
484
  **Note:** SAS tokens are generated using a *user delegation key* which has a maximum validity of **7 days** (Azure limitation). See [Microsoft documentation](https://learn.microsoft.com/en-us/rest/api/storageservices/get-user-delegation-key#request-body) for details.
375
485
 
376
- 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 — so requesting `--write` only produces a write-capable token if your RBAC role allows it. See [Microsoft documentation on user delegation SAS](https://learn.microsoft.com/en-us/rest/api/storageservices/create-user-delegation-sas#permissions-for-a-user-delegation-sas) for details.
486
+ 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.
487
+ This means requesting `--write` only produces a write-capable token if your RBAC role allows it.
488
+ See [Microsoft documentation on user delegation SAS](https://learn.microsoft.com/en-us/rest/api/storageservices/create-user-delegation-sas#permissions-for-a-user-delegation-sas) for details.
377
489
 
378
490
  ## API Reference
379
491
 
@@ -433,9 +545,9 @@ StorageHandler(
433
545
  ### AzureStorageConfig
434
546
 
435
547
  Configuration dataclass for Azure storage settings.
436
- Fields: `storage_account`, `tenant_id` (optional), `dotenv_path` (optional — the file the config was loaded from; useful in logs and `repr`), `client_id` (optional), `client_secret` (optional) and `device_code_client_id` (optional).
548
+ Fields: `storage_account`, `tenant_id` (optional), `dotenv_path` (optional), `client_id` (optional), `client_secret` (optional), `device_code_client_id` (optional).
549
+ `dotenv_path` is the file the config was loaded from; it is useful in logs and `repr`.
437
550
  `storage_endpoint` is a derived property — `https://<storage_account>.blob.core.windows.net`.
438
-
439
551
  The config is the single carrier of account and identity data: the handler reads these fields and never reads environment variables for them itself.
440
552
  `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`).
441
553
  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.
@@ -497,8 +609,8 @@ Errors:
497
609
 
498
610
  ### Providing config to the CLI
499
611
 
500
- The CLI reads required Azure variables from `os.environ` — it does not parse any specific config format itself.
501
- This keeps the CLI format-agnostic and lets each project choose how to manage config.
612
+ 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.
613
+ For the native combined TOML + direnv approach, pass `--container-config` (see [TOML + direnv configuration](#toml--direnv-configuration)); the rest of this section covers the default dotenv / env-var flow.
502
614
 
503
615
  **Dotenv is the default.**
504
616
  If a `.env.<name>` file matching `--env <name>` exists, the CLI auto-loads it via `python-dotenv` before reading `os.environ`.
@@ -611,6 +723,7 @@ The `.gitsv/config.yaml` file defines how versions are bumped based on commit me
611
723
  4. **Release**: The CI pipeline automatically handles tagging and publishing
612
724
 
613
725
  The CI pipeline will automatically:
726
+
614
727
  - Bump the version based on commit history
615
728
  - Generate and pin the changelog as a Git issue
616
729
  - Build and publish the package to Forgejo
@@ -629,6 +742,7 @@ The repository includes a Crow CI pipeline that automatically builds, tests, and
629
742
  1. `git sv tag`: generate tag with version and push it to remote in one go, based on git commit parsing.
630
743
 
631
744
  The pipeline will:
745
+
632
746
  - Run linting and tests
633
747
  - Update the version in `pyproject.toml` automatically
634
748
  - Generate changelog from git history using `git-sv`
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "containerio"
3
- version = "2.4.1"
3
+ version = "2.5.0"
4
4
  description = "Azure Blob Storage handler with unified local/cloud interface"
5
5
  readme = "README.md"
6
6
  requires-python = ">=3.11"
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "containerio"
3
- version = "2.4.1"
3
+ version = "2.5.0"
4
4
  description = "Azure Blob Storage handler with unified local/cloud interface"
5
5
  readme = "README.md"
6
6
  requires-python = ">=3.11"
@@ -1,4 +1,5 @@
1
1
  from containerio.azure_config import AzureStorageConfig
2
+ from containerio.config import resolve_container_config
2
3
  from containerio.handler import AzureAuthType, BlobInfo, StorageHandler
3
4
 
4
5
  __all__ = [
@@ -6,4 +7,5 @@ __all__ = [
6
7
  "AzureStorageConfig",
7
8
  "BlobInfo",
8
9
  "StorageHandler",
10
+ "resolve_container_config",
9
11
  ]
@@ -0,0 +1,209 @@
1
+ """Resolve container configuration from a TOML file + environment.
2
+
3
+ This module implements the Python-native equivalent of r-containerio's
4
+ `containerio_config()`: a project-local `container-config.toml` maps
5
+ logical container keys and environments (`staging`, `prod`) to concrete
6
+ Azure container names plus auth specs.
7
+
8
+ Non-sensitive fields (storage account, container names) come from the
9
+ TOML. Identity values (`tenant_id`, `client_id`,
10
+ `device_code_client_id`) and the client secret are resolved
11
+ environment-first: their `*_env` keys name an environment variable that
12
+ wins when it is set at runtime, and the TOML value is only the
13
+ fallback. That way a CI-injected value (Crow `from_secret` becomes a
14
+ plain environment variable) or an existing shell export is never
15
+ overwritten by the local direnv fetcher. The client secret has no TOML
16
+ fallback: it is resolved from `os.environ` by name only. Combined with
17
+ direnv (a commit-safe `.envrc` that pulls values at runtime, e.g. from
18
+ Azure Key Vault via the authenticated `az` CLI), the TOML file is safe
19
+ to commit.
20
+
21
+ If the resolved auth spec defines no `storage_account`, the module
22
+ falls back to `AzureStorageConfig.from_env()`, which reads the full
23
+ configuration from environment variables.
24
+ """
25
+
26
+ import os
27
+ import tomllib
28
+ from pathlib import Path
29
+ from typing import Optional, Tuple, Union
30
+
31
+ from containerio.azure_config import AzureStorageConfig, is_local
32
+
33
+ DEFAULT_CONTAINER_CONFIG_FILE = "container-config.toml"
34
+
35
+
36
+ def _resolve_env_or_direct(
37
+ auth: dict,
38
+ value_key: str,
39
+ env_key: str,
40
+ ) -> Optional[str]:
41
+ """Resolve an auth value environment-first, else TOML.
42
+
43
+ The environment variable named by `env_key` wins when it is set at
44
+ runtime; otherwise the direct `value_key` from the TOML is used.
45
+ This lets a CI-injected or pre-exported value take precedence over
46
+ the local direnv fallback.
47
+
48
+ Args:
49
+ auth (dict): The resolved auth table.
50
+ value_key (str): TOML key holding the direct fallback value.
51
+ env_key (str): TOML key naming the environment variable.
52
+
53
+ Returns:
54
+ Optional[str]: The resolved value, or `None` when neither the
55
+ named environment variable nor the direct value is set.
56
+ """
57
+ env_var = auth.get(env_key)
58
+
59
+ if env_var:
60
+ value = os.getenv(env_var)
61
+
62
+ if value:
63
+ return value
64
+
65
+ return auth.get(value_key)
66
+
67
+
68
+ def resolve_container_config(
69
+ container_key: str,
70
+ env: str,
71
+ toml_file: Union[str, Path] = DEFAULT_CONTAINER_CONFIG_FILE,
72
+ ) -> Tuple[Optional[str], Optional[AzureStorageConfig]]:
73
+ """Resolve a logical container key to a name + Azure config.
74
+
75
+ The config is resolved from a TOML file plus the environment.
76
+
77
+ Looks up the container definition under `[containers]` in the TOML
78
+ file and maps it to an Azure auth configuration for the selected
79
+ `env`. Auth can be referenced in two ways:
80
+
81
+ - **Tag indirection** (shared auth across containers):
82
+ `azure_auth.<env>` is a string referencing a
83
+ `[azure_auth.<tag>]` section.
84
+ - **Inline nested auth** (per-container): `azure_auth.<env>` is a
85
+ nested table defining the auth fields directly under
86
+ `[containers.<key>.azure_auth.<env>]`.
87
+
88
+ `tenant_id_env`, `client_id_env`, and `device_code_client_id_env`
89
+ name the environment variables that hold those identity values. An
90
+ environment variable that is set at runtime wins; otherwise the
91
+ direct `tenant_id`, `client_id`, or `device_code_client_id` value
92
+ from the TOML is used. This keeps CI-provided or pre-exported
93
+ values authoritative over the local direnv fallback.
94
+
95
+ `client_secret_env` is the name of the environment variable that
96
+ holds the client secret; it is resolved from `os.environ` at load
97
+ time so no secret is stored in the TOML. An unset variable yields
98
+ `None` rather than an error: only client-credentials auth needs the
99
+ secret, and the storage handler reports the missing value when that
100
+ method is actually used. This keeps device-code login working when
101
+ only `device_code_client_id` is available. SAS tokens are not part
102
+ of this file: the storage handler resolves them from the
103
+ environment via `sas_token_env_var()` when connecting.
104
+
105
+ If the resolved auth spec defines no `storage_account`, the full
106
+ configuration is read from environment variables via
107
+ `AzureStorageConfig.from_env()` instead.
108
+
109
+ Args:
110
+ container_key (str): Logical container key under `[containers]`.
111
+ env (str): Environment name (e.g. 'staging', 'prod'). 'local'
112
+ short-circuits to the local filesystem.
113
+ toml_file (Union[str, Path]): Path to the TOML config file.
114
+ Defaults to `container-config.toml` in the current working
115
+ directory.
116
+
117
+ Returns:
118
+ Tuple[Optional[str], Optional[AzureStorageConfig]]: The resolved
119
+ concrete container name and an `AzureStorageConfig` suitable
120
+ for `StorageHandler`. Both are `None` for the local
121
+ filesystem (`env='local'`).
122
+
123
+ Raises:
124
+ FileNotFoundError: If `toml_file` does not exist.
125
+ ValueError: If the container key is unknown, the TOML is
126
+ missing the auth or name for the selected env, `name` or
127
+ `azure_auth` is not a table keyed by environment, or the
128
+ referenced auth tag does not exist.
129
+ """
130
+ if is_local(env):
131
+ return None, None
132
+
133
+ path = Path(toml_file)
134
+
135
+ if not path.is_file():
136
+ raise FileNotFoundError(f"container config file not found: {path}")
137
+
138
+ with path.open("rb") as handle:
139
+ cfg = tomllib.load(handle)
140
+
141
+ containers = cfg.get("containers")
142
+ container = containers.get(container_key) if isinstance(containers, dict) else None
143
+
144
+ if container is None:
145
+ raise ValueError(f"Unknown container: {container_key}")
146
+
147
+ name_map = container.get("name") or {}
148
+
149
+ if not isinstance(name_map, dict):
150
+ raise ValueError(
151
+ f"Invalid 'name' for container '{container_key}': expected a "
152
+ f'table keyed by environment (e.g. name.{env} = "..."), '
153
+ f"got {type(name_map).__name__}."
154
+ )
155
+
156
+ container_name = name_map.get(env)
157
+
158
+ if not container_name:
159
+ raise ValueError(
160
+ f"No container name defined for env '{env}' in container '{container_key}'"
161
+ )
162
+
163
+ auth_map = container.get("azure_auth") or {}
164
+
165
+ if not isinstance(auth_map, dict):
166
+ raise ValueError(
167
+ f"Invalid 'azure_auth' for container '{container_key}': expected "
168
+ f"a table keyed by environment (e.g. azure_auth.{env} = ...), "
169
+ f"got {type(auth_map).__name__}."
170
+ )
171
+
172
+ auth_spec = auth_map.get(env)
173
+
174
+ if auth_spec is None:
175
+ raise ValueError(
176
+ f"Container '{container_key}' has no azure_auth defined for env '{env}'."
177
+ )
178
+
179
+ # `azure_auth.<env>` may be either:
180
+ # - a string referencing a shared `[azure_auth.<tag>]` section, or
181
+ # - an inline (nested) table defining the auth fields directly
182
+ # under `[containers.<key>.azure_auth.<env>]`.
183
+ if isinstance(auth_spec, dict):
184
+ auth = auth_spec
185
+ else:
186
+ tag = auth_spec
187
+ shared = cfg.get("azure_auth") or {}
188
+ auth = shared.get(tag) if isinstance(shared, dict) else None
189
+
190
+ if auth is None:
191
+ raise ValueError(f"Unknown azure_auth: {tag}")
192
+
193
+ if auth.get("storage_account"):
194
+ client_secret_env = auth.get("client_secret_env")
195
+ client_secret = os.getenv(client_secret_env) if client_secret_env else None
196
+
197
+ config = AzureStorageConfig(
198
+ storage_account=auth["storage_account"],
199
+ tenant_id=_resolve_env_or_direct(auth, "tenant_id", "tenant_id_env"),
200
+ client_id=_resolve_env_or_direct(auth, "client_id", "client_id_env"),
201
+ client_secret=client_secret,
202
+ device_code_client_id=_resolve_env_or_direct(
203
+ auth, "device_code_client_id", "device_code_client_id_env"
204
+ ),
205
+ )
206
+ else:
207
+ config = AzureStorageConfig.from_env()
208
+
209
+ return container_name, config
@@ -33,6 +33,15 @@ class Session:
33
33
  """The Azure container name (e.g. 'raw-data')."""
34
34
  return self._data.get("container")
35
35
 
36
+ @property
37
+ def container_config(self) -> Optional[str]:
38
+ """The container-config TOML path saved with the session.
39
+
40
+ When set, `container` holds a logical key that is resolved
41
+ through this TOML file on later runs.
42
+ """
43
+ return self._data.get("container_config")
44
+
36
45
  @property
37
46
  def is_active(self) -> bool:
38
47
  """Whether a session has any data."""
@@ -43,6 +52,8 @@ class Session:
43
52
  auth_method: Optional[str] = None,
44
53
  env: Optional[str] = None,
45
54
  container: Optional[str] = None,
55
+ container_config: Optional[str] = None,
56
+ clear_container_config: bool = False,
46
57
  ) -> None:
47
58
  """Update session fields and persist to disk.
48
59
 
@@ -50,6 +61,11 @@ class Session:
50
61
  auth_method (Optional[str]): Azure auth method.
51
62
  env (Optional[str]): Storage environment.
52
63
  container (Optional[str]): Azure container name.
64
+ container_config (Optional[str]): Path to the TOML file used
65
+ to resolve `container` as a logical key.
66
+ clear_container_config (bool): Remove any stored
67
+ container-config path. Takes precedence over
68
+ `container_config`.
53
69
  """
54
70
  if auth_method is not None:
55
71
  self._data["auth_method"] = auth_method
@@ -60,6 +76,11 @@ class Session:
60
76
  if container is not None:
61
77
  self._data["container"] = container
62
78
 
79
+ if clear_container_config:
80
+ self._data.pop("container_config", None)
81
+ elif container_config is not None:
82
+ self._data["container_config"] = container_config
83
+
63
84
  self._save()
64
85
 
65
86
  def clear(self) -> None:
@@ -29,6 +29,10 @@ from containerio import (
29
29
  StorageHandler,
30
30
  )
31
31
  from containerio.azure_config import env_file, is_local, sas_token_env_var
32
+ from containerio.config import (
33
+ DEFAULT_CONTAINER_CONFIG_FILE,
34
+ resolve_container_config,
35
+ )
32
36
  from containerio.session import Session
33
37
 
34
38
  MAX_SAS_DAYS_VALID = 7
@@ -86,6 +90,54 @@ def _require_container(args: argparse.Namespace) -> None:
86
90
  )
87
91
 
88
92
 
93
+ def _apply_container_config(args: argparse.Namespace) -> None:
94
+ """Resolve --container-config selection onto args, in place.
95
+
96
+ When `--container-config` is set and a cloud env is selected,
97
+ `--container` is interpreted as a logical key under `[containers]`
98
+ in the TOML file. It is replaced by the resolved concrete container
99
+ name and the resolved `AzureStorageConfig` is stashed on the
100
+ namespace as `_toml_config` for `_build_config` to pick up.
101
+
102
+ The 'session' command is skipped so logical keys — not concrete
103
+ names — persist in the session. `_cmd_session` additionally stores
104
+ the TOML path, and `_apply_session_defaults` restores it on later
105
+ runs so the key keeps resolving.
106
+
107
+ Args:
108
+ args (argparse.Namespace): Parsed CLI arguments.
109
+ """
110
+ toml_file = getattr(args, "container_config", None)
111
+
112
+ if not toml_file or args.command == "session":
113
+ return
114
+
115
+ if not args.env:
116
+ _fail(
117
+ "Error: --env <name> is required when --container-config is used. "
118
+ "Pass a cloud env (e.g. --env staging) or drop --container-config."
119
+ )
120
+
121
+ if is_local(args.env):
122
+ return
123
+
124
+ if not args.container:
125
+ _fail(
126
+ "Error: --container <logical-key> is required when "
127
+ "--container-config is used. Pass a key defined under "
128
+ "[containers] in the TOML file."
129
+ )
130
+
131
+ container_name, config = resolve_container_config(
132
+ container_key=args.container,
133
+ env=args.env,
134
+ toml_file=toml_file,
135
+ )
136
+
137
+ args.container = container_name
138
+ args._toml_config = config
139
+
140
+
89
141
  def _build_config(args: argparse.Namespace) -> Optional[AzureStorageConfig]:
90
142
  """Build an AzureStorageConfig from CLI args, or None for local.
91
143
 
@@ -106,6 +158,12 @@ def _build_config(args: argparse.Namespace) -> Optional[AzureStorageConfig]:
106
158
  """
107
159
  if not args.env or is_local(args.env):
108
160
  return None
161
+
162
+ toml_config = getattr(args, "_toml_config", None)
163
+
164
+ if toml_config is not None:
165
+ return toml_config
166
+
109
167
  return AzureStorageConfig.from_env()
110
168
 
111
169
 
@@ -127,6 +185,38 @@ def _require_config(args: argparse.Namespace) -> AzureStorageConfig:
127
185
  return config
128
186
 
129
187
 
188
+ def _apply_session_defaults(
189
+ args: argparse.Namespace,
190
+ session: Session,
191
+ ) -> None:
192
+ """Fill missing env/container/container-config from a session.
193
+
194
+ Explicit flags always win. When the container is taken from the
195
+ session and that session was started with `--container-config`, the
196
+ stored TOML path is restored too, so the logical key keeps
197
+ resolving on later runs instead of being treated as a concrete
198
+ container name.
199
+
200
+ Args:
201
+ args (argparse.Namespace): Parsed CLI arguments, updated in
202
+ place.
203
+ session (Session): The active session.
204
+ """
205
+ # The 'session' command reads the session directly and must see the
206
+ # user's explicit flags, so it never inherits defaults.
207
+ if getattr(args, "command", None) == "session":
208
+ return
209
+
210
+ if not args.env and session.env:
211
+ args.env = session.env
212
+
213
+ if getattr(args, "container", None) is None and session.container:
214
+ args.container = session.container
215
+
216
+ if session.container_config and not getattr(args, "container_config", None):
217
+ args.container_config = session.container_config
218
+
219
+
130
220
  def _get_handler(args: argparse.Namespace) -> StorageHandler:
131
221
  """Validate --container and construct a StorageHandler.
132
222
 
@@ -153,7 +243,13 @@ def _cmd_session(args: argparse.Namespace) -> None:
153
243
  return
154
244
 
155
245
  if args.env and args.container:
156
- session.update(env=args.env, container=args.container)
246
+ container_config = getattr(args, "container_config", None)
247
+ session.update(
248
+ env=args.env,
249
+ container=args.container,
250
+ container_config=container_config,
251
+ clear_container_config=not container_config,
252
+ )
157
253
  print(f"Session started: env={args.env}, container={args.container}")
158
254
  return
159
255
 
@@ -856,7 +952,20 @@ def main() -> None:
856
952
  parent.add_argument(
857
953
  "--container",
858
954
  default=None,
859
- help="Azure container name (required for Azure operations)",
955
+ help="Azure container name (required for Azure operations). "
956
+ "With --container-config, this is a logical key under "
957
+ "[containers] in the TOML file.",
958
+ )
959
+ parent.add_argument(
960
+ "--container-config",
961
+ nargs="?",
962
+ const=DEFAULT_CONTAINER_CONFIG_FILE,
963
+ default=None,
964
+ help="Resolve --container as a logical key from a "
965
+ "container-config.toml file (default name: "
966
+ "container-config.toml in the current directory). Secrets "
967
+ "are resolved from the environment (e.g. via direnv). "
968
+ "Requires --env.",
860
969
  )
861
970
 
862
971
  parser = argparse.ArgumentParser(
@@ -1043,20 +1152,20 @@ def main() -> None:
1043
1152
  sys.exit(1)
1044
1153
 
1045
1154
  session = Session()
1046
-
1047
- if not args.env and session.env:
1048
- args.env = session.env
1049
- if getattr(args, "container", None) is None and session.container:
1050
- args.container = session.container
1155
+ _apply_session_defaults(args=args, session=session)
1051
1156
 
1052
1157
  if args.env:
1053
1158
  os.environ["STORAGE_ENV"] = args.env
1054
1159
  if not is_local(args.env):
1055
- load_dotenv(dotenv_path=env_file(args.env), override=True)
1160
+ load_dotenv(
1161
+ dotenv_path=env_file(args.env),
1162
+ override=not getattr(args, "container_config", None),
1163
+ )
1056
1164
 
1057
1165
  try:
1166
+ _apply_container_config(args=args)
1058
1167
  args.func(args)
1059
- except ValueError as error:
1168
+ except (ValueError, FileNotFoundError) as error:
1060
1169
  _fail(f"Error: {error}")
1061
1170
 
1062
1171