Add annot removal via client methods or ctx-mngr
Since leaking annots to a remote `chart` actor probably isn't a thing we want to do (often), add a removal/deletion handler block to the `remote_annotate()` ctx which can be triggered using a `{rm_annot: aid}` msg. Augmnent the `AnnotCtl` with, - `.remove() which sends said msg (from above) and returns a `bool` indicating success. - add an `.open_rect()` acm which does the `.add_rect()` / `.remove()` calls underneath for use in scope oriented client usage. - add a `._annot_stack: AsyncExitStack` which will always have any/all non-`.open_rect()` calls to `.add_rect()` register removal on client teardown, to avoid leaking annots when a client finally disconnects. - comment out the `.modify()` meth idea for now. - rename all `Xstream` var-tags to `Xipc` names.distribute_dis
parent
e33d6333ec
commit
bbb98597a0
|
@ -20,10 +20,13 @@ to a chart from some other actor.
|
||||||
|
|
||||||
'''
|
'''
|
||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
from contextlib import asynccontextmanager as acm
|
from contextlib import (
|
||||||
|
asynccontextmanager as acm,
|
||||||
|
AsyncExitStack,
|
||||||
|
)
|
||||||
from pprint import pformat
|
from pprint import pformat
|
||||||
from typing import (
|
from typing import (
|
||||||
Any,
|
# Any,
|
||||||
AsyncContextManager,
|
AsyncContextManager,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
@ -45,6 +48,7 @@ from ._display import DisplayState
|
||||||
from ._interaction import ChartView
|
from ._interaction import ChartView
|
||||||
from ._editors import SelectRect
|
from ._editors import SelectRect
|
||||||
from ._chart import ChartPlotWidget
|
from ._chart import ChartPlotWidget
|
||||||
|
from ..brokers import SymbolNotFound
|
||||||
|
|
||||||
|
|
||||||
log = get_logger(__name__)
|
log = get_logger(__name__)
|
||||||
|
@ -61,8 +65,10 @@ _dss: dict[str, DisplayState] | None = None
|
||||||
# _ctxs: set[Context] = set()
|
# _ctxs: set[Context] = set()
|
||||||
_ctxs: list[Context] = []
|
_ctxs: list[Context] = []
|
||||||
|
|
||||||
# global map of all uniquely created annotation-graphics
|
# XXX: global map of all uniquely created annotation-graphics so
|
||||||
# so that they can be mutated (eventually) by a client.
|
# that they can be mutated (eventually) by a client.
|
||||||
|
# NOTE: this map is only populated on the `chart` actor side (aka
|
||||||
|
# the "annotations server" which actually renders to a Qt canvas).
|
||||||
_annots: dict[int, QGraphicsItem] = {}
|
_annots: dict[int, QGraphicsItem] = {}
|
||||||
|
|
||||||
|
|
||||||
|
@ -123,7 +129,22 @@ async def remote_annotate(
|
||||||
# delegate generically to the requested method
|
# delegate generically to the requested method
|
||||||
getattr(rect, meth)(**kwargs)
|
getattr(rect, meth)(**kwargs)
|
||||||
rect.show()
|
rect.show()
|
||||||
await annot_req_stream.send(id(rect))
|
aid: int = id(rect)
|
||||||
|
_annots[aid] = rect
|
||||||
|
await annot_req_stream.send(aid)
|
||||||
|
|
||||||
|
case {
|
||||||
|
'rm_annot': int(aid),
|
||||||
|
}:
|
||||||
|
# NOTE: this is normally entered on
|
||||||
|
# a client's annotation de-alloc normally
|
||||||
|
# prior to detach or modify.
|
||||||
|
annot: 'QGraphicsItem' = _annots[aid]
|
||||||
|
annot.delete()
|
||||||
|
|
||||||
|
# respond to client indicating annot
|
||||||
|
# was indeed deleted.
|
||||||
|
await annot_req_stream.send(aid)
|
||||||
|
|
||||||
case _:
|
case _:
|
||||||
log.error(
|
log.error(
|
||||||
|
@ -142,7 +163,9 @@ class AnnotCtl(Struct):
|
||||||
|
|
||||||
'''
|
'''
|
||||||
ctx2fqmes: dict[str, str]
|
ctx2fqmes: dict[str, str]
|
||||||
fqme2stream: dict[str, MsgStream]
|
fqme2ipc: dict[str, MsgStream]
|
||||||
|
_annot_stack = AsyncExitStack()
|
||||||
|
_ipcs: dict[int, MsgStream] = {}
|
||||||
|
|
||||||
async def add_rect(
|
async def add_rect(
|
||||||
self,
|
self,
|
||||||
|
@ -155,13 +178,21 @@ class AnnotCtl(Struct):
|
||||||
domain: str = 'view', # or 'scene'
|
domain: str = 'view', # or 'scene'
|
||||||
color: str = 'dad_blue',
|
color: str = 'dad_blue',
|
||||||
|
|
||||||
|
from_acm: bool = False,
|
||||||
|
|
||||||
) -> int:
|
) -> int:
|
||||||
'''
|
'''
|
||||||
Add a `SelectRect` annotation to the target view, return
|
Add a `SelectRect` annotation to the target view, return
|
||||||
the instances `id(obj)` from the remote UI actor.
|
the instances `id(obj)` from the remote UI actor.
|
||||||
|
|
||||||
'''
|
'''
|
||||||
ipc: MsgStream = self.fqme2stream[fqme]
|
ipc: MsgStream = self.fqme2ipc.get(fqme)
|
||||||
|
if ipc is None:
|
||||||
|
raise SymbolNotFound(
|
||||||
|
'No chart (actor) seems to have mkt feed loaded?\n'
|
||||||
|
f'{fqme}'
|
||||||
|
)
|
||||||
|
|
||||||
await ipc.send({
|
await ipc.send({
|
||||||
'fqme': fqme,
|
'fqme': fqme,
|
||||||
'annot': 'SelectRect',
|
'annot': 'SelectRect',
|
||||||
|
@ -175,32 +206,58 @@ class AnnotCtl(Struct):
|
||||||
'update_label': False,
|
'update_label': False,
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
return (await ipc.receive())
|
aid: int = await ipc.receive()
|
||||||
|
self._ipcs[aid] = ipc
|
||||||
async def modify(
|
if not from_acm:
|
||||||
self,
|
self._annot_stack.push_async_exit(
|
||||||
aid: int, # annotation id
|
self.remove(aid)
|
||||||
meth: str, # far end graphics object method to invoke
|
)
|
||||||
params: dict[str, Any], # far end `meth(**kwargs)`
|
return aid
|
||||||
) -> bool:
|
|
||||||
'''
|
|
||||||
Modify an existing (remote) annotation's graphics
|
|
||||||
paramters, thus changing it's appearance / state in real
|
|
||||||
time.
|
|
||||||
|
|
||||||
'''
|
|
||||||
raise NotImplementedError
|
|
||||||
|
|
||||||
async def remove(
|
async def remove(
|
||||||
self,
|
self,
|
||||||
uid: int,
|
aid: int,
|
||||||
|
|
||||||
) -> bool:
|
) -> bool:
|
||||||
'''
|
'''
|
||||||
Remove an existing annotation by instance id.
|
Remove an existing annotation by instance id.
|
||||||
|
|
||||||
'''
|
'''
|
||||||
raise NotImplementedError
|
ipc: MsgStream = self._ipcs[aid]
|
||||||
|
await ipc.send({
|
||||||
|
'rm_annot': aid,
|
||||||
|
})
|
||||||
|
removed: bool = await ipc.receive()
|
||||||
|
return removed
|
||||||
|
|
||||||
|
@acm
|
||||||
|
async def open_rect(
|
||||||
|
self,
|
||||||
|
**kwargs,
|
||||||
|
) -> int:
|
||||||
|
try:
|
||||||
|
aid: int = await self.add_rect(
|
||||||
|
from_acm=True,
|
||||||
|
**kwargs,
|
||||||
|
)
|
||||||
|
yield aid
|
||||||
|
finally:
|
||||||
|
await self.remove(aid)
|
||||||
|
|
||||||
|
# TODO: do we even need this?
|
||||||
|
# async def modify(
|
||||||
|
# self,
|
||||||
|
# aid: int, # annotation id
|
||||||
|
# meth: str, # far end graphics object method to invoke
|
||||||
|
# params: dict[str, Any], # far end `meth(**kwargs)`
|
||||||
|
# ) -> bool:
|
||||||
|
# '''
|
||||||
|
# Modify an existing (remote) annotation's graphics
|
||||||
|
# paramters, thus changing it's appearance / state in real
|
||||||
|
# time.
|
||||||
|
|
||||||
|
# '''
|
||||||
|
# raise NotImplementedError
|
||||||
|
|
||||||
|
|
||||||
@acm
|
@acm
|
||||||
|
@ -235,20 +292,25 @@ async def open_annot_ctl(
|
||||||
)
|
)
|
||||||
|
|
||||||
ctx2fqmes: dict[str, set[str]] = {}
|
ctx2fqmes: dict[str, set[str]] = {}
|
||||||
fqme2stream: dict[str, MsgStream] = {}
|
fqme2ipc: dict[str, MsgStream] = {}
|
||||||
|
stream_ctxs: list[AsyncContextManager] = []
|
||||||
client = AnnotCtl(
|
client = AnnotCtl(
|
||||||
ctx2fqmes=ctx2fqmes,
|
ctx2fqmes=ctx2fqmes,
|
||||||
fqme2stream=fqme2stream,
|
fqme2ipc=fqme2ipc,
|
||||||
|
# _annot_stack=annots_stack,
|
||||||
)
|
)
|
||||||
|
|
||||||
stream_ctxs: list[AsyncContextManager] = []
|
async with (
|
||||||
async with trionics.gather_contexts(ctx_mngrs) as ctxs:
|
# AsyncExitStack() as annots_stack,
|
||||||
|
client._annot_stack, # as astack,
|
||||||
|
trionics.gather_contexts(ctx_mngrs) as ctxs,
|
||||||
|
):
|
||||||
for (ctx, fqmes) in ctxs:
|
for (ctx, fqmes) in ctxs:
|
||||||
stream_ctxs.append(ctx.open_stream())
|
stream_ctxs.append(ctx.open_stream())
|
||||||
|
|
||||||
# fill lookup table of mkt addrs to IPC ctxs
|
# fill lookup table of mkt addrs to IPC ctxs
|
||||||
for fqme in fqmes:
|
for fqme in fqmes:
|
||||||
if other := fqme2stream.get(fqme):
|
if other := fqme2ipc.get(fqme):
|
||||||
raise ValueError(
|
raise ValueError(
|
||||||
f'More then one chart displays {fqme}!?\n'
|
f'More then one chart displays {fqme}!?\n'
|
||||||
'Other UI actor info:\n'
|
'Other UI actor info:\n'
|
||||||
|
@ -266,7 +328,7 @@ async def open_annot_ctl(
|
||||||
for stream in streams:
|
for stream in streams:
|
||||||
fqmes: set[str] = ctx2fqmes[stream._ctx.cid]
|
fqmes: set[str] = ctx2fqmes[stream._ctx.cid]
|
||||||
for fqme in fqmes:
|
for fqme in fqmes:
|
||||||
fqme2stream[fqme] = stream
|
fqme2ipc[fqme] = stream
|
||||||
|
|
||||||
yield client
|
yield client
|
||||||
# TODO: on graceful teardown should we try to
|
# TODO: on graceful teardown should we try to
|
||||||
|
|
Loading…
Reference in New Issue