Skip to content

Commit

Permalink
WIP: making a command work
Browse files Browse the repository at this point in the history
  • Loading branch information
delfick committed Dec 2, 2023
1 parent 00aa76b commit d2dd34a
Show file tree
Hide file tree
Showing 16 changed files with 287 additions and 65 deletions.
5 changes: 4 additions & 1 deletion apps/interactor/interactor/addon.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@
from delfick_project.addons import addon_hook
from interactor.errors import InteractorError
from interactor.options import Options
from interactor.server import Server
from interactor.server import InteractorServer as Server
from photons_app import helpers as hp
from photons_app.formatter import MergedOptionStringFormatter
from photons_app.tasks import task_register as task
Expand Down Expand Up @@ -45,6 +45,9 @@ def port(self):
async def server_kwargs(self):
async with self.target.session() as sender:
yield dict(
reference_resolver_register=self.collector.configuration[
"reference_resolver_register"
],
sender=sender,
options=self.options,
cleaners=self.photons_app.cleaners,
Expand Down
25 changes: 25 additions & 0 deletions apps/interactor/interactor/commander/commands/discover.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
import sanic
from interactor.commander.devices import Devices
from interactor.commander.selector import Selector
from interactor.commander.store import Command, store
from photons_web_server import commander


@store.command
class DiscoveryCommands(Command):
@classmethod
def add_routes(kls, routes: commander.RouteTransformer) -> None:
routes.http(kls.discover, "/v1/discover/serials/<selector>", methods=["GET"])
routes.http(
kls.discover, "/v1/discover/serials", methods=["GET"], name="v1_discover_serials_all"
)

async def discover(
self,
progress: commander.Progress,
request: commander.Request,
selector: Selector,
/,
) -> commander.Response | None:
devices = self.create(Devices, {"selector": selector})
return sanic.json(await devices.serials())
6 changes: 6 additions & 0 deletions apps/interactor/interactor/commander/commands2/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
import os

for filename in os.listdir(os.path.dirname(__file__)):
if not filename.startswith("_") and filename.endswith(".py"):
name = filename[:-3]
__import__(f"interactor.commander.commands.{name}")
Original file line number Diff line number Diff line change
@@ -1,17 +1,28 @@
from textwrap import dedent

import sanic
from delfick_project.norms import dictobj, sb
from interactor.commander import helpers as ihp
from interactor.commander.errors import NoSuchCommand
from interactor.commander.store import store
from interactor.commander.store import Command, store
from photons_web_server import commander


@store.command(name="help")
class HelpCommand(store.Command):
class HelpCommand(Command):
"""
Display the documentation for the specified command
"""

@classmethod
def add_routes(kls, routes: commander.RouteTransformer) -> None:
routes.http(kls.help, "/v1/help")

def route1(
s, progress: commander.Progress, request: commander.Request, /, command: str
) -> commander.Response | None:
return sanic.text("route1")

path = store.injected("path")
store = store.injected("store")

Expand Down
Original file line number Diff line number Diff line change
@@ -1,36 +1,20 @@
from delfick_project.norms import dictobj, sb
import typing as tp

import attrs
import strcs
from interactor.commander import helpers as ihp
from interactor.commander.store import store
from photons_app import helpers as hp
from photons_app.special import SpecialReference
from photons_control.device_finder import DeviceFinder, Filter
from photons_transport.comms.base import Communication


class DeviceChangeMixin(dictobj.Spec):
finder = store.injected("finder")
sender = store.injected("sender")

matcher = dictobj.NullableField(
sb.or_spec(sb.string_spec(), sb.dictionary_spec()),
help="""
What lights to target. If this isn't specified then we interact with all
the lights that can be found on the network.
This can be specfied as either a space separated key=value string or as
a dictionary.
For example,
"label=kitchen,bathroom location_name=home"
or
``{"label": ["kitchen", "bathroom"], "location_name": "home"}``
See https://photons.delfick.com/interacting/device_finder.html#valid-filters
for more information on what filters are available.
""",
)
@attrs.define
class DeviceFinder:
finder: tp.Annotated[DeviceFinder, strcs.FromMeta("finder")]
sender: tp.Annotated[Communication, strcs.FromMeta("sender")]

timeout = dictobj.Field(
sb.float_spec, default=20, help="The max amount of time we wait for replies from the lights"
)
matcher: dict | str = "_"

@hp.memoized_property
def filter(self):
Expand Down Expand Up @@ -60,7 +44,45 @@ async def serials(self):
async for device in self.finder.find(self.filter):
yield device.serial

async def send(self, msg, add_replies=True, result=None, serials=sb.NotSpecified, **kwargs):
async def send(self, msg, add_replies=True, result=None, serials=strcs.NotSpecified, **kwargs):
"""
Send our message and return a ResultBuilder from the results.
If add_replies is False then we won't add packets to the result builder
"""
if result is None:
result = ihp.ResultBuilder()

if serials is strcs.NotSpecified:
serials = await self.serials

result.add_serials(serials)

options = dict(kwargs)
if "message_timeout" not in options:
options["message_timeout"] = self.timeout
if "find_timeout" not in options:
options["find_timeout"] = self.timeout

async for pkt in self.sender(msg, serials, error_catcher=result.error, **options):
if add_replies:
result.add_packet(pkt)

return result


@attrs.define
class Devices:
sender: tp.Annotated[Communication, strcs.FromMeta("sender")]

selector: SpecialReference
timeout: int = 20

async def serials(self) -> list[str]:
_, serials = await self.selector.find(self.sender, timeout=self.timeout)
return serials

async def send(self, msg, add_replies=True, result=None, serials=strcs.NotSpecified, **kwargs):
"""
Send our message and return a ResultBuilder from the results.
Expand All @@ -69,7 +91,7 @@ async def send(self, msg, add_replies=True, result=None, serials=sb.NotSpecified
if result is None:
result = ihp.ResultBuilder()

if serials is sb.NotSpecified:
if serials is strcs.NotSpecified:
serials = await self.serials

result.add_serials(serials)
Expand Down
31 changes: 31 additions & 0 deletions apps/interactor/interactor/commander/selector.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
import attrs
import strcs
from interactor.commander.store import creator
from photons_app.registers import ReferenceResolverRegister
from photons_app.special import SpecialReference


@attrs.define
class Selector:
raw: str
selector: SpecialReference


@creator(Selector)
def create_selector(
val: object, /, reference_resolver_register: ReferenceResolverRegister
) -> strcs.ConvertResponse[Selector]:
if val is strcs.NotSpecified:
val = "_"
if isinstance(val, str):
return {"raw": val, "selector": reference_resolver_register.reference_object(val)}
return None


@creator(SpecialReference)
def create_special_reference(
val: object, /, reference_resolver_register: ReferenceResolverRegister
) -> strcs.ConvertResponse[SpecialReference]:
if isinstance(val, Selector):
return val.selector
return None
10 changes: 8 additions & 2 deletions apps/interactor/interactor/commander/store.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,12 @@
from photons_web_server.commander.store import Store
import strcs
from photons_web_server import commander

store = Store()
reg = strcs.CreateRegister()
creator = reg.make_decorator()

store = commander.Store(strcs_register=reg)

Command = commander.Command


def load_commands():
Expand Down
77 changes: 54 additions & 23 deletions apps/interactor/interactor/server.py
Original file line number Diff line number Diff line change
@@ -1,21 +1,47 @@
import asyncio
import time
import typing as tp

import strcs
from interactor.commander.animations import Animations
from interactor.database import DB
from photons_app import helpers as hp
from photons_app.registers import ReferenceResolverRegister
from photons_control.device_finder import DeviceFinderDaemon, Finder
from photons_web_server.commander import Store
from photons_web_server import commander
from photons_web_server.server import Server
from sanic.request import Request
from strcs import Meta


class Server(Server):
store: Store | None
class InteractorMessageFromExc(commander.MessageFromExc):
def modify_error_dict(
self,
exc_type: commander.store.ExcTypO,
exc: commander.store.ExcO,
tb: commander.store.TBO,
dct: dict[str, object],
) -> dict[str, object]:
if exc_type is strcs.errors.UnableToConvert:
if isinstance(dct.get("into"), dict):
into = dct["into"]
for k in ("cache", "_memoized_cache", "disassemble"):
if k in into:
del into[k]
return dct


class InteractorServer(Server):
store: commander.Store | None

async def setup(
self, *, options, sender, cleaners, store: Store | None = None, animation_options=None
self,
*,
options,
sender,
cleaners,
store: commander.Store | None = None,
animation_options=None,
reference_resolver_register: ReferenceResolverRegister
):
if store is None:
from interactor.commander.store import load_commands, store
Expand All @@ -25,7 +51,7 @@ async def setup(
self.store = store
self.sender = sender
self.cleaners = cleaners
self.wsconnections = {}
self.wsconnections: dict[str, asyncio.Future] = {}
self.server_options = options
self.animation_options = animation_options

Expand All @@ -44,19 +70,18 @@ async def setup(
self.animations = Animations(
self.final_future, self.tasks, self.sender, self.animation_options
)
self.meta = (
Meta(
dict(
tasks=self.tasks,
sender=self.sender,
finder=self.finder,
zeroconf=self.server_options.zeroconf,
database=self.database,
animations=self.animations,
final_future=self.final_future,
server_options=self.server_options,
)
),
self.meta = strcs.Meta(
dict(
tasks=self.tasks,
sender=self.sender,
finder=self.finder,
zeroconf=self.server_options.zeroconf,
reference_resolver_register=reference_resolver_register,
database=self.database,
animations=self.animations,
final_future=self.final_future,
server_options=self.server_options,
)
)

self.app.ctx.server = self
Expand All @@ -65,11 +90,17 @@ async def setup(

async def setup_routes(self):
await super().setup_routes()
self.app.add_route(self.commander.http_handler, "/v1/lifx/command", methods=["PUT"])
self.app.add_websocket_route(
self.wrap_websocket_handler(self.commander.ws_handler), "/v1/ws"
# self.app.add_route(self.commander.http_handler, "/v1/lifx/command", methods=["PUT"])
# self.app.add_websocket_route(
# self.wrap_websocket_handler(self.commander.ws_handler), "/v1/ws"
# )
self.store.register_commands(
self.server_stop_future,
self.meta,
self.app,
self,
message_from_exc=InteractorMessageFromExc,
)
self.store.register_commands(self.server_stop_future, self.meta, self.app, self)

async def before_start(self):
await self.server_options.zeroconf.start(
Expand Down
5 changes: 5 additions & 0 deletions modules/photons_web_server/commander/__init__.py
Original file line number Diff line number Diff line change
@@ -1,3 +1,6 @@
from sanic.request import Request
from sanic.response import BaseHTTPResponse as Response

from .const import REQUEST_IDENTIFIER_HEADER
from .messages import MessageFromExc, ProgressMessageMaker
from .messages import TProgressMessageMaker as Progress
Expand Down Expand Up @@ -26,4 +29,6 @@
"WSSender",
"Websocket",
"WithCommanderClass",
"Request",
"Response",
]
Loading

0 comments on commit d2dd34a

Please sign in to comment.