Skip to content

Commit 301814d

Browse files
authored
Merge pull request #31 from Enapter/rnovatorov/http
Extend HTTP API Support
2 parents 7549dbf + 0f89d97 commit 301814d

File tree

8 files changed

+190
-6
lines changed

8 files changed

+190
-6
lines changed

src/enapter/cli/http/api/blueprint_command.py

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,9 @@
33
from enapter import cli
44

55
from .blueprint_download_command import BlueprintDownloadCommand
6+
from .blueprint_get_command import BlueprintGetCommand
67
from .blueprint_upload_command import BlueprintUploadCommand
8+
from .blueprint_validate_command import BlueprintValidateCommand
79

810

911
class BlueprintCommand(cli.Command):
@@ -16,7 +18,9 @@ def register(parent: cli.Subparsers) -> None:
1618
subparsers = parser.add_subparsers(dest="blueprint_command", required=True)
1719
for command in [
1820
BlueprintDownloadCommand,
21+
BlueprintGetCommand,
1922
BlueprintUploadCommand,
23+
BlueprintValidateCommand,
2024
]:
2125
command.register(subparsers)
2226

@@ -25,7 +29,11 @@ async def run(args: argparse.Namespace) -> None:
2529
match args.blueprint_command:
2630
case "download":
2731
await BlueprintDownloadCommand.run(args)
32+
case "get":
33+
await BlueprintGetCommand.run(args)
2834
case "upload":
2935
await BlueprintUploadCommand.run(args)
36+
case "validate":
37+
await BlueprintValidateCommand.run(args)
3038
case _:
3139
raise NotImplementedError(args.command_command)
Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
import argparse
2+
import json
3+
import logging
4+
5+
from enapter import cli, http
6+
7+
LOGGER = logging.getLogger(__name__)
8+
9+
10+
class BlueprintGetCommand(cli.Command):
11+
12+
@staticmethod
13+
def register(parent: cli.Subparsers) -> None:
14+
parser = parent.add_parser(
15+
"get", formatter_class=argparse.ArgumentDefaultsHelpFormatter
16+
)
17+
parser.add_argument("id", help="ID of the blueprint to get")
18+
19+
@staticmethod
20+
async def run(args: argparse.Namespace) -> None:
21+
async with http.api.Client(http.api.Config.from_env()) as client:
22+
blueprint = await client.blueprints.get(blueprint_id=args.id)
23+
print(json.dumps(blueprint.to_dto()))
Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
1+
import argparse
2+
import logging
3+
import pathlib
4+
5+
from enapter import cli, http
6+
7+
LOGGER = logging.getLogger(__name__)
8+
9+
10+
class BlueprintValidateCommand(cli.Command):
11+
12+
@staticmethod
13+
def register(parent: cli.Subparsers) -> None:
14+
parser = parent.add_parser(
15+
"validate", formatter_class=argparse.ArgumentDefaultsHelpFormatter
16+
)
17+
parser.add_argument(
18+
"path", type=pathlib.Path, help="Path to a directory or a zip file"
19+
)
20+
21+
@staticmethod
22+
async def run(args: argparse.Namespace) -> None:
23+
async with http.api.Client(http.api.Config.from_env()) as client:
24+
if args.path.is_dir():
25+
await client.blueprints.validate_directory(args.path)
26+
else:
27+
await client.blueprints.validate_file(args.path)

src/enapter/cli/http/api/device_command.py

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,9 @@
33
from enapter import cli
44

55
from .device_assign_blueprint_command import DeviceAssignBlueprintCommand
6+
from .device_create_lua_command import DeviceCreateLuaCommand
67
from .device_create_standalone_command import DeviceCreateStandaloneCommand
8+
from .device_create_vucm_command import DeviceCreateVUCMCommand
79
from .device_delete_command import DeviceDeleteCommand
810
from .device_generate_communication_config_command import (
911
DeviceGenerateCommunicationConfigCommand,
@@ -24,7 +26,9 @@ def register(parent: cli.Subparsers) -> None:
2426
for command in [
2527
DeviceAssignBlueprintCommand,
2628
DeviceCreateStandaloneCommand,
29+
DeviceCreateVUCMCommand,
2730
DeviceDeleteCommand,
31+
DeviceCreateLuaCommand,
2832
DeviceGenerateCommunicationConfigCommand,
2933
DeviceGetCommand,
3034
DeviceListCommand,
@@ -35,6 +39,8 @@ def register(parent: cli.Subparsers) -> None:
3539
@staticmethod
3640
async def run(args: argparse.Namespace) -> None:
3741
match args.device_command:
42+
case "create-lua":
43+
await DeviceCreateLuaCommand.run(args)
3844
case "assign-blueprint":
3945
await DeviceAssignBlueprintCommand.run(args)
4046
case "create-standalone":
@@ -49,5 +55,7 @@ async def run(args: argparse.Namespace) -> None:
4955
await DeviceListCommand.run(args)
5056
case "update":
5157
await DeviceUpdateCommand.run(args)
58+
case "create-vucm":
59+
await DeviceCreateVUCMCommand.run(args)
5260
case _:
5361
raise NotImplementedError(args.device_command)
Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,30 @@
1+
import argparse
2+
import json
3+
4+
from enapter import cli, http
5+
6+
7+
class DeviceCreateLuaCommand(cli.Command):
8+
9+
@staticmethod
10+
def register(parent: cli.Subparsers) -> None:
11+
parser = parent.add_parser(
12+
"create-lua", formatter_class=argparse.ArgumentDefaultsHelpFormatter
13+
)
14+
parser.add_argument("-r", "--runtime-id", help="Runtime ID of the Lua device")
15+
parser.add_argument(
16+
"-b", "--blueprint-id", help="Blueprint ID of the Lua device"
17+
)
18+
parser.add_argument("-s", "--slug", help="Slug of the Lua device")
19+
parser.add_argument("name", help="Name of the Lua device to create")
20+
21+
@staticmethod
22+
async def run(args: argparse.Namespace) -> None:
23+
async with http.api.Client(http.api.Config.from_env()) as client:
24+
device = await client.devices.create_lua(
25+
name=args.name,
26+
runtime_id=args.runtime_id,
27+
blueprint_id=args.blueprint_id,
28+
slug=args.slug,
29+
)
30+
print(json.dumps(device.to_dto()))
Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
import argparse
2+
import json
3+
4+
from enapter import cli, http
5+
6+
7+
class DeviceCreateVUCMCommand(cli.Command):
8+
9+
@staticmethod
10+
def register(parent: cli.Subparsers) -> None:
11+
parser = parent.add_parser(
12+
"create-vucm", formatter_class=argparse.ArgumentDefaultsHelpFormatter
13+
)
14+
parser.add_argument("-s", "--site-id", help="Site ID to create device in")
15+
parser.add_argument("--hardware-id", help="Hardware ID of the VUCM device")
16+
parser.add_argument("name", help="Name of the VUCM device to create")
17+
18+
@staticmethod
19+
async def run(args: argparse.Namespace) -> None:
20+
async with http.api.Client(http.api.Config.from_env()) as client:
21+
device = await client.devices.create_vucm(
22+
name=args.name, site_id=args.site_id, hardware_id=args.hardware_id
23+
)
24+
print(json.dumps(device.to_dto()))

src/enapter/http/api/blueprints/client.py

Lines changed: 34 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -14,17 +14,20 @@ class Client:
1414
def __init__(self, client: httpx.AsyncClient) -> None:
1515
self._client = client
1616

17+
async def get(self, blueprint_id: str) -> Blueprint:
18+
url = f"v3/blueprints/{blueprint_id}"
19+
response = await self._client.get(url)
20+
api.check_error(response)
21+
return Blueprint.from_dto(response.json()["blueprint"])
22+
1723
async def upload_file(self, path: pathlib.Path) -> Blueprint:
1824
with path.open("rb") as file:
1925
data = file.read()
2026
return await self.upload(data)
2127

2228
async def upload_directory(self, path: pathlib.Path) -> Blueprint:
23-
buffer = io.BytesIO()
24-
with zipfile.ZipFile(buffer, "w", zipfile.ZIP_DEFLATED) as zip_file:
25-
for file_path in path.rglob("*"):
26-
zip_file.write(file_path, arcname=file_path.relative_to(path))
27-
return await self.upload(buffer.getvalue())
29+
data = await self._zip_directory(path)
30+
return await self.upload(data)
2831

2932
async def upload(self, data: bytes) -> Blueprint:
3033
url = "v3/blueprints/upload"
@@ -39,3 +42,29 @@ async def download(
3942
response = await self._client.get(url, params={"view": view.value})
4043
api.check_error(response)
4144
return response.content
45+
46+
async def validate_file(self, path: pathlib.Path) -> None:
47+
with path.open("rb") as file:
48+
data = file.read()
49+
await self.validate(data)
50+
51+
async def validate_directory(self, path: pathlib.Path) -> None:
52+
data = await self._zip_directory(path)
53+
await self.validate(data)
54+
55+
async def validate(self, data: bytes) -> None:
56+
url = "v3/blueprints/validate"
57+
response = await self._client.post(url, content=data)
58+
api.check_error(response)
59+
validation_errors = response.json().get("validation_errors", [])
60+
if validation_errors:
61+
raise api.MultiError(
62+
[api.Error(msg, code=None, details=None) for msg in validation_errors]
63+
)
64+
65+
async def _zip_directory(self, path: pathlib.Path) -> bytes:
66+
buffer = io.BytesIO()
67+
with zipfile.ZipFile(buffer, "w", zipfile.ZIP_DEFLATED) as zip_file:
68+
for file_path in path.rglob("*"):
69+
zip_file.write(file_path, arcname=file_path.relative_to(path))
70+
return buffer.getvalue()

src/enapter/http/api/devices/client.py

Lines changed: 36 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
import random
12
from typing import AsyncGenerator
23

34
import httpx
@@ -17,7 +18,37 @@ def __init__(self, client: httpx.AsyncClient) -> None:
1718

1819
async def create_standalone(self, name: str, site_id: str | None = None) -> Device:
1920
url = "v3/provisioning/standalone"
20-
response = await self._client.post(url, json={"name": name, "site_id": site_id})
21+
response = await self._client.post(url, json={"slug": name, "site_id": site_id})
22+
api.check_error(response)
23+
return await self.get(device_id=response.json()["device_id"])
24+
25+
async def create_vucm(
26+
self, name: str, hardware_id: str | None = None, site_id: str | None = None
27+
) -> Device:
28+
if hardware_id is None:
29+
hardware_id = random_hardware_id()
30+
url = "v3/provisioning/vucm"
31+
response = await self._client.post(
32+
url, json={"name": name, "hardware_id": hardware_id, "site_id": site_id}
33+
)
34+
api.check_error(response)
35+
return await self.get(
36+
device_id=response.json()["device_id"], expand_communication=True
37+
)
38+
39+
async def create_lua(
40+
self, name: str, runtime_id: str, blueprint_id: str, slug: str | None = None
41+
) -> Device:
42+
url = "v3/provisioning/lua_device"
43+
response = await self._client.post(
44+
url,
45+
json={
46+
"name": name,
47+
"runtime_id": runtime_id,
48+
"blueprint_id": blueprint_id,
49+
"slug": slug,
50+
},
51+
)
2152
api.check_error(response)
2253
return await self.get(device_id=response.json()["device_id"])
2354

@@ -99,3 +130,7 @@ async def generate_communication_config(
99130
response = await self._client.post(url, json={"protocol": mqtt_protocol.value})
100131
api.check_error(response)
101132
return CommunicationConfig.from_dto(response.json()["config"])
133+
134+
135+
def random_hardware_id() -> str:
136+
return "V" + "".join(f"{b:02X}" for b in random.randbytes(16))

0 commit comments

Comments
 (0)