krons 0.1.0__py3-none-any.whl → 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.
- krons/__init__.py +49 -0
- krons/agent/__init__.py +144 -0
- krons/agent/mcps/__init__.py +14 -0
- krons/agent/mcps/loader.py +287 -0
- krons/agent/mcps/wrapper.py +799 -0
- krons/agent/message/__init__.py +20 -0
- krons/agent/message/action.py +69 -0
- krons/agent/message/assistant.py +52 -0
- krons/agent/message/common.py +49 -0
- krons/agent/message/instruction.py +130 -0
- krons/agent/message/prepare_msg.py +187 -0
- krons/agent/message/role.py +53 -0
- krons/agent/message/system.py +53 -0
- krons/agent/operations/__init__.py +82 -0
- krons/agent/operations/act.py +100 -0
- krons/agent/operations/generate.py +145 -0
- krons/agent/operations/llm_reparse.py +89 -0
- krons/agent/operations/operate.py +247 -0
- krons/agent/operations/parse.py +243 -0
- krons/agent/operations/react.py +286 -0
- krons/agent/operations/specs.py +235 -0
- krons/agent/operations/structure.py +151 -0
- krons/agent/operations/utils.py +79 -0
- krons/agent/providers/__init__.py +17 -0
- krons/agent/providers/anthropic_messages.py +146 -0
- krons/agent/providers/claude_code.py +276 -0
- krons/agent/providers/gemini.py +268 -0
- krons/agent/providers/match.py +75 -0
- krons/agent/providers/oai_chat.py +174 -0
- krons/agent/third_party/__init__.py +2 -0
- krons/agent/third_party/anthropic_models.py +154 -0
- krons/agent/third_party/claude_code.py +682 -0
- krons/agent/third_party/gemini_models.py +508 -0
- krons/agent/third_party/openai_models.py +295 -0
- krons/agent/tool.py +291 -0
- krons/core/__init__.py +127 -0
- krons/core/base/__init__.py +121 -0
- {kronos/core → krons/core/base}/broadcaster.py +7 -3
- {kronos/core → krons/core/base}/element.py +15 -7
- {kronos/core → krons/core/base}/event.py +41 -8
- {kronos/core → krons/core/base}/eventbus.py +4 -2
- {kronos/core → krons/core/base}/flow.py +14 -7
- {kronos/core → krons/core/base}/graph.py +27 -11
- {kronos/core → krons/core/base}/node.py +47 -22
- {kronos/core → krons/core/base}/pile.py +26 -12
- {kronos/core → krons/core/base}/processor.py +23 -9
- {kronos/core → krons/core/base}/progression.py +5 -3
- {kronos → krons/core}/specs/__init__.py +0 -5
- {kronos → krons/core}/specs/adapters/dataclass_field.py +16 -8
- {kronos → krons/core}/specs/adapters/pydantic_adapter.py +11 -5
- {kronos → krons/core}/specs/adapters/sql_ddl.py +16 -10
- {kronos → krons/core}/specs/catalog/__init__.py +2 -2
- {kronos → krons/core}/specs/catalog/_audit.py +3 -3
- {kronos → krons/core}/specs/catalog/_common.py +2 -2
- {kronos → krons/core}/specs/catalog/_content.py +5 -5
- {kronos → krons/core}/specs/catalog/_enforcement.py +4 -4
- {kronos → krons/core}/specs/factory.py +7 -7
- {kronos → krons/core}/specs/operable.py +9 -3
- {kronos → krons/core}/specs/protocol.py +4 -2
- {kronos → krons/core}/specs/spec.py +25 -13
- {kronos → krons/core}/types/base.py +7 -5
- {kronos → krons/core}/types/db_types.py +2 -2
- {kronos → krons/core}/types/identity.py +1 -1
- {kronos → krons}/errors.py +13 -13
- {kronos → krons}/protocols.py +9 -4
- krons/resource/__init__.py +89 -0
- {kronos/services → krons/resource}/backend.py +50 -24
- {kronos/services → krons/resource}/endpoint.py +28 -14
- {kronos/services → krons/resource}/hook.py +22 -9
- {kronos/services → krons/resource}/imodel.py +50 -32
- {kronos/services → krons/resource}/registry.py +27 -25
- {kronos/services → krons/resource}/utilities/rate_limited_executor.py +10 -6
- {kronos/services → krons/resource}/utilities/rate_limiter.py +4 -2
- {kronos/services → krons/resource}/utilities/resilience.py +17 -7
- krons/resource/utilities/token_calculator.py +185 -0
- {kronos → krons}/session/__init__.py +12 -17
- krons/session/constraints.py +70 -0
- {kronos → krons}/session/exchange.py +14 -6
- {kronos → krons}/session/message.py +4 -2
- krons/session/registry.py +35 -0
- {kronos → krons}/session/session.py +165 -174
- krons/utils/__init__.py +85 -0
- krons/utils/_function_arg_parser.py +99 -0
- krons/utils/_pythonic_function_call.py +249 -0
- {kronos → krons}/utils/_to_list.py +9 -3
- {kronos → krons}/utils/_utils.py +9 -5
- {kronos → krons}/utils/concurrency/__init__.py +38 -38
- {kronos → krons}/utils/concurrency/_async_call.py +6 -4
- {kronos → krons}/utils/concurrency/_errors.py +3 -1
- {kronos → krons}/utils/concurrency/_patterns.py +3 -1
- {kronos → krons}/utils/concurrency/_resource_tracker.py +6 -2
- krons/utils/display.py +257 -0
- {kronos → krons}/utils/fuzzy/__init__.py +6 -1
- {kronos → krons}/utils/fuzzy/_fuzzy_match.py +14 -8
- {kronos → krons}/utils/fuzzy/_string_similarity.py +3 -1
- {kronos → krons}/utils/fuzzy/_to_dict.py +3 -1
- krons/utils/schemas/__init__.py +26 -0
- krons/utils/schemas/_breakdown_pydantic_annotation.py +131 -0
- krons/utils/schemas/_formatter.py +72 -0
- krons/utils/schemas/_minimal_yaml.py +151 -0
- krons/utils/schemas/_typescript.py +153 -0
- {kronos → krons}/utils/sql/_sql_validation.py +1 -1
- krons/utils/validators/__init__.py +3 -0
- krons/utils/validators/_validate_image_url.py +56 -0
- krons/work/__init__.py +126 -0
- krons/work/engine.py +333 -0
- krons/work/form.py +305 -0
- {kronos → krons/work}/operations/__init__.py +7 -4
- {kronos → krons/work}/operations/builder.py +4 -4
- {kronos/enforcement → krons/work/operations}/context.py +37 -6
- {kronos → krons/work}/operations/flow.py +17 -9
- krons/work/operations/node.py +103 -0
- krons/work/operations/registry.py +103 -0
- {kronos/specs → krons/work}/phrase.py +131 -14
- {kronos/enforcement → krons/work}/policy.py +3 -3
- krons/work/report.py +268 -0
- krons/work/rules/__init__.py +47 -0
- {kronos/enforcement → krons/work/rules}/common/boolean.py +3 -1
- {kronos/enforcement → krons/work/rules}/common/choice.py +9 -3
- {kronos/enforcement → krons/work/rules}/common/number.py +3 -1
- {kronos/enforcement → krons/work/rules}/common/string.py +9 -3
- {kronos/enforcement → krons/work/rules}/rule.py +2 -2
- {kronos/enforcement → krons/work/rules}/validator.py +21 -6
- {kronos/enforcement → krons/work}/service.py +16 -7
- krons/work/worker.py +266 -0
- {krons-0.1.0.dist-info → krons-0.2.0.dist-info}/METADATA +19 -5
- krons-0.2.0.dist-info/RECORD +154 -0
- kronos/core/__init__.py +0 -145
- kronos/enforcement/__init__.py +0 -57
- kronos/operations/node.py +0 -101
- kronos/operations/registry.py +0 -92
- kronos/services/__init__.py +0 -81
- kronos/specs/adapters/__init__.py +0 -0
- kronos/utils/__init__.py +0 -40
- krons-0.1.0.dist-info/RECORD +0 -101
- {kronos → krons/core/specs/adapters}/__init__.py +0 -0
- {kronos → krons/core}/specs/adapters/_utils.py +0 -0
- {kronos → krons/core}/specs/adapters/factory.py +0 -0
- {kronos → krons/core}/types/__init__.py +0 -0
- {kronos → krons/core}/types/_sentinel.py +0 -0
- {kronos → krons}/py.typed +0 -0
- {kronos/services → krons/resource}/utilities/__init__.py +0 -0
- {kronos/services → krons/resource}/utilities/header_factory.py +0 -0
- {kronos → krons}/utils/_hash.py +0 -0
- {kronos → krons}/utils/_json_dump.py +0 -0
- {kronos → krons}/utils/_lazy_init.py +0 -0
- {kronos → krons}/utils/_to_num.py +0 -0
- {kronos → krons}/utils/concurrency/_cancel.py +0 -0
- {kronos → krons}/utils/concurrency/_primitives.py +0 -0
- {kronos → krons}/utils/concurrency/_priority_queue.py +0 -0
- {kronos → krons}/utils/concurrency/_run_async.py +0 -0
- {kronos → krons}/utils/concurrency/_task.py +0 -0
- {kronos → krons}/utils/concurrency/_utils.py +0 -0
- {kronos → krons}/utils/fuzzy/_extract_json.py +0 -0
- {kronos → krons}/utils/fuzzy/_fuzzy_json.py +0 -0
- {kronos → krons}/utils/sql/__init__.py +0 -0
- {kronos/enforcement → krons/work/rules}/common/__init__.py +0 -0
- {kronos/enforcement → krons/work/rules}/common/mapping.py +0 -0
- {kronos/enforcement → krons/work/rules}/common/model.py +0 -0
- {kronos/enforcement → krons/work/rules}/registry.py +0 -0
- {krons-0.1.0.dist-info → krons-0.2.0.dist-info}/WHEEL +0 -0
- {krons-0.1.0.dist-info → krons-0.2.0.dist-info}/licenses/LICENSE +0 -0
|
@@ -64,7 +64,9 @@ class LeakTracker:
|
|
|
64
64
|
name: Identifier (defaults to "obj-{id}").
|
|
65
65
|
kind: Optional category label.
|
|
66
66
|
"""
|
|
67
|
-
info = LeakInfo(
|
|
67
|
+
info = LeakInfo(
|
|
68
|
+
name=name or f"obj-{id(obj)}", kind=kind, created_at=time.time()
|
|
69
|
+
)
|
|
68
70
|
key = id(obj)
|
|
69
71
|
|
|
70
72
|
def _finalizer(_key: int = key) -> None:
|
|
@@ -94,7 +96,9 @@ class LeakTracker:
|
|
|
94
96
|
_TRACKER = LeakTracker()
|
|
95
97
|
|
|
96
98
|
|
|
97
|
-
def track_resource(
|
|
99
|
+
def track_resource(
|
|
100
|
+
obj: object, name: str | None = None, kind: str | None = None
|
|
101
|
+
) -> None:
|
|
98
102
|
"""Track an object using the global leak tracker.
|
|
99
103
|
|
|
100
104
|
Args:
|
krons/utils/display.py
ADDED
|
@@ -0,0 +1,257 @@
|
|
|
1
|
+
# Copyright (c) 2025 - 2026, HaiyangLi <quantocean.li at gmail dot com>
|
|
2
|
+
# SPDX-License-Identifier: Apache-2.0
|
|
3
|
+
|
|
4
|
+
"""Display utilities for verbose operation tracking.
|
|
5
|
+
|
|
6
|
+
Ported from lionagi's as_readable system. Provides Rich-based
|
|
7
|
+
console output with YAML/JSON syntax highlighting, environment
|
|
8
|
+
detection, and truncation support.
|
|
9
|
+
"""
|
|
10
|
+
|
|
11
|
+
from __future__ import annotations
|
|
12
|
+
|
|
13
|
+
import json
|
|
14
|
+
import sys
|
|
15
|
+
import time
|
|
16
|
+
from typing import Any
|
|
17
|
+
|
|
18
|
+
try:
|
|
19
|
+
from rich.align import Align
|
|
20
|
+
from rich.box import ROUNDED
|
|
21
|
+
from rich.console import Console
|
|
22
|
+
from rich.padding import Padding
|
|
23
|
+
from rich.panel import Panel
|
|
24
|
+
from rich.syntax import Syntax
|
|
25
|
+
from rich.theme import Theme
|
|
26
|
+
|
|
27
|
+
DARK_THEME = Theme(
|
|
28
|
+
{
|
|
29
|
+
"info": "bright_cyan",
|
|
30
|
+
"warning": "bright_yellow",
|
|
31
|
+
"error": "bold bright_red",
|
|
32
|
+
"success": "bold bright_green",
|
|
33
|
+
"panel.border": "bright_blue",
|
|
34
|
+
"panel.title": "bold bright_cyan",
|
|
35
|
+
"json.key": "bright_cyan",
|
|
36
|
+
"json.string": "bright_green",
|
|
37
|
+
"json.number": "bright_yellow",
|
|
38
|
+
"json.boolean": "bright_magenta",
|
|
39
|
+
"json.null": "bright_red",
|
|
40
|
+
"yaml.key": "bright_cyan",
|
|
41
|
+
"yaml.string": "bright_green",
|
|
42
|
+
"yaml.number": "bright_yellow",
|
|
43
|
+
"yaml.boolean": "bright_magenta",
|
|
44
|
+
}
|
|
45
|
+
)
|
|
46
|
+
RICH_AVAILABLE = True
|
|
47
|
+
except ImportError:
|
|
48
|
+
RICH_AVAILABLE = False
|
|
49
|
+
DARK_THEME = None
|
|
50
|
+
|
|
51
|
+
_console: Console | None = None
|
|
52
|
+
|
|
53
|
+
|
|
54
|
+
def _get_console() -> Console | None:
|
|
55
|
+
global _console
|
|
56
|
+
if not RICH_AVAILABLE:
|
|
57
|
+
return None
|
|
58
|
+
if _console is None:
|
|
59
|
+
_console = Console(theme=DARK_THEME)
|
|
60
|
+
return _console
|
|
61
|
+
|
|
62
|
+
|
|
63
|
+
def in_notebook() -> bool:
|
|
64
|
+
"""Check if running inside a Jupyter notebook."""
|
|
65
|
+
try:
|
|
66
|
+
from IPython import get_ipython
|
|
67
|
+
|
|
68
|
+
shell = get_ipython().__class__.__name__
|
|
69
|
+
return "ZMQInteractiveShell" in shell
|
|
70
|
+
except Exception:
|
|
71
|
+
return False
|
|
72
|
+
|
|
73
|
+
|
|
74
|
+
def in_console() -> bool:
|
|
75
|
+
"""Check if running in a terminal with TTY."""
|
|
76
|
+
return hasattr(sys.stdout, "isatty") and sys.stdout.isatty() and not in_notebook()
|
|
77
|
+
|
|
78
|
+
|
|
79
|
+
def format_dict(data: Any, indent: int = 0) -> str:
|
|
80
|
+
"""Format data as YAML-like readable string."""
|
|
81
|
+
lines = []
|
|
82
|
+
prefix = " " * indent
|
|
83
|
+
|
|
84
|
+
if isinstance(data, dict):
|
|
85
|
+
for key, value in data.items():
|
|
86
|
+
if isinstance(value, dict):
|
|
87
|
+
lines.append(f"{prefix}{key}:")
|
|
88
|
+
lines.append(format_dict(value, indent + 1))
|
|
89
|
+
elif isinstance(value, list):
|
|
90
|
+
lines.append(f"{prefix}{key}:")
|
|
91
|
+
for item in value:
|
|
92
|
+
item_str = format_dict(item, indent + 2).lstrip()
|
|
93
|
+
lines.append(f"{prefix} - {item_str}")
|
|
94
|
+
elif isinstance(value, str) and "\n" in value:
|
|
95
|
+
lines.append(f"{prefix}{key}: |")
|
|
96
|
+
subprefix = " " * (indent + 1)
|
|
97
|
+
for line in value.splitlines():
|
|
98
|
+
lines.append(f"{subprefix}{line}")
|
|
99
|
+
else:
|
|
100
|
+
item_str = format_dict(value, indent + 1).lstrip()
|
|
101
|
+
lines.append(f"{prefix}{key}: {item_str}")
|
|
102
|
+
return "\n".join(lines)
|
|
103
|
+
|
|
104
|
+
elif isinstance(data, list):
|
|
105
|
+
for item in data:
|
|
106
|
+
item_str = format_dict(item, indent + 1).lstrip()
|
|
107
|
+
lines.append(f"{prefix}- {item_str}")
|
|
108
|
+
return "\n".join(lines)
|
|
109
|
+
|
|
110
|
+
return prefix + str(data)
|
|
111
|
+
|
|
112
|
+
|
|
113
|
+
def as_readable(
|
|
114
|
+
input_: Any,
|
|
115
|
+
/,
|
|
116
|
+
*,
|
|
117
|
+
md: bool = False,
|
|
118
|
+
format_curly: bool = True,
|
|
119
|
+
max_chars: int | None = None,
|
|
120
|
+
) -> str:
|
|
121
|
+
"""Convert data to human-readable string.
|
|
122
|
+
|
|
123
|
+
Args:
|
|
124
|
+
input_: Data to format (dict, model, list, etc.).
|
|
125
|
+
md: Wrap in code fences for markdown.
|
|
126
|
+
format_curly: YAML-like (True) or JSON (False).
|
|
127
|
+
max_chars: Truncate output.
|
|
128
|
+
|
|
129
|
+
Returns:
|
|
130
|
+
Formatted string.
|
|
131
|
+
"""
|
|
132
|
+
from krons.utils.fuzzy import to_dict
|
|
133
|
+
|
|
134
|
+
# Convert to dict
|
|
135
|
+
def safe_dict(obj: Any) -> Any:
|
|
136
|
+
try:
|
|
137
|
+
return to_dict(
|
|
138
|
+
obj,
|
|
139
|
+
use_model_dump=True,
|
|
140
|
+
fuzzy_parse=True,
|
|
141
|
+
recursive=True,
|
|
142
|
+
recursive_python_only=False,
|
|
143
|
+
max_recursive_depth=5,
|
|
144
|
+
)
|
|
145
|
+
except Exception:
|
|
146
|
+
return str(obj)
|
|
147
|
+
|
|
148
|
+
if isinstance(input_, list):
|
|
149
|
+
items = [safe_dict(x) for x in input_]
|
|
150
|
+
else:
|
|
151
|
+
maybe = safe_dict(input_)
|
|
152
|
+
items = maybe if isinstance(maybe, list) else [maybe]
|
|
153
|
+
|
|
154
|
+
rendered = []
|
|
155
|
+
for item in items:
|
|
156
|
+
if format_curly:
|
|
157
|
+
rendered.append(
|
|
158
|
+
format_dict(item) if isinstance(item, (dict, list)) else str(item)
|
|
159
|
+
)
|
|
160
|
+
else:
|
|
161
|
+
try:
|
|
162
|
+
rendered.append(json.dumps(item, indent=2, ensure_ascii=False))
|
|
163
|
+
except Exception:
|
|
164
|
+
rendered.append(str(item))
|
|
165
|
+
|
|
166
|
+
text = "\n\n".join(rendered).strip()
|
|
167
|
+
|
|
168
|
+
if md:
|
|
169
|
+
lang = "yaml" if format_curly else "json"
|
|
170
|
+
text = f"```{lang}\n{text}\n```"
|
|
171
|
+
|
|
172
|
+
if max_chars and len(text) > max_chars:
|
|
173
|
+
text = text[:max_chars] + "...\n\n[Truncated]"
|
|
174
|
+
|
|
175
|
+
return text
|
|
176
|
+
|
|
177
|
+
|
|
178
|
+
def display(text: str, *, title: str | None = None, lang: str = "yaml") -> None:
|
|
179
|
+
"""Display text with Rich formatting if available.
|
|
180
|
+
|
|
181
|
+
Args:
|
|
182
|
+
text: Text to display.
|
|
183
|
+
title: Optional panel title.
|
|
184
|
+
lang: Syntax language for highlighting.
|
|
185
|
+
"""
|
|
186
|
+
console = _get_console()
|
|
187
|
+
|
|
188
|
+
if console and in_console():
|
|
189
|
+
syntax = Syntax(
|
|
190
|
+
text,
|
|
191
|
+
lang,
|
|
192
|
+
theme="github-dark",
|
|
193
|
+
line_numbers=False,
|
|
194
|
+
word_wrap=True,
|
|
195
|
+
background_color="default",
|
|
196
|
+
)
|
|
197
|
+
content = syntax
|
|
198
|
+
if title:
|
|
199
|
+
content = Panel(
|
|
200
|
+
Align.left(syntax, pad=False),
|
|
201
|
+
title=title,
|
|
202
|
+
title_align="left",
|
|
203
|
+
border_style="panel.border",
|
|
204
|
+
box=ROUNDED,
|
|
205
|
+
width=min(console.width - 4, 140),
|
|
206
|
+
expand=False,
|
|
207
|
+
)
|
|
208
|
+
console.print(Padding(content, (0, 0, 0, 2)))
|
|
209
|
+
else:
|
|
210
|
+
console.print(Padding(content, (0, 0, 0, 2)))
|
|
211
|
+
return
|
|
212
|
+
|
|
213
|
+
# Fallback
|
|
214
|
+
if title:
|
|
215
|
+
print(f"\n--- {title} ---")
|
|
216
|
+
print(text)
|
|
217
|
+
|
|
218
|
+
|
|
219
|
+
def status(msg: str, *, style: str = "info") -> None:
|
|
220
|
+
"""Print a status message with optional Rich styling.
|
|
221
|
+
|
|
222
|
+
Args:
|
|
223
|
+
msg: Status message.
|
|
224
|
+
style: Rich style name (info, success, warning, error).
|
|
225
|
+
"""
|
|
226
|
+
console = _get_console()
|
|
227
|
+
if console and in_console():
|
|
228
|
+
console.print(f" [{style}]{msg}[/{style}]")
|
|
229
|
+
else:
|
|
230
|
+
print(f" {msg}")
|
|
231
|
+
|
|
232
|
+
|
|
233
|
+
def phase(title: str) -> None:
|
|
234
|
+
"""Print a phase header."""
|
|
235
|
+
console = _get_console()
|
|
236
|
+
if console and in_console():
|
|
237
|
+
console.print(f"\n[bold bright_cyan]=== {title} ===[/bold bright_cyan]")
|
|
238
|
+
else:
|
|
239
|
+
print(f"\n=== {title} ===")
|
|
240
|
+
|
|
241
|
+
|
|
242
|
+
class Timer:
|
|
243
|
+
"""Simple context manager for timing operations."""
|
|
244
|
+
|
|
245
|
+
def __init__(self, label: str = ""):
|
|
246
|
+
self.label = label
|
|
247
|
+
self.start = 0.0
|
|
248
|
+
self.elapsed = 0.0
|
|
249
|
+
|
|
250
|
+
def __enter__(self):
|
|
251
|
+
self.start = time.monotonic()
|
|
252
|
+
return self
|
|
253
|
+
|
|
254
|
+
def __exit__(self, *args):
|
|
255
|
+
self.elapsed = time.monotonic() - self.start
|
|
256
|
+
if self.label:
|
|
257
|
+
status(f"{self.label}: {self.elapsed:.1f}s", style="success")
|
|
@@ -1,14 +1,19 @@
|
|
|
1
1
|
from ._extract_json import extract_json
|
|
2
2
|
from ._fuzzy_json import fuzzy_json
|
|
3
|
-
from ._fuzzy_match import fuzzy_match_keys
|
|
3
|
+
from ._fuzzy_match import HandleUnmatched, fuzzy_match_keys
|
|
4
4
|
from ._string_similarity import SimilarityAlgo, string_similarity
|
|
5
5
|
from ._to_dict import to_dict
|
|
6
6
|
|
|
7
|
+
# Alias for backward compatibility
|
|
8
|
+
fuzzy_validate_mapping = fuzzy_match_keys
|
|
9
|
+
|
|
7
10
|
__all__ = (
|
|
8
11
|
"extract_json",
|
|
9
12
|
"fuzzy_json",
|
|
10
13
|
"fuzzy_match_keys",
|
|
14
|
+
"fuzzy_validate_mapping",
|
|
11
15
|
"string_similarity",
|
|
12
16
|
"SimilarityAlgo",
|
|
13
17
|
"to_dict",
|
|
18
|
+
"HandleUnmatched",
|
|
14
19
|
)
|
|
@@ -1,18 +1,16 @@
|
|
|
1
1
|
from __future__ import annotations
|
|
2
2
|
|
|
3
3
|
from enum import Enum
|
|
4
|
-
from typing import TYPE_CHECKING, Any
|
|
4
|
+
from typing import TYPE_CHECKING, Any
|
|
5
5
|
|
|
6
6
|
if TYPE_CHECKING:
|
|
7
|
-
from
|
|
7
|
+
from krons.core.types import KeysLike
|
|
8
8
|
|
|
9
|
-
from
|
|
9
|
+
from krons.core.types._sentinel import Unset
|
|
10
10
|
|
|
11
11
|
from ._string_similarity import SimilarityAlgo, string_similarity
|
|
12
12
|
|
|
13
|
-
__all__ = ("fuzzy_match_keys",)
|
|
14
|
-
|
|
15
|
-
HandleUnmatched = Literal["ignore", "raise", "remove", "fill", "force"]
|
|
13
|
+
__all__ = ("fuzzy_match_keys", "HandleUnmatched")
|
|
16
14
|
|
|
17
15
|
|
|
18
16
|
class HandleUnmatched(Enum):
|
|
@@ -114,7 +112,13 @@ def fuzzy_match_keys(
|
|
|
114
112
|
)
|
|
115
113
|
|
|
116
114
|
if matches:
|
|
117
|
-
match =
|
|
115
|
+
match = (
|
|
116
|
+
matches
|
|
117
|
+
if isinstance(matches, str)
|
|
118
|
+
else matches[0]
|
|
119
|
+
if matches
|
|
120
|
+
else None
|
|
121
|
+
)
|
|
118
122
|
if match:
|
|
119
123
|
corrected_out[match] = d_[key]
|
|
120
124
|
matched_expected.add(match)
|
|
@@ -136,7 +140,9 @@ def fuzzy_match_keys(
|
|
|
136
140
|
elif handle_unmatched in (HandleUnmatched.FILL, HandleUnmatched.FORCE):
|
|
137
141
|
for key in unmatched_expected:
|
|
138
142
|
corrected_out[key] = (
|
|
139
|
-
fill_mapping[key]
|
|
143
|
+
fill_mapping[key]
|
|
144
|
+
if fill_mapping and key in fill_mapping
|
|
145
|
+
else fill_value
|
|
140
146
|
)
|
|
141
147
|
|
|
142
148
|
if handle_unmatched == HandleUnmatched.FILL:
|
|
@@ -165,7 +165,9 @@ def string_similarity(
|
|
|
165
165
|
raise ValueError(f"Unsupported algorithm: {algorithm}")
|
|
166
166
|
|
|
167
167
|
results = []
|
|
168
|
-
for idx, (orig_word, comp_word) in enumerate(
|
|
168
|
+
for idx, (orig_word, comp_word) in enumerate(
|
|
169
|
+
zip(original_words, compare_words, strict=False)
|
|
170
|
+
):
|
|
169
171
|
if algo_name == "hamming" and len(comp_word) != len(compare_word):
|
|
170
172
|
continue # Hamming requires equal length
|
|
171
173
|
score = score_func(compare_word, comp_word) # type: ignore[operator]
|
|
@@ -310,7 +310,9 @@ def _preprocess_recursive(
|
|
|
310
310
|
|
|
311
311
|
if recursive_custom_types:
|
|
312
312
|
with contextlib.suppress(Exception):
|
|
313
|
-
mapped = _object_to_mapping_like(
|
|
313
|
+
mapped = _object_to_mapping_like(
|
|
314
|
+
obj, prioritize_model_dump=prioritize_model_dump
|
|
315
|
+
)
|
|
314
316
|
return _preprocess_recursive(
|
|
315
317
|
mapped,
|
|
316
318
|
depth=depth + 1,
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
# Copyright (c) 2025 - 2026, HaiyangLi <quantocean.li at gmail dot com>
|
|
2
|
+
# SPDX-License-Identifier: Apache-2.0
|
|
3
|
+
|
|
4
|
+
"""Schema utilities for YAML, TypeScript, and Pydantic processing."""
|
|
5
|
+
|
|
6
|
+
from ._breakdown_pydantic_annotation import (
|
|
7
|
+
breakdown_pydantic_annotation,
|
|
8
|
+
is_pydantic_model,
|
|
9
|
+
)
|
|
10
|
+
from ._formatter import (
|
|
11
|
+
format_clean_multiline_strings,
|
|
12
|
+
format_model_schema,
|
|
13
|
+
format_schema_pretty,
|
|
14
|
+
)
|
|
15
|
+
from ._minimal_yaml import minimal_yaml
|
|
16
|
+
from ._typescript import typescript_schema
|
|
17
|
+
|
|
18
|
+
__all__ = (
|
|
19
|
+
"breakdown_pydantic_annotation",
|
|
20
|
+
"is_pydantic_model",
|
|
21
|
+
"minimal_yaml",
|
|
22
|
+
"typescript_schema",
|
|
23
|
+
"format_model_schema",
|
|
24
|
+
"format_schema_pretty",
|
|
25
|
+
"format_clean_multiline_strings",
|
|
26
|
+
)
|
|
@@ -0,0 +1,131 @@
|
|
|
1
|
+
# Copyright (c) 2025 - 2026, HaiyangLi <quantocean.li at gmail dot com>
|
|
2
|
+
# SPDX-License-Identifier: Apache-2.0
|
|
3
|
+
|
|
4
|
+
import re
|
|
5
|
+
from inspect import isclass
|
|
6
|
+
from typing import Any, get_args, get_origin
|
|
7
|
+
|
|
8
|
+
from pydantic import BaseModel
|
|
9
|
+
|
|
10
|
+
# Pattern to match module-qualified names like __main__.Foo or lionagi.x.y.Bar
|
|
11
|
+
_MODULE_PATTERN = re.compile(r"([a-zA-Z_][a-zA-Z0-9_]*\.)+([a-zA-Z_][a-zA-Z0-9_]*)")
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
# Pattern to extract type name from <class 'typename'>
|
|
15
|
+
_CLASS_PATTERN = re.compile(r"<class '([^']+)'>")
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
def _clean_type_repr(t: Any) -> str:
|
|
19
|
+
"""Convert a type annotation to a clean Python-like string.
|
|
20
|
+
|
|
21
|
+
Handles:
|
|
22
|
+
- <class 'str'> -> "str"
|
|
23
|
+
- <class 'int'> -> "int"
|
|
24
|
+
- Module-qualified names -> just class name
|
|
25
|
+
"""
|
|
26
|
+
s = str(t) if not isinstance(t, str) else t
|
|
27
|
+
|
|
28
|
+
# Handle <class 'typename'> pattern
|
|
29
|
+
if match := _CLASS_PATTERN.match(s):
|
|
30
|
+
type_name = match.group(1)
|
|
31
|
+
# Strip module prefix if present (e.g., 'builtins.str' -> 'str')
|
|
32
|
+
return type_name.rsplit(".", 1)[-1]
|
|
33
|
+
|
|
34
|
+
# Replace module-qualified names with just the class name
|
|
35
|
+
s = _MODULE_PATTERN.sub(r"\2", s)
|
|
36
|
+
|
|
37
|
+
return s
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
# Default maximum recursion depth for safety
|
|
41
|
+
DEFAULT_MAX_DEPTH = 50
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
def breakdown_pydantic_annotation(
|
|
45
|
+
model: type[BaseModel],
|
|
46
|
+
max_depth: int | None = DEFAULT_MAX_DEPTH,
|
|
47
|
+
clean_types: bool = True,
|
|
48
|
+
) -> dict[str, Any]:
|
|
49
|
+
"""Break down a Pydantic model's annotations into a nested dict structure.
|
|
50
|
+
|
|
51
|
+
Args:
|
|
52
|
+
model: The Pydantic model class to break down.
|
|
53
|
+
max_depth: Maximum recursion depth for nested models (default: 50).
|
|
54
|
+
Set to None for unlimited depth (not recommended).
|
|
55
|
+
clean_types: If True, convert type annotations to clean strings
|
|
56
|
+
without module prefixes (e.g., 'list[CodeModule]' instead of
|
|
57
|
+
'list[__main__.CodeModule]').
|
|
58
|
+
|
|
59
|
+
Returns:
|
|
60
|
+
Dict mapping field names to type representations:
|
|
61
|
+
- Strings for simple types
|
|
62
|
+
- Dicts for nested Pydantic models
|
|
63
|
+
- Lists containing the above for list fields
|
|
64
|
+
|
|
65
|
+
Raises:
|
|
66
|
+
TypeError: If model is not a Pydantic BaseModel subclass.
|
|
67
|
+
RecursionError: If max_depth is exceeded during traversal.
|
|
68
|
+
"""
|
|
69
|
+
result = _breakdown_pydantic_annotation(
|
|
70
|
+
model=model,
|
|
71
|
+
max_depth=max_depth,
|
|
72
|
+
current_depth=0,
|
|
73
|
+
)
|
|
74
|
+
if clean_types:
|
|
75
|
+
return _clean_result(result)
|
|
76
|
+
return result
|
|
77
|
+
|
|
78
|
+
|
|
79
|
+
def _clean_result(result: dict[str, Any]) -> dict[str, Any]:
|
|
80
|
+
"""Recursively clean type representations in the result dict."""
|
|
81
|
+
out: dict[str, Any] = {}
|
|
82
|
+
for k, v in result.items():
|
|
83
|
+
if isinstance(v, dict):
|
|
84
|
+
out[k] = _clean_result(v)
|
|
85
|
+
elif isinstance(v, list) and v:
|
|
86
|
+
if isinstance(v[0], dict):
|
|
87
|
+
out[k] = [_clean_result(v[0])]
|
|
88
|
+
else:
|
|
89
|
+
out[k] = [_clean_type_repr(v[0])]
|
|
90
|
+
else:
|
|
91
|
+
out[k] = _clean_type_repr(v)
|
|
92
|
+
return out
|
|
93
|
+
|
|
94
|
+
|
|
95
|
+
def _breakdown_pydantic_annotation(
|
|
96
|
+
model: type[BaseModel],
|
|
97
|
+
max_depth: int | None = None,
|
|
98
|
+
current_depth: int = 0,
|
|
99
|
+
) -> dict[str, Any]:
|
|
100
|
+
if not is_pydantic_model(model):
|
|
101
|
+
raise TypeError("Input must be a Pydantic model")
|
|
102
|
+
|
|
103
|
+
if max_depth is not None and current_depth >= max_depth:
|
|
104
|
+
raise RecursionError("Maximum recursion depth reached")
|
|
105
|
+
|
|
106
|
+
out: dict[str, Any] = {}
|
|
107
|
+
for k, v in model.__annotations__.items():
|
|
108
|
+
origin = get_origin(v)
|
|
109
|
+
if is_pydantic_model(v):
|
|
110
|
+
out[k] = _breakdown_pydantic_annotation(v, max_depth, current_depth + 1)
|
|
111
|
+
elif origin is list:
|
|
112
|
+
args = get_args(v)
|
|
113
|
+
if args and is_pydantic_model(args[0]):
|
|
114
|
+
out[k] = [
|
|
115
|
+
_breakdown_pydantic_annotation(
|
|
116
|
+
args[0], max_depth, current_depth + 1
|
|
117
|
+
)
|
|
118
|
+
]
|
|
119
|
+
else:
|
|
120
|
+
out[k] = [args[0] if args else Any]
|
|
121
|
+
else:
|
|
122
|
+
out[k] = v
|
|
123
|
+
|
|
124
|
+
return out
|
|
125
|
+
|
|
126
|
+
|
|
127
|
+
def is_pydantic_model(x: Any) -> bool:
|
|
128
|
+
try:
|
|
129
|
+
return isclass(x) and issubclass(x, BaseModel)
|
|
130
|
+
except TypeError:
|
|
131
|
+
return False
|
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
import textwrap
|
|
2
|
+
|
|
3
|
+
from pydantic import BaseModel
|
|
4
|
+
|
|
5
|
+
from ._typescript import typescript_schema
|
|
6
|
+
|
|
7
|
+
__all__ = (
|
|
8
|
+
"format_model_schema",
|
|
9
|
+
"format_schema_pretty",
|
|
10
|
+
"format_clean_multiline_strings",
|
|
11
|
+
)
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
def format_model_schema(request_model: type[BaseModel]) -> str:
|
|
15
|
+
model_schema = request_model.model_json_schema()
|
|
16
|
+
schema_text = ""
|
|
17
|
+
if defs := model_schema.get("$defs"):
|
|
18
|
+
for def_name, def_schema in defs.items():
|
|
19
|
+
if def_ts := typescript_schema(def_schema):
|
|
20
|
+
schema_text += f"\n{def_name}:\n" + textwrap.indent(def_ts, " ")
|
|
21
|
+
return schema_text
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
def format_schema_pretty(schema: dict, indent: int = 0) -> str:
|
|
25
|
+
"""Format schema dict with unquoted Python type values."""
|
|
26
|
+
lines = ["{"]
|
|
27
|
+
items = list(schema.items())
|
|
28
|
+
for i, (key, value) in enumerate(items):
|
|
29
|
+
comma = "," if i < len(items) - 1 else ""
|
|
30
|
+
if isinstance(value, dict):
|
|
31
|
+
nested = format_schema_pretty(value, indent + 4)
|
|
32
|
+
lines.append(f'{" " * (indent + 4)}"{key}": {nested}{comma}')
|
|
33
|
+
elif isinstance(value, list) and value:
|
|
34
|
+
if isinstance(value[0], dict):
|
|
35
|
+
nested = format_schema_pretty(value[0], indent + 4)
|
|
36
|
+
lines.append(f'{" " * (indent + 4)}"{key}": [{nested}]{comma}')
|
|
37
|
+
else:
|
|
38
|
+
lines.append(f'{" " * (indent + 4)}"{key}": [{value[0]}]{comma}')
|
|
39
|
+
else:
|
|
40
|
+
lines.append(f'{" " * (indent + 4)}"{key}": {value}{comma}')
|
|
41
|
+
lines.append(f"{' ' * indent}}}")
|
|
42
|
+
return "\n".join(lines)
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
def format_clean_multiline_strings(data: dict) -> dict:
|
|
46
|
+
"""Clean multiline strings for YAML block scalars (| not |-)."""
|
|
47
|
+
cleaned: dict[str, object] = {}
|
|
48
|
+
for k, v in data.items():
|
|
49
|
+
if isinstance(v, str) and "\n" in v:
|
|
50
|
+
# Strip trailing whitespace from each line, ensure ends with newline for "|"
|
|
51
|
+
lines = "\n".join(line.rstrip() for line in v.split("\n"))
|
|
52
|
+
cleaned[k] = lines if lines.endswith("\n") else lines + "\n"
|
|
53
|
+
elif isinstance(v, list):
|
|
54
|
+
cleaned[k] = [
|
|
55
|
+
(
|
|
56
|
+
_clean_multiline(item)
|
|
57
|
+
if isinstance(item, str) and "\n" in item
|
|
58
|
+
else item
|
|
59
|
+
)
|
|
60
|
+
for item in v
|
|
61
|
+
]
|
|
62
|
+
elif isinstance(v, dict):
|
|
63
|
+
cleaned[k] = format_clean_multiline_strings(v)
|
|
64
|
+
else:
|
|
65
|
+
cleaned[k] = v
|
|
66
|
+
return cleaned
|
|
67
|
+
|
|
68
|
+
|
|
69
|
+
def _clean_multiline(s: str) -> str:
|
|
70
|
+
"""Clean a multiline string: strip line trailing whitespace, ensure final newline."""
|
|
71
|
+
lines = "\n".join(line.rstrip() for line in s.split("\n"))
|
|
72
|
+
return lines if lines.endswith("\n") else lines + "\n"
|