diff --git a/cli/pyproject.toml b/cli/pyproject.toml index 0c8471e..00d3ceb 100644 --- a/cli/pyproject.toml +++ b/cli/pyproject.toml @@ -14,6 +14,7 @@ dependencies = [ "copier>=9.10.3", "gitpython>=3.1.50", "pydantic>=2.13.4", + "pydantic-settings>=2.14.2", "pyyaml>=6.0.3", "typer>=0.23.2", ] diff --git a/cli/src/config/paths.py b/cli/src/config/paths.py new file mode 100644 index 0000000..d3050d3 --- /dev/null +++ b/cli/src/config/paths.py @@ -0,0 +1,35 @@ +from pathlib import Path + +from pydantic import DirectoryPath, FilePath +from pydantic_settings import BaseSettings + +from core.utils import get_git_root + +REPO_ROOT = get_git_root(__file__) + + +class Paths(BaseSettings): + # fmt: off + 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" + # fmt: on + + def relative(self, path: Path) -> str: + """ + Convert any absolute path into a string path relative to repo_root. + """ + return str(path.relative_to(self.repo_root)) + + def get_service_dir(self, service_dir_name: str) -> Path: + """ + Helper to get a service path dynamically. + """ + return self.repo_root / "services" / service_dir_name / "current" + + +paths = Paths() diff --git a/cli/src/core/models.py b/cli/src/core/models.py index 3eb2306..6006982 100644 --- a/cli/src/core/models.py +++ b/cli/src/core/models.py @@ -1,27 +1,13 @@ import re import xml.etree.ElementTree as ET +from pathlib import Path from typing import Any, ClassVar import yaml from pydantic import BaseModel, ConfigDict, field_validator +from config.paths import paths from core.enums import ServiceStatus -from core.utils import get_git_root - -REPO_ROOT = get_git_root(__file__) -CONFIG_DIR = REPO_ROOT / "config" -SERVICE_REGISTRY_FILENAME = CONFIG_DIR / "services_registry.yml" - -MASTER_SERVICE_DIR = REPO_ROOT / "services" / "master" / "current" -MASTER_SERVICE_CONFIG_DIR = MASTER_SERVICE_DIR / "app" / "config" -MASTER_HLA_NAMES_FILENAME = MASTER_SERVICE_CONFIG_DIR / "hla_names.yml" - -IOCS_OVERVIEW_FILENAME = REPO_ROOT / "docs" / "ioc" / "user" / "ioc_overview.md" - -SERVICE_MANAGER_UI_FILENAME = REPO_ROOT / "qt" / "A_BD_ServiceManager.ui" - -MASTER_SERVICE_DIR = REPO_ROOT / "services" / "master" / "current" -MASTER_IOC_SUBS_FILE = MASTER_SERVICE_DIR / "ioc" / "AGEBD-CPCL-MASTER_main.subs" class Service(BaseModel): @@ -60,7 +46,7 @@ class Service(BaseModel): class ServiceRegistry(BaseModel): - config_path: ClassVar[str] = str(SERVICE_REGISTRY_FILENAME) + config_path: ClassVar[Path] = paths.service_registry_file next_available_ioc_port: int services: list[dict] @@ -101,7 +87,7 @@ class ServiceRegistry(BaseModel): class MasterHLANames(BaseModel): - config_path: ClassVar[str] = str(MASTER_HLA_NAMES_FILENAME) + config_path: ClassVar[Path] = paths.master_hla_names_file hla_apps: list[str] @@ -122,7 +108,7 @@ class MasterHLANames(BaseModel): class IocsOverview(BaseModel): - config_path: ClassVar[str] = str(IOCS_OVERVIEW_FILENAME) + config_path: ClassVar[Path] = paths.iocs_overview_filename content: str @@ -148,7 +134,7 @@ class IocsOverview(BaseModel): class MasterIocSubs(BaseModel): - config_path: ClassVar[str] = str(MASTER_IOC_SUBS_FILE) + config_path: ClassVar[Path] = paths.master_ioc_subs_file content: str @@ -193,7 +179,7 @@ class MasterIocSubs(BaseModel): # TODO: maybe separate models into their own files class ServiceManagerUI(BaseModel): model_config = ConfigDict(arbitrary_types_allowed=True) - config_path: ClassVar[str] = str(SERVICE_MANAGER_UI_FILENAME) + config_path: ClassVar[Path] = paths.service_manager_ui_file tree: Any diff --git a/cli/src/core/service_creator.py b/cli/src/core/service_creator.py index 2ac7201..b48be6b 100644 --- a/cli/src/core/service_creator.py +++ b/cli/src/core/service_creator.py @@ -6,6 +6,7 @@ from pathlib import Path import copier import git +from config.paths import paths from core.exceptions import UVError from core.git import ( assert_clean_repo, @@ -30,9 +31,6 @@ SERVICE_TEMPLATES_DIR = REPO_ROOT / "templates" / "service" SERVICE_DEST_DIR = REPO_ROOT / "services" -# TODO: lots of hardcoded paths. Create config object? - - class ServiceCreator: def __init__(self, user: str, ioc_description: str, ui_name: str, ui_filename: str) -> None: self.user = user @@ -81,12 +79,12 @@ class ServiceCreator: service_manager.write_to_file() files_to_stage = [ - f"services/{service.dir_name}/current", - "config/services_registry.yml", - "services/master/current/app/config/hla_names.yml", - "services/master/current/ioc/AGEBD-CPCL-MASTER_main.subs", - "docs/ioc/user/ioc_overview.md", - "qt/A_BD_ServiceManager.ui", + paths.relative(paths.get_service_dir(service.dir_name)), + paths.relative(paths.service_registry_file), + paths.relative(paths.master_hla_names_file), + paths.relative(paths.master_ioc_subs_file), + paths.relative(paths.iocs_overview_filename), + paths.relative(paths.service_manager_ui_file), ] # TODO: can't push if branch already exists in remote git_push_changes( diff --git a/cli/tests/test_paths.py b/cli/tests/test_paths.py new file mode 100644 index 0000000..1269557 --- /dev/null +++ b/cli/tests/test_paths.py @@ -0,0 +1,8 @@ +from config.paths import paths + + +def test_paths(): + """ + Will fail when paths is imported if a path is invalid + """ + assert True diff --git a/cli/tests/test_service.py b/cli/tests/test_service.py new file mode 100644 index 0000000..ee03a8e --- /dev/null +++ b/cli/tests/test_service.py @@ -0,0 +1,6 @@ +from core.models import IocsOverview +from core.service_creator import ServiceCreator + + +def test_add_new_service(): + pass diff --git a/cli/uv.lock b/cli/uv.lock index 1486664..64ded5d 100644 --- a/cli/uv.lock +++ b/cli/uv.lock @@ -8,12 +8,14 @@ version = "0.1.0" source = { editable = "../packages/agebd" } dependencies = [ { name = "h5py" }, + { name = "pyepics" }, { name = "typer" }, ] [package.metadata] requires-dist = [ { name = "h5py", specifier = ">=3.16.0" }, + { name = "pyepics", specifier = ">=3.5.10" }, { name = "typer", specifier = ">=0.23.2" }, ] @@ -36,6 +38,7 @@ dependencies = [ { name = "copier" }, { name = "gitpython" }, { name = "pydantic" }, + { name = "pydantic-settings" }, { name = "pyyaml" }, { name = "typer" }, ] @@ -55,6 +58,7 @@ requires-dist = [ { name = "copier", specifier = ">=9.10.3" }, { name = "gitpython", specifier = ">=3.1.50" }, { name = "pydantic", specifier = ">=2.13.4" }, + { name = "pydantic-settings", specifier = ">=2.14.2" }, { name = "pyyaml", specifier = ">=6.0.3" }, { name = "typer", specifier = ">=0.23.2" }, ] @@ -541,6 +545,33 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/01/aa/62f082da2c91fac1c234bc9ee0066257ce83f0604abd72e4c9d5991f2d84/pydantic_core-2.46.4-cp310-cp310-win_amd64.whl", hash = "sha256:8358a950c8909158e3df31538a7e4edc2d7265a7c54b47f0864d9e5bae9dcebf", size = 2074311, upload-time = "2026-05-06T13:39:59.922Z" }, ] +[[package]] +name = "pydantic-settings" +version = "2.14.2" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "pydantic" }, + { name = "python-dotenv" }, + { name = "typing-inspection" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/5c/b5/8f48e906c3e0205276e8bd8cb7512217a87b2685304d64be27cad5b3019f/pydantic_settings-2.14.2.tar.gz", hash = "sha256:c19dd64b19097f1de80184f0cc7b0272a13ae6e170cbf240a3e27e381ed14a5f", size = 237700, upload-time = "2026-06-19T13:44:56.324Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/77/c1/6e422f34e569cf8e18df68d1939c81c099d2b61e4f7d9621c8a77560799c/pydantic_settings-2.14.2-py3-none-any.whl", hash = "sha256:a20c97b37910b6550d5ea50fbcc2d4187defe58cd57070b73863d069419c9440", size = 61715, upload-time = "2026-06-19T13:44:55.02Z" }, +] + +[[package]] +name = "pyepics" +version = "3.5.10" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "numpy" }, + { name = "pyparsing" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/69/57/29e1e9ef11ba2a3419f489946ae1d8190025a1e4f4e1c1686758bb2b6e0a/pyepics-3.5.10.tar.gz", hash = "sha256:f390cf9be40aba757b9528888114bc14d8db01086d0c93914720b1772460375b", size = 6150481, upload-time = "2026-05-20T18:44:36.336Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/d6/36/e517fd83bd97d6bfe4105e0c6b322070c5cc36fb56a8fab2866e319f8804/pyepics-3.5.10-py3-none-any.whl", hash = "sha256:c9fac2c54f2b595268e66fc348cf5ec24e63718e7dbd07d73d1b37d4da6bfee1", size = 5332376, upload-time = "2026-05-20T18:44:34.242Z" }, +] + [[package]] name = "pygments" version = "2.20.0" @@ -550,6 +581,15 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/f4/7e/a72dd26f3b0f4f2bf1dd8923c85f7ceb43172af56d63c7383eb62b332364/pygments-2.20.0-py3-none-any.whl", hash = "sha256:81a9e26dd42fd28a23a2d169d86d7ac03b46e2f8b59ed4698fb4785f946d0176", size = 1231151, upload-time = "2026-03-29T13:29:30.038Z" }, ] +[[package]] +name = "pyparsing" +version = "3.3.2" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/f3/91/9c6ee907786a473bf81c5f53cf703ba0957b23ab84c264080fb5a450416f/pyparsing-3.3.2.tar.gz", hash = "sha256:c777f4d763f140633dcb6d8a3eda953bf7a214dc4eff598413c070bcdc117cbc", size = 6851574, upload-time = "2026-01-21T03:57:59.36Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/10/bd/c038d7cc38edc1aa5bf91ab8068b63d4308c66c4c8bb3cbba7dfbc049f9c/pyparsing-3.3.2-py3-none-any.whl", hash = "sha256:850ba148bd908d7e2411587e247a1e4f0327839c40e2e5e6d05a007ecc69911d", size = 122781, upload-time = "2026-01-21T03:57:55.912Z" }, +] + [[package]] name = "pyright" version = "1.1.411" @@ -593,6 +633,15 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/45/3c/b88167e2d6785c0e781ee5d498b07472aeb9b6765da3b19e7cc9e0813841/python_daemon-3.1.2-py3-none-any.whl", hash = "sha256:b906833cef63502994ad48e2eab213259ed9bb18d54fa8774dcba2ff7864cec6", size = 30872, upload-time = "2024-12-03T08:41:03.322Z" }, ] +[[package]] +name = "python-dotenv" +version = "1.2.2" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/82/ed/0301aeeac3e5353ef3d94b6ec08bbcabd04a72018415dcb29e588514bba8/python_dotenv-1.2.2.tar.gz", hash = "sha256:2c371a91fbd7ba082c2c1dc1f8bf89ca22564a087c2c287cd9b662adde799cf3", size = 50135, upload-time = "2026-03-01T16:00:26.196Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/0b/d7/1959b9648791274998a9c3526f6d0ec8fd2233e4d4acce81bbae76b44b2a/python_dotenv-1.2.2-py3-none-any.whl", hash = "sha256:1d8214789a24de455a8b8bd8ae6fe3c6b69a5e3d64aa8a8e5d68e694bbcb285a", size = 22101, upload-time = "2026-03-01T16:00:25.09Z" }, +] + [[package]] name = "pyyaml" version = "6.0.3"