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
14 changes: 14 additions & 0 deletions examples/http/list_sites.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
import asyncio

import enapter


async def main():
config = enapter.http.api.Config.from_env()
async with enapter.http.api.Client(config=config) as client:
async for site in client.sites.list():
print(site)


if __name__ == "__main__":
asyncio.run(main())
4 changes: 4 additions & 0 deletions src/enapter/cli/http/api/command.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
from enapter import cli

from .device_command import DeviceCommand
from .site_command import SiteCommand


class Command(cli.Command):
Expand All @@ -15,6 +16,7 @@ def register(parent: cli.Subparsers) -> None:
subparsers = parser.add_subparsers(dest="http_api_command", required=True)
for command in [
DeviceCommand,
SiteCommand,
]:
command.register(subparsers)

Expand All @@ -23,5 +25,7 @@ async def run(args: argparse.Namespace) -> None:
match args.http_api_command:
case "device":
await DeviceCommand.run(args)
case "site":
await SiteCommand.run(args)
case _:
raise NotImplementedError(args.device_command)
4 changes: 1 addition & 3 deletions src/enapter/cli/http/api/device_create_standalone_command.py
Original file line number Diff line number Diff line change
Expand Up @@ -11,9 +11,7 @@ def register(parent: cli.Subparsers) -> None:
parser = parent.add_parser(
"create-standalone", formatter_class=argparse.ArgumentDefaultsHelpFormatter
)
parser.add_argument(
"name", help="ID or slug of the device to get information about"
)
parser.add_argument("name", help="Name of the standalone device to create")
parser.add_argument(
"-s", "--site-id", help="Site ID to create device in", default=None
)
Expand Down
43 changes: 43 additions & 0 deletions src/enapter/cli/http/api/site_command.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
import argparse

from enapter import cli

from .site_create_command import SiteCreateCommand
from .site_delete_command import SiteDeleteCommand
from .site_get_command import SiteGetCommand
from .site_list_command import SiteListCommand
from .site_update_command import SiteUpdateCommand


class SiteCommand(cli.Command):

@staticmethod
def register(parent: cli.Subparsers) -> None:
parser = parent.add_parser(
"site", formatter_class=argparse.ArgumentDefaultsHelpFormatter
)
subparsers = parser.add_subparsers(dest="http_api_site_command", required=True)
for command in [
SiteCreateCommand,
SiteDeleteCommand,
SiteGetCommand,
SiteListCommand,
SiteUpdateCommand,
]:
command.register(subparsers)

@staticmethod
async def run(args: argparse.Namespace) -> None:
match args.http_api_site_command:
case "create":
await SiteCreateCommand.run(args)
case "delete":
await SiteDeleteCommand.run(args)
case "get":
await SiteGetCommand.run(args)
case "list":
await SiteListCommand.run(args)
case "update":
await SiteUpdateCommand.run(args)
case _:
raise NotImplementedError(args.device_command)
43 changes: 43 additions & 0 deletions src/enapter/cli/http/api/site_create_command.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
import argparse
import json

from enapter import cli, http

from .site_location import parse_site_location


class SiteCreateCommand(cli.Command):

@staticmethod
def register(parent: cli.Subparsers) -> None:
parser = parent.add_parser(
"create", formatter_class=argparse.ArgumentDefaultsHelpFormatter
)
parser.add_argument("name", help="Name of the site to create")
parser.add_argument(
"-t", "--timezone", help="Timezone of the site to create", default="UTC"
)
parser.add_argument(
"-l",
"--location",
type=parse_site_location,
help="Site location in the format NAME,LATITUDE,LONGITUDE",
)

@staticmethod
async def run(args: argparse.Namespace) -> None:
async with http.api.Client(http.api.Config.from_env()) as client:
site = await client.sites.create(
name=args.name,
timezone=args.timezone,
location=(
http.api.sites.Location(
name=args.location[0],
latitude=args.location[1],
longitude=args.location[2],
)
if args.location is not None
else None
),
)
print(json.dumps(site.to_dto()))
18 changes: 18 additions & 0 deletions src/enapter/cli/http/api/site_delete_command.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
import argparse

from enapter import cli, http


class SiteDeleteCommand(cli.Command):

@staticmethod
def register(parent: cli.Subparsers) -> None:
parser = parent.add_parser(
"delete", formatter_class=argparse.ArgumentDefaultsHelpFormatter
)
parser.add_argument("id", type=str, help="ID of the site to delete")

@staticmethod
async def run(args: argparse.Namespace) -> None:
async with http.api.Client(http.api.Config.from_env()) as client:
await client.sites.delete(args.id)
22 changes: 22 additions & 0 deletions src/enapter/cli/http/api/site_get_command.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
import argparse
import json

from enapter import cli, http


class SiteGetCommand(cli.Command):

@staticmethod
def register(parent: cli.Subparsers) -> None:
parser = parent.add_parser(
"get", formatter_class=argparse.ArgumentDefaultsHelpFormatter
)
parser.add_argument(
"id", nargs="?", type=str, help="ID of the site to retrieve"
)

@staticmethod
async def run(args: argparse.Namespace) -> None:
async with http.api.Client(http.api.Config.from_env()) as client:
site = await client.sites.get(args.id)
print(json.dumps(site.to_dto()))
33 changes: 33 additions & 0 deletions src/enapter/cli/http/api/site_list_command.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
import argparse
import json

from enapter import cli, http


class SiteListCommand(cli.Command):

@staticmethod
def register(parent: cli.Subparsers) -> None:
parser = parent.add_parser(
"list", formatter_class=argparse.ArgumentDefaultsHelpFormatter
)
parser.add_argument(
"-l",
"--limit",
type=int,
help="Maximum number of sites to list",
default=-1,
)

@staticmethod
async def run(args: argparse.Namespace) -> None:
if args.limit == 0:
return
async with http.api.Client(http.api.Config.from_env()) as client:
async with client.sites.list() as stream:
count = 0
async for site in stream:
print(json.dumps(site.to_dto()))
count += 1
if args.limit > 0 and count == args.limit:
break
11 changes: 11 additions & 0 deletions src/enapter/cli/http/api/site_location.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
import argparse


def parse_site_location(location_str: str) -> tuple[str, float, float]:
try:
name, lat_str, lon_str = location_str.split(",")
return name, float(lat_str), float(lon_str)
except ValueError:
raise argparse.ArgumentTypeError(
"Location must be in the format NAME,LATITUDE,LONGITUDE"
)
45 changes: 45 additions & 0 deletions src/enapter/cli/http/api/site_update_command.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
import argparse
import json

from enapter import cli, http

from .site_location import parse_site_location


class SiteUpdateCommand(cli.Command):

@staticmethod
def register(parent: cli.Subparsers) -> None:
parser = parent.add_parser(
"update", formatter_class=argparse.ArgumentDefaultsHelpFormatter
)
parser.add_argument("id", nargs="?", type=str, help="ID of the site to update")
parser.add_argument("-n", "--name", type=str, help="New name for the site")
parser.add_argument(
"-t", "--timezone", type=str, help="New timezone for the site"
)
parser.add_argument(
"-l",
"--location",
type=parse_site_location,
help="New location for the site",
)

@staticmethod
async def run(args: argparse.Namespace) -> None:
async with http.api.Client(http.api.Config.from_env()) as client:
site = await client.sites.update(
site_id=args.id,
name=args.name,
timezone=args.timezone,
location=(
http.api.sites.Location(
name=args.location[0],
latitude=args.location[1],
longitude=args.location[2],
)
if args.location is not None
else None
),
)
print(json.dumps(site.to_dto()))
4 changes: 2 additions & 2 deletions src/enapter/http/api/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,6 @@
from .config import Config
from .errors import Error, MultiError, check_error

from . import devices # isort: skip
from . import devices, sites # isort: skip

__all__ = ["Client", "Config", "devices", "Error", "MultiError", "check_error"]
__all__ = ["Client", "Config", "devices", "sites", "Error", "MultiError", "check_error"]
6 changes: 5 additions & 1 deletion src/enapter/http/api/client.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@

import httpx

from enapter.http.api import devices
from enapter.http.api import devices, sites

from .config import Config

Expand Down Expand Up @@ -33,3 +33,7 @@ async def __aexit__(self, *exc) -> None:
@property
def devices(self) -> devices.Client:
return devices.Client(client=self._client)

@property
def sites(self) -> sites.Client:
return sites.Client(client=self._client)
9 changes: 9 additions & 0 deletions src/enapter/http/api/sites/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
from .client import Client
from .location import Location
from .site import Site

__all__ = [
"Client",
"Site",
"Location",
]
79 changes: 79 additions & 0 deletions src/enapter/http/api/sites/client.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,79 @@
from typing import AsyncGenerator

import httpx

from enapter import async_
from enapter.http import api

from .location import Location
from .site import Site


class Client:

def __init__(self, client: httpx.AsyncClient) -> None:
self._client = client

async def create(
self, name: str, timezone: str, location: Location | None = None
) -> Site:
url = "v3/sites"
response = await self._client.post(
url,
json={
"name": name,
"timezone": timezone,
"location": location.to_dto() if location is not None else None,
},
)
api.check_error(response)
return Site.from_dto(response.json()["site"])

async def get(self, site_id: str | None) -> Site:
url = f"v3/sites/{site_id}" if site_id is not None else "v3/site"
response = await self._client.get(url)
api.check_error(response)
return Site.from_dto(response.json()["site"])

@async_.generator
async def list(self) -> AsyncGenerator[Site, None]:
url = "v3/sites"
limit = 50
offset = 0
while True:
response = await self._client.get(
url, params={"limit": limit, "offset": offset}
)
api.check_error(response)
payload = response.json()
if not payload["sites"]:
return
for dto in payload["sites"]:
yield Site.from_dto(dto)
offset += limit

async def update(
self,
site_id: str | None,
name: str | None = None,
timezone: str | None = None,
location: Location | None = None,
) -> Site:
if name is None and timezone is None and location is None:
return await self.get(site_id)
url = f"v3/sites/{site_id}" if site_id is not None else "v3/site"
response = await self._client.patch(
url,
json={
"name": name,
"timezone": timezone,
"location": location.to_dto() if location is not None else None,
},
)
api.check_error(response)
return Site.from_dto(response.json()["site"])

async def delete(self, site_id: str) -> None:
url = f"v3/sites/{site_id}"
response = await self._client.delete(url)
api.check_error(response)
Loading