krons 0.1.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.
Files changed (101) hide show
  1. kronos/__init__.py +0 -0
  2. kronos/core/__init__.py +145 -0
  3. kronos/core/broadcaster.py +116 -0
  4. kronos/core/element.py +225 -0
  5. kronos/core/event.py +316 -0
  6. kronos/core/eventbus.py +116 -0
  7. kronos/core/flow.py +356 -0
  8. kronos/core/graph.py +442 -0
  9. kronos/core/node.py +982 -0
  10. kronos/core/pile.py +575 -0
  11. kronos/core/processor.py +494 -0
  12. kronos/core/progression.py +296 -0
  13. kronos/enforcement/__init__.py +57 -0
  14. kronos/enforcement/common/__init__.py +34 -0
  15. kronos/enforcement/common/boolean.py +85 -0
  16. kronos/enforcement/common/choice.py +97 -0
  17. kronos/enforcement/common/mapping.py +118 -0
  18. kronos/enforcement/common/model.py +102 -0
  19. kronos/enforcement/common/number.py +98 -0
  20. kronos/enforcement/common/string.py +140 -0
  21. kronos/enforcement/context.py +129 -0
  22. kronos/enforcement/policy.py +80 -0
  23. kronos/enforcement/registry.py +153 -0
  24. kronos/enforcement/rule.py +312 -0
  25. kronos/enforcement/service.py +370 -0
  26. kronos/enforcement/validator.py +198 -0
  27. kronos/errors.py +146 -0
  28. kronos/operations/__init__.py +32 -0
  29. kronos/operations/builder.py +228 -0
  30. kronos/operations/flow.py +398 -0
  31. kronos/operations/node.py +101 -0
  32. kronos/operations/registry.py +92 -0
  33. kronos/protocols.py +414 -0
  34. kronos/py.typed +0 -0
  35. kronos/services/__init__.py +81 -0
  36. kronos/services/backend.py +286 -0
  37. kronos/services/endpoint.py +608 -0
  38. kronos/services/hook.py +471 -0
  39. kronos/services/imodel.py +465 -0
  40. kronos/services/registry.py +115 -0
  41. kronos/services/utilities/__init__.py +36 -0
  42. kronos/services/utilities/header_factory.py +87 -0
  43. kronos/services/utilities/rate_limited_executor.py +271 -0
  44. kronos/services/utilities/rate_limiter.py +180 -0
  45. kronos/services/utilities/resilience.py +414 -0
  46. kronos/session/__init__.py +41 -0
  47. kronos/session/exchange.py +258 -0
  48. kronos/session/message.py +60 -0
  49. kronos/session/session.py +411 -0
  50. kronos/specs/__init__.py +25 -0
  51. kronos/specs/adapters/__init__.py +0 -0
  52. kronos/specs/adapters/_utils.py +45 -0
  53. kronos/specs/adapters/dataclass_field.py +246 -0
  54. kronos/specs/adapters/factory.py +56 -0
  55. kronos/specs/adapters/pydantic_adapter.py +309 -0
  56. kronos/specs/adapters/sql_ddl.py +946 -0
  57. kronos/specs/catalog/__init__.py +36 -0
  58. kronos/specs/catalog/_audit.py +39 -0
  59. kronos/specs/catalog/_common.py +43 -0
  60. kronos/specs/catalog/_content.py +59 -0
  61. kronos/specs/catalog/_enforcement.py +70 -0
  62. kronos/specs/factory.py +120 -0
  63. kronos/specs/operable.py +314 -0
  64. kronos/specs/phrase.py +405 -0
  65. kronos/specs/protocol.py +140 -0
  66. kronos/specs/spec.py +506 -0
  67. kronos/types/__init__.py +60 -0
  68. kronos/types/_sentinel.py +311 -0
  69. kronos/types/base.py +369 -0
  70. kronos/types/db_types.py +260 -0
  71. kronos/types/identity.py +66 -0
  72. kronos/utils/__init__.py +40 -0
  73. kronos/utils/_hash.py +234 -0
  74. kronos/utils/_json_dump.py +392 -0
  75. kronos/utils/_lazy_init.py +63 -0
  76. kronos/utils/_to_list.py +165 -0
  77. kronos/utils/_to_num.py +85 -0
  78. kronos/utils/_utils.py +375 -0
  79. kronos/utils/concurrency/__init__.py +205 -0
  80. kronos/utils/concurrency/_async_call.py +333 -0
  81. kronos/utils/concurrency/_cancel.py +122 -0
  82. kronos/utils/concurrency/_errors.py +96 -0
  83. kronos/utils/concurrency/_patterns.py +363 -0
  84. kronos/utils/concurrency/_primitives.py +328 -0
  85. kronos/utils/concurrency/_priority_queue.py +135 -0
  86. kronos/utils/concurrency/_resource_tracker.py +110 -0
  87. kronos/utils/concurrency/_run_async.py +67 -0
  88. kronos/utils/concurrency/_task.py +95 -0
  89. kronos/utils/concurrency/_utils.py +79 -0
  90. kronos/utils/fuzzy/__init__.py +14 -0
  91. kronos/utils/fuzzy/_extract_json.py +90 -0
  92. kronos/utils/fuzzy/_fuzzy_json.py +288 -0
  93. kronos/utils/fuzzy/_fuzzy_match.py +149 -0
  94. kronos/utils/fuzzy/_string_similarity.py +187 -0
  95. kronos/utils/fuzzy/_to_dict.py +396 -0
  96. kronos/utils/sql/__init__.py +13 -0
  97. kronos/utils/sql/_sql_validation.py +142 -0
  98. krons-0.1.0.dist-info/METADATA +70 -0
  99. krons-0.1.0.dist-info/RECORD +101 -0
  100. krons-0.1.0.dist-info/WHEEL +4 -0
  101. krons-0.1.0.dist-info/licenses/LICENSE +201 -0
@@ -0,0 +1,396 @@
1
+ # Copyright (c) 2025 - 2026, HaiyangLi <quantocean.li at gmail dot com>
2
+ # SPDX-License-Identifier: Apache-2.0
3
+
4
+ """Dictionary conversion utilities with recursive processing and JSON parsing."""
5
+
6
+ from __future__ import annotations
7
+
8
+ import contextlib
9
+ import dataclasses
10
+ from collections.abc import Callable, Iterable, Mapping, Sequence
11
+ from enum import Enum as _Enum
12
+ from typing import Any, cast
13
+
14
+ import orjson
15
+
16
+ from ._fuzzy_json import fuzzy_json
17
+
18
+ __all__ = ("to_dict",)
19
+
20
+
21
+ def to_dict(
22
+ input_: Any,
23
+ /,
24
+ *,
25
+ prioritize_model_dump: bool = False,
26
+ fuzzy_parse: bool = False,
27
+ suppress: bool = False,
28
+ parser: Callable[[str], Any] | None = None,
29
+ recursive: bool = False,
30
+ max_recursive_depth: int | None = None,
31
+ recursive_python_only: bool = True,
32
+ use_enum_values: bool = False,
33
+ **kwargs: Any,
34
+ ) -> dict[str | int, Any]:
35
+ """Convert input to dictionary with optional recursive processing.
36
+
37
+ Type handling:
38
+ - Mapping: copied to dict
39
+ - str: parsed as JSON (orjson or custom parser)
40
+ - set: {v: v for v in set}
41
+ - Enum class: {name: member} or {name: value} if use_enum_values
42
+ - list/tuple: {0: v0, 1: v1, ...} (enumerated)
43
+ - Pydantic BaseModel: model_dump() or dict-like conversion
44
+ - dataclass: dataclasses.asdict()
45
+ - objects with __dict__: returns __dict__
46
+ - None/Undefined: {}
47
+
48
+ Args:
49
+ input_: Object to convert.
50
+ prioritize_model_dump: Call .model_dump() first for Pydantic models.
51
+ fuzzy_parse: Use fuzzy_json() for malformed JSON strings.
52
+ suppress: Return {} on errors instead of raising.
53
+ parser: Custom parser(str, **kwargs) -> Any for string inputs.
54
+ recursive: Recursively process nested structures.
55
+ max_recursive_depth: Max depth (default 5, clamped to 10).
56
+ recursive_python_only: Only recurse into Python builtins (not custom objects).
57
+ use_enum_values: Use .value for Enum members.
58
+ **kwargs: Passed to parser and model_dump().
59
+
60
+ Returns:
61
+ Dictionary representation. Keys are str or int (for enumerated iterables).
62
+
63
+ Raises:
64
+ ValueError: max_recursive_depth negative or >10.
65
+ Exception: Conversion failure (unless suppress=True).
66
+
67
+ Edge Cases:
68
+ - Empty string with suppress=False: raises
69
+ - Empty string with suppress=True: returns {}
70
+ - Circular references: limited by max_recursive_depth
71
+ """
72
+ try:
73
+ if not isinstance(max_recursive_depth, int):
74
+ max_depth = 5
75
+ else:
76
+ if max_recursive_depth < 0:
77
+ raise ValueError("max_recursive_depth must be a non-negative integer")
78
+ if max_recursive_depth > 10:
79
+ raise ValueError("max_recursive_depth must be less than or equal to 10")
80
+ max_depth = max_recursive_depth
81
+
82
+ str_parse_opts = {
83
+ "fuzzy_parse": fuzzy_parse,
84
+ "parser": parser,
85
+ "use_enum_values": use_enum_values,
86
+ **kwargs,
87
+ }
88
+
89
+ obj = input_
90
+ if recursive:
91
+ obj = _preprocess_recursive(
92
+ obj,
93
+ depth=0,
94
+ max_depth=max_depth,
95
+ recursive_custom_types=not recursive_python_only,
96
+ str_parse_opts=str_parse_opts,
97
+ prioritize_model_dump=prioritize_model_dump,
98
+ )
99
+
100
+ return _convert_top_level_to_dict(
101
+ obj,
102
+ fuzzy_parse=fuzzy_parse,
103
+ parser=parser,
104
+ prioritize_model_dump=prioritize_model_dump,
105
+ use_enum_values=use_enum_values,
106
+ **kwargs,
107
+ )
108
+
109
+ except Exception as e:
110
+ if suppress or input_ == "":
111
+ return {}
112
+ raise e
113
+
114
+
115
+ def _is_na(obj: Any) -> bool:
116
+ """Check if obj is None or a Pydantic/kron undefined sentinel (by typename)."""
117
+ if obj is None:
118
+ return True
119
+ tname = type(obj).__name__
120
+ return tname in {
121
+ "Undefined",
122
+ "UndefinedType",
123
+ "PydanticUndefined",
124
+ "PydanticUndefinedType",
125
+ }
126
+
127
+
128
+ def _enum_class_to_dict(enum_cls: type[_Enum], use_enum_values: bool) -> dict[str, Any]:
129
+ """Convert Enum class to {name: member} or {name: value} dict."""
130
+ members = dict(enum_cls.__members__)
131
+ if use_enum_values:
132
+ return {k: v.value for k, v in members.items()}
133
+ return {k: v for k, v in members.items()}
134
+
135
+
136
+ def _parse_str(
137
+ s: str,
138
+ *,
139
+ fuzzy_parse: bool,
140
+ parser: Callable[[str], Any] | None,
141
+ **kwargs: Any,
142
+ ) -> Any:
143
+ """Parse string to Python object via JSON or custom parser.
144
+
145
+ Args:
146
+ s: String to parse.
147
+ fuzzy_parse: Use fuzzy_json() for malformed JSON.
148
+ parser: Custom parser(s, **kwargs). If provided, takes precedence.
149
+ **kwargs: Passed to custom parser only (orjson.loads ignores them).
150
+
151
+ Returns:
152
+ Parsed Python object.
153
+ """
154
+ if parser is not None:
155
+ return parser(s, **kwargs)
156
+ if fuzzy_parse:
157
+ with contextlib.suppress(NameError):
158
+ return fuzzy_json(s)
159
+ return orjson.loads(s)
160
+
161
+
162
+ def _object_to_mapping_like(
163
+ obj: Any,
164
+ *,
165
+ prioritize_model_dump: bool = False,
166
+ **kwargs: Any,
167
+ ) -> Mapping | dict | Any:
168
+ """Convert custom object to mapping-like via duck-typing.
169
+
170
+ Conversion order:
171
+ 1. Pydantic model_dump() (if prioritize_model_dump)
172
+ 2. Common methods: to_dict, model_dump, dict, to_json, json
173
+ 3. dataclasses.asdict()
174
+ 4. __dict__ attribute
175
+ 5. dict(obj) fallback
176
+
177
+ Args:
178
+ obj: Object to convert.
179
+ prioritize_model_dump: Try model_dump() first.
180
+ **kwargs: Passed to conversion methods.
181
+
182
+ Returns:
183
+ Mapping-like object or dict.
184
+
185
+ Raises:
186
+ TypeError: If obj is not convertible (from dict() fallback).
187
+ """
188
+ if prioritize_model_dump and hasattr(obj, "model_dump"):
189
+ return obj.model_dump(**kwargs)
190
+
191
+ for name in ("to_dict", "model_dump", "dict", "to_json", "json"):
192
+ if hasattr(obj, name):
193
+ res = getattr(obj, name)(**kwargs)
194
+ return orjson.loads(res) if isinstance(res, str) else res
195
+
196
+ if dataclasses.is_dataclass(obj) and not isinstance(obj, type):
197
+ return dataclasses.asdict(obj)
198
+
199
+ if hasattr(obj, "__dict__"):
200
+ return obj.__dict__
201
+
202
+ return dict(obj)
203
+
204
+
205
+ def _enumerate_iterable(it: Iterable) -> dict[int, Any]:
206
+ """Convert iterable to dict with integer indices as keys."""
207
+ return {i: v for i, v in enumerate(it)}
208
+
209
+
210
+ def _preprocess_recursive(
211
+ obj: Any,
212
+ *,
213
+ depth: int,
214
+ max_depth: int,
215
+ recursive_custom_types: bool,
216
+ str_parse_opts: dict[str, Any],
217
+ prioritize_model_dump: bool,
218
+ ) -> Any:
219
+ """Recursively preprocess nested structures before final conversion.
220
+
221
+ Processing:
222
+ - Strings: parsed as JSON, then recursed
223
+ - Mappings: recurse into values (keys unchanged)
224
+ - list/tuple/set/frozenset: recurse into items, preserve container type
225
+ - Enum classes: convert to dict, then recurse
226
+ - Custom objects (if recursive_custom_types): convert to mapping, then recurse
227
+
228
+ Args:
229
+ obj: Object to process.
230
+ depth: Current recursion depth.
231
+ max_depth: Maximum depth (stops recursion when reached).
232
+ recursive_custom_types: Also convert custom objects.
233
+ str_parse_opts: Options for _parse_str().
234
+ prioritize_model_dump: Passed to _object_to_mapping_like().
235
+
236
+ Returns:
237
+ Preprocessed object with same container types.
238
+ """
239
+ if depth >= max_depth:
240
+ return obj
241
+
242
+ # Fast paths by exact type where possible
243
+ t = type(obj)
244
+
245
+ # Strings: try to parse; on failure, keep as-is
246
+ if t is str:
247
+ with contextlib.suppress(Exception):
248
+ return _preprocess_recursive(
249
+ _parse_str(obj, **str_parse_opts),
250
+ depth=depth + 1,
251
+ max_depth=max_depth,
252
+ recursive_custom_types=recursive_custom_types,
253
+ str_parse_opts=str_parse_opts,
254
+ prioritize_model_dump=prioritize_model_dump,
255
+ )
256
+ return obj
257
+
258
+ # Dict-like
259
+ if isinstance(obj, Mapping):
260
+ # Recurse only into values (keys kept as-is)
261
+ return {
262
+ k: _preprocess_recursive(
263
+ v,
264
+ depth=depth + 1,
265
+ max_depth=max_depth,
266
+ recursive_custom_types=recursive_custom_types,
267
+ str_parse_opts=str_parse_opts,
268
+ prioritize_model_dump=prioritize_model_dump,
269
+ )
270
+ for k, v in obj.items()
271
+ }
272
+
273
+ # Sequence/Set-like (but not str)
274
+ if isinstance(obj, list | tuple | set | frozenset):
275
+ items = [
276
+ _preprocess_recursive(
277
+ v,
278
+ depth=depth + 1,
279
+ max_depth=max_depth,
280
+ recursive_custom_types=recursive_custom_types,
281
+ str_parse_opts=str_parse_opts,
282
+ prioritize_model_dump=prioritize_model_dump,
283
+ )
284
+ for v in obj
285
+ ]
286
+ if t is list:
287
+ return items
288
+ if t is tuple:
289
+ return tuple(items)
290
+ if t is set:
291
+ return set(items)
292
+ if t is frozenset:
293
+ return frozenset(items)
294
+
295
+ if isinstance(obj, type) and issubclass(obj, _Enum):
296
+ with contextlib.suppress(Exception):
297
+ enum_map = _enum_class_to_dict(
298
+ obj,
299
+ use_enum_values=str_parse_opts.get("use_enum_values", True),
300
+ )
301
+ return _preprocess_recursive(
302
+ enum_map,
303
+ depth=depth + 1,
304
+ max_depth=max_depth,
305
+ recursive_custom_types=recursive_custom_types,
306
+ str_parse_opts=str_parse_opts,
307
+ prioritize_model_dump=prioritize_model_dump,
308
+ )
309
+ return obj
310
+
311
+ if recursive_custom_types:
312
+ with contextlib.suppress(Exception):
313
+ mapped = _object_to_mapping_like(obj, prioritize_model_dump=prioritize_model_dump)
314
+ return _preprocess_recursive(
315
+ mapped,
316
+ depth=depth + 1,
317
+ max_depth=max_depth,
318
+ recursive_custom_types=recursive_custom_types,
319
+ str_parse_opts=str_parse_opts,
320
+ prioritize_model_dump=prioritize_model_dump,
321
+ )
322
+
323
+ return obj
324
+
325
+
326
+ def _convert_top_level_to_dict(
327
+ obj: Any,
328
+ *,
329
+ fuzzy_parse: bool,
330
+ parser: Callable[[str], Any] | None,
331
+ prioritize_model_dump: bool,
332
+ use_enum_values: bool,
333
+ **kwargs: Any,
334
+ ) -> dict[str | int, Any]:
335
+ """Convert single object to dict (final conversion step).
336
+
337
+ Conversion order:
338
+ 1. set -> {v: v}
339
+ 2. Enum class -> {name: member/value}
340
+ 3. Mapping -> dict(obj)
341
+ 4. None/undefined -> {}
342
+ 5. str -> parse as JSON
343
+ 6. Non-sequence objects -> _object_to_mapping_like
344
+ 7. Iterables -> enumerate to {int: value}
345
+ 8. Dataclass fallback
346
+ 9. dict(obj) last resort
347
+
348
+ Args:
349
+ obj: Object to convert.
350
+ fuzzy_parse: Use fuzzy JSON parsing.
351
+ parser: Custom string parser.
352
+ prioritize_model_dump: Prefer model_dump() for Pydantic.
353
+ use_enum_values: Use .value for Enum members.
354
+ **kwargs: Passed to conversion methods.
355
+
356
+ Returns:
357
+ Dictionary with str or int keys.
358
+ """
359
+ if isinstance(obj, set):
360
+ return cast(dict[str | int, Any], {v: v for v in obj})
361
+
362
+ if isinstance(obj, type) and issubclass(obj, _Enum):
363
+ return cast(dict[str | int, Any], _enum_class_to_dict(obj, use_enum_values))
364
+
365
+ if isinstance(obj, Mapping):
366
+ return cast(dict[str | int, Any], dict(obj))
367
+
368
+ if _is_na(obj):
369
+ return cast(dict[str | int, Any], {})
370
+
371
+ if isinstance(obj, str):
372
+ return _parse_str(obj, fuzzy_parse=fuzzy_parse, parser=parser, **kwargs)
373
+
374
+ with contextlib.suppress(Exception):
375
+ if not isinstance(obj, Sequence):
376
+ converted = _object_to_mapping_like(
377
+ obj, prioritize_model_dump=prioritize_model_dump, **kwargs
378
+ )
379
+ if isinstance(converted, str):
380
+ return _parse_str(converted, fuzzy_parse=fuzzy_parse, parser=None)
381
+ if isinstance(converted, Mapping):
382
+ return dict(converted)
383
+ if isinstance(converted, Iterable) and not isinstance(
384
+ converted, str | bytes | bytearray
385
+ ):
386
+ return cast(dict[str | int, Any], _enumerate_iterable(converted))
387
+ return dict(converted)
388
+
389
+ if isinstance(obj, Iterable) and not isinstance(obj, str | bytes | bytearray):
390
+ return cast(dict[str | int, Any], _enumerate_iterable(obj))
391
+
392
+ with contextlib.suppress(Exception):
393
+ if dataclasses.is_dataclass(obj) and not isinstance(obj, type):
394
+ return cast(dict[str | int, Any], dataclasses.asdict(obj))
395
+
396
+ return dict(obj)
@@ -0,0 +1,13 @@
1
+ from ._sql_validation import (
2
+ MAX_IDENTIFIER_LENGTH,
3
+ SAFE_IDENTIFIER_PATTERN,
4
+ sanitize_order_by,
5
+ validate_identifier,
6
+ )
7
+
8
+ __all__ = (
9
+ "validate_identifier",
10
+ "sanitize_order_by",
11
+ "MAX_IDENTIFIER_LENGTH",
12
+ "SAFE_IDENTIFIER_PATTERN",
13
+ )
@@ -0,0 +1,142 @@
1
+ # Copyright (c) 2025 - 2026, HaiyangLi <quantocean.li at gmail dot com>
2
+ # SPDX-License-Identifier: Apache-2.0
3
+
4
+ """SQL validation utilities for injection prevention.
5
+
6
+ Validates SQL identifiers and clauses before interpolation into DDL/queries.
7
+ """
8
+
9
+ from __future__ import annotations
10
+
11
+ import re
12
+
13
+ from kronos.errors import ValidationError
14
+
15
+ __all__ = (
16
+ "validate_identifier",
17
+ "sanitize_order_by",
18
+ "MAX_IDENTIFIER_LENGTH",
19
+ "SAFE_IDENTIFIER_PATTERN",
20
+ )
21
+
22
+ # SQL identifier: alphanumeric and underscores, starting with letter/underscore
23
+ SAFE_IDENTIFIER_PATTERN = re.compile(r"^[a-zA-Z_][a-zA-Z0-9_]*$")
24
+ VALID_DIRECTIONS = frozenset({"ASC", "DESC"})
25
+ MAX_IDENTIFIER_LENGTH = 63 # PostgreSQL limit
26
+
27
+
28
+ def validate_identifier(name: str, kind: str = "identifier") -> str:
29
+ """Validate SQL identifier to prevent injection.
30
+
31
+ Checks that identifier:
32
+ - Is non-empty
33
+ - Does not exceed PostgreSQL's 63 character limit
34
+ - Contains only alphanumeric characters and underscores
35
+ - Starts with a letter or underscore
36
+
37
+ Args:
38
+ name: The identifier to validate.
39
+ kind: Description for error messages (e.g., "table", "column").
40
+
41
+ Returns:
42
+ The validated identifier (unchanged if valid).
43
+
44
+ Raises:
45
+ ValidationError: If identifier is empty, too long, or contains unsafe chars.
46
+
47
+ Example:
48
+ >>> validate_identifier("user_name", "column")
49
+ 'user_name'
50
+ >>> validate_identifier("123bad", "table") # Raises ValidationError
51
+ """
52
+ if not name:
53
+ raise ValidationError(
54
+ f"Empty {kind} name not allowed",
55
+ details={"kind": kind, "value": name},
56
+ )
57
+
58
+ if len(name) > MAX_IDENTIFIER_LENGTH:
59
+ raise ValidationError(
60
+ f"{kind.capitalize()} identifier too long: {name!r} ({len(name)} chars). "
61
+ f"Maximum is {MAX_IDENTIFIER_LENGTH} characters.",
62
+ details={"kind": kind, "value": name, "length": len(name)},
63
+ )
64
+
65
+ if not SAFE_IDENTIFIER_PATTERN.match(name):
66
+ raise ValidationError(
67
+ f"Unsafe {kind} identifier: {name!r}. "
68
+ f"Must be alphanumeric/underscore, starting with letter or underscore.",
69
+ details={"kind": kind, "value": name},
70
+ )
71
+
72
+ return name
73
+
74
+
75
+ def sanitize_order_by(order_by: str) -> str:
76
+ """Sanitize ORDER BY clause to prevent SQL injection.
77
+
78
+ Accepts formats:
79
+ - "column"
80
+ - "column ASC"
81
+ - "column DESC"
82
+ - "column1, column2 DESC"
83
+
84
+ Returns safely quoted SQL fragment.
85
+
86
+ Args:
87
+ order_by: The ORDER BY clause to sanitize.
88
+
89
+ Returns:
90
+ Sanitized ORDER BY clause with quoted identifiers.
91
+
92
+ Raises:
93
+ ValidationError: If column name or direction is invalid.
94
+
95
+ Example:
96
+ >>> sanitize_order_by("name, created_at DESC")
97
+ '"name" ASC, "created_at" DESC'
98
+ """
99
+ parts: list[str] = []
100
+
101
+ for clause in order_by.split(","):
102
+ clause = clause.strip()
103
+ if not clause:
104
+ continue
105
+
106
+ tokens = clause.split()
107
+ if len(tokens) == 1:
108
+ column = tokens[0]
109
+ direction = "ASC"
110
+ elif len(tokens) == 2:
111
+ column, direction = tokens
112
+ direction = direction.upper()
113
+ else:
114
+ raise ValidationError(
115
+ f"Invalid ORDER BY clause: {clause!r}. Expected 'column' or 'column ASC/DESC'.",
116
+ details={"clause": clause},
117
+ )
118
+
119
+ # Validate column name
120
+ if not SAFE_IDENTIFIER_PATTERN.match(column):
121
+ raise ValidationError(
122
+ f"Invalid column in ORDER BY: {column!r}. "
123
+ "Must be alphanumeric/underscore identifier.",
124
+ details={"column": column},
125
+ )
126
+
127
+ # Validate direction
128
+ if direction not in VALID_DIRECTIONS:
129
+ raise ValidationError(
130
+ f"Invalid direction in ORDER BY: {direction!r}. Must be ASC or DESC.",
131
+ details={"direction": direction},
132
+ )
133
+
134
+ parts.append(f'"{column}" {direction}')
135
+
136
+ if not parts:
137
+ raise ValidationError(
138
+ f"Empty ORDER BY clause: {order_by!r}",
139
+ details={"order_by": order_by},
140
+ )
141
+
142
+ return ", ".join(parts)
@@ -0,0 +1,70 @@
1
+ Metadata-Version: 2.4
2
+ Name: krons
3
+ Version: 0.1.0
4
+ Summary: Spec-based composable framework for building type-safe systems
5
+ Project-URL: Homepage, https://github.com/khive-ai/kronos
6
+ Project-URL: Repository, https://github.com/khive-ai/kronos
7
+ Project-URL: Issues, https://github.com/khive-ai/kronos/issues
8
+ Author-email: HaiyangLi <quantocean.li@gmail.com>
9
+ License-Expression: Apache-2.0
10
+ License-File: LICENSE
11
+ Keywords: composable,framework,pydantic,spec,type-safe,validation
12
+ Classifier: Development Status :: 4 - Beta
13
+ Classifier: Intended Audience :: Developers
14
+ Classifier: License :: OSI Approved :: Apache Software License
15
+ Classifier: Programming Language :: Python :: 3
16
+ Classifier: Programming Language :: Python :: 3.11
17
+ Classifier: Programming Language :: Python :: 3.12
18
+ Classifier: Topic :: Software Development :: Libraries :: Python Modules
19
+ Classifier: Typing :: Typed
20
+ Requires-Python: >=3.11
21
+ Requires-Dist: anyio>=4.10.0
22
+ Requires-Dist: httpx>=0.26.0
23
+ Requires-Dist: orjson>=3.10.0
24
+ Requires-Dist: pydantic>=2.10.0
25
+ Requires-Dist: rapidfuzz>=3.10.0
26
+ Description-Content-Type: text/markdown
27
+
28
+ # krons
29
+
30
+ Spec-based composable framework for building type-safe systems.
31
+
32
+ ## Installation
33
+
34
+ ```bash
35
+ pip install krons
36
+ ```
37
+
38
+ ## Features
39
+
40
+ - **Spec/Operable**: Type-safe field definitions with validation, defaults, and DB metadata
41
+ - **Node**: Polymorphic content containers with DB serialization
42
+ - **Services**: Unified service interfaces (iModel) with hooks and rate limiting
43
+ - **Enforcement**: Policy evaluation and action handlers with typed I/O
44
+ - **Protocols**: Runtime-checkable protocols with `@implements` decorator
45
+
46
+ ## Quick Start
47
+
48
+ ```python
49
+ from kronos.specs import Spec, Operable
50
+
51
+ # Define specs
52
+ name_spec = Spec(str, name="name")
53
+ count_spec = Spec(int, name="count", default=0, ge=0)
54
+
55
+ # Compose into structure
56
+ operable = Operable([name_spec, count_spec])
57
+ MyModel = operable.compose_structure("MyModel")
58
+ ```
59
+
60
+ ## Requirements
61
+
62
+ - Python 3.11+
63
+ - pydantic 2.x
64
+ - anyio
65
+ - httpx
66
+ - orjson
67
+
68
+ ## License
69
+
70
+ Apache-2.0