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 ADDED
@@ -0,0 +1,13 @@
1
+ from .client import JSTDataClient
2
+ from .models import Entity, Metric, Series, Observation, Taxonomy
3
+ from .session import Session
4
+
5
+ __all__ = [
6
+ "JSTDataClient",
7
+ "Entity",
8
+ "Metric",
9
+ "Series",
10
+ "Observation",
11
+ "Taxonomy",
12
+ "Session",
13
+ ]
jstdata/agent_guide.py ADDED
@@ -0,0 +1,530 @@
1
+ """Generate the markdown agent bootstrap guide (`jst agent-guide`).
2
+
3
+ CLI commands, interactive steps, and session field names are derived from the
4
+ live Click tree, step registry, and ``Session`` dataclass. Conceptual glue
5
+ (policy, modes, recipes, data model, sessions) is static prose that changes rarely.
6
+ """
7
+
8
+ from __future__ import annotations
9
+
10
+ import re
11
+ from dataclasses import fields
12
+ from importlib.metadata import PackageNotFoundError, version
13
+ from typing import Iterable, get_args, get_origin
14
+
15
+ import click
16
+
17
+ from .session import Session
18
+ from .workflows import list_steps
19
+
20
+ _PACKAGE = "jstdata"
21
+
22
+ # Session fields that expand the action space without helping typical agent work.
23
+ _SESSION_FIELDS_OMIT = frozenset(
24
+ {"start_date", "end_date", "start_time", "end_time"}
25
+ )
26
+
27
+ _HARD_RULES = """\
28
+ ## Hard rules
29
+
30
+ - **Never invent IDs.** Never invent metric, entity, taxonomy, or series ids from
31
+ English names. Resolve via search (or scoped list) first.
32
+ - **Resolve before use.** Resolve ids before `jst query`, session JSON, or workflow args.
33
+ - **Use `--format json`** on resource and search commands unless the user needs a table.
34
+ - **Do not launch interactive TUIs** (`jst run`, `jst workflow run`) unless the user
35
+ explicitly asks you to operate the UI. Prepare a Session/workflow and give them the
36
+ launch command.
37
+ - **Do not paginate exhaustively.** During discovery, use `--limit` 20–50. Do not walk
38
+ `offset` through the catalog unless the returned set is clearly insufficient.
39
+ - **Do not enumerate** `jst metric ls`, `jst series ls`, or full taxonomy populations
40
+ unless the task requires enumeration. Prefer `search` with a query (and taxonomy /
41
+ relation filters when relevant).
42
+ - **Do not retrieve deep history** until the candidate universe is narrowed. Use
43
+ `tail=1` (or similarly shallow queries) for triage.
44
+ - **Do not create a workflow** when direct CLI analysis satisfies the request. Use
45
+ workflows for human handoff of an interactive investigation.
46
+ - **Stop when good enough.** Prefer a partial useful result over open-ended search.
47
+ """
48
+
49
+ _OPERATING_POLICY = """\
50
+ ## Default operating policy
51
+
52
+ When solving a JST research task:
53
+
54
+ 1. **Use metadata before observations.** Resolve entities, metrics, and taxonomies
55
+ before querying values.
56
+ 2. **Start narrow.** Use the user's literal terminology first, then a small number of
57
+ obvious synonyms.
58
+ 3. **Prefer bounded calls.** Small result limits during discovery; shallow `tail` /
59
+ `head` when values are needed only to triage.
60
+ 4. **Prefer good-enough candidate sets** over exhaustive search unless the user asks
61
+ for comprehensiveness.
62
+ 5. **Escalate to deep history only after narrowing** (specific series ids, date bounds).
63
+ 6. **Hand off TUIs to the human.** Prepare Session JSON and/or a saved workflow; print
64
+ the `jst workflow run … --session …` (or `jst run …`) command for them.
65
+ 7. **Prefer JSON** for anything you will parse or reason over.
66
+ """
67
+
68
+ _OPERATING_MODES = """\
69
+ ## Choose an operating mode
70
+
71
+ ### Catalog discovery
72
+
73
+ Example: “Find metrics relevant to defense spending.”
74
+
75
+ Use search / show. Do **not** retrieve observations unless needed to disambiguate
76
+ candidates. Select a useful set and stop.
77
+
78
+ ### Agent analysis
79
+
80
+ Example: “Compare military expenditure as a share of GDP across NATO countries.”
81
+
82
+ Resolve ids → bounded `jst query` → deeper `series observations` only if needed →
83
+ answer from structured CLI output. No TUI or workflow required.
84
+
85
+ ### Human handoff
86
+
87
+ Example: “Set me up to explore defense spending across Europe.”
88
+
89
+ Resolve resources → write Session JSON → compose/save a workflow when useful → hand
90
+ the user the launch command. Do not substitute extra research for completing the
91
+ handoff.
92
+ """
93
+
94
+ _DATA_MODEL = """\
95
+ ## Data model
96
+
97
+ - **Metric** — a measurable theme (e.g. GDP, employed-persons). Identified by a slug id.
98
+ - **Entity** — a context the metric is measured in (e.g. a country, a company). Slug id.
99
+ - **Taxonomy** — a named population of entities (e.g. `country`, `us-county`,
100
+ `sec-central-index-key`). Use it to scope search, query, and ranking to that population.
101
+ - **Series** — one concrete time series: a metric observed for an entity (at a frequency).
102
+ - **Session** — portable analytical intent (resource ids + query filters). See **Sessions**
103
+ below. No observations live in a session file.
104
+
105
+ Ids are opaque slugs. Display names are not ids.
106
+ """
107
+
108
+ _RESOLUTION = """\
109
+ ## Resolution
110
+
111
+ Never invent metric, entity, taxonomy, or series ids from English names.
112
+
113
+ Always resolve via search (prefer a query string) before writing ids into a workflow,
114
+ `--session` JSON, or `jst query` flags. Scope with `--taxonomy` / `--relation` when the
115
+ user names a population or relationship. Confirm ambiguous hits with `show` before use.
116
+ """
117
+
118
+ _RECIPES = """\
119
+ ## Common recipes
120
+
121
+ ### Find relevant metrics
122
+
123
+ ```bash
124
+ jst metric search "defense spending" --taxonomy country --limit 20 --format json
125
+ ```
126
+
127
+ If insufficient, try one or two synonyms the same way. Inspect only promising ids:
128
+
129
+ ```bash
130
+ jst metric show <resolved-id> --format json
131
+ ```
132
+
133
+ Stop once a useful candidate set exists. Do not page the full catalog.
134
+
135
+ ### Compare latest values
136
+
137
+ ```bash
138
+ jst query \\
139
+ --metric <metric-id> \\
140
+ --taxonomy country \\
141
+ --tail 1 \\
142
+ --sort-by value \\
143
+ --limit 50 \\
144
+ --format json
145
+ ```
146
+
147
+ ### Retrieve history after narrowing
148
+
149
+ ```bash
150
+ jst metric series <metric-id> --format json
151
+ jst series observations <series-id> \\
152
+ --start-date 2015-01-01 \\
153
+ --format json
154
+ ```
155
+
156
+ ### Prepare a human investigation
157
+
158
+ 1. Resolve metric/entity/taxonomy ids via search.
159
+ 2. Write a Session JSON with those ids and filters (`taxonomy`, `tail`, …).
160
+ 3. Create or reuse a workflow if the human needs a TUI pipeline.
161
+ 4. Hand them a launch command, for example:
162
+
163
+ ```bash
164
+ jst workflow run eu-analysis --session defense.json
165
+ ```
166
+
167
+ Do not substitute extra research for completing the requested handoff.
168
+ """
169
+
170
+ _COMPOSITION = """\
171
+ ## Composition and workflows
172
+
173
+ Interactive investigation is composed in the shell, not a custom DSL:
174
+
175
+ ```bash
176
+ jst run STEP [ARGS...] : STEP [ARGS...] : ...
177
+ ```
178
+
179
+ - **Steps** are TUI screens for the **human**. They edit one shared session for the run.
180
+ - **Step args** seed that step; they are not the session.
181
+ - **Saved workflows** (`jst workflow create` / `run`) persist step topology + step args
182
+ only. Bake discovered metrics/entities into a **session JSON** and pass `--session`
183
+ when running (see **Sessions**).
184
+ - Pipeline tokens for `workflow create` must follow a `--` boundary, e.g.
185
+ `jst workflow create --id gdp-rank -- console : rank --taxonomy country`.
186
+
187
+ `jst run` and `jst workflow run` launch interactive TUIs. Agents should normally prepare
188
+ the Session/workflow and provide the launch command rather than controlling the TUI.
189
+ For step-specific help, run `jst step <id>` (or `jst step <id> --json`) on demand.
190
+ """
191
+
192
+ _SESSION_HOWTO = """\
193
+ ### Create and modify
194
+
195
+ **Write a JSON file** (usual agent path after resolving ids):
196
+
197
+ ```json
198
+ {
199
+ "metric": ["employed-persons", "unemployment-rate"],
200
+ "taxonomy": "country",
201
+ "tail": 1,
202
+ "sort_by": "value"
203
+ }
204
+ ```
205
+
206
+ Omit unused fields. Empty lists may be omitted. Ids must already be resolved.
207
+
208
+ **Python API** (same shape as the JSON file):
209
+
210
+ ```python
211
+ from jstdata import Session
212
+
213
+ session = Session(
214
+ metric=["employed-persons", "unemployment-rate"],
215
+ taxonomy="country",
216
+ tail=1,
217
+ sort_by="value",
218
+ )
219
+ session.add_metric("gdp") # no-op if already present
220
+ session.add_entity("united-states")
221
+ session.remove_id("gdp") # removes from metric/entity/series
222
+ session.save("labor.json") # write JSON
223
+ session = Session.load("labor.json")
224
+ ```
225
+
226
+ Edit the JSON by hand or re-save from Python; there is no separate session CLI.
227
+
228
+ ### Use with steps and workflows
229
+
230
+ Preload into a pipeline or saved workflow:
231
+
232
+ ```bash
233
+ jst run --session labor.json rank --taxonomy country
234
+ jst workflow run gdp-rank --session labor.json
235
+ ```
236
+
237
+ `--session` copies the file into the live session at startup. The human can still
238
+ change it in the TUI. Typical agent pattern after metric discovery:
239
+
240
+ 1. Resolve metric (and taxonomy) ids via search.
241
+ 2. Write a session JSON with those `metric` ids (and optional filters).
242
+ 3. Create or reuse a workflow whose steps consume session metrics (e.g. `rank`).
243
+ 4. Hand the user: `jst workflow run <id> --session <file>.json`.
244
+ """
245
+
246
+
247
+ def package_version() -> str:
248
+ try:
249
+ return version(_PACKAGE)
250
+ except PackageNotFoundError:
251
+ return "unknown"
252
+
253
+
254
+ def _first_line(text: str | None) -> str:
255
+ if not text:
256
+ return ""
257
+ line = text.strip().splitlines()[0].strip()
258
+ # Click often embeds ``\\b`` blocks; collapse leftover whitespace.
259
+ return re.sub(r"\s+", " ", line)
260
+
261
+
262
+ def _md_cell(text: str) -> str:
263
+ return text.replace("|", "\\|").replace("\n", " ").strip()
264
+
265
+
266
+ def _param_token(param: click.Parameter) -> str | None:
267
+ """Compact signature fragment for one Click parameter."""
268
+ if getattr(param, "hidden", False):
269
+ return None
270
+ if isinstance(param, click.Argument):
271
+ name = (param.metavar or param.name or "ARG").upper()
272
+ if param.nargs == -1:
273
+ name = f"{name}..."
274
+ return name if param.required else f"[{name}]"
275
+ if isinstance(param, click.Option):
276
+ # Prefer the long form.
277
+ opt = next((o for o in param.opts if o.startswith("--")), None)
278
+ if opt is None:
279
+ opt = param.opts[0] if param.opts else f"--{param.name}"
280
+ if param.is_flag or param.count:
281
+ return opt if param.required else f"[{opt}]"
282
+ meta = (param.metavar or param.name or "VALUE").upper()
283
+ token = f"{opt} {meta}"
284
+ return token if param.required else f"[{token}]"
285
+ return None
286
+
287
+
288
+ def format_command_params(cmd: click.Command) -> str:
289
+ parts: list[str] = []
290
+ for param in cmd.params:
291
+ token = _param_token(param)
292
+ if token:
293
+ parts.append(token)
294
+ return " ".join(parts)
295
+
296
+
297
+ def iter_leaf_commands(
298
+ root: click.Group,
299
+ *,
300
+ prog: str = "jst",
301
+ ) -> Iterable[tuple[str, click.Command]]:
302
+ """Yield ``(invocation, command)`` for every leaf command under ``root``."""
303
+
304
+ def walk(cmd: click.Command, parts: list[str], ctx: click.Context) -> Iterable[tuple[str, click.Command]]:
305
+ if isinstance(cmd, click.Group):
306
+ seen: set[int] = set()
307
+ for name in sorted(cmd.list_commands(ctx)):
308
+ sub = cmd.get_command(ctx, name)
309
+ if sub is None:
310
+ continue
311
+ # Skip Click aliases that point at the same command object.
312
+ sub_id = id(sub)
313
+ if sub_id in seen:
314
+ continue
315
+ seen.add(sub_id)
316
+ sub_ctx = click.Context(sub, info_name=name, parent=ctx)
317
+ yield from walk(sub, parts + [name], sub_ctx)
318
+ return
319
+ invocation = " ".join(parts)
320
+ yield invocation, cmd
321
+
322
+ root_ctx = click.Context(root, info_name=prog)
323
+ yield from walk(root, [prog], root_ctx)
324
+
325
+
326
+ def render_commands_table(root: click.Group) -> str:
327
+ lines = [
328
+ "| Command | Description | Parameters |",
329
+ "|---------|-------------|------------|",
330
+ ]
331
+ for invocation, cmd in iter_leaf_commands(root):
332
+ desc = _first_line(cmd.help) or _first_line(cmd.short_help) or ""
333
+ params = format_command_params(cmd)
334
+ lines.append(
335
+ f"| `{_md_cell(invocation)}` | {_md_cell(desc)} | {_md_cell(params)} |"
336
+ )
337
+ return "\n".join(lines)
338
+
339
+
340
+ def _format_step_args(spec) -> str:
341
+ if not spec.arguments:
342
+ return "_(none)_"
343
+ bits: list[str] = []
344
+ for arg in spec.arguments:
345
+ flag = arg.flag()
346
+ piece = f"`{flag}`"
347
+ if arg.multiple:
348
+ piece += " (repeatable)"
349
+ if arg.required:
350
+ piece += " (required)"
351
+ elif arg.default is not None:
352
+ piece += f" [default: `{arg.default}`]"
353
+ if arg.choices:
354
+ choices = ", ".join(f"`{c}`" for c in arg.choices)
355
+ piece += f" {{{choices}}}"
356
+ bits.append(piece)
357
+ return ", ".join(bits)
358
+
359
+
360
+ def _annotation_label(annotation: object) -> str:
361
+ """Human-readable type for a Session field annotation."""
362
+ origin = get_origin(annotation)
363
+ if origin is list:
364
+ args = get_args(annotation)
365
+ inner = _annotation_label(args[0]) if args else "any"
366
+ return f"list[{inner}]"
367
+ if origin is not None:
368
+ # Optional[T] / Union[T, None]
369
+ args = [a for a in get_args(annotation) if a is not type(None)]
370
+ if len(args) == 1:
371
+ return f"{_annotation_label(args[0])} | null"
372
+ if annotation is str:
373
+ return "string"
374
+ if annotation is int:
375
+ return "integer"
376
+ if isinstance(annotation, type):
377
+ return annotation.__name__
378
+ text = str(annotation)
379
+ return text.replace("typing.", "").replace("None", "null")
380
+
381
+
382
+ _SESSION_FIELD_NOTES: dict[str, str] = {
383
+ "metric": "Resolved metric slug ids staged for the investigation",
384
+ "entity": "Resolved entity slug ids",
385
+ "series": "Resolved series slug ids (when targeting series directly)",
386
+ "frequency": "Optional frequency filter (Annual, Quarterly, Monthly, Daily, Intraday)",
387
+ "taxonomy": "Restrict analysis to entities in this population (taxonomy slug)",
388
+ "head": "Earliest N observations per series for `/query`",
389
+ "tail": "Latest N observations per series for `/query`",
390
+ "as_of": "Timezone-aware ISO-8601 cutoff on release_timestamp",
391
+ "sort_by": "`id` (default) or `value` (desc) for `/query` ordering",
392
+ "order_by": "Observation order preference where applicable (`asc`/`desc`)",
393
+ }
394
+
395
+
396
+ def render_session_section() -> str:
397
+ """Sessions how-to plus a field table derived from ``Session``."""
398
+ lines = [
399
+ "## Sessions",
400
+ "",
401
+ "A **session** is the portable bag of resource ids and filters that steps share.",
402
+ "It mirrors `jst query` / `JSTDataClient.query` intent: no observations, only what",
403
+ "is needed to (re)run a query or drive a TUI pipeline.",
404
+ "",
405
+ "Saved workflows do **not** store session contents. After discovering metrics or",
406
+ "entities, write them into a session JSON and pass `--session` on `jst run` or",
407
+ "`jst workflow run`. Steps such as `rank` (metrics in session) and `discover`",
408
+ "(entities in session) read that staged state.",
409
+ "",
410
+ "### Session JSON fields",
411
+ "",
412
+ "Primary fields for agent-written sessions (from the installed `Session` model).",
413
+ "Prefer `head` / `tail` / `as_of` for query windows. Omit unused fields.",
414
+ "",
415
+ "| Field | Type | Notes |",
416
+ "|-------|------|-------|",
417
+ ]
418
+ for f in fields(Session):
419
+ if f.name in _SESSION_FIELDS_OMIT:
420
+ continue
421
+ note = _SESSION_FIELD_NOTES.get(f.name, "")
422
+ lines.append(
423
+ f"| `{_md_cell(f.name)}` | {_md_cell(_annotation_label(f.type))} | {_md_cell(note)} |"
424
+ )
425
+ lines.append("")
426
+ lines.append(_SESSION_HOWTO.rstrip())
427
+ return "\n".join(lines)
428
+
429
+
430
+ def render_steps_section() -> str:
431
+ specs = list_steps()
432
+ lines = [
433
+ "## Interactive steps",
434
+ "",
435
+ "Atoms for `jst run` / saved workflows (human-facing TUIs). Order-agnostic: any",
436
+ "step accepts an empty or arbitrary session. Do not operate these UIs yourself;",
437
+ "use `jst step <id>` if you need details while helping a human.",
438
+ "",
439
+ "| Id | Name | Description | Arguments | Example |",
440
+ "|----|------|-------------|-----------|---------|",
441
+ ]
442
+ if not specs:
443
+ lines.append("| _(none registered)_ | | | | |")
444
+ return "\n".join(lines)
445
+
446
+ for spec in specs:
447
+ lines.append(
448
+ "| "
449
+ + " | ".join(
450
+ [
451
+ f"`{_md_cell(spec.id)}`",
452
+ _md_cell(spec.name),
453
+ _md_cell(spec.description),
454
+ _md_cell(_format_step_args(spec)),
455
+ f"`{_md_cell(spec.example or f'jst run {spec.id}')}`",
456
+ ]
457
+ )
458
+ + " |"
459
+ )
460
+
461
+ lines.append("")
462
+ lines.append("### Step details")
463
+ for spec in specs:
464
+ lines.append("")
465
+ lines.append(f"#### `{spec.id}` — {spec.name}")
466
+ lines.append("")
467
+ lines.append(spec.description.strip())
468
+ lines.append("")
469
+ if spec.arguments:
470
+ lines.append("Arguments:")
471
+ lines.append("")
472
+ for arg in spec.arguments:
473
+ req = "required" if arg.required else "optional"
474
+ default = (
475
+ f", default `{arg.default}`" if arg.default is not None else ""
476
+ )
477
+ multi = ", repeatable" if arg.multiple else ""
478
+ lines.append(
479
+ f"- `{arg.flag()}` ({arg.type}, {req}{multi}{default}): "
480
+ f"{arg.description}"
481
+ )
482
+ if arg.choices:
483
+ lines.append(
484
+ f" - Choices: {', '.join(f'`{c}`' for c in arg.choices)}"
485
+ )
486
+ lines.append("")
487
+ example = spec.example or f"jst run {spec.id}"
488
+ lines.append(f"Example: `{example}`")
489
+ return "\n".join(lines)
490
+
491
+
492
+ def render_agent_guide(cli_group: click.Group) -> str:
493
+ """Build the full markdown guide for the given Click root group."""
494
+ ver = package_version()
495
+ sections = [
496
+ f"# jstdata agent guide (v{ver})",
497
+ "",
498
+ "Operating manual for agents helping a user with this installed CLI. "
499
+ "Prefer this document over guessing command shapes or resource ids. "
500
+ "Follow **Hard rules** and **Choose an operating mode** before browsing the "
501
+ "full command tables.",
502
+ "",
503
+ _HARD_RULES.rstrip(),
504
+ "",
505
+ _OPERATING_POLICY.rstrip(),
506
+ "",
507
+ _OPERATING_MODES.rstrip(),
508
+ "",
509
+ _DATA_MODEL.rstrip(),
510
+ "",
511
+ _RESOLUTION.rstrip(),
512
+ "",
513
+ _RECIPES.rstrip(),
514
+ "",
515
+ _COMPOSITION.rstrip(),
516
+ "",
517
+ render_session_section(),
518
+ "",
519
+ render_steps_section(),
520
+ "",
521
+ "## CLI commands",
522
+ "",
523
+ "One row per leaf command, derived from the installed CLI. Prefer the recipes "
524
+ "and hard rules above; use this table to confirm flags, not as a checklist to "
525
+ "execute.",
526
+ "",
527
+ render_commands_table(cli_group),
528
+ "",
529
+ ]
530
+ return "\n".join(sections)