From 644001cdb5e84cb65837f04877494d7955c5fae4 Mon Sep 17 00:00:00 2001 From: Mohcine Tor Date: Wed, 4 Feb 2026 16:54:47 +0100 Subject: [PATCH 1/5] feat: show state both local & remote --- Babylon/commands/namespace/get_all_states.py | 46 +++++++++++++++----- Babylon/utils/environment.py | 12 +++++ 2 files changed, 47 insertions(+), 11 deletions(-) diff --git a/Babylon/commands/namespace/get_all_states.py b/Babylon/commands/namespace/get_all_states.py index 55a309f4..adbb68e1 100644 --- a/Babylon/commands/namespace/get_all_states.py +++ b/Babylon/commands/namespace/get_all_states.py @@ -1,6 +1,6 @@ from logging import getLogger -from click import command +from click import command, argument, Choice, echo, style from Babylon.utils.environment import Environment from Babylon.utils.response import CommandResponse @@ -10,13 +10,37 @@ @command() -def get_states() -> CommandResponse: - """Display all states in your local machine""" - states_dir = env.state_dir - if not env.state_dir.exists(): - logger.error(f"directory {env.state_dir} not found") - return CommandResponse.fail() - states_files = sorted(states_dir.glob("state.*.yaml")) - for f in states_files: - print(f" {f.name}") - return CommandResponse.success() +@argument("target", type=Choice(["local", "remote"], case_sensitive=False)) +def get_states(target: str) -> CommandResponse: + """Display states from local machine or Azure remote storage.""" + + results_found = False + if target == "local": + echo(style("\n 📂 Local States", bold=True, fg="cyan")) + states_dir = env.state_dir + + if not states_dir.exists(): + logger.error(f" [bold red]✘[/bold red] Directory not found: [dim]{states_dir}[/dim]") + else: + local_files = sorted(states_dir.glob("state.*.yaml")) + if not local_files: + logger.warning(" [yellow]⚠[/yellow] No local state files found") + else: + for f in local_files: + echo(style(" • ", fg="green") + f.name) + results_found = True + + elif target == "remote": + echo(style("\n ☁️ Remote States", bold=True, fg="cyan")) + try: + remote_files = env.list_remote_states() + if not remote_files: + logger.warning(" [yellow]⚠[/yellow] No remote states found on Azure") + else: + for name in sorted(remote_files): + echo(style(" • ", fg="green") + name) + results_found = True + except Exception as e: + logger.error(f" [bold red]✘[/bold red] Failed to reach Azure storage: {e}") + + return CommandResponse.success() if results_found else CommandResponse.fail() \ No newline at end of file diff --git a/Babylon/utils/environment.py b/Babylon/utils/environment.py index 4fc0377d..c51bb431 100644 --- a/Babylon/utils/environment.py +++ b/Babylon/utils/environment.py @@ -187,6 +187,18 @@ def store_state_in_cloud(self, state: dict): state_blob.delete_blob() state_blob.upload_blob(data=dump(state).encode("utf-8")) + def list_remote_states(self) -> list[str]: + """Liste les noms des fichiers de state présents dans le container Azure.""" + try: + self.set_blob_client() + container_client = self.blob_client.get_container_client(container="babylon-states") + # On filtre pour ne prendre que les fichiers state.*.yaml + blobs = container_client.list_blobs(name_starts_with="state.") + return [b.name for b in blobs if b.name.endswith(".yaml")] + except Exception as e: + logger.error(f" [bold red]✘[/bold red] Impossible de lister les states distants: {e}") + return [] + def get_state_from_local(self): state_file = self.state_dir / f"state.{self.context_id}.{self.environ_id}.{self.state_id}.yaml" if not state_file.exists(): From 2e73686e3688a02c46290cff027023c72ea303e4 Mon Sep 17 00:00:00 2001 From: Mohcine Tor Date: Wed, 4 Feb 2026 18:02:10 +0100 Subject: [PATCH 2/5] fix: unit & e2e tests --- tests/unit/test_macro.py | 8 +------- 1 file changed, 1 insertion(+), 7 deletions(-) diff --git a/tests/unit/test_macro.py b/tests/unit/test_macro.py index 8714b1ce..e0f2c6cb 100644 --- a/tests/unit/test_macro.py +++ b/tests/unit/test_macro.py @@ -75,13 +75,7 @@ def test_resolve_inclusion_exclusion_exclude_all_valid(): def test_resolve_inclusion_exclusion_include_duplicates(): - assert resolve_inclusion_exclusion(include=("organization", "organization"), exclude=()) == ( - True, - False, - False, - False, - ) - + assert resolve_inclusion_exclusion(include=("organization", "organization"), exclude=()) == (True, False, False, False) def test_resolve_inclusion_exclusion_invalid_exclude(): with pytest.raises(Abort): From 7412ce0aeb1303008b11c9ea6583b8bb7a42c0ab Mon Sep 17 00:00:00 2001 From: Mohcine Tor Date: Wed, 4 Feb 2026 18:05:01 +0100 Subject: [PATCH 3/5] fix: ruff formatting --- Babylon/commands/namespace/get_all_states.py | 10 +++++----- Babylon/utils/environment.py | 2 +- tests/unit/test_macro.py | 7 ++++++- 3 files changed, 12 insertions(+), 7 deletions(-) diff --git a/Babylon/commands/namespace/get_all_states.py b/Babylon/commands/namespace/get_all_states.py index adbb68e1..2c12029a 100644 --- a/Babylon/commands/namespace/get_all_states.py +++ b/Babylon/commands/namespace/get_all_states.py @@ -1,6 +1,6 @@ from logging import getLogger -from click import command, argument, Choice, echo, style +from click import Choice, argument, command, echo, style from Babylon.utils.environment import Environment from Babylon.utils.response import CommandResponse @@ -13,12 +13,12 @@ @argument("target", type=Choice(["local", "remote"], case_sensitive=False)) def get_states(target: str) -> CommandResponse: """Display states from local machine or Azure remote storage.""" - + results_found = False if target == "local": echo(style("\n 📂 Local States", bold=True, fg="cyan")) states_dir = env.state_dir - + if not states_dir.exists(): logger.error(f" [bold red]✘[/bold red] Directory not found: [dim]{states_dir}[/dim]") else: @@ -29,7 +29,7 @@ def get_states(target: str) -> CommandResponse: for f in local_files: echo(style(" • ", fg="green") + f.name) results_found = True - + elif target == "remote": echo(style("\n ☁️ Remote States", bold=True, fg="cyan")) try: @@ -43,4 +43,4 @@ def get_states(target: str) -> CommandResponse: except Exception as e: logger.error(f" [bold red]✘[/bold red] Failed to reach Azure storage: {e}") - return CommandResponse.success() if results_found else CommandResponse.fail() \ No newline at end of file + return CommandResponse.success() if results_found else CommandResponse.fail() diff --git a/Babylon/utils/environment.py b/Babylon/utils/environment.py index c51bb431..1018b1c7 100644 --- a/Babylon/utils/environment.py +++ b/Babylon/utils/environment.py @@ -198,7 +198,7 @@ def list_remote_states(self) -> list[str]: except Exception as e: logger.error(f" [bold red]✘[/bold red] Impossible de lister les states distants: {e}") return [] - + def get_state_from_local(self): state_file = self.state_dir / f"state.{self.context_id}.{self.environ_id}.{self.state_id}.yaml" if not state_file.exists(): diff --git a/tests/unit/test_macro.py b/tests/unit/test_macro.py index e0f2c6cb..19f27254 100644 --- a/tests/unit/test_macro.py +++ b/tests/unit/test_macro.py @@ -75,7 +75,12 @@ def test_resolve_inclusion_exclusion_exclude_all_valid(): def test_resolve_inclusion_exclusion_include_duplicates(): - assert resolve_inclusion_exclusion(include=("organization", "organization"), exclude=()) == (True, False, False, False) + assert resolve_inclusion_exclusion(include=("organization", "organization"), exclude=()) == ( + True, + False, + False, + False, + ) def test_resolve_inclusion_exclusion_invalid_exclude(): with pytest.raises(Abort): From fa33968a7aa3138e06d6e5fa812d067f7782536e Mon Sep 17 00:00:00 2001 From: Mohcine Tor Date: Thu, 5 Feb 2026 09:28:53 +0100 Subject: [PATCH 4/5] fix: tests with new feature --- tests/e2e/test_e2e.sh | 2 +- tests/integration/test_api_endpoints.sh | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/tests/e2e/test_e2e.sh b/tests/e2e/test_e2e.sh index bc427a4f..1f8a3c3a 100755 --- a/tests/e2e/test_e2e.sh +++ b/tests/e2e/test_e2e.sh @@ -14,7 +14,7 @@ export TENANT="sphinx" export STATE="teststate" babylon namespace use -c ${CONTEXT} -t ${TENANT} -s $STATE -babylon namespace get-states +babylon namespace get-states local babylon namespace get-contexts # Get version diff --git a/tests/integration/test_api_endpoints.sh b/tests/integration/test_api_endpoints.sh index 57372755..c9a8c50f 100755 --- a/tests/integration/test_api_endpoints.sh +++ b/tests/integration/test_api_endpoints.sh @@ -16,7 +16,7 @@ export TENANT="sphinx" export STATE="teststate" babylon namespace use -c ${CONTEXT} -t ${TENANT} -s $STATE -babylon namespace get-states +babylon namespace get-states local babylon namespace get-contexts # Get version From 3fd1ffa14b0b378102189c4a8080fb5dcc05b1b6 Mon Sep 17 00:00:00 2001 From: Mohcine Tor Date: Fri, 13 Feb 2026 16:53:50 +0100 Subject: [PATCH 5/5] fix: review PR comments --- Babylon/utils/environment.py | 20 +++++++++++++------- 1 file changed, 13 insertions(+), 7 deletions(-) diff --git a/Babylon/utils/environment.py b/Babylon/utils/environment.py index 1018b1c7..80df274b 100644 --- a/Babylon/utils/environment.py +++ b/Babylon/utils/environment.py @@ -46,6 +46,9 @@ def __call__(cls, *args, **kwargs): class Environment(metaclass=SingletonMeta): + # Azure Blob Storage configuration + STATE_CONTAINER = "babylon-states" + def __init__(self): self.remote = False self.pwd = Path.cwd() @@ -68,6 +71,10 @@ def __init__(self): self.working_dir = WorkingDir(working_dir_path=self.pwd) self.variable_files: list[Path] = [] + def _get_state_blob_client(self, blob_name: str): + """Get a blob client for state management""" + return self.blob_client.get_blob_client(container=self.STATE_CONTAINER, blob=blob_name) + def get_variables(self): merged_data, duplicate_keys = self.merge_yaml_files(self.variable_files) if len(duplicate_keys) > 0: @@ -179,24 +186,23 @@ def store_state_in_local(self, state: dict): def store_state_in_cloud(self, state: dict): state_file = f"state.{self.context_id}.{self.environ_id}.{self.state_id}.yaml" - state_container = self.blob_client.get_container_client(container="babylon-states") + state_container = self.blob_client.get_container_client(container=self.STATE_CONTAINER) if not state_container.exists(): state_container.create_container() - state_blob = self.blob_client.get_blob_client(container="babylon-states", blob=state_file) + state_blob = self._get_state_blob_client(state_file) if state_blob.exists(): state_blob.delete_blob() state_blob.upload_blob(data=dump(state).encode("utf-8")) def list_remote_states(self) -> list[str]: - """Liste les noms des fichiers de state présents dans le container Azure.""" + """List state file names present in the Azure blob container.""" try: self.set_blob_client() - container_client = self.blob_client.get_container_client(container="babylon-states") - # On filtre pour ne prendre que les fichiers state.*.yaml + container_client = self.blob_client.get_container_client(container=self.STATE_CONTAINER) blobs = container_client.list_blobs(name_starts_with="state.") return [b.name for b in blobs if b.name.endswith(".yaml")] except Exception as e: - logger.error(f" [bold red]✘[/bold red] Impossible de lister les states distants: {e}") + logger.error(f" [bold red]✘[/bold red] Failed to list remote states: {e}") return [] def get_state_from_local(self): @@ -224,7 +230,7 @@ def get_state_from_local(self): def get_state_from_cloud(self) -> dict: s = f"state.{self.context_id}.{self.environ_id}.{self.state_id}.yaml" - state_blob = self.blob_client.get_blob_client(container="babylon-states", blob=s) + state_blob = self._get_state_blob_client(s) exists = state_blob.exists() if not exists: return {