okstra 0.30.3 → 0.32.0
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.
- package/docs/kr/architecture.md +2 -2
- package/docs/kr/cli.md +2 -2
- package/package.json +1 -1
- package/runtime/BUILD.json +2 -2
- package/runtime/agents/SKILL.md +7 -5
- package/runtime/agents/workers/claude-worker.md +1 -1
- package/runtime/agents/workers/codex-worker.md +23 -6
- package/runtime/agents/workers/gemini-worker.md +23 -6
- package/runtime/agents/workers/report-writer-worker.md +45 -66
- package/runtime/bin/okstra-codex-exec.sh +31 -0
- package/runtime/bin/okstra-gemini-exec.sh +26 -0
- package/runtime/bin/okstra-render-final-report.py +101 -0
- package/runtime/bin/okstra-render-report-views.py +17 -10
- package/runtime/bin/okstra-token-usage.py +3 -1
- package/runtime/python/lib/okstra/globals.sh +1 -1
- package/runtime/python/lib/okstra/usage.sh +2 -2
- package/runtime/python/okstra_ctl/final_report_schema.py +253 -0
- package/runtime/python/okstra_ctl/models.py +2 -0
- package/runtime/python/okstra_ctl/render_final_report.py +201 -0
- package/runtime/python/okstra_ctl/report_views.py +276 -297
- package/runtime/python/okstra_ctl/run.py +1 -1
- package/runtime/python/okstra_ctl/wizard.py +53 -14
- package/runtime/python/okstra_ctl/workers.py +45 -11
- package/runtime/python/okstra_token_usage/__init__.py +5 -1
- package/runtime/python/okstra_token_usage/cli.py +66 -36
- package/runtime/python/okstra_token_usage/pricing.py +1 -0
- package/runtime/python/okstra_token_usage/report.py +148 -65
- package/runtime/python/okstra_vendor/__init__.py +37 -0
- package/runtime/python/okstra_vendor/jinja2/__init__.py +38 -0
- package/runtime/python/okstra_vendor/jinja2/_identifier.py +6 -0
- package/runtime/python/okstra_vendor/jinja2/async_utils.py +99 -0
- package/runtime/python/okstra_vendor/jinja2/bccache.py +408 -0
- package/runtime/python/okstra_vendor/jinja2/compiler.py +1998 -0
- package/runtime/python/okstra_vendor/jinja2/constants.py +20 -0
- package/runtime/python/okstra_vendor/jinja2/debug.py +191 -0
- package/runtime/python/okstra_vendor/jinja2/defaults.py +48 -0
- package/runtime/python/okstra_vendor/jinja2/environment.py +1672 -0
- package/runtime/python/okstra_vendor/jinja2/exceptions.py +166 -0
- package/runtime/python/okstra_vendor/jinja2/ext.py +870 -0
- package/runtime/python/okstra_vendor/jinja2/filters.py +1873 -0
- package/runtime/python/okstra_vendor/jinja2/idtracking.py +318 -0
- package/runtime/python/okstra_vendor/jinja2/lexer.py +868 -0
- package/runtime/python/okstra_vendor/jinja2/loaders.py +693 -0
- package/runtime/python/okstra_vendor/jinja2/meta.py +112 -0
- package/runtime/python/okstra_vendor/jinja2/nativetypes.py +130 -0
- package/runtime/python/okstra_vendor/jinja2/nodes.py +1206 -0
- package/runtime/python/okstra_vendor/jinja2/optimizer.py +48 -0
- package/runtime/python/okstra_vendor/jinja2/parser.py +1049 -0
- package/runtime/python/okstra_vendor/jinja2/py.typed +0 -0
- package/runtime/python/okstra_vendor/jinja2/runtime.py +1062 -0
- package/runtime/python/okstra_vendor/jinja2/sandbox.py +436 -0
- package/runtime/python/okstra_vendor/jinja2/tests.py +256 -0
- package/runtime/python/okstra_vendor/jinja2/utils.py +766 -0
- package/runtime/python/okstra_vendor/jinja2/visitor.py +92 -0
- package/runtime/python/okstra_vendor/markupsafe/__init__.py +396 -0
- package/runtime/python/okstra_vendor/markupsafe/_native.py +8 -0
- package/runtime/python/okstra_vendor/markupsafe/py.typed +0 -0
- package/runtime/schemas/final-report-v1.0.schema.json +1391 -0
- package/runtime/skills/okstra-report-writer/SKILL.md +31 -30
- package/runtime/skills/okstra-run/SKILL.md +6 -4
- package/runtime/skills/okstra-team-contract/SKILL.md +27 -3
- package/runtime/templates/reports/final-report.template.md +370 -405
- package/runtime/templates/reports/report.css +57 -4
- package/runtime/templates/reports/report.js +63 -7
- package/runtime/templates/reports/settings.template.json +1 -0
- package/runtime/validators/lib/fixtures.sh +7 -7
- package/runtime/validators/validate-report-views.py +24 -153
- package/runtime/validators/validate-run.py +102 -19
- package/src/install.mjs +21 -1
|
@@ -1,93 +1,176 @@
|
|
|
1
|
-
"""Final-report
|
|
1
|
+
"""Final-report data.json mutation helpers (Phase 7 token cells)."""
|
|
2
2
|
from __future__ import annotations
|
|
3
3
|
|
|
4
|
+
import json
|
|
5
|
+
import sys
|
|
4
6
|
from pathlib import Path
|
|
7
|
+
from typing import Any
|
|
5
8
|
|
|
9
|
+
# Make sibling packages importable when this module runs as part of an
|
|
10
|
+
# in-repo invocation. The installed runtime puts everything under
|
|
11
|
+
# ~/.okstra/lib/python which is already on PYTHONPATH via the bin
|
|
12
|
+
# wrapper, so this path tweak is a no-op there.
|
|
13
|
+
_HERE = Path(__file__).resolve().parent.parent
|
|
14
|
+
if str(_HERE) not in sys.path:
|
|
15
|
+
sys.path.insert(0, str(_HERE))
|
|
6
16
|
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
return "0"
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
def _format_usd(v) -> str:
|
|
15
|
-
try:
|
|
16
|
-
return f"${float(v):.2f}"
|
|
17
|
-
except (TypeError, ValueError):
|
|
18
|
-
return "$0.00"
|
|
17
|
+
from okstra_ctl.render_final_report import ( # noqa: E402
|
|
18
|
+
RenderError,
|
|
19
|
+
render_to_file,
|
|
20
|
+
)
|
|
19
21
|
|
|
20
22
|
|
|
21
23
|
class SubstituteRefusedError(RuntimeError):
|
|
22
|
-
"""Raised when substitution would write
|
|
24
|
+
"""Raised when substitution would write zero-only token cells.
|
|
23
25
|
|
|
24
26
|
Shipping `0` / `$0.00` in the Lead / Worker / Grand rows is the
|
|
25
27
|
observed silent-failure mode where the collector ran but every
|
|
26
|
-
session jsonl was empty (or the writer fabricated zeros).
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
be retried with a re-collection.
|
|
28
|
+
session jsonl was empty (or the writer fabricated zeros). Surfacing
|
|
29
|
+
the refusal here lets the caller retry after locating the missing
|
|
30
|
+
session jsonls instead of baking zeros into the report.
|
|
30
31
|
"""
|
|
31
32
|
|
|
32
33
|
|
|
33
|
-
def
|
|
34
|
-
"""
|
|
35
|
-
|
|
34
|
+
def _role_match(state_role: str, data_role: str) -> bool:
|
|
35
|
+
"""Loose match between team-state worker role and data.json
|
|
36
|
+
executionStatus row role. team-state uses tags like ``claude-worker``
|
|
37
|
+
or ``report-writer``; data.json uses human strings like ``Claude
|
|
38
|
+
worker``. We normalise both to ``alnum-only-lowercase`` so the two
|
|
39
|
+
line up across rephrasings.
|
|
40
|
+
"""
|
|
41
|
+
def normalise(s: str) -> str:
|
|
42
|
+
return "".join(ch.lower() for ch in s if ch.isalnum())
|
|
43
|
+
return normalise(state_role) == normalise(data_role)
|
|
36
44
|
|
|
37
|
-
Returns the number of placeholder occurrences replaced. If the report file
|
|
38
|
-
does not exist, returns -1 without raising.
|
|
39
45
|
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
summary with ``grandTotalTokens`` > 0 or remove the summary entirely
|
|
45
|
-
so substitution is skipped.
|
|
46
|
+
def _populate_execution_row(row: dict, source: dict) -> None:
|
|
47
|
+
"""Fill the executionStatus row's token / cost / duration cells from a
|
|
48
|
+
team-state worker (or lead) entry. ``source`` is the dict that owns
|
|
49
|
+
a ``usage`` sub-dict and (for workers) a ``status`` field.
|
|
46
50
|
"""
|
|
47
|
-
|
|
48
|
-
|
|
51
|
+
usage = source.get("usage") or {}
|
|
52
|
+
if usage.get("source") == "unavailable":
|
|
53
|
+
# Leave cells null; renderer emits `--`. A note in the row's
|
|
54
|
+
# summary is the worker's responsibility, not ours.
|
|
55
|
+
return
|
|
56
|
+
if "totalTokens" in usage:
|
|
57
|
+
row["totalTokens"] = usage["totalTokens"]
|
|
58
|
+
if "billableEquivalentTokens" in usage:
|
|
59
|
+
row["billableTokens"] = usage["billableEquivalentTokens"]
|
|
60
|
+
if "estimatedCostUsd" in usage:
|
|
61
|
+
row["costUsd"] = usage["estimatedCostUsd"]
|
|
62
|
+
if "durationMs" in usage:
|
|
63
|
+
row["durationMs"] = usage["durationMs"]
|
|
64
|
+
if "cliTotalTokens" in usage:
|
|
65
|
+
row["cliTotalTokens"] = usage["cliTotalTokens"]
|
|
66
|
+
if "cliEstimatedCostUsd" in usage:
|
|
67
|
+
row["cliCostUsd"] = usage["cliEstimatedCostUsd"]
|
|
68
|
+
|
|
69
|
+
|
|
70
|
+
def populate_data_token_cells(data_path: Path, team_state: dict) -> int:
|
|
71
|
+
"""Mutate ``data_path`` (final-report data.json) in place: fill
|
|
72
|
+
``tokenUsage`` rows and ``executionStatus`` row token cells from
|
|
73
|
+
``team_state['usageSummary']`` and ``team_state['workers']`` /
|
|
74
|
+
``team_state['lead']``. Returns the number of cells changed (rough
|
|
75
|
+
count for diagnostics).
|
|
49
76
|
|
|
50
|
-
|
|
77
|
+
Raises ``SubstituteRefusedError`` when ``usageSummary.grandTotalTokens``
|
|
78
|
+
is zero — substituting zeros bakes in the "collector ran but found
|
|
79
|
+
nothing" failure mode. Callers that intentionally want zeros (e.g.
|
|
80
|
+
unit-test fixtures) must omit ``usageSummary`` from the team-state.
|
|
81
|
+
"""
|
|
82
|
+
summary = team_state.get("usageSummary") or {}
|
|
51
83
|
grand_total = summary.get("grandTotalTokens", 0)
|
|
52
84
|
if isinstance(grand_total, (int, float)) and grand_total == 0 and summary:
|
|
53
85
|
raise SubstituteRefusedError(
|
|
54
|
-
"Refusing to substitute zero-only usageSummary into
|
|
55
|
-
f"
|
|
56
|
-
"
|
|
57
|
-
"Re-run `python3 scripts/okstra-token-usage.py <team-state> "
|
|
58
|
-
"--write --summary --substitute-final-report <report-path>` "
|
|
86
|
+
"Refusing to substitute zero-only usageSummary into "
|
|
87
|
+
f"{data_path}. grandTotalTokens=0 means the collector ran "
|
|
88
|
+
"but every session jsonl was empty (or absent). Re-run "
|
|
59
89
|
"after locating the missing session jsonls. To intentionally "
|
|
60
|
-
"ship zeros (test fixtures only), omit `usageSummary` from
|
|
61
|
-
"team-state
|
|
90
|
+
"ship zeros (test fixtures only), omit `usageSummary` from "
|
|
91
|
+
"team-state before calling this."
|
|
62
92
|
)
|
|
93
|
+
|
|
94
|
+
if not data_path.is_file():
|
|
95
|
+
raise SubstituteRefusedError(f"data file not found: {data_path}")
|
|
96
|
+
|
|
97
|
+
data = json.loads(data_path.read_text(encoding="utf-8"))
|
|
98
|
+
changes = 0
|
|
99
|
+
|
|
100
|
+
# Token Usage Summary table — four rows.
|
|
63
101
|
cost = summary.get("estimatedCostUsd") or {}
|
|
64
102
|
lead_cost = cost.get("lead") or 0
|
|
65
103
|
worker_cost = cost.get("claudeWorkers") or 0
|
|
66
104
|
cli_cost = cost.get("cliWorkers") or 0
|
|
67
|
-
grand_cost = lead_cost + worker_cost # CLI tracked on its own row per template
|
|
68
|
-
|
|
69
|
-
mapping = {
|
|
70
|
-
"{{LEAD_TOTAL_TOKENS}}": _format_int(summary.get("leadTotalTokens", 0)),
|
|
71
|
-
"{{LEAD_BILLABLE_TOKENS}}": _format_int(summary.get("leadBillableEquivalentTokens", 0)),
|
|
72
|
-
"{{LEAD_COST_USD}}": _format_usd(lead_cost),
|
|
73
|
-
"{{WORKER_TOTAL_TOKENS}}": _format_int(summary.get("workerTotalTokens", 0)),
|
|
74
|
-
"{{WORKER_BILLABLE_TOKENS}}": _format_int(summary.get("workerBillableEquivalentTokens", 0)),
|
|
75
|
-
"{{WORKER_COST_USD}}": _format_usd(worker_cost),
|
|
76
|
-
"{{GRAND_TOTAL_TOKENS}}": _format_int(summary.get("grandTotalTokens", 0)),
|
|
77
|
-
"{{GRAND_BILLABLE_TOKENS}}": _format_int(summary.get("grandBillableEquivalentTokens", 0)),
|
|
78
|
-
"{{GRAND_COST_USD}}": _format_usd(grand_cost),
|
|
79
|
-
"{{CLI_COST_USD}}": _format_usd(cli_cost),
|
|
80
|
-
}
|
|
81
|
-
|
|
82
|
-
text = report_path.read_text()
|
|
83
|
-
replaced = 0
|
|
84
|
-
for needle, value in mapping.items():
|
|
85
|
-
count = text.count(needle)
|
|
86
|
-
if count:
|
|
87
|
-
text = text.replace(needle, value)
|
|
88
|
-
replaced += count
|
|
89
|
-
|
|
90
|
-
if replaced:
|
|
91
|
-
report_path.write_text(text)
|
|
92
|
-
return replaced
|
|
93
105
|
|
|
106
|
+
token_usage = data.setdefault("tokenUsage", {})
|
|
107
|
+
token_usage.setdefault("lead", {}).update({
|
|
108
|
+
"totalTokens": summary.get("leadTotalTokens"),
|
|
109
|
+
"billableTokens": summary.get("leadBillableEquivalentTokens"),
|
|
110
|
+
"costUsd": lead_cost,
|
|
111
|
+
})
|
|
112
|
+
token_usage.setdefault("worker", {}).update({
|
|
113
|
+
"totalTokens": summary.get("workerTotalTokens"),
|
|
114
|
+
"billableTokens": summary.get("workerBillableEquivalentTokens"),
|
|
115
|
+
"costUsd": worker_cost,
|
|
116
|
+
})
|
|
117
|
+
token_usage.setdefault("grand", {}).update({
|
|
118
|
+
"totalTokens": summary.get("grandTotalTokens"),
|
|
119
|
+
"billableTokens": summary.get("grandBillableEquivalentTokens"),
|
|
120
|
+
# CLI tracked on its own row per the template — grand here means
|
|
121
|
+
# lead + claudeWorkers, not lead + claudeWorkers + cli.
|
|
122
|
+
"costUsd": lead_cost + worker_cost,
|
|
123
|
+
})
|
|
124
|
+
token_usage.setdefault("cli", {})["costUsd"] = cli_cost
|
|
125
|
+
changes += 4
|
|
126
|
+
|
|
127
|
+
# Execution Status by Agent — per-row token / cost / duration.
|
|
128
|
+
lead_entry = team_state.get("lead") or {}
|
|
129
|
+
workers = team_state.get("workers") or []
|
|
130
|
+
rows = data.get("executionStatus") or []
|
|
131
|
+
for row in rows:
|
|
132
|
+
role = row.get("role", "")
|
|
133
|
+
if "lead" in role.lower():
|
|
134
|
+
_populate_execution_row(row, lead_entry)
|
|
135
|
+
changes += 1
|
|
136
|
+
continue
|
|
137
|
+
for worker in workers:
|
|
138
|
+
if _role_match(worker.get("role", ""), role):
|
|
139
|
+
_populate_execution_row(row, worker)
|
|
140
|
+
changes += 1
|
|
141
|
+
break
|
|
142
|
+
|
|
143
|
+
data_path.write_text(
|
|
144
|
+
json.dumps(data, indent=2, ensure_ascii=False) + "\n",
|
|
145
|
+
encoding="utf-8",
|
|
146
|
+
)
|
|
147
|
+
return changes
|
|
148
|
+
|
|
149
|
+
|
|
150
|
+
def populate_and_render(
|
|
151
|
+
data_path: Path,
|
|
152
|
+
team_state: dict,
|
|
153
|
+
*,
|
|
154
|
+
output_path: Path | None = None,
|
|
155
|
+
) -> tuple[int, int]:
|
|
156
|
+
"""Mutate data.json + render the markdown sibling. Returns
|
|
157
|
+
``(cells_changed, bytes_written)``.
|
|
158
|
+
|
|
159
|
+
The caller is normally Phase 7 step 1 (token-usage CLI with the
|
|
160
|
+
``--substitute-data`` flag). The rendered markdown is the canonical
|
|
161
|
+
user-facing artifact; data.json is the SSOT.
|
|
162
|
+
"""
|
|
163
|
+
changes = populate_data_token_cells(data_path, team_state)
|
|
164
|
+
|
|
165
|
+
if output_path is None:
|
|
166
|
+
name = data_path.name
|
|
167
|
+
if name.endswith(".data.json"):
|
|
168
|
+
output_path = data_path.with_name(name[: -len(".data.json")] + ".md")
|
|
169
|
+
else:
|
|
170
|
+
output_path = data_path.with_suffix(".md")
|
|
171
|
+
|
|
172
|
+
try:
|
|
173
|
+
bytes_written = render_to_file(data_path, output_path)
|
|
174
|
+
except RenderError as exc:
|
|
175
|
+
raise SubstituteRefusedError(f"render after substitute failed: {exc}") from exc
|
|
176
|
+
return changes, bytes_written
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
"""Vendored third-party dependencies used by okstra runtime scripts.
|
|
2
|
+
|
|
3
|
+
This package isolates third-party Python libraries that okstra needs at
|
|
4
|
+
runtime so that end users do not need to `pip install` anything separate
|
|
5
|
+
from the npm-distributed runtime. The build system (`tools/build.mjs`)
|
|
6
|
+
copies this package into `runtime/python/okstra_vendor/` and the
|
|
7
|
+
installer (`src/install.mjs`) drops it under `~/.okstra/lib/python/`.
|
|
8
|
+
|
|
9
|
+
Currently vendored:
|
|
10
|
+
- jinja2 3.1.6 (BSD-3-Clause) — final-report template rendering.
|
|
11
|
+
- markupsafe 3.0.3 (BSD-3-Clause) — jinja2 dependency. C extension
|
|
12
|
+
(`_speedups.c` / `_speedups.pyi`) is stripped; the pure-python
|
|
13
|
+
`_native` fallback is used at import time.
|
|
14
|
+
|
|
15
|
+
Source provenance and licenses are preserved verbatim under each
|
|
16
|
+
sub-package. Do not edit vendored files in place — re-vendor from PyPI
|
|
17
|
+
sdists when upgrading.
|
|
18
|
+
|
|
19
|
+
Import alias rationale: jinja2 uses absolute imports (`from markupsafe
|
|
20
|
+
import ...`, and compiled templates `import jinja2`) internally. Without
|
|
21
|
+
re-routing, those would either fail (no system install) or resolve to
|
|
22
|
+
unrelated user-installed copies. We register the vendored copies in
|
|
23
|
+
`sys.modules` under their canonical top-level names so jinja2's internal
|
|
24
|
+
imports find them deterministically.
|
|
25
|
+
"""
|
|
26
|
+
|
|
27
|
+
import sys as _sys
|
|
28
|
+
|
|
29
|
+
# Register markupsafe alias BEFORE importing jinja2 — jinja2's __init__
|
|
30
|
+
# evaluates `from .environment import ...`, which does `from markupsafe
|
|
31
|
+
# import Markup` at module-load time. Reversing the order would crash
|
|
32
|
+
# the import chain before the alias was registered.
|
|
33
|
+
from . import markupsafe as _markupsafe
|
|
34
|
+
_sys.modules.setdefault("markupsafe", _markupsafe)
|
|
35
|
+
|
|
36
|
+
from . import jinja2 as _jinja2 # noqa: E402
|
|
37
|
+
_sys.modules.setdefault("jinja2", _jinja2)
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
"""Jinja is a template engine written in pure Python. It provides a
|
|
2
|
+
non-XML syntax that supports inline expressions and an optional
|
|
3
|
+
sandboxed environment.
|
|
4
|
+
"""
|
|
5
|
+
|
|
6
|
+
from .bccache import BytecodeCache as BytecodeCache
|
|
7
|
+
from .bccache import FileSystemBytecodeCache as FileSystemBytecodeCache
|
|
8
|
+
from .bccache import MemcachedBytecodeCache as MemcachedBytecodeCache
|
|
9
|
+
from .environment import Environment as Environment
|
|
10
|
+
from .environment import Template as Template
|
|
11
|
+
from .exceptions import TemplateAssertionError as TemplateAssertionError
|
|
12
|
+
from .exceptions import TemplateError as TemplateError
|
|
13
|
+
from .exceptions import TemplateNotFound as TemplateNotFound
|
|
14
|
+
from .exceptions import TemplateRuntimeError as TemplateRuntimeError
|
|
15
|
+
from .exceptions import TemplatesNotFound as TemplatesNotFound
|
|
16
|
+
from .exceptions import TemplateSyntaxError as TemplateSyntaxError
|
|
17
|
+
from .exceptions import UndefinedError as UndefinedError
|
|
18
|
+
from .loaders import BaseLoader as BaseLoader
|
|
19
|
+
from .loaders import ChoiceLoader as ChoiceLoader
|
|
20
|
+
from .loaders import DictLoader as DictLoader
|
|
21
|
+
from .loaders import FileSystemLoader as FileSystemLoader
|
|
22
|
+
from .loaders import FunctionLoader as FunctionLoader
|
|
23
|
+
from .loaders import ModuleLoader as ModuleLoader
|
|
24
|
+
from .loaders import PackageLoader as PackageLoader
|
|
25
|
+
from .loaders import PrefixLoader as PrefixLoader
|
|
26
|
+
from .runtime import ChainableUndefined as ChainableUndefined
|
|
27
|
+
from .runtime import DebugUndefined as DebugUndefined
|
|
28
|
+
from .runtime import make_logging_undefined as make_logging_undefined
|
|
29
|
+
from .runtime import StrictUndefined as StrictUndefined
|
|
30
|
+
from .runtime import Undefined as Undefined
|
|
31
|
+
from .utils import clear_caches as clear_caches
|
|
32
|
+
from .utils import is_undefined as is_undefined
|
|
33
|
+
from .utils import pass_context as pass_context
|
|
34
|
+
from .utils import pass_environment as pass_environment
|
|
35
|
+
from .utils import pass_eval_context as pass_eval_context
|
|
36
|
+
from .utils import select_autoescape as select_autoescape
|
|
37
|
+
|
|
38
|
+
__version__ = "3.1.6"
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
import re
|
|
2
|
+
|
|
3
|
+
# generated by scripts/generate_identifier_pattern.py
|
|
4
|
+
pattern = re.compile(
|
|
5
|
+
r"[\w·̀-ͯ·҃-֑҇-ׇֽֿׁׂׅׄؐ-ًؚ-ٰٟۖ-ۜ۟-۪ۤۧۨ-ܑۭܰ-݊ަ-ް߫-߽߳ࠖ-࠙ࠛ-ࠣࠥ-ࠧࠩ-࡙࠭-࡛࣓-ࣣ࣡-ःऺ-़ा-ॏ॑-ॗॢॣঁ-ঃ়া-ৄেৈো-্ৗৢৣ৾ਁ-ਃ਼ਾ-ੂੇੈੋ-੍ੑੰੱੵઁ-ઃ઼ા-ૅે-ૉો-્ૢૣૺ-૿ଁ-ଃ଼ା-ୄେୈୋ-୍ୖୗୢୣஂா-ூெ-ைொ-்ௗఀ-ఄా-ౄె-ైొ-్ౕౖౢౣಁ-ಃ಼ಾ-ೄೆ-ೈೊ-್ೕೖೢೣഀ-ഃ഻഼ാ-ൄെ-ൈൊ-്ൗൢൣංඃ්ා-ුූෘ-ෟෲෳัิ-ฺ็-๎ັິ-ູົຼ່-ໍ༹༘༙༵༷༾༿ཱ-྄྆྇ྍ-ྗྙ-ྼ࿆ါ-ှၖ-ၙၞ-ၠၢ-ၤၧ-ၭၱ-ၴႂ-ႍႏႚ-ႝ፝-፟ᜒ-᜔ᜲ-᜴ᝒᝓᝲᝳ឴-៓៝᠋-᠍ᢅᢆᢩᤠ-ᤫᤰ-᤻ᨗ-ᨛᩕ-ᩞ᩠-᩿᩼᪰-᪽ᬀ-ᬄ᬴-᭄᭫-᭳ᮀ-ᮂᮡ-ᮭ᯦-᯳ᰤ-᰷᳐-᳔᳒-᳨᳭ᳲ-᳴᳷-᳹᷀-᷹᷻-᷿‿⁀⁔⃐-⃥⃜⃡-⃰℘℮⳯-⵿⳱ⷠ-〪ⷿ-゙゚〯꙯ꙴ-꙽ꚞꚟ꛰꛱ꠂ꠆ꠋꠣ-ꠧꢀꢁꢴ-ꣅ꣠-꣱ꣿꤦ-꤭ꥇ-꥓ꦀ-ꦃ꦳-꧀ꧥꨩ-ꨶꩃꩌꩍꩻ-ꩽꪰꪲ-ꪴꪷꪸꪾ꪿꫁ꫫ-ꫯꫵ꫶ꯣ-ꯪ꯬꯭ﬞ︀-️︠-︯︳︴﹍-﹏_𐇽𐋠𐍶-𐍺𐨁-𐨃𐨅𐨆𐨌-𐨏𐨸-𐨿𐨺𐫦𐫥𐴤-𐽆𐴧-𐽐𑀀-𑀂𑀸-𑁆𑁿-𑂂𑂰-𑂺𑄀-𑄂𑄧-𑄴𑅅𑅆𑅳𑆀-𑆂𑆳-𑇀𑇉-𑇌𑈬-𑈷𑈾𑋟-𑋪𑌀-𑌃𑌻𑌼𑌾-𑍄𑍇𑍈𑍋-𑍍𑍗𑍢𑍣𑍦-𑍬𑍰-𑍴𑐵-𑑆𑑞𑒰-𑓃𑖯-𑖵𑖸-𑗀𑗜𑗝𑘰-𑙀𑚫-𑚷𑜝-𑜫𑠬-𑠺𑨁-𑨊𑨳-𑨹𑨻-𑨾𑩇𑩑-𑩛𑪊-𑪙𑰯-𑰶𑰸-𑰿𑲒-𑲧𑲩-𑲶𑴱-𑴶𑴺𑴼𑴽𑴿-𑵅𑵇𑶊-𑶎𑶐𑶑𑶓-𑶗𑻳-𑻶𖫰-𖫴𖬰-𖬶𖽑-𖽾𖾏-𖾒𛲝𛲞𝅥-𝅩𝅭-𝅲𝅻-𝆂𝆅-𝆋𝆪-𝆭𝉂-𝉄𝨀-𝨶𝨻-𝩬𝩵𝪄𝪛-𝪟𝪡-𝪯𞀀-𞀆𞀈-𞀘𞀛-𞀡𞀣𞀤𞀦-𞣐𞀪-𞣖𞥄-𞥊󠄀-󠇯]+" # noqa: B950
|
|
6
|
+
)
|
|
@@ -0,0 +1,99 @@
|
|
|
1
|
+
import inspect
|
|
2
|
+
import typing as t
|
|
3
|
+
from functools import WRAPPER_ASSIGNMENTS
|
|
4
|
+
from functools import wraps
|
|
5
|
+
|
|
6
|
+
from .utils import _PassArg
|
|
7
|
+
from .utils import pass_eval_context
|
|
8
|
+
|
|
9
|
+
if t.TYPE_CHECKING:
|
|
10
|
+
import typing_extensions as te
|
|
11
|
+
|
|
12
|
+
V = t.TypeVar("V")
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
def async_variant(normal_func): # type: ignore
|
|
16
|
+
def decorator(async_func): # type: ignore
|
|
17
|
+
pass_arg = _PassArg.from_obj(normal_func)
|
|
18
|
+
need_eval_context = pass_arg is None
|
|
19
|
+
|
|
20
|
+
if pass_arg is _PassArg.environment:
|
|
21
|
+
|
|
22
|
+
def is_async(args: t.Any) -> bool:
|
|
23
|
+
return t.cast(bool, args[0].is_async)
|
|
24
|
+
|
|
25
|
+
else:
|
|
26
|
+
|
|
27
|
+
def is_async(args: t.Any) -> bool:
|
|
28
|
+
return t.cast(bool, args[0].environment.is_async)
|
|
29
|
+
|
|
30
|
+
# Take the doc and annotations from the sync function, but the
|
|
31
|
+
# name from the async function. Pallets-Sphinx-Themes
|
|
32
|
+
# build_function_directive expects __wrapped__ to point to the
|
|
33
|
+
# sync function.
|
|
34
|
+
async_func_attrs = ("__module__", "__name__", "__qualname__")
|
|
35
|
+
normal_func_attrs = tuple(set(WRAPPER_ASSIGNMENTS).difference(async_func_attrs))
|
|
36
|
+
|
|
37
|
+
@wraps(normal_func, assigned=normal_func_attrs)
|
|
38
|
+
@wraps(async_func, assigned=async_func_attrs, updated=())
|
|
39
|
+
def wrapper(*args, **kwargs): # type: ignore
|
|
40
|
+
b = is_async(args)
|
|
41
|
+
|
|
42
|
+
if need_eval_context:
|
|
43
|
+
args = args[1:]
|
|
44
|
+
|
|
45
|
+
if b:
|
|
46
|
+
return async_func(*args, **kwargs)
|
|
47
|
+
|
|
48
|
+
return normal_func(*args, **kwargs)
|
|
49
|
+
|
|
50
|
+
if need_eval_context:
|
|
51
|
+
wrapper = pass_eval_context(wrapper)
|
|
52
|
+
|
|
53
|
+
wrapper.jinja_async_variant = True # type: ignore[attr-defined]
|
|
54
|
+
return wrapper
|
|
55
|
+
|
|
56
|
+
return decorator
|
|
57
|
+
|
|
58
|
+
|
|
59
|
+
_common_primitives = {int, float, bool, str, list, dict, tuple, type(None)}
|
|
60
|
+
|
|
61
|
+
|
|
62
|
+
async def auto_await(value: t.Union[t.Awaitable["V"], "V"]) -> "V":
|
|
63
|
+
# Avoid a costly call to isawaitable
|
|
64
|
+
if type(value) in _common_primitives:
|
|
65
|
+
return t.cast("V", value)
|
|
66
|
+
|
|
67
|
+
if inspect.isawaitable(value):
|
|
68
|
+
return await t.cast("t.Awaitable[V]", value)
|
|
69
|
+
|
|
70
|
+
return value
|
|
71
|
+
|
|
72
|
+
|
|
73
|
+
class _IteratorToAsyncIterator(t.Generic[V]):
|
|
74
|
+
def __init__(self, iterator: "t.Iterator[V]"):
|
|
75
|
+
self._iterator = iterator
|
|
76
|
+
|
|
77
|
+
def __aiter__(self) -> "te.Self":
|
|
78
|
+
return self
|
|
79
|
+
|
|
80
|
+
async def __anext__(self) -> V:
|
|
81
|
+
try:
|
|
82
|
+
return next(self._iterator)
|
|
83
|
+
except StopIteration as e:
|
|
84
|
+
raise StopAsyncIteration(e.value) from e
|
|
85
|
+
|
|
86
|
+
|
|
87
|
+
def auto_aiter(
|
|
88
|
+
iterable: "t.Union[t.AsyncIterable[V], t.Iterable[V]]",
|
|
89
|
+
) -> "t.AsyncIterator[V]":
|
|
90
|
+
if hasattr(iterable, "__aiter__"):
|
|
91
|
+
return iterable.__aiter__()
|
|
92
|
+
else:
|
|
93
|
+
return _IteratorToAsyncIterator(iter(iterable))
|
|
94
|
+
|
|
95
|
+
|
|
96
|
+
async def auto_to_list(
|
|
97
|
+
value: "t.Union[t.AsyncIterable[V], t.Iterable[V]]",
|
|
98
|
+
) -> t.List["V"]:
|
|
99
|
+
return [x async for x in auto_aiter(value)]
|