Skip to content

Commit 12be0d5

Browse files
committed
http: api: implement basic blueprints support
1 parent f7bb16e commit 12be0d5

File tree

9 files changed

+195
-3
lines changed

9 files changed

+195
-3
lines changed
Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,31 @@
1+
import argparse
2+
3+
from enapter import cli
4+
5+
from .blueprint_download_command import BlueprintDownloadCommand
6+
from .blueprint_upload_command import BlueprintUploadCommand
7+
8+
9+
class BlueprintCommand(cli.Command):
10+
11+
@staticmethod
12+
def register(parent: cli.Subparsers) -> None:
13+
parser = parent.add_parser(
14+
"blueprint", formatter_class=argparse.ArgumentDefaultsHelpFormatter
15+
)
16+
subparsers = parser.add_subparsers(dest="blueprint_command", required=True)
17+
for command in [
18+
BlueprintDownloadCommand,
19+
BlueprintUploadCommand,
20+
]:
21+
command.register(subparsers)
22+
23+
@staticmethod
24+
async def run(args: argparse.Namespace) -> None:
25+
match args.blueprint_command:
26+
case "download":
27+
await BlueprintDownloadCommand.run(args)
28+
case "upload":
29+
await BlueprintUploadCommand.run(args)
30+
case _:
31+
raise NotImplementedError(args.command_command)
Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,36 @@
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 BlueprintDownloadCommand(cli.Command):
11+
12+
@staticmethod
13+
def register(parent: cli.Subparsers) -> None:
14+
parser = parent.add_parser(
15+
"download", formatter_class=argparse.ArgumentDefaultsHelpFormatter
16+
)
17+
parser.add_argument("id", help="ID of the blueprint to download")
18+
parser.add_argument(
19+
"-o", "--output", type=pathlib.Path, help="Output file path", required=True
20+
)
21+
parser.add_argument(
22+
"-v",
23+
"--view",
24+
choices=["original", "compiled"],
25+
default="original",
26+
help="Blueprint view type",
27+
)
28+
29+
@staticmethod
30+
async def run(args: argparse.Namespace) -> None:
31+
async with http.api.Client(http.api.Config.from_env()) as client:
32+
content = await client.blueprints.download(
33+
args.id, view=http.api.blueprints.BlueprintView(args.view.upper())
34+
)
35+
with open(args.output, "wb") as f:
36+
f.write(content)
Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,29 @@
1+
import argparse
2+
import json
3+
import logging
4+
import pathlib
5+
6+
from enapter import cli, http
7+
8+
LOGGER = logging.getLogger(__name__)
9+
10+
11+
class BlueprintUploadCommand(cli.Command):
12+
13+
@staticmethod
14+
def register(parent: cli.Subparsers) -> None:
15+
parser = parent.add_parser(
16+
"upload", formatter_class=argparse.ArgumentDefaultsHelpFormatter
17+
)
18+
parser.add_argument(
19+
"path", type=pathlib.Path, help="Path to a directory or a zip file"
20+
)
21+
22+
@staticmethod
23+
async def run(args: argparse.Namespace) -> None:
24+
async with http.api.Client(http.api.Config.from_env()) as client:
25+
if args.path.is_dir():
26+
blueprint = await client.blueprints.upload_directory(args.path)
27+
else:
28+
blueprint = await client.blueprints.upload_file(args.path)
29+
print(json.dumps(blueprint.to_dto()))

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

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22

33
from enapter import cli
44

5+
from .blueprint_command import BlueprintCommand
56
from .device_command import DeviceCommand
67
from .site_command import SiteCommand
78

@@ -15,6 +16,7 @@ def register(parent: cli.Subparsers) -> None:
1516
)
1617
subparsers = parser.add_subparsers(dest="api_command", required=True)
1718
for command in [
19+
BlueprintCommand,
1820
DeviceCommand,
1921
SiteCommand,
2022
]:
@@ -23,6 +25,8 @@ def register(parent: cli.Subparsers) -> None:
2325
@staticmethod
2426
async def run(args: argparse.Namespace) -> None:
2527
match args.api_command:
28+
case "blueprint":
29+
await BlueprintCommand.run(args)
2630
case "device":
2731
await DeviceCommand.run(args)
2832
case "site":

src/enapter/http/api/__init__.py

Lines changed: 11 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,15 @@
22
from .config import Config
33
from .errors import Error, MultiError, check_error
44

5-
from . import devices, sites # isort: skip
5+
from . import devices, sites, blueprints # isort: skip
66

7-
__all__ = ["Client", "Config", "devices", "sites", "Error", "MultiError", "check_error"]
7+
__all__ = [
8+
"Client",
9+
"Config",
10+
"devices",
11+
"blueprints",
12+
"sites",
13+
"Error",
14+
"MultiError",
15+
"check_error",
16+
]
Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
from .blueprint import Blueprint, BlueprintView
2+
from .client import Client
3+
4+
__all__ = [
5+
"Client",
6+
"Blueprint",
7+
"BlueprintView",
8+
]
Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,30 @@
1+
import dataclasses
2+
import datetime
3+
import enum
4+
from typing import Any, Self
5+
6+
7+
class BlueprintView(enum.Enum):
8+
9+
ORIGINAL = "ORIGINAL"
10+
COMPILED = "COMPILED"
11+
12+
13+
@dataclasses.dataclass
14+
class Blueprint:
15+
16+
id: str
17+
created_at: datetime.datetime
18+
19+
@classmethod
20+
def from_dto(cls, dto: dict[str, Any]) -> Self:
21+
return cls(
22+
id=dto["id"],
23+
created_at=datetime.datetime.fromisoformat(dto["created_at"]),
24+
)
25+
26+
def to_dto(self) -> dict[str, Any]:
27+
return {
28+
"id": self.id,
29+
"created_at": self.created_at.isoformat(),
30+
}
Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,41 @@
1+
import io
2+
import pathlib
3+
import zipfile
4+
5+
import httpx
6+
7+
from enapter.http import api
8+
9+
from .blueprint import Blueprint, BlueprintView
10+
11+
12+
class Client:
13+
14+
def __init__(self, client: httpx.AsyncClient) -> None:
15+
self._client = client
16+
17+
async def upload_file(self, path: pathlib.Path) -> Blueprint:
18+
with path.open("rb") as file:
19+
data = file.read()
20+
return await self.upload(data)
21+
22+
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())
28+
29+
async def upload(self, data: bytes) -> Blueprint:
30+
url = "v3/blueprints/upload"
31+
response = await self._client.post(url, content=data)
32+
api.check_error(response)
33+
return Blueprint.from_dto(response.json()["blueprint"])
34+
35+
async def download(
36+
self, blueprint_id: str, view: BlueprintView = BlueprintView.ORIGINAL
37+
) -> bytes:
38+
url = f"v3/blueprints/{blueprint_id}/zip"
39+
response = await self._client.get(url, params={"view": view.value})
40+
api.check_error(response)
41+
return response.content

src/enapter/http/api/client.py

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@
22

33
import httpx
44

5-
from enapter.http.api import devices, sites
5+
from enapter.http.api import blueprints, devices, sites
66

77
from .config import Config
88

@@ -37,3 +37,7 @@ def devices(self) -> devices.Client:
3737
@property
3838
def sites(self) -> sites.Client:
3939
return sites.Client(client=self._client)
40+
41+
@property
42+
def blueprints(self) -> blueprints.Client:
43+
return blueprints.Client(client=self._client)

0 commit comments

Comments
 (0)