scalim-cli 0.8.3__tar.gz
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.
- scalim_cli-0.8.3/.gitignore +301 -0
- scalim_cli-0.8.3/PKG-INFO +35 -0
- scalim_cli-0.8.3/README.md +19 -0
- scalim_cli-0.8.3/pyproject.toml +31 -0
- scalim_cli-0.8.3/scalim_cli/__init__.py +3 -0
- scalim_cli-0.8.3/scalim_cli/log.py +199 -0
- scalim_cli-0.8.3/scalim_cli/main.py +31 -0
- scalim_cli-0.8.3/scalim_cli/yaml_dsl.py +832 -0
- scalim_cli-0.8.3/scalim_cli/yaml_dsl_lsp.py +239 -0
|
@@ -0,0 +1,301 @@
|
|
|
1
|
+
# User Custom
|
|
2
|
+
**/.vscode
|
|
3
|
+
|
|
4
|
+
# .gitignore file created for free with Tower (https://www.git-tower.com/)
|
|
5
|
+
# For suggestions or improvements please contact us using support@git-tower.com
|
|
6
|
+
# Generated on 2026-03-05
|
|
7
|
+
# Includes: python, npm, pnpm, svelte, react, rust, go
|
|
8
|
+
|
|
9
|
+
### Universal ###
|
|
10
|
+
# Common files that should be ignored in all projects
|
|
11
|
+
|
|
12
|
+
# Logs
|
|
13
|
+
*.log
|
|
14
|
+
# Temporary files
|
|
15
|
+
*.tmp
|
|
16
|
+
*~
|
|
17
|
+
|
|
18
|
+
*.bak
|
|
19
|
+
# Environment files (containing secrets, API keys, credentials)
|
|
20
|
+
.env
|
|
21
|
+
*.env
|
|
22
|
+
.env.*
|
|
23
|
+
|
|
24
|
+
# Local configuration that shouldn't be shared
|
|
25
|
+
*.local
|
|
26
|
+
|
|
27
|
+
### Python ###
|
|
28
|
+
# Python compiled files, virtual environments, and cache
|
|
29
|
+
|
|
30
|
+
**/__pycache__/
|
|
31
|
+
*.py[cod]
|
|
32
|
+
*$py.class
|
|
33
|
+
*.so
|
|
34
|
+
.Python
|
|
35
|
+
|
|
36
|
+
pypackages/
|
|
37
|
+
build/
|
|
38
|
+
develop-eggs/
|
|
39
|
+
dist/
|
|
40
|
+
downloads/
|
|
41
|
+
|
|
42
|
+
eggs/
|
|
43
|
+
.eggs/
|
|
44
|
+
lib/
|
|
45
|
+
lib64/
|
|
46
|
+
parts/
|
|
47
|
+
|
|
48
|
+
sdist/
|
|
49
|
+
var/
|
|
50
|
+
wheels/
|
|
51
|
+
share/python-wheels/
|
|
52
|
+
*.egg-info/
|
|
53
|
+
|
|
54
|
+
.installed.cfg
|
|
55
|
+
*.egg
|
|
56
|
+
MANIFEST
|
|
57
|
+
.venv
|
|
58
|
+
env/
|
|
59
|
+
|
|
60
|
+
venv/
|
|
61
|
+
ENV/
|
|
62
|
+
.pytest_cache/
|
|
63
|
+
.coverage
|
|
64
|
+
htmlcov/
|
|
65
|
+
|
|
66
|
+
# PyInstaller
|
|
67
|
+
*.manifest
|
|
68
|
+
*.spec
|
|
69
|
+
# Installer logs
|
|
70
|
+
pip-log.txt
|
|
71
|
+
|
|
72
|
+
pip-delete-this-directory.txt
|
|
73
|
+
# Unit test / coverage reports
|
|
74
|
+
.tox/
|
|
75
|
+
.nox/
|
|
76
|
+
.coverage.*
|
|
77
|
+
|
|
78
|
+
nosetests.xml
|
|
79
|
+
coverage.xml
|
|
80
|
+
*.cover
|
|
81
|
+
*.py,cover
|
|
82
|
+
.hypothesis/
|
|
83
|
+
|
|
84
|
+
cover/
|
|
85
|
+
# Translations
|
|
86
|
+
*.mo
|
|
87
|
+
*.pot
|
|
88
|
+
# Django
|
|
89
|
+
|
|
90
|
+
local_settings.py
|
|
91
|
+
db.sqlite3
|
|
92
|
+
db.sqlite3-journal
|
|
93
|
+
# Flask
|
|
94
|
+
instance/
|
|
95
|
+
|
|
96
|
+
.webassets-cache
|
|
97
|
+
# Scrapy
|
|
98
|
+
.scrapy
|
|
99
|
+
# Sphinx
|
|
100
|
+
docs/_build/
|
|
101
|
+
|
|
102
|
+
# PyBuilder
|
|
103
|
+
.pybuilder/
|
|
104
|
+
target/
|
|
105
|
+
# Jupyter Notebook
|
|
106
|
+
.ipynb_checkpoints
|
|
107
|
+
|
|
108
|
+
# IPython
|
|
109
|
+
profile_default/
|
|
110
|
+
ipython_config.py
|
|
111
|
+
# pyenv
|
|
112
|
+
#.python-version
|
|
113
|
+
|
|
114
|
+
# pipenv
|
|
115
|
+
#Pipfile.lock
|
|
116
|
+
# poetry
|
|
117
|
+
#poetry.lock
|
|
118
|
+
# pdm
|
|
119
|
+
|
|
120
|
+
.pdm.toml
|
|
121
|
+
__pypackages__/
|
|
122
|
+
# Celery
|
|
123
|
+
celerybeat-schedule
|
|
124
|
+
celerybeat.pid
|
|
125
|
+
|
|
126
|
+
# SageMath
|
|
127
|
+
*.sage.py
|
|
128
|
+
# Spyder
|
|
129
|
+
.spyderproject
|
|
130
|
+
.spyproject
|
|
131
|
+
|
|
132
|
+
# Rope
|
|
133
|
+
.ropeproject
|
|
134
|
+
# Zensical docs
|
|
135
|
+
/site
|
|
136
|
+
# mypy
|
|
137
|
+
|
|
138
|
+
.mypy_cache/
|
|
139
|
+
.dmypy.json
|
|
140
|
+
dmypy.json
|
|
141
|
+
# Pyre
|
|
142
|
+
.pyre/
|
|
143
|
+
|
|
144
|
+
# pytype
|
|
145
|
+
.pytype/
|
|
146
|
+
# Cython
|
|
147
|
+
cython_debug/
|
|
148
|
+
# Poetry
|
|
149
|
+
|
|
150
|
+
poetry.toml
|
|
151
|
+
# ruff
|
|
152
|
+
.ruff_cache/
|
|
153
|
+
# LSP
|
|
154
|
+
pyrightconfig.json
|
|
155
|
+
|
|
156
|
+
*.sage.py
|
|
157
|
+
.pdm.toml
|
|
158
|
+
profile_default/
|
|
159
|
+
.nox/
|
|
160
|
+
.spyproject
|
|
161
|
+
|
|
162
|
+
docs/site/
|
|
163
|
+
|
|
164
|
+
### Npm ###
|
|
165
|
+
# NPM package manager files and directories
|
|
166
|
+
|
|
167
|
+
# NPM (Node Package Manager)
|
|
168
|
+
node_modules/
|
|
169
|
+
package-lock.json
|
|
170
|
+
.npm/
|
|
171
|
+
.node-gyp/
|
|
172
|
+
|
|
173
|
+
npm-debug.log*
|
|
174
|
+
.npmrc
|
|
175
|
+
.npm-cache/
|
|
176
|
+
.npm-global/
|
|
177
|
+
.node_repl_history
|
|
178
|
+
|
|
179
|
+
.env
|
|
180
|
+
npm-shrinkwrap.json
|
|
181
|
+
.npmignore
|
|
182
|
+
.package-lock.json
|
|
183
|
+
npm-debug.log.*
|
|
184
|
+
|
|
185
|
+
.npm-debug/
|
|
186
|
+
.npm-tmp/
|
|
187
|
+
.npm-tarball/
|
|
188
|
+
node_modules_*
|
|
189
|
+
.npm-init.js
|
|
190
|
+
|
|
191
|
+
.npm-update-notifier
|
|
192
|
+
.node_modules.ember-try/
|
|
193
|
+
.npm-version
|
|
194
|
+
.node_history
|
|
195
|
+
.v8flags.*
|
|
196
|
+
|
|
197
|
+
.babel.json
|
|
198
|
+
package.json.bak
|
|
199
|
+
|
|
200
|
+
### Pnpm ###
|
|
201
|
+
# pnpm package manager specific files
|
|
202
|
+
|
|
203
|
+
# PNPM
|
|
204
|
+
pnpm-lock.yaml
|
|
205
|
+
pnpm-workspace.yaml
|
|
206
|
+
.pnpm/
|
|
207
|
+
.pnpm-debug.log
|
|
208
|
+
|
|
209
|
+
.pnpm-store/
|
|
210
|
+
pnpm-lock.yaml.bak
|
|
211
|
+
.pnpmfile.cjs
|
|
212
|
+
node_modules/
|
|
213
|
+
.pnpm-cache/
|
|
214
|
+
|
|
215
|
+
### Svelte ###
|
|
216
|
+
# svelte specific files
|
|
217
|
+
|
|
218
|
+
node_modules/
|
|
219
|
+
build/
|
|
220
|
+
.cache/
|
|
221
|
+
*.log
|
|
222
|
+
|
|
223
|
+
### React ###
|
|
224
|
+
# React build directory and environment files
|
|
225
|
+
|
|
226
|
+
build/
|
|
227
|
+
.env.local
|
|
228
|
+
.env.development.local
|
|
229
|
+
.env.test.local
|
|
230
|
+
.env.production.local
|
|
231
|
+
|
|
232
|
+
npm-debug.log*
|
|
233
|
+
yarn-debug.log*
|
|
234
|
+
yarn-error.log*
|
|
235
|
+
.DS_*
|
|
236
|
+
**/*.backup.*
|
|
237
|
+
|
|
238
|
+
**/*.back.*
|
|
239
|
+
*.sublime*
|
|
240
|
+
psd
|
|
241
|
+
thumb
|
|
242
|
+
sketch
|
|
243
|
+
|
|
244
|
+
coverage/
|
|
245
|
+
|
|
246
|
+
### Rust ###
|
|
247
|
+
# rust specific files
|
|
248
|
+
|
|
249
|
+
/target/
|
|
250
|
+
**/*.rs.bk
|
|
251
|
+
debug/
|
|
252
|
+
target/debug/
|
|
253
|
+
target/release/
|
|
254
|
+
|
|
255
|
+
### Go ###
|
|
256
|
+
# go specific files
|
|
257
|
+
|
|
258
|
+
*.exe
|
|
259
|
+
*.test
|
|
260
|
+
# vendor/
|
|
261
|
+
*.out
|
|
262
|
+
go.work
|
|
263
|
+
go.sum
|
|
264
|
+
|
|
265
|
+
### Project (scalim) ###
|
|
266
|
+
# Bench / profiling outputs (dev-only)
|
|
267
|
+
.benchmarks/memray/
|
|
268
|
+
|
|
269
|
+
# Local artifacts / CI uploads (not committed)
|
|
270
|
+
.tmp/
|
|
271
|
+
|
|
272
|
+
# Viz replay artifacts (generated; not committed)
|
|
273
|
+
artifacts/scalim-viz/
|
|
274
|
+
|
|
275
|
+
# Agent skill build manifests (deprecated)
|
|
276
|
+
artifacts/skills/*.build-manifest.json
|
|
277
|
+
|
|
278
|
+
# Docs artifacts (generated)
|
|
279
|
+
docs/doc/notebooks/**/*.html
|
|
280
|
+
|
|
281
|
+
# Keep lockfiles committed (template previously ignored these)
|
|
282
|
+
!pnpm-lock.yaml
|
|
283
|
+
!pnpm-workspace.yaml
|
|
284
|
+
|
|
285
|
+
# Local openspec sanitization rules
|
|
286
|
+
openspec/sanitize_rules.local.yaml
|
|
287
|
+
|
|
288
|
+
# vendors clib
|
|
289
|
+
!src/scalim/vendor/yamlx/**/*.so
|
|
290
|
+
|
|
291
|
+
# VS Code extension workspace (extras/vscode-scalim)
|
|
292
|
+
extras/vscode-scalim/node_modules/
|
|
293
|
+
extras/vscode-scalim/dist/
|
|
294
|
+
extras/vscode-scalim/out/
|
|
295
|
+
extras/vscode-scalim/.vscode-test/
|
|
296
|
+
extras/vscode-scalim/*.vsix
|
|
297
|
+
|
|
298
|
+
# Keep extension scaffolding tracked (override global ignores)
|
|
299
|
+
!extras/vscode-scalim/.npmrc
|
|
300
|
+
!extras/vscode-scalim/.vscode/
|
|
301
|
+
!extras/vscode-scalim/.vscode/**
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: scalim-cli
|
|
3
|
+
Version: 0.8.3
|
|
4
|
+
Summary: Command-line tools for scalim
|
|
5
|
+
Author-email: straydragon <straydragonl@foxmail.com>
|
|
6
|
+
Classifier: Programming Language :: Python :: 3 :: Only
|
|
7
|
+
Classifier: Programming Language :: Python :: 3.10
|
|
8
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
9
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
10
|
+
Classifier: Programming Language :: Python :: 3.13
|
|
11
|
+
Classifier: Programming Language :: Python :: 3.14
|
|
12
|
+
Requires-Python: >=3.10
|
|
13
|
+
Requires-Dist: jsonschema>=4.0.0
|
|
14
|
+
Requires-Dist: scalim
|
|
15
|
+
Description-Content-Type: text/markdown
|
|
16
|
+
|
|
17
|
+
# scalim-cli
|
|
18
|
+
|
|
19
|
+
Standalone command-line utilities for `scalim` (dev/tooling only).
|
|
20
|
+
|
|
21
|
+
Install:
|
|
22
|
+
|
|
23
|
+
```bash
|
|
24
|
+
uv tool install scalim-cli
|
|
25
|
+
|
|
26
|
+
# Run once without installing:
|
|
27
|
+
uvx scalim-cli --help
|
|
28
|
+
```
|
|
29
|
+
|
|
30
|
+
Usage:
|
|
31
|
+
|
|
32
|
+
```bash
|
|
33
|
+
scalim-cli --help
|
|
34
|
+
scalim-cli yaml-dsl --help
|
|
35
|
+
```
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
# scalim-cli
|
|
2
|
+
|
|
3
|
+
Standalone command-line utilities for `scalim` (dev/tooling only).
|
|
4
|
+
|
|
5
|
+
Install:
|
|
6
|
+
|
|
7
|
+
```bash
|
|
8
|
+
uv tool install scalim-cli
|
|
9
|
+
|
|
10
|
+
# Run once without installing:
|
|
11
|
+
uvx scalim-cli --help
|
|
12
|
+
```
|
|
13
|
+
|
|
14
|
+
Usage:
|
|
15
|
+
|
|
16
|
+
```bash
|
|
17
|
+
scalim-cli --help
|
|
18
|
+
scalim-cli yaml-dsl --help
|
|
19
|
+
```
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
[project]
|
|
2
|
+
name = "scalim-cli"
|
|
3
|
+
version = "0.8.3"
|
|
4
|
+
description = "Command-line tools for scalim"
|
|
5
|
+
readme = "README.md"
|
|
6
|
+
requires-python = ">=3.10"
|
|
7
|
+
|
|
8
|
+
authors = [{ name = "straydragon", email = "straydragonl@foxmail.com" }]
|
|
9
|
+
classifiers = [
|
|
10
|
+
"Programming Language :: Python :: 3 :: Only",
|
|
11
|
+
"Programming Language :: Python :: 3.10",
|
|
12
|
+
"Programming Language :: Python :: 3.11",
|
|
13
|
+
"Programming Language :: Python :: 3.12",
|
|
14
|
+
"Programming Language :: Python :: 3.13",
|
|
15
|
+
"Programming Language :: Python :: 3.14",
|
|
16
|
+
]
|
|
17
|
+
|
|
18
|
+
dependencies = [
|
|
19
|
+
"scalim",
|
|
20
|
+
"jsonschema>=4.0.0",
|
|
21
|
+
]
|
|
22
|
+
|
|
23
|
+
[project.scripts]
|
|
24
|
+
scalim-cli = "scalim_cli.main:main"
|
|
25
|
+
|
|
26
|
+
[build-system]
|
|
27
|
+
requires = ["hatchling"]
|
|
28
|
+
build-backend = "hatchling.build"
|
|
29
|
+
|
|
30
|
+
[tool.hatch.build]
|
|
31
|
+
sources = ["src"]
|
|
@@ -0,0 +1,199 @@
|
|
|
1
|
+
import argparse
|
|
2
|
+
import json
|
|
3
|
+
import sys
|
|
4
|
+
from pathlib import Path
|
|
5
|
+
from typing import Any, Dict, Iterable, Iterator, List, Tuple
|
|
6
|
+
|
|
7
|
+
from scalim.ob.structured_logging import normalize_keys_to_full
|
|
8
|
+
|
|
9
|
+
_LOG_LEVEL_ERROR = 40
|
|
10
|
+
_MAX_JOINED_CHARS = 1024 * 1024
|
|
11
|
+
_MAX_BUFFER_LINES = 200
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
def register(subparsers: Any) -> None:
|
|
15
|
+
parser = subparsers.add_parser("log", help="Structured log utilities (JSONL)")
|
|
16
|
+
_set_help_default(parser)
|
|
17
|
+
log_subparsers = parser.add_subparsers(dest="log_command")
|
|
18
|
+
|
|
19
|
+
fmt_parser = log_subparsers.add_parser("fmt", help="Render structured logs (human-friendly)")
|
|
20
|
+
_add_input_arg(fmt_parser)
|
|
21
|
+
_ = fmt_parser.add_argument("--max-fields", type=int, default=12, help="Max number of fields to show per record")
|
|
22
|
+
_ = fmt_parser.add_argument("--max-value-chars", type=int, default=120, help="Max characters per value when rendering")
|
|
23
|
+
fmt_parser.set_defaults(func=_run_fmt)
|
|
24
|
+
|
|
25
|
+
summarize_parser = log_subparsers.add_parser("summarize", help="Summarize structured logs (llm-friendly)")
|
|
26
|
+
_add_input_arg(summarize_parser)
|
|
27
|
+
_ = summarize_parser.add_argument("--budget-chars", type=int, default=8000, help="Max characters in output")
|
|
28
|
+
summarize_parser.set_defaults(func=_run_summarize)
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
def _set_help_default(parser: argparse.ArgumentParser) -> None:
|
|
32
|
+
def _show_help(_args: argparse.Namespace) -> int:
|
|
33
|
+
parser.print_help()
|
|
34
|
+
return 2
|
|
35
|
+
|
|
36
|
+
parser.set_defaults(func=_show_help)
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
def _add_input_arg(parser: argparse.ArgumentParser) -> None:
|
|
40
|
+
_ = parser.add_argument(
|
|
41
|
+
"input",
|
|
42
|
+
nargs="?",
|
|
43
|
+
default="-",
|
|
44
|
+
type=str,
|
|
45
|
+
help="Input JSONL or mixed log file. Use '-' for stdin.",
|
|
46
|
+
)
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
def _iter_input_lines(path: str) -> Iterator[str]:
|
|
50
|
+
if path == "-" or not path:
|
|
51
|
+
for line in sys.stdin:
|
|
52
|
+
yield line
|
|
53
|
+
return
|
|
54
|
+
with Path(path).open("r", encoding="utf-8", errors="replace") as f:
|
|
55
|
+
for line in f:
|
|
56
|
+
yield line
|
|
57
|
+
|
|
58
|
+
|
|
59
|
+
def _iter_json_objects(lines: Iterable[str]) -> Iterator[Dict[str, Any]]:
|
|
60
|
+
"""Best-effort JSON object scanner with multiline recovery.
|
|
61
|
+
|
|
62
|
+
Strategy:
|
|
63
|
+
- Ignore non-JSON lines.
|
|
64
|
+
- When a line looks like a JSON object start, try parse; if fails, keep buffering until parse succeeds.
|
|
65
|
+
"""
|
|
66
|
+
buf: List[str] = []
|
|
67
|
+
for raw in lines:
|
|
68
|
+
line = raw.rstrip("\n")
|
|
69
|
+
if not buf:
|
|
70
|
+
stripped = line.lstrip()
|
|
71
|
+
if not stripped.startswith("{"):
|
|
72
|
+
continue
|
|
73
|
+
try:
|
|
74
|
+
obj = json.loads(stripped)
|
|
75
|
+
except json.JSONDecodeError:
|
|
76
|
+
buf.append(stripped)
|
|
77
|
+
continue
|
|
78
|
+
if isinstance(obj, dict):
|
|
79
|
+
yield normalize_keys_to_full(obj) # type: ignore[arg-type]
|
|
80
|
+
continue
|
|
81
|
+
|
|
82
|
+
buf.append(line)
|
|
83
|
+
joined = "\n".join(buf)
|
|
84
|
+
try:
|
|
85
|
+
obj = json.loads(joined)
|
|
86
|
+
except json.JSONDecodeError:
|
|
87
|
+
if len(joined) > _MAX_JOINED_CHARS or len(buf) > _MAX_BUFFER_LINES:
|
|
88
|
+
buf = []
|
|
89
|
+
continue
|
|
90
|
+
buf = []
|
|
91
|
+
if isinstance(obj, dict):
|
|
92
|
+
yield normalize_keys_to_full(obj) # type: ignore[arg-type]
|
|
93
|
+
|
|
94
|
+
|
|
95
|
+
def _shorten(value: object, *, max_chars: int) -> str:
|
|
96
|
+
text = str(value)
|
|
97
|
+
if max_chars <= 0:
|
|
98
|
+
return text
|
|
99
|
+
if len(text) <= max_chars:
|
|
100
|
+
return text
|
|
101
|
+
return text[: max_chars - 1] + "…"
|
|
102
|
+
|
|
103
|
+
|
|
104
|
+
def _format_kv(items: Dict[str, Any], *, max_fields: int, max_value_chars: int) -> str:
|
|
105
|
+
parts: List[str] = []
|
|
106
|
+
for key in sorted(items.keys()):
|
|
107
|
+
if max_fields > 0 and len(parts) >= max_fields:
|
|
108
|
+
parts.append("…")
|
|
109
|
+
break
|
|
110
|
+
val = items[key]
|
|
111
|
+
if val is None:
|
|
112
|
+
continue
|
|
113
|
+
parts.append("{}={}".format(key, _shorten(val, max_chars=max_value_chars)))
|
|
114
|
+
return ", ".join(parts)
|
|
115
|
+
|
|
116
|
+
|
|
117
|
+
def _format_human(record: Dict[str, Any], *, max_fields: int, max_value_chars: int) -> str:
|
|
118
|
+
level = int(record.get("level") or 0)
|
|
119
|
+
logger = str(record.get("logger") or "")
|
|
120
|
+
kind = str(record.get("kind") or "")
|
|
121
|
+
message = str(record.get("message") or "")
|
|
122
|
+
|
|
123
|
+
ctx = record.get("context") or {}
|
|
124
|
+
fields = record.get("fields") or {}
|
|
125
|
+
err = record.get("error") or {}
|
|
126
|
+
|
|
127
|
+
ctx_text = ""
|
|
128
|
+
if isinstance(ctx, dict) and ctx:
|
|
129
|
+
picked: Dict[str, Any] = {}
|
|
130
|
+
for k in ("demand", "workflow_node_id", "run_id", "demand_path"):
|
|
131
|
+
if k in ctx and ctx[k] not in (None, ""):
|
|
132
|
+
picked[k] = ctx[k]
|
|
133
|
+
if picked:
|
|
134
|
+
ctx_text = " ctx({})".format(_format_kv(picked, max_fields=6, max_value_chars=max_value_chars))
|
|
135
|
+
|
|
136
|
+
fields_text = ""
|
|
137
|
+
if isinstance(fields, dict) and fields:
|
|
138
|
+
fields_text = " {}".format(_format_kv(fields, max_fields=max_fields, max_value_chars=max_value_chars))
|
|
139
|
+
|
|
140
|
+
err_text = ""
|
|
141
|
+
if isinstance(err, dict) and err:
|
|
142
|
+
err_text = " err({})".format(_format_kv(err, max_fields=6, max_value_chars=max_value_chars))
|
|
143
|
+
|
|
144
|
+
head = kind or message or "<log>"
|
|
145
|
+
if logger:
|
|
146
|
+
head = "{} {}".format(logger, head)
|
|
147
|
+
|
|
148
|
+
prefix = "ERROR " if level >= _LOG_LEVEL_ERROR else ""
|
|
149
|
+
return "{}{}{}{}{}".format(prefix, head, ctx_text, fields_text, err_text).strip()
|
|
150
|
+
|
|
151
|
+
|
|
152
|
+
def _run_fmt(args: argparse.Namespace) -> int:
|
|
153
|
+
path = str(getattr(args, "input", "-") or "-")
|
|
154
|
+
max_fields = int(getattr(args, "max_fields", 12) or 12)
|
|
155
|
+
max_value_chars = int(getattr(args, "max_value_chars", 120) or 120)
|
|
156
|
+
for record in _iter_json_objects(_iter_input_lines(path)):
|
|
157
|
+
sys.stdout.write(_format_human(record, max_fields=max_fields, max_value_chars=max_value_chars) + "\n")
|
|
158
|
+
return 0
|
|
159
|
+
|
|
160
|
+
|
|
161
|
+
def _summarize_kinds(records: List[Dict[str, Any]]) -> List[Tuple[str, int]]:
|
|
162
|
+
counts: Dict[str, int] = {}
|
|
163
|
+
for r in records:
|
|
164
|
+
kind = str(r.get("kind") or "")
|
|
165
|
+
if not kind:
|
|
166
|
+
continue
|
|
167
|
+
counts[kind] = int(counts.get(kind, 0)) + 1
|
|
168
|
+
return sorted(counts.items(), key=lambda kv: (-int(kv[1]), kv[0]))
|
|
169
|
+
|
|
170
|
+
|
|
171
|
+
def _run_summarize(args: argparse.Namespace) -> int:
|
|
172
|
+
path = str(getattr(args, "input", "-") or "-")
|
|
173
|
+
budget = int(getattr(args, "budget_chars", 8000) or 8000)
|
|
174
|
+
records = list(_iter_json_objects(_iter_input_lines(path)))
|
|
175
|
+
|
|
176
|
+
lines: List[str] = []
|
|
177
|
+
lines.append("records={}".format(len(records)))
|
|
178
|
+
|
|
179
|
+
for kind, count in _summarize_kinds(records)[:20]:
|
|
180
|
+
lines.append("{}={}".format(kind, count))
|
|
181
|
+
|
|
182
|
+
# best-effort: show last performance/relations summaries if present
|
|
183
|
+
for want_kind in ("performance.summary", "performance.loader_breakdown", "relations.summary"):
|
|
184
|
+
picked = None
|
|
185
|
+
for r in reversed(records):
|
|
186
|
+
if str(r.get("kind") or "") == want_kind:
|
|
187
|
+
picked = r
|
|
188
|
+
break
|
|
189
|
+
if picked and isinstance(picked.get("fields"), dict):
|
|
190
|
+
lines.append("{} {}".format(want_kind, _format_kv(picked["fields"], max_fields=12, max_value_chars=120))) # type: ignore[index]
|
|
191
|
+
|
|
192
|
+
text = "\n".join(lines)
|
|
193
|
+
if budget > 0 and len(text) > budget:
|
|
194
|
+
text = text[: max(0, budget - 1)] + "…"
|
|
195
|
+
sys.stdout.write(text + "\n")
|
|
196
|
+
return 0
|
|
197
|
+
|
|
198
|
+
|
|
199
|
+
__all__ = ("register",)
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
import argparse
|
|
2
|
+
from typing import List, Optional
|
|
3
|
+
|
|
4
|
+
from . import log as log_cmd
|
|
5
|
+
from . import yaml_dsl
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
def _build_parser() -> argparse.ArgumentParser:
|
|
9
|
+
parser = argparse.ArgumentParser(prog="scalim-cli", description="Scalim CLI")
|
|
10
|
+
|
|
11
|
+
def _show_help(_args: argparse.Namespace) -> int:
|
|
12
|
+
parser.print_help()
|
|
13
|
+
return 2
|
|
14
|
+
|
|
15
|
+
parser.set_defaults(func=_show_help)
|
|
16
|
+
subparsers = parser.add_subparsers(dest="command")
|
|
17
|
+
yaml_dsl.register(subparsers)
|
|
18
|
+
log_cmd.register(subparsers)
|
|
19
|
+
return parser
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
def main(argv: Optional[List[str]] = None) -> int:
|
|
23
|
+
parser = _build_parser()
|
|
24
|
+
args = parser.parse_args(argv)
|
|
25
|
+
return args.func(args) # type: ignore[attr-defined]
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
if __name__ == "__main__":
|
|
29
|
+
raise SystemExit(main())
|
|
30
|
+
|
|
31
|
+
__all__ = ()
|