This is an automated email from the ASF dual-hosted git repository.

asorokoumov pushed a commit to branch fixup-0.7.0-rc3
in repository https://gitbox.apache.org/repos/asf/otava.git

commit 444e7e35b5e0f3fc0dc66fc5ec5e1a36dc4f573d
Author: Alex Sorokoumov <[email protected]>
AuthorDate: Thu Nov 27 20:34:10 2025 -0800

    Revert "OTAVA-82: use ConfigArgParse to create Config (#86)"
    
    This reverts commit 69d2b97a6c38873b74755bf1104a69f099b922b9.
---
 otava/bigquery.py                                  |  14 --
 otava/config.py                                    | 186 +++++++++++----------
 otava/grafana.py                                   |  14 --
 otava/graphite.py                                  |  10 --
 otava/main.py                                      |  38 ++---
 otava/postgres.py                                  |  18 --
 .../resources/otava.yaml                           |  43 +++--
 otava/slack.py                                     |  14 --
 pyproject.toml                                     |   2 +-
 tests/config_test.py                               |  71 +-------
 uv.lock                                            |  26 +--
 11 files changed, 164 insertions(+), 272 deletions(-)

diff --git a/otava/bigquery.py b/otava/bigquery.py
index c9b77a6..7ef7c92 100644
--- a/otava/bigquery.py
+++ b/otava/bigquery.py
@@ -32,20 +32,6 @@ class BigQueryConfig:
     dataset: str
     credentials: str
 
-    @staticmethod
-    def add_parser_args(arg_group):
-        arg_group.add_argument("--bigquery-project-id", help="BigQuery project 
ID", env_var="BIGQUERY_PROJECT_ID")
-        arg_group.add_argument("--bigquery-dataset", help="BigQuery dataset", 
env_var="BIGQUERY_DATASET")
-        arg_group.add_argument("--bigquery-credentials", help="BigQuery 
credentials file", env_var="BIGQUERY_VAULT_SECRET")
-
-    @staticmethod
-    def from_parser_args(args):
-        return BigQueryConfig(
-            project_id=getattr(args, 'bigquery_project_id', None),
-            dataset=getattr(args, 'bigquery_dataset', None),
-            credentials=getattr(args, 'bigquery_credentials', None)
-        )
-
 
 @dataclass
 class BigQueryError(Exception):
diff --git a/otava/config.py b/otava/config.py
index c0e8d54..37936c6 100644
--- a/otava/config.py
+++ b/otava/config.py
@@ -14,12 +14,13 @@
 # KIND, either express or implied.  See the License for the
 # specific language governing permissions and limitations
 # under the License.
-import logging
+
+import os
 from dataclasses import dataclass
 from pathlib import Path
 from typing import Dict, List, Optional
 
-import configargparse
+from expandvars import expandvars
 from ruamel.yaml import YAML
 
 from otava.bigquery import BigQueryConfig
@@ -95,90 +96,107 @@ def load_test_groups(config: Dict, tests: Dict[str, 
TestConfig]) -> Dict[str, Li
     return result
 
 
-def load_config_from_parser_args(args: configargparse.Namespace) -> Config:
-    config_file = getattr(args, "config_file", None)
-    if config_file is not None:
+def load_config_from(config_file: Path) -> Config:
+    """Loads config from the specified location"""
+    try:
+        content = expandvars(config_file.read_text(), nounset=True)
         yaml = YAML(typ="safe")
-        config = yaml.load(Path(config_file).read_text())
+        config = yaml.load(content)
+        """
+        if Grafana configs not explicitly set in yaml file, default to same as 
Graphite
+        server at port 3000
+        """
+        graphite_config = None
+        grafana_config = None
+        if "graphite" in config:
+            if "url" not in config["graphite"]:
+                raise ValueError("graphite.url")
+            graphite_config = GraphiteConfig(url=config["graphite"]["url"])
+            if config.get("grafana") is None:
+                config["grafana"] = {}
+                config["grafana"]["url"] = 
f"{config['graphite']['url'].strip('/')}:3000/"
+                config["grafana"]["user"] = os.environ.get("GRAFANA_USER", 
"admin")
+                config["grafana"]["password"] = 
os.environ.get("GRAFANA_PASSWORD", "admin")
+            grafana_config = GrafanaConfig(
+                url=config["grafana"]["url"],
+                user=config["grafana"]["user"],
+                password=config["grafana"]["password"],
+            )
+
+        slack_config = None
+        if config.get("slack") is not None:
+            if not config["slack"]["token"]:
+                raise ValueError("slack.token")
+            slack_config = SlackConfig(
+                bot_token=config["slack"]["token"],
+            )
+
+        postgres_config = None
+        if config.get("postgres") is not None:
+            if not config["postgres"]["hostname"]:
+                raise ValueError("postgres.hostname")
+            if not config["postgres"]["port"]:
+                raise ValueError("postgres.port")
+            if not config["postgres"]["username"]:
+                raise ValueError("postgres.username")
+            if not config["postgres"]["password"]:
+                raise ValueError("postgres.password")
+            if not config["postgres"]["database"]:
+                raise ValueError("postgres.database")
+
+            postgres_config = PostgresConfig(
+                hostname=config["postgres"]["hostname"],
+                port=config["postgres"]["port"],
+                username=config["postgres"]["username"],
+                password=config["postgres"]["password"],
+                database=config["postgres"]["database"],
+            )
+
+        bigquery_config = None
+        if config.get("bigquery") is not None:
+            bigquery_config = BigQueryConfig(
+                project_id=config["bigquery"]["project_id"],
+                dataset=config["bigquery"]["dataset"],
+                credentials=config["bigquery"]["credentials"],
+            )
 
         templates = load_templates(config)
         tests = load_tests(config, templates)
         groups = load_test_groups(config, tests)
-    else:
-        logging.warning("Otava configuration file not found or not specified")
-        tests = {}
-        groups = {}
-
-    return Config(
-        graphite=GraphiteConfig.from_parser_args(args),
-        grafana=GrafanaConfig.from_parser_args(args),
-        slack=SlackConfig.from_parser_args(args),
-        postgres=PostgresConfig.from_parser_args(args),
-        bigquery=BigQueryConfig.from_parser_args(args),
-        tests=tests,
-        test_groups=groups,
-    )
-
-
-class NestedYAMLConfigFileParser(configargparse.ConfigFileParser):
-    """
-    Custom YAML config file parser that supports nested YAML structures.
-    Maps nested keys like 'slack: {token: value}' to 'slack-token=value', i.e. 
CLI argument style.
-    Recasts values from YAML inferred types to strings as expected for CLI 
arguments.
-    """
-
-    def parse(self, stream):
-        yaml = YAML(typ="safe")
-        config_data = yaml.load(stream)
-        if config_data is None:
-            return {}
-        flattened_dict = {}
-        self._flatten_dict(config_data, flattened_dict)
-        return flattened_dict
-
-    def _flatten_dict(self, nested_dict, flattened_dict, prefix=''):
-        """Recursively flatten nested dictionaries using CLI dash-separated 
notation for keys."""
-        if not isinstance(nested_dict, dict):
-            return
-
-        for key, value in nested_dict.items():
-            new_key = f"{prefix}{key}" if prefix else key
-
-            # yaml keys typically use snake case
-            # replace underscore with dash to convert snake case to CLI 
dash-separated style
-            new_key = new_key.replace("_", "-")
-
-            if isinstance(value, dict):
-                # Recursively process nested dictionaries
-                self._flatten_dict(value, flattened_dict, f"{new_key}-")
-            else:
-                # Add leaf values to the flattened dictionary
-                # Value must be cast to string here, so arg parser can cast 
from string to expected type later
-                flattened_dict[new_key] = str(value)
-
-
-def create_config_parser() -> configargparse.ArgumentParser:
-    parser = configargparse.ArgumentParser(
-        add_help=False,
-        config_file_parser_class=NestedYAMLConfigFileParser,
-        default_config_files=[
-            Path().home() / ".otava/conf.yaml",
-            Path().home() / ".otava/otava.yaml",
-        ],
-        allow_abbrev=False,  # required for correct parsing of nested values 
from config file
-    )
-    parser.add_argument('--config-file', is_config_file=True, help='Otava 
config file path', env_var="OTAVA_CONFIG")
-    GraphiteConfig.add_parser_args(parser.add_argument_group('Graphite 
Options', 'Options for Graphite configuration'))
-    GrafanaConfig.add_parser_args(parser.add_argument_group('Grafana Options', 
'Options for Grafana configuration'))
-    SlackConfig.add_parser_args(parser.add_argument_group('Slack Options', 
'Options for Slack configuration'))
-    PostgresConfig.add_parser_args(parser.add_argument_group('Postgres 
Options', 'Options for Postgres configuration'))
-    BigQueryConfig.add_parser_args(parser.add_argument_group('BigQuery 
Options', 'Options for BigQuery configuration'))
-    return parser
-
-
-def load_config_from_file(config_file: str, arg_overrides: Optional[List[str]] 
= None) -> Config:
-    if arg_overrides is None:
-        arg_overrides = []
-    arg_overrides.extend(["--config-file", config_file])
-    args, _ = create_config_parser().parse_known_args(args=arg_overrides)
-    return load_config_from_parser_args(args)
+
+        return Config(
+            graphite=graphite_config,
+            grafana=grafana_config,
+            slack=slack_config,
+            postgres=postgres_config,
+            bigquery=bigquery_config,
+            tests=tests,
+            test_groups=groups,
+        )
+
+    except FileNotFoundError as e:
+        raise ConfigError(f"Configuration file not found: {e.filename}")
+    except KeyError as e:
+        raise ConfigError(f"Configuration key not found: {e.args[0]}")
+    except ValueError as e:
+        raise ConfigError(f"Value for configuration key not found: 
{e.args[0]}")
+
+
+def load_config() -> Config:
+    """Loads config from one of the default locations"""
+
+    env_config_path = os.environ.get("OTAVA_CONFIG")
+    if env_config_path:
+        return load_config_from(Path(env_config_path).absolute())
+
+    paths = [
+        Path().home() / ".otava/otava.yaml",
+        Path().home() / ".otava/conf.yaml",
+        Path(os.path.realpath(__file__)).parent / "resources/otava.yaml",
+    ]
+
+    for p in paths:
+        if p.exists():
+            return load_config_from(p)
+
+    raise ConfigError(f"No configuration file found. Checked $OTAVA_CONFIG and 
searched: {paths}")
diff --git a/otava/grafana.py b/otava/grafana.py
index bc90998..dbf9e42 100644
--- a/otava/grafana.py
+++ b/otava/grafana.py
@@ -30,20 +30,6 @@ class GrafanaConfig:
     user: str
     password: str
 
-    @staticmethod
-    def add_parser_args(arg_group):
-        arg_group.add_argument("--grafana-url", help="Grafana server URL", 
env_var="GRAFANA_ADDRESS")
-        arg_group.add_argument("--grafana-user", help="Grafana server user", 
env_var="GRAFANA_USER", default="admin")
-        arg_group.add_argument("--grafana-password", help="Grafana server 
password", env_var="GRAFANA_PASSWORD", default="admin")
-
-    @staticmethod
-    def from_parser_args(args):
-        return GrafanaConfig(
-            url=getattr(args, 'grafana_url', None),
-            user=getattr(args, 'grafana_user', None),
-            password=getattr(args, 'grafana_password', None)
-        )
-
 
 @dataclass
 class GrafanaError(Exception):
diff --git a/otava/graphite.py b/otava/graphite.py
index 7c3709d..6536079 100644
--- a/otava/graphite.py
+++ b/otava/graphite.py
@@ -31,16 +31,6 @@ from otava.util import parse_datetime
 class GraphiteConfig:
     url: str
 
-    @staticmethod
-    def add_parser_args(arg_group):
-        arg_group.add_argument("--graphite-url", help="Graphite server URL", 
env_var="GRAPHITE_ADDRESS")
-
-    @staticmethod
-    def from_parser_args(args):
-        return GraphiteConfig(
-            url=getattr(args, 'graphite_url', None)
-        )
-
 
 @dataclass
 class DataPoint:
diff --git a/otava/main.py b/otava/main.py
index 60d6e37..a158f2c 100644
--- a/otava/main.py
+++ b/otava/main.py
@@ -15,6 +15,7 @@
 # specific language governing permissions and limitations
 # under the License.
 
+import argparse
 import copy
 import logging
 import sys
@@ -22,14 +23,13 @@ from dataclasses import dataclass
 from datetime import datetime, timedelta
 from typing import Dict, List, Optional
 
-import configargparse as argparse
 import pytz
 from slack_sdk import WebClient
 
 from otava import config
 from otava.attributes import get_back_links
 from otava.bigquery import BigQuery, BigQueryError
-from otava.config import Config
+from otava.config import Config, ConfigError
 from otava.data_selector import DataSelector
 from otava.grafana import Annotation, Grafana, GrafanaError
 from otava.graphite import GraphiteError
@@ -514,12 +514,19 @@ def analysis_options_from_args(args: argparse.Namespace) 
-> AnalysisOptions:
     return conf
 
 
-def create_otava_cli_parser() -> argparse.ArgumentParser:
-    parser = argparse.ArgumentParser(
-        description="Hunts performance regressions in Fallout results",
-        parents=[config.create_config_parser()],
-        allow_abbrev=False,  # required for correct parsing of nested values 
from config file
-    )
+def main():
+    try:
+        conf = config.load_config()
+    except ConfigError as err:
+        logging.error(err.message)
+        exit(1)
+    script_main(conf)
+
+
+def script_main(conf: Config, args: List[str] = None):
+    logging.basicConfig(format="%(levelname)s: %(message)s", 
level=logging.INFO)
+
+    parser = argparse.ArgumentParser(description="Hunts performance 
regressions in Fallout results")
 
     subparsers = parser.add_subparsers(dest="command")
     list_tests_parser = subparsers.add_parser("list-tests", help="list 
available tests")
@@ -594,17 +601,8 @@ def create_otava_cli_parser() -> argparse.ArgumentParser:
         "validate", help="validates the tests and metrics defined in the 
configuration"
     )
 
-    return parser
-
-
-def script_main(conf: Config = None, args: List[str] = None):
-    logging.basicConfig(format="%(levelname)s: %(message)s", 
level=logging.INFO)
-    parser = create_otava_cli_parser()
-
     try:
-        args, _ = parser.parse_known_args(args=args)
-        if conf is None:
-            conf = config.load_config_from_parser_args(args)
+        args = parser.parse_args(args=args)
         otava = Otava(conf)
 
         if args.command == "list-groups":
@@ -729,9 +727,5 @@ def script_main(conf: Config = None, args: List[str] = 
None):
         exit(1)
 
 
-def main():
-    script_main()
-
-
 if __name__ == "__main__":
     main()
diff --git a/otava/postgres.py b/otava/postgres.py
index d014bb6..6b98c0f 100644
--- a/otava/postgres.py
+++ b/otava/postgres.py
@@ -33,24 +33,6 @@ class PostgresConfig:
     password: str
     database: str
 
-    @staticmethod
-    def add_parser_args(arg_group):
-        arg_group.add_argument("--postgres-hostname", help="PostgreSQL server 
hostname", env_var="POSTGRES_HOSTNAME")
-        arg_group.add_argument("--postgres-port", type=int, help="PostgreSQL 
server port", env_var="POSTGRES_PORT")
-        arg_group.add_argument("--postgres-username", help="PostgreSQL 
username", env_var="POSTGRES_USERNAME")
-        arg_group.add_argument("--postgres-password", help="PostgreSQL 
password", env_var="POSTGRES_PASSWORD")
-        arg_group.add_argument("--postgres-database", help="PostgreSQL 
database name", env_var="POSTGRES_DATABASE")
-
-    @staticmethod
-    def from_parser_args(args):
-        return PostgresConfig(
-            hostname=getattr(args, 'postgres_hostname', None),
-            port=getattr(args, 'postgres_port', None),
-            username=getattr(args, 'postgres_username', None),
-            password=getattr(args, 'postgres_password', None),
-            database=getattr(args, 'postgres_database', None)
-        )
-
 
 @dataclass
 class PostgresError(Exception):
diff --git a/tests/resources/substitution_test_config.yaml 
b/otava/resources/otava.yaml
similarity index 60%
rename from tests/resources/substitution_test_config.yaml
rename to otava/resources/otava.yaml
index 6b6b024..44479f3 100644
--- a/tests/resources/substitution_test_config.yaml
+++ b/otava/resources/otava.yaml
@@ -15,25 +15,34 @@
 # specific language governing permissions and limitations
 # under the License.
 
-slack:
-  token: config_slack_token
-
+# External systems connectors configuration:
 graphite:
-  url: config_graphite_url
+  url: ${GRAPHITE_ADDRESS}
 
 grafana:
-  url: config_grafana_url
-  user: config_grafana_user
-  password: config_grafana_password
+  url: ${GRAFANA_ADDRESS}
+  user: ${GRAFANA_USER}
+  password: ${GRAFANA_PASSWORD}
+
+slack:
+  token: ${SLACK_BOT_TOKEN}
+
+# Templates define common bits shared between test definitions:
+templates:
+
+# Define your tests here:
+tests:
+  local.sample:
+    type: csv
+    file: tests/resources/sample.csv
+    time_column: time
+    metrics: [metric1, metric2]
+    attributes: [commit]
+    csv_options:
+      delimiter: ','
+      quote_char: "'"
 
-bigquery:
-  project_id: config_bigquery_project_id
-  dataset: config_bigquery_dataset
-  credentials: config_bigquery_credentials
 
-postgres:
-  hostname: config_postgres_hostname
-  port: 1111
-  username: config_postgres_username
-  password: config_postgres_password
-  database: config_postgres_database
+test_groups:
+  local:
+    - local.sample
diff --git a/otava/slack.py b/otava/slack.py
index 0d9b751..1cad43f 100644
--- a/otava/slack.py
+++ b/otava/slack.py
@@ -36,20 +36,6 @@ class NotificationError(Exception):
 class SlackConfig:
     bot_token: str
 
-    @staticmethod
-    def add_parser_args(parser):
-        parser.add_argument(
-            "--slack-token",
-            help="Slack bot token to use for sending notifications",
-            env_var="SLACK_BOT_TOKEN",
-        )
-
-    @staticmethod
-    def from_parser_args(args):
-        return SlackConfig(
-            bot_token=getattr(args, "slack_token", None)
-        )
-
 
 class SlackNotification:
     tests_with_insufficient_data: List[str]
diff --git a/pyproject.toml b/pyproject.toml
index 24e3b36..46edadd 100644
--- a/pyproject.toml
+++ b/pyproject.toml
@@ -36,6 +36,7 @@ classifiers = [
 ]
 dependencies = [
     "dateparser>=1.0.0",
+    "expandvars>=0.6.5",
     "numpy==1.24.*",
     "python-dateutil>=2.8.1",
     "signal-processing-algorithms==1.3.5",
@@ -47,7 +48,6 @@ dependencies = [
     "slack-sdk>=3.4.2",
     "google-cloud-bigquery>=3.25.0",
     "pg8000>=1.31.2",
-    "configargparse>=1.7.1",
 ]
 
 [project.optional-dependencies]
diff --git a/tests/config_test.py b/tests/config_test.py
index f1e4a1b..a59e83c 100644
--- a/tests/config_test.py
+++ b/tests/config_test.py
@@ -14,16 +14,15 @@
 # KIND, either express or implied.  See the License for the
 # specific language governing permissions and limitations
 # under the License.
-import os
 
-import pytest
+from pathlib import Path
 
-from otava.config import load_config_from_file
+from otava.config import load_config_from
 from otava.test_config import CsvTestConfig, GraphiteTestConfig, 
HistoStatTestConfig
 
 
 def test_load_graphite_tests():
-    config = load_config_from_file("tests/resources/sample_config.yaml")
+    config = load_config_from(Path("tests/resources/sample_config.yaml"))
     tests = config.tests
     assert len(tests) == 4
     test = tests["remote1"]
@@ -40,7 +39,7 @@ def test_load_graphite_tests():
 
 
 def test_load_csv_tests():
-    config = load_config_from_file("tests/resources/sample_config.yaml")
+    config = load_config_from(Path("tests/resources/sample_config.yaml"))
     tests = config.tests
     assert len(tests) == 4
     test = tests["local1"]
@@ -61,75 +60,17 @@ def test_load_csv_tests():
 
 
 def test_load_test_groups():
-    config = load_config_from_file("tests/resources/sample_config.yaml")
+    config = load_config_from(Path("tests/resources/sample_config.yaml"))
     groups = config.test_groups
     assert len(groups) == 2
     assert len(groups["remote"]) == 2
 
 
 def test_load_histostat_config():
-    config = 
load_config_from_file("tests/resources/histostat_test_config.yaml")
+    config = 
load_config_from(Path("tests/resources/histostat_test_config.yaml"))
     tests = config.tests
     assert len(tests) == 1
     test = tests["histostat-sample"]
     assert isinstance(test, HistoStatTestConfig)
     # 14 tags * 12 tag_metrics == 168 unique metrics
     assert len(test.fully_qualified_metric_names()) == 168
-
-
[email protected](
-    "config_property",
-    [
-        # property, accessor, env_var, cli_flag, [config value, env value, cli 
value]
-        ("slack_token", lambda c: c.slack.bot_token, "SLACK_BOT_TOKEN", 
"--slack-token"),
-        ("bigquery_project_id", lambda c: c.bigquery.project_id, 
"BIGQUERY_PROJECT_ID", "--bigquery-project-id"),
-        ("bigquery_dataset", lambda c: c.bigquery.dataset, "BIGQUERY_DATASET", 
"--bigquery-dataset"),
-        ("bigquery_credentials", lambda c: c.bigquery.credentials, 
"BIGQUERY_VAULT_SECRET", "--bigquery-credentials"),
-        ("grafana_url", lambda c: c.grafana.url, "GRAFANA_ADDRESS", 
"--grafana-url"),
-        ("grafana_user", lambda c: c.grafana.user, "GRAFANA_USER", 
"--grafana-user"),
-        ("grafana_password", lambda c: c.grafana.password, "GRAFANA_PASSWORD", 
"--grafana-password"),
-        ("graphite_url", lambda c: c.graphite.url, "GRAPHITE_ADDRESS", 
"--graphite-url"),
-        ("postgres_hostname", lambda c: c.postgres.hostname, 
"POSTGRES_HOSTNAME", "--postgres-hostname"),
-        ("postgres_port", lambda c: c.postgres.port, "POSTGRES_PORT", 
"--postgres-port", 1111, 2222, 3333),
-        ("postgres_username", lambda c: c.postgres.username, 
"POSTGRES_USERNAME", "--postgres-username"),
-        ("postgres_password", lambda c: c.postgres.password, 
"POSTGRES_PASSWORD", "--postgres-password"),
-        ("postgres_database", lambda c: c.postgres.database, 
"POSTGRES_DATABASE", "--postgres-database"),
-    ],
-    ids=lambda v: v[0],  # use the property name for the parameterized test 
name
-)
-def test_configuration_substitutions(config_property):
-    config_file = "tests/resources/substitution_test_config.yaml"
-    accessor = config_property[1]
-
-    if len(config_property) == 4:
-        config_value = f"config_{config_property[0]}"
-        env_config_value = f"env_{config_property[0]}"
-        cli_config_value = f"cli_{config_property[0]}"
-    else:
-        config_value = config_property[4]
-        env_config_value = config_property[5]
-        cli_config_value = config_property[6]
-
-    # test value from config file
-    config = load_config_from_file(config_file)
-    assert accessor(config) == config_value
-
-    # test env var overrides values from config file
-    os.environ[config_property[2]] = str(env_config_value)
-    try:
-        config = load_config_from_file(config_file)
-        assert accessor(config) == env_config_value
-    finally:
-        os.environ.pop(config_property[2])
-
-    # test cli values override values from config file
-    config = load_config_from_file(config_file, 
arg_overrides=[config_property[3], str(cli_config_value)])
-    assert accessor(config) == cli_config_value
-
-    # test cli values override values from config file and env var
-    os.environ[config_property[2]] = str(env_config_value)
-    try:
-        config = load_config_from_file(config_file, 
arg_overrides=[config_property[3], str(cli_config_value)])
-        assert accessor(config) == cli_config_value
-    finally:
-        os.environ.pop(config_property[2])
diff --git a/uv.lock b/uv.lock
index a5beddf..fb4d970 100644
--- a/uv.lock
+++ b/uv.lock
@@ -1,5 +1,5 @@
 version = 1
-revision = 3
+revision = 2
 requires-python = ">=3.8, <3.11"
 resolution-markers = [
     "python_full_version >= '3.10'",
@@ -13,8 +13,8 @@ name = "apache-otava"
 version = "0.6.1"
 source = { editable = "." }
 dependencies = [
-    { name = "configargparse" },
     { name = "dateparser" },
+    { name = "expandvars" },
     { name = "google-cloud-bigquery", version = "3.30.0", source = { registry 
= "https://pypi.org/simple"; }, marker = "python_full_version < '3.9'" },
     { name = "google-cloud-bigquery", version = "3.35.1", source = { registry 
= "https://pypi.org/simple"; }, marker = "python_full_version >= '3.9'" },
     { name = "numpy" },
@@ -53,8 +53,8 @@ dev = [
 [package.metadata]
 requires-dist = [
     { name = "autoflake", marker = "extra == 'dev'", specifier = ">=1.4" },
-    { name = "configargparse", specifier = ">=1.7.1" },
     { name = "dateparser", specifier = ">=1.0.0" },
+    { name = "expandvars", specifier = ">=0.6.5" },
     { name = "flake8", marker = "extra == 'dev'", specifier = ">=4.0.1" },
     { name = "google-cloud-bigquery", specifier = ">=3.25.0" },
     { name = "isort", marker = "extra == 'dev'", specifier = ">=5.10.1" },
@@ -218,15 +218,6 @@ wheels = [
     { url = 
"https://files.pythonhosted.org/packages/d1/d6/3965ed04c63042e047cb6a3e6ed1a63a35087b6a609aa3a15ed8ac56c221/colorama-0.4.6-py2.py3-none-any.whl";,
 hash = 
"sha256:4f1d9991f5acc0ca119f9d443620b77f9d6b33703e51011c16baf57afb285fc6", size 
= 25335, upload-time = "2022-10-25T02:36:20.889Z" },
 ]
 
-[[package]]
-name = "configargparse"
-version = "1.7.1"
-source = { registry = "https://pypi.org/simple"; }
-sdist = { url = 
"https://files.pythonhosted.org/packages/85/4d/6c9ef746dfcc2a32e26f3860bb4a011c008c392b83eabdfb598d1a8bbe5d/configargparse-1.7.1.tar.gz";,
 hash = 
"sha256:79c2ddae836a1e5914b71d58e4b9adbd9f7779d4e6351a637b7d2d9b6c46d3d9", size 
= 43958, upload-time = "2025-05-23T14:26:17.369Z" }
-wheels = [
-    { url = 
"https://files.pythonhosted.org/packages/31/28/d28211d29bcc3620b1fece85a65ce5bb22f18670a03cd28ea4b75ede270c/configargparse-1.7.1-py3-none-any.whl";,
 hash = 
"sha256:8b586a31f9d873abd1ca527ffbe58863c99f36d896e2829779803125e83be4b6", size 
= 25607, upload-time = "2025-05-23T14:26:15.923Z" },
-]
-
 [[package]]
 name = "dateparser"
 version = "1.2.0"
@@ -261,6 +252,15 @@ wheels = [
     { url = 
"https://files.pythonhosted.org/packages/02/cc/b7e31358aac6ed1ef2bb790a9746ac2c69bcb3c8588b41616914eb106eaf/exceptiongroup-1.2.2-py3-none-any.whl";,
 hash = 
"sha256:3111b9d131c238bec2f8f516e123e14ba243563fb135d3fe885990585aa7795b", size 
= 16453, upload-time = "2024-07-12T22:25:58.476Z" },
 ]
 
+[[package]]
+name = "expandvars"
+version = "1.1.1"
+source = { registry = "https://pypi.org/simple"; }
+sdist = { url = 
"https://files.pythonhosted.org/packages/dc/c9/c0a46f462058446aafe953bf76a957c17f78550216a95fbded2270f83117/expandvars-1.1.1.tar.gz";,
 hash = 
"sha256:98add8268b760dfee457bde1c17bf745795fdebc22b7ddab75fd3278653f1e05", size 
= 70787, upload-time = "2025-07-12T07:46:22.308Z" }
+wheels = [
+    { url = 
"https://files.pythonhosted.org/packages/2b/ca/0753ba3a81255ac49748ec8b665ab01f8efcf711f74bbccb5457a6193acc/expandvars-1.1.1-py3-none-any.whl";,
 hash = 
"sha256:09ca39e6bfcb0d899db8778a00dd3d89cfeb0080795c54f16f6279afd0ef8c5b", size 
= 7522, upload-time = "2025-07-12T07:46:18.984Z" },
+]
+
 [[package]]
 name = "filelock"
 version = "3.16.1"
@@ -707,7 +707,7 @@ resolution-markers = [
     "python_full_version == '3.9.*'",
 ]
 dependencies = [
-    { name = "zipp", version = "3.23.0", source = { registry = 
"https://pypi.org/simple"; }, marker = "python_full_version == '3.9.*'" },
+    { name = "zipp", version = "3.23.0", source = { registry = 
"https://pypi.org/simple"; }, marker = "python_full_version >= '3.9'" },
 ]
 sdist = { url = 
"https://files.pythonhosted.org/packages/76/66/650a33bd90f786193e4de4b3ad86ea60b53c89b669a5c7be931fac31cdb0/importlib_metadata-8.7.0.tar.gz";,
 hash = 
"sha256:d13b81ad223b890aa16c5471f2ac3056cf76c5f10f82d6f9292f0b415f389000", size 
= 56641, upload-time = "2025-04-27T15:29:01.736Z" }
 wheels = [

Reply via email to