diff --git a/CHANGELOG.md b/CHANGELOG.md index 3d2e374..15f784d 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,6 +5,14 @@ All notable changes to this project will be documented in this file. The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/), and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). +## [1.7.1] - 2026-02-11 + +### Fixed +- **`base_url` in local mode procedures**: `run_post_export_sql()` now accepts a `config` parameter from the caller instead of always creating a fresh `ExportConfig(load_from_env=True)` + - Prevents `"None"` appearing in curl command URLs when running in local mode without `.env` files + - Falls back to `dictionary_metadata` table in the database if `BASE_URL` is still None + - `substitute_parameters()` now skips substitution when a config value is `None` instead of converting to literal `"None"` string + ## [1.7.0] - 2026-02-05 ### Changed diff --git a/gooddata_export/cli/main.py b/gooddata_export/cli/main.py index be7ffbc..368f585 100644 --- a/gooddata_export/cli/main.py +++ b/gooddata_export/cli/main.py @@ -211,8 +211,11 @@ def run_enrich_command(args): configure_logging(args.debug) try: + # Load config for parameter substitution (procedures need base_url etc.) + config = ExportConfig(load_from_env=True) + # Run post-export processing - run_post_export_sql(args.db_path) + run_post_export_sql(args.db_path, config=config) # Calculate duration end_time = datetime.now() diff --git a/gooddata_export/export/__init__.py b/gooddata_export/export/__init__.py index 7543cdc..3cacd54 100644 --- a/gooddata_export/export/__init__.py +++ b/gooddata_export/export/__init__.py @@ -184,10 +184,14 @@ def export_all_metadata( if config.INCLUDE_CHILD_WORKSPACES: # Multi-workspace: enrich only parent workspace to avoid confusing duplicates logger.debug("Multi-workspace mode: enriching parent workspace only") - run_post_export_sql(db_path, parent_workspace_id=config.WORKSPACE_ID) + run_post_export_sql( + db_path, + parent_workspace_id=config.WORKSPACE_ID, + config=config, + ) else: # Single workspace: enrich all data (no filter needed) - run_post_export_sql(db_path) + run_post_export_sql(db_path, config=config) except ExportError as e: # Capture error but continue (database still usable without enrichment) post_export_error = str(e) diff --git a/gooddata_export/post_export.py b/gooddata_export/post_export.py index 6fc50cc..78a1250 100644 --- a/gooddata_export/post_export.py +++ b/gooddata_export/post_export.py @@ -16,7 +16,7 @@ from gooddata_export.common import ExportError from gooddata_export.config import ExportConfig -from gooddata_export.db import connect_database +from gooddata_export.db import connect_database, database_connection logger = logging.getLogger(__name__) @@ -196,8 +196,15 @@ def substitute_parameters(sql_script, parameters, config): config_key = param_template[2:-2].strip() if hasattr(config, config_key): value = getattr(config, config_key) - result = result.replace(f"{{{param_name}}}", str(value)) - logger.debug(" Substituted {%s} with %s", param_name, value) + if value is None: + logger.warning( + " Config key %s is None, skipping substitution for {%s}", + config_key, + param_name, + ) + else: + result = result.replace(f"{{{param_name}}}", str(value)) + logger.debug(" Substituted {%s} with %s", param_name, value) else: logger.warning( " Config key %s not found, skipping substitution", config_key @@ -306,7 +313,34 @@ def ensure_columns_exist(cursor, table_name, required_columns): ) -def run_post_export_sql(db_path, parent_workspace_id: str | None = None) -> None: +def _read_metadata_value(db_path: str, key: str) -> str | None: + """Read a single value from dictionary_metadata table. + + Args: + db_path: Path to the SQLite database + key: Metadata key to look up + + Returns: + str | None: The value if found, None otherwise + """ + try: + with database_connection(db_path) as conn: + cursor = conn.cursor() + cursor.execute( + "SELECT value FROM dictionary_metadata WHERE key = ?", (key,) + ) + row = cursor.fetchone() + return row[0] if row and row[0] else None + except Exception as e: + logger.debug("Could not read metadata key '%s': %s", key, e) + return None + + +def run_post_export_sql( + db_path, + parent_workspace_id: str | None = None, + config: ExportConfig | None = None, +) -> None: """Run all post-export SQL operations on the database. This is the main entry point for post-export processing. @@ -317,6 +351,9 @@ def run_post_export_sql(db_path, parent_workspace_id: str | None = None) -> None parent_workspace_id: Optional workspace ID to filter updates to. When provided, UPDATE statements only affect rows for this workspace. Used in multi-workspace exports to enrich only the parent workspace. + config: Optional ExportConfig instance. When provided, used for parameter + substitution in procedures. Falls back to ExportConfig(load_from_env=True) + if not provided, then to dictionary_metadata table for base_url. Raises: ExportError: If post-export processing fails @@ -331,8 +368,15 @@ def run_post_export_sql(db_path, parent_workspace_id: str | None = None) -> None yaml_config = load_post_export_config() sql_dir = Path(__file__).parent / "sql" - # Load export config for parameter substitution - export_config = ExportConfig(load_from_env=True) + # Use provided config or fall back to env-loaded config + export_config = config or ExportConfig(load_from_env=True) + + # If BASE_URL is still None (e.g. local mode without .env), read from DB + if not export_config.BASE_URL: + db_base_url = _read_metadata_value(db_path, "base_url") + if db_base_url: + export_config.BASE_URL = db_base_url + logger.debug("Loaded BASE_URL from database metadata: %s", db_base_url) # Connect to database conn = connect_database(db_path) diff --git a/pyproject.toml b/pyproject.toml index e9c99b2..96076a2 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta" [project] name = "gooddata-export" -version = "1.7.0" +version = "1.7.1" description = "Export GoodData workspace metadata to SQLite and CSV" readme = "README.md" license = {text = "MIT"}