diff --git a/src/enapter/cli/http/api/blueprint_command.py b/src/enapter/cli/http/api/blueprint_command.py index fc2c3ac..fd254f1 100644 --- a/src/enapter/cli/http/api/blueprint_command.py +++ b/src/enapter/cli/http/api/blueprint_command.py @@ -3,7 +3,9 @@ from enapter import cli from .blueprint_download_command import BlueprintDownloadCommand +from .blueprint_get_command import BlueprintGetCommand from .blueprint_upload_command import BlueprintUploadCommand +from .blueprint_validate_command import BlueprintValidateCommand class BlueprintCommand(cli.Command): @@ -16,7 +18,9 @@ def register(parent: cli.Subparsers) -> None: subparsers = parser.add_subparsers(dest="blueprint_command", required=True) for command in [ BlueprintDownloadCommand, + BlueprintGetCommand, BlueprintUploadCommand, + BlueprintValidateCommand, ]: command.register(subparsers) @@ -25,7 +29,11 @@ async def run(args: argparse.Namespace) -> None: match args.blueprint_command: case "download": await BlueprintDownloadCommand.run(args) + case "get": + await BlueprintGetCommand.run(args) case "upload": await BlueprintUploadCommand.run(args) + case "validate": + await BlueprintValidateCommand.run(args) case _: raise NotImplementedError(args.command_command) diff --git a/src/enapter/cli/http/api/blueprint_get_command.py b/src/enapter/cli/http/api/blueprint_get_command.py new file mode 100644 index 0000000..ff36ea5 --- /dev/null +++ b/src/enapter/cli/http/api/blueprint_get_command.py @@ -0,0 +1,23 @@ +import argparse +import json +import logging + +from enapter import cli, http + +LOGGER = logging.getLogger(__name__) + + +class BlueprintGetCommand(cli.Command): + + @staticmethod + def register(parent: cli.Subparsers) -> None: + parser = parent.add_parser( + "get", formatter_class=argparse.ArgumentDefaultsHelpFormatter + ) + parser.add_argument("id", help="ID of the blueprint to get") + + @staticmethod + async def run(args: argparse.Namespace) -> None: + async with http.api.Client(http.api.Config.from_env()) as client: + blueprint = await client.blueprints.get(blueprint_id=args.id) + print(json.dumps(blueprint.to_dto())) diff --git a/src/enapter/cli/http/api/blueprint_validate_command.py b/src/enapter/cli/http/api/blueprint_validate_command.py new file mode 100644 index 0000000..7af0687 --- /dev/null +++ b/src/enapter/cli/http/api/blueprint_validate_command.py @@ -0,0 +1,27 @@ +import argparse +import logging +import pathlib + +from enapter import cli, http + +LOGGER = logging.getLogger(__name__) + + +class BlueprintValidateCommand(cli.Command): + + @staticmethod + def register(parent: cli.Subparsers) -> None: + parser = parent.add_parser( + "validate", formatter_class=argparse.ArgumentDefaultsHelpFormatter + ) + parser.add_argument( + "path", type=pathlib.Path, help="Path to a directory or a zip file" + ) + + @staticmethod + async def run(args: argparse.Namespace) -> None: + async with http.api.Client(http.api.Config.from_env()) as client: + if args.path.is_dir(): + await client.blueprints.validate_directory(args.path) + else: + await client.blueprints.validate_file(args.path) diff --git a/src/enapter/cli/http/api/device_command.py b/src/enapter/cli/http/api/device_command.py index 4daed9f..e0d2ab8 100644 --- a/src/enapter/cli/http/api/device_command.py +++ b/src/enapter/cli/http/api/device_command.py @@ -3,7 +3,9 @@ from enapter import cli from .device_assign_blueprint_command import DeviceAssignBlueprintCommand +from .device_create_lua_command import DeviceCreateLuaCommand from .device_create_standalone_command import DeviceCreateStandaloneCommand +from .device_create_vucm_command import DeviceCreateVUCMCommand from .device_delete_command import DeviceDeleteCommand from .device_generate_communication_config_command import ( DeviceGenerateCommunicationConfigCommand, @@ -24,7 +26,9 @@ def register(parent: cli.Subparsers) -> None: for command in [ DeviceAssignBlueprintCommand, DeviceCreateStandaloneCommand, + DeviceCreateVUCMCommand, DeviceDeleteCommand, + DeviceCreateLuaCommand, DeviceGenerateCommunicationConfigCommand, DeviceGetCommand, DeviceListCommand, @@ -35,6 +39,8 @@ def register(parent: cli.Subparsers) -> None: @staticmethod async def run(args: argparse.Namespace) -> None: match args.device_command: + case "create-lua": + await DeviceCreateLuaCommand.run(args) case "assign-blueprint": await DeviceAssignBlueprintCommand.run(args) case "create-standalone": @@ -49,5 +55,7 @@ async def run(args: argparse.Namespace) -> None: await DeviceListCommand.run(args) case "update": await DeviceUpdateCommand.run(args) + case "create-vucm": + await DeviceCreateVUCMCommand.run(args) case _: raise NotImplementedError(args.device_command) diff --git a/src/enapter/cli/http/api/device_create_lua_command.py b/src/enapter/cli/http/api/device_create_lua_command.py new file mode 100644 index 0000000..a0e7389 --- /dev/null +++ b/src/enapter/cli/http/api/device_create_lua_command.py @@ -0,0 +1,30 @@ +import argparse +import json + +from enapter import cli, http + + +class DeviceCreateLuaCommand(cli.Command): + + @staticmethod + def register(parent: cli.Subparsers) -> None: + parser = parent.add_parser( + "create-lua", formatter_class=argparse.ArgumentDefaultsHelpFormatter + ) + parser.add_argument("-r", "--runtime-id", help="Runtime ID of the Lua device") + parser.add_argument( + "-b", "--blueprint-id", help="Blueprint ID of the Lua device" + ) + parser.add_argument("-s", "--slug", help="Slug of the Lua device") + parser.add_argument("name", help="Name of the Lua device to create") + + @staticmethod + async def run(args: argparse.Namespace) -> None: + async with http.api.Client(http.api.Config.from_env()) as client: + device = await client.devices.create_lua( + name=args.name, + runtime_id=args.runtime_id, + blueprint_id=args.blueprint_id, + slug=args.slug, + ) + print(json.dumps(device.to_dto())) diff --git a/src/enapter/cli/http/api/device_create_vucm_command.py b/src/enapter/cli/http/api/device_create_vucm_command.py new file mode 100644 index 0000000..83a63d4 --- /dev/null +++ b/src/enapter/cli/http/api/device_create_vucm_command.py @@ -0,0 +1,24 @@ +import argparse +import json + +from enapter import cli, http + + +class DeviceCreateVUCMCommand(cli.Command): + + @staticmethod + def register(parent: cli.Subparsers) -> None: + parser = parent.add_parser( + "create-vucm", formatter_class=argparse.ArgumentDefaultsHelpFormatter + ) + parser.add_argument("-s", "--site-id", help="Site ID to create device in") + parser.add_argument("--hardware-id", help="Hardware ID of the VUCM device") + parser.add_argument("name", help="Name of the VUCM device to create") + + @staticmethod + async def run(args: argparse.Namespace) -> None: + async with http.api.Client(http.api.Config.from_env()) as client: + device = await client.devices.create_vucm( + name=args.name, site_id=args.site_id, hardware_id=args.hardware_id + ) + print(json.dumps(device.to_dto())) diff --git a/src/enapter/http/api/blueprints/client.py b/src/enapter/http/api/blueprints/client.py index cbcae14..b16c6eb 100644 --- a/src/enapter/http/api/blueprints/client.py +++ b/src/enapter/http/api/blueprints/client.py @@ -14,17 +14,20 @@ class Client: def __init__(self, client: httpx.AsyncClient) -> None: self._client = client + async def get(self, blueprint_id: str) -> Blueprint: + url = f"v3/blueprints/{blueprint_id}" + response = await self._client.get(url) + api.check_error(response) + return Blueprint.from_dto(response.json()["blueprint"]) + async def upload_file(self, path: pathlib.Path) -> Blueprint: with path.open("rb") as file: data = file.read() return await self.upload(data) async def upload_directory(self, path: pathlib.Path) -> Blueprint: - buffer = io.BytesIO() - with zipfile.ZipFile(buffer, "w", zipfile.ZIP_DEFLATED) as zip_file: - for file_path in path.rglob("*"): - zip_file.write(file_path, arcname=file_path.relative_to(path)) - return await self.upload(buffer.getvalue()) + data = await self._zip_directory(path) + return await self.upload(data) async def upload(self, data: bytes) -> Blueprint: url = "v3/blueprints/upload" @@ -39,3 +42,29 @@ async def download( response = await self._client.get(url, params={"view": view.value}) api.check_error(response) return response.content + + async def validate_file(self, path: pathlib.Path) -> None: + with path.open("rb") as file: + data = file.read() + await self.validate(data) + + async def validate_directory(self, path: pathlib.Path) -> None: + data = await self._zip_directory(path) + await self.validate(data) + + async def validate(self, data: bytes) -> None: + url = "v3/blueprints/validate" + response = await self._client.post(url, content=data) + api.check_error(response) + validation_errors = response.json().get("validation_errors", []) + if validation_errors: + raise api.MultiError( + [api.Error(msg, code=None, details=None) for msg in validation_errors] + ) + + async def _zip_directory(self, path: pathlib.Path) -> bytes: + buffer = io.BytesIO() + with zipfile.ZipFile(buffer, "w", zipfile.ZIP_DEFLATED) as zip_file: + for file_path in path.rglob("*"): + zip_file.write(file_path, arcname=file_path.relative_to(path)) + return buffer.getvalue() diff --git a/src/enapter/http/api/devices/client.py b/src/enapter/http/api/devices/client.py index b940dd3..5aa6e83 100644 --- a/src/enapter/http/api/devices/client.py +++ b/src/enapter/http/api/devices/client.py @@ -1,3 +1,4 @@ +import random from typing import AsyncGenerator import httpx @@ -17,7 +18,37 @@ def __init__(self, client: httpx.AsyncClient) -> None: async def create_standalone(self, name: str, site_id: str | None = None) -> Device: url = "v3/provisioning/standalone" - response = await self._client.post(url, json={"name": name, "site_id": site_id}) + response = await self._client.post(url, json={"slug": name, "site_id": site_id}) + api.check_error(response) + return await self.get(device_id=response.json()["device_id"]) + + async def create_vucm( + self, name: str, hardware_id: str | None = None, site_id: str | None = None + ) -> Device: + if hardware_id is None: + hardware_id = random_hardware_id() + url = "v3/provisioning/vucm" + response = await self._client.post( + url, json={"name": name, "hardware_id": hardware_id, "site_id": site_id} + ) + api.check_error(response) + return await self.get( + device_id=response.json()["device_id"], expand_communication=True + ) + + async def create_lua( + self, name: str, runtime_id: str, blueprint_id: str, slug: str | None = None + ) -> Device: + url = "v3/provisioning/lua_device" + response = await self._client.post( + url, + json={ + "name": name, + "runtime_id": runtime_id, + "blueprint_id": blueprint_id, + "slug": slug, + }, + ) api.check_error(response) return await self.get(device_id=response.json()["device_id"]) @@ -99,3 +130,7 @@ async def generate_communication_config( response = await self._client.post(url, json={"protocol": mqtt_protocol.value}) api.check_error(response) return CommunicationConfig.from_dto(response.json()["config"]) + + +def random_hardware_id() -> str: + return "V" + "".join(f"{b:02X}" for b in random.randbytes(16))