Repository: dbt-labs/dbt-core

Issue:
[CT-898] [Feature] Make entire dbt invocation command available to Jinja context
### Is this your first time opening an issue?

- [X] I have read the [expectations for open source contributors](https://docs.getdbt.com/docs/contributing/oss-expectations)

### Describe the Feature

Right now `flags.WHICH` can be used to return the top-level command, but it would be great to surface the entire command string of args, flags, vars etc. The use case here is the dbt_artifacts package's https://github.com/brooklyn-data/dbt_artifacts/blob/main/models/fct_dbt__invocations.sql model.

Even better would be to also make each section of the command accessible separately, such as the selector, any args, flags etc.

### Describe alternatives you've considered

_No response_

### Who will this benefit?

_No response_

### Are you interested in contributing this feature?

_No response_

### Anything else?

_No response_


Discussion:
I think what we want here is just good old `args_to_dict`:

https://github.com/dbt-labs/dbt-core/blob/94a7cfa58df3df5d3b07b619c0510fead9195700/core/dbt/utils.py#L630-L636

That's what we use in the [`MainReportArgs` event](https://github.com/dbt-labs/dbt-core/blob/94a7cfa58df3df5d3b07b619c0510fead9195700/core/dbt/main.py#L230), and it's also what we use [when writing `run_results.json`](https://github.com/dbt-labs/dbt-core/blob/94a7cfa58df3df5d3b07b619c0510fead9195700/core/dbt/task/runnable.py#L585). It also captures the values of "global" configs set via env var or user config (`profiles.yml`), instead of CLI flags.

I think this could quite naturally land as a property or method in the [`ProviderContext`](https://github.com/dbt-labs/dbt-core/blob/94a7cfa58df3df5d3b07b619c0510fead9195700/core/dbt/context/providers.py#L678):
```python
    @contextproperty
    def invocation_args_dict(self):
        from dbt.utils import args_to_dict  # not here, of course
        return args_to_dict(self.config.args)
```
```
select {{ invocation_args_dict }}
```

Note that the exact structure of the args dict is _definitely_ liable to change, as we rework our CLI to use `click` instead of `argparse`: https://github.com/dbt-labs/dbt-core/issues/5526.
@jtcohen6 I've had a go at implementing this and I'm at the stage of trying to add the required tests. 

Are you able to provide some direction on how tests for the ProviderContext should be implemented?


Resolving patch:
diff --git a/core/dbt/context/providers.py b/core/dbt/context/providers.py
--- a/core/dbt/context/providers.py
+++ b/core/dbt/context/providers.py
@@ -63,7 +63,7 @@
 from dbt.config import IsFQNResource
 from dbt.node_types import NodeType, ModelLanguage
 
-from dbt.utils import merge, AttrDict, MultiDict
+from dbt.utils import merge, AttrDict, MultiDict, args_to_dict
 
 from dbt import selected_resources
 
@@ -710,6 +710,10 @@ def _get_namespace_builder(self):
             self.model,
         )
 
+    @contextproperty
+    def invocation_args_dict(self):
+        return args_to_dict(self.config.args)
+
     @contextproperty
     def _sql_results(self) -> Dict[str, AttrDict]:
         return self.sql_results


Test patch:
diff --git a/test/unit/test_context.py b/test/unit/test_context.py
--- a/test/unit/test_context.py
+++ b/test/unit/test_context.py
@@ -238,6 +238,7 @@ def assert_has_keys(required_keys: Set[str], maybe_keys: Set[str], ctx: Dict[str
     "sql_now",
     "adapter_macro",
     "selected_resources",
+    "invocation_args_dict",
 }
 REQUIRED_MODEL_KEYS = REQUIRED_MACRO_KEYS | {"this", "compiled_code",}
 MAYBE_KEYS = frozenset({"debug"})
@@ -417,6 +418,22 @@ def test_macro_runtime_context(config_postgres, manifest_fx, get_adapter, get_in
     )
     assert_has_keys(REQUIRED_MACRO_KEYS, MAYBE_KEYS, ctx)
 
+def test_invocation_args_to_dict_in_macro_runtime_context(
+    config_postgres, manifest_fx, get_adapter, get_include_paths
+):
+    ctx = providers.generate_runtime_macro_context(
+        macro=manifest_fx.macros["macro.root.macro_a"],
+        config=config_postgres,
+        manifest=manifest_fx,
+        package_name="root",
+    )
+
+    # Comes from dbt/flags.py as they are the only values set that aren't None at default
+    assert ctx["invocation_args_dict"]["event_buffer_size"] == 100000
+    assert ctx["invocation_args_dict"]["printer_width"] == 80
+
+    # Comes from unit/utils.py config_from_parts_or_dicts method
+    assert ctx["invocation_args_dict"]["profile_dir"] == "/dev/null"
 
 def test_model_parse_context(config_postgres, manifest_fx, get_adapter, get_include_paths):
     ctx = providers.generate_parser_model_context(
diff --git a/tests/functional/context_methods/test_builtin_functions.py b/tests/functional/context_methods/test_builtin_functions.py
--- a/tests/functional/context_methods/test_builtin_functions.py
+++ b/tests/functional/context_methods/test_builtin_functions.py
@@ -1,4 +1,5 @@
 import pytest
+import json
 
 from dbt.tests.util import run_dbt, run_dbt_and_capture, write_file
 from dbt.exceptions import CompilationException
@@ -23,6 +24,12 @@
 {% endmacro %}
 """
 
+macros__validate_invocation_sql = """
+{% macro validate_invocation(my_variable) %}
+    {{ log("invocation_result: "~ invocation_args_dict) }}
+{% endmacro %}
+"""
+
 models__set_exception_sql = """
 {% set set_strict_result = set_strict(1) %}
 """
@@ -38,6 +45,7 @@ def macros(self):
         return {
             "validate_set.sql": macros__validate_set_sql,
             "validate_zip.sql": macros__validate_zip_sql,
+            "validate_invocation.sql": macros__validate_invocation_sql,
         }
 
     def test_builtin_set_function(self, project):
@@ -55,6 +63,46 @@ def test_builtin_zip_function(self, project):
         assert f"zip_result: {expected_zip}" in log_output
         assert f"zip_strict_result: {expected_zip}" in log_output
 
+    def test_builtin_invocation_args_dict_function(self, project):
+        _, log_output = run_dbt_and_capture(
+            [
+                "--debug",
+                "--log-format=json",
+                "run-operation",
+                "validate_invocation",
+                "--args",
+                "{my_variable: test_variable}",
+            ]
+        )
+
+        parsed_logs = []
+        for line in log_output.split("\n"):
+            try:
+                log = json.loads(line)
+            except ValueError:
+                continue
+
+            parsed_logs.append(log)
+
+        # Value of msg is a string
+        result = next(
+            (
+                item
+                for item in parsed_logs
+                if "invocation_result" in item["data"].get("msg", "msg")
+            ),
+            False,
+        )
+
+        assert result
+
+        # Result is checked in two parts because profiles_dir is unique each test run
+        expected = "invocation_result: {'debug': True, 'log_format': 'json', 'write_json': True, 'use_colors': True, 'printer_width': 80, 'version_check': True, 'partial_parse': True, 'static_parser': True, 'profiles_dir': "
+        assert expected in str(result)
+
+        expected = "'send_anonymous_usage_stats': False, 'event_buffer_size': 100000, 'quiet': False, 'no_print': False, 'macro': 'validate_invocation', 'args': '{my_variable: test_variable}', 'which': 'run-operation', 'rpc_method': 'run-operation', 'indirect_selection': 'eager'}"
+        assert expected in str(result)
+
 
 class TestContextBuiltinExceptions:
     # Assert compilation errors are raised with _strict equivalents