Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 8 additions & 0 deletions src/enapter/cli/http/api/blueprint_command.py
Original file line number Diff line number Diff line change
Expand Up @@ -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):
Expand All @@ -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)

Expand All @@ -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)
23 changes: 23 additions & 0 deletions src/enapter/cli/http/api/blueprint_get_command.py
Original file line number Diff line number Diff line change
@@ -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()))
27 changes: 27 additions & 0 deletions src/enapter/cli/http/api/blueprint_validate_command.py
Original file line number Diff line number Diff line change
@@ -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)
8 changes: 8 additions & 0 deletions src/enapter/cli/http/api/device_command.py
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand All @@ -24,7 +26,9 @@ def register(parent: cli.Subparsers) -> None:
for command in [
DeviceAssignBlueprintCommand,
DeviceCreateStandaloneCommand,
DeviceCreateVUCMCommand,
DeviceDeleteCommand,
DeviceCreateLuaCommand,
DeviceGenerateCommunicationConfigCommand,
DeviceGetCommand,
DeviceListCommand,
Expand All @@ -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":
Expand All @@ -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)
30 changes: 30 additions & 0 deletions src/enapter/cli/http/api/device_create_lua_command.py
Original file line number Diff line number Diff line change
@@ -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()))
24 changes: 24 additions & 0 deletions src/enapter/cli/http/api/device_create_vucm_command.py
Original file line number Diff line number Diff line change
@@ -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()))
39 changes: 34 additions & 5 deletions src/enapter/http/api/blueprints/client.py
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand All @@ -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()
37 changes: 36 additions & 1 deletion src/enapter/http/api/devices/client.py
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import random
from typing import AsyncGenerator

import httpx
Expand All @@ -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"])

Expand Down Expand Up @@ -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))