jstdata 0.2.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- jstdata/__init__.py +13 -0
- jstdata/agent_guide.py +530 -0
- jstdata/cli.py +682 -0
- jstdata/client.py +543 -0
- jstdata/models.py +161 -0
- jstdata/session.py +251 -0
- jstdata/utils.py +103 -0
- jstdata/workflows/__init__.py +88 -0
- jstdata/workflows/base.py +356 -0
- jstdata/workflows/bundled/__init__.py +1 -0
- jstdata/workflows/bundled/tutorial.yaml +14 -0
- jstdata/workflows/bundled/tutorial_script.yaml +45 -0
- jstdata/workflows/chart.py +357 -0
- jstdata/workflows/console.py +560 -0
- jstdata/workflows/discover.py +502 -0
- jstdata/workflows/export.py +171 -0
- jstdata/workflows/find.py +386 -0
- jstdata/workflows/host.py +255 -0
- jstdata/workflows/rank.py +658 -0
- jstdata/workflows/session_manager.py +277 -0
- jstdata/workflows/store.py +308 -0
- jstdata/workflows/tutorial.py +163 -0
- jstdata/workflows/tutorial_host.py +178 -0
- jstdata-0.2.0.dist-info/METADATA +227 -0
- jstdata-0.2.0.dist-info/RECORD +28 -0
- jstdata-0.2.0.dist-info/WHEEL +4 -0
- jstdata-0.2.0.dist-info/entry_points.txt +3 -0
- jstdata-0.2.0.dist-info/licenses/LICENSE +21 -0
|
@@ -0,0 +1,560 @@
|
|
|
1
|
+
"""Console step — one-pane catalog search that stages into the session."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import asyncio
|
|
6
|
+
from typing import TypeAlias
|
|
7
|
+
|
|
8
|
+
from textual import on, work
|
|
9
|
+
from textual.app import ComposeResult
|
|
10
|
+
from textual.binding import Binding
|
|
11
|
+
from textual.containers import Horizontal, Vertical
|
|
12
|
+
from textual.screen import Screen
|
|
13
|
+
from textual.widgets import Input, Label, ListItem, ListView
|
|
14
|
+
|
|
15
|
+
from ..client import JSTDataClient
|
|
16
|
+
from ..models import Entity, Metric, Series, Resource as ApiResource
|
|
17
|
+
from ..session import Session
|
|
18
|
+
from .base import StepArgument, StepBinding, StepSpec, register
|
|
19
|
+
|
|
20
|
+
Resource: TypeAlias = Series | Entity | Metric | ApiResource
|
|
21
|
+
|
|
22
|
+
PREFETCH_LIMIT = 201
|
|
23
|
+
LOCAL_CAP = 200
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
def _normalize_relations(relation: str | list[str] | tuple[str, ...] | None) -> list[str] | None:
|
|
27
|
+
if relation is None:
|
|
28
|
+
return None
|
|
29
|
+
if isinstance(relation, str):
|
|
30
|
+
value = relation.strip()
|
|
31
|
+
return [value] if value else None
|
|
32
|
+
items = [str(v).strip() for v in relation if str(v).strip()]
|
|
33
|
+
return items or None
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
class SearchResultRow(ListItem):
|
|
37
|
+
"""A row in the search results list."""
|
|
38
|
+
|
|
39
|
+
def __init__(self, resource: Resource):
|
|
40
|
+
super().__init__()
|
|
41
|
+
self.resource = resource
|
|
42
|
+
|
|
43
|
+
def compose(self) -> ComposeResult:
|
|
44
|
+
res = self.resource
|
|
45
|
+
name = getattr(res, "label", getattr(res, "name", "Unknown"))
|
|
46
|
+
res_type = (
|
|
47
|
+
"SERIES"
|
|
48
|
+
if isinstance(res, Series)
|
|
49
|
+
else "ENTITY"
|
|
50
|
+
if isinstance(res, Entity)
|
|
51
|
+
else "METRIC"
|
|
52
|
+
)
|
|
53
|
+
source = getattr(res, "source", "N/A")
|
|
54
|
+
|
|
55
|
+
yield Horizontal(
|
|
56
|
+
Label(f"{name}", classes="col-name"),
|
|
57
|
+
Label(f"{res.id[:15]:<15}", classes="col-id"),
|
|
58
|
+
Label(f"{source[:10]:<10}", classes="col-src"),
|
|
59
|
+
Label(f"{res_type:<10}", classes="col-type"),
|
|
60
|
+
)
|
|
61
|
+
|
|
62
|
+
|
|
63
|
+
class WorkspaceScreen(Screen):
|
|
64
|
+
"""Single-pane search: prefetch a catalog, filter locally when it fits."""
|
|
65
|
+
|
|
66
|
+
BINDINGS = [
|
|
67
|
+
Binding("slash", "focus_filter", "Search", key_display="/"),
|
|
68
|
+
Binding("escape", "leave_filter", "Leave search", show=False),
|
|
69
|
+
Binding("j", "cursor_down", "Cursor Down", show=False),
|
|
70
|
+
Binding("k", "cursor_up", "Cursor Up", show=False),
|
|
71
|
+
]
|
|
72
|
+
|
|
73
|
+
DEFAULT_CSS = """
|
|
74
|
+
#workspace-body {
|
|
75
|
+
height: 1fr;
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
.pane-container {
|
|
79
|
+
border: solid #222;
|
|
80
|
+
background: #0f0f0f;
|
|
81
|
+
height: 1fr;
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
.pane-header {
|
|
85
|
+
background: #1a1a1a;
|
|
86
|
+
color: #4ade80;
|
|
87
|
+
padding: 0 1;
|
|
88
|
+
text-style: bold;
|
|
89
|
+
height: 1;
|
|
90
|
+
}
|
|
91
|
+
.table-header {
|
|
92
|
+
height: 1;
|
|
93
|
+
background: #111;
|
|
94
|
+
border-bottom: solid #333;
|
|
95
|
+
padding: 0 1;
|
|
96
|
+
}
|
|
97
|
+
.table-header Label {
|
|
98
|
+
color: #888;
|
|
99
|
+
text-style: bold;
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
#search-input {
|
|
103
|
+
height: 3;
|
|
104
|
+
background: transparent;
|
|
105
|
+
border: none;
|
|
106
|
+
border-bottom: solid #333;
|
|
107
|
+
padding: 0 1;
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
#results-list {
|
|
111
|
+
height: 1fr;
|
|
112
|
+
background: #0f0f0f;
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
.col-name { width: 55%; height: 1; overflow: hidden; }
|
|
116
|
+
.col-id { width: 25%; height: 1; overflow: hidden; }
|
|
117
|
+
.col-src { width: 10%; height: 1; overflow: hidden; }
|
|
118
|
+
.col-type { width: 10%; height: 1; overflow: hidden; }
|
|
119
|
+
|
|
120
|
+
ListView > ListItem,
|
|
121
|
+
SearchResultRow {
|
|
122
|
+
height: 1;
|
|
123
|
+
min-height: 1;
|
|
124
|
+
max-height: 1;
|
|
125
|
+
width: 100%;
|
|
126
|
+
padding: 0 1;
|
|
127
|
+
overflow: hidden;
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
SearchResultRow > Horizontal {
|
|
131
|
+
height: 1;
|
|
132
|
+
min-height: 1;
|
|
133
|
+
max-height: 1;
|
|
134
|
+
overflow: hidden;
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
SearchResultRow:focus,
|
|
138
|
+
SearchResultRow.-highlighted {
|
|
139
|
+
background: #1a3a1a;
|
|
140
|
+
color: #4ade80;
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
#cmd-bar {
|
|
144
|
+
height: 3;
|
|
145
|
+
background: #0f0f0f;
|
|
146
|
+
border-top: solid #4ade80;
|
|
147
|
+
align: left middle;
|
|
148
|
+
padding: 0 1;
|
|
149
|
+
}
|
|
150
|
+
#help-hint { color: #888; }
|
|
151
|
+
"""
|
|
152
|
+
|
|
153
|
+
def __init__(
|
|
154
|
+
self,
|
|
155
|
+
client: JSTDataClient,
|
|
156
|
+
session: Session,
|
|
157
|
+
taxonomy: str | None = None,
|
|
158
|
+
resource_type: str | None = None,
|
|
159
|
+
relation: str | list[str] | None = None,
|
|
160
|
+
) -> None:
|
|
161
|
+
super().__init__()
|
|
162
|
+
self.client = client
|
|
163
|
+
self.session = session
|
|
164
|
+
self.taxonomy = taxonomy or None
|
|
165
|
+
self.taxonomy_name = taxonomy
|
|
166
|
+
self.resource_type = (resource_type or "").strip().lower() or None
|
|
167
|
+
self.relations = _normalize_relations(relation)
|
|
168
|
+
if self.resource_type not in (None, "entity", "metric", "series"):
|
|
169
|
+
self.resource_type = None
|
|
170
|
+
|
|
171
|
+
self.search_task: asyncio.Task[None] | None = None
|
|
172
|
+
self.catalog: list[Resource] = []
|
|
173
|
+
self.large_search_space = False
|
|
174
|
+
|
|
175
|
+
def _relation_label(self) -> str:
|
|
176
|
+
if not self.relations:
|
|
177
|
+
return ""
|
|
178
|
+
if len(self.relations) == 1:
|
|
179
|
+
return self.relations[0]
|
|
180
|
+
return f"{len(self.relations)} relations"
|
|
181
|
+
|
|
182
|
+
def compose(self) -> ComposeResult:
|
|
183
|
+
with Vertical(id="workspace-body"):
|
|
184
|
+
with Vertical(classes="pane-container", id="results-pane"):
|
|
185
|
+
yield Label(
|
|
186
|
+
"RESULTS // SEARCH",
|
|
187
|
+
classes="pane-header",
|
|
188
|
+
id="results-header",
|
|
189
|
+
)
|
|
190
|
+
yield Input(
|
|
191
|
+
placeholder="/ search catalog…",
|
|
192
|
+
id="search-input",
|
|
193
|
+
)
|
|
194
|
+
with Horizontal(classes="table-header"):
|
|
195
|
+
yield Label("NAME", classes="col-name")
|
|
196
|
+
yield Label("ID", classes="col-id")
|
|
197
|
+
yield Label("SRC", classes="col-src")
|
|
198
|
+
yield Label("TYPE", classes="col-type")
|
|
199
|
+
yield ListView(id="results-list")
|
|
200
|
+
with Horizontal(id="cmd-bar"):
|
|
201
|
+
yield Label("", id="help-hint")
|
|
202
|
+
|
|
203
|
+
def on_mount(self) -> None:
|
|
204
|
+
self._apply_search_chrome()
|
|
205
|
+
self._refresh_help_hint()
|
|
206
|
+
self.run_prefetch()
|
|
207
|
+
self.query_one("#results-list", ListView).focus()
|
|
208
|
+
|
|
209
|
+
def _apply_search_chrome(self) -> None:
|
|
210
|
+
tax = self.taxonomy_name or self.taxonomy or ""
|
|
211
|
+
rtype = self.resource_type.upper() if self.resource_type else ""
|
|
212
|
+
rel = self._relation_label()
|
|
213
|
+
if rel and rtype:
|
|
214
|
+
header = f"RESULTS // {rtype} // {rel}"
|
|
215
|
+
placeholder = f"/ search {rtype.lower()} linked via {rel}…"
|
|
216
|
+
elif rel:
|
|
217
|
+
header = f"RESULTS // ENTITY // {rel}"
|
|
218
|
+
placeholder = f"/ search entities linked via {rel}…"
|
|
219
|
+
elif tax and rtype:
|
|
220
|
+
header = f"RESULTS // {rtype} IN {tax}"
|
|
221
|
+
placeholder = f"/ search {rtype.lower()} in {tax}…"
|
|
222
|
+
elif tax:
|
|
223
|
+
header = f"RESULTS // TAXONOMY {tax}"
|
|
224
|
+
placeholder = f"/ search within {tax}…"
|
|
225
|
+
elif rtype:
|
|
226
|
+
header = f"RESULTS // {rtype}"
|
|
227
|
+
placeholder = f"/ search {rtype.lower()}…"
|
|
228
|
+
else:
|
|
229
|
+
header = "RESULTS // SEARCH"
|
|
230
|
+
placeholder = "/ search catalog…"
|
|
231
|
+
self.query_one("#results-header").update(header)
|
|
232
|
+
self.query_one("#search-input", Input).placeholder = placeholder
|
|
233
|
+
|
|
234
|
+
def _refresh_help_hint(self) -> None:
|
|
235
|
+
n = getattr(self.app, "step_count", 1)
|
|
236
|
+
i = getattr(self.app, "step_index", 0) + 1
|
|
237
|
+
step_id = getattr(self.app, "current_step_id", "console")
|
|
238
|
+
parts = [f"{step_id} {i}/{n}"]
|
|
239
|
+
if self.taxonomy:
|
|
240
|
+
parts.append(f"tax {self.taxonomy}")
|
|
241
|
+
if self.relations:
|
|
242
|
+
rel = self._relation_label()
|
|
243
|
+
if len(rel) > 28:
|
|
244
|
+
rel = rel[:25] + "…"
|
|
245
|
+
parts.append(f"rel {rel}")
|
|
246
|
+
if self.resource_type:
|
|
247
|
+
parts.append(self.resource_type)
|
|
248
|
+
if self.large_search_space:
|
|
249
|
+
parts.append("server search")
|
|
250
|
+
else:
|
|
251
|
+
parts.append("local")
|
|
252
|
+
parts.extend(
|
|
253
|
+
[
|
|
254
|
+
"[bold]/[/bold] search",
|
|
255
|
+
"[bold]enter[/bold] add",
|
|
256
|
+
"[bold]s[/bold] session",
|
|
257
|
+
"[bold]f[/bold] find",
|
|
258
|
+
"[bold]e[/bold] export",
|
|
259
|
+
"[bold]n[/bold]/[bold]p[/bold]",
|
|
260
|
+
"[bold]q[/bold] quit",
|
|
261
|
+
"[bold]?[/bold]",
|
|
262
|
+
]
|
|
263
|
+
)
|
|
264
|
+
self.query_one("#help-hint").update(" // ".join(parts))
|
|
265
|
+
|
|
266
|
+
def _show_rows(self, resources: list[Resource]) -> None:
|
|
267
|
+
list_view = self.query_one("#results-list", ListView)
|
|
268
|
+
list_view.clear()
|
|
269
|
+
for resource in resources:
|
|
270
|
+
list_view.append(SearchResultRow(resource))
|
|
271
|
+
|
|
272
|
+
def _filter_catalog(self, query: str) -> list[Resource]:
|
|
273
|
+
q = query.lower().strip()
|
|
274
|
+
if not q:
|
|
275
|
+
return list(self.catalog)
|
|
276
|
+
hits: list[Resource] = []
|
|
277
|
+
for resource in self.catalog:
|
|
278
|
+
name = (
|
|
279
|
+
getattr(resource, "label", None)
|
|
280
|
+
or getattr(resource, "name", None)
|
|
281
|
+
or ""
|
|
282
|
+
)
|
|
283
|
+
if q in name.lower() or q in resource.id.lower():
|
|
284
|
+
hits.append(resource)
|
|
285
|
+
return hits
|
|
286
|
+
|
|
287
|
+
@work(exclusive=True, group="prefetch")
|
|
288
|
+
async def run_prefetch(self) -> None:
|
|
289
|
+
"""Load a first page so the pane is never empty; note if the set is huge."""
|
|
290
|
+
try:
|
|
291
|
+
entities, metrics, series = await self._fetch_catalog()
|
|
292
|
+
except Exception as e:
|
|
293
|
+
self.notify(f"Could not load catalog: {e}", severity="error")
|
|
294
|
+
return
|
|
295
|
+
self.catalog = [*metrics, *entities, *series]
|
|
296
|
+
self.large_search_space = (
|
|
297
|
+
len(entities) > LOCAL_CAP
|
|
298
|
+
or len(metrics) > LOCAL_CAP
|
|
299
|
+
or len(series) > LOCAL_CAP
|
|
300
|
+
)
|
|
301
|
+
self._show_rows(self.catalog)
|
|
302
|
+
self._refresh_help_hint()
|
|
303
|
+
|
|
304
|
+
async def _fetch_catalog(
|
|
305
|
+
self,
|
|
306
|
+
) -> tuple[list[Entity], list[Metric], list[Series]]:
|
|
307
|
+
rtype = self.resource_type
|
|
308
|
+
tax = self.taxonomy
|
|
309
|
+
rel = self.relations
|
|
310
|
+
if rtype == "entity":
|
|
311
|
+
entities = await asyncio.to_thread(
|
|
312
|
+
self.client.search_entities,
|
|
313
|
+
None,
|
|
314
|
+
taxonomy=tax,
|
|
315
|
+
relation=rel,
|
|
316
|
+
limit=PREFETCH_LIMIT,
|
|
317
|
+
)
|
|
318
|
+
return list(entities), [], []
|
|
319
|
+
if rtype == "metric":
|
|
320
|
+
metrics = await asyncio.to_thread(
|
|
321
|
+
self.client.search_metrics,
|
|
322
|
+
None,
|
|
323
|
+
taxonomy=tax,
|
|
324
|
+
limit=PREFETCH_LIMIT,
|
|
325
|
+
)
|
|
326
|
+
return [], list(metrics), []
|
|
327
|
+
if rtype == "series":
|
|
328
|
+
series = await asyncio.to_thread(
|
|
329
|
+
self.client.list_series, PREFETCH_LIMIT
|
|
330
|
+
)
|
|
331
|
+
return [], [], list(series)
|
|
332
|
+
if rel:
|
|
333
|
+
entities = await asyncio.to_thread(
|
|
334
|
+
self.client.search_entities,
|
|
335
|
+
None,
|
|
336
|
+
taxonomy=tax,
|
|
337
|
+
relation=rel,
|
|
338
|
+
limit=PREFETCH_LIMIT,
|
|
339
|
+
)
|
|
340
|
+
if tax:
|
|
341
|
+
metrics = await asyncio.to_thread(
|
|
342
|
+
self.client.get_taxonomy_metrics, tax, PREFETCH_LIMIT
|
|
343
|
+
)
|
|
344
|
+
return list(entities), list(metrics), []
|
|
345
|
+
return list(entities), [], []
|
|
346
|
+
if tax:
|
|
347
|
+
entities, metrics = await asyncio.gather(
|
|
348
|
+
asyncio.to_thread(
|
|
349
|
+
self.client.get_taxonomy_entities, tax, PREFETCH_LIMIT
|
|
350
|
+
),
|
|
351
|
+
asyncio.to_thread(
|
|
352
|
+
self.client.get_taxonomy_metrics, tax, PREFETCH_LIMIT
|
|
353
|
+
),
|
|
354
|
+
)
|
|
355
|
+
return list(entities), list(metrics), []
|
|
356
|
+
entities, metrics, series = await asyncio.gather(
|
|
357
|
+
asyncio.to_thread(
|
|
358
|
+
self.client.search_entities, None, limit=PREFETCH_LIMIT
|
|
359
|
+
),
|
|
360
|
+
asyncio.to_thread(
|
|
361
|
+
self.client.search_metrics, None, limit=PREFETCH_LIMIT
|
|
362
|
+
),
|
|
363
|
+
asyncio.to_thread(self.client.list_series, PREFETCH_LIMIT),
|
|
364
|
+
)
|
|
365
|
+
return list(entities), list(metrics), list(series)
|
|
366
|
+
|
|
367
|
+
def action_focus_filter(self) -> None:
|
|
368
|
+
self.query_one("#search-input", Input).focus()
|
|
369
|
+
|
|
370
|
+
def action_leave_filter(self) -> None:
|
|
371
|
+
if isinstance(self.focused, Input):
|
|
372
|
+
self.query_one("#results-list", ListView).focus()
|
|
373
|
+
|
|
374
|
+
def action_cursor_down(self) -> None:
|
|
375
|
+
focused = self.focused
|
|
376
|
+
if isinstance(focused, Input):
|
|
377
|
+
return
|
|
378
|
+
if focused and hasattr(focused, "action_cursor_down"):
|
|
379
|
+
focused.action_cursor_down()
|
|
380
|
+
|
|
381
|
+
def action_cursor_up(self) -> None:
|
|
382
|
+
focused = self.focused
|
|
383
|
+
if isinstance(focused, Input):
|
|
384
|
+
return
|
|
385
|
+
if focused and hasattr(focused, "action_cursor_up"):
|
|
386
|
+
focused.action_cursor_up()
|
|
387
|
+
|
|
388
|
+
@on(Input.Changed, "#search-input")
|
|
389
|
+
def on_search_changed(self, event: Input.Changed) -> None:
|
|
390
|
+
if self.search_task:
|
|
391
|
+
self.search_task.cancel()
|
|
392
|
+
query = event.value.strip()
|
|
393
|
+
if not query:
|
|
394
|
+
self._show_rows(self.catalog)
|
|
395
|
+
return
|
|
396
|
+
if self.large_search_space:
|
|
397
|
+
if len(query) < 2:
|
|
398
|
+
self._show_rows(self._filter_catalog(query))
|
|
399
|
+
return
|
|
400
|
+
self.search_task = asyncio.create_task(self._do_server_search(query))
|
|
401
|
+
return
|
|
402
|
+
self._show_rows(self._filter_catalog(query))
|
|
403
|
+
|
|
404
|
+
async def _do_server_search(self, query: str) -> None:
|
|
405
|
+
try:
|
|
406
|
+
await asyncio.sleep(0.3)
|
|
407
|
+
results = await self._search_remote(query)
|
|
408
|
+
self._show_rows(results)
|
|
409
|
+
except asyncio.CancelledError:
|
|
410
|
+
pass
|
|
411
|
+
except Exception as e:
|
|
412
|
+
self.notify(f"Search error: {e}", severity="error")
|
|
413
|
+
|
|
414
|
+
async def _search_remote(self, query: str) -> list[Resource]:
|
|
415
|
+
tax = self.taxonomy
|
|
416
|
+
rel = self.relations
|
|
417
|
+
rtype = self.resource_type
|
|
418
|
+
if rtype == "entity":
|
|
419
|
+
return list(
|
|
420
|
+
await asyncio.to_thread(
|
|
421
|
+
self.client.search_entities,
|
|
422
|
+
query,
|
|
423
|
+
taxonomy=tax,
|
|
424
|
+
relation=rel,
|
|
425
|
+
limit=20,
|
|
426
|
+
)
|
|
427
|
+
)
|
|
428
|
+
if rtype == "metric":
|
|
429
|
+
return list(
|
|
430
|
+
await asyncio.to_thread(
|
|
431
|
+
self.client.search_metrics, query, taxonomy=tax, limit=20
|
|
432
|
+
)
|
|
433
|
+
)
|
|
434
|
+
if rtype == "series":
|
|
435
|
+
return list(
|
|
436
|
+
await asyncio.to_thread(self.client.search_series, query, limit=20)
|
|
437
|
+
)
|
|
438
|
+
if rel:
|
|
439
|
+
entities = list(
|
|
440
|
+
await asyncio.to_thread(
|
|
441
|
+
self.client.search_entities,
|
|
442
|
+
query,
|
|
443
|
+
taxonomy=tax,
|
|
444
|
+
relation=rel,
|
|
445
|
+
limit=20,
|
|
446
|
+
)
|
|
447
|
+
)
|
|
448
|
+
if tax:
|
|
449
|
+
metrics = list(
|
|
450
|
+
await asyncio.to_thread(
|
|
451
|
+
self.client.search_metrics, query, taxonomy=tax, limit=15
|
|
452
|
+
)
|
|
453
|
+
)
|
|
454
|
+
return [*metrics, *entities]
|
|
455
|
+
return entities
|
|
456
|
+
if tax:
|
|
457
|
+
entities, metrics = await asyncio.gather(
|
|
458
|
+
asyncio.to_thread(
|
|
459
|
+
self.client.search_entities, query, taxonomy=tax, limit=15
|
|
460
|
+
),
|
|
461
|
+
asyncio.to_thread(
|
|
462
|
+
self.client.search_metrics, query, taxonomy=tax, limit=15
|
|
463
|
+
),
|
|
464
|
+
)
|
|
465
|
+
return [*metrics, *entities]
|
|
466
|
+
return list(await asyncio.to_thread(self.client.search, query, limit=20))
|
|
467
|
+
|
|
468
|
+
@on(Input.Submitted, "#search-input")
|
|
469
|
+
def on_search_submit(self) -> None:
|
|
470
|
+
self.query_one("#results-list", ListView).focus()
|
|
471
|
+
|
|
472
|
+
@on(ListView.Selected, "#results-list")
|
|
473
|
+
def add_to_session(self, event: ListView.Selected) -> None:
|
|
474
|
+
resource = event.item.resource
|
|
475
|
+
if not self._add_resource_to_session(resource):
|
|
476
|
+
self.notify(f"{resource.id} is already in session", severity="warning")
|
|
477
|
+
return
|
|
478
|
+
self.notify("Added to session")
|
|
479
|
+
|
|
480
|
+
def _resource_label(self, resource: Resource) -> str:
|
|
481
|
+
return (
|
|
482
|
+
getattr(resource, "label", None)
|
|
483
|
+
or getattr(resource, "name", None)
|
|
484
|
+
or resource.id
|
|
485
|
+
)
|
|
486
|
+
|
|
487
|
+
def _add_resource_to_session(self, resource: Resource) -> bool:
|
|
488
|
+
if isinstance(resource, Metric):
|
|
489
|
+
added = self.session.add_metric(resource.id)
|
|
490
|
+
elif isinstance(resource, Entity):
|
|
491
|
+
added = self.session.add_entity(resource.id)
|
|
492
|
+
elif isinstance(resource, Series):
|
|
493
|
+
added = self.session.add_series(resource.id)
|
|
494
|
+
elif resource.id in self.session.resource_ids():
|
|
495
|
+
return False
|
|
496
|
+
else:
|
|
497
|
+
added = self.session.add_series(resource.id)
|
|
498
|
+
if added:
|
|
499
|
+
self.app.remember_label(resource.id, self._resource_label(resource))
|
|
500
|
+
return added
|
|
501
|
+
|
|
502
|
+
|
|
503
|
+
def create_console_screen(
|
|
504
|
+
client, session, taxonomy=None, resource_type=None, relation=None, **kwargs
|
|
505
|
+
):
|
|
506
|
+
return WorkspaceScreen(
|
|
507
|
+
client,
|
|
508
|
+
session,
|
|
509
|
+
taxonomy=taxonomy,
|
|
510
|
+
resource_type=resource_type,
|
|
511
|
+
relation=relation,
|
|
512
|
+
)
|
|
513
|
+
|
|
514
|
+
|
|
515
|
+
CONSOLE = register(
|
|
516
|
+
StepSpec(
|
|
517
|
+
id="console",
|
|
518
|
+
name="Console",
|
|
519
|
+
description=(
|
|
520
|
+
"Search metrics, entities, and series, then stage them into the session."
|
|
521
|
+
),
|
|
522
|
+
create_screen=create_console_screen,
|
|
523
|
+
arguments=(
|
|
524
|
+
StepArgument(
|
|
525
|
+
name="taxonomy",
|
|
526
|
+
type="string",
|
|
527
|
+
description=(
|
|
528
|
+
"Restrict search to entities and metrics in this taxonomy "
|
|
529
|
+
"(slug, e.g. sec-central-index-key)"
|
|
530
|
+
),
|
|
531
|
+
),
|
|
532
|
+
StepArgument(
|
|
533
|
+
name="relation",
|
|
534
|
+
type="string",
|
|
535
|
+
multiple=True,
|
|
536
|
+
description=(
|
|
537
|
+
"Restrict entity search to those linked to an anchor via a typed "
|
|
538
|
+
"relationship (<relationship_type>:<to_entity_id>, "
|
|
539
|
+
"e.g. classified_as:sic:3674). Repeatable; OR'd."
|
|
540
|
+
),
|
|
541
|
+
),
|
|
542
|
+
StepArgument(
|
|
543
|
+
name="resource_type",
|
|
544
|
+
type="string",
|
|
545
|
+
description="Restrict search to series, metric, or entity",
|
|
546
|
+
choices=("series", "metric", "entity"),
|
|
547
|
+
),
|
|
548
|
+
),
|
|
549
|
+
bindings=(
|
|
550
|
+
StepBinding("/", "focus_filter", "Search the catalog"),
|
|
551
|
+
StepBinding("enter", "submit", "Focus results / add highlighted item"),
|
|
552
|
+
StepBinding("j / ↓", "cursor_down", "Move highlight down"),
|
|
553
|
+
StepBinding("k / ↑", "cursor_up", "Move highlight up"),
|
|
554
|
+
StepBinding("escape", "leave_filter", "Leave the search field"),
|
|
555
|
+
),
|
|
556
|
+
example=(
|
|
557
|
+
"jst run console --relation classified_as:sic:3674 --resource-type entity"
|
|
558
|
+
),
|
|
559
|
+
)
|
|
560
|
+
)
|