diff --git a/.github/workflows/python.yaml b/.github/workflows/python.yaml index 1cd7da5b..0a778f04 100644 --- a/.github/workflows/python.yaml +++ b/.github/workflows/python.yaml @@ -47,7 +47,7 @@ jobs: with: version: "0.5.20" - - run: uv sync + - run: uv sync --extra dev - run: uv run pytest . - run: uv run mypy . - run: uv run ruff check . diff --git a/pyproject.toml b/pyproject.toml index 28b9ec1a..c608d23d 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -2,6 +2,11 @@ requires = ["hatchling"] build-backend = "hatchling.build" +[tool.hatch.build.targets.wheel.hooks.mypyc] +dependencies = ["hatch-mypyc"] +require-runtime-dependencies = true +require-runtime-features = ["dev"] + [project] name = "nyl" version = "0.8.1" @@ -28,11 +33,8 @@ requires-python = ">=3.11" readme = "README.md" license = {text = "MIT"} -[project.scripts] -nyl = "nyl.commands:app" - -[tool.uv] -dev-dependencies = [ +[project.optional-dependencies] +dev = [ "kubernetes-stubs>=22.6.0.post1", "mypy>=1.13.0", "pytest>=8.2.2", @@ -41,6 +43,9 @@ dev-dependencies = [ "types-requests>=2.32.0.20240712", ] +[project.scripts] +nyl = "nyl.commands:app" + [tool.mypy] explicit_package_bases = true namespace_packages = true diff --git a/src/nyl/commands/tun.py b/src/nyl/commands/tun.py index 7ed39baf..9b4ab45c 100644 --- a/src/nyl/commands/tun.py +++ b/src/nyl/commands/tun.py @@ -12,7 +12,7 @@ from nyl.commands import PROVIDER from nyl.profiles import get_tunnel_spec from nyl.profiles.config import ProfileConfig -from nyl.profiles.tunnel import TunnelManager, TunnelSpec, TunnelStatus +from nyl.profiles.tunnel import TunnelManager, TunnelSpec, TunnelSpecForwarding, TunnelSpecLocator, TunnelStatus from nyl.tools.fs import shorter_path from nyl.tools.typer import new_typer @@ -98,8 +98,8 @@ def start(profile_name: str = Argument("default", envvar="NYL_PROFILE")) -> None # TODO: Know the Kubernetes host/port to forward. spec = TunnelSpec( - locator=TunnelSpec.Locator(str(config.file), profile_name), - forwardings={"kubernetes": TunnelSpec.Forwarding(host="localhost", port=6443)}, + locator=TunnelSpecLocator(str(config.file), profile_name), + forwardings={"kubernetes": TunnelSpecForwarding(host="localhost", port=6443)}, user=profile.tunnel.user, host=profile.tunnel.host, identity_file=profile.tunnel.identity_file, @@ -123,4 +123,4 @@ def stop(profile_name: str = Argument("default", envvar="NYL_PROFILE"), all: boo config = PROVIDER.get(ProfileConfig) with TunnelManager() as manager: - manager.close_tunnel(TunnelSpec.Locator(str(config.file), profile_name)) + manager.close_tunnel(TunnelSpecLocator(str(config.file), profile_name)) diff --git a/src/nyl/generator/components.py b/src/nyl/generator/components.py index 7d12227b..722b1284 100644 --- a/src/nyl/generator/components.py +++ b/src/nyl/generator/components.py @@ -78,15 +78,18 @@ def generate(self, /, resource: Manifest) -> Manifests: if instance.remainder: raise RuntimeError(f"unexpected fields in component {instance.metadata}: {instance.remainder.keys()}") - match component: - case HelmComponent(path): - chart = HelmChart( - metadata=instance.metadata, - spec=HelmChartSpec( - chart=ChartRef(path=str(path.resolve())), - values={"metadata": resource["metadata"], **instance.spec}, - ), - ) - return self.helm_generator.generate(chart) - case _: - raise RuntimeError(f"unexpected component type: {component}") + # match component: + # case HelmComponent(path): + if isinstance(component, HelmComponent): + path = component.path + chart = HelmChart( + metadata=instance.metadata, + spec=HelmChartSpec( + chart=ChartRef(path=str(path.resolve())), + values={"metadata": resource["metadata"], **instance.spec}, + ), + ) + return self.helm_generator.generate(chart) + else: + # case _: + raise RuntimeError(f"unexpected component type: {component}") diff --git a/src/nyl/generator/dispatch_test.py b/src/nyl/generator/dispatch_test.py index 76b623c2..d6c49a7e 100644 --- a/src/nyl/generator/dispatch_test.py +++ b/src/nyl/generator/dispatch_test.py @@ -1,6 +1,7 @@ import importlib import pkgutil from pathlib import Path +from typing import cast from unittest.mock import MagicMock from loguru import logger @@ -25,7 +26,8 @@ def test__DispatchingGenerator__default__creates_generator_for_every_nyl_inline_ isinstance(value, type) and issubclass(value, NylResource) and value != NylResource - and value.__module__ == info.name + and cast(object, value).__module__ + == info.name # note: see https://github.com/python/typeshed/issues/12128 and value.API_VERSION == API_VERSION_INLINE ): resource_kinds.add(value.KIND) diff --git a/src/nyl/profiles/__init__.py b/src/nyl/profiles/__init__.py index d66c3516..4c420b5f 100644 --- a/src/nyl/profiles/__init__.py +++ b/src/nyl/profiles/__init__.py @@ -13,7 +13,7 @@ from .config import ProfileConfig, SshTunnel from .kubeconfig import KubeconfigManager -from .tunnel import TunnelManager, TunnelSpec +from .tunnel import TunnelManager, TunnelSpec, TunnelSpecForwarding, TunnelSpecLocator @dataclass @@ -159,8 +159,8 @@ def _wait_for_api_server(url: str, timeout: float) -> None: def get_tunnel_spec(config_file: Path, profile: str, conf: SshTunnel) -> TunnelSpec: return TunnelSpec( - locator=TunnelSpec.Locator(str(config_file), profile), - forwardings={"kubernetes": TunnelSpec.Forwarding(host="localhost", port=6443)}, + locator=TunnelSpecLocator(str(config_file), profile), + forwardings={"kubernetes": TunnelSpecForwarding(host="localhost", port=6443)}, user=conf.user, host=conf.host, identity_file=conf.identity_file, diff --git a/src/nyl/profiles/config.py b/src/nyl/profiles/config.py index a9e2d147..4dd65e95 100644 --- a/src/nyl/profiles/config.py +++ b/src/nyl/profiles/config.py @@ -2,7 +2,7 @@ from dataclasses import dataclass, field from pathlib import Path -from typing import Literal +from typing import ClassVar, Literal from loguru import logger @@ -126,9 +126,9 @@ class SshTunnel: @dataclass class ProfileConfig: - FILENAMES = ["nyl-profiles.yaml", "nyl-profiles.toml", "nyl-profiles.json"] - GLOBAL_CONFIG_DIR = Path.home() / ".config" / "nyl" - STATE_DIRNAME = ".nyl" + FILENAMES: ClassVar = ["nyl-profiles.yaml", "nyl-profiles.toml", "nyl-profiles.json"] + GLOBAL_CONFIG_DIR: ClassVar = Path.home() / ".config" / "nyl" + STATE_DIRNAME: ClassVar = ".nyl" file: Path | None profiles: dict[str, Profile] diff --git a/src/nyl/profiles/tunnel.py b/src/nyl/profiles/tunnel.py index a2685e90..1c3bbe55 100644 --- a/src/nyl/profiles/tunnel.py +++ b/src/nyl/profiles/tunnel.py @@ -4,7 +4,7 @@ import subprocess from dataclasses import dataclass from pathlib import Path -from typing import Any, Iterable, Literal +from typing import Any, ClassVar, Iterable, Literal from loguru import logger from stablehash import stablehash @@ -13,29 +13,31 @@ from nyl.tools.shell import pretty_cmd +@dataclass +class TunnelSpecForwarding: + host: str + port: int + + +@dataclass(frozen=True) +class TunnelSpecLocator: + config_file: str + profile: str + + def __str__(self) -> str: + return f"{self.config_file}:{self.profile}" + + @dataclass class TunnelSpec: """ Defines an SSH tunel that is to be opened. """ - @dataclass - class Forwarding: - host: str - port: int - - @dataclass(frozen=True) - class Locator: - config_file: str - profile: str - - def __str__(self) -> str: - return f"{self.config_file}:{self.profile}" - - locator: Locator + locator: TunnelSpecLocator " Locator for where the tunnel spec is defined." - forwardings: dict[str, Forwarding] + forwardings: dict[str, TunnelSpecForwarding] """ A map from forwarding alias to forwarding configuration. The local ports will be randomly assigned. and must be obtained from the [TunnelStatus]. """ @@ -77,7 +79,7 @@ class TunnelManager: Before the tunnel manager can be used, its context manager must be entered to lock the global state. """ - DEFAULT_STATE_DIR = Path.home() / ".nyl" / "tunnels" + DEFAULT_STATE_DIR: ClassVar = Path.home() / ".nyl" / "tunnels" def __init__(self, state_dir: Path | None = None) -> None: """ @@ -136,7 +138,7 @@ def get_tunnels(self) -> Iterable[tuple[TunnelSpec, TunnelStatus]]: self._store.set(key, (spec, status)) yield spec, status - def get_tunnel(self, locator: TunnelSpec.Locator) -> tuple[TunnelSpec, TunnelStatus] | None: + def get_tunnel(self, locator: TunnelSpecLocator) -> tuple[TunnelSpec, TunnelStatus] | None: """ Retrieve the last known tunnel status and spec based on the tunnel locator. """ @@ -218,7 +220,7 @@ def open_tunnel(self, spec: TunnelSpec) -> TunnelStatus: return status - def close_tunnel(self, locator: TunnelSpec.Locator) -> TunnelStatus: + def close_tunnel(self, locator: TunnelSpecLocator) -> TunnelStatus: """ Close a tunnel by it's locator. """ diff --git a/src/nyl/project/config.py b/src/nyl/project/config.py index 584af688..0c437cc2 100644 --- a/src/nyl/project/config.py +++ b/src/nyl/project/config.py @@ -1,6 +1,6 @@ from dataclasses import dataclass, field from pathlib import Path -from typing import Callable, Literal +from typing import Callable, ClassVar, Literal from loguru import logger @@ -91,7 +91,7 @@ class ProjectConfig: Wrapper for the project configuration file. """ - FILENAMES = ["nyl-project.yaml", "nyl-project.toml", "nyl-project.json"] + FILENAMES: ClassVar = ["nyl-project.yaml", "nyl-project.toml", "nyl-project.json"] file: Path | None config: Project diff --git a/src/nyl/secrets/config.py b/src/nyl/secrets/config.py index 7d8071c7..bba5c3ae 100644 --- a/src/nyl/secrets/config.py +++ b/src/nyl/secrets/config.py @@ -1,5 +1,6 @@ from dataclasses import dataclass from pathlib import Path +from typing import ClassVar from loguru import logger @@ -11,7 +12,7 @@ @dataclass class SecretsConfig: - FILENAMES = ["nyl-secrets.yaml", "nyl-secrets.toml", "nyl-secrets.json"] + FILENAMES: ClassVar = ["nyl-secrets.yaml", "nyl-secrets.toml", "nyl-secrets.json"] file: Path | None providers: dict[str, SecretProvider] diff --git a/src/nyl/tools/di.py b/src/nyl/tools/di.py index 49845f8b..42ec8282 100644 --- a/src/nyl/tools/di.py +++ b/src/nyl/tools/di.py @@ -75,7 +75,7 @@ def get(self, object_type: type[T]) -> T: raise DependencyNotSatisfiedError(object_type) -class DependencyNotSatisfiedError(RuntimeError): +class DependencyNotSatisfiedError(Exception): def __init__(self, object_type: type[T]) -> None: self.object_type = object_type diff --git a/src/nyl/tools/yaml.py b/src/nyl/tools/yaml.py index ca968dcf..da66af49 100644 --- a/src/nyl/tools/yaml.py +++ b/src/nyl/tools/yaml.py @@ -6,10 +6,11 @@ class PatchedSafeLoader(SafeLoader): - yaml_implicit_resolvers = SafeLoader.yaml_implicit_resolvers.copy() - - # See https://github.com/yaml/pyyaml/issues/89 - yaml_implicit_resolvers.pop("=") + def __init__(self, *args: Any, **kwargs: Any) -> None: + # See https://github.com/yaml/pyyaml/issues/89 + super().__init__(*args, **kwargs) + self.yaml_implicit_resolvers = self.yaml_implicit_resolvers.copy() + self.yaml_implicit_resolvers.pop("=") class PatchedSafeDumper(SafeDumper): diff --git a/uv.lock b/uv.lock index 0b13f079..9623537f 100644 --- a/uv.lock +++ b/uv.lock @@ -384,7 +384,7 @@ wheels = [ [[package]] name = "nyl" -version = "0.7.2" +version = "0.8.1" source = { editable = "." } dependencies = [ { name = "bcrypt" }, @@ -402,7 +402,7 @@ dependencies = [ { name = "typing-extensions" }, ] -[package.dev-dependencies] +[package.optional-dependencies] dev = [ { name = "kubernetes-stubs" }, { name = "mypy" }, @@ -419,26 +419,22 @@ requires-dist = [ { name = "filelock", specifier = ">=3.15.4" }, { name = "jinja2", specifier = ">=3.1.4" }, { name = "kubernetes", specifier = ">=30.1.0" }, + { name = "kubernetes-stubs", marker = "extra == 'dev'", specifier = ">=22.6.0.post1" }, { name = "loguru", specifier = ">=0.7.2" }, + { name = "mypy", marker = "extra == 'dev'", specifier = ">=1.13.0" }, { name = "nr-stream", specifier = ">=1.1.5" }, + { name = "pytest", marker = "extra == 'dev'", specifier = ">=8.2.2" }, { name = "pyyaml", specifier = ">=6.0.1" }, { name = "requests", specifier = ">=2.32.3" }, + { name = "ruff", marker = "extra == 'dev'", specifier = ">=0.7.2" }, { name = "stablehash", specifier = ">=0.2.1,<0.3.0" }, { name = "structured-templates", specifier = ">=0.1.1" }, { name = "typer", specifier = ">=0.12.3" }, + { name = "types-pyyaml", marker = "extra == 'dev'", specifier = ">=6.0.12.20240311" }, + { name = "types-requests", marker = "extra == 'dev'", specifier = ">=2.32.0.20240712" }, { name = "typing-extensions", specifier = ">=4.12.2" }, ] -[package.metadata.requires-dev] -dev = [ - { name = "kubernetes-stubs", specifier = ">=22.6.0.post1" }, - { name = "mypy", specifier = ">=1.13.0" }, - { name = "pytest", specifier = ">=8.2.2" }, - { name = "ruff", specifier = ">=0.7.2" }, - { name = "types-pyyaml", specifier = ">=6.0.12.20240311" }, - { name = "types-requests", specifier = ">=2.32.0.20240712" }, -] - [[package]] name = "oauthlib" version = "3.2.2"