Repository: PrefectHQ/prefect
Pull request: include integration info in `prefect version`
closes https://github.com/PrefectHQ/prefect/issues/14533

includes installed packages named `prefect-*` that are published by us

```console
(prefect) nate :: ~/github.com/prefecthq/prefect ‹main*›
» prefect version
Version:             3.0.0rc10+37.g3c36b3fe90.dirty
API version:         0.8.4
Python version:      3.12.3
Git commit:          3c36b3fe
Built:               Wed, Jul 10, 2024 4:52 PM
OS/Arch:             darwin/arm64
Profile:             staging
Server type:         cloud
Pydantic version:    2.8.2
Integrations:
  prefect-aws:       0.5.0rc2
  prefect-docker:    0.6.0rc3

» pip list | rg prefect-
prefect-aws                0.5.0rc2
prefect-docker             0.6.0rc3
```
---

just in case it matters to someone, i kept a flag to turn this off

```console
» prefect version --omit-integrations
Version:             3.0.0rc10+38.g1c671668c9
API version:         0.8.4
Python version:      3.12.3
Git commit:          1c671668
Built:               Wed, Jul 10, 2024 4:50 PM
OS/Arch:             darwin/arm64
Profile:             staging
Server type:         cloud
Pydantic version:    2.8.2
```


Problem statement:
integration information in `prefect version`
### First check

- [X] I added a descriptive title to this issue.
- [X] I used the GitHub search to find a similar request and didn't find it.
- [X] I searched the Prefect documentation for this feature.

### Prefect Version

3.x

### Describe the current behavior

`prefect version` only returns information about Prefect, not anything about any integrations being used.

### Describe the proposed behavior

Adding integrations and their respective versions to the output might help resolve some issues faster.

### Example Use

_No response_

### Additional context

_No response_

Hints from issue discussion:

Commit message: Merge branch 'main' into integration-versions

Patch to review:
diff --git a/src/prefect/cli/root.py b/src/prefect/cli/root.py
index 9c994de174d3..d01afe5d730e 100644
--- a/src/prefect/cli/root.py
+++ b/src/prefect/cli/root.py
@@ -5,6 +5,9 @@
 import asyncio
 import platform
 import sys
+from importlib.metadata import PackageNotFoundError
+from importlib.metadata import version as import_version
+from typing import Any, Dict
 
 import pendulum
 import typer
@@ -92,10 +95,13 @@ def main(
 
 
 @app.command()
-async def version():
-    """Get the current Prefect version."""
+async def version(
+    omit_integrations: bool = typer.Option(
+        False, "--omit-integrations", help="Omit integration information"
+    ),
+):
+    """Get the current Prefect version and integration information."""
     import sqlite3
-    from importlib.metadata import PackageNotFoundError, version
 
     from prefect.server.utilities.database import get_dialect
     from prefect.settings import PREFECT_API_DATABASE_CONNECTION_URL
@@ -115,8 +121,6 @@ async def version():
     server_type: str
 
     try:
-        # We do not context manage the client because when using an ephemeral app we do not
-        # want to create the database or run migrations
         client = prefect.get_client()
         server_type = client.server_type.value
     except Exception:
@@ -125,27 +129,47 @@ async def version():
     version_info["Server type"] = server_type.lower()
 
     try:
-        pydantic_version = version("pydantic")
+        pydantic_version = import_version("pydantic")
     except PackageNotFoundError:
         pydantic_version = "Not installed"
 
     version_info["Pydantic version"] = pydantic_version
 
-    # TODO: Consider adding an API route to retrieve this information?
     if server_type == ServerType.EPHEMERAL.value:
         database = get_dialect(PREFECT_API_DATABASE_CONNECTION_URL.value()).name
         version_info["Server"] = {"Database": database}
         if database == "sqlite":
             version_info["Server"]["SQLite version"] = sqlite3.sqlite_version
 
-    def display(object: dict, nesting: int = 0):
-        # Recursive display of a dictionary with nesting
-        for key, value in object.items():
-            key += ":"
-            if isinstance(value, dict):
-                app.console.print(key)
-                return display(value, nesting + 2)
-            prefix = " " * nesting
-            app.console.print(f"{prefix}{key.ljust(20 - len(prefix))} {value}")
+    if not omit_integrations:
+        integrations = get_prefect_integrations()
+        if integrations:
+            version_info["Integrations"] = integrations
 
     display(version_info)
+
+
+def get_prefect_integrations() -> Dict[str, str]:
+    """Get information about installed Prefect integrations."""
+    from importlib.metadata import distributions
+
+    integrations = {}
+    for dist in distributions():
+        if dist.metadata["Name"].startswith(
+            "prefect-"
+        ) and "prefect.io" in dist.metadata.get("Author-email", ""):
+            integrations[dist.metadata["Name"]] = dist.version
+
+    return integrations
+
+
+def display(object: Dict[str, Any], nesting: int = 0):
+    """Recursive display of a dictionary with nesting."""
+    for key, value in object.items():
+        key += ":"
+        if isinstance(value, dict):
+            app.console.print(" " * nesting + key)
+            display(value, nesting + 2)
+        else:
+            prefix = " " * nesting
+            app.console.print(f"{prefix}{key.ljust(20 - len(prefix))} {value}")


Code review comments:
diff_hunk: @@ -125,27 +129,47 @@
     version_info["Server type"] = server_type.lower()
 
     try:
-        pydantic_version = version("pydantic")
+        pydantic_version = import_version("pydantic")
     except PackageNotFoundError:
         pydantic_version = "Not installed"
 
     version_info["Pydantic version"] = pydantic_version
 
-    # TODO: Consider adding an API route to retrieve this information?
     if server_type == ServerType.EPHEMERAL.value:
         database = get_dialect(PREFECT_API_DATABASE_CONNECTION_URL.value()).name
         version_info["Server"] = {"Database": database}
         if database == "sqlite":
             version_info["Server"]["SQLite version"] = sqlite3.sqlite_version
 
-    def display(object: dict, nesting: int = 0):
-        # Recursive display of a dictionary with nesting
-        for key, value in object.items():
-            key += ":"
-            if isinstance(value, dict):
-                app.console.print(key)
-                return display(value, nesting + 2)
-            prefix = " " * nesting
-            app.console.print(f"{prefix}{key.ljust(20 - len(prefix))} {value}")
+    if not omit_integrations:
+        integrations = get_prefect_integrations()
+        if integrations:
+            version_info["Integrations"] = integrations
 
     display(version_info)
+
+
+def get_prefect_integrations() -> Dict[str, str]:
+    """Get information about installed Prefect integrations."""
+    from importlib.metadata import distributions
+
+    integrations = {}
+    for dist in distributions():
+        if dist.metadata["Name"].startswith(
+            "prefect-"
+        ) and "prefect.io" in dist.metadata.get("Author-email", ""):
path: src/prefect/cli/root.py
text: @user1:
## Incomplete URL substring sanitization

The string [prefect.io](1) may be at an arbitrary position in the sanitized URL.

[Show more details](https://github.com/PrefectHQ/prefect/security/code-scanning/2019)

@author:
resolved by [bfc1b2e](https://github.com/PrefectHQ/prefect/pull/14557/commits/bfc1b2ef1aea0c38974e8e7db55a105263e17563)