diff --git a/tests/test_backend_dynamo.py b/tests/test_backend_dynamo.py index 1681693..594c850 100644 --- a/tests/test_backend_dynamo.py +++ b/tests/test_backend_dynamo.py @@ -5,7 +5,6 @@ from __future__ import annotations import json -import os import time from unittest import mock @@ -13,18 +12,18 @@ import pytest from moto import mock_aws -from zebra_day.backends.dynamo import DynamoBackend, _BACKUP_DEBOUNCE_SECONDS +from zebra_day.backends.dynamo import DynamoBackend from zebra_day.exceptions import ( ConfigError, LabelTemplateNotFoundError, VersionConflictError, ) - # --------------------------------------------------------------------------- # Fixtures # --------------------------------------------------------------------------- + @pytest.fixture def aws_env(monkeypatch): """Set fake AWS credentials for moto.""" @@ -60,6 +59,7 @@ def dynamo_backend(aws_env): # Config CRUD # --------------------------------------------------------------------------- + class TestLoadConfig: def test_empty_table_returns_default(self, dynamo_backend): config = dynamo_backend.load_config() @@ -119,6 +119,7 @@ def test_config_exists(self, dynamo_backend): # Template CRUD # --------------------------------------------------------------------------- + class TestTemplateCRUD: def test_save_and_get(self, dynamo_backend): dynamo_backend.save_template("my_label", "^XA^FO10,10^A0N,30,30^FDHello^FS^XZ") @@ -163,11 +164,11 @@ def test_template_version_conflict(self, dynamo_backend): dynamo_backend.save_template("tpl", "v2") - # --------------------------------------------------------------------------- # S3 Backup & Restore # --------------------------------------------------------------------------- + class TestS3Backup: def test_backup_creates_manifest(self, dynamo_backend): dynamo_backend.save_config({"schema_version": "2.1.0", "labs": {"a": {}}}) @@ -230,6 +231,7 @@ def test_restore_round_trip(self, dynamo_backend): # Resource Tagging # --------------------------------------------------------------------------- + class TestResourceTagging: def test_dynamodb_table_tags(self, aws_env): with mock_aws(): @@ -243,7 +245,9 @@ def test_dynamodb_table_tags(self, aws_env): backend.create_table() # Check tags via describe - arn = backend._ddb_client.describe_table(TableName="tag-test-table")["Table"]["TableArn"] + arn = backend._ddb_client.describe_table(TableName="tag-test-table")["Table"][ + "TableArn" + ] tags_resp = backend._ddb_client.list_tags_of_resource(ResourceArn=arn) tags = {t["Key"]: t["Value"] for t in tags_resp.get("Tags", [])} assert tags["lsmc-cost-center"] == "my-cc" @@ -293,6 +297,7 @@ def test_tag_from_env(self, aws_env, monkeypatch): # Factory & Profile Rules # --------------------------------------------------------------------------- + class TestFromEnv: def test_from_env_basic(self, aws_env, monkeypatch): monkeypatch.setenv("ZEBRA_DAY_DYNAMO_TABLE", "my-table") @@ -341,6 +346,7 @@ def test_no_explicit_default_profile(self, aws_env): # Status # --------------------------------------------------------------------------- + class TestStatus: def test_get_status(self, dynamo_backend): dynamo_backend.save_config({"schema_version": "2.1.0", "labs": {}}) @@ -355,6 +361,7 @@ def test_get_status(self, dynamo_backend): # Integration: zpl() with DynamoBackend # --------------------------------------------------------------------------- + class TestZplIntegration: def test_zpl_init_with_backend(self, dynamo_backend): from zebra_day.print_mgr import zpl @@ -419,6 +426,7 @@ def test_env_var_selection(self, aws_env, monkeypatch): # AWS Permission Checks # --------------------------------------------------------------------------- + class TestCheckAWSPermissions: def test_all_checks_pass_with_resources(self, dynamo_backend): result = dynamo_backend.check_aws_permissions() diff --git a/tests/test_cli_dynamo.py b/tests/test_cli_dynamo.py index ccb6cce..d57c2a9 100644 --- a/tests/test_cli_dynamo.py +++ b/tests/test_cli_dynamo.py @@ -5,10 +5,7 @@ from __future__ import annotations import json -import os -from pathlib import Path -import boto3 import pytest from moto import mock_aws from typer.testing import CliRunner @@ -67,10 +64,14 @@ def test_init_success(self, aws_env): result = runner.invoke( app, [ - "dynamo", "init", - "--table-name", "test-zebra-config", - "--s3-bucket", "test-backup-bucket", - "--region", "us-east-1", + "dynamo", + "init", + "--table-name", + "test-zebra-config", + "--s3-bucket", + "test-backup-bucket", + "--region", + "us-east-1", ], ) assert result.exit_code == 0, result.output @@ -94,10 +95,14 @@ def test_init_with_s3_config_file(self, aws_env, tmp_path): result = runner.invoke( app, [ - "dynamo", "init", - "--table-name", "test-zebra-config", - "--region", "us-east-1", - "--s3-config-file", str(cfg), + "dynamo", + "init", + "--table-name", + "test-zebra-config", + "--region", + "us-east-1", + "--s3-config-file", + str(cfg), ], ) assert result.exit_code == 0, result.output @@ -110,11 +115,16 @@ def test_init_s3_bucket_flag_overrides_config_file(self, aws_env, tmp_path): result = runner.invoke( app, [ - "dynamo", "init", - "--table-name", "test-zebra-config", - "--region", "us-east-1", - "--s3-bucket", "flag-bucket", - "--s3-config-file", str(cfg), + "dynamo", + "init", + "--table-name", + "test-zebra-config", + "--region", + "us-east-1", + "--s3-bucket", + "flag-bucket", + "--s3-config-file", + str(cfg), ], ) assert result.exit_code == 0, result.output @@ -127,10 +137,14 @@ def test_init_bad_s3_config_file(self, aws_env, tmp_path): result = runner.invoke( app, [ - "dynamo", "init", - "--table-name", "test-zebra-config", - "--region", "us-east-1", - "--s3-config-file", str(cfg), + "dynamo", + "init", + "--table-name", + "test-zebra-config", + "--region", + "us-east-1", + "--s3-config-file", + str(cfg), ], ) assert result.exit_code == 1 @@ -140,10 +154,14 @@ def test_init_nonexistent_s3_config_file(self, aws_env): result = runner.invoke( app, [ - "dynamo", "init", - "--table-name", "test-zebra-config", - "--region", "us-east-1", - "--s3-config-file", "/tmp/no-such-file-12345.json", + "dynamo", + "init", + "--table-name", + "test-zebra-config", + "--region", + "us-east-1", + "--s3-config-file", + "/tmp/no-such-file-12345.json", ], ) assert result.exit_code == 1 @@ -154,24 +172,35 @@ def test_init_shows_permission_checks(self, aws_env): result = runner.invoke( app, [ - "dynamo", "init", - "--table-name", "test-zebra-config", - "--s3-bucket", "test-backup-bucket", - "--region", "us-east-1", + "dynamo", + "init", + "--table-name", + "test-zebra-config", + "--s3-bucket", + "test-backup-bucket", + "--region", + "us-east-1", ], ) assert result.exit_code == 0, result.output - assert "permission checks passed" in result.output.lower() or "Checking AWS" in result.output + assert ( + "permission checks passed" in result.output.lower() + or "Checking AWS" in result.output + ) def test_init_skip_checks(self, aws_env): with mock_aws(): result = runner.invoke( app, [ - "dynamo", "init", - "--table-name", "test-zebra-config", - "--s3-bucket", "test-backup-bucket", - "--region", "us-east-1", + "dynamo", + "init", + "--table-name", + "test-zebra-config", + "--s3-bucket", + "test-backup-bucket", + "--region", + "us-east-1", "--skip-checks", ], ) @@ -225,9 +254,12 @@ def test_bootstrap_with_config_and_templates(self, aws_env, tmp_path): result = runner.invoke( app, [ - "dynamo", "bootstrap", - "--config-file", str(cfg), - "--templates-dir", str(tpl_dir), + "dynamo", + "bootstrap", + "--config-file", + str(cfg), + "--templates-dir", + str(tpl_dir), "--no-include-package", ], ) @@ -237,8 +269,6 @@ def test_bootstrap_with_config_and_templates(self, aws_env, tmp_path): assert "Backup written" in result.output - - # --------------------------------------------------------------------------- # export # --------------------------------------------------------------------------- @@ -381,7 +411,6 @@ def test_destroy_success(self, aws_env): assert "Backups preserved" in result.output - # --------------------------------------------------------------------------- # Interactive S3 bucket prompt # --------------------------------------------------------------------------- @@ -398,9 +427,7 @@ def test_status_prompts_for_bucket_interactively(self, aws_env, monkeypatch): with mock_aws(): _provision_resources() # Provide bucket name via CliRunner input - result = runner.invoke( - app, ["dynamo", "status"], input="test-backup-bucket\n" - ) + result = runner.invoke(app, ["dynamo", "status"], input="test-backup-bucket\n") assert result.exit_code == 0, result.output assert "test-zebra-config" in result.output @@ -429,9 +456,7 @@ def test_backup_prompts_for_bucket(self, aws_env, monkeypatch): with mock_aws(): backend = _provision_resources() backend.save_config({"labs": {}}) - result = runner.invoke( - app, ["dynamo", "backup"], input="test-backup-bucket\n" - ) + result = runner.invoke(app, ["dynamo", "backup"], input="test-backup-bucket\n") assert result.exit_code == 0, result.output assert "Backup written" in result.output @@ -443,9 +468,12 @@ def test_init_prompts_for_bucket(self, aws_env, monkeypatch): result = runner.invoke( app, [ - "dynamo", "init", - "--table-name", "test-zebra-config", - "--region", "us-east-1", + "dynamo", + "init", + "--table-name", + "test-zebra-config", + "--region", + "us-east-1", ], input="test-backup-bucket\n", ) @@ -467,6 +495,7 @@ def test_status_create_s3_if_missing(self, aws_env, monkeypatch): _provision_resources() # Delete the bucket so it doesn't exist import boto3 + s3 = boto3.client("s3", region_name="us-east-1") # Empty and delete the bucket try: @@ -479,9 +508,7 @@ def test_status_create_s3_if_missing(self, aws_env, monkeypatch): # Use a new bucket name via env monkeypatch.setenv("ZEBRA_DAY_S3_BACKUP_BUCKET", "new-auto-bucket") - result = runner.invoke( - app, ["dynamo", "status", "--create-s3-if-missing"] - ) + result = runner.invoke(app, ["dynamo", "status", "--create-s3-if-missing"]) assert result.exit_code == 0, result.output # Bucket should have been created assert "created" in result.output.lower() or "test-zebra-config" in result.output @@ -494,9 +521,7 @@ def test_backup_create_s3_if_missing(self, aws_env, monkeypatch): # Point to a new bucket that doesn't exist monkeypatch.setenv("ZEBRA_DAY_S3_BACKUP_BUCKET", "auto-backup-bucket") - result = runner.invoke( - app, ["dynamo", "backup", "--create-s3-if-missing"] - ) + result = runner.invoke(app, ["dynamo", "backup", "--create-s3-if-missing"]) assert result.exit_code == 0, result.output assert "Backup written" in result.output @@ -510,11 +535,13 @@ def test_bootstrap_create_s3_if_missing(self, aws_env, monkeypatch, tmp_path): result = runner.invoke( app, [ - "dynamo", "bootstrap", - "--config-file", str(cfg), + "dynamo", + "bootstrap", + "--config-file", + str(cfg), "--no-include-package", "--create-s3-if-missing", ], ) assert result.exit_code == 0, result.output - assert "created" in result.output.lower() or "Config uploaded" in result.output \ No newline at end of file + assert "created" in result.output.lower() or "Config uploaded" in result.output diff --git a/tests/test_core_functions.py b/tests/test_core_functions.py index bcb578d..b70a8f9 100644 --- a/tests/test_core_functions.py +++ b/tests/test_core_functions.py @@ -354,8 +354,10 @@ def test_probe_accepts_valid_stub(self): zp = zdpm.zpl() # Mock HTTPConnection and HTTPSConnection so the 255-IP loop # completes instantly without real network calls. - with mock.patch("http.client.HTTPConnection") as mock_http, \ - mock.patch("http.client.HTTPSConnection") as mock_https: + with ( + mock.patch("http.client.HTTPConnection") as mock_http, + mock.patch("http.client.HTTPSConnection") as mock_https, + ): # Make every connection attempt raise immediately (no printer) mock_http.return_value.request.side_effect = OSError("mocked") mock_https.return_value.request.side_effect = OSError("mocked") diff --git a/tests/test_mkcert.py b/tests/test_mkcert.py index 771d168..be64114 100644 --- a/tests/test_mkcert.py +++ b/tests/test_mkcert.py @@ -1,11 +1,8 @@ """ Tests for the mkcert integration module. """ -import platform -from pathlib import Path -from unittest import mock -import pytest +from unittest import mock from zebra_day import mkcert @@ -51,8 +48,7 @@ def test_is_ca_installed_true(self): with mock.patch("shutil.which", return_value="/usr/local/bin/mkcert"): with mock.patch("subprocess.run") as mock_run: mock_run.return_value = mock.Mock( - returncode=0, - stdout="/Users/test/.local/share/mkcert\n" + returncode=0, stdout="/Users/test/.local/share/mkcert\n" ) with mock.patch("pathlib.Path.exists", return_value=True): assert mkcert.is_ca_installed() is True @@ -137,4 +133,3 @@ def test_try_auto_generate_success(self): assert "success" in message.lower() assert cert is not None assert key is not None - diff --git a/tests/test_web_server.py b/tests/test_web_server.py index 43a0031..bf93fca 100644 --- a/tests/test_web_server.py +++ b/tests/test_web_server.py @@ -2,7 +2,7 @@ import json import threading -from unittest.mock import MagicMock, PropertyMock +from unittest.mock import MagicMock import pytest from fastapi.testclient import TestClient @@ -710,8 +710,17 @@ def test_backend_status_has_required_keys(self, client): """Test response contains all required keys.""" response = client.get("/api/v1/config/backend-status") data = response.json() - for key in ["backend_type", "aws_profile", "dynamo_table", "aws_region", - "s3_bucket", "s3_prefix", "last_backup", "config_version", "error"]: + for key in [ + "backend_type", + "aws_profile", + "dynamo_table", + "aws_region", + "s3_bucket", + "s3_prefix", + "last_backup", + "config_version", + "error", + ]: assert key in data, f"Missing key: {key}" @@ -937,9 +946,7 @@ def test_detect_tables_accepts_profile_param(self, client): """Test detect-tables endpoint accepts profile query parameter.""" # This will fail with boto3 error (no credentials), but we're testing # that the endpoint accepts the parameter without 400 error - response = client.get( - "/api/v1/config/detect-tables?region=us-west-2&profile=test-profile" - ) + response = client.get("/api/v1/config/detect-tables?region=us-west-2&profile=test-profile") # Should get 503 (AWS error) or 501 (boto3 not installed), not 400 (bad request) assert response.status_code in [501, 503] @@ -948,6 +955,7 @@ def test_detect_tables_accepts_profile_param(self, client): # Helper: create a mock DynamoBackend that passes isinstance checks # --------------------------------------------------------------------------- + def _make_mock_dynamo_backend( profile="test-profile", table_name="zebra-day-config", @@ -1096,6 +1104,7 @@ def test_config_page_profile_empty_when_local(self, client): html = response.text # Find the profile input and check it's empty import re + match = re.search(r'id="switch-profile"[^>]*value="([^"]*)"', html) assert match is not None assert match.group(1) == "" @@ -1107,6 +1116,7 @@ def test_config_page_profile_filters_sentinel_values(self, client): response = client.get("/config") import re + match = re.search(r'id="switch-profile"[^>]*value="([^"]*)"', response.text) assert match is not None # Should be empty since profile is None → "default credential chain" diff --git a/zebra_day/backends/__init__.py b/zebra_day/backends/__init__.py index a9a7683..d5fa732 100644 --- a/zebra_day/backends/__init__.py +++ b/zebra_day/backends/__init__.py @@ -112,4 +112,3 @@ def get_backend(config_path: str | None = None) -> ConfigBackend: "ConfigBackend", "get_backend", ] - diff --git a/zebra_day/backends/dynamo.py b/zebra_day/backends/dynamo.py index 154598c..5a84409 100644 --- a/zebra_day/backends/dynamo.py +++ b/zebra_day/backends/dynamo.py @@ -134,7 +134,6 @@ def check_aws_permissions(self) -> dict[str, Any]: # 1. STS identity check — do credentials work at all? try: - sts = self._ddb_resource.meta.client.meta.events # Use the session to create an STS client session_kwargs: dict[str, Any] = {"region_name": self.region} import boto3 as _b3 @@ -371,7 +370,7 @@ def save_config(self, config: dict) -> None: ExpressionAttributeValues={":expected": current_version}, ) except self._ddb_resource.meta.client.exceptions.ConditionalCheckFailedException: - raise VersionConflictError("CONFIG#printer_config", current_version) + raise VersionConflictError("CONFIG#printer_config", current_version) from None _log.debug("Config saved to DynamoDB (version %d)", current_version + 1) self._maybe_backup("save_config") @@ -438,7 +437,7 @@ def save_template(self, name: str, zpl_content: str) -> None: ExpressionAttributeValues={":expected": current_version}, ) except self._ddb_resource.meta.client.exceptions.ConditionalCheckFailedException: - raise VersionConflictError(f"TEMPLATE#{stem}", current_version) + raise VersionConflictError(f"TEMPLATE#{stem}", current_version) from None _log.debug("Template '%s' saved to DynamoDB (version %d)", stem, current_version + 1) self._maybe_backup("save_template") diff --git a/zebra_day/cli/dynamo.py b/zebra_day/cli/dynamo.py index 635c4a4..99b97a0 100644 --- a/zebra_day/cli/dynamo.py +++ b/zebra_day/cli/dynamo.py @@ -128,7 +128,7 @@ def _load_s3_config_file(path: str) -> dict: data = json_mod.loads(p.read_text()) except Exception as exc: output.error(f"Failed to parse S3 config file: {exc}") - raise typer.Exit(1) + raise typer.Exit(1) from exc if not isinstance(data, dict): output.error("S3 config file must contain a JSON object") raise typer.Exit(1) @@ -293,7 +293,7 @@ def init_cmd( output.warning(f"Table '{backend.table_name}' already exists") else: output.error(f"Failed to create table: {exc}") - raise typer.Exit(1) + raise typer.Exit(1) from exc # Create S3 bucket (creates if not exists, applies tags) output.action(f"Ensuring S3 bucket '{backend.s3_bucket}' exists...") @@ -302,7 +302,7 @@ def init_cmd( output.success(f"S3 bucket '{backend.s3_bucket}' ready") except Exception as exc: output.error(f"Failed to create/access bucket: {exc}") - raise typer.Exit(1) + raise typer.Exit(1) from exc # Write META output.action("Writing metadata...") @@ -336,7 +336,7 @@ def status_cmd( ) except (ConfigError, ImportError) as exc: output.error(str(exc)) - raise typer.Exit(1) + raise typer.Exit(1) from exc try: status = backend.get_status() @@ -345,7 +345,7 @@ def status_cmd( type(exc).__name__ ): output.error("Table not found. Run 'zday dynamo init' first.") - raise typer.Exit(1) + raise typer.Exit(1) from exc raise template_count = len(backend.list_templates()) @@ -407,7 +407,7 @@ def bootstrap_cmd( backend = _get_backend_from_env(create_s3_if_missing=create_s3_if_missing) except (ConfigError, ImportError) as exc: output.error(str(exc)) - raise typer.Exit(1) + raise typer.Exit(1) from exc output.heading("DynamoDB Bootstrap") @@ -481,7 +481,7 @@ def export_cmd( backend = _get_backend_from_env() except (ConfigError, ImportError) as exc: output.error(str(exc)) - raise typer.Exit(1) + raise typer.Exit(1) from exc out = Path(output_dir) tpl_dir = out / "templates" @@ -530,7 +530,7 @@ def backup_cmd( backend = _get_backend_from_env(create_s3_if_missing=create_s3_if_missing) except (ConfigError, ImportError) as exc: output.error(str(exc)) - raise typer.Exit(1) + raise typer.Exit(1) from exc output.heading("DynamoDB Backup") @@ -539,7 +539,7 @@ def backup_cmd( output.success(f"Backup written to s3://{backend.s3_bucket}/{prefix}") except Exception as exc: output.error(f"Backup failed: {exc}") - raise typer.Exit(1) + raise typer.Exit(1) from exc # ----------------------------------------------------------------- @@ -560,7 +560,7 @@ def restore_cmd( backend = _get_backend_from_env() except (ConfigError, ImportError) as exc: output.error(str(exc)) - raise typer.Exit(1) + raise typer.Exit(1) from exc if list_backups: backups = backend.list_backups() @@ -602,7 +602,7 @@ def restore_cmd( output.success("Restore complete") except Exception as exc: output.error(f"Restore failed: {exc}") - raise typer.Exit(1) + raise typer.Exit(1) from exc # ----------------------------------------------------------------- @@ -623,7 +623,7 @@ def destroy_cmd( backend = _get_backend_from_env() except (ConfigError, ImportError) as exc: output.error(str(exc)) - raise typer.Exit(1) + raise typer.Exit(1) from exc output.heading("DynamoDB Table Destruction") @@ -642,7 +642,7 @@ def destroy_cmd( output.success("Table deleted. Backups preserved in S3.") except Exception as exc: output.error(f"Delete failed: {exc}") - raise typer.Exit(1) + raise typer.Exit(1) from exc def register(registry: CommandRegistry, spec: CliSpec) -> None: diff --git a/zebra_day/mkcert.py b/zebra_day/mkcert.py index 5241c71..768d72d 100644 --- a/zebra_day/mkcert.py +++ b/zebra_day/mkcert.py @@ -209,9 +209,7 @@ def try_auto_generate_certificates() -> tuple[bool, str, Path | None, Path | Non if not is_mkcert_installed(): install_cmd = get_platform_install_command() msg = ( - f"mkcert is not installed. Install it with:\n" - f" {install_cmd}\n" - f"Then run: mkcert -install" + f"mkcert is not installed. Install it with:\n {install_cmd}\nThen run: mkcert -install" ) _log.warning("mkcert not installed - cannot auto-generate certificates") return False, msg, None, None diff --git a/zebra_day/web/app.py b/zebra_day/web/app.py index 28419f7..e9c35d2 100644 --- a/zebra_day/web/app.py +++ b/zebra_day/web/app.py @@ -16,7 +16,8 @@ from fastapi.staticfiles import StaticFiles from fastapi.templating import Jinja2Templates -from zebra_day import __version__, paths as xdg +from zebra_day import __version__ +from zebra_day import paths as xdg from zebra_day.logging_config import get_logger from zebra_day.web.middleware import RequestLoggingMiddleware, print_rate_limiter @@ -194,6 +195,7 @@ def run_server( 5. Fall back to HTTP with guidance if certificate setup fails """ import uvicorn + from zebra_day import mkcert # Store auth mode in environment for factory function diff --git a/zebra_day/web/routers/api.py b/zebra_day/web/routers/api.py index 5498709..a646834 100644 --- a/zebra_day/web/routers/api.py +++ b/zebra_day/web/routers/api.py @@ -729,7 +729,7 @@ async def config_refresh(request: Request) -> dict[str, Any]: return {"success": True, "message": "Configuration reloaded from backend."} except Exception as exc: _log.error("Config refresh failed: %s", exc) - raise HTTPException(status_code=500, detail=str(exc)) + raise HTTPException(status_code=500, detail=str(exc)) from exc @router.get("/config/detect-tables") @@ -764,12 +764,12 @@ async def config_detect_tables( raise HTTPException( status_code=501, detail="boto3 is not installed. Install with: pip install zebra_day[aws]", - ) + ) from None except Exception as exc: raise HTTPException( status_code=503, detail=f"Unable to create AWS session: {exc}", - ) + ) from exc try: # List all tables (paginate) @@ -817,7 +817,7 @@ async def config_detect_tables( raise HTTPException( status_code=503, detail=f"Failed to scan DynamoDB tables in {scan_region}: {exc}", - ) + ) from exc class CheckS3BucketRequest(BaseModel): @@ -862,7 +862,7 @@ async def config_check_s3_bucket(body: CheckS3BucketRequest) -> dict[str, Any]: raise HTTPException( status_code=501, detail="boto3 is not installed. Install with: pip install zebra_day[aws]", - ) + ) from None except Exception: return {"exists": False, "bucket": body.bucket} @@ -903,12 +903,12 @@ async def config_create_s3_bucket(body: CreateS3BucketRequest) -> dict[str, Any] raise HTTPException( status_code=501, detail="boto3 is not installed. Install with: pip install zebra_day[aws]", - ) + ) from None except Exception as exc: raise HTTPException( status_code=503, detail=f"Failed to create S3 bucket '{body.bucket}': {exc}", - ) + ) from exc class SwitchBackendRequest(BaseModel): @@ -970,13 +970,13 @@ async def config_switch_backend( raise HTTPException( status_code=501, detail="boto3 is not installed. Install with: pip install zebra_day[aws]", - ) + ) from None except Exception as exc: raise HTTPException( status_code=503, detail=f"Cannot connect to DynamoDB table '{body.table_name}' " f"in {body.region}: {exc}", - ) + ) from exc else: # Switch to local backend @@ -998,7 +998,7 @@ async def config_switch_backend( raise HTTPException( status_code=500, detail=f"Backend switch failed during config reload: {exc}", - ) + ) from exc _log.info("Backend switched to %s", body.backend_type) return {