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.
@@ -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,3 @@
1
+ """Scalim CLI package (dev tooling)."""
2
+
3
+ __all__ = ()
@@ -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__ = ()