Repository: apache/airflow

Issue:
Need an REST API or/and Airflow CLI to fetch last parsed time of a given DAG
### Description


We need to access the time at which a given DAG was parsed last.

Airflow Version : 2.2.2 and above.

### Use case/motivation

End users want to run a given DAG post applying the changes they have done on them. This would mean that the DAG should be parsed post the edits done to it. Right now the last parsed time is available by accessing the airflow database only. Querying the database directly is not the best solution to the problem. Ideally airflow should be exposing APIs that end users can consume that can help provide the last parsed time for a given DAG.

### Related issues

Not Aware.

### Are you willing to submit a PR?

- [ ] Yes I am willing to submit a PR!

### Code of Conduct

- [X] I agree to follow this project's [Code of Conduct](https://github.com/apache/airflow/blob/main/CODE_OF_CONDUCT.md)



Discussion:
Thanks for opening your first issue here! Be sure to follow the issue template! If you are willing to raise PR to address this issue please do so, no need to wait for approval.

If this is a legit requirement, I'd be willing to submit a PR. Please /assign.
@maahir22 - this definitely is a legit requirement, especially in 2 cases - 1/ users who update and trigger DAGs programmatically, and are looking to confirm that triggering DAG now will include the latest changes. 2/ users who run multiple Airflow environments to accommodate their DAGs and build a simple UI for tracking across all environments.   
Correct me if I am wrong, since I am new to this repo - doesn't the end-point GET https://airflow.apache.org/api/v1/dags/{dag_id} provides last_parsed_time for a DAG in the response field? Or is this referring to something else.

Refer this:
{
  "dag_id": "string",
  "root_dag_id": "string",
  "is_paused": true,
  "is_active": true,
  "is_subdag": true,
  "**last_parsed_time**": "2019-08-24T14:15:22Z",
  "last_pickled": "2019-08-24T14:15:22Z",
  "last_expired": "2019-08-24T14:15:22Z",
  "scheduler_lock": true,
  "pickle_id": "string",
  "default_view": "string",
  "fileloc": "string",
  "file_token": "string",
  "owners": [
    "string"
  ],
  "description": "string",
  ....
}
You're right! Thanks for pointing it out, I missed that. REST API does provide it, but the CLI doesn't. It would be great to add it to the CLI. However, I can't really find where in CLI it can fit. May be `dags list` can have option to include last_parsed_time? 
The REST API part already exists, but I think adding it to the CLI through a new command `airflow dags details <dag id>` which returns all the dag details (as https://airflow.apache.org/api/v1/dags/{dag_id}) could be a nice new feature.
@maahir22 assigned you

Resolving patch:
diff --git a/airflow/cli/cli_config.py b/airflow/cli/cli_config.py
--- a/airflow/cli/cli_config.py
+++ b/airflow/cli/cli_config.py
@@ -1101,6 +1101,12 @@ class GroupCommand(NamedTuple):
 CLICommand = Union[ActionCommand, GroupCommand]
 
 DAGS_COMMANDS = (
+    ActionCommand(
+        name="details",
+        help="Get DAG details given a DAG id",
+        func=lazy_load_command("airflow.cli.commands.dag_command.dag_details"),
+        args=(ARG_DAG_ID, ARG_OUTPUT, ARG_VERBOSE),
+    ),
     ActionCommand(
         name="list",
         help="List all the DAGs",
diff --git a/airflow/cli/commands/dag_command.py b/airflow/cli/commands/dag_command.py
--- a/airflow/cli/commands/dag_command.py
+++ b/airflow/cli/commands/dag_command.py
@@ -32,6 +32,7 @@
 
 from airflow import settings
 from airflow.api.client import get_current_api_client
+from airflow.api_connexion.schemas.dag_schema import dag_schema
 from airflow.cli.simple_table import AirflowConsole
 from airflow.configuration import conf
 from airflow.exceptions import AirflowException, RemovedInAirflow3Warning
@@ -346,6 +347,27 @@ def dag_list_dags(args) -> None:
     )
 
 
+@cli_utils.action_cli
+@suppress_logs_and_warning
+@provide_session
+def dag_details(args, session=NEW_SESSION):
+    """Get DAG details given a DAG id"""
+    dag = DagModel.get_dagmodel(args.dag_id, session=session)
+    if not dag:
+        raise SystemExit(f"DAG: {args.dag_id} does not exist in 'dag' table")
+    dag_detail = dag_schema.dump(dag)
+
+    if args.output in ["table", "plain"]:
+        data = [{"property_name": key, "property_value": value} for key, value in dag_detail.items()]
+    else:
+        data = [dag_detail]
+
+    AirflowConsole().print_as(
+        data=data,
+        output=args.output,
+    )
+
+
 @cli_utils.action_cli
 @suppress_logs_and_warning
 def dag_list_import_errors(args) -> None:


Test patch:
diff --git a/tests/cli/commands/test_dag_command.py b/tests/cli/commands/test_dag_command.py
--- a/tests/cli/commands/test_dag_command.py
+++ b/tests/cli/commands/test_dag_command.py
@@ -31,6 +31,7 @@
 import time_machine
 
 from airflow import settings
+from airflow.api_connexion.schemas.dag_schema import DAGSchema
 from airflow.cli import cli_parser
 from airflow.cli.commands import dag_command
 from airflow.exceptions import AirflowException
@@ -470,6 +471,25 @@ def test_cli_report(self):
         assert "airflow/example_dags/example_complex.py" in out
         assert "example_complex" in out
 
+    @conf_vars({("core", "load_examples"): "true"})
+    def test_cli_get_dag_details(self):
+        args = self.parser.parse_args(["dags", "details", "example_complex", "--output", "yaml"])
+        with contextlib.redirect_stdout(io.StringIO()) as temp_stdout:
+            dag_command.dag_details(args)
+            out = temp_stdout.getvalue()
+
+        dag_detail_fields = DAGSchema().fields.keys()
+
+        # Check if DAG Details field are present
+        for field in dag_detail_fields:
+            assert field in out
+
+        # Check if identifying values are present
+        dag_details_values = ["airflow", "airflow/example_dags/example_complex.py", "16", "example_complex"]
+
+        for value in dag_details_values:
+            assert value in out
+
     @conf_vars({("core", "load_examples"): "true"})
     def test_cli_list_dags(self):
         args = self.parser.parse_args(["dags", "list", "--output", "yaml"])