Repository: gabrieldemarmiesse/python-on-whales (language: python)

Issue:
docker compose ls -> docker.compose.ls()?
AFAIK there isn't a method to handle `docker compose ls` CLI command. Just wondering if there's anything in the works to add support for this.


Discussion / hints:
At first I thought that it was equivalent to `docker.ps()` but it seems that it filters per compose project. Thus it makes sense to implement it. I'll do it when I have time. If you wish you can open a pull request too :) 
@gabrieldemarmiesse thanks for the response. I can get a PR going sometime this week
That would be awesome, thanks!

Fix patch:
diff --git a/python_on_whales/components/compose/cli_wrapper.py b/python_on_whales/components/compose/cli_wrapper.py
index 2159c822..f2b5ec5b 100644
--- a/python_on_whales/components/compose/cli_wrapper.py
+++ b/python_on_whales/components/compose/cli_wrapper.py
@@ -7,7 +7,7 @@
 
 import python_on_whales.components.container.cli_wrapper
 from python_on_whales.client_config import DockerCLICaller
-from python_on_whales.components.compose.models import ComposeConfig
+from python_on_whales.components.compose.models import ComposeConfig, ComposeProject
 from python_on_whales.utils import (
     format_dict_for_cli,
     run,
@@ -338,6 +338,36 @@ def ps(self) -> List[python_on_whales.components.container.cli_wrapper.Container
         Container = python_on_whales.components.container.cli_wrapper.Container
         return [Container(self.client_config, x, is_immutable_id=True) for x in ids]
 
+    def ls(
+        self, all: bool = False, filters: Dict[str, str] = {}
+    ) -> List[ComposeProject]:
+        """Returns a list of docker compose projects
+
+        # Arguments
+            all_stopped: Results include all stopped compose projects.
+            project_filters: Filter results based on conditions provided.
+
+        # Returns
+            A `List[python_on_whales.ComposeProject]`
+        """
+        full_cmd = self.docker_compose_cmd + ["ls", "--format", "json"]
+        full_cmd.add_flag("--all", all)
+        full_cmd.add_args_list("--filter", format_dict_for_cli(filters))
+        return [
+            ComposeProject(
+                name=proj["Name"],
+                status=proj["Status"].split("(")[0],
+                count=int(proj["Status"].split("(")[1].strip(")")),
+                config_files=[
+                    Path(path)
+                    for path in proj.get("ConfigFiles", "").split(",")
+                    if "ConfigFiles" in proj
+                ]
+                or None,
+            )
+            for proj in json.loads(run(full_cmd))
+        ]
+
     def pull(
         self,
         services: Union[List[str], str, None] = None,
diff --git a/python_on_whales/components/compose/models.py b/python_on_whales/components/compose/models.py
index e15a5bc5..006636cd 100644
--- a/python_on_whales/components/compose/models.py
+++ b/python_on_whales/components/compose/models.py
@@ -118,3 +118,10 @@ class ComposeConfig(BaseModel):
     volumes: Dict[str, ComposeConfigVolume] = Field(default_factory=dict)
     configs: Any
     secrets: Any
+
+
+class ComposeProject(BaseModel):
+    name: str
+    status: str
+    count: int
+    config_files: Optional[List[Path]]


Test patch:
diff --git a/tests/python_on_whales/components/test_compose.py b/tests/python_on_whales/components/test_compose.py
index 178452d8..bcca7054 100644
--- a/tests/python_on_whales/components/test_compose.py
+++ b/tests/python_on_whales/components/test_compose.py
@@ -722,3 +722,29 @@ def test_compose_port():
         assert e.args == ValueError("Private port cannot be empty").args
 
     d.compose.down(timeout=1)
+
+
+def test_compose_ls_project_running():
+    d = DockerClient(
+        compose_files=[
+            PROJECT_ROOT / "tests/python_on_whales/components/dummy_compose.yml",
+        ],
+        compose_compatibility=True,
+        compose_project_name="test_compose_ls",
+    )
+    d.compose.up(["busybox"], detach=True)
+    time.sleep(2)
+
+    projects = d.compose.ls()
+    project = [
+        proj
+        for proj in projects
+        if proj.name == d.compose.client_config.compose_project_name
+    ][0]
+
+    assert project.status == "running"
+    assert project.count == 1
+    if project.config_files:
+        assert sorted(project.config_files) == sorted(d.client_config.compose_files)
+
+    d.compose.down(timeout=1)