Compare commits
86 Commits
tpt_closed
...
main
| Author | SHA1 | Date |
|---|---|---|
|
|
9a720f8e21 | |
|
|
d17e6ab5d9 | |
|
|
cd0c780d04 | |
|
|
1417c49051 | |
|
|
044afb0f6e | |
|
|
c96ecdab75 | |
|
|
e1e59453a9 | |
|
|
d784af9df9 | |
|
|
cabd3fde92 | |
|
|
2d0005ce48 | |
|
|
d0add050b7 | |
|
|
709bc8a5be | |
|
|
c7979d0100 | |
|
|
9a97c477e2 | |
|
|
2516d97fe4 | |
|
|
5bfc9d46e1 | |
|
|
aa403bd390 | |
|
|
c1530c7a37 | |
|
|
50ffc1095b | |
|
|
437d87ab5f | |
|
|
0087cc8876 | |
|
|
034fa19372 | |
|
|
0f0bbd1cda | |
|
|
3b6484c340 | |
|
|
6d896eeed2 | |
|
|
bdedb16cdc | |
|
|
d8bfdd775c | |
|
|
73369fb1ef | |
|
|
8dd969e85f | |
|
|
90fce9fcd4 | |
|
|
a97f6c8dcf | |
|
|
a940018721 | |
|
|
964f207150 | |
|
|
fdb3999902 | |
|
|
d1991b3300 | |
|
|
b90edf95a7 | |
|
|
ce3d8e7a1e | |
|
|
f2b04c4071 | |
|
|
e75c3d8a34 | |
|
|
be4adfc202 | |
|
|
763faa0cc1 | |
|
|
0f1f2e263d | |
|
|
fd92cd99c2 | |
|
|
8c2fd7c780 | |
|
|
1016f54c98 | |
|
|
b4944916c9 | |
|
|
3f001cc1f6 | |
|
|
0845b257d9 | |
|
|
7964cc3cf4 | |
|
|
4e7e4a7a1b | |
|
|
fe11f79f21 | |
|
|
40cbc8546d | |
|
|
e3d7077f18 | |
|
|
7ddcf5893e | |
|
|
b1d6c595ec | |
|
|
33ec37a83f | |
|
|
e6c7834a01 | |
|
|
4ef5a5beb8 | |
|
|
11e95d9cbf | |
|
|
51ca9cd4d9 | |
|
|
27d077ade5 | |
|
|
9257af02b9 | |
|
|
582f9be02f | |
|
|
5f6e24f55c | |
|
|
bd418078ca | |
|
|
d5af471192 | |
|
|
6c28b1cbbc | |
|
|
8af8ac4f7b | |
|
|
f4d9090d6d | |
|
|
b0953ecbee | |
|
|
b5e4c83341 | |
|
|
0ef98ba25c | |
|
|
6b70fea5d4 | |
|
|
4e24cb1bff | |
|
|
3d83b61f3f | |
|
|
6f390dc88c | |
|
|
e1f3d7c3f8 | |
|
|
600636784c | |
|
|
0b63a73954 | |
|
|
8fb47f761a | |
|
|
ad37ebabb2 | |
|
|
5020266bd5 | |
|
|
d6a56d87bf | |
|
|
e8152b8534 | |
|
|
bb81c74353 | |
|
|
7eaf28479c |
|
|
@ -9,10 +9,7 @@
|
||||||
};
|
};
|
||||||
|
|
||||||
outputs =
|
outputs =
|
||||||
{
|
{ nixpkgs, ... }:
|
||||||
nixpkgs,
|
|
||||||
...
|
|
||||||
}:
|
|
||||||
let
|
let
|
||||||
inherit (nixpkgs) lib;
|
inherit (nixpkgs) lib;
|
||||||
forAllSystems = lib.genAttrs lib.systems.flakeExposed;
|
forAllSystems = lib.genAttrs lib.systems.flakeExposed;
|
||||||
|
|
@ -35,6 +32,7 @@
|
||||||
in
|
in
|
||||||
{
|
{
|
||||||
default = pkgs.mkShell {
|
default = pkgs.mkShell {
|
||||||
|
|
||||||
packages = with pkgs; [
|
packages = with pkgs; [
|
||||||
# XXX, ensure sh completions active!
|
# XXX, ensure sh completions active!
|
||||||
bashInteractive
|
bashInteractive
|
||||||
|
|
|
||||||
|
|
@ -272,14 +272,8 @@ async def maybe_spawn_brokerd(
|
||||||
'''
|
'''
|
||||||
from piker.service import maybe_spawn_daemon
|
from piker.service import maybe_spawn_daemon
|
||||||
|
|
||||||
# if (
|
|
||||||
# loglevel != 'info'
|
|
||||||
# ):
|
|
||||||
# await tractor.pause()
|
|
||||||
|
|
||||||
async with maybe_spawn_daemon(
|
async with maybe_spawn_daemon(
|
||||||
|
service_name=f'brokerd.{brokername}',
|
||||||
f'brokerd.{brokername}',
|
|
||||||
service_task_target=spawn_brokerd,
|
service_task_target=spawn_brokerd,
|
||||||
spawn_args={
|
spawn_args={
|
||||||
'brokername': brokername,
|
'brokername': brokername,
|
||||||
|
|
|
||||||
|
|
@ -819,11 +819,6 @@ async def maybe_open_feed(
|
||||||
'''
|
'''
|
||||||
fqme = fqmes[0]
|
fqme = fqmes[0]
|
||||||
|
|
||||||
# if (
|
|
||||||
# loglevel != 'info'
|
|
||||||
# ):
|
|
||||||
# await tractor.pause()
|
|
||||||
|
|
||||||
async with trionics.maybe_open_context(
|
async with trionics.maybe_open_context(
|
||||||
acm_func=open_feed,
|
acm_func=open_feed,
|
||||||
kwargs={
|
kwargs={
|
||||||
|
|
@ -890,10 +885,6 @@ async def open_feed(
|
||||||
providers.setdefault(mod, []).append(bs_fqme)
|
providers.setdefault(mod, []).append(bs_fqme)
|
||||||
feed.mods[mod.name] = mod
|
feed.mods[mod.name] = mod
|
||||||
|
|
||||||
if (
|
|
||||||
loglevel != 'info'
|
|
||||||
):
|
|
||||||
await tractor.pause()
|
|
||||||
# one actor per brokerd for now
|
# one actor per brokerd for now
|
||||||
brokerd_ctxs = []
|
brokerd_ctxs = []
|
||||||
for brokermod, bfqmes in providers.items():
|
for brokermod, bfqmes in providers.items():
|
||||||
|
|
|
||||||
|
|
@ -294,11 +294,6 @@ def ldshm(
|
||||||
f'Something is wrong with time period for {shm}:\n{times}'
|
f'Something is wrong with time period for {shm}:\n{times}'
|
||||||
)
|
)
|
||||||
period_s: float = float(max(d1, d2, med))
|
period_s: float = float(max(d1, d2, med))
|
||||||
log.info(
|
|
||||||
f'Processing shm buffer:\n'
|
|
||||||
f' file: {shmfile.name}\n'
|
|
||||||
f' period: {period_s}s\n'
|
|
||||||
)
|
|
||||||
|
|
||||||
null_segs: tuple = tsp.get_null_segs(
|
null_segs: tuple = tsp.get_null_segs(
|
||||||
frame=shm.array,
|
frame=shm.array,
|
||||||
|
|
|
||||||
|
|
@ -30,11 +30,6 @@ import tractor
|
||||||
|
|
||||||
from piker.data._formatters import BGM
|
from piker.data._formatters import BGM
|
||||||
from piker.storage import log
|
from piker.storage import log
|
||||||
from piker.toolz.profile import (
|
|
||||||
Profiler,
|
|
||||||
pg_profile_enabled,
|
|
||||||
ms_slower_then,
|
|
||||||
)
|
|
||||||
from piker.ui._style import get_fonts
|
from piker.ui._style import get_fonts
|
||||||
|
|
||||||
if TYPE_CHECKING:
|
if TYPE_CHECKING:
|
||||||
|
|
@ -97,22 +92,12 @@ async def markup_gaps(
|
||||||
# gap's duration.
|
# gap's duration.
|
||||||
show_txt: bool = False,
|
show_txt: bool = False,
|
||||||
|
|
||||||
# A/B comparison: render individual arrows alongside batch
|
|
||||||
# for visual comparison
|
|
||||||
show_individual_arrows: bool = False,
|
|
||||||
|
|
||||||
) -> dict[int, dict]:
|
) -> dict[int, dict]:
|
||||||
'''
|
'''
|
||||||
Remote annotate time-gaps in a dt-fielded ts (normally OHLC)
|
Remote annotate time-gaps in a dt-fielded ts (normally OHLC)
|
||||||
with rectangles.
|
with rectangles.
|
||||||
|
|
||||||
'''
|
'''
|
||||||
profiler = Profiler(
|
|
||||||
msg=f'markup_gaps() for {gaps.height} gaps',
|
|
||||||
disabled=False,
|
|
||||||
ms_threshold=0.0,
|
|
||||||
)
|
|
||||||
|
|
||||||
# XXX: force chart redraw FIRST to ensure PlotItem coordinate
|
# XXX: force chart redraw FIRST to ensure PlotItem coordinate
|
||||||
# system is properly initialized before we position annotations!
|
# system is properly initialized before we position annotations!
|
||||||
# Without this, annotations may be misaligned on first creation
|
# Without this, annotations may be misaligned on first creation
|
||||||
|
|
@ -121,19 +106,6 @@ async def markup_gaps(
|
||||||
fqme=fqme,
|
fqme=fqme,
|
||||||
timeframe=timeframe,
|
timeframe=timeframe,
|
||||||
)
|
)
|
||||||
profiler('first `.redraw()` before annot creation')
|
|
||||||
|
|
||||||
log.info(
|
|
||||||
f'markup_gaps() called:\n'
|
|
||||||
f' fqme: {fqme}\n'
|
|
||||||
f' timeframe: {timeframe}s\n'
|
|
||||||
f' gaps.height: {gaps.height}\n'
|
|
||||||
)
|
|
||||||
|
|
||||||
# collect all annotation specs for batch submission
|
|
||||||
rect_specs: list[dict] = []
|
|
||||||
arrow_specs: list[dict] = []
|
|
||||||
text_specs: list[dict] = []
|
|
||||||
|
|
||||||
aids: dict[int] = {}
|
aids: dict[int] = {}
|
||||||
for i in range(gaps.height):
|
for i in range(gaps.height):
|
||||||
|
|
@ -245,38 +217,56 @@ async def markup_gaps(
|
||||||
# 1: 'wine', # down-gap
|
# 1: 'wine', # down-gap
|
||||||
# }[sgn]
|
# }[sgn]
|
||||||
|
|
||||||
# collect rect spec (no fqme/timeframe, added by batch
|
rect_kwargs: dict[str, Any] = dict(
|
||||||
# API)
|
fqme=fqme,
|
||||||
rect_spec: dict[str, Any] = dict(
|
timeframe=timeframe,
|
||||||
meth='set_view_pos',
|
|
||||||
start_pos=lc,
|
start_pos=lc,
|
||||||
end_pos=ro,
|
end_pos=ro,
|
||||||
color=color,
|
color=color,
|
||||||
update_label=False,
|
|
||||||
start_time=start_time,
|
start_time=start_time,
|
||||||
end_time=end_time,
|
end_time=end_time,
|
||||||
)
|
)
|
||||||
rect_specs.append(rect_spec)
|
|
||||||
|
|
||||||
|
# add up/down rects
|
||||||
|
aid: int|None = await actl.add_rect(**rect_kwargs)
|
||||||
|
if aid is None:
|
||||||
|
log.error(
|
||||||
|
f'Failed to add rect for,\n'
|
||||||
|
f'{rect_kwargs!r}\n'
|
||||||
|
f'\n'
|
||||||
|
f'Skipping to next gap!\n'
|
||||||
|
)
|
||||||
|
continue
|
||||||
|
|
||||||
|
assert aid
|
||||||
|
aids[aid] = rect_kwargs
|
||||||
direction: str = (
|
direction: str = (
|
||||||
'down' if down_gap
|
'down' if down_gap
|
||||||
else 'up'
|
else 'up'
|
||||||
)
|
)
|
||||||
|
# TODO! mk this a `msgspec.Struct` which we deserialize
|
||||||
# collect arrow spec
|
# on the server side!
|
||||||
|
# XXX: send timestamp for server-side index lookup
|
||||||
|
# to ensure alignment with current shm state
|
||||||
gap_time: float = row['time'][0]
|
gap_time: float = row['time'][0]
|
||||||
arrow_spec: dict[str, Any] = dict(
|
arrow_kwargs: dict[str, Any] = dict(
|
||||||
|
fqme=fqme,
|
||||||
|
timeframe=timeframe,
|
||||||
x=iend, # fallback if timestamp lookup fails
|
x=iend, # fallback if timestamp lookup fails
|
||||||
y=cls,
|
y=cls,
|
||||||
time=gap_time, # for server-side index lookup
|
time=gap_time, # for server-side index lookup
|
||||||
color=color,
|
color=color,
|
||||||
alpha=169,
|
alpha=169,
|
||||||
pointing=direction,
|
pointing=direction,
|
||||||
|
# TODO: expose these as params to markup_gaps()?
|
||||||
headLen=10,
|
headLen=10,
|
||||||
headWidth=2.222,
|
headWidth=2.222,
|
||||||
pxMode=True,
|
pxMode=True,
|
||||||
)
|
)
|
||||||
arrow_specs.append(arrow_spec)
|
|
||||||
|
aid: int = await actl.add_arrow(
|
||||||
|
**arrow_kwargs
|
||||||
|
)
|
||||||
|
|
||||||
# add duration label to RHS of arrow
|
# add duration label to RHS of arrow
|
||||||
if up_gap:
|
if up_gap:
|
||||||
|
|
@ -288,12 +278,15 @@ async def markup_gaps(
|
||||||
assert flat
|
assert flat
|
||||||
anchor = (0, 0) # up from bottom
|
anchor = (0, 0) # up from bottom
|
||||||
|
|
||||||
# collect text spec if enabled
|
# use a slightly smaller font for gap label txt.
|
||||||
if show_txt:
|
font, small_font = get_fonts()
|
||||||
font, small_font = get_fonts()
|
font_size: int = small_font.px_size - 1
|
||||||
font_size: int = small_font.px_size - 1
|
assert isinstance(font_size, int)
|
||||||
|
|
||||||
text_spec: dict[str, Any] = dict(
|
if show_txt:
|
||||||
|
text_aid: int = await actl.add_text(
|
||||||
|
fqme=fqme,
|
||||||
|
timeframe=timeframe,
|
||||||
text=gap_label,
|
text=gap_label,
|
||||||
x=iend + 1, # fallback if timestamp lookup fails
|
x=iend + 1, # fallback if timestamp lookup fails
|
||||||
y=cls,
|
y=cls,
|
||||||
|
|
@ -302,46 +295,12 @@ async def markup_gaps(
|
||||||
anchor=anchor,
|
anchor=anchor,
|
||||||
font_size=font_size,
|
font_size=font_size,
|
||||||
)
|
)
|
||||||
text_specs.append(text_spec)
|
aids[text_aid] = {'text': gap_label}
|
||||||
|
|
||||||
# submit all annotations in single batch IPC msg
|
# tell chart to redraw all its
|
||||||
log.info(
|
# graphics view layers Bo
|
||||||
f'Submitting batch annotations:\n'
|
|
||||||
f' rects: {len(rect_specs)}\n'
|
|
||||||
f' arrows: {len(arrow_specs)}\n'
|
|
||||||
f' texts: {len(text_specs)}\n'
|
|
||||||
)
|
|
||||||
profiler('built all annotation specs')
|
|
||||||
|
|
||||||
result: dict[str, list[int]] = await actl.add_batch(
|
|
||||||
fqme=fqme,
|
|
||||||
timeframe=timeframe,
|
|
||||||
rects=rect_specs,
|
|
||||||
arrows=arrow_specs,
|
|
||||||
texts=text_specs,
|
|
||||||
show_individual_arrows=show_individual_arrows,
|
|
||||||
)
|
|
||||||
profiler('batch `.add_batch()` IPC call complete')
|
|
||||||
|
|
||||||
# build aids dict from batch results
|
|
||||||
for aid in result['rects']:
|
|
||||||
aids[aid] = {'type': 'rect'}
|
|
||||||
for aid in result['arrows']:
|
|
||||||
aids[aid] = {'type': 'arrow'}
|
|
||||||
for aid in result['texts']:
|
|
||||||
aids[aid] = {'type': 'text'}
|
|
||||||
|
|
||||||
log.info(
|
|
||||||
f'Batch submission complete: {len(aids)} annotation(s) '
|
|
||||||
f'created'
|
|
||||||
)
|
|
||||||
profiler('built aids result dict')
|
|
||||||
|
|
||||||
# tell chart to redraw all its graphics view layers
|
|
||||||
await actl.redraw(
|
await actl.redraw(
|
||||||
fqme=fqme,
|
fqme=fqme,
|
||||||
timeframe=timeframe,
|
timeframe=timeframe,
|
||||||
)
|
)
|
||||||
profiler('final `.redraw()` after annot creation')
|
|
||||||
|
|
||||||
return aids
|
return aids
|
||||||
|
|
|
||||||
|
|
@ -24,11 +24,8 @@ from pyqtgraph import (
|
||||||
Point,
|
Point,
|
||||||
functions as fn,
|
functions as fn,
|
||||||
Color,
|
Color,
|
||||||
GraphicsObject,
|
|
||||||
)
|
)
|
||||||
from pyqtgraph.Qt import internals
|
|
||||||
import numpy as np
|
import numpy as np
|
||||||
import pyqtgraph as pg
|
|
||||||
|
|
||||||
from piker.ui.qt import (
|
from piker.ui.qt import (
|
||||||
QtCore,
|
QtCore,
|
||||||
|
|
@ -38,10 +35,6 @@ from piker.ui.qt import (
|
||||||
QRectF,
|
QRectF,
|
||||||
QGraphicsPathItem,
|
QGraphicsPathItem,
|
||||||
)
|
)
|
||||||
from piker.ui._style import hcolor
|
|
||||||
from piker.log import get_logger
|
|
||||||
|
|
||||||
log = get_logger(__name__)
|
|
||||||
|
|
||||||
|
|
||||||
def mk_marker_path(
|
def mk_marker_path(
|
||||||
|
|
@ -111,7 +104,7 @@ def mk_marker_path(
|
||||||
|
|
||||||
class LevelMarker(QGraphicsPathItem):
|
class LevelMarker(QGraphicsPathItem):
|
||||||
'''
|
'''
|
||||||
An arrow marker path graphic which redraws itself
|
An arrow marker path graphich which redraws itself
|
||||||
to the specified view coordinate level on each paint cycle.
|
to the specified view coordinate level on each paint cycle.
|
||||||
|
|
||||||
'''
|
'''
|
||||||
|
|
@ -258,9 +251,9 @@ def qgo_draw_markers(
|
||||||
|
|
||||||
) -> float:
|
) -> float:
|
||||||
'''
|
'''
|
||||||
Paint markers in ``pg.GraphicsItem`` style by first removing the
|
Paint markers in ``pg.GraphicsItem`` style by first
|
||||||
view transform for the painter, drawing the markers in scene
|
removing the view transform for the painter, drawing the markers
|
||||||
coords, then restoring the view coords.
|
in scene coords, then restoring the view coords.
|
||||||
|
|
||||||
'''
|
'''
|
||||||
# paint markers in native coordinate system
|
# paint markers in native coordinate system
|
||||||
|
|
@ -302,449 +295,3 @@ def qgo_draw_markers(
|
||||||
|
|
||||||
p.setTransform(orig_tr)
|
p.setTransform(orig_tr)
|
||||||
return max(sizes)
|
return max(sizes)
|
||||||
|
|
||||||
|
|
||||||
class GapAnnotations(GraphicsObject):
|
|
||||||
'''
|
|
||||||
Batch-rendered gap annotations using Qt's efficient drawing
|
|
||||||
APIs.
|
|
||||||
|
|
||||||
Instead of creating individual `QGraphicsItem` instances per
|
|
||||||
gap (which is very slow for 1000+ gaps), this class stores all
|
|
||||||
gap rectangles and arrows in numpy-backed arrays and renders
|
|
||||||
them in single batch paint calls.
|
|
||||||
|
|
||||||
Performance: ~1000x faster than individual items for large gap
|
|
||||||
counts.
|
|
||||||
|
|
||||||
Based on patterns from:
|
|
||||||
- `pyqtgraph.BarGraphItem` (batch rect rendering)
|
|
||||||
- `pyqtgraph.ScatterPlotItem` (fragment rendering)
|
|
||||||
- `piker.ui._curve.FlowGraphic` (single path pattern)
|
|
||||||
|
|
||||||
'''
|
|
||||||
def __init__(
|
|
||||||
self,
|
|
||||||
gap_specs: list[dict],
|
|
||||||
array: np.ndarray|None = None,
|
|
||||||
color: str = 'dad_blue',
|
|
||||||
alpha: int = 169,
|
|
||||||
arrow_size: float = 10.0,
|
|
||||||
fqme: str|None = None,
|
|
||||||
timeframe: float|None = None,
|
|
||||||
) -> None:
|
|
||||||
'''
|
|
||||||
gap_specs: list of dicts with keys:
|
|
||||||
- start_pos: (x, y) tuple for left corner of rect
|
|
||||||
- end_pos: (x, y) tuple for right corner of rect
|
|
||||||
- arrow_x: x position for arrow
|
|
||||||
- arrow_y: y position for arrow
|
|
||||||
- pointing: 'up' or 'down' for arrow direction
|
|
||||||
- start_time: (optional) timestamp for repositioning
|
|
||||||
- end_time: (optional) timestamp for repositioning
|
|
||||||
|
|
||||||
array: optional OHLC numpy array for repositioning on
|
|
||||||
backfill updates (when abs-index changes)
|
|
||||||
|
|
||||||
fqme: symbol name for these gaps (for logging/debugging)
|
|
||||||
|
|
||||||
timeframe: period in seconds that these gaps were
|
|
||||||
detected on (used to skip reposition when
|
|
||||||
called with wrong timeframe's array)
|
|
||||||
|
|
||||||
'''
|
|
||||||
super().__init__()
|
|
||||||
self._gap_specs = gap_specs
|
|
||||||
self._array = array
|
|
||||||
self._fqme = fqme
|
|
||||||
self._timeframe = timeframe
|
|
||||||
n_gaps = len(gap_specs)
|
|
||||||
|
|
||||||
# shared pen/brush matching original SelectRect/ArrowItem style
|
|
||||||
base_color = pg.mkColor(hcolor(color))
|
|
||||||
|
|
||||||
# rect pen: base color, fully opaque for outline
|
|
||||||
self._rect_pen = pg.mkPen(base_color, width=1)
|
|
||||||
|
|
||||||
# rect brush: base color with alpha=66 (SelectRect default)
|
|
||||||
rect_fill = pg.mkColor(hcolor(color))
|
|
||||||
rect_fill.setAlpha(66)
|
|
||||||
self._rect_brush = pg.functions.mkBrush(rect_fill)
|
|
||||||
|
|
||||||
# arrow pen: same as rects
|
|
||||||
self._arrow_pen = pg.mkPen(base_color, width=1)
|
|
||||||
|
|
||||||
# arrow brush: base color with user-specified alpha (default 169)
|
|
||||||
arrow_fill = pg.mkColor(hcolor(color))
|
|
||||||
arrow_fill.setAlpha(alpha)
|
|
||||||
self._arrow_brush = pg.functions.mkBrush(arrow_fill)
|
|
||||||
|
|
||||||
# allocate rect array using Qt's efficient storage
|
|
||||||
self._rectarray = internals.PrimitiveArray(
|
|
||||||
QtCore.QRectF,
|
|
||||||
4,
|
|
||||||
)
|
|
||||||
self._rectarray.resize(n_gaps)
|
|
||||||
rect_memory = self._rectarray.ndarray()
|
|
||||||
|
|
||||||
# fill rect array from gap specs
|
|
||||||
for (
|
|
||||||
i,
|
|
||||||
spec,
|
|
||||||
) in enumerate(gap_specs):
|
|
||||||
(
|
|
||||||
start_x,
|
|
||||||
start_y,
|
|
||||||
) = spec['start_pos']
|
|
||||||
(
|
|
||||||
end_x,
|
|
||||||
end_y,
|
|
||||||
) = spec['end_pos']
|
|
||||||
|
|
||||||
# QRectF expects (x, y, width, height)
|
|
||||||
rect_memory[i, 0] = start_x
|
|
||||||
rect_memory[i, 1] = min(start_y, end_y)
|
|
||||||
rect_memory[i, 2] = end_x - start_x
|
|
||||||
rect_memory[i, 3] = abs(end_y - start_y)
|
|
||||||
|
|
||||||
# build single QPainterPath for all arrows
|
|
||||||
self._arrow_path = QtGui.QPainterPath()
|
|
||||||
self._arrow_size = arrow_size
|
|
||||||
|
|
||||||
for spec in gap_specs:
|
|
||||||
arrow_x = spec['arrow_x']
|
|
||||||
arrow_y = spec['arrow_y']
|
|
||||||
pointing = spec['pointing']
|
|
||||||
|
|
||||||
# create arrow polygon
|
|
||||||
if pointing == 'down':
|
|
||||||
# arrow points downward
|
|
||||||
arrow_poly = QtGui.QPolygonF([
|
|
||||||
QPointF(arrow_x, arrow_y), # tip
|
|
||||||
QPointF(
|
|
||||||
arrow_x - arrow_size/2,
|
|
||||||
arrow_y - arrow_size,
|
|
||||||
), # left
|
|
||||||
QPointF(
|
|
||||||
arrow_x + arrow_size/2,
|
|
||||||
arrow_y - arrow_size,
|
|
||||||
), # right
|
|
||||||
])
|
|
||||||
else: # up
|
|
||||||
# arrow points upward
|
|
||||||
arrow_poly = QtGui.QPolygonF([
|
|
||||||
QPointF(arrow_x, arrow_y), # tip
|
|
||||||
QPointF(
|
|
||||||
arrow_x - arrow_size/2,
|
|
||||||
arrow_y + arrow_size,
|
|
||||||
), # left
|
|
||||||
QPointF(
|
|
||||||
arrow_x + arrow_size/2,
|
|
||||||
arrow_y + arrow_size,
|
|
||||||
), # right
|
|
||||||
])
|
|
||||||
|
|
||||||
self._arrow_path.addPolygon(arrow_poly)
|
|
||||||
self._arrow_path.closeSubpath()
|
|
||||||
|
|
||||||
# cache bounding rect
|
|
||||||
self._br: QRectF|None = None
|
|
||||||
|
|
||||||
def boundingRect(self) -> QRectF:
|
|
||||||
'''
|
|
||||||
Compute bounding rect from rect array and arrow path.
|
|
||||||
|
|
||||||
'''
|
|
||||||
if self._br is not None:
|
|
||||||
return self._br
|
|
||||||
|
|
||||||
# get rect bounds
|
|
||||||
rect_memory = self._rectarray.ndarray()
|
|
||||||
if len(rect_memory) == 0:
|
|
||||||
self._br = QRectF()
|
|
||||||
return self._br
|
|
||||||
|
|
||||||
x_min = rect_memory[:, 0].min()
|
|
||||||
y_min = rect_memory[:, 1].min()
|
|
||||||
x_max = (rect_memory[:, 0] + rect_memory[:, 2]).max()
|
|
||||||
y_max = (rect_memory[:, 1] + rect_memory[:, 3]).max()
|
|
||||||
|
|
||||||
# expand for arrow path
|
|
||||||
arrow_br = self._arrow_path.boundingRect()
|
|
||||||
x_min = min(x_min, arrow_br.left())
|
|
||||||
y_min = min(y_min, arrow_br.top())
|
|
||||||
x_max = max(x_max, arrow_br.right())
|
|
||||||
y_max = max(y_max, arrow_br.bottom())
|
|
||||||
|
|
||||||
self._br = QRectF(
|
|
||||||
x_min,
|
|
||||||
y_min,
|
|
||||||
x_max - x_min,
|
|
||||||
y_max - y_min,
|
|
||||||
)
|
|
||||||
return self._br
|
|
||||||
|
|
||||||
def paint(
|
|
||||||
self,
|
|
||||||
p: QtGui.QPainter,
|
|
||||||
opt: QtWidgets.QStyleOptionGraphicsItem,
|
|
||||||
w: QtWidgets.QWidget,
|
|
||||||
) -> None:
|
|
||||||
'''
|
|
||||||
Batch render all rects and arrows in minimal paint calls.
|
|
||||||
|
|
||||||
'''
|
|
||||||
# draw all rects in single batch call (data coordinates)
|
|
||||||
p.setPen(self._rect_pen)
|
|
||||||
p.setBrush(self._rect_brush)
|
|
||||||
drawargs = self._rectarray.drawargs()
|
|
||||||
p.drawRects(*drawargs)
|
|
||||||
|
|
||||||
# draw arrows in scene/pixel coordinates so they maintain
|
|
||||||
# size regardless of zoom level
|
|
||||||
orig_tr = p.transform()
|
|
||||||
p.resetTransform()
|
|
||||||
|
|
||||||
# rebuild arrow path in scene coordinates
|
|
||||||
arrow_path_scene = QtGui.QPainterPath()
|
|
||||||
|
|
||||||
# arrow geometry matching pg.ArrowItem defaults
|
|
||||||
# headLen=10, headWidth=2.222
|
|
||||||
# headWidth is the half-width (center to edge distance)
|
|
||||||
head_len = self._arrow_size
|
|
||||||
head_width = head_len * 0.2222 # 2.222 at size=10
|
|
||||||
|
|
||||||
for spec in self._gap_specs:
|
|
||||||
if 'arrow_x' not in spec:
|
|
||||||
continue
|
|
||||||
|
|
||||||
arrow_x = spec['arrow_x']
|
|
||||||
arrow_y = spec['arrow_y']
|
|
||||||
pointing = spec['pointing']
|
|
||||||
|
|
||||||
# transform data coords to scene coords
|
|
||||||
scene_pt = orig_tr.map(QPointF(arrow_x, arrow_y))
|
|
||||||
sx = scene_pt.x()
|
|
||||||
sy = scene_pt.y()
|
|
||||||
|
|
||||||
# create arrow polygon in scene/pixel coords
|
|
||||||
# matching pg.ArrowItem geometry but rotated for up/down
|
|
||||||
if pointing == 'down':
|
|
||||||
# tip points downward (negative y direction)
|
|
||||||
arrow_poly = QtGui.QPolygonF([
|
|
||||||
QPointF(sx, sy), # tip
|
|
||||||
QPointF(
|
|
||||||
sx - head_width,
|
|
||||||
sy - head_len,
|
|
||||||
), # left base
|
|
||||||
QPointF(
|
|
||||||
sx + head_width,
|
|
||||||
sy - head_len,
|
|
||||||
), # right base
|
|
||||||
])
|
|
||||||
else: # up
|
|
||||||
# tip points upward (positive y direction)
|
|
||||||
arrow_poly = QtGui.QPolygonF([
|
|
||||||
QPointF(sx, sy), # tip
|
|
||||||
QPointF(
|
|
||||||
sx - head_width,
|
|
||||||
sy + head_len,
|
|
||||||
), # left base
|
|
||||||
QPointF(
|
|
||||||
sx + head_width,
|
|
||||||
sy + head_len,
|
|
||||||
), # right base
|
|
||||||
])
|
|
||||||
|
|
||||||
arrow_path_scene.addPolygon(arrow_poly)
|
|
||||||
arrow_path_scene.closeSubpath()
|
|
||||||
|
|
||||||
p.setPen(self._arrow_pen)
|
|
||||||
p.setBrush(self._arrow_brush)
|
|
||||||
p.drawPath(arrow_path_scene)
|
|
||||||
|
|
||||||
# restore original transform
|
|
||||||
p.setTransform(orig_tr)
|
|
||||||
|
|
||||||
def reposition(
|
|
||||||
self,
|
|
||||||
array: np.ndarray|None = None,
|
|
||||||
fqme: str|None = None,
|
|
||||||
timeframe: float|None = None,
|
|
||||||
) -> None:
|
|
||||||
'''
|
|
||||||
Reposition all annotations based on timestamps.
|
|
||||||
|
|
||||||
Used when viz is updated (eg during backfill) and abs-index
|
|
||||||
range changes - we need to lookup new indices from timestamps.
|
|
||||||
|
|
||||||
'''
|
|
||||||
# skip reposition if timeframe doesn't match
|
|
||||||
# (e.g., 1s gaps being repositioned with 60s array)
|
|
||||||
if (
|
|
||||||
timeframe is not None
|
|
||||||
and
|
|
||||||
self._timeframe is not None
|
|
||||||
and
|
|
||||||
timeframe != self._timeframe
|
|
||||||
):
|
|
||||||
log.debug(
|
|
||||||
f'Skipping reposition for {self._fqme} gaps:\n'
|
|
||||||
f' gap timeframe: {self._timeframe}s\n'
|
|
||||||
f' array timeframe: {timeframe}s\n'
|
|
||||||
)
|
|
||||||
return
|
|
||||||
|
|
||||||
if array is None:
|
|
||||||
array = self._array
|
|
||||||
|
|
||||||
if array is None:
|
|
||||||
log.warning(
|
|
||||||
'GapAnnotations.reposition() called but no array '
|
|
||||||
'provided'
|
|
||||||
)
|
|
||||||
return
|
|
||||||
|
|
||||||
# collect all unique timestamps we need to lookup
|
|
||||||
timestamps: set[float] = set()
|
|
||||||
for spec in self._gap_specs:
|
|
||||||
if spec.get('start_time') is not None:
|
|
||||||
timestamps.add(spec['start_time'])
|
|
||||||
if spec.get('end_time') is not None:
|
|
||||||
timestamps.add(spec['end_time'])
|
|
||||||
if spec.get('time') is not None:
|
|
||||||
timestamps.add(spec['time'])
|
|
||||||
|
|
||||||
# vectorized timestamp -> row lookup using binary search
|
|
||||||
time_to_row: dict[float, dict] = {}
|
|
||||||
if timestamps:
|
|
||||||
import numpy as np
|
|
||||||
time_arr = array['time']
|
|
||||||
ts_array = np.array(list(timestamps))
|
|
||||||
|
|
||||||
search_indices = np.searchsorted(
|
|
||||||
time_arr,
|
|
||||||
ts_array,
|
|
||||||
)
|
|
||||||
|
|
||||||
# vectorized bounds check and exact match verification
|
|
||||||
valid_mask = (
|
|
||||||
(search_indices < len(array))
|
|
||||||
& (time_arr[search_indices] == ts_array)
|
|
||||||
)
|
|
||||||
|
|
||||||
valid_indices = search_indices[valid_mask]
|
|
||||||
valid_timestamps = ts_array[valid_mask]
|
|
||||||
matched_rows = array[valid_indices]
|
|
||||||
|
|
||||||
time_to_row = {
|
|
||||||
float(ts): {
|
|
||||||
'index': float(row['index']),
|
|
||||||
'open': float(row['open']),
|
|
||||||
'close': float(row['close']),
|
|
||||||
}
|
|
||||||
for ts, row in zip(
|
|
||||||
valid_timestamps,
|
|
||||||
matched_rows,
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
# rebuild rect array from gap specs with new indices
|
|
||||||
rect_memory = self._rectarray.ndarray()
|
|
||||||
|
|
||||||
for (
|
|
||||||
i,
|
|
||||||
spec,
|
|
||||||
) in enumerate(self._gap_specs):
|
|
||||||
start_time = spec.get('start_time')
|
|
||||||
end_time = spec.get('end_time')
|
|
||||||
|
|
||||||
if (
|
|
||||||
start_time is None
|
|
||||||
or end_time is None
|
|
||||||
):
|
|
||||||
continue
|
|
||||||
|
|
||||||
start_row = time_to_row.get(start_time)
|
|
||||||
end_row = time_to_row.get(end_time)
|
|
||||||
|
|
||||||
if (
|
|
||||||
start_row is None
|
|
||||||
or end_row is None
|
|
||||||
):
|
|
||||||
log.warning(
|
|
||||||
f'Timestamp lookup failed for gap[{i}] during '
|
|
||||||
f'reposition:\n'
|
|
||||||
f' fqme: {fqme}\n'
|
|
||||||
f' timeframe: {timeframe}s\n'
|
|
||||||
f' start_time: {start_time}\n'
|
|
||||||
f' end_time: {end_time}\n'
|
|
||||||
f' array time range: '
|
|
||||||
f'{array["time"][0]} -> {array["time"][-1]}\n'
|
|
||||||
)
|
|
||||||
continue
|
|
||||||
|
|
||||||
start_idx = start_row['index']
|
|
||||||
end_idx = end_row['index']
|
|
||||||
start_close = start_row['close']
|
|
||||||
end_open = end_row['open']
|
|
||||||
|
|
||||||
from_idx: float = 0.16 - 0.06
|
|
||||||
start_x = start_idx + 1 - from_idx
|
|
||||||
end_x = end_idx + from_idx
|
|
||||||
|
|
||||||
# update rect in array
|
|
||||||
rect_memory[i, 0] = start_x
|
|
||||||
rect_memory[i, 1] = min(start_close, end_open)
|
|
||||||
rect_memory[i, 2] = end_x - start_x
|
|
||||||
rect_memory[i, 3] = abs(end_open - start_close)
|
|
||||||
|
|
||||||
# rebuild arrow path with new indices
|
|
||||||
self._arrow_path.clear()
|
|
||||||
|
|
||||||
for spec in self._gap_specs:
|
|
||||||
time_val = spec.get('time')
|
|
||||||
if time_val is None:
|
|
||||||
continue
|
|
||||||
|
|
||||||
arrow_row = time_to_row.get(time_val)
|
|
||||||
if arrow_row is None:
|
|
||||||
continue
|
|
||||||
|
|
||||||
arrow_x = arrow_row['index']
|
|
||||||
arrow_y = arrow_row['close']
|
|
||||||
pointing = spec['pointing']
|
|
||||||
|
|
||||||
# create arrow polygon
|
|
||||||
if pointing == 'down':
|
|
||||||
arrow_poly = QtGui.QPolygonF([
|
|
||||||
QPointF(arrow_x, arrow_y),
|
|
||||||
QPointF(
|
|
||||||
arrow_x - self._arrow_size/2,
|
|
||||||
arrow_y - self._arrow_size,
|
|
||||||
),
|
|
||||||
QPointF(
|
|
||||||
arrow_x + self._arrow_size/2,
|
|
||||||
arrow_y - self._arrow_size,
|
|
||||||
),
|
|
||||||
])
|
|
||||||
else: # up
|
|
||||||
arrow_poly = QtGui.QPolygonF([
|
|
||||||
QPointF(arrow_x, arrow_y),
|
|
||||||
QPointF(
|
|
||||||
arrow_x - self._arrow_size/2,
|
|
||||||
arrow_y + self._arrow_size,
|
|
||||||
),
|
|
||||||
QPointF(
|
|
||||||
arrow_x + self._arrow_size/2,
|
|
||||||
arrow_y + self._arrow_size,
|
|
||||||
),
|
|
||||||
])
|
|
||||||
|
|
||||||
self._arrow_path.addPolygon(arrow_poly)
|
|
||||||
self._arrow_path.closeSubpath()
|
|
||||||
|
|
||||||
# invalidate bounding rect cache
|
|
||||||
self._br = None
|
|
||||||
self.prepareGeometryChange()
|
|
||||||
self.update()
|
|
||||||
|
|
|
||||||
|
|
@ -168,7 +168,7 @@ class ArrowEditor(Struct):
|
||||||
'''
|
'''
|
||||||
uid: str = arrow._uid
|
uid: str = arrow._uid
|
||||||
arrows: list[pg.ArrowItem] = self._arrows[uid]
|
arrows: list[pg.ArrowItem] = self._arrows[uid]
|
||||||
log.debug(
|
log.info(
|
||||||
f'Removing arrow from views\n'
|
f'Removing arrow from views\n'
|
||||||
f'uid: {uid!r}\n'
|
f'uid: {uid!r}\n'
|
||||||
f'{arrow!r}\n'
|
f'{arrow!r}\n'
|
||||||
|
|
@ -286,9 +286,7 @@ class LineEditor(Struct):
|
||||||
for line in lines:
|
for line in lines:
|
||||||
line.show_labels()
|
line.show_labels()
|
||||||
line.hide_markers()
|
line.hide_markers()
|
||||||
log.debug(
|
log.debug(f'Level active for level: {line.value()}')
|
||||||
f'Line active @ level: {line.value()!r}'
|
|
||||||
)
|
|
||||||
# TODO: other flashy things to indicate the order is active
|
# TODO: other flashy things to indicate the order is active
|
||||||
|
|
||||||
return lines
|
return lines
|
||||||
|
|
@ -331,11 +329,7 @@ class LineEditor(Struct):
|
||||||
if line in hovered:
|
if line in hovered:
|
||||||
hovered.remove(line)
|
hovered.remove(line)
|
||||||
|
|
||||||
log.debug(
|
log.debug(f'deleting {line} with oid: {uuid}')
|
||||||
f'Deleting level-line\n'
|
|
||||||
f'line: {line!r}\n'
|
|
||||||
f'oid: {uuid!r}\n'
|
|
||||||
)
|
|
||||||
line.delete()
|
line.delete()
|
||||||
|
|
||||||
# make sure the xhair doesn't get left off
|
# make sure the xhair doesn't get left off
|
||||||
|
|
@ -343,11 +337,7 @@ class LineEditor(Struct):
|
||||||
cursor.show_xhair()
|
cursor.show_xhair()
|
||||||
|
|
||||||
else:
|
else:
|
||||||
log.warning(
|
log.warning(f'Could not find line for {line}')
|
||||||
f'Could not find line for removal ??\n'
|
|
||||||
f'\n'
|
|
||||||
f'{line!r}\n'
|
|
||||||
)
|
|
||||||
|
|
||||||
return lines
|
return lines
|
||||||
|
|
||||||
|
|
@ -579,11 +569,11 @@ class SelectRect(QtWidgets.QGraphicsRectItem):
|
||||||
if update_label:
|
if update_label:
|
||||||
self.init_label(view_rect)
|
self.init_label(view_rect)
|
||||||
|
|
||||||
log.debug(
|
print(
|
||||||
f'SelectRect modify,\n'
|
'SelectRect modify:\n'
|
||||||
f'QRectF: {view_rect}\n'
|
f'QRectF: {view_rect}\n'
|
||||||
f'start_pos: {start_pos!r}\n'
|
f'start_pos: {start_pos}\n'
|
||||||
f'end_pos: {end_pos!r}\n'
|
f'end_pos: {end_pos}\n'
|
||||||
)
|
)
|
||||||
self.show()
|
self.show()
|
||||||
|
|
||||||
|
|
@ -650,11 +640,8 @@ class SelectRect(QtWidgets.QGraphicsRectItem):
|
||||||
dmn=dmn,
|
dmn=dmn,
|
||||||
))
|
))
|
||||||
|
|
||||||
# tracing
|
# print(f'x2, y2: {(x2, y2)}')
|
||||||
# log.info(
|
# print(f'xmn, ymn: {(xmn, ymx)}')
|
||||||
# f'x2, y2: {(x2, y2)}\n'
|
|
||||||
# f'xmn, ymn: {(xmn, ymx)}\n'
|
|
||||||
# )
|
|
||||||
|
|
||||||
label_anchor = Point(
|
label_anchor = Point(
|
||||||
xmx + 2,
|
xmx + 2,
|
||||||
|
|
|
||||||
|
|
@ -38,6 +38,7 @@ from piker.ui.qt import (
|
||||||
QtGui,
|
QtGui,
|
||||||
QGraphicsPathItem,
|
QGraphicsPathItem,
|
||||||
QStyleOptionGraphicsItem,
|
QStyleOptionGraphicsItem,
|
||||||
|
QGraphicsItem,
|
||||||
QGraphicsScene,
|
QGraphicsScene,
|
||||||
QWidget,
|
QWidget,
|
||||||
QPointF,
|
QPointF,
|
||||||
|
|
|
||||||
|
|
@ -22,7 +22,6 @@ a chart from some other actor.
|
||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
from contextlib import (
|
from contextlib import (
|
||||||
asynccontextmanager as acm,
|
asynccontextmanager as acm,
|
||||||
contextmanager as cm,
|
|
||||||
AsyncExitStack,
|
AsyncExitStack,
|
||||||
)
|
)
|
||||||
from functools import partial
|
from functools import partial
|
||||||
|
|
@ -47,7 +46,6 @@ from piker.log import get_logger
|
||||||
from piker.types import Struct
|
from piker.types import Struct
|
||||||
from piker.service import find_service
|
from piker.service import find_service
|
||||||
from piker.brokers import SymbolNotFound
|
from piker.brokers import SymbolNotFound
|
||||||
from piker.toolz import Profiler
|
|
||||||
from piker.ui.qt import (
|
from piker.ui.qt import (
|
||||||
QGraphicsItem,
|
QGraphicsItem,
|
||||||
)
|
)
|
||||||
|
|
@ -100,8 +98,6 @@ def rm_annot(
|
||||||
annot: ArrowEditor|SelectRect|pg.TextItem
|
annot: ArrowEditor|SelectRect|pg.TextItem
|
||||||
) -> bool:
|
) -> bool:
|
||||||
global _editors
|
global _editors
|
||||||
from piker.ui._annotate import GapAnnotations
|
|
||||||
|
|
||||||
match annot:
|
match annot:
|
||||||
case pg.ArrowItem():
|
case pg.ArrowItem():
|
||||||
editor = _editors[annot._uid]
|
editor = _editors[annot._uid]
|
||||||
|
|
@ -126,35 +122,9 @@ def rm_annot(
|
||||||
scene.removeItem(annot)
|
scene.removeItem(annot)
|
||||||
return True
|
return True
|
||||||
|
|
||||||
case GapAnnotations():
|
|
||||||
scene = annot.scene()
|
|
||||||
if scene:
|
|
||||||
scene.removeItem(annot)
|
|
||||||
return True
|
|
||||||
|
|
||||||
return False
|
return False
|
||||||
|
|
||||||
|
|
||||||
@cm
|
|
||||||
def no_qt_updates(*items):
|
|
||||||
'''
|
|
||||||
Disable Qt widget/item updates during context to batch
|
|
||||||
render operations and only trigger single repaint on exit.
|
|
||||||
|
|
||||||
Accepts both QWidgets and QGraphicsItems.
|
|
||||||
|
|
||||||
'''
|
|
||||||
for item in items:
|
|
||||||
if hasattr(item, 'setUpdatesEnabled'):
|
|
||||||
item.setUpdatesEnabled(False)
|
|
||||||
try:
|
|
||||||
yield
|
|
||||||
finally:
|
|
||||||
for item in items:
|
|
||||||
if hasattr(item, 'setUpdatesEnabled'):
|
|
||||||
item.setUpdatesEnabled(True)
|
|
||||||
|
|
||||||
|
|
||||||
async def serve_rc_annots(
|
async def serve_rc_annots(
|
||||||
ipc_key: str,
|
ipc_key: str,
|
||||||
annot_req_stream: MsgStream,
|
annot_req_stream: MsgStream,
|
||||||
|
|
@ -459,333 +429,6 @@ async def serve_rc_annots(
|
||||||
aids.add(aid)
|
aids.add(aid)
|
||||||
await annot_req_stream.send(aid)
|
await annot_req_stream.send(aid)
|
||||||
|
|
||||||
case {
|
|
||||||
'cmd': 'batch',
|
|
||||||
'fqme': fqme,
|
|
||||||
'timeframe': timeframe,
|
|
||||||
'rects': list(rect_specs),
|
|
||||||
'arrows': list(arrow_specs),
|
|
||||||
'texts': list(text_specs),
|
|
||||||
'show_individual_arrows': bool(show_individual_arrows),
|
|
||||||
}:
|
|
||||||
# batch submission handler - process multiple
|
|
||||||
# annotations in single IPC round-trip
|
|
||||||
ds: DisplayState = _dss[fqme]
|
|
||||||
try:
|
|
||||||
chart: ChartPlotWidget = {
|
|
||||||
60: ds.hist_chart,
|
|
||||||
1: ds.chart,
|
|
||||||
}[timeframe]
|
|
||||||
except KeyError:
|
|
||||||
msg: str = (
|
|
||||||
f'No chart for timeframe={timeframe}s, '
|
|
||||||
f'skipping batch annotation'
|
|
||||||
)
|
|
||||||
log.error(msg)
|
|
||||||
await annot_req_stream.send({'error': msg})
|
|
||||||
continue
|
|
||||||
|
|
||||||
cv: ChartView = chart.cv
|
|
||||||
viz: Viz = chart.get_viz(fqme)
|
|
||||||
shm = viz.shm
|
|
||||||
arr = shm.array
|
|
||||||
|
|
||||||
result: dict[str, list[int]] = {
|
|
||||||
'rects': [],
|
|
||||||
'arrows': [],
|
|
||||||
'texts': [],
|
|
||||||
}
|
|
||||||
|
|
||||||
profiler = Profiler(
|
|
||||||
msg=(
|
|
||||||
f'Batch annotate {len(rect_specs)} gaps '
|
|
||||||
f'on {fqme}@{timeframe}s'
|
|
||||||
),
|
|
||||||
disabled=False,
|
|
||||||
delayed=False,
|
|
||||||
)
|
|
||||||
|
|
||||||
aids_set: set[int] = ctxs[ipc_key][1]
|
|
||||||
|
|
||||||
# build unified gap_specs for GapAnnotations class
|
|
||||||
from piker.ui._annotate import GapAnnotations
|
|
||||||
|
|
||||||
gap_specs: list[dict] = []
|
|
||||||
n_gaps: int = max(
|
|
||||||
len(rect_specs),
|
|
||||||
len(arrow_specs),
|
|
||||||
)
|
|
||||||
profiler('setup batch annot creation')
|
|
||||||
|
|
||||||
# collect all unique timestamps for vectorized lookup
|
|
||||||
timestamps: list[float] = []
|
|
||||||
for rect_spec in rect_specs:
|
|
||||||
if start_time := rect_spec.get('start_time'):
|
|
||||||
timestamps.append(start_time)
|
|
||||||
if end_time := rect_spec.get('end_time'):
|
|
||||||
timestamps.append(end_time)
|
|
||||||
for arrow_spec in arrow_specs:
|
|
||||||
if time_val := arrow_spec.get('time'):
|
|
||||||
timestamps.append(time_val)
|
|
||||||
|
|
||||||
profiler('collect `timestamps: list` complet!')
|
|
||||||
|
|
||||||
# build timestamp -> row mapping using binary search
|
|
||||||
# O(m log n) instead of O(n*m) with np.isin
|
|
||||||
time_to_row: dict[float, dict] = {}
|
|
||||||
if timestamps:
|
|
||||||
import numpy as np
|
|
||||||
time_arr = arr['time']
|
|
||||||
ts_array = np.array(timestamps)
|
|
||||||
|
|
||||||
# binary search for each timestamp in sorted time array
|
|
||||||
search_indices = np.searchsorted(
|
|
||||||
time_arr,
|
|
||||||
ts_array,
|
|
||||||
)
|
|
||||||
|
|
||||||
profiler('`np.searchsorted()` complete!')
|
|
||||||
|
|
||||||
# vectorized bounds check and exact match verification
|
|
||||||
valid_mask = (
|
|
||||||
(search_indices < len(arr))
|
|
||||||
& (time_arr[search_indices] == ts_array)
|
|
||||||
)
|
|
||||||
|
|
||||||
# get all valid indices and timestamps
|
|
||||||
valid_indices = search_indices[valid_mask]
|
|
||||||
valid_timestamps = ts_array[valid_mask]
|
|
||||||
|
|
||||||
# use fancy indexing to get all rows at once
|
|
||||||
matched_rows = arr[valid_indices]
|
|
||||||
|
|
||||||
# extract fields to plain arrays BEFORE dict building
|
|
||||||
indices_arr = matched_rows['index'].astype(float)
|
|
||||||
opens_arr = matched_rows['open'].astype(float)
|
|
||||||
closes_arr = matched_rows['close'].astype(float)
|
|
||||||
|
|
||||||
profiler('extracted field arrays')
|
|
||||||
|
|
||||||
# build dict from plain arrays (much faster)
|
|
||||||
time_to_row: dict[float, dict] = {
|
|
||||||
float(ts): {
|
|
||||||
'index': idx,
|
|
||||||
'open': opn,
|
|
||||||
'close': cls,
|
|
||||||
}
|
|
||||||
for (
|
|
||||||
ts,
|
|
||||||
idx,
|
|
||||||
opn,
|
|
||||||
cls,
|
|
||||||
) in zip(
|
|
||||||
valid_timestamps,
|
|
||||||
indices_arr,
|
|
||||||
opens_arr,
|
|
||||||
closes_arr,
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
profiler('`time_to_row` creation complete!')
|
|
||||||
|
|
||||||
profiler(f'built timestamp lookup for {len(timestamps)} times')
|
|
||||||
|
|
||||||
# build gap_specs from rect+arrow specs
|
|
||||||
for i in range(n_gaps):
|
|
||||||
gap_spec: dict = {}
|
|
||||||
|
|
||||||
# get rect spec for this gap
|
|
||||||
if i < len(rect_specs):
|
|
||||||
rect_spec: dict = rect_specs[i].copy()
|
|
||||||
start_time = rect_spec.get('start_time')
|
|
||||||
end_time = rect_spec.get('end_time')
|
|
||||||
|
|
||||||
if (
|
|
||||||
start_time is not None
|
|
||||||
and end_time is not None
|
|
||||||
):
|
|
||||||
# lookup from pre-built mapping
|
|
||||||
start_row = time_to_row.get(start_time)
|
|
||||||
end_row = time_to_row.get(end_time)
|
|
||||||
|
|
||||||
if (
|
|
||||||
start_row is None
|
|
||||||
or end_row is None
|
|
||||||
):
|
|
||||||
log.warning(
|
|
||||||
f'Timestamp lookup failed for '
|
|
||||||
f'gap[{i}], skipping'
|
|
||||||
)
|
|
||||||
continue
|
|
||||||
|
|
||||||
start_idx = start_row['index']
|
|
||||||
end_idx = end_row['index']
|
|
||||||
start_close = start_row['close']
|
|
||||||
end_open = end_row['open']
|
|
||||||
|
|
||||||
from_idx: float = 0.16 - 0.06
|
|
||||||
gap_spec['start_pos'] = (
|
|
||||||
start_idx + 1 - from_idx,
|
|
||||||
start_close,
|
|
||||||
)
|
|
||||||
gap_spec['end_pos'] = (
|
|
||||||
end_idx + from_idx,
|
|
||||||
end_open,
|
|
||||||
)
|
|
||||||
gap_spec['start_time'] = start_time
|
|
||||||
gap_spec['end_time'] = end_time
|
|
||||||
gap_spec['color'] = rect_spec.get(
|
|
||||||
'color',
|
|
||||||
'dad_blue',
|
|
||||||
)
|
|
||||||
|
|
||||||
# get arrow spec for this gap
|
|
||||||
if i < len(arrow_specs):
|
|
||||||
arrow_spec: dict = arrow_specs[i].copy()
|
|
||||||
x: float = float(arrow_spec.get('x', 0))
|
|
||||||
y: float = float(arrow_spec.get('y', 0))
|
|
||||||
time_val: float|None = arrow_spec.get('time')
|
|
||||||
|
|
||||||
# timestamp-based index lookup (only for x, NOT y!)
|
|
||||||
# y is already set to the PREVIOUS bar's close
|
|
||||||
if time_val is not None:
|
|
||||||
arrow_row = time_to_row.get(time_val)
|
|
||||||
if arrow_row is not None:
|
|
||||||
x = arrow_row['index']
|
|
||||||
# NOTE: do NOT update y! it's the
|
|
||||||
# previous bar's close, not current
|
|
||||||
else:
|
|
||||||
log.warning(
|
|
||||||
f'Arrow timestamp {time_val} not '
|
|
||||||
f'found for gap[{i}], using x={x}'
|
|
||||||
)
|
|
||||||
|
|
||||||
gap_spec['arrow_x'] = x
|
|
||||||
gap_spec['arrow_y'] = y
|
|
||||||
gap_spec['time'] = time_val
|
|
||||||
gap_spec['pointing'] = arrow_spec.get(
|
|
||||||
'pointing',
|
|
||||||
'down',
|
|
||||||
)
|
|
||||||
gap_spec['alpha'] = arrow_spec.get('alpha', 169)
|
|
||||||
|
|
||||||
gap_specs.append(gap_spec)
|
|
||||||
|
|
||||||
profiler(f'built {len(gap_specs)} gap_specs')
|
|
||||||
|
|
||||||
# create single GapAnnotations item for all gaps
|
|
||||||
if gap_specs:
|
|
||||||
gaps_item = GapAnnotations(
|
|
||||||
gap_specs=gap_specs,
|
|
||||||
array=arr,
|
|
||||||
color=gap_specs[0].get('color', 'dad_blue'),
|
|
||||||
alpha=gap_specs[0].get('alpha', 169),
|
|
||||||
arrow_size=10.0,
|
|
||||||
fqme=fqme,
|
|
||||||
timeframe=timeframe,
|
|
||||||
)
|
|
||||||
chart.plotItem.addItem(gaps_item)
|
|
||||||
|
|
||||||
# register single item for repositioning
|
|
||||||
aid: int = id(gaps_item)
|
|
||||||
annots[aid] = gaps_item
|
|
||||||
aids_set.add(aid)
|
|
||||||
result['rects'].append(aid)
|
|
||||||
profiler(
|
|
||||||
f'created GapAnnotations item for {len(gap_specs)} '
|
|
||||||
f'gaps'
|
|
||||||
)
|
|
||||||
|
|
||||||
# A/B comparison: optionally create individual arrows
|
|
||||||
# alongside batch for visual comparison
|
|
||||||
if show_individual_arrows:
|
|
||||||
godw = chart.linked.godwidget
|
|
||||||
arrows: ArrowEditor = ArrowEditor(godw=godw)
|
|
||||||
for i, spec in enumerate(gap_specs):
|
|
||||||
if 'arrow_x' not in spec:
|
|
||||||
continue
|
|
||||||
|
|
||||||
aid_str: str = str(uuid4())
|
|
||||||
arrow: pg.ArrowItem = arrows.add(
|
|
||||||
plot=chart.plotItem,
|
|
||||||
uid=aid_str,
|
|
||||||
x=spec['arrow_x'],
|
|
||||||
y=spec['arrow_y'],
|
|
||||||
pointing=spec['pointing'],
|
|
||||||
color='bracket', # different color
|
|
||||||
alpha=spec.get('alpha', 169),
|
|
||||||
headLen=10.0,
|
|
||||||
headWidth=2.222,
|
|
||||||
pxMode=True,
|
|
||||||
)
|
|
||||||
arrow._abs_x = spec['arrow_x']
|
|
||||||
arrow._abs_y = spec['arrow_y']
|
|
||||||
|
|
||||||
annots[aid_str] = arrow
|
|
||||||
_editors[aid_str] = arrows
|
|
||||||
aids_set.add(aid_str)
|
|
||||||
result['arrows'].append(aid_str)
|
|
||||||
|
|
||||||
profiler(
|
|
||||||
f'created {len(gap_specs)} individual arrows '
|
|
||||||
f'for comparison'
|
|
||||||
)
|
|
||||||
|
|
||||||
# handle text items separately (less common, keep
|
|
||||||
# individual items)
|
|
||||||
n_texts: int = 0
|
|
||||||
for text_spec in text_specs:
|
|
||||||
kwargs: dict = text_spec.copy()
|
|
||||||
text: str = kwargs.pop('text')
|
|
||||||
x: float = float(kwargs.pop('x'))
|
|
||||||
y: float = float(kwargs.pop('y'))
|
|
||||||
time_val: float|None = kwargs.pop('time', None)
|
|
||||||
|
|
||||||
# timestamp-based index lookup
|
|
||||||
if time_val is not None:
|
|
||||||
matches = arr[arr['time'] == time_val]
|
|
||||||
if len(matches) > 0:
|
|
||||||
x = float(matches[0]['index'])
|
|
||||||
y = float(matches[0]['close'])
|
|
||||||
|
|
||||||
color = kwargs.pop('color', 'dad_blue')
|
|
||||||
anchor = kwargs.pop('anchor', (0, 1))
|
|
||||||
font_size = kwargs.pop('font_size', None)
|
|
||||||
|
|
||||||
text_item: pg.TextItem = pg.TextItem(
|
|
||||||
text,
|
|
||||||
color=hcolor(color),
|
|
||||||
anchor=anchor,
|
|
||||||
)
|
|
||||||
|
|
||||||
if font_size is None:
|
|
||||||
from ._style import get_fonts
|
|
||||||
font, font_small = get_fonts()
|
|
||||||
font_size = font_small.px_size - 1
|
|
||||||
|
|
||||||
qfont: QFont = text_item.textItem.font()
|
|
||||||
qfont.setPixelSize(font_size)
|
|
||||||
text_item.setFont(qfont)
|
|
||||||
|
|
||||||
text_item.setPos(float(x), float(y))
|
|
||||||
chart.plotItem.addItem(text_item)
|
|
||||||
|
|
||||||
text_item._abs_x = float(x)
|
|
||||||
text_item._abs_y = float(y)
|
|
||||||
|
|
||||||
aid: str = str(uuid4())
|
|
||||||
annots[aid] = text_item
|
|
||||||
aids_set.add(aid)
|
|
||||||
result['texts'].append(aid)
|
|
||||||
n_texts += 1
|
|
||||||
|
|
||||||
profiler(
|
|
||||||
f'created text annotations: {n_texts} texts'
|
|
||||||
)
|
|
||||||
profiler.finish()
|
|
||||||
|
|
||||||
await annot_req_stream.send(result)
|
|
||||||
|
|
||||||
case {
|
case {
|
||||||
'cmd': 'remove',
|
'cmd': 'remove',
|
||||||
'aid': int(aid)|str(aid),
|
'aid': int(aid)|str(aid),
|
||||||
|
|
@ -828,26 +471,10 @@ async def serve_rc_annots(
|
||||||
# XXX: reposition all annotations to ensure they
|
# XXX: reposition all annotations to ensure they
|
||||||
# stay aligned with viz data after reset (eg during
|
# stay aligned with viz data after reset (eg during
|
||||||
# backfill when abs-index range changes)
|
# backfill when abs-index range changes)
|
||||||
chart: ChartPlotWidget = {
|
|
||||||
60: ds.hist_chart,
|
|
||||||
1: ds.chart,
|
|
||||||
}[timeframe]
|
|
||||||
viz: Viz = chart.get_viz(fqme)
|
|
||||||
arr = viz.shm.array
|
|
||||||
|
|
||||||
n_repositioned: int = 0
|
n_repositioned: int = 0
|
||||||
for aid, annot in annots.items():
|
for aid, annot in annots.items():
|
||||||
# GapAnnotations batch items have .reposition()
|
|
||||||
if hasattr(annot, 'reposition'):
|
|
||||||
annot.reposition(
|
|
||||||
array=arr,
|
|
||||||
fqme=fqme,
|
|
||||||
timeframe=timeframe,
|
|
||||||
)
|
|
||||||
n_repositioned += 1
|
|
||||||
|
|
||||||
# arrows and text items use abs x,y coords
|
# arrows and text items use abs x,y coords
|
||||||
elif (
|
if (
|
||||||
hasattr(annot, '_abs_x')
|
hasattr(annot, '_abs_x')
|
||||||
and
|
and
|
||||||
hasattr(annot, '_abs_y')
|
hasattr(annot, '_abs_y')
|
||||||
|
|
@ -912,21 +539,12 @@ async def remote_annotate(
|
||||||
finally:
|
finally:
|
||||||
# ensure all annots for this connection are deleted
|
# ensure all annots for this connection are deleted
|
||||||
# on any final teardown
|
# on any final teardown
|
||||||
profiler = Profiler(
|
|
||||||
msg=f'Annotation teardown for ctx {ctx.cid}',
|
|
||||||
disabled=False,
|
|
||||||
ms_threshold=0.0,
|
|
||||||
)
|
|
||||||
(_ctx, aids) = _ctxs[ctx.cid]
|
(_ctx, aids) = _ctxs[ctx.cid]
|
||||||
assert _ctx is ctx
|
assert _ctx is ctx
|
||||||
profiler(f'got {len(aids)} aids to remove')
|
|
||||||
|
|
||||||
for aid in aids:
|
for aid in aids:
|
||||||
annot: QGraphicsItem = _annots[aid]
|
annot: QGraphicsItem = _annots[aid]
|
||||||
assert rm_annot(annot)
|
assert rm_annot(annot)
|
||||||
|
|
||||||
profiler(f'removed all {len(aids)} annotations')
|
|
||||||
|
|
||||||
|
|
||||||
class AnnotCtl(Struct):
|
class AnnotCtl(Struct):
|
||||||
'''
|
'''
|
||||||
|
|
@ -1128,64 +746,6 @@ class AnnotCtl(Struct):
|
||||||
)
|
)
|
||||||
return aid
|
return aid
|
||||||
|
|
||||||
async def add_batch(
|
|
||||||
self,
|
|
||||||
fqme: str,
|
|
||||||
timeframe: float,
|
|
||||||
rects: list[dict]|None = None,
|
|
||||||
arrows: list[dict]|None = None,
|
|
||||||
texts: list[dict]|None = None,
|
|
||||||
show_individual_arrows: bool = False,
|
|
||||||
|
|
||||||
from_acm: bool = False,
|
|
||||||
|
|
||||||
) -> dict[str, list[int]]:
|
|
||||||
'''
|
|
||||||
Batch submit multiple annotations in single IPC msg for
|
|
||||||
much faster remote annotation vs. per-annot round-trips.
|
|
||||||
|
|
||||||
Returns dict of annotation IDs:
|
|
||||||
{
|
|
||||||
'rects': [aid1, aid2, ...],
|
|
||||||
'arrows': [aid3, aid4, ...],
|
|
||||||
'texts': [aid5, aid6, ...],
|
|
||||||
}
|
|
||||||
|
|
||||||
'''
|
|
||||||
ipc: MsgStream = self._get_ipc(fqme)
|
|
||||||
with trio.fail_after(10):
|
|
||||||
await ipc.send({
|
|
||||||
'fqme': fqme,
|
|
||||||
'cmd': 'batch',
|
|
||||||
'timeframe': timeframe,
|
|
||||||
'rects': rects or [],
|
|
||||||
'arrows': arrows or [],
|
|
||||||
'texts': texts or [],
|
|
||||||
'show_individual_arrows': show_individual_arrows,
|
|
||||||
})
|
|
||||||
result: dict = await ipc.receive()
|
|
||||||
match result:
|
|
||||||
case {'error': str(msg)}:
|
|
||||||
log.error(msg)
|
|
||||||
return {
|
|
||||||
'rects': [],
|
|
||||||
'arrows': [],
|
|
||||||
'texts': [],
|
|
||||||
}
|
|
||||||
|
|
||||||
# register all AIDs with their IPC streams
|
|
||||||
for aid_list in result.values():
|
|
||||||
for aid in aid_list:
|
|
||||||
self._ipcs[aid] = ipc
|
|
||||||
if not from_acm:
|
|
||||||
self._annot_stack.push_async_callback(
|
|
||||||
partial(
|
|
||||||
self.remove,
|
|
||||||
aid,
|
|
||||||
)
|
|
||||||
)
|
|
||||||
return result
|
|
||||||
|
|
||||||
async def add_text(
|
async def add_text(
|
||||||
self,
|
self,
|
||||||
fqme: str,
|
fqme: str,
|
||||||
|
|
@ -1321,14 +881,3 @@ async def open_annot_ctl(
|
||||||
_annot_stack=annots_stack,
|
_annot_stack=annots_stack,
|
||||||
)
|
)
|
||||||
yield client
|
yield client
|
||||||
|
|
||||||
# client exited, measure teardown time
|
|
||||||
teardown_profiler = Profiler(
|
|
||||||
msg='Client AnnotCtl teardown',
|
|
||||||
disabled=False,
|
|
||||||
ms_threshold=0.0,
|
|
||||||
)
|
|
||||||
teardown_profiler('exiting annots_stack')
|
|
||||||
|
|
||||||
teardown_profiler('annots_stack exited')
|
|
||||||
teardown_profiler('exiting gather_contexts')
|
|
||||||
|
|
|
||||||
|
|
@ -99,6 +99,7 @@ python-downloads = 'manual'
|
||||||
# https://docs.astral.sh/uv/concepts/projects/dependencies/#default-groups
|
# https://docs.astral.sh/uv/concepts/projects/dependencies/#default-groups
|
||||||
default-groups = [
|
default-groups = [
|
||||||
'uis',
|
'uis',
|
||||||
|
'repl',
|
||||||
]
|
]
|
||||||
# ------ tool.uv ------
|
# ------ tool.uv ------
|
||||||
|
|
||||||
|
|
@ -130,7 +131,7 @@ repl = [
|
||||||
"greenback >=1.1.1, <2.0.0",
|
"greenback >=1.1.1, <2.0.0",
|
||||||
|
|
||||||
# @goodboy's preferred console toolz
|
# @goodboy's preferred console toolz
|
||||||
"xonsh",
|
"xonsh>=0.22.2",
|
||||||
"prompt-toolkit ==3.0.40",
|
"prompt-toolkit ==3.0.40",
|
||||||
"pyperclip>=1.9.0",
|
"pyperclip>=1.9.0",
|
||||||
|
|
||||||
|
|
@ -191,23 +192,19 @@ include = ["piker"]
|
||||||
|
|
||||||
|
|
||||||
[tool.uv.sources]
|
[tool.uv.sources]
|
||||||
|
pyqtgraph = { git = "https://github.com/pikers/pyqtgraph.git" }
|
||||||
tomlkit = { git = "https://github.com/pikers/tomlkit.git", branch ="piker_pin" }
|
tomlkit = { git = "https://github.com/pikers/tomlkit.git", branch ="piker_pin" }
|
||||||
pyvnc = { git = "https://github.com/regulad/pyvnc.git" }
|
pyvnc = { git = "https://github.com/regulad/pyvnc.git" }
|
||||||
|
|
||||||
pyqtgraph = { git = "https://github.com/pyqtgraph/pyqtgraph.git", branch = 'master' }
|
|
||||||
# pyqtgraph = { path = '../pyqtgraph', editable = true }
|
|
||||||
# ?TODO, resync our fork?
|
|
||||||
# pyqtgraph = { git = "https://github.com/pikers/pyqtgraph.git" }
|
|
||||||
|
|
||||||
# to get fancy next-cmd/suggestion feats prior to 0.22.2 B)
|
# to get fancy next-cmd/suggestion feats prior to 0.22.2 B)
|
||||||
# https://github.com/xonsh/xonsh/pull/6037
|
# https://github.com/xonsh/xonsh/pull/6037
|
||||||
# https://github.com/xonsh/xonsh/pull/6048
|
# https://github.com/xonsh/xonsh/pull/6048
|
||||||
xonsh = { git = 'https://github.com/xonsh/xonsh.git', branch = 'main' }
|
# xonsh = { git = 'https://github.com/xonsh/xonsh.git', branch = 'main' }
|
||||||
|
|
||||||
# XXX since, we're like, always hacking new shite all-the-time. Bp
|
# XXX since, we're like, always hacking new shite all-the-time. Bp
|
||||||
# tractor = { git = "https://github.com/goodboy/tractor.git", branch ="piker_pin" }
|
tractor = { git = "https://github.com/goodboy/tractor.git", branch ="piker_pin" }
|
||||||
# tractor = { git = "https://pikers.dev/goodboy/tractor", branch = "piker_pin" }
|
# tractor = { git = "https://pikers.dev/goodboy/tractor", branch = "piker_pin" }
|
||||||
# tractor = { git = "https://pikers.dev/goodboy/tractor", branch = "main" }
|
# tractor = { git = "https://pikers.dev/goodboy/tractor", branch = "main" }
|
||||||
# ------ goodboy ------
|
# ------ goodboy ------
|
||||||
# hackin dev-envs, usually there's something new he's hackin in..
|
# hackin dev-envs, usually there's something new he's hackin in..
|
||||||
tractor = { path = "../tractor", editable = true }
|
# tractor = { path = "../tractor", editable = true }
|
||||||
|
|
|
||||||
75
uv.lock
75
uv.lock
|
|
@ -1197,7 +1197,7 @@ requires-dist = [
|
||||||
{ name = "tomli", specifier = ">=2.0.1,<3.0.0" },
|
{ name = "tomli", specifier = ">=2.0.1,<3.0.0" },
|
||||||
{ name = "tomli-w", specifier = ">=1.0.0,<2.0.0" },
|
{ name = "tomli-w", specifier = ">=1.0.0,<2.0.0" },
|
||||||
{ name = "tomlkit", git = "https://github.com/pikers/tomlkit.git?branch=piker_pin" },
|
{ name = "tomlkit", git = "https://github.com/pikers/tomlkit.git?branch=piker_pin" },
|
||||||
{ name = "tractor", editable = "../tractor" },
|
{ name = "tractor", git = "https://github.com/goodboy/tractor.git?branch=piker_pin" },
|
||||||
{ name = "trio", specifier = ">=0.27" },
|
{ name = "trio", specifier = ">=0.27" },
|
||||||
{ name = "trio-typing", specifier = ">=0.10.0" },
|
{ name = "trio-typing", specifier = ">=0.10.0" },
|
||||||
{ name = "trio-util", specifier = ">=0.7.0,<0.8.0" },
|
{ name = "trio-util", specifier = ">=0.7.0,<0.8.0" },
|
||||||
|
|
@ -1218,11 +1218,11 @@ dev = [
|
||||||
{ name = "prompt-toolkit", specifier = "==3.0.40" },
|
{ name = "prompt-toolkit", specifier = "==3.0.40" },
|
||||||
{ name = "pyperclip", specifier = ">=1.9.0" },
|
{ name = "pyperclip", specifier = ">=1.9.0" },
|
||||||
{ name = "pyqt6", specifier = ">=6.7.0,<7.0.0" },
|
{ name = "pyqt6", specifier = ">=6.7.0,<7.0.0" },
|
||||||
{ name = "pyqtgraph", git = "https://github.com/pyqtgraph/pyqtgraph.git?branch=master" },
|
{ name = "pyqtgraph", git = "https://github.com/pikers/pyqtgraph.git" },
|
||||||
{ name = "pytest" },
|
{ name = "pytest" },
|
||||||
{ name = "qdarkstyle", specifier = ">=3.0.2,<4.0.0" },
|
{ name = "qdarkstyle", specifier = ">=3.0.2,<4.0.0" },
|
||||||
{ name = "rapidfuzz", specifier = ">=3.2.0,<4.0.0" },
|
{ name = "rapidfuzz", specifier = ">=3.2.0,<4.0.0" },
|
||||||
{ name = "xonsh", git = "https://github.com/xonsh/xonsh.git?branch=main" },
|
{ name = "xonsh", specifier = ">=0.22.2" },
|
||||||
]
|
]
|
||||||
lint = [{ name = "ruff", specifier = ">=0.9.6" }]
|
lint = [{ name = "ruff", specifier = ">=0.9.6" }]
|
||||||
repl = [
|
repl = [
|
||||||
|
|
@ -1231,23 +1231,23 @@ repl = [
|
||||||
{ name = "pexpect", specifier = ">=4.9.0" },
|
{ name = "pexpect", specifier = ">=4.9.0" },
|
||||||
{ name = "prompt-toolkit", specifier = "==3.0.40" },
|
{ name = "prompt-toolkit", specifier = "==3.0.40" },
|
||||||
{ name = "pyperclip", specifier = ">=1.9.0" },
|
{ name = "pyperclip", specifier = ">=1.9.0" },
|
||||||
{ name = "xonsh", git = "https://github.com/xonsh/xonsh.git?branch=main" },
|
{ name = "xonsh", specifier = ">=0.22.2" },
|
||||||
]
|
]
|
||||||
testing = [{ name = "pytest" }]
|
testing = [{ name = "pytest" }]
|
||||||
uis = [
|
uis = [
|
||||||
{ name = "pyqt6", specifier = ">=6.7.0,<7.0.0" },
|
{ name = "pyqt6", specifier = ">=6.7.0,<7.0.0" },
|
||||||
{ name = "pyqtgraph", git = "https://github.com/pyqtgraph/pyqtgraph.git?branch=master" },
|
{ name = "pyqtgraph", git = "https://github.com/pikers/pyqtgraph.git" },
|
||||||
{ name = "qdarkstyle", specifier = ">=3.0.2,<4.0.0" },
|
{ name = "qdarkstyle", specifier = ">=3.0.2,<4.0.0" },
|
||||||
{ name = "rapidfuzz", specifier = ">=3.2.0,<4.0.0" },
|
{ name = "rapidfuzz", specifier = ">=3.2.0,<4.0.0" },
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "platformdirs"
|
name = "platformdirs"
|
||||||
version = "4.9.2"
|
version = "4.6.0"
|
||||||
source = { registry = "https://pypi.org/simple" }
|
source = { registry = "https://pypi.org/simple" }
|
||||||
sdist = { url = "https://files.pythonhosted.org/packages/1b/04/fea538adf7dbbd6d186f551d595961e564a3b6715bdf276b477460858672/platformdirs-4.9.2.tar.gz", hash = "sha256:9a33809944b9db043ad67ca0db94b14bf452cc6aeaac46a88ea55b26e2e9d291", size = 28394, upload-time = "2026-02-16T03:56:10.574Z" }
|
sdist = { url = "https://files.pythonhosted.org/packages/20/e5/474d0a8508029286b905622e6929470fb84337cfa08f9d09fbb624515249/platformdirs-4.6.0.tar.gz", hash = "sha256:4a13c2db1071e5846c3b3e04e5b095c0de36b2a24be9a3bc0145ca66fce4e328", size = 23433, upload-time = "2026-02-12T14:36:21.288Z" }
|
||||||
wheels = [
|
wheels = [
|
||||||
{ url = "https://files.pythonhosted.org/packages/48/31/05e764397056194206169869b50cf2fee4dbbbc71b344705b9c0d878d4d8/platformdirs-4.9.2-py3-none-any.whl", hash = "sha256:9170634f126f8efdae22fb58ae8a0eaa86f38365bc57897a6c4f781d1f5875bd", size = 21168, upload-time = "2026-02-16T03:56:08.891Z" },
|
{ url = "https://files.pythonhosted.org/packages/da/10/1b0dcf51427326f70e50d98df21b18c228117a743a1fc515a42f8dc7d342/platformdirs-4.6.0-py3-none-any.whl", hash = "sha256:dd7f808d828e1764a22ebff09e60f175ee3c41876606a6132a688d809c7c9c73", size = 19549, upload-time = "2026-02-12T14:36:19.743Z" },
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
|
|
@ -1603,10 +1603,9 @@ wheels = [
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "pyqtgraph"
|
name = "pyqtgraph"
|
||||||
version = "0.15.0.dev0"
|
version = "0.12.3"
|
||||||
source = { git = "https://github.com/pyqtgraph/pyqtgraph.git?branch=master#d588dd3ec30915e61c8496a0fe1db21fc25e4da5" }
|
source = { git = "https://github.com/pikers/pyqtgraph.git#373f9561ea8ec4fef9b4e8bdcdd4bbf372dd6512" }
|
||||||
dependencies = [
|
dependencies = [
|
||||||
{ name = "colorama" },
|
|
||||||
{ name = "numpy" },
|
{ name = "numpy" },
|
||||||
]
|
]
|
||||||
|
|
||||||
|
|
@ -1971,7 +1970,7 @@ wheels = [
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "tractor"
|
name = "tractor"
|
||||||
version = "0.1.0a6.dev0"
|
version = "0.1.0a6.dev0"
|
||||||
source = { editable = "../tractor" }
|
source = { git = "https://github.com/goodboy/tractor.git?branch=piker_pin#36307c59175a1d04fecc77ef2c28f5c943b5f3d1" }
|
||||||
dependencies = [
|
dependencies = [
|
||||||
{ name = "bidict" },
|
{ name = "bidict" },
|
||||||
{ name = "cffi" },
|
{ name = "cffi" },
|
||||||
|
|
@ -1984,48 +1983,6 @@ dependencies = [
|
||||||
{ name = "wrapt" },
|
{ name = "wrapt" },
|
||||||
]
|
]
|
||||||
|
|
||||||
[package.metadata]
|
|
||||||
requires-dist = [
|
|
||||||
{ name = "bidict", specifier = ">=0.23.1" },
|
|
||||||
{ name = "cffi", specifier = ">=1.17.1" },
|
|
||||||
{ name = "colorlog", specifier = ">=6.8.2,<7" },
|
|
||||||
{ name = "msgspec", specifier = ">=0.19.0" },
|
|
||||||
{ name = "pdbp", specifier = ">=1.8.2,<2" },
|
|
||||||
{ name = "platformdirs", specifier = ">=4.4.0" },
|
|
||||||
{ name = "tricycle", specifier = ">=0.4.1,<0.5" },
|
|
||||||
{ name = "trio", specifier = ">0.27" },
|
|
||||||
{ name = "wrapt", specifier = ">=1.16.0,<2" },
|
|
||||||
]
|
|
||||||
|
|
||||||
[package.metadata.requires-dev]
|
|
||||||
dev = [
|
|
||||||
{ name = "greenback", specifier = ">=1.2.1,<2" },
|
|
||||||
{ name = "pexpect", specifier = ">=4.9.0,<5" },
|
|
||||||
{ name = "prompt-toolkit", specifier = ">=3.0.50" },
|
|
||||||
{ name = "psutil", specifier = ">=7.0.0" },
|
|
||||||
{ name = "pyperclip", specifier = ">=1.9.0" },
|
|
||||||
{ name = "pytest", specifier = ">=8.3.5" },
|
|
||||||
{ name = "stackscope", specifier = ">=0.2.2,<0.3" },
|
|
||||||
{ name = "typing-extensions", specifier = ">=4.14.1" },
|
|
||||||
{ name = "xonsh", specifier = ">=0.22.2" },
|
|
||||||
]
|
|
||||||
devx = [
|
|
||||||
{ name = "greenback", specifier = ">=1.2.1,<2" },
|
|
||||||
{ name = "stackscope", specifier = ">=0.2.2,<0.3" },
|
|
||||||
{ name = "typing-extensions", specifier = ">=4.14.1" },
|
|
||||||
]
|
|
||||||
lint = [{ name = "ruff", specifier = ">=0.9.6" }]
|
|
||||||
repl = [
|
|
||||||
{ name = "prompt-toolkit", specifier = ">=3.0.50" },
|
|
||||||
{ name = "psutil", specifier = ">=7.0.0" },
|
|
||||||
{ name = "pyperclip", specifier = ">=1.9.0" },
|
|
||||||
{ name = "xonsh", specifier = ">=0.22.2" },
|
|
||||||
]
|
|
||||||
testing = [
|
|
||||||
{ name = "pexpect", specifier = ">=4.9.0,<5" },
|
|
||||||
{ name = "pytest", specifier = ">=8.3.5" },
|
|
||||||
]
|
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "tricycle"
|
name = "tricycle"
|
||||||
version = "0.4.1"
|
version = "0.4.1"
|
||||||
|
|
@ -2265,8 +2222,14 @@ wheels = [
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "xonsh"
|
name = "xonsh"
|
||||||
version = "0.22.3"
|
version = "0.22.4"
|
||||||
source = { git = "https://github.com/xonsh/xonsh.git?branch=main#b446946fd94c3913e002318db1d1b41ee4fa1f9a" }
|
source = { registry = "https://pypi.org/simple" }
|
||||||
|
sdist = { url = "https://files.pythonhosted.org/packages/48/df/1fc9ed62b3d7c14612e1713e9eb7bd41d54f6ad1028a8fbb6b7cddebc345/xonsh-0.22.4.tar.gz", hash = "sha256:6be346563fec2db75778ba5d2caee155525e634e99d9cc8cc347626025c0b3fa", size = 826665, upload-time = "2026-02-17T07:53:39.424Z" }
|
||||||
|
wheels = [
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/2e/00/7cbc0c1fb64365a0a317c54ce3a151c9644eea5a509d9cbaae61c9fd1426/xonsh-0.22.4-py311-none-any.whl", hash = "sha256:38b29b29fa85aa756462d9d9bbcaa1d85478c2108da3de6cc590a69a4bcd1a01", size = 654375, upload-time = "2026-02-17T07:53:37.702Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/2e/c2/3dd498dc28d8f89cdd52e39950c5e591499ae423f61694c0bb4d03ed1d82/xonsh-0.22.4-py312-none-any.whl", hash = "sha256:4e538fac9f4c3d866ddbdeca068f0c0515469c997ed58d3bfee963878c6df5a5", size = 654300, upload-time = "2026-02-17T07:53:35.813Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/82/7d/1f9c7147518e9f03f6ce081b5bfc4f1aceb6ec5caba849024d005e41d3be/xonsh-0.22.4-py313-none-any.whl", hash = "sha256:cc5fabf0ad0c56a2a11bed1e6a43c4ec6416a5b30f24f126b8e768547c3793e2", size = 654818, upload-time = "2026-02-17T07:53:33.477Z" },
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "yapic-json"
|
name = "yapic-json"
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue