From e310453487795a1811cc6818f97c079b0db9f198 Mon Sep 17 00:00:00 2001 From: Benjamin Labrecque Date: Thu, 23 Jul 2026 15:41:22 +0200 Subject: [PATCH] cli: add tests for agebd service new --- cli/pyproject.toml | 8 +- cli/src/config/paths.py | 50 ++++- cli/src/core/git.py | 131 ++++++------ cli/src/core/service_creator.py | 41 ++-- cli/src/models/context.py | 32 +-- cli/src/models/service_manager_ui.py | 5 + ...ice_registry.yml => services_registry.yml} | 0 .../docs/user/ioc/iocs_overview.md | 2 +- .../packages/agebd/pyproject.toml | 0 .../expected_output/qt/A_BD_ServiceManager.ui | 2 +- .../current/ioc/AGEBD-CPCL-MASTER_main.subs | 2 +- .../current/app/pyproject.toml | 0 .../app/src/agebd_test_service/__init__.py | 0 .../app/src/agebd_test_service/main.py | 2 - .../app/src/agebd_test_service/service.py | 2 +- .../current/app/tests/test_app.py | 0 .../services/test-service/current/app/uv.lock | 0 .../ioc/AGEBD-CPCL-TEST-SERVICE_main.subs | 0 .../AGEBD-CPCL-TEST-SERVICE_parameters.yaml | 0 .../current/ioc/TEST-SERVICE.template | 0 .../current/ioc/startup.script | 0 .../current/qt/A_BD_TestService.ui | 0 .../AGEBD-SERVICE-TEST-SERVICE.service | 0 cli/tests/tests/test_service_creator.py | 81 ++++++- cli/uv.lock | 198 +++++++++++++++--- qt/A_BD_ServiceManager.ui | 2 +- .../main.py.jinja | 2 - .../service.py.jinja | 11 +- 28 files changed, 405 insertions(+), 166 deletions(-) rename cli/tests/fixtures/expected_output/config/{service_registry.yml => services_registry.yml} (100%) rename cli/tests/fixtures/{test_repo_root => expected_output}/packages/agebd/pyproject.toml (100%) rename cli/tests/fixtures/expected_output/services/{test_service => test-service}/current/app/pyproject.toml (100%) rename cli/tests/fixtures/expected_output/services/{test_service => test-service}/current/app/src/agebd_test_service/__init__.py (100%) rename cli/tests/fixtures/expected_output/services/{test_service => test-service}/current/app/src/agebd_test_service/main.py (96%) rename cli/tests/fixtures/expected_output/services/{test_service => test-service}/current/app/src/agebd_test_service/service.py (98%) rename cli/tests/fixtures/expected_output/services/{test_service => test-service}/current/app/tests/test_app.py (100%) create mode 100644 cli/tests/fixtures/expected_output/services/test-service/current/app/uv.lock rename cli/tests/fixtures/expected_output/services/{test_service => test-service}/current/ioc/AGEBD-CPCL-TEST-SERVICE_main.subs (100%) rename cli/tests/fixtures/expected_output/services/{test_service => test-service}/current/ioc/AGEBD-CPCL-TEST-SERVICE_parameters.yaml (100%) rename cli/tests/fixtures/expected_output/services/{test_service => test-service}/current/ioc/TEST-SERVICE.template (100%) rename cli/tests/fixtures/expected_output/services/{test_service => test-service}/current/ioc/startup.script (100%) rename cli/tests/fixtures/expected_output/services/{test_service => test-service}/current/qt/A_BD_TestService.ui (100%) rename cli/tests/fixtures/expected_output/services/{test_service => test-service}/current/systemd/AGEBD-SERVICE-TEST-SERVICE.service (100%) diff --git a/cli/pyproject.toml b/cli/pyproject.toml index f2f10f6..f43cc5b 100644 --- a/cli/pyproject.toml +++ b/cli/pyproject.toml @@ -15,6 +15,7 @@ dependencies = [ "gitpython>=3.1.50", "pydantic>=2.13.4", "pydantic-settings>=2.14.2", + "pytest-mock>=3.15.1", "pyyaml>=6.0.3", "typer>=0.23.2", ] @@ -22,6 +23,7 @@ dependencies = [ # Dev dependencies [dependency-groups] dev = [ + "ipython>=8.39.0", "pyright>=1.1.410", "pytest>=8.4.2", "ruff>=0.15.19", @@ -46,6 +48,10 @@ where = ["src"] [tool.uv] package = true +[tool.pytest.ini_options] +# avoid pytest running tests inside tests/fixtures +testpaths = ["tests/tests"] + [tool.ruff] include = [ "src/**/*.py", @@ -58,7 +64,7 @@ extend-select = [ # use isort to sort imports "I", # remove unused imports - "F401", + # "F401", ] [tool.ruff.lint.isort] diff --git a/cli/src/config/paths.py b/cli/src/config/paths.py index 4e232d9..67d8d4a 100644 --- a/cli/src/config/paths.py +++ b/cli/src/config/paths.py @@ -1,28 +1,31 @@ from pathlib import Path -from pydantic import BaseModel, DirectoryPath, FilePath +from pydantic import DirectoryPath, FilePath, model_validator +from pydantic_settings import BaseSettings from core.utils import get_git_root REPO_ROOT = get_git_root(__file__) -class Paths(BaseModel): +class Paths(BaseSettings): # fmt: off - repo_root: DirectoryPath = REPO_ROOT + repo_root: DirectoryPath = REPO_ROOT - service_registry_file: FilePath = repo_root / "config" / "services_registry.yml" - iocs_overview_filename: FilePath = repo_root / "docs" / "user" / "ioc" / "ioc_overview.md" - master_hla_names_file: FilePath = repo_root / "services" / "master" / "current" / "app" / "config" / "hla_names.yml" - master_ioc_subs_file: FilePath = repo_root / "services" / "master" / "current" / "ioc" / "AGEBD-CPCL-MASTER_main.subs" - service_manager_ui_file: FilePath = repo_root / "qt" / "A_BD_ServiceManager.ui" + services_dir: DirectoryPath = Path("services") + + service_registry_file: FilePath = Path("config/services_registry.yml") + iocs_overview_filename: FilePath = Path("docs/user/ioc/iocs_overview.md") + master_hla_names_file: FilePath = Path("services/master/current/app/config/hla_names.yml") + master_ioc_subs_file: FilePath = Path("services/master/current/ioc/AGEBD-CPCL-MASTER_main.subs") + service_manager_ui_file: FilePath = Path("qt/A_BD_ServiceManager.ui") # fmt: on def get_files_to_stage(self, service_dir_name: str) -> list[str]: """ Returns all required relative file paths for git staging. """ - service_current = self.repo_root / "services" / service_dir_name / "current" + service_current = self.services_dir / service_dir_name / "current" staged_paths = [ service_current, @@ -40,3 +43,32 @@ class Paths(BaseModel): Convert any absolute path into a string path relative to repo_root. """ return str(path.relative_to(self.repo_root)) + + @model_validator(mode="before") + @classmethod + def resolve_paths_against_repo_root(cls, data): + """ + Allows to set a different repo_root on object creation: + + paths = Paths(repo_root=Path("/my/path")) + + and have the other paths use that repo_root. + """ + if not isinstance(data, dict): + return data + + # Determine what repo_root to use (either passed in data or default constant) + root = Path(data.get("repo_root", REPO_ROOT)) + + # Prep-end root to any relative paths before Pydantic validates FilePath / DirectoryPath + for field_name, field_info in cls.model_fields.items(): + if field_name == "repo_root": + continue + + val = data.get(field_name, field_info.default) + if val is not None: + path_val = Path(val) + if not path_val.is_absolute(): + data[field_name] = root / path_val + + return data diff --git a/cli/src/core/git.py b/cli/src/core/git.py index c2e4c07..f695092 100644 --- a/cli/src/core/git.py +++ b/cli/src/core/git.py @@ -1,83 +1,88 @@ import logging +from pathlib import Path import git logger = logging.getLogger() -def git_push_changes( - repo: git.Repo, - branch_name: str, - files_to_stage: list[str], - commit_msg: str, -): - """ - Push newly created service to Gitea - """ - logger.info(f"Staging files inside: {files_to_stage}") - repo.index.add(files_to_stage) +class GitRepoManager: + def __init__(self, repo_root: Path | str) -> None: + self.repo = git.Repo(repo_root) - # Check if there are changes to avoid empty commit crashes - if not repo.is_dirty(index=True, working_tree=False): - logger.info("No changes detected. Skipping commit.") - return + def git_push_changes( + self, + branch_name: str, + files_to_stage: list[str], + commit_msg: str, + ): + """ + Push newly created service to Gitea + """ + logger.info(f"Staging files inside: {files_to_stage}") + self.repo.index.add(files_to_stage) - logger.info("Committing changes...") - repo.index.commit(commit_msg) + # Check if there are changes to avoid empty commit crashes + if not self.repo.is_dirty(index=True, working_tree=False): + logger.info("No changes detected. Skipping commit.") + return - try: - logger.info(f"Pushing branch '{branch_name}' to origin...") - origin = repo.remote(name="origin") - origin.push(refspec=f"{branch_name}:{branch_name}").raise_if_error() - except git.GitCommandError as e: - logger.error(f"Push failed due to an error: {e}") - logger.info("Rolling back local Git index changes...") + logger.info("Committing changes...") + self.repo.index.commit(commit_msg) - # Step A: Undo the local commit if it was created (moves HEAD back 1 commit, keeps changes) - # This prevents leaving a dead/unpushed commit on your local branch try: - repo.head.reset("HEAD~1", index=True, working_tree=False) - except Exception: - # Fallback if the exception happened BEFORE the commit step even ran - pass + logger.info(f"Pushing branch '{branch_name}' to origin...") + origin = self.repo.remote(name="origin") + origin.push(refspec=f"{branch_name}:{branch_name}").raise_if_error() + except git.GitCommandError as e: + logger.error(f"Push failed due to an error: {e}") + logger.info("Rolling back local Git index changes...") - # Step B: Unstage the specific files (Equivalent to: git reset HEAD ) - # This leaves the files intact but removes them from the staging area - repo.index.reset(paths=files_to_stage) - logger.info("Staging area successfully reset to clean state.") + # Step A: Undo the local commit if it was created (moves HEAD back 1 commit, keeps changes) + # This prevents leaving a dead/unpushed commit on your local branch + try: + self.repo.head.reset("HEAD~1", index=True, working_tree=False) + except Exception: + # Fallback if the exception happened BEFORE the commit step even ran + pass - # TODO: we could also force push... need to think about workflow though. - raise RuntimeError( - f"Failed to push to git due to: {e}.\nDoes the branch already exist in Gitea? If yes, it needs to be deleted first." - ) + # Step B: Unstage the specific files (Equivalent to: git reset HEAD ) + # This leaves the files intact but removes them from the staging area + self.repo.index.reset(paths=files_to_stage) + logger.info("Staging area successfully reset to clean state.") - logger.info("Successfully pushed!") + # TODO: we could also force push... need to think about workflow though. + raise RuntimeError( + f"Failed to push to git due to: {e}.\nDoes the branch already exist in Gitea? If yes, it needs to be deleted first." + ) + logger.info("Successfully pushed!") -def delete_local_branch(repo: git.Repo, branch_name: str): - """ - Delete local branch if it already exists - """ - if branch_name in repo.branches: - logger.info(f"Deleting local branch '{branch_name}'...") - repo.git.branch("-D", branch_name) + def get_active_branch_name(self): + return self.repo.active_branch.name + def delete_local_branch(self, branch_name: str): + """ + Delete local branch if it already exists + """ + if branch_name in self.repo.branches: + logger.info(f"Deleting local branch '{branch_name}'...") + self.repo.git.branch("-D", branch_name) -def switch_branch(repo: git.Repo, branch_name: str): - """ - Create branch if it doesn't exist, then checkout - """ - logger.info(f"Switching to branch '{branch_name}'...") - if branch_name in repo.heads: - new_branch = repo.heads[branch_name] - else: - new_branch = repo.create_head(branch_name) - new_branch.checkout() + def switch_branch(self, branch_name: str): + """ + Create branch if it doesn't exist, then checkout + """ + logger.info(f"Switching to branch '{branch_name}'...") + if branch_name in self.repo.heads: + new_branch = self.repo.heads[branch_name] + else: + new_branch = self.repo.create_head(branch_name) + new_branch.checkout() - -def assert_clean_repo(repo: git.Repo): - if repo.is_dirty(index=True, working_tree=True): - raise RuntimeError( - "Your Git repository has uncommitted modifications to tracked files.\n" - "Please commit, stash, or discard them before proceeding." - ) + def assert_clean_repo(self): + if self.repo.is_dirty(index=True, working_tree=True): + raise RuntimeError( + "Your Git repository has uncommitted modifications to tracked files.\n" + "Please commit, stash, or discard them before proceeding." + ) diff --git a/cli/src/core/service_creator.py b/cli/src/core/service_creator.py index 20da279..976817a 100644 --- a/cli/src/core/service_creator.py +++ b/cli/src/core/service_creator.py @@ -4,15 +4,9 @@ import subprocess from pathlib import Path import copier -import git from core.exceptions import UVError -from core.git import ( - assert_clean_repo, - delete_local_branch, - git_push_changes, - switch_branch, -) +from core.git import GitRepoManager from core.utils import get_git_root from models import ( Service, @@ -23,12 +17,12 @@ logger = logging.getLogger(__name__) REPO_ROOT = get_git_root(__file__) SERVICE_TEMPLATES_DIR = REPO_ROOT / "templates" / "service" -SERVICE_DEST_DIR = REPO_ROOT / "services" class ServiceCreator: - def __init__(self, ctx: ServicesContext) -> None: + def __init__(self, ctx: ServicesContext, repo_manager: GitRepoManager) -> None: self.ctx = ctx + self.repo_manager = repo_manager def add_service( self, @@ -38,9 +32,8 @@ class ServiceCreator: ui_name: str, ui_filename: str, ): - repo = git.Repo(REPO_ROOT) - assert_clean_repo(repo) - original_branch = repo.active_branch.name + self.repo_manager.assert_clean_repo() + original_branch = self.repo_manager.get_active_branch_name() backup = self.ctx.snapshot() @@ -53,8 +46,8 @@ class ServiceCreator: ) branch_name = f"feature/add-service-{service.dir_name}" - delete_local_branch(repo, branch_name) - switch_branch(repo, branch_name) + self.repo_manager.delete_local_branch(branch_name) + self.repo_manager.switch_branch(branch_name) try: self._render_new_service_templates( @@ -66,21 +59,21 @@ class ServiceCreator: self.ctx.write_all() - files_to_stage = self.ctx.paths.get_files_to_stage(service_dir_name=service.dir_name) + files_to_stage = self.ctx.write_paths.get_files_to_stage( + service_dir_name=service.dir_name + ) # TODO: can't push if branch already exists in remote - git_push_changes( - repo=repo, + self.repo_manager.git_push_changes( branch_name=branch_name, files_to_stage=files_to_stage, commit_msg=f"feature: add new service {service.name_lower}", ) - switch_branch(repo, original_branch) + self.repo_manager.switch_branch(original_branch) except Exception as e: - shutil.rmtree(Path(SERVICE_DEST_DIR / service.dir_name), ignore_errors=True) - print("WRITE ALL") + shutil.rmtree(self.ctx.write_paths.services_dir / service.dir_name, ignore_errors=True) backup.write_all() - switch_branch(repo, original_branch) + self.repo_manager.switch_branch(original_branch) raise e def _render_new_service_templates(self, service: Service, user: str, ui_filename: str): @@ -89,7 +82,7 @@ class ServiceCreator: """ copier.run_copy( src_path=str(SERVICE_TEMPLATES_DIR), - dst_path=str(SERVICE_DEST_DIR), + dst_path=str(self.ctx.write_paths.services_dir), data={ "service_name_upper": service.name_upper, "service_name_lower": service.name_lower, @@ -103,7 +96,9 @@ class ServiceCreator: """ Generates a uv.lock file by running 'uv lock' in the target directory. """ - app_path = Path(SERVICE_DEST_DIR / service.dir_name / "current" / "app").resolve() + app_path = Path( + self.ctx.write_paths.services_dir / service.dir_name / "current" / "app" + ).resolve() logger.info(f"Generating uv.lock inside: {app_path}") try: diff --git a/cli/src/models/context.py b/cli/src/models/context.py index 699c740..3d87eec 100644 --- a/cli/src/models/context.py +++ b/cli/src/models/context.py @@ -1,5 +1,3 @@ -from typing import Any, Dict - from pydantic import BaseModel from config.paths import Paths @@ -8,6 +6,7 @@ from models import IocsOverview, MasterHLANames, MasterIocSubs, ServiceManagerUI class ServicesContext(BaseModel): paths: Paths + _write_paths: Paths | None = None service_registry: ServiceRegistry master_hla_names: MasterHLANames @@ -15,6 +14,15 @@ class ServicesContext(BaseModel): master_ioc_subs: MasterIocSubs service_manager_ui: ServiceManagerUI + @property + def write_paths(self) -> Paths: + """ + Mainly used to change the write location in tests + """ + if self._write_paths is None: + return self.paths + return self._write_paths + @classmethod def load_from_disk(cls, paths: Paths) -> "ServicesContext": return cls( @@ -27,7 +35,7 @@ class ServicesContext(BaseModel): ) def snapshot(self) -> "ServicesContext": - """Creates an in-memory backup of all configs for rollback.""" + """Creates an backup of all configs for rollback.""" return ServicesContext( paths=self.paths, service_registry=self.service_registry.model_copy(deep=True), @@ -37,18 +45,10 @@ class ServicesContext(BaseModel): service_manager_ui=self.service_manager_ui.model_copy(deep=True), ) - def restore_snapshot(self, snapshot: Dict[str, Any]) -> None: - """Restores state from a backup dictionary.""" - self.service_registry = snapshot["service_registry"] - self.master_hla_names = snapshot["master_hla_names"] - self.iocs_overview = snapshot["iocs_overview"] - self.master_ioc_subs = snapshot["master_ioc_subs"] - self.service_manager_ui = snapshot["service_manager_ui"] - def write_all(self) -> None: """Persists all modified objects to disk.""" - self.service_registry.write_to_file(self.paths.service_registry_file) - self.master_hla_names.write_to_file(self.paths.master_hla_names_file) - self.iocs_overview.write_to_file(self.paths.iocs_overview_filename) - self.master_ioc_subs.write_to_file(self.paths.master_ioc_subs_file) - self.service_manager_ui.write_to_file(self.paths.service_manager_ui_file) + self.service_registry.write_to_file(self.write_paths.service_registry_file) + self.master_hla_names.write_to_file(self.write_paths.master_hla_names_file) + self.iocs_overview.write_to_file(self.write_paths.iocs_overview_filename) + self.master_ioc_subs.write_to_file(self.write_paths.master_ioc_subs_file) + self.service_manager_ui.write_to_file(self.write_paths.service_manager_ui_file) diff --git a/cli/src/models/service_manager_ui.py b/cli/src/models/service_manager_ui.py index a99d023..119b44c 100644 --- a/cli/src/models/service_manager_ui.py +++ b/cli/src/models/service_manager_ui.py @@ -115,6 +115,11 @@ class ServiceManagerUI(BaseModel): return cainclude_height def write_to_file(self, path: Path) -> None: + # raw_xml = ET.tostring(self.tree.getroot(), encoding="utf-8").decode("utf-8") + # + # # Clean up newline right before + # cleaned_xml = raw_xml.replace("\n", "") + # path.write_text(cleaned_xml) self.tree.write(path, encoding="utf-8", xml_declaration=True) diff --git a/cli/tests/fixtures/expected_output/config/service_registry.yml b/cli/tests/fixtures/expected_output/config/services_registry.yml similarity index 100% rename from cli/tests/fixtures/expected_output/config/service_registry.yml rename to cli/tests/fixtures/expected_output/config/services_registry.yml diff --git a/cli/tests/fixtures/expected_output/docs/user/ioc/iocs_overview.md b/cli/tests/fixtures/expected_output/docs/user/ioc/iocs_overview.md index 50dc7ea..d402e65 100644 --- a/cli/tests/fixtures/expected_output/docs/user/ioc/iocs_overview.md +++ b/cli/tests/fixtures/expected_output/docs/user/ioc/iocs_overview.md @@ -1,4 +1,4 @@ | IOC NAME | Description | |---|---| | AGEBD-CPCL-MASTER | IOC providing PVs ... | -| AGEBD-CPCL-TEST-SERVICE | Test description | +| AGEBD-CPCL-TEST-SERVICE | Test Description | diff --git a/cli/tests/fixtures/test_repo_root/packages/agebd/pyproject.toml b/cli/tests/fixtures/expected_output/packages/agebd/pyproject.toml similarity index 100% rename from cli/tests/fixtures/test_repo_root/packages/agebd/pyproject.toml rename to cli/tests/fixtures/expected_output/packages/agebd/pyproject.toml diff --git a/cli/tests/fixtures/expected_output/qt/A_BD_ServiceManager.ui b/cli/tests/fixtures/expected_output/qt/A_BD_ServiceManager.ui index 3f9ff36..a79e7cd 100644 --- a/cli/tests/fixtures/expected_output/qt/A_BD_ServiceManager.ui +++ b/cli/tests/fixtures/expected_output/qt/A_BD_ServiceManager.ui @@ -138,7 +138,7 @@ caLabel[statusTip="Operation"]{ - IOC=AGEBD-CPCL-DBPM3CURR,service=DBPM3CURR,name=DBPM3Current,panel=1,svc_exist=1,panelcmd=A_BD_DBPM3Current.ui;IOC=AGEBD-CPCL-TAUBPM,service=TAUBPM,name=LifetimeBPM,panel=1,svc_exist=1,panelcmd=A_BD_LifetimeBPM.ui;IOC=AGEBD-CPCL-TAUPCT,service=TAUPCT,name=LifetimePCT,panel=1,svc_exist=1,panelcmd=A_BD_LifetimePCT.ui;IOC=AGEBD-CPCL-TIMING,service=TIMING,name=Timing,panel=1,svc_exist=1,panelcmd=A_BD_Timing.ui;IOC=AGEBD-CPCL-SCRUBBING,service=SCRUBBING,name=Scrubbing,panel=1,svc_exist=1,panelcmd=A_BD_Scrubbing.ui;IOC=AGEBD-CPCL-INJECTIONGUARD,service=INJECTIONGUARD,name=InjectionGuard,panel=1,svc_exist=1,panelcmd=A_BD_InjectionGuard.ui;IOC=AGEBD-CPCL-POSTMORTEMLOG,service=POSTMORTEMLOG,name=PostMortemLog,panel=1,svc_exist=1,panelcmd=A_BD_PostMortemLog.ui;IOC=AGEBD-CPCL-TUNEBUMP,service=TUNEBUMP,name=TuneBump,panel=1,svc_exist=1,panelcmd=A_BD_TuneBump.ui;IOC=AGEBD-CPCL-PLOTS,service=PLOTS,name=Plots,panel=0,svc_exist=1,panelcmd=A_BD_Plots.ui;IOC=AGEBD-CPCL-ORBITBUMP,service=ORBITBUMP,name=OrbitBump,panel=1,svc_exist=0,panelcmd=A_BD_OrbitBump.ui;IOC=AGEBD-CPCL-TUNEFBX,service=TUNEFBX,name=Hor. Tune Feedback,panel=1,svc_exist=1,panelcmd=A_BD_TuneBump.ui;IOC=AGEBD-CPCL-TUNEFBY,service=TUNEFBY,name=Ver. Tune Feedback,panel=1,svc_exist=1,panelcmd=A_BD_TuneBump.ui;IOC=AGEBD-CPCL-TEST-SERVICE,service=TEST-SERVICE,name=Test Service Ui Name,panel=1,svc_exist=1,panelcmd=A_BD_TestService.ui; + IOC=AGEBD-CPCL-DBPM3CURR,service=DBPM3CURR,name=DBPM3Current,panel=1,svc_exist=1,panelcmd=A_BD_DBPM3Current.ui;IOC=AGEBD-CPCL-TAUBPM,service=TAUBPM,name=LifetimeBPM,panel=1,svc_exist=1,panelcmd=A_BD_LifetimeBPM.ui;IOC=AGEBD-CPCL-TAUPCT,service=TAUPCT,name=LifetimePCT,panel=1,svc_exist=1,panelcmd=A_BD_LifetimePCT.ui;IOC=AGEBD-CPCL-TIMING,service=TIMING,name=Timing,panel=1,svc_exist=1,panelcmd=A_BD_Timing.ui;IOC=AGEBD-CPCL-SCRUBBING,service=SCRUBBING,name=Scrubbing,panel=1,svc_exist=1,panelcmd=A_BD_Scrubbing.ui;IOC=AGEBD-CPCL-INJECTIONGUARD,service=INJECTIONGUARD,name=InjectionGuard,panel=1,svc_exist=1,panelcmd=A_BD_InjectionGuard.ui;IOC=AGEBD-CPCL-POSTMORTEMLOG,service=POSTMORTEMLOG,name=PostMortemLog,panel=1,svc_exist=1,panelcmd=A_BD_PostMortemLog.ui;IOC=AGEBD-CPCL-TUNEBUMP,service=TUNEBUMP,name=TuneBump,panel=1,svc_exist=1,panelcmd=A_BD_TuneBump.ui;IOC=AGEBD-CPCL-PLOTS,service=PLOTS,name=Plots,panel=0,svc_exist=1,panelcmd=A_BD_Plots.ui;IOC=AGEBD-CPCL-ORBITBUMP,service=ORBITBUMP,name=OrbitBump,panel=1,svc_exist=0,panelcmd=A_BD_OrbitBump.ui;IOC=AGEBD-CPCL-TUNEFBX,service=TUNEFBX,name=Hor. Tune Feedback,panel=1,svc_exist=1,panelcmd=A_BD_TuneBump.ui;IOC=AGEBD-CPCL-TUNEFBY,service=TUNEFBY,name=Ver. Tune Feedback,panel=1,svc_exist=1,panelcmd=A_BD_TuneBump.ui;IOC=AGEBD-CPCL-TEST-SERVICE,service=TEST-SERVICE,name=Test Service Ui Name,panel=1,svc_exist=1,panelcmd=A_BD_TestService.ui A_BD_ServiceManager_inc.ui diff --git a/cli/tests/fixtures/expected_output/services/master/current/ioc/AGEBD-CPCL-MASTER_main.subs b/cli/tests/fixtures/expected_output/services/master/current/ioc/AGEBD-CPCL-MASTER_main.subs index 2628f47..50c2f43 100644 --- a/cli/tests/fixtures/expected_output/services/master/current/ioc/AGEBD-CPCL-MASTER_main.subs +++ b/cli/tests/fixtures/expected_output/services/master/current/ioc/AGEBD-CPCL-MASTER_main.subs @@ -1,5 +1,5 @@ file MASTER.template { pattern { SUFFIX SERVICE STARTON AUTOOFF } { "{{ agebd_env_suffix }}", "MASTER" , "0", "30" } - { "{{ agebd_env_suffix }}", "TEST-SERVICE" , "0", "30" } + { "{{ agebd_env_suffix }}", "TEST-SERVICE" , "0", "0" } } diff --git a/cli/tests/fixtures/expected_output/services/test_service/current/app/pyproject.toml b/cli/tests/fixtures/expected_output/services/test-service/current/app/pyproject.toml similarity index 100% rename from cli/tests/fixtures/expected_output/services/test_service/current/app/pyproject.toml rename to cli/tests/fixtures/expected_output/services/test-service/current/app/pyproject.toml diff --git a/cli/tests/fixtures/expected_output/services/test_service/current/app/src/agebd_test_service/__init__.py b/cli/tests/fixtures/expected_output/services/test-service/current/app/src/agebd_test_service/__init__.py similarity index 100% rename from cli/tests/fixtures/expected_output/services/test_service/current/app/src/agebd_test_service/__init__.py rename to cli/tests/fixtures/expected_output/services/test-service/current/app/src/agebd_test_service/__init__.py diff --git a/cli/tests/fixtures/expected_output/services/test_service/current/app/src/agebd_test_service/main.py b/cli/tests/fixtures/expected_output/services/test-service/current/app/src/agebd_test_service/main.py similarity index 96% rename from cli/tests/fixtures/expected_output/services/test_service/current/app/src/agebd_test_service/main.py rename to cli/tests/fixtures/expected_output/services/test-service/current/app/src/agebd_test_service/main.py index f506bcc..0f293e9 100644 --- a/cli/tests/fixtures/expected_output/services/test_service/current/app/src/agebd_test_service/main.py +++ b/cli/tests/fixtures/expected_output/services/test-service/current/app/src/agebd_test_service/main.py @@ -5,8 +5,6 @@ from agebd.runner import CallbackRunner from agebd.utils import init_logging from agebd_test_service import PVs, Service -# TODO: remove: cicd test 10 - def main( log_level: LogLevel = typer.Option( diff --git a/cli/tests/fixtures/expected_output/services/test_service/current/app/src/agebd_test_service/service.py b/cli/tests/fixtures/expected_output/services/test-service/current/app/src/agebd_test_service/service.py similarity index 98% rename from cli/tests/fixtures/expected_output/services/test_service/current/app/src/agebd_test_service/service.py rename to cli/tests/fixtures/expected_output/services/test-service/current/app/src/agebd_test_service/service.py index 5913356..364b135 100644 --- a/cli/tests/fixtures/expected_output/services/test_service/current/app/src/agebd_test_service/service.py +++ b/cli/tests/fixtures/expected_output/services/test-service/current/app/src/agebd_test_service/service.py @@ -38,7 +38,7 @@ class PVs(BasePVs): # TODO: class attribute names usually lowercase # usually it is advisable to trigger the mainloop of a service on changes of certain PVs: - self.CallbackPV = PV("AGEBD-TEST-SERVICE:CALLBACK", auto_monitor=dbr.DBE_VALUE) # TODO: ok? + self.CallbackPV = PV("AGEBD-TEST-SERVICE:CALLBACK", auto_monitor=dbr.DBE_VALUE) class Service(BaseService[PVs]): diff --git a/cli/tests/fixtures/expected_output/services/test_service/current/app/tests/test_app.py b/cli/tests/fixtures/expected_output/services/test-service/current/app/tests/test_app.py similarity index 100% rename from cli/tests/fixtures/expected_output/services/test_service/current/app/tests/test_app.py rename to cli/tests/fixtures/expected_output/services/test-service/current/app/tests/test_app.py diff --git a/cli/tests/fixtures/expected_output/services/test-service/current/app/uv.lock b/cli/tests/fixtures/expected_output/services/test-service/current/app/uv.lock new file mode 100644 index 0000000..e69de29 diff --git a/cli/tests/fixtures/expected_output/services/test_service/current/ioc/AGEBD-CPCL-TEST-SERVICE_main.subs b/cli/tests/fixtures/expected_output/services/test-service/current/ioc/AGEBD-CPCL-TEST-SERVICE_main.subs similarity index 100% rename from cli/tests/fixtures/expected_output/services/test_service/current/ioc/AGEBD-CPCL-TEST-SERVICE_main.subs rename to cli/tests/fixtures/expected_output/services/test-service/current/ioc/AGEBD-CPCL-TEST-SERVICE_main.subs diff --git a/cli/tests/fixtures/expected_output/services/test_service/current/ioc/AGEBD-CPCL-TEST-SERVICE_parameters.yaml b/cli/tests/fixtures/expected_output/services/test-service/current/ioc/AGEBD-CPCL-TEST-SERVICE_parameters.yaml similarity index 100% rename from cli/tests/fixtures/expected_output/services/test_service/current/ioc/AGEBD-CPCL-TEST-SERVICE_parameters.yaml rename to cli/tests/fixtures/expected_output/services/test-service/current/ioc/AGEBD-CPCL-TEST-SERVICE_parameters.yaml diff --git a/cli/tests/fixtures/expected_output/services/test_service/current/ioc/TEST-SERVICE.template b/cli/tests/fixtures/expected_output/services/test-service/current/ioc/TEST-SERVICE.template similarity index 100% rename from cli/tests/fixtures/expected_output/services/test_service/current/ioc/TEST-SERVICE.template rename to cli/tests/fixtures/expected_output/services/test-service/current/ioc/TEST-SERVICE.template diff --git a/cli/tests/fixtures/expected_output/services/test_service/current/ioc/startup.script b/cli/tests/fixtures/expected_output/services/test-service/current/ioc/startup.script similarity index 100% rename from cli/tests/fixtures/expected_output/services/test_service/current/ioc/startup.script rename to cli/tests/fixtures/expected_output/services/test-service/current/ioc/startup.script diff --git a/cli/tests/fixtures/expected_output/services/test_service/current/qt/A_BD_TestService.ui b/cli/tests/fixtures/expected_output/services/test-service/current/qt/A_BD_TestService.ui similarity index 100% rename from cli/tests/fixtures/expected_output/services/test_service/current/qt/A_BD_TestService.ui rename to cli/tests/fixtures/expected_output/services/test-service/current/qt/A_BD_TestService.ui diff --git a/cli/tests/fixtures/expected_output/services/test_service/current/systemd/AGEBD-SERVICE-TEST-SERVICE.service b/cli/tests/fixtures/expected_output/services/test-service/current/systemd/AGEBD-SERVICE-TEST-SERVICE.service similarity index 100% rename from cli/tests/fixtures/expected_output/services/test_service/current/systemd/AGEBD-SERVICE-TEST-SERVICE.service rename to cli/tests/fixtures/expected_output/services/test-service/current/systemd/AGEBD-SERVICE-TEST-SERVICE.service diff --git a/cli/tests/tests/test_service_creator.py b/cli/tests/tests/test_service_creator.py index cc382a0..597d4ab 100644 --- a/cli/tests/tests/test_service_creator.py +++ b/cli/tests/tests/test_service_creator.py @@ -1,3 +1,8 @@ +import filecmp +from difflib import unified_diff +from pathlib import Path +from unittest.mock import patch + from pytest_mock import MockerFixture from config.paths import Paths @@ -5,12 +10,18 @@ from core.git import GitRepoManager from core.service_creator import ServiceCreator from core.utils import get_git_root from models.context import ServicesContext -from tests.tests.utils import are_dir_trees_equal REPO_ROOT = get_git_root(__file__) TEST_REPO_ROOT = REPO_ROOT / "cli" / "tests" / "fixtures" / "test_repo_root" EXPECTED_OUTPUT_DIR = REPO_ROOT / "cli" / "tests" / "fixtures" / "expected_output" +mock_agebd_pyproject_toml = """[project] +name = "agebd" +version = "0.1.0" +description = "SLS HLA Framework" +requires-python = "==3.10.*" +""" + def test_paths_exist(): """ @@ -26,8 +37,32 @@ def test_add_new_service(tmp_path, mocker: MockerFixture): """ paths = Paths(repo_root=TEST_REPO_ROOT) + with ( + patch("pathlib.Path.exists", return_value=True), + patch("pathlib.Path.is_file", return_value=True), + patch("pathlib.Path.is_dir", return_value=True), + ): + # Avoid Pydantic's FilePath / DirectoryPath validation errors for this + # test + write_paths = Paths(repo_root=tmp_path) + + # Outside of tests, these file are read and written to + # In test we read from one location and write to a tmp path + # so we need to make sure the paths exist + (tmp_path / write_paths.services_dir.parent).mkdir(parents=True, exist_ok=True) + (tmp_path / write_paths.service_registry_file.parent).mkdir(parents=True, exist_ok=True) + (tmp_path / write_paths.iocs_overview_filename.parent).mkdir(parents=True, exist_ok=True) + (tmp_path / write_paths.master_hla_names_file.parent).mkdir(parents=True, exist_ok=True) + (tmp_path / write_paths.master_ioc_subs_file.parent).mkdir(parents=True, exist_ok=True) + (tmp_path / write_paths.service_manager_ui_file.parent).mkdir(parents=True, exist_ok=True) + + # Because uv needs packages/agebd to generate the lock file of the services as it is + # a dependency + (tmp_path / "packages/agebd").mkdir(parents=True, exist_ok=True) + (tmp_path / "packages/agebd/pyproject.toml").write_text(mock_agebd_pyproject_toml) + ctx = ServicesContext.load_from_disk(paths) - ctx.write_paths = tmp_path + ctx._write_paths = write_paths creator = ServiceCreator(ctx=ctx, repo_manager=mocker.Mock(spec=GitRepoManager)) creator.add_service( @@ -38,4 +73,44 @@ def test_add_new_service(tmp_path, mocker: MockerFixture): ui_filename="TestService", ) - assert are_dir_trees_equal(tmp_path, EXPECTED_OUTPUT_DIR) + assert_are_dir_trees_equal(tmp_path, EXPECTED_OUTPUT_DIR, tmp_path) + + +def assert_are_dir_trees_equal(dir1, dir2, curr_dir: Path | None = None): + """ + Compare two directories recursively. Files in each directory are + assumed to be equal if their names and contents are equal. + + @param dir1: First directory path + @param dir2: Second directory path + """ + if curr_dir is None: + curr_dir = Path() + + dirs_cmp = filecmp.dircmp(dir1, dir2) + assert len(dirs_cmp.left_only) == 0, f"diff in {curr_dir} dir" + assert len(dirs_cmp.right_only) == 0, f"diff in {curr_dir} dir" + assert len(dirs_cmp.funny_files) == 0, f"diff in {curr_dir} dir" + + (_, mismatch, _) = filecmp.cmpfiles(dir1, dir2, dirs_cmp.common_files, shallow=False) + for filename in mismatch: + if filename == "uv.lock": + continue + assert_files_equal(dir1 / filename, dir2 / filename) + for common_dir in dirs_cmp.common_dirs: + new_dir1 = dir1 / common_dir + new_dir2 = dir2 / common_dir + assert_are_dir_trees_equal(new_dir1, new_dir2, curr_dir / common_dir) + return True + + +def assert_files_equal(f1, f2): + actual_lines = [ + line + "\n" for line in Path(f1).read_text(encoding="utf-8").strip().splitlines() + ] + expected_lines = [ + line + "\n" for line in Path(f2).read_text(encoding="utf-8").strip().splitlines() + ] + + diff = list(unified_diff(actual_lines, expected_lines, n=0)) + assert diff == [], f"Unexpected file contents in file {f1}:\n" + "".join(diff) diff --git a/cli/uv.lock b/cli/uv.lock index 64ded5d..70ef340 100644 --- a/cli/uv.lock +++ b/cli/uv.lock @@ -39,12 +39,14 @@ dependencies = [ { name = "gitpython" }, { name = "pydantic" }, { name = "pydantic-settings" }, + { name = "pytest-mock" }, { name = "pyyaml" }, { name = "typer" }, ] [package.dev-dependencies] dev = [ + { name = "ipython" }, { name = "pyright" }, { name = "pytest" }, { name = "ruff" }, @@ -59,12 +61,14 @@ requires-dist = [ { name = "gitpython", specifier = ">=3.1.50" }, { name = "pydantic", specifier = ">=2.13.4" }, { name = "pydantic-settings", specifier = ">=2.14.2" }, + { name = "pytest-mock", specifier = ">=3.15.1" }, { name = "pyyaml", specifier = ">=6.0.3" }, { name = "typer", specifier = ">=0.23.2" }, ] [package.metadata.requires-dev] dev = [ + { name = "ipython", specifier = ">=8.39.0" }, { name = "pyright", specifier = ">=1.1.410" }, { name = "pytest", specifier = ">=8.4.2" }, { name = "ruff", specifier = ">=0.15.19" }, @@ -131,6 +135,15 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/e3/89/fa7f81db6134caa455c9351373ee934b5fdeed212eb939d6c42d80d683d7/ansible_runner-2.4.3-py3-none-any.whl", hash = "sha256:cdac6daa151a50084ffda710e769db23fa975fc0507796191d7708831b286e37", size = 80257, upload-time = "2026-03-16T18:42:13.72Z" }, ] +[[package]] +name = "asttokens" +version = "3.0.2" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/25/1e/faf0f247f6f881b98fc4d6d07e14085cb89d13665084e6d6ac1dc2c03d0b/asttokens-3.0.2.tar.gz", hash = "sha256:3ecdbd8f2cc195f53ccada3a613538bb5f9ef6f6869129f13e03c30a677b8fe2", size = 63136, upload-time = "2026-07-12T03:31:49.084Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/d4/2b/04b8a15f3a1c77bc79ddf5c73875327f34b4fa75982df2b76e45e402d364/asttokens-3.0.2-py3-none-any.whl", hash = "sha256:9da13157f5b28becde0bd374fc677dcd3c290614264eff096f167c469cd9f933", size = 28702, upload-time = "2026-07-12T03:31:47.542Z" }, +] + [[package]] name = "cffi" version = "2.1.0" @@ -165,7 +178,7 @@ wheels = [ [[package]] name = "copier" -version = "9.16.0" +version = "9.17.0" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "colorama" }, @@ -183,9 +196,9 @@ dependencies = [ { name = "questionary" }, { name = "typing-extensions" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/9c/c7/3cd18cd539ff41e8e7a95b0146dbace0ba332a4c97f468066b2c2daca2ed/copier-9.16.0.tar.gz", hash = "sha256:4db1a9861d0760f745cc6241f99be37b476f849b8ad700133e2f620b7df92eb2", size = 643997, upload-time = "2026-06-23T17:09:04.995Z" } +sdist = { url = "https://files.pythonhosted.org/packages/08/6a/73ccc2ce53c17a29d650246a625df6342f74cb9d4f15b78a77d311fc6007/copier-9.17.0.tar.gz", hash = "sha256:d966b043a15c74595f7904a6af89f3291135682f8313c4b71ef368811ed554f2", size = 645743, upload-time = "2026-07-13T14:33:04.838Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/0c/a4/cf8df35b6488c04cae9aa7338c7b531a87aae2515da3993d2906d49b1dc1/copier-9.16.0-py3-none-any.whl", hash = "sha256:fadc55a22db6fb0f99d52878d3df4df3fa9c314dd96b0d81fef785a5e803114b", size = 65483, upload-time = "2026-06-23T17:09:03.617Z" }, + { url = "https://files.pythonhosted.org/packages/22/2c/68dec88ee26f96af2d4d8ee9d8567313a0826f4fd6bc5ae4527be670d354/copier-9.17.0-py3-none-any.whl", hash = "sha256:fe4e7b59faf4c0e1386eccbb79c6797e6df33ca20d5de49a8372571d8669f2ad", size = 65954, upload-time = "2026-07-13T14:33:03.301Z" }, ] [[package]] @@ -226,6 +239,15 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/c2/e6/f60198ea8d9dfa15fff9ed4ca02ce362f6eadd9ba757dcc50634c4257b63/cryptography-49.0.0-cp39-abi3-win_amd64.whl", hash = "sha256:026ac7423e6fa66872d3bf889be5974507da3944f866f704fa200eadacd00001", size = 3785547, upload-time = "2026-06-12T20:02:26.847Z" }, ] +[[package]] +name = "decorator" +version = "5.3.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/60/8b/32f9823da46cde7df2087faa08cd98d01b908f8dcab982cdba9c84e85355/decorator-5.3.1.tar.gz", hash = "sha256:4cbcdd55a6efadb9dbea26b858f4fb3264567b52d69ca0d25b721b553f60ea82", size = 58084, upload-time = "2026-05-18T06:03:28.057Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/05/7f/798705f5296a58ca505d600456748d1be48078eac8a7050d8a98bc9edb89/decorator-5.3.1-py3-none-any.whl", hash = "sha256:f47fe6fdbd2edd623ecfe36875d37aba411624e2670dd395dddae1358689bb3c", size = 10365, upload-time = "2026-05-18T06:03:26.517Z" }, +] + [[package]] name = "dunamai" version = "1.26.1" @@ -250,6 +272,15 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/8a/0e/97c33bf5009bdbac74fd2beace167cab3f978feb69cc36f1ef79360d6c4e/exceptiongroup-1.3.1-py3-none-any.whl", hash = "sha256:a7a39a3bd276781e98394987d3a5701d0c4edffb633bb7a5144577f82c773598", size = 16740, upload-time = "2025-11-21T23:01:53.443Z" }, ] +[[package]] +name = "executing" +version = "2.2.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/cc/28/c14e053b6762b1044f34a13aab6859bbf40456d37d23aa286ac24cfd9a5d/executing-2.2.1.tar.gz", hash = "sha256:3632cc370565f6648cc328b32435bd120a1e4ebb20c77e3fdde9a13cd1e533c4", size = 1129488, upload-time = "2025-09-01T09:48:10.866Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/c1/ea/53f2148663b321f21b5a606bd5f191517cf40b7072c0497d3c92c4a13b1e/executing-2.2.1-py2.py3-none-any.whl", hash = "sha256:760643d3452b4d777d295bb167ccc74c64a81df23fb5e08eff250c425a4b2017", size = 28317, upload-time = "2025-09-01T09:48:08.5Z" }, +] + [[package]] name = "funcy" version = "2.0" @@ -273,14 +304,14 @@ wheels = [ [[package]] name = "gitpython" -version = "3.1.50" +version = "3.1.55" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "gitdb" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/33/f6/354ae6491228b5eb40e10d89c4d13c651fe1cf7556e35ebdded50cff57ce/gitpython-3.1.50.tar.gz", hash = "sha256:80da2d12504d52e1f998772dc5baf6e553f8d2fcfe1fcc226c9d9a2ee3372dcc", size = 219798, upload-time = "2026-05-06T04:01:26.571Z" } +sdist = { url = "https://files.pythonhosted.org/packages/b2/ab/ba0d29f2fa2277ed6256b2ac09003494045355f3a10bf32f351761287870/gitpython-3.1.55.tar.gz", hash = "sha256:781e3b1624dad81b24e9524bf0297b69786a0706db2cbceec1e2b05c38e5152f", size = 225071, upload-time = "2026-07-23T02:52:43.246Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/20/7a/1c6e3562dfd8950adbb11ffbc65d21e7c89d01a6e4f137fa981056de25c5/gitpython-3.1.50-py3-none-any.whl", hash = "sha256:d352abe2908d07355014abdd21ddf798c2a961469239afec4962e9da884858f9", size = 212507, upload-time = "2026-05-06T04:01:23.799Z" }, + { url = "https://files.pythonhosted.org/packages/20/6a/d3b8208d2f8aac66abe8ccc1c23fa2c89464ec42cc71a601e95d05902428/gitpython-3.1.55-py3-none-any.whl", hash = "sha256:7c9ec1e69c158c081632ab35c41471e302c96db2ae42165036a5d2403378812e", size = 216590, upload-time = "2026-07-23T02:52:41.932Z" }, ] [[package]] @@ -310,6 +341,40 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/cb/b1/3846dd7f199d53cb17f49cba7e651e9ce294d8497c8c150530ed11865bb8/iniconfig-2.3.0-py3-none-any.whl", hash = "sha256:f631c04d2c48c52b84d0d0549c99ff3859c98df65b3101406327ecc7d53fbf12", size = 7484, upload-time = "2025-10-18T21:55:41.639Z" }, ] +[[package]] +name = "ipython" +version = "8.39.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "colorama", marker = "sys_platform == 'win32'" }, + { name = "decorator" }, + { name = "exceptiongroup" }, + { name = "jedi" }, + { name = "matplotlib-inline" }, + { name = "pexpect", marker = "sys_platform != 'emscripten' and sys_platform != 'win32'" }, + { name = "prompt-toolkit" }, + { name = "pygments" }, + { name = "stack-data" }, + { name = "traitlets" }, + { name = "typing-extensions" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/40/18/f8598d287006885e7136451fdea0755af4ebcbfe342836f24deefaed1164/ipython-8.39.0.tar.gz", hash = "sha256:4110ae96012c379b8b6db898a07e186c40a2a1ef5d57a7fa83166047d9da7624", size = 5513971, upload-time = "2026-03-27T10:02:13.94Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/c0/56/4cc7fc9e9e3f38fd324f24f8afe0ad8bb5fa41283f37f1aaf9de0612c968/ipython-8.39.0-py3-none-any.whl", hash = "sha256:bb3c51c4fa8148ab1dea07a79584d1c854e234ea44aa1283bcb37bc75054651f", size = 831849, upload-time = "2026-03-27T10:02:07.846Z" }, +] + +[[package]] +name = "jedi" +version = "0.20.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "parso" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/46/b7/a3635f6a2d7cf5b5dd98064fc1d5fbbafcb25477bcea204a3a92145d158b/jedi-0.20.0.tar.gz", hash = "sha256:c3f4ccbd276696f4b19c54618d4fb18f9fc24b0aef02acf704b23f487daa1011", size = 3119416, upload-time = "2026-05-01T23:38:47.814Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/9a/93/242e2eab5fe682ffcb8b0084bde703a41d51e17ee0f3a31ff0d9d813620a/jedi-0.20.0-py2.py3-none-any.whl", hash = "sha256:7bdd9c2634f56713299976f4cbd59cb3fa92165cc5e05ea811fb253480728b67", size = 4884812, upload-time = "2026-05-01T23:38:43.919Z" }, +] + [[package]] name = "jinja2" version = "3.1.6" @@ -375,6 +440,18 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/d0/9e/0a02226640c255d1da0b8d12e24ac2aa6734da68bff14c05dd53b94a0fc3/markupsafe-3.0.3-cp310-cp310-win_arm64.whl", hash = "sha256:e2103a929dfa2fcaf9bb4e7c091983a49c9ac3b19c9061b6d5427dd7d14d81a1", size = 13932, upload-time = "2025-09-27T18:36:17.311Z" }, ] +[[package]] +name = "matplotlib-inline" +version = "0.2.2" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "traitlets" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/bd/c0/9f7c9a46090390368a4d7bcb76bb87a4a36c421e4c0792cdb53486ffac7a/matplotlib_inline-0.2.2.tar.gz", hash = "sha256:72f3fe8fce36b70d4a5b612f899090cd0401deddc4ea90e1572b9f4bfb058c79", size = 8150, upload-time = "2026-05-08T17:33:33.49Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/41/09/5b161152e2d90f7b87f781c2e1267494aef9c32498df793f73ad0a0a494a/matplotlib_inline-0.2.2-py3-none-any.whl", hash = "sha256:3c821cf1c209f59fb2d2d64abbf5b23b67bcb2210d663f9918dd851c6da1fcf6", size = 9534, upload-time = "2026-05-08T17:33:32.055Z" }, +] + [[package]] name = "mdurl" version = "0.1.2" @@ -424,6 +501,15 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/df/b2/87e62e8c3e2f4b32e5fe99e0b86d576da1312593b39f47d8ceef365e95ed/packaging-26.2-py3-none-any.whl", hash = "sha256:5fc45236b9446107ff2415ce77c807cee2862cb6fac22b8a73826d0693b0980e", size = 100195, upload-time = "2026-04-24T20:15:22.081Z" }, ] +[[package]] +name = "parso" +version = "0.8.7" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/30/4b/90c937815137d43ce71ba043cd3566221e9df6b9c805f24b5d138c9d40a7/parso-0.8.7.tar.gz", hash = "sha256:eaaac4c9fdd5e9e8852dc778d2d7405897ec510f2a298071453e5e3a07914bb1", size = 401824, upload-time = "2026-05-01T23:13:02.138Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/99/5d/8268b644392ee874ee82a635cd0df1773de230bde356c38de28e298392cc/parso-0.8.7-py2.py3-none-any.whl", hash = "sha256:a8926eb2a1b915486941fdbd31e86a4baf88fe8c210f25f2f35ecec5b574ca1c", size = 107025, upload-time = "2026-05-01T23:12:58.867Z" }, +] + [[package]] name = "pathspec" version = "1.1.1" @@ -447,11 +533,11 @@ wheels = [ [[package]] name = "platformdirs" -version = "4.10.0" +version = "4.11.0" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/d7/47/e4501f49c178ae1d9f4a75073fda4204f52647993f075a9db4d14930e0c5/platformdirs-4.10.0.tar.gz", hash = "sha256:31e761a6a0ca04faf7353ea759bdba55652be214725111e5aac52dfa29d4bef7", size = 31224, upload-time = "2026-05-28T03:32:53.587Z" } +sdist = { url = "https://files.pythonhosted.org/packages/78/9b/560e4be8e26f6fd133a03630a8df0c663b9e8d61b4ade152b72005aec83b/platformdirs-4.11.0.tar.gz", hash = "sha256:0555d18370482847566ffabcaa53ad7c6c1c29f195989ae1ed634a05f76ea1e0", size = 31953, upload-time = "2026-07-21T13:09:36.565Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/81/e6/cd9575ac904136b3cbf7aa7ee819ef86eedb7274e46f230e94ea4342e729/platformdirs-4.10.0-py3-none-any.whl", hash = "sha256:fb516cdb12eb0d857d0cd85a7c57cea4d060bee4578d6cf5a14dfdf8cbf8784a", size = 22743, upload-time = "2026-05-28T03:32:52.175Z" }, + { url = "https://files.pythonhosted.org/packages/7d/68/d8d58938dfb1370b266a1a729e6d77a985be23689a0496498ee17b2cbf90/platformdirs-4.11.0-py3-none-any.whl", hash = "sha256:360ccded2b7fce0af0ff80cc8f5942a1c5d99b0e856033acb030bfc634709e74", size = 23247, upload-time = "2026-07-21T13:09:35.422Z" }, ] [[package]] @@ -465,14 +551,14 @@ wheels = [ [[package]] name = "plumbum" -version = "2.0.1" +version = "2.0.2" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "typing-extensions" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/0c/6a/1d1b143420fcdfc8902f2db6b7d1d2325211461c5f2a43c849de7afad688/plumbum-2.0.1.tar.gz", hash = "sha256:61623f856dcb09eb20dcd5aa708dfb3cd04b6f4ab10224d39303b163bb1c4c61", size = 377668, upload-time = "2026-06-08T14:44:06.17Z" } +sdist = { url = "https://files.pythonhosted.org/packages/6c/d2/578712a979a50e9aba08ea173a38e7598461130d3c9899f176373bc03280/plumbum-2.0.2.tar.gz", hash = "sha256:233751d7819c9e6743ec1c2405927eb4fa52a284c7b894bd10e28106a9309a92", size = 390799, upload-time = "2026-07-22T04:13:12.157Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/59/2d/d741fbbbcba7eb6f9f1a829373c558114d59cb882b95f941aa0dc060861f/plumbum-2.0.1-py3-none-any.whl", hash = "sha256:27a454980f91689aae8f18242a36daaf2636219171cf0e6a849744aa1d6fff85", size = 164460, upload-time = "2026-06-08T14:44:04.75Z" }, + { url = "https://files.pythonhosted.org/packages/b5/55/fdd01894807d20fb6181f13590ae247585b10e909b42e6ada833379fe520/plumbum-2.0.2-py3-none-any.whl", hash = "sha256:a865771826c1d2cce0ca8b2a2c48aa0447ed7a518859e389a95f158e9c369547", size = 168804, upload-time = "2026-07-22T04:13:10.76Z" }, ] [[package]] @@ -496,6 +582,15 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/22/a6/858897256d0deac81a172289110f31629fc4cee19b6f01283303e18c8db3/ptyprocess-0.7.0-py2.py3-none-any.whl", hash = "sha256:4b41f3967fce3af57cc7e94b888626c18bf37a083e3651ca8feeb66d492fef35", size = 13993, upload-time = "2020-12-28T15:15:28.35Z" }, ] +[[package]] +name = "pure-eval" +version = "0.2.3" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/cd/05/0a34433a064256a578f1783a10da6df098ceaa4a57bbeaa96a6c0352786b/pure_eval-0.2.3.tar.gz", hash = "sha256:5f4e983f40564c576c7c8635ae88db5956bb2229d7e9237d03b3c0b0190eaf42", size = 19752, upload-time = "2024-07-21T12:58:21.801Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/8e/37/efad0257dc6e593a18957422533ff0f87ede7c9c6ea010a2177d738fb82f/pure_eval-0.2.3-py3-none-any.whl", hash = "sha256:1db8e35b67b3d218d818ae653e27f06c3aa420901fa7b081ca98cbedc874e0d0", size = 11842, upload-time = "2024-07-21T12:58:20.04Z" }, +] + [[package]] name = "pycparser" version = "3.0" @@ -621,6 +716,18 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/24/25/1de2678b631f5a49215c6c96fff41ba892b0a34df68d6d80292b1b48aa7f/pytest-9.1.1-py3-none-any.whl", hash = "sha256:37a86b45efb9a47a61a36449063e8e18d0cab3161329fc099eb21783169c4f0c", size = 386536, upload-time = "2026-06-19T10:58:31.347Z" }, ] +[[package]] +name = "pytest-mock" +version = "3.15.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "pytest" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/68/14/eb014d26be205d38ad5ad20d9a80f7d201472e08167f0bb4361e251084a9/pytest_mock-3.15.1.tar.gz", hash = "sha256:1849a238f6f396da19762269de72cb1814ab44416fa73a8686deac10b0d87a0f", size = 34036, upload-time = "2025-09-16T16:37:27.081Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/5a/cc/06253936f4a7fa2e0f48dfe6d851d9c56df896a9ab09ac019d70b760619c/pytest_mock-3.15.1-py3-none-any.whl", hash = "sha256:0a25e2eb88fe5168d535041d09a4529a188176ae608a6d249ee65abc0949630d", size = 10095, upload-time = "2025-09-16T16:37:25.734Z" }, +] + [[package]] name = "python-daemon" version = "3.1.2" @@ -695,27 +802,27 @@ wheels = [ [[package]] name = "ruff" -version = "0.15.20" +version = "0.15.22" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/43/dc/35b341fc554ba02f217fc10da57d1a75168cfbcf75b0ef2202176d4c4f2d/ruff-0.15.20.tar.gz", hash = "sha256:1416eb04349192646b54de98f146c4f59afe37d0decfc02c3cbbf396f3a28566", size = 4755489, upload-time = "2026-06-25T17:20:37.578Z" } +sdist = { url = "https://files.pythonhosted.org/packages/3a/06/ae069393fc66e8ff33036d4b368003833bf6e88ccf182e17e7a2f1c754fd/ruff-0.15.22.tar.gz", hash = "sha256:3f15175b1fb580126f58285a5dae6b2ea89000136d980c64499211f116b54809", size = 4785063, upload-time = "2026-07-16T15:14:13.244Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/94/d9/2d5014f0253ba541d2061d9fa7193f48e941c8b21bb88a7ff9bbe0bd0596/ruff-0.15.20-py3-none-linux_armv6l.whl", hash = "sha256:00e188c53e499c3c1637f73c91dcf2fb56d576cab76ce1be50a27c4e80e37078", size = 10839665, upload-time = "2026-06-25T17:19:44.702Z" }, - { url = "https://files.pythonhosted.org/packages/c6/d3/ac1798ba64f670698867fcfc591d50e7e421bef137db564858f619a30fcf/ruff-0.15.20-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:9ebd1fd9b9c95fc0bd7b2761aebec1f030013d2e193a2901b224af68fe47251b", size = 11208649, upload-time = "2026-06-25T17:19:48.787Z" }, - { url = "https://files.pythonhosted.org/packages/47/47/d3ac899991202095dfcf3d5176be4272642be3cf981a2f1a30f72a2afb95/ruff-0.15.20-py3-none-macosx_11_0_arm64.whl", hash = "sha256:c5b16cdd67ca108185cd36dce98c576350c03b1660a751de725fb049193a0632", size = 10622638, upload-time = "2026-06-25T17:19:51.354Z" }, - { url = "https://files.pythonhosted.org/packages/33/13/4e043fe30aa94d4ff5213a9881fc296d12960f5971b234a5263fdc225312/ruff-0.15.20-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:3413bb3c3d2ca6a8208f1f4809cd2dca3c6de6d0b491c0e70847672bde6e6efd", size = 10984227, upload-time = "2026-06-25T17:19:54.044Z" }, - { url = "https://files.pythonhosted.org/packages/76/e6/92e7bf40388bc5800073b96564f56264f7e48bfd1a498f5ced6ae6d5a769/ruff-0.15.20-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:bd7ec42b3bb3da066488db093308a69c4ac5ee6d2af333a86ba6e2eb2e7dd44b", size = 10622882, upload-time = "2026-06-25T17:19:57.037Z" }, - { url = "https://files.pythonhosted.org/packages/13/7a/43460be3f24495a3aa46d4b16873e2c4941b3b5f0b00cf88c03b7b94b339/ruff-0.15.20-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:e1a36ad0eb77fba9aabfb69ede54de6f376d04ac18ebea022847046d340a8267", size = 11474808, upload-time = "2026-06-25T17:20:00.357Z" }, - { url = "https://files.pythonhosted.org/packages/27/a0/f37077884873221c6b33b4ab49eb18f9f88e54a16a25a5bca59bef46dd66/ruff-0.15.20-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:b6df3b1e4610432f0386dba04d853b5f08cbbc903410c6fcc02f620f05aff53c", size = 12293094, upload-time = "2026-06-25T17:20:03.446Z" }, - { url = "https://files.pythonhosted.org/packages/a6/74/165545b60256a9704c21ac0ec4a0d07933b320812f9584836c9f4aca4292/ruff-0.15.20-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:e89f198a1ea6ef0d727c1cf16088bc91a6cb0ab947dedc966715691647186eae", size = 11526176, upload-time = "2026-06-25T17:20:06.301Z" }, - { url = "https://files.pythonhosted.org/packages/86/b1/a976a136d40ade83ce743578399865f57001003a409acadc0ecbb3051082/ruff-0.15.20-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:309809086c2acb67624950a3c8133e80f32d0d3e27106c0cd60ff26657c9f24b", size = 11520767, upload-time = "2026-06-25T17:20:09.191Z" }, - { url = "https://files.pythonhosted.org/packages/19/0f/f032696cb01c9b54c0263fa393474d7758f1cdc021a01b04e3cbc2500999/ruff-0.15.20-py3-none-manylinux_2_31_riscv64.whl", hash = "sha256:2d2374caa2f2c2f9e2b7da0a50802cfb8b79f55a9b5e49379f564544fbf56487", size = 11500132, upload-time = "2026-06-25T17:20:13.602Z" }, - { url = "https://files.pythonhosted.org/packages/4b/f4/51b1a14bc69e8c224b15dab9cce8e99b425e0455d462caa2b3c9be2b6a8e/ruff-0.15.20-py3-none-musllinux_1_2_aarch64.whl", hash = "sha256:a1ed17b65293e0c2f22fc387bc13198a5de94bf4429589b0ff6946b0feaf21a3", size = 10943828, upload-time = "2026-06-25T17:20:16.635Z" }, - { url = "https://files.pythonhosted.org/packages/71/4b/fe267640783cd02bf6c5cc290b1df1051be2ec294c678b5c15fe19e52343/ruff-0.15.20-py3-none-musllinux_1_2_armv7l.whl", hash = "sha256:f701305e66b38ea6c91882490eb73459796808e4c6362a1b765255e0cdcd4053", size = 10645418, upload-time = "2026-06-25T17:20:19.4Z" }, - { url = "https://files.pythonhosted.org/packages/b0/c0/a65aa4ec2f5e87a1df32dc3ec1fede434fe3dfd5cbcf3b503cafc676ab54/ruff-0.15.20-py3-none-musllinux_1_2_i686.whl", hash = "sha256:5b9c0c367ad8e5d0d5b5b8537864c469a0a0e55417aadfbeca41fa61333be9f4", size = 11211770, upload-time = "2026-06-25T17:20:22.033Z" }, - { url = "https://files.pythonhosted.org/packages/5a/a4/0caa331d954ae2723d729d351c989cb4ca8b6077d5c6c2cb6de75e98c041/ruff-0.15.20-py3-none-musllinux_1_2_x86_64.whl", hash = "sha256:01cc00dd58f0df339d0e902219dd53990ea99996a0344e5d9cc8d45d5307e460", size = 11618698, upload-time = "2026-06-25T17:20:25.259Z" }, - { url = "https://files.pythonhosted.org/packages/10/9b/5f14927848d2fd4aa891fd88d883788c5a7baba561c7874732364045708c/ruff-0.15.20-py3-none-win32.whl", hash = "sha256:ed65ef510e43a137207e0f01cfcf998aeddb1aeeda5c9d35023e910284d7cf21", size = 10857322, upload-time = "2026-06-25T17:20:28.612Z" }, - { url = "https://files.pythonhosted.org/packages/fa/f0/fe47c501f9dea92a26d788ff98bb5d92ed4cb4c88792c5c88af6b697dc8e/ruff-0.15.20-py3-none-win_amd64.whl", hash = "sha256:a525c81c70fb0380344dd1d8745d8cc1c890b7fc94a58d5a07bd8eb9557b8415", size = 11993274, upload-time = "2026-06-25T17:20:31.871Z" }, - { url = "https://files.pythonhosted.org/packages/d7/2b/9555445e1201d92b3195f45cdb153a0b68f24e0a4273f6e3d5ab46e212bb/ruff-0.15.20-py3-none-win_arm64.whl", hash = "sha256:2f5b2a6d614e8700388806a14996c40fab2c47b819ef57d790a34878858ed9ca", size = 11343498, upload-time = "2026-06-25T17:20:35.03Z" }, + { url = "https://files.pythonhosted.org/packages/23/18/ee54b7ae1e121be7a28ea6da4b67564ebb0530e183a54415ab7e3bcd2c4e/ruff-0.15.22-py3-none-linux_armv6l.whl", hash = "sha256:44423e73493737f5e7c5b41d475483898ff37afcdae38bc3da5085e29af1c2d8", size = 10781258, upload-time = "2026-07-16T15:13:19.452Z" }, + { url = "https://files.pythonhosted.org/packages/2f/d2/2520cb14761ddbeaf57642a76942fc36adcbdbe53b4532241995f6fc485c/ruff-0.15.22-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:b82c6482946e9eda7ff2e091d25b8bad3f718684e1916d41bd56873cee05b697", size = 10999477, upload-time = "2026-07-16T15:13:23.318Z" }, + { url = "https://files.pythonhosted.org/packages/c9/10/74e53572aa758dfaa678c2a2646b5c5515d884b7ca56be4d2ce03ca4b560/ruff-0.15.22-py3-none-macosx_11_0_arm64.whl", hash = "sha256:11c1c715af53a09f714e011106bffc419751ec8232fcb5da42173284ea3fec6f", size = 10466716, upload-time = "2026-07-16T15:13:26.162Z" }, + { url = "https://files.pythonhosted.org/packages/1e/cc/44eaaf0844e028182f2d0a8f2190d0f359159aed0a9e5ab861d892f1ae2a/ruff-0.15.22-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:742a29cf29bddb7c8327895d6a10e0e6c5b38a96dd407af9b5d0857f809c0576", size = 10892644, upload-time = "2026-07-16T15:13:29.229Z" }, + { url = "https://files.pythonhosted.org/packages/9f/21/8edf559014d2b0f82beea19cfb713993ad802ccda16868769979c6090a84/ruff-0.15.22-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:72af58b951b0ae395935ae79763dc349bc0eb706319d28f7a33ad2cfb3cfc178", size = 10576719, upload-time = "2026-07-16T15:13:32.35Z" }, + { url = "https://files.pythonhosted.org/packages/bf/1e/3a13abd392a3b50b62e5938a831f9ab6e588358cacad5c18545b716d2182/ruff-0.15.22-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:62d425005c1835eb24e2ee4161cb90e8db263415f4a71c8c72c33abaa6c0c224", size = 11376494, upload-time = "2026-07-16T15:13:35.958Z" }, + { url = "https://files.pythonhosted.org/packages/bf/3e/422d3d95bcf04dd78e1aeac22184d4f9a8fb2c01865d39d44618484a0317/ruff-0.15.22-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:e8b9b3f8779a4f08c969defc3c8c35abffaa757e601ed5ae66d6d1db6519969a", size = 12208370, upload-time = "2026-07-16T15:13:39.185Z" }, + { url = "https://files.pythonhosted.org/packages/1e/91/5d065a0e0a02bf4813f5119ad278462eed081d2b832eb7c021ade0ec9e65/ruff-0.15.22-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:1e0dd1b2e4d3d585f897a0d137cbf4eaf6223bef4e8ce34d6bb12556c5f9249e", size = 11581098, upload-time = "2026-07-16T15:13:42.132Z" }, + { url = "https://files.pythonhosted.org/packages/f6/f9/a0d4871d12fae702eb1f41b686caf05f1f8b124dc6db6f784f53d74918fa/ruff-0.15.22-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:365523eb91d9224e1bcb03b022fbf0facb8f9e23792a2c53d9d4b3924bdbdebb", size = 11399422, upload-time = "2026-07-16T15:13:45.2Z" }, + { url = "https://files.pythonhosted.org/packages/18/80/c843a5176cddbceb0b7e8dd41cf9993490796c1c469348d384f5a5c13c56/ruff-0.15.22-py3-none-manylinux_2_31_riscv64.whl", hash = "sha256:fabfd168afdf29fee5be98b831efa9683c94d7c5a3b58b9ce5a2e38444589a74", size = 11381683, upload-time = "2026-07-16T15:13:48.46Z" }, + { url = "https://files.pythonhosted.org/packages/d4/00/8485de0ae92239438a36cfc51350db9b9e85c9ebdfaea91b18e422706662/ruff-0.15.22-py3-none-musllinux_1_2_aarch64.whl", hash = "sha256:225dbf095a87f1d9f90f5fd7924d2613ee452a75a4308c63a8f50f761787aa7c", size = 10850295, upload-time = "2026-07-16T15:13:51.655Z" }, + { url = "https://files.pythonhosted.org/packages/fa/91/24977ec2ec72eaf15e4394ace2959fdff2dd1e14f03e005e838023407169/ruff-0.15.22-py3-none-musllinux_1_2_armv7l.whl", hash = "sha256:1877d63b9d24ed278744f1523fd11b85540566d54641f97c566d7d9dc5ca5296", size = 10579640, upload-time = "2026-07-16T15:13:54.79Z" }, + { url = "https://files.pythonhosted.org/packages/9c/47/9b51216951974df1f263ac19da550d34252e0ed7218c25f10c5ef9ed7517/ruff-0.15.22-py3-none-musllinux_1_2_i686.whl", hash = "sha256:a1606c510bd7215680d32efab38965f7cdec3ef69f5170a3f4791404ffdd5262", size = 11105077, upload-time = "2026-07-16T15:13:57.915Z" }, + { url = "https://files.pythonhosted.org/packages/c2/47/20e9d4a3b8016778acea5fc32bb50d35d207500a17ddb529ffa6996feef8/ruff-0.15.22-py3-none-musllinux_1_2_x86_64.whl", hash = "sha256:630479b18625f5ffc373f77603a22a9f8ac0acd7ff0501178b5db28ec71e9c64", size = 11490980, upload-time = "2026-07-16T15:14:01.032Z" }, + { url = "https://files.pythonhosted.org/packages/4d/76/3f72d8fc38c1cb77b38c56a70da9d0c17700cc1cc50f9649c9d3c8f5ba71/ruff-0.15.22-py3-none-win32.whl", hash = "sha256:e5ba0e4a13fd14abbed2a77b517a3911290c6c6c59ef67784328d1668fab76cf", size = 10789165, upload-time = "2026-07-16T15:14:04.16Z" }, + { url = "https://files.pythonhosted.org/packages/cb/46/4965251734c2b6fcdca1b1b187d20bcac3af0ee5b083b89c910bb961ce3a/ruff-0.15.22-py3-none-win_amd64.whl", hash = "sha256:9be63ba1eb936acd2d1342fb8337c356353706fce233b2a15a09a97037e6acde", size = 11938297, upload-time = "2026-07-16T15:14:07.316Z" }, + { url = "https://files.pythonhosted.org/packages/57/c9/e69b1ff4c8b69093ef08b8919ab767af0569666865b39c30a8795d88d3c6/ruff-0.15.22-py3-none-win_arm64.whl", hash = "sha256:e1168075b72158510839f250027659cdd78476f40507dd517892304c41318661", size = 11298172, upload-time = "2026-07-16T15:14:10.51Z" }, ] [[package]] @@ -736,6 +843,20 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/c1/d4/59e74daffcb57a07668852eeeb6035af9f32cbfd7a1d2511f17d2fe6a738/smmap-5.0.3-py3-none-any.whl", hash = "sha256:c106e05d5a61449cf6ba9a1e650227ecfb141590d2a98412103ff35d89fc7b2f", size = 24390, upload-time = "2026-03-09T03:43:24.361Z" }, ] +[[package]] +name = "stack-data" +version = "0.6.3" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "asttokens" }, + { name = "executing" }, + { name = "pure-eval" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/28/e3/55dcc2cfbc3ca9c29519eb6884dd1415ecb53b0e934862d3559ddcb7e20b/stack_data-0.6.3.tar.gz", hash = "sha256:836a778de4fec4dcd1dcd89ed8abff8a221f58308462e1c4aa2a3cf30148f0b9", size = 44707, upload-time = "2023-09-30T13:58:05.479Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/f1/7b/ce1eafaf1a76852e2ec9b22edecf1daa58175c090266e9f6c64afcd81d91/stack_data-0.6.3-py3-none-any.whl", hash = "sha256:d5558e0c25a4cb0853cddad3d77da9891a08cb85dd9f9f91b9f8cd66e511e695", size = 24521, upload-time = "2023-09-30T13:58:03.53Z" }, +] + [[package]] name = "tomli" version = "2.4.1" @@ -745,9 +866,18 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/7b/61/cceae43728b7de99d9b847560c262873a1f6c98202171fd5ed62640b494b/tomli-2.4.1-py3-none-any.whl", hash = "sha256:0d85819802132122da43cb86656f8d1f8c6587d54ae7dcaf30e90533028b49fe", size = 14583, upload-time = "2026-03-25T20:22:03.012Z" }, ] +[[package]] +name = "traitlets" +version = "5.15.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/57/a9/a2584b8313b89f94869ddb3c4074617a691de1812a614d2d50e32ca5a7a6/traitlets-5.15.1.tar.gz", hash = "sha256:7b1c07854fe25acb39e009bae49f11b79ff6cbb2f27999104e9110e7a6b53722", size = 163344, upload-time = "2026-06-03T12:26:06.181Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/96/8d/1080ee4c231f361b6ce4470d556c8c435b67c7e0753aaa641497ee92f88b/traitlets-5.15.1-py3-none-any.whl", hash = "sha256:770a53705f84b81ac107e83a1b3328ff2dae16094d8fc3cfc004e4b22dfd8e92", size = 85858, upload-time = "2026-06-03T12:26:04.395Z" }, +] + [[package]] name = "typer" -version = "0.26.8" +version = "0.27.0" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "annotated-doc" }, @@ -755,9 +885,9 @@ dependencies = [ { name = "rich" }, { name = "shellingham" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/7c/f7/68adc395201b20b872d68e975386832e8005ffeacedd43a1d837a32815be/typer-0.26.8.tar.gz", hash = "sha256:c244a6bd558886fe3f8780efb6bdd28bb9aff005a94eedebaa5cb32926fe2f7e", size = 202097, upload-time = "2026-06-26T09:22:45.705Z" } +sdist = { url = "https://files.pythonhosted.org/packages/37/78/fda3361b56efc27944f24225f6ecd13d96d6fcfe37bd0eb34e2f4c63f9fc/typer-0.27.0.tar.gz", hash = "sha256:629bd12ea5d13a17148125d9a264f949eb171fb3f120f9b04d85873cab054fa5", size = 203430, upload-time = "2026-07-15T19:21:07.007Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/80/87/b9fd69c92c6102a066e1b86a35243f53e70bd4c709f2a26d9f4fee4f4dc0/typer-0.26.8-py3-none-any.whl", hash = "sha256:3512ca79ac5c11113414b36e80281b872884477722440691c89d1112e321a49c", size = 122564, upload-time = "2026-06-26T09:22:44.72Z" }, + { url = "https://files.pythonhosted.org/packages/40/03/26a383c9e58c213199d1aad1c3d353cfc22d4444ec6d2c0bf8ad02523843/typer-0.27.0-py3-none-any.whl", hash = "sha256:6f4b27631e47f077871b7dc30e933ec0131c1390fbe0e387ea5574b5bac9ccf1", size = 122716, upload-time = "2026-07-15T19:21:05.553Z" }, ] [[package]] diff --git a/qt/A_BD_ServiceManager.ui b/qt/A_BD_ServiceManager.ui index 329f51a..ff496c8 100644 --- a/qt/A_BD_ServiceManager.ui +++ b/qt/A_BD_ServiceManager.ui @@ -1322,4 +1322,4 @@ caLabel[statusTip="Operation"]{ - \ No newline at end of file + diff --git a/templates/service/{{service_name_lower}}/current/app/src/agebd_{{service_name_lower_underscores}}/main.py.jinja b/templates/service/{{service_name_lower}}/current/app/src/agebd_{{service_name_lower_underscores}}/main.py.jinja index d732eb1..8dcadc1 100644 --- a/templates/service/{{service_name_lower}}/current/app/src/agebd_{{service_name_lower_underscores}}/main.py.jinja +++ b/templates/service/{{service_name_lower}}/current/app/src/agebd_{{service_name_lower_underscores}}/main.py.jinja @@ -6,8 +6,6 @@ from agebd.utils import init_logging from agebd_{{service_name_lower_underscores}} import PVs, Service -# TODO: remove: cicd test 10 - def main( log_level: LogLevel = typer.Option( LogLevel.INFO, diff --git a/templates/service/{{service_name_lower}}/current/app/src/agebd_{{service_name_lower_underscores}}/service.py.jinja b/templates/service/{{service_name_lower}}/current/app/src/agebd_{{service_name_lower_underscores}}/service.py.jinja index 091eac9..ff61667 100644 --- a/templates/service/{{service_name_lower}}/current/app/src/agebd_{{service_name_lower_underscores}}/service.py.jinja +++ b/templates/service/{{service_name_lower}}/current/app/src/agebd_{{service_name_lower_underscores}}/service.py.jinja @@ -2,11 +2,10 @@ import time from epics import dbr -from agebd.pv import LocalPVLink +from agebd.pv import LocalPVLink, get_pv_class from agebd.service.base import BaseService from agebd.service.pvs import BasePVs from agebd.utils import printgetversion -from agebd.pv import get_pv_class __version__ = printgetversion(__file__) @@ -32,18 +31,14 @@ class PVs(BasePVs): # Option 2: ## Service specific PVs from dedicated IOC - self.my_pv2 = PV( - "AGEBD-{{ service_name_upper }}:BO" - ) # TODO: is this a good PV to use? + self.my_pv2 = PV("AGEBD-{{ service_name_upper }}:BO") # TODO: is this a good PV to use? ## Service specific PVs from other IOCs # ... # TODO: class attribute names usually lowercase # usually it is advisable to trigger the mainloop of a service on changes of certain PVs: - self.CallbackPV = PV( - "AGEBD-{{ service_name_upper }}:CALLBACK", auto_monitor=dbr.DBE_VALUE - ) # TODO: ok? + self.CallbackPV = PV("AGEBD-{{ service_name_upper }}:CALLBACK", auto_monitor=dbr.DBE_VALUE) class Service(BaseService[PVs]):