diff --git a/Makefile b/Makefile index 757df8a6e..f41e80560 100644 --- a/Makefile +++ b/Makefile @@ -19,8 +19,13 @@ doctest: test-%: packages/% uv run --isolated --directory $< pytest +mypy-%: packages/% + uv run --isolated --directory $< mypy . + test-packages: $(addprefix test-,$(PKG_TARGETS)) +mypy-packages: $(addprefix mypy-,$(PKG_TARGETS)) + clean-venv: -rm -rf ./.venv -find . -type d -name __pycache__ -exec rm -r {} \+ @@ -38,6 +43,8 @@ sync: test: test-packages doctest +mypy: mypy-packages + generate: buf generate @@ -46,4 +53,16 @@ build: clean: clean-docs clean-venv clean-build clean-test -.PHONY: sync docs test test-packages build clean-test clean-docs clean-venv clean-build +.PHONY: sync docs test test-packages build clean-test clean-docs clean-venv clean-build \ + mypy-jumpstarter \ + mypy-jumpstarter-cli-admin \ + mypy-jumpstarter-cli-client \ + mypy-jumpstarter-driver-can \ + mypy-jumpstarter-driver-dutlink \ + mypy-jumpstarter-driver-network \ + mypy-jumpstarter-driver-raspberrypi \ + mypy-jumpstarter-driver-sdwire \ + mypy-jumpstarter-driver-tftp \ + mypy-jumpstarter-driver-yepkit \ + mypy-jumpstarter-kubernetes \ + mypy-jumpstarter-protocol diff --git a/buf.gen.yaml b/buf.gen.yaml index 6b52d6ac1..29d0e2f2d 100644 --- a/buf.gen.yaml +++ b/buf.gen.yaml @@ -3,9 +3,9 @@ managed: enabled: true plugins: - remote: buf.build/protocolbuffers/python - out: ./packages/jumpstarter_protocol/jumpstarter_protocol + out: ./packages/jumpstarter-protocol/jumpstarter_protocol - remote: buf.build/grpc/python - out: ./packages/jumpstarter_protocol/jumpstarter_protocol + out: ./packages/jumpstarter-protocol/jumpstarter_protocol inputs: - git_repo: https://github.com/jumpstarter-dev/jumpstarter-protocol.git subdir: proto diff --git a/conftest.py b/conftest.py new file mode 100644 index 000000000..00aa94741 --- /dev/null +++ b/conftest.py @@ -0,0 +1,21 @@ +from contextlib import contextmanager + +import pytest + +try: + from jumpstarter.common.utils import serve + from jumpstarter.config import ExporterConfigV1Alpha1DriverInstance +except ImportError: + # some packages in the workspace does not depend on jumpstarter + pass +else: + + @contextmanager + def run(config): + with serve(ExporterConfigV1Alpha1DriverInstance.from_str(config).instantiate()) as client: + yield client + + @pytest.fixture(autouse=True) + def jumpstarter_namespace(doctest_namespace): + doctest_namespace["serve"] = serve + doctest_namespace["run"] = run diff --git a/docs/source/api-reference/drivers/index.md b/docs/source/api-reference/drivers/index.md index 9720ad6d6..d7e4ff01a 100644 --- a/docs/source/api-reference/drivers/index.md +++ b/docs/source/api-reference/drivers/index.md @@ -8,6 +8,7 @@ Drivers packages from the [drivers](https://github.com/jumpstarter-dev/jumpstart ```{toctree} can.md +network.md pyserial.md sdwire.md shell.md diff --git a/docs/source/api-reference/drivers/network.md b/docs/source/api-reference/drivers/network.md new file mode 100644 index 000000000..1e996565c --- /dev/null +++ b/docs/source/api-reference/drivers/network.md @@ -0,0 +1,28 @@ +# Network drivers + +The network drivers are a set of drivers for interacting with network servers. + +## Driver configuration + +```{eval-rst} +.. autoclass:: jumpstarter_driver_network.driver.TcpNetwork() +``` + +```{eval-rst} +.. autoclass:: jumpstarter_driver_network.driver.UdpNetwork() +``` + +```{eval-rst} +.. autoclass:: jumpstarter_driver_network.driver.UnixNetwork() +``` + +```{eval-rst} +.. autoclass:: jumpstarter_driver_network.driver.EchoNetwork() +``` + +## Client API + +```{eval-rst} +.. autoclass:: jumpstarter_driver_network.client.NetworkClient() + :members: +``` diff --git a/docs/source/conf.py b/docs/source/conf.py index 45415b4c6..22f35d7f8 100644 --- a/docs/source/conf.py +++ b/docs/source/conf.py @@ -49,3 +49,5 @@ "version": "0.5.0", "controller_version": "0.5.0", } + +doctest_test_doctest_blocks = "" diff --git a/packages/jumpstarter-cli-admin/jumpstarter_cli_admin/py.typed b/packages/jumpstarter-cli-admin/jumpstarter_cli_admin/py.typed new file mode 100644 index 000000000..e69de29bb diff --git a/packages/jumpstarter-cli-client/jumpstarter_cli_client/py.typed b/packages/jumpstarter-cli-client/jumpstarter_cli_client/py.typed new file mode 100644 index 000000000..e69de29bb diff --git a/packages/jumpstarter-cli-common/jumpstarter_cli_common/py.typed b/packages/jumpstarter-cli-common/jumpstarter_cli_common/py.typed new file mode 100644 index 000000000..e69de29bb diff --git a/packages/jumpstarter-cli-exporter/jumpstarter_cli_exporter/py.typed b/packages/jumpstarter-cli-exporter/jumpstarter_cli_exporter/py.typed new file mode 100644 index 000000000..e69de29bb diff --git a/packages/jumpstarter-cli/jumpstarter_cli/py.typed b/packages/jumpstarter-cli/jumpstarter_cli/py.typed new file mode 100644 index 000000000..e69de29bb diff --git a/packages/jumpstarter-driver-can/jumpstarter_driver_can/driver.py b/packages/jumpstarter-driver-can/jumpstarter_driver_can/driver.py index b8b04edcd..a9c92665e 100644 --- a/packages/jumpstarter-driver-can/jumpstarter_driver_can/driver.py +++ b/packages/jumpstarter-driver-can/jumpstarter_driver_can/driver.py @@ -33,7 +33,7 @@ class Can(Driver): """ The CAN bus instance used for communication. """ - bus: can.Bus = field(init=False) + bus: can.BusABC = field(init=False) """ A dict of cyclic send tasks to run. @@ -100,6 +100,7 @@ def state(self, value: can.BusState | None = None) -> can.BusState | None: """ if value: self.bus.state = value + return None else: return self.bus.state @@ -180,7 +181,7 @@ class IsoTpPython(Driver): """ The CAN bus instance used for communication. """ - bus: can.Bus = field(init=False) + bus: can.BusABC = field(init=False) """ The CAN bus notifier instance used to receive messages. diff --git a/packages/jumpstarter-driver-can/jumpstarter_driver_can/py.typed b/packages/jumpstarter-driver-can/jumpstarter_driver_can/py.typed new file mode 100644 index 000000000..e69de29bb diff --git a/packages/jumpstarter-driver-composite/jumpstarter_driver_composite/py.typed b/packages/jumpstarter-driver-composite/jumpstarter_driver_composite/py.typed new file mode 100644 index 000000000..e69de29bb diff --git a/packages/jumpstarter-driver-dutlink/examples/dutlink.py b/packages/jumpstarter-driver-dutlink/examples/dutlink.py index dc2fea042..80add81a9 100755 --- a/packages/jumpstarter-driver-dutlink/examples/dutlink.py +++ b/packages/jumpstarter-driver-dutlink/examples/dutlink.py @@ -15,39 +15,40 @@ # initialize client from environment # e.g. `jmp-exporter shell dutlink` -with env() as client: - dutlink = client.dutlink - click.secho("Connected to Dutlink", fg="green") - # apply adapter to console for expect support - with PexpectAdapter(client=dutlink.console) as console: - # stream console output to stdout - console.logfile = sys.stdout.buffer - # ensure DUT is powered off - dutlink.power.off() - - click.secho("Writing system image", fg="red") - dutlink.storage.write_local_file("/tmp/nixos-visionfive2.img") - click.secho("Written system image", fg="red") - - dutlink.storage.dut() - click.secho("Connected storage device to DUT", fg="green") - - dutlink.power.on() - click.secho("Powered DUT on", fg="green") - - click.secho("Waiting for boot menu", fg="red") - console.expect("Enter choice:") - console.sendline("1") - click.secho("Selected boot entry", fg="red") - - click.secho("Waiting for login prompt", fg="red") - console.expect("nixos@nixos", timeout=300) - time.sleep(3) - - reading = next(dutlink.power.read()) - click.secho(f"Current power reading: {reading}", fg="blue") - - console.sendline("uname -a") - console.expect("riscv64 GNU/Linux") - - dutlink.power.off() +if __name__ == "__main__": + with env() as client: + dutlink = client.dutlink + click.secho("Connected to Dutlink", fg="green") + # apply adapter to console for expect support + with PexpectAdapter(client=dutlink.console) as console: + # stream console output to stdout + console.logfile = sys.stdout.buffer + # ensure DUT is powered off + dutlink.power.off() + + click.secho("Writing system image", fg="red") + dutlink.storage.write_local_file("/tmp/nixos-visionfive2.img") + click.secho("Written system image", fg="red") + + dutlink.storage.dut() + click.secho("Connected storage device to DUT", fg="green") + + dutlink.power.on() + click.secho("Powered DUT on", fg="green") + + click.secho("Waiting for boot menu", fg="red") + console.expect("Enter choice:") + console.sendline("1") + click.secho("Selected boot entry", fg="red") + + click.secho("Waiting for login prompt", fg="red") + console.expect("nixos@nixos", timeout=300) + time.sleep(3) + + reading = next(dutlink.power.read()) + click.secho(f"Current power reading: {reading}", fg="blue") + + console.sendline("uname -a") + console.expect("riscv64 GNU/Linux") + + dutlink.power.off() diff --git a/packages/jumpstarter-driver-dutlink/jumpstarter_driver_dutlink/py.typed b/packages/jumpstarter-driver-dutlink/jumpstarter_driver_dutlink/py.typed new file mode 100644 index 000000000..e69de29bb diff --git a/packages/jumpstarter-driver-http/jumpstarter_driver_http/driver.py b/packages/jumpstarter-driver-http/jumpstarter_driver_http/driver.py index 649e7bdbe..b08c338de 100644 --- a/packages/jumpstarter-driver-http/jumpstarter_driver_http/driver.py +++ b/packages/jumpstarter-driver-http/jumpstarter_driver_http/driver.py @@ -23,7 +23,7 @@ class HttpServer(Driver): """HTTP Server driver for Jumpstarter""" root_dir: str = "/var/www" - host: str = field(default=None) + host: str | None = field(default=None) port: int = 8080 app: web.Application = field(init=False, default_factory=web.Application) runner: Optional[web.AppRunner] = field(init=False, default=None) @@ -203,7 +203,7 @@ def get_url(self) -> str: return f"http://{self.host}:{self.port}" @export - def get_host(self) -> str: + def get_host(self) -> str | None: """ Get the host IP address of the HTTP server. diff --git a/packages/jumpstarter-driver-http/jumpstarter_driver_http/py.typed b/packages/jumpstarter-driver-http/jumpstarter_driver_http/py.typed new file mode 100644 index 000000000..e69de29bb diff --git a/packages/jumpstarter-driver-network/jumpstarter_driver_network/conftest.py b/packages/jumpstarter-driver-network/jumpstarter_driver_network/conftest.py new file mode 100644 index 000000000..f8410c472 --- /dev/null +++ b/packages/jumpstarter-driver-network/jumpstarter_driver_network/conftest.py @@ -0,0 +1,20 @@ +import pytest +from anyio.from_thread import start_blocking_portal + +from jumpstarter.common import TemporaryTcpListener + + +async def echo_handler(stream): + async with stream: + while True: + try: + await stream.send(await stream.receive()) + except Exception: + pass + + +@pytest.fixture +def tcp_echo_server(): + with start_blocking_portal() as portal: + with portal.wrap_async_context_manager(TemporaryTcpListener(echo_handler, local_host="127.0.0.1")) as addr: + yield addr diff --git a/packages/jumpstarter-driver-network/jumpstarter_driver_network/driver.py b/packages/jumpstarter-driver-network/jumpstarter_driver_network/driver.py index 2296df78c..4ea3c7ad0 100644 --- a/packages/jumpstarter-driver-network/jumpstarter_driver_network/driver.py +++ b/packages/jumpstarter-driver-network/jumpstarter_driver_network/driver.py @@ -25,6 +25,22 @@ async def connect(self): ... @dataclass(kw_only=True) class TcpNetwork(NetworkInterface, Driver): + ''' + TcpNetwork is a driver for connecting to TCP sockets + + >>> addr = getfixture("tcp_echo_server") # start a tcp echo server + >>> config = f""" + ... type: jumpstarter_driver_network.driver.TcpNetwork + ... config: + ... host: {addr[0]} # 127.0.0.1 + ... port: {addr[1]} # random port + ... """ + >>> with run(config) as tcp: + ... with tcp.stream() as conn: + ... conn.send(b"hello") + ... assert conn.receive() == b"hello" + ''' + host: str port: int @@ -38,6 +54,19 @@ async def connect(self): @dataclass(kw_only=True) class UdpNetwork(NetworkInterface, Driver): + ''' + UdpNetwork is a driver for connecting to UDP sockets + + >>> config = f""" + ... type: jumpstarter_driver_network.driver.UdpNetwork + ... config: + ... host: 127.0.0.1 + ... port: 41336 + ... """ + >>> with run(config) as udp: + ... pass + ''' + host: str port: int @@ -51,6 +80,18 @@ async def connect(self): @dataclass(kw_only=True) class UnixNetwork(NetworkInterface, Driver): + ''' + UnixNetwork is a driver for connecting to Unix domain sockets + + >>> config = f""" + ... type: jumpstarter_driver_network.driver.UnixNetwork + ... config: + ... path: /tmp/example.sock + ... """ + >>> with run(config) as unix: + ... pass + ''' + path: str @exportstream @@ -62,6 +103,18 @@ async def connect(self): class EchoNetwork(NetworkInterface, Driver): + ''' + EchoNetwork is a mock driver implementing the NetworkInterface + + >>> config = """ + ... type: jumpstarter_driver_network.driver.EchoNetwork + ... """ + >>> with run(config) as echo: + ... with echo.stream() as conn: + ... conn.send(b"hello") + ... assert conn.receive() == b"hello" + ''' + @exportstream @asynccontextmanager async def connect(self): diff --git a/packages/jumpstarter-driver-network/jumpstarter_driver_network/driver_test.py b/packages/jumpstarter-driver-network/jumpstarter_driver_network/driver_test.py index 80e4c4864..fd5d3f855 100644 --- a/packages/jumpstarter-driver-network/jumpstarter_driver_network/driver_test.py +++ b/packages/jumpstarter-driver-network/jumpstarter_driver_network/driver_test.py @@ -7,18 +7,11 @@ from anyio.from_thread import start_blocking_portal from .adapters import TcpPortforwardAdapter, UnixPortforwardAdapter -from .driver import EchoNetwork, TcpNetwork, UdpNetwork, UnixNetwork -from jumpstarter.common import TemporaryTcpListener, TemporaryUnixListener +from .driver import TcpNetwork, UdpNetwork, UnixNetwork +from jumpstarter.common import TemporaryUnixListener from jumpstarter.common.utils import serve -def test_echo_network(): - with serve(EchoNetwork()) as client: - with client.stream() as stream: - stream.send(b"hello") - assert stream.receive() == b"hello" - - async def echo_handler(stream): async with stream: while True: @@ -28,24 +21,13 @@ async def echo_handler(stream): pass -def test_tcp_network(): - with start_blocking_portal() as portal: - with portal.wrap_async_context_manager(TemporaryTcpListener(echo_handler, local_host="127.0.0.1")) as addr: - with serve(TcpNetwork(host=addr[0], port=addr[1])) as client: - with client.stream() as stream: - stream.send(b"hello") - assert stream.receive() == b"hello" - - -def test_tcp_network_portforward(): - with start_blocking_portal() as portal: - with portal.wrap_async_context_manager(TemporaryTcpListener(echo_handler, local_host="127.0.0.1")) as inner: - with serve(TcpNetwork(host=inner[0], port=inner[1])) as client: - with TcpPortforwardAdapter(client=client) as addr: - stream = socket.socket(socket.AF_INET, socket.SOCK_STREAM) - stream.connect(addr) - stream.send(b"hello") - assert stream.recv(5) == b"hello" +def test_tcp_network_portforward(tcp_echo_server): + with serve(TcpNetwork(host=tcp_echo_server[0], port=tcp_echo_server[1])) as client: + with TcpPortforwardAdapter(client=client) as addr: + stream = socket.socket(socket.AF_INET, socket.SOCK_STREAM) + stream.connect(addr) + stream.send(b"hello") + assert stream.recv(5) == b"hello" def test_unix_network_portforward(): diff --git a/packages/jumpstarter-driver-network/jumpstarter_driver_network/py.typed b/packages/jumpstarter-driver-network/jumpstarter_driver_network/py.typed new file mode 100644 index 000000000..e69de29bb diff --git a/packages/jumpstarter-driver-network/pyproject.toml b/packages/jumpstarter-driver-network/pyproject.toml index 83a01880e..a43815772 100644 --- a/packages/jumpstarter-driver-network/pyproject.toml +++ b/packages/jumpstarter-driver-network/pyproject.toml @@ -32,7 +32,9 @@ Novnc = "jumpstarter_driver_network.adapters:NovncAdapter" dev = [ "pytest>=8.3.2", "pytest-cov>=5.0.0", - "websocket-client>=1.8.0" + "types-paramiko>=3.5.0.20240928", + "types-pexpect>=4.9.0.20241208", + "websocket-client>=1.8.0", ] [tool.hatch.metadata.hooks.vcs.urls] diff --git a/packages/jumpstarter-driver-opendal/jumpstarter_driver_opendal/driver.py b/packages/jumpstarter-driver-opendal/jumpstarter_driver_opendal/driver.py index 0b2f17934..8eb1e2ac0 100644 --- a/packages/jumpstarter-driver-opendal/jumpstarter_driver_opendal/driver.py +++ b/packages/jumpstarter-driver-opendal/jumpstarter_driver_opendal/driver.py @@ -1,6 +1,6 @@ from abc import ABCMeta, abstractmethod from dataclasses import dataclass, field -from tempfile import NamedTemporaryFile +from tempfile import NamedTemporaryFile, _TemporaryFileWrapper from anyio.streams.file import FileReadStream, FileWriteStream @@ -30,7 +30,7 @@ async def read(self, dst: str): ... @dataclass class MockStorageMux(StorageMuxInterface, Driver): - file: NamedTemporaryFile = field(default_factory=NamedTemporaryFile) + file: _TemporaryFileWrapper = field(default_factory=NamedTemporaryFile) @export async def host(self): diff --git a/packages/jumpstarter-driver-opendal/jumpstarter_driver_opendal/py.typed b/packages/jumpstarter-driver-opendal/jumpstarter_driver_opendal/py.typed new file mode 100644 index 000000000..e69de29bb diff --git a/packages/jumpstarter-driver-power/jumpstarter_driver_power/client_test.py b/packages/jumpstarter-driver-power/jumpstarter_driver_power/client_test.py deleted file mode 100644 index 9f204dd8a..000000000 --- a/packages/jumpstarter-driver-power/jumpstarter_driver_power/client_test.py +++ /dev/null @@ -1,25 +0,0 @@ -from .common import PowerReading -from .driver import MockPower, SyncMockPower -from jumpstarter.common.utils import serve - - -def test_client_mock_power(): - with serve(MockPower()) as client: - client.on() - client.off() - - assert list(client.read()) == [ - PowerReading(voltage=0.0, current=0.0), - PowerReading(voltage=5.0, current=2.0), - ] - - -def test_client_sync_mock_power(): - with serve(SyncMockPower()) as client: - client.on() - client.off() - - assert list(client.read()) == [ - PowerReading(voltage=0.0, current=0.0), - PowerReading(voltage=5.0, current=2.0), - ] diff --git a/packages/jumpstarter-driver-power/jumpstarter_driver_power/driver.py b/packages/jumpstarter-driver-power/jumpstarter_driver_power/driver.py index cd23c2ba4..d1baa7bd1 100644 --- a/packages/jumpstarter-driver-power/jumpstarter_driver_power/driver.py +++ b/packages/jumpstarter-driver-power/jumpstarter_driver_power/driver.py @@ -21,6 +21,19 @@ async def read(self) -> AsyncGenerator[PowerReading, None]: ... class MockPower(PowerInterface, Driver): + """ + MockPower is a mock driver implementing the PowerInterface + + >>> with serve(MockPower()) as power: + ... power.on() + ... power.off() + ... + ... assert list(power.read()) == [ + ... PowerReading(voltage=0.0, current=0.0), + ... PowerReading(voltage=5.0, current=2.0), + ... ] + """ + @export async def on(self) -> None: pass @@ -36,6 +49,19 @@ async def read(self) -> AsyncGenerator[PowerReading, None]: class SyncMockPower(PowerInterface, Driver): + """ + SyncMockPower is a mock driver implementing the PowerInterface + + >>> with serve(SyncMockPower()) as power: + ... power.on() + ... power.off() + ... + ... assert list(power.read()) == [ + ... PowerReading(voltage=0.0, current=0.0), + ... PowerReading(voltage=5.0, current=2.0), + ... ] + """ + @export def on(self) -> None: pass diff --git a/packages/jumpstarter-driver-power/jumpstarter_driver_power/driver_test.py b/packages/jumpstarter-driver-power/jumpstarter_driver_power/driver_test.py deleted file mode 100644 index 9f766a1f8..000000000 --- a/packages/jumpstarter-driver-power/jumpstarter_driver_power/driver_test.py +++ /dev/null @@ -1,30 +0,0 @@ -import pytest - -from .common import PowerReading -from .driver import MockPower, SyncMockPower - -pytestmark = pytest.mark.anyio - - -async def test_driver_mock_power(): - driver = MockPower() - - await driver.on() - await driver.off() - - assert [v async for v in driver.read()] == [ - PowerReading(voltage=0.0, current=0.0), - PowerReading(voltage=5.0, current=2.0), - ] - - -def test_driver_sync_mock_power(): - driver = SyncMockPower() - - driver.on() - driver.off() - - assert list(driver.read()) == [ - PowerReading(voltage=0.0, current=0.0), - PowerReading(voltage=5.0, current=2.0), - ] diff --git a/packages/jumpstarter-driver-power/jumpstarter_driver_power/py.typed b/packages/jumpstarter-driver-power/jumpstarter_driver_power/py.typed new file mode 100644 index 000000000..e69de29bb diff --git a/packages/jumpstarter-driver-pyserial/jumpstarter_driver_pyserial/py.typed b/packages/jumpstarter-driver-pyserial/jumpstarter_driver_pyserial/py.typed new file mode 100644 index 000000000..e69de29bb diff --git a/packages/jumpstarter-driver-pyserial/pyproject.toml b/packages/jumpstarter-driver-pyserial/pyproject.toml index 5330454c8..fc1726ae2 100644 --- a/packages/jumpstarter-driver-pyserial/pyproject.toml +++ b/packages/jumpstarter-driver-pyserial/pyproject.toml @@ -20,6 +20,8 @@ dependencies = [ dev = [ "pytest>=8.3.2", "pytest-cov>=5.0.0", + "types-pexpect>=4.9.0.20241208", + "types-pyserial>=3.5.0.20250130", ] [tool.hatch.metadata.hooks.vcs.urls] diff --git a/packages/jumpstarter-driver-raspberrypi/jumpstarter_driver_raspberrypi/py.typed b/packages/jumpstarter-driver-raspberrypi/jumpstarter_driver_raspberrypi/py.typed new file mode 100644 index 000000000..e69de29bb diff --git a/packages/jumpstarter-driver-sdwire/jumpstarter_driver_sdwire/py.typed b/packages/jumpstarter-driver-sdwire/jumpstarter_driver_sdwire/py.typed new file mode 100644 index 000000000..e69de29bb diff --git a/packages/jumpstarter-driver-tftp/jumpstarter_driver_tftp/py.typed b/packages/jumpstarter-driver-tftp/jumpstarter_driver_tftp/py.typed new file mode 100644 index 000000000..e69de29bb diff --git a/packages/jumpstarter-driver-ustreamer/jumpstarter_driver_ustreamer/py.typed b/packages/jumpstarter-driver-ustreamer/jumpstarter_driver_ustreamer/py.typed new file mode 100644 index 000000000..e69de29bb diff --git a/packages/jumpstarter-driver-yepkit/jumpstarter_driver_yepkit/py.typed b/packages/jumpstarter-driver-yepkit/jumpstarter_driver_yepkit/py.typed new file mode 100644 index 000000000..e69de29bb diff --git a/packages/jumpstarter-imagehash/jumpstarter_imagehash/py.typed b/packages/jumpstarter-imagehash/jumpstarter_imagehash/py.typed new file mode 100644 index 000000000..e69de29bb diff --git a/packages/jumpstarter-kubernetes/jumpstarter_kubernetes/py.typed b/packages/jumpstarter-kubernetes/jumpstarter_kubernetes/py.typed new file mode 100644 index 000000000..e69de29bb diff --git a/packages/jumpstarter-protocol/jumpstarter_protocol/jumpstarter/v1/__init__.py b/packages/jumpstarter-protocol/jumpstarter_protocol/jumpstarter/v1/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/packages/jumpstarter-protocol/jumpstarter_protocol/py.typed b/packages/jumpstarter-protocol/jumpstarter_protocol/py.typed new file mode 100644 index 000000000..e69de29bb diff --git a/packages/jumpstarter-testing/jumpstarter_testing/py.typed b/packages/jumpstarter-testing/jumpstarter_testing/py.typed new file mode 100644 index 000000000..e69de29bb diff --git a/packages/jumpstarter/jumpstarter/config/exporter.py b/packages/jumpstarter/jumpstarter/config/exporter.py index 3d7b1f35f..b995aabb3 100644 --- a/packages/jumpstarter/jumpstarter/config/exporter.py +++ b/packages/jumpstarter/jumpstarter/config/exporter.py @@ -34,6 +34,10 @@ def from_path(cls, path: str) -> ExporterConfigV1Alpha1DriverInstance: with open(path) as f: return cls.model_validate(yaml.safe_load(f)) + @classmethod + def from_str(cls, config: str) -> ExporterConfigV1Alpha1DriverInstance: + return cls.model_validate(yaml.safe_load(config)) + class ExporterConfigV1Alpha1(BaseModel): BASE_PATH: ClassVar[Path] = Path("/etc/jumpstarter/exporters") diff --git a/packages/jumpstarter/jumpstarter/py.typed b/packages/jumpstarter/jumpstarter/py.typed new file mode 100644 index 000000000..e69de29bb diff --git a/pyproject.toml b/pyproject.toml index 55db768f6..7acf8d378 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -44,6 +44,7 @@ dev = [ "typos>=1.23.6", "pre-commit>=3.8.0", "esbonio>=0.16.5", + "mypy>=1.15.0", ] [tool.ruff] @@ -66,4 +67,4 @@ omit = ["conftest.py", "test_*.py", "*_test.py", "*_pb2.py", "*_pb2_grpc.py"] skip_empty = true [tool.pytest.ini_options] -addopts = "--capture=no --cov --cov-report=html --cov-report=xml --cov-append" +addopts = "--capture=no --doctest-modules --cov --cov-report=html --cov-report=xml" diff --git a/uv.lock b/uv.lock index 515580042..548fa5168 100644 --- a/uv.lock +++ b/uv.lock @@ -35,6 +35,7 @@ members = [ [manifest.dependency-groups] dev = [ { name = "esbonio", specifier = ">=0.16.5" }, + { name = "mypy", specifier = ">=1.15.0" }, { name = "pre-commit", specifier = ">=3.8.0" }, { name = "ruff", specifier = "==0.9.2" }, { name = "typos", specifier = ">=1.23.6" }, @@ -1105,6 +1106,8 @@ dependencies = [ dev = [ { name = "pytest" }, { name = "pytest-cov" }, + { name = "types-paramiko" }, + { name = "types-pexpect" }, { name = "websocket-client" }, ] @@ -1120,6 +1123,8 @@ requires-dist = [ dev = [ { name = "pytest", specifier = ">=8.3.2" }, { name = "pytest-cov", specifier = ">=5.0.0" }, + { name = "types-paramiko", specifier = ">=3.5.0.20240928" }, + { name = "types-pexpect", specifier = ">=4.9.0.20241208" }, { name = "websocket-client", specifier = ">=1.8.0" }, ] @@ -1195,6 +1200,8 @@ dependencies = [ dev = [ { name = "pytest" }, { name = "pytest-cov" }, + { name = "types-pexpect" }, + { name = "types-pyserial" }, ] [package.metadata] @@ -1209,6 +1216,8 @@ requires-dist = [ dev = [ { name = "pytest", specifier = ">=8.3.2" }, { name = "pytest-cov", specifier = ">=5.0.0" }, + { name = "types-pexpect", specifier = ">=4.9.0.20241208" }, + { name = "types-pyserial", specifier = ">=3.5.0.20250130" }, ] [[package]] @@ -1728,6 +1737,40 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/99/b7/b9e70fde2c0f0c9af4cc5277782a89b66d35948ea3369ec9f598358c3ac5/multidict-6.1.0-py3-none-any.whl", hash = "sha256:48e171e52d1c4d33888e529b999e5900356b9ae588c2f09a52dcefb158b27506", size = 10051 }, ] +[[package]] +name = "mypy" +version = "1.15.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "mypy-extensions" }, + { name = "typing-extensions" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/ce/43/d5e49a86afa64bd3839ea0d5b9c7103487007d728e1293f52525d6d5486a/mypy-1.15.0.tar.gz", hash = "sha256:404534629d51d3efea5c800ee7c42b72a6554d6c400e6a79eafe15d11341fd43", size = 3239717 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/98/3a/03c74331c5eb8bd025734e04c9840532226775c47a2c39b56a0c8d4f128d/mypy-1.15.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:aea39e0583d05124836ea645f412e88a5c7d0fd77a6d694b60d9b6b2d9f184fd", size = 10793981 }, + { url = "https://files.pythonhosted.org/packages/f0/1a/41759b18f2cfd568848a37c89030aeb03534411eef981df621d8fad08a1d/mypy-1.15.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:2f2147ab812b75e5b5499b01ade1f4a81489a147c01585cda36019102538615f", size = 9749175 }, + { url = "https://files.pythonhosted.org/packages/12/7e/873481abf1ef112c582db832740f4c11b2bfa510e829d6da29b0ab8c3f9c/mypy-1.15.0-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:ce436f4c6d218a070048ed6a44c0bbb10cd2cc5e272b29e7845f6a2f57ee4464", size = 11455675 }, + { url = "https://files.pythonhosted.org/packages/b3/d0/92ae4cde706923a2d3f2d6c39629134063ff64b9dedca9c1388363da072d/mypy-1.15.0-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:8023ff13985661b50a5928fc7a5ca15f3d1affb41e5f0a9952cb68ef090b31ee", size = 12410020 }, + { url = "https://files.pythonhosted.org/packages/46/8b/df49974b337cce35f828ba6fda228152d6db45fed4c86ba56ffe442434fd/mypy-1.15.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:1124a18bc11a6a62887e3e137f37f53fbae476dc36c185d549d4f837a2a6a14e", size = 12498582 }, + { url = "https://files.pythonhosted.org/packages/13/50/da5203fcf6c53044a0b699939f31075c45ae8a4cadf538a9069b165c1050/mypy-1.15.0-cp312-cp312-win_amd64.whl", hash = "sha256:171a9ca9a40cd1843abeca0e405bc1940cd9b305eaeea2dda769ba096932bb22", size = 9366614 }, + { url = "https://files.pythonhosted.org/packages/6a/9b/fd2e05d6ffff24d912f150b87db9e364fa8282045c875654ce7e32fffa66/mypy-1.15.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:93faf3fdb04768d44bf28693293f3904bbb555d076b781ad2530214ee53e3445", size = 10788592 }, + { url = "https://files.pythonhosted.org/packages/74/37/b246d711c28a03ead1fd906bbc7106659aed7c089d55fe40dd58db812628/mypy-1.15.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:811aeccadfb730024c5d3e326b2fbe9249bb7413553f15499a4050f7c30e801d", size = 9753611 }, + { url = "https://files.pythonhosted.org/packages/a6/ac/395808a92e10cfdac8003c3de9a2ab6dc7cde6c0d2a4df3df1b815ffd067/mypy-1.15.0-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:98b7b9b9aedb65fe628c62a6dc57f6d5088ef2dfca37903a7d9ee374d03acca5", size = 11438443 }, + { url = "https://files.pythonhosted.org/packages/d2/8b/801aa06445d2de3895f59e476f38f3f8d610ef5d6908245f07d002676cbf/mypy-1.15.0-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:c43a7682e24b4f576d93072216bf56eeff70d9140241f9edec0c104d0c515036", size = 12402541 }, + { url = "https://files.pythonhosted.org/packages/c7/67/5a4268782eb77344cc613a4cf23540928e41f018a9a1ec4c6882baf20ab8/mypy-1.15.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:baefc32840a9f00babd83251560e0ae1573e2f9d1b067719479bfb0e987c6357", size = 12494348 }, + { url = "https://files.pythonhosted.org/packages/83/3e/57bb447f7bbbfaabf1712d96f9df142624a386d98fb026a761532526057e/mypy-1.15.0-cp313-cp313-win_amd64.whl", hash = "sha256:b9378e2c00146c44793c98b8d5a61039a048e31f429fb0eb546d93f4b000bedf", size = 9373648 }, + { url = "https://files.pythonhosted.org/packages/09/4e/a7d65c7322c510de2c409ff3828b03354a7c43f5a8ed458a7a131b41c7b9/mypy-1.15.0-py3-none-any.whl", hash = "sha256:5469affef548bd1895d86d3bf10ce2b44e33d86923c29e4d675b3e323437ea3e", size = 2221777 }, +] + +[[package]] +name = "mypy-extensions" +version = "1.0.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/98/a4/1ab47638b92648243faf97a5aeb6ea83059cc3624972ab6b8d2316078d3f/mypy_extensions-1.0.0.tar.gz", hash = "sha256:75dbf8955dc00442a438fc4d0666508a9a97b6bd41aa2f0ffe9d2f2725af0782", size = 4433 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/2a/e2/5d3f6ada4297caebe1a2add3b126fe800c96f56dbe5d1988a2cbe0b267aa/mypy_extensions-1.0.0-py3-none-any.whl", hash = "sha256:4392f6c0eb8a5668a69e23d168ffa70f0be9ccfd32b5cc2d26a34ae5b844552d", size = 4695 }, +] + [[package]] name = "myst-parser" version = "4.0.0" @@ -2641,6 +2684,36 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/b4/04/9954a59e1fb6732f5436225c9af963811d7b24ea62a8bf96991f2cb8c26e/trio-0.28.0-py3-none-any.whl", hash = "sha256:56d58977acc1635735a96581ec70513cc781b8b6decd299c487d3be2a721cd94", size = 486317 }, ] +[[package]] +name = "types-paramiko" +version = "3.5.0.20240928" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "cryptography" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/ae/00/733b05fa97328becc7606301d9ae4c7ec7ec9e92324b8420208e397f3ac7/types-paramiko-3.5.0.20240928.tar.gz", hash = "sha256:79dd9b2ee510b76a3b60d8ac1f3f348c45fcecf01347ca79e14db726bbfc442d", size = 23213 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/8e/e5/afb514829e7184e7518a62c014dc364f7a9d47f9112ff26c63835cbf0de5/types_paramiko-3.5.0.20240928-py3-none-any.whl", hash = "sha256:cda0aff4905fe8efe4b5448331a80e943d42a796bd4beb77a3eed3485bc96a85", size = 34016 }, +] + +[[package]] +name = "types-pexpect" +version = "4.9.0.20241208" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/35/8b/2ac0c1db88bdc441d21477f151a074b1789aa3a4deac571b079a097bd987/types_pexpect-4.9.0.20241208.tar.gz", hash = "sha256:bbca0d0819947a719989a5cfe83641d9212bef893e2f0a7a01e47926bc82401d", size = 13222 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/37/c6/14c23e04bae6a83e051da86c1670684e59acadab333a8497384aa201defd/types_pexpect-4.9.0.20241208-py3-none-any.whl", hash = "sha256:1928f478528454f0fea3495c16cf1ee2e67fca5c9fe97d60b868ac48c1fd5633", size = 17085 }, +] + +[[package]] +name = "types-pyserial" +version = "3.5.0.20250130" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/aa/09/26350878b1f0e537e21b9493b4499c887785a29425a3e831efc50fe14a9a/types_pyserial-3.5.0.20250130.tar.gz", hash = "sha256:bc23c799bf27ff5105ee6b7cb973d47ba1f222ad8f0a4223c0b0a979bd671f3e", size = 19325 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/39/a1/75e2ab241987f678fe3ec19ee35145fad23140613c594820f0523259c86a/types_pyserial-3.5.0.20250130-py3-none-any.whl", hash = "sha256:cd5dccb65fe83e2741853aaf45caabeecfcd4c5d050107b5e56f586b3d0f46cd", size = 25041 }, +] + [[package]] name = "typing-extensions" version = "4.12.2"