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
kronos/specs/spec.py ADDED
@@ -0,0 +1,506 @@
1
+ # Copyright (c) 2025 - 2026, HaiyangLi <quantocean.li at gmail dot com>
2
+ # SPDX-License-Identifier: Apache-2.0
3
+
4
+ from __future__ import annotations
5
+
6
+ import contextlib
7
+ import os
8
+ import threading
9
+ from collections import OrderedDict
10
+ from collections.abc import Callable
11
+ from dataclasses import dataclass
12
+ from typing import Annotated, Any, Self
13
+
14
+ from kronos.protocols import Hashable, implements
15
+ from kronos.types._sentinel import (
16
+ MaybeUndefined,
17
+ Undefined,
18
+ is_sentinel,
19
+ is_undefined,
20
+ not_sentinel,
21
+ )
22
+ from kronos.types.base import Enum, Meta
23
+ from kronos.utils.concurrency import is_coro_func
24
+
25
+ # Global cache for annotated types with bounded size
26
+ _MAX_CACHE_SIZE = int(os.environ.get("kron_FIELD_CACHE_SIZE", "10000"))
27
+ _annotated_cache: OrderedDict[tuple[type, tuple[Meta, ...]], type] = OrderedDict()
28
+ _cache_lock = threading.RLock() # Thread-safe access to cache
29
+
30
+
31
+ __all__ = ("CommonMeta", "Spec")
32
+
33
+
34
+ class CommonMeta(Enum):
35
+ """Standard metadata keys for Spec field configuration.
36
+
37
+ Keys:
38
+ NAME: Field identifier for serialization/composition
39
+ NULLABLE: Allows None values (becomes T | None)
40
+ LISTABLE: Wraps type in list[T]
41
+ VALIDATOR: Callable(s) for value validation
42
+ DEFAULT: Static default value
43
+ DEFAULT_FACTORY: Callable producing default (mutually exclusive with DEFAULT)
44
+ FROZEN: Marks field as immutable after creation
45
+ AS_FK: Foreign key target (str model name or type). When set,
46
+ annotated() includes FKMeta in the Annotated type.
47
+
48
+ Used by Spec to define field semantics in a framework-agnostic way.
49
+ Adapters translate these to framework-specific constructs.
50
+ """
51
+
52
+ NAME = "name"
53
+ NULLABLE = "nullable"
54
+ LISTABLE = "listable"
55
+ VALIDATOR = "validator"
56
+ DEFAULT = "default"
57
+ DEFAULT_FACTORY = "default_factory"
58
+ FROZEN = "frozen"
59
+ AS_FK = "as_fk"
60
+
61
+ @classmethod
62
+ def _validate_common_metas(cls, **kw):
63
+ """Validate metadata constraints. Raises ExceptionGroup for multiple errors."""
64
+ errors: list[Exception] = []
65
+
66
+ if kw.get("default") and kw.get("default_factory"):
67
+ errors.append(ValueError("Cannot provide both 'default' and 'default_factory'"))
68
+ if (_df := kw.get("default_factory")) and not callable(_df):
69
+ errors.append(ValueError("'default_factory' must be callable"))
70
+ if _val := kw.get("validator"):
71
+ _val = [_val] if not isinstance(_val, list) else _val
72
+ if not all(callable(v) for v in _val):
73
+ errors.append(ValueError("Validators must be a list of functions or a function"))
74
+
75
+ if errors:
76
+ raise ExceptionGroup("Metadata validation failed", errors)
77
+
78
+ @classmethod
79
+ def prepare(
80
+ cls, *args: Meta, metadata: tuple[Meta, ...] | None = None, **kw: Any
81
+ ) -> tuple[Meta, ...]:
82
+ """Prepare metadata tuple from args/kw. Validates no duplicates, constraints."""
83
+ # Lazy import to avoid circular dependency
84
+ from kronos.utils._to_list import to_list
85
+
86
+ seen_keys = set()
87
+ metas = []
88
+
89
+ if metadata:
90
+ for meta in metadata:
91
+ if meta.key in seen_keys:
92
+ raise ValueError(f"Duplicate metadata key: {meta.key}")
93
+ seen_keys.add(meta.key)
94
+ metas.append(meta)
95
+
96
+ if args:
97
+ _args = to_list(args, flatten=True, flatten_tuple_set=True, dropna=True)
98
+ for meta in _args:
99
+ if meta.key in seen_keys:
100
+ raise ValueError(f"Duplicate metadata key: {meta.key}")
101
+ seen_keys.add(meta.key)
102
+ metas.append(meta)
103
+
104
+ for k, v in kw.items():
105
+ if k in seen_keys:
106
+ raise ValueError(f"Duplicate metadata key: {k}")
107
+ seen_keys.add(k)
108
+ metas.append(Meta(k, v))
109
+
110
+ meta_dict = {m.key: m.value for m in metas}
111
+ cls._validate_common_metas(**meta_dict)
112
+
113
+ return tuple(metas)
114
+
115
+
116
+ @implements(Hashable)
117
+ @dataclass(frozen=True, slots=True, init=False)
118
+ class Spec:
119
+ """Framework-agnostic field specification for type-safe data modeling.
120
+
121
+ Spec is the fundamental building block for defining typed fields that can be
122
+ translated to any target framework (Pydantic, SQL, dataclass, etc.) via adapters.
123
+
124
+ Design:
125
+ - Immutable: frozen dataclass ensures hashability and cacheability
126
+ - Composable: chain methods (as_nullable, with_default) for derived specs
127
+ - Adapter-agnostic: metadata interpreted by SpecAdapter implementations
128
+
129
+ Attributes:
130
+ base_type: The Python type (int, str, custom class, generic like list[str])
131
+ metadata: Tuple of Meta(key, value) pairs defining field semantics
132
+
133
+ Usage:
134
+ # Basic field
135
+ name_spec = Spec(str, name="username")
136
+
137
+ # With modifiers
138
+ tags_spec = Spec(str, name="tags").as_listable().as_nullable()
139
+
140
+ # With validation
141
+ age_spec = Spec(int, name="age", validator=lambda x: x >= 0)
142
+
143
+ # Convert to framework type
144
+ annotated_type = spec.annotated() # -> Annotated[str, Meta(...)]
145
+
146
+ Adapter Integration:
147
+ Specs are collected in Operable, then composed via adapter:
148
+ >>> op = Operable([name_spec, age_spec], adapter="pydantic")
149
+ >>> Model = op.compose_structure("User") # -> Pydantic BaseModel
150
+
151
+ See Also:
152
+ CommonMeta: Standard metadata keys
153
+ Operable: Spec collection with adapter interface
154
+ """
155
+
156
+ base_type: type
157
+ metadata: tuple[Meta, ...]
158
+
159
+ def __init__(
160
+ self,
161
+ base_type: type | None = None,
162
+ *args,
163
+ metadata: tuple[Meta, ...] | None = None,
164
+ **kw,
165
+ ) -> None:
166
+ """Initialize Spec with type and metadata.
167
+
168
+ Args:
169
+ base_type: Python type or type annotation (int, str, list[T], etc.)
170
+ *args: Meta objects to include in metadata
171
+ metadata: Pre-built metadata tuple (merged with args/kw)
172
+ **kw: Key-value pairs converted to Meta objects
173
+
174
+ Raises:
175
+ ValueError: If base_type is not a valid type, name is invalid,
176
+ or conflicting defaults provided
177
+ """
178
+ metas = CommonMeta.prepare(*args, metadata=metadata, **kw)
179
+
180
+ meta_dict = {m.key: m.value for m in metas}
181
+ if "name" in meta_dict:
182
+ name_value = meta_dict["name"]
183
+ if not isinstance(name_value, str) or not name_value:
184
+ raise ValueError("Spec name must be a non-empty string")
185
+
186
+ if not_sentinel(base_type, {"none"}):
187
+ import types
188
+
189
+ is_valid_type = (
190
+ isinstance(base_type, type)
191
+ or hasattr(base_type, "__origin__")
192
+ or isinstance(base_type, types.UnionType)
193
+ )
194
+ if not is_valid_type:
195
+ raise ValueError(f"base_type must be a type or type annotation, got {base_type}")
196
+
197
+ if kw.get("default_factory") and is_coro_func(kw["default_factory"]):
198
+ import warnings
199
+
200
+ warnings.warn(
201
+ "Async default factories are not yet fully supported by all adapters. "
202
+ "Consider using sync factories for compatibility.",
203
+ UserWarning,
204
+ stacklevel=2,
205
+ )
206
+
207
+ object.__setattr__(self, "base_type", base_type)
208
+ object.__setattr__(self, "metadata", metas)
209
+
210
+ def __getitem__(self, key: str) -> Any:
211
+ """Get metadata value by key.
212
+
213
+ Raises:
214
+ KeyError: If key not found in metadata
215
+ """
216
+ for meta in self.metadata:
217
+ if meta.key == key:
218
+ return meta.value
219
+ raise KeyError(f"Metadata key '{key}' undefined in Spec.")
220
+
221
+ def get(self, key: str, default: Any = Undefined) -> Any:
222
+ """Get metadata value by key, returning default if not found."""
223
+ with contextlib.suppress(KeyError):
224
+ return self[key]
225
+ return default
226
+
227
+ @property
228
+ def name(self) -> MaybeUndefined[str]:
229
+ """Get the field name from metadata."""
230
+ return self.get(CommonMeta.NAME.value)
231
+
232
+ @property
233
+ def is_nullable(self) -> bool:
234
+ """Check if field is nullable."""
235
+ return self.get(CommonMeta.NULLABLE.value) is True
236
+
237
+ @property
238
+ def is_listable(self) -> bool:
239
+ """Check if field is listable."""
240
+ return self.get(CommonMeta.LISTABLE.value) is True
241
+
242
+ @property
243
+ def default(self) -> MaybeUndefined[Any]:
244
+ """Get default value or factory."""
245
+ return self.get(
246
+ CommonMeta.DEFAULT.value,
247
+ self.get(CommonMeta.DEFAULT_FACTORY.value),
248
+ )
249
+
250
+ @property
251
+ def has_default_factory(self) -> bool:
252
+ """Check if this spec has a default factory."""
253
+ return _is_factory(self.get(CommonMeta.DEFAULT_FACTORY.value))[0]
254
+
255
+ @property
256
+ def has_async_default_factory(self) -> bool:
257
+ """Check if this spec has an async default factory."""
258
+ return _is_factory(self.get(CommonMeta.DEFAULT_FACTORY.value))[1]
259
+
260
+ @property
261
+ def is_frozen(self) -> bool:
262
+ """Check if this spec is marked as frozen (immutable)."""
263
+ return self.get(CommonMeta.FROZEN.value) is True
264
+
265
+ @property
266
+ def is_fk(self) -> bool:
267
+ """Check if this spec is marked as a foreign key."""
268
+ val = self.get(CommonMeta.AS_FK.value)
269
+ return not is_undefined(val) and val is not False
270
+
271
+ @property
272
+ def fk_target(self) -> MaybeUndefined[str | type]:
273
+ """Get the FK target model reference (str name or type).
274
+
275
+ Resolution order:
276
+ 1. Explicit target (str or type) from as_fk(target)
277
+ 2. base_type itself if Observable (has UUID id)
278
+ 3. Undefined otherwise
279
+ """
280
+ val = self.get(CommonMeta.AS_FK.value)
281
+ if is_undefined(val) or val is False:
282
+ return Undefined
283
+ if val is not True:
284
+ return val
285
+ # as_fk=True: resolve from base_type if Observable
286
+ if isinstance(self.base_type, type) and _is_observable(self.base_type):
287
+ return self.base_type
288
+ return Undefined
289
+
290
+ def as_fk(self, target: str | type | None = None) -> Self:
291
+ """Return new Spec marked as a foreign key.
292
+
293
+ Args:
294
+ target: Referenced model (str name or type). When provided,
295
+ annotated() will include FKMeta(target) in the Annotated type.
296
+ If None and base_type is Observable, target resolves to base_type.
297
+
298
+ Example:
299
+ >>> Spec(UUID, name="user_id").as_fk("User")
300
+ >>> Spec(Person, name="person_id").as_fk() # target = Person
301
+ """
302
+ return self.with_updates(as_fk=target if target is not None else True)
303
+
304
+ def as_frozen(self) -> Self:
305
+ """Return new Spec with frozen=True metadata."""
306
+ return self.with_updates(frozen=True)
307
+
308
+ def create_default_value(self) -> Any:
309
+ """Create default value (sync). Raises ValueError if no default or async factory."""
310
+ if is_undefined(self.default):
311
+ raise ValueError("No default value or factory defined in Spec.")
312
+ if self.has_async_default_factory:
313
+ raise ValueError(
314
+ "Default factory is asynchronous; cannot create default synchronously. "
315
+ "Use 'await spec.acreate_default_value()' instead."
316
+ )
317
+ if self.has_default_factory:
318
+ return self.default() # type: ignore[operator]
319
+ return self.default
320
+
321
+ async def acreate_default_value(self) -> Any:
322
+ """Create default value (async). Handles both sync/async factories."""
323
+ if self.has_async_default_factory:
324
+ return await self.default() # type: ignore[operator]
325
+ return self.create_default_value()
326
+
327
+ def with_updates(self, **kw) -> Self:
328
+ """Create new Spec with updated/added metadata keys. Sentinel values are excluded."""
329
+ _filtered = [meta for meta in self.metadata if meta.key not in kw]
330
+ for k, v in kw.items():
331
+ if not_sentinel(v):
332
+ _filtered.append(Meta(k, v))
333
+ _metas = tuple(_filtered)
334
+ return type(self)(self.base_type, metadata=_metas)
335
+
336
+ def as_nullable(self) -> Self:
337
+ """Return new Spec with nullable=True (allows None values)."""
338
+ return self.with_updates(nullable=True)
339
+
340
+ def as_listable(self) -> Self:
341
+ """Return new Spec with listable=True (wraps type in list[T])."""
342
+ return self.with_updates(listable=True)
343
+
344
+ def as_optional(self) -> Self:
345
+ """Return new Spec that is nullable with default=None."""
346
+ return self.as_nullable().with_default(None)
347
+
348
+ def with_default(self, default: Any) -> Self:
349
+ """Return new Spec with default. Callables become default_factory."""
350
+ if callable(default):
351
+ return self.with_updates(default_factory=default)
352
+ return self.with_updates(default=default)
353
+
354
+ @classmethod
355
+ def from_model(
356
+ cls,
357
+ model: type,
358
+ name: str | None = None,
359
+ *,
360
+ nullable: bool = False,
361
+ listable: bool = False,
362
+ default: Any = Undefined,
363
+ ) -> Self:
364
+ """Create Spec from a model class (e.g., Pydantic BaseModel).
365
+
366
+ Args:
367
+ model: The model class to use as base_type
368
+ name: Field name (defaults to lowercase class name)
369
+ nullable: Whether field is nullable
370
+ listable: Whether field is a list
371
+ default: Default value (Undefined means no default)
372
+
373
+ Returns:
374
+ Spec configured for the model type
375
+
376
+ Example:
377
+ >>> Spec.from_model(ProgressReport) # name="progressreport"
378
+ >>> Spec.from_model(CodeBlock, name="blocks", listable=True, nullable=True)
379
+ """
380
+ field_name = name if name is not None else model.__name__.lower()
381
+ spec = cls(base_type=model, name=field_name)
382
+
383
+ if listable:
384
+ spec = spec.as_listable()
385
+ if nullable:
386
+ spec = spec.as_nullable()
387
+ if not_sentinel(default):
388
+ spec = spec.with_default(default)
389
+
390
+ return spec
391
+
392
+ def with_validator(self, validator: Callable[..., Any] | list[Callable[..., Any]]) -> Self:
393
+ """Return new Spec with validator function(s) attached."""
394
+ return self.with_updates(validator=validator)
395
+
396
+ @property
397
+ def annotation(self) -> type[Any]:
398
+ """Type annotation with fk/listable/nullable modifiers applied.
399
+
400
+ When FK target resolves, produces FK[target] = Annotated[UUID, FKMeta(target)].
401
+ Order: FK[target] -> list[T] -> T | None
402
+ """
403
+ if is_sentinel(self.base_type, {"none"}):
404
+ return Any
405
+ t_ = self.base_type # type: ignore[valid-type]
406
+ fk = self.fk_target
407
+ if not is_undefined(fk):
408
+ from uuid import UUID
409
+
410
+ from kronos.types.db_types import FKMeta
411
+
412
+ t_ = Annotated[UUID, FKMeta(fk)] # type: ignore[valid-type]
413
+ if self.is_listable:
414
+ t_ = list[t_] # type: ignore[valid-type]
415
+ if self.is_nullable:
416
+ t_ = t_ | None # type: ignore[assignment]
417
+ return t_ # type: ignore[return-value]
418
+
419
+ def annotated(self) -> type[Any]:
420
+ """Create Annotated[base_type, metadata...] with thread-safe LRU cache.
421
+
422
+ Returns:
423
+ Annotated type with metadata attached, suitable for Pydantic/dataclass fields.
424
+ Nullable specs produce T | None annotation.
425
+ FK specs produce Annotated[UUID, ..., FKMeta(target)] when target resolves.
426
+ """
427
+ cache_key = (self.base_type, self.metadata)
428
+
429
+ with _cache_lock:
430
+ if cache_key in _annotated_cache:
431
+ _annotated_cache.move_to_end(cache_key)
432
+ return _annotated_cache[cache_key]
433
+
434
+ actual_type = Any if is_sentinel(self.base_type, {"none"}) else self.base_type
435
+ current_metadata = self.metadata
436
+
437
+ # Resolve FK target (explicit or Observable base_type)
438
+ extra_annotations: list[Any] = []
439
+ resolved_fk = self.fk_target
440
+ if not is_undefined(resolved_fk):
441
+ from uuid import UUID
442
+
443
+ from kronos.types.db_types import FKMeta
444
+
445
+ actual_type = UUID # FK fields are UUID references
446
+ extra_annotations.append(FKMeta(resolved_fk))
447
+
448
+ if any(m.key == "nullable" and m.value for m in current_metadata):
449
+ actual_type = actual_type | None # type: ignore
450
+
451
+ if current_metadata or extra_annotations:
452
+ args = [actual_type, *list(current_metadata), *extra_annotations]
453
+ # Python 3.11-3.12 vs 3.13+ compatibility
454
+ try:
455
+ result = Annotated.__class_getitem__(tuple(args)) # type: ignore
456
+ except AttributeError:
457
+ import operator
458
+
459
+ result = operator.getitem(Annotated, tuple(args)) # type: ignore
460
+ else:
461
+ result = actual_type # type: ignore[misc]
462
+
463
+ _annotated_cache[cache_key] = result # type: ignore[assignment]
464
+
465
+ while len(_annotated_cache) > _MAX_CACHE_SIZE:
466
+ _annotated_cache.popitem(last=False)
467
+
468
+ return result # type: ignore[return-value]
469
+
470
+ def metadict(
471
+ self, exclude: set[str] | None = None, exclude_common: bool = False
472
+ ) -> dict[str, Any]:
473
+ """Convert metadata to dict, optionally excluding keys or CommonMeta keys."""
474
+ if exclude is None:
475
+ exclude = set()
476
+ if exclude_common:
477
+ exclude = exclude | set(CommonMeta.allowed())
478
+ return {meta.key: meta.value for meta in self.metadata if meta.key not in exclude}
479
+
480
+
481
+ def _is_observable(cls: type) -> bool:
482
+ """Check if a type satisfies the Observable protocol (has UUID id property).
483
+
484
+ Uses structural check rather than issubclass() for Python 3.11 compatibility
485
+ with runtime_checkable protocols that have property members.
486
+ """
487
+ id_attr = getattr(cls, "id", None)
488
+ return isinstance(id_attr, property) or (
489
+ hasattr(cls, "__annotations__") and "id" in getattr(cls, "__annotations__", {})
490
+ )
491
+
492
+
493
+ def _is_factory(obj: Any) -> tuple[bool, bool]:
494
+ """Check if object is a factory function.
495
+
496
+ Args:
497
+ obj: Object to check
498
+
499
+ Returns:
500
+ Tuple of (is_factory, is_async)
501
+ """
502
+ if not callable(obj):
503
+ return (False, False)
504
+ if is_coro_func(obj):
505
+ return (True, True)
506
+ return (True, False)
@@ -0,0 +1,60 @@
1
+ # Copyright (c) 2025 - 2026, HaiyangLi <quantocean.li at gmail dot com>
2
+ # SPDX-License-Identifier: Apache-2.0
3
+
4
+ from ._sentinel import (
5
+ MaybeSentinel,
6
+ MaybeUndefined,
7
+ MaybeUnset,
8
+ SingletonType,
9
+ T,
10
+ Undefined,
11
+ UndefinedType,
12
+ Unset,
13
+ UnsetType,
14
+ is_sentinel,
15
+ is_undefined,
16
+ is_unset,
17
+ not_sentinel,
18
+ )
19
+ from .base import (
20
+ DataClass,
21
+ Enum,
22
+ HashableModel,
23
+ KeysDict,
24
+ KeysLike,
25
+ Meta,
26
+ ModelConfig,
27
+ Params,
28
+ )
29
+ from .db_types import FK, FKMeta, Vector, VectorMeta, extract_kron_db_meta
30
+ from .identity import ID
31
+
32
+ __all__ = (
33
+ "MaybeSentinel",
34
+ "MaybeUndefined",
35
+ "MaybeUnset",
36
+ "SingletonType",
37
+ "T",
38
+ "Undefined",
39
+ "UndefinedType",
40
+ "Unset",
41
+ "UnsetType",
42
+ "is_sentinel",
43
+ "is_undefined",
44
+ "is_unset",
45
+ "not_sentinel",
46
+ "DataClass",
47
+ "Enum",
48
+ "HashableModel",
49
+ "KeysDict",
50
+ "KeysLike",
51
+ "Meta",
52
+ "ModelConfig",
53
+ "Params",
54
+ "FK",
55
+ "FKMeta",
56
+ "Vector",
57
+ "VectorMeta",
58
+ "extract_kron_db_meta",
59
+ "ID",
60
+ )