confluid 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.
- confluid/__init__.py +152 -0
- confluid/bake.py +174 -0
- confluid/configurator.py +509 -0
- confluid/decorators.py +442 -0
- confluid/dumper.py +238 -0
- confluid/engine.py +1871 -0
- confluid/env.py +58 -0
- confluid/exceptions.py +58 -0
- confluid/fluid.py +154 -0
- confluid/introspect.py +271 -0
- confluid/lazy.py +85 -0
- confluid/llm_schema.py +296 -0
- confluid/loader.py +492 -0
- confluid/mandatory.py +96 -0
- confluid/merger.py +93 -0
- confluid/no_broadcast.py +83 -0
- confluid/py.typed +0 -0
- confluid/pydantic_export.py +593 -0
- confluid/registry.py +263 -0
- confluid/report.py +118 -0
- confluid/resolver.py +502 -0
- confluid/schema.py +541 -0
- confluid/scopes.py +227 -0
- confluid/validation.py +362 -0
- confluid-0.1.0.dist-info/METADATA +199 -0
- confluid-0.1.0.dist-info/RECORD +30 -0
- confluid-0.1.0.dist-info/WHEEL +5 -0
- confluid-0.1.0.dist-info/entry_points.txt +2 -0
- confluid-0.1.0.dist-info/licenses/LICENSE +21 -0
- confluid-0.1.0.dist-info/top_level.txt +1 -0
confluid/__init__.py
ADDED
|
@@ -0,0 +1,152 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Confluid: Modern, hierarchical configuration and dependency injection.
|
|
3
|
+
|
|
4
|
+
The pydantic-powered schema-export API (``to_pydantic``, ``confluid_class_of``)
|
|
5
|
+
is exposed lazily via :pep:`562` ``__getattr__`` so importing confluid never
|
|
6
|
+
requires pydantic — it is the optional ``confluid[pydantic]`` extra. Accessing
|
|
7
|
+
those names without pydantic installed raises an ``ImportError`` naming the
|
|
8
|
+
extra.
|
|
9
|
+
|
|
10
|
+
``__all__`` is the CURATED public surface (pruned 2026-07): internal
|
|
11
|
+
machinery (validation plumbing, scope resolution, annotation predicates,
|
|
12
|
+
marker internals) stays importable from its home module but is deliberately
|
|
13
|
+
not re-exported here.
|
|
14
|
+
"""
|
|
15
|
+
|
|
16
|
+
from typing import TYPE_CHECKING, Any
|
|
17
|
+
|
|
18
|
+
from confluid.configurator import configure, configure_from_file
|
|
19
|
+
from confluid.decorators import configurable, ignore_config, output, register
|
|
20
|
+
from confluid.dumper import dump
|
|
21
|
+
from confluid.engine import active_context, cast, collect_report, flow, get_configurable_attrs, materialize, resolve
|
|
22
|
+
from confluid.exceptions import (
|
|
23
|
+
CircularIncludeError,
|
|
24
|
+
ConfigFileNotFoundError,
|
|
25
|
+
ConfigurableDefinitionError,
|
|
26
|
+
ConfigurationError,
|
|
27
|
+
ConfluidError,
|
|
28
|
+
ConstructionError,
|
|
29
|
+
IntrospectionError,
|
|
30
|
+
ReferenceResolutionError,
|
|
31
|
+
ScopeError,
|
|
32
|
+
UnknownClassError,
|
|
33
|
+
ValidationModeError,
|
|
34
|
+
WorkspaceEnvError,
|
|
35
|
+
)
|
|
36
|
+
from confluid.fluid import Class, Clone, Fluid, Instance
|
|
37
|
+
from confluid.fluid import Lazy as LazyClass
|
|
38
|
+
from confluid.fluid import Reference, format_yaml_loc
|
|
39
|
+
from confluid.lazy import Lazy, lazy_param_names
|
|
40
|
+
from confluid.llm_schema import sanitize_schema
|
|
41
|
+
from confluid.loader import get_app_name, load, load_config, load_config_with_paths, resolve_config_path, set_app_name
|
|
42
|
+
from confluid.mandatory import Mandatory, mandatory_param_names
|
|
43
|
+
from confluid.merger import deep_merge, expand_dotted_keys
|
|
44
|
+
from confluid.no_broadcast import NoBroadcast, no_broadcast_param_names
|
|
45
|
+
from confluid.registry import get_registry
|
|
46
|
+
from confluid.report import ConfigurationReport
|
|
47
|
+
from confluid.resolver import parse_value
|
|
48
|
+
from confluid.schema import (
|
|
49
|
+
InputSpec,
|
|
50
|
+
OutputSpec,
|
|
51
|
+
get_hierarchy,
|
|
52
|
+
get_hierarchy_from_instance,
|
|
53
|
+
input_specs,
|
|
54
|
+
output_specs,
|
|
55
|
+
parse_param_docs,
|
|
56
|
+
shortest_unique_paths,
|
|
57
|
+
)
|
|
58
|
+
from confluid.scopes import discover_dimensions
|
|
59
|
+
from confluid.validation import ValidationMode, ValidationPolicy, get_policy, reset_policy, set_policy, validate_model
|
|
60
|
+
|
|
61
|
+
__all__ = [
|
|
62
|
+
"ConfluidError",
|
|
63
|
+
"ConfigurationError",
|
|
64
|
+
"CircularIncludeError",
|
|
65
|
+
"ReferenceResolutionError",
|
|
66
|
+
"UnknownClassError",
|
|
67
|
+
"ConfigurableDefinitionError",
|
|
68
|
+
"ValidationModeError",
|
|
69
|
+
"ScopeError",
|
|
70
|
+
"ConfigFileNotFoundError",
|
|
71
|
+
"ConstructionError",
|
|
72
|
+
"WorkspaceEnvError",
|
|
73
|
+
"IntrospectionError",
|
|
74
|
+
"configurable",
|
|
75
|
+
"register",
|
|
76
|
+
"ignore_config",
|
|
77
|
+
"output",
|
|
78
|
+
"get_registry",
|
|
79
|
+
"load",
|
|
80
|
+
"load_config",
|
|
81
|
+
"load_config_with_paths",
|
|
82
|
+
"resolve_config_path",
|
|
83
|
+
"set_app_name",
|
|
84
|
+
"get_app_name",
|
|
85
|
+
"materialize",
|
|
86
|
+
"resolve",
|
|
87
|
+
"active_context",
|
|
88
|
+
"deep_merge",
|
|
89
|
+
"expand_dotted_keys",
|
|
90
|
+
"parse_value",
|
|
91
|
+
"dump",
|
|
92
|
+
"configure",
|
|
93
|
+
"configure_from_file",
|
|
94
|
+
"ConfigurationReport",
|
|
95
|
+
"collect_report",
|
|
96
|
+
"Fluid",
|
|
97
|
+
"Class",
|
|
98
|
+
"Clone",
|
|
99
|
+
"Instance",
|
|
100
|
+
"Reference",
|
|
101
|
+
"flow",
|
|
102
|
+
"cast",
|
|
103
|
+
"format_yaml_loc",
|
|
104
|
+
"Lazy",
|
|
105
|
+
"LazyClass",
|
|
106
|
+
"lazy_param_names",
|
|
107
|
+
"Mandatory",
|
|
108
|
+
"mandatory_param_names",
|
|
109
|
+
"NoBroadcast",
|
|
110
|
+
"no_broadcast_param_names",
|
|
111
|
+
"get_hierarchy",
|
|
112
|
+
"get_hierarchy_from_instance",
|
|
113
|
+
"input_specs",
|
|
114
|
+
"output_specs",
|
|
115
|
+
"InputSpec",
|
|
116
|
+
"OutputSpec",
|
|
117
|
+
"parse_param_docs",
|
|
118
|
+
"shortest_unique_paths",
|
|
119
|
+
"get_configurable_attrs",
|
|
120
|
+
"to_pydantic",
|
|
121
|
+
"confluid_class_of",
|
|
122
|
+
"discover_dimensions",
|
|
123
|
+
"ValidationMode",
|
|
124
|
+
"ValidationPolicy",
|
|
125
|
+
"get_policy",
|
|
126
|
+
"set_policy",
|
|
127
|
+
"reset_policy",
|
|
128
|
+
"validate_model",
|
|
129
|
+
"sanitize_schema",
|
|
130
|
+
]
|
|
131
|
+
|
|
132
|
+
if TYPE_CHECKING:
|
|
133
|
+
from confluid.pydantic_export import confluid_class_of, to_pydantic
|
|
134
|
+
|
|
135
|
+
# Names served lazily from ``confluid.pydantic_export`` (requires the
|
|
136
|
+
# ``confluid[pydantic]`` extra) — see the module docstring.
|
|
137
|
+
_PYDANTIC_EXPORTS = ("to_pydantic", "confluid_class_of")
|
|
138
|
+
|
|
139
|
+
|
|
140
|
+
def __getattr__(name: str) -> Any:
|
|
141
|
+
if name in _PYDANTIC_EXPORTS:
|
|
142
|
+
try:
|
|
143
|
+
from confluid import pydantic_export
|
|
144
|
+
except ModuleNotFoundError as exc:
|
|
145
|
+
if exc.name in ("pydantic", "annotated_types"):
|
|
146
|
+
raise ImportError(
|
|
147
|
+
f"confluid.{name} requires pydantic, which is an optional dependency — "
|
|
148
|
+
"install the extra: pip install 'confluid[pydantic]'"
|
|
149
|
+
) from exc
|
|
150
|
+
raise
|
|
151
|
+
return getattr(pydantic_export, name)
|
|
152
|
+
raise AttributeError(f"module 'confluid' has no attribute {name!r}")
|
confluid/bake.py
ADDED
|
@@ -0,0 +1,174 @@
|
|
|
1
|
+
"""Build-time AST bake of ``__init__`` body-slot names — the packaged-mode scan.
|
|
2
|
+
|
|
3
|
+
Confluid's broadcasting discovers post-init body attributes (``self.loss_fn = …``
|
|
4
|
+
inside ``__init__``) by AST-scanning constructor SOURCE at runtime
|
|
5
|
+
(:func:`confluid.introspect.scan_init_body`). Compiled / frozen / zip
|
|
6
|
+
deployments ship no source, so the runtime scan is silently empty and body
|
|
7
|
+
slots vanish from the broadcast surface. This module runs the SAME scan at
|
|
8
|
+
BUILD time — while source still exists — and emits a generated
|
|
9
|
+
``<package>/_confluid_baked.py`` table that the engine unions in at runtime
|
|
10
|
+
(scanned ∪ declared ``broadcast_attrs`` ∪ baked): automatic dev/packaged
|
|
11
|
+
parity, no hand-maintained ``broadcast_attrs`` lists.
|
|
12
|
+
|
|
13
|
+
Run it in the packaging pipeline, BEFORE freezing/zipping::
|
|
14
|
+
|
|
15
|
+
python -m confluid.bake mypackage [otherpackage ...]
|
|
16
|
+
python -m confluid.bake mypackage --check # CI drift guard (no write)
|
|
17
|
+
|
|
18
|
+
Every class the package defines with its own ``__init__`` is baked — not only
|
|
19
|
+
``@configurable`` ones — so in-package base classes contribute their body
|
|
20
|
+
slots through the MRO exactly as the runtime scan would. An empty entry is
|
|
21
|
+
meaningful ("scanned at build time, no body slots"): it silences the engine's
|
|
22
|
+
cannot-scan warning for that class.
|
|
23
|
+
|
|
24
|
+
Frozen-bundler caveat: the engine imports ``<pkg>._confluid_baked`` lazily by
|
|
25
|
+
dotted name (:func:`confluid.introspect.baked_init_attrs`), which static
|
|
26
|
+
import tracers (PyInstaller) don't see — declare it as a hidden import
|
|
27
|
+
(``--hidden-import mypkg._confluid_baked``) or import it explicitly from the
|
|
28
|
+
package's ``__init__``. Wheel/zip/pyc-only deployments need nothing extra.
|
|
29
|
+
"""
|
|
30
|
+
|
|
31
|
+
from __future__ import annotations
|
|
32
|
+
|
|
33
|
+
import argparse
|
|
34
|
+
import importlib
|
|
35
|
+
import pkgutil
|
|
36
|
+
import sys
|
|
37
|
+
from pathlib import Path
|
|
38
|
+
from typing import Any, Dict, List, Optional, Tuple
|
|
39
|
+
|
|
40
|
+
from loggair import get_logger
|
|
41
|
+
|
|
42
|
+
from confluid.exceptions import ConfigurationError
|
|
43
|
+
from confluid.introspect import BAKED_MODULE_BASENAME, init_setattr_names
|
|
44
|
+
|
|
45
|
+
logger = get_logger("confluid.bake")
|
|
46
|
+
|
|
47
|
+
_HEADER_TEMPLATE = """\
|
|
48
|
+
# =========================================================================
|
|
49
|
+
# AUTO-GENERATED FILE — DO NOT EDIT BY HAND
|
|
50
|
+
# Generated by: python -m confluid.bake {package}
|
|
51
|
+
# Source: confluid/bake.py (bake_broadcast_attrs)
|
|
52
|
+
# To regenerate: python -m confluid.bake {package}
|
|
53
|
+
# =========================================================================
|
|
54
|
+
\"\"\"Baked ``__init__`` body-slot names for ``{package}`` (build-time AST scan).
|
|
55
|
+
|
|
56
|
+
Consumed by ``confluid.introspect.baked_init_attrs`` so broadcasting keeps
|
|
57
|
+
seeing post-init attributes in compiled/frozen/zip deployments where
|
|
58
|
+
``inspect.getsource`` fails. See ``confluid/bake.py``.
|
|
59
|
+
\"\"\"
|
|
60
|
+
|
|
61
|
+
from typing import Dict, Tuple
|
|
62
|
+
|
|
63
|
+
BROADCAST_ATTRS: Dict[str, Tuple[str, ...]] = {{
|
|
64
|
+
{entries}}}
|
|
65
|
+
"""
|
|
66
|
+
|
|
67
|
+
|
|
68
|
+
def scan_package(package_name: str) -> Dict[str, Tuple[str, ...]]:
|
|
69
|
+
"""AST-scan every class the package defines; return ``{module.QualName: (names,)}``.
|
|
70
|
+
|
|
71
|
+
Imports the package and each of its submodules (a module that fails to
|
|
72
|
+
import is skipped with a warning — its classes simply stay unbaked), then
|
|
73
|
+
scans the ``__init__`` of every top-level class DEFINED in those modules
|
|
74
|
+
(``cls.__module__`` matches — re-exports are skipped, as is the bake
|
|
75
|
+
module itself). Deterministic: modules and attr names are sorted.
|
|
76
|
+
"""
|
|
77
|
+
package = importlib.import_module(package_name)
|
|
78
|
+
module_names: List[str] = [package_name]
|
|
79
|
+
search_paths = getattr(package, "__path__", None)
|
|
80
|
+
if search_paths is not None:
|
|
81
|
+
module_names.extend(info.name for info in pkgutil.walk_packages(search_paths, prefix=f"{package_name}."))
|
|
82
|
+
|
|
83
|
+
table: Dict[str, Tuple[str, ...]] = {}
|
|
84
|
+
for module_name in sorted(module_names):
|
|
85
|
+
if module_name.rsplit(".", 1)[-1] == BAKED_MODULE_BASENAME:
|
|
86
|
+
continue # never scan a previous bake output
|
|
87
|
+
try:
|
|
88
|
+
module = importlib.import_module(module_name)
|
|
89
|
+
except Exception as exc: # noqa: BLE001 — a broken optional submodule must not kill the bake
|
|
90
|
+
logger.warning(f"confluid.bake: skipping module {module_name!r} (import failed: {exc})")
|
|
91
|
+
continue
|
|
92
|
+
for value in vars(module).values():
|
|
93
|
+
if not isinstance(value, type) or value.__module__ != module_name:
|
|
94
|
+
continue
|
|
95
|
+
init = value.__dict__.get("__init__")
|
|
96
|
+
if init is None:
|
|
97
|
+
continue
|
|
98
|
+
table[f"{value.__module__}.{value.__qualname__}"] = tuple(sorted(init_setattr_names(init)))
|
|
99
|
+
return table
|
|
100
|
+
|
|
101
|
+
|
|
102
|
+
def render_baked_module(package_name: str, table: Dict[str, Tuple[str, ...]]) -> str:
|
|
103
|
+
"""Render the generated ``_confluid_baked.py`` source (deterministic output)."""
|
|
104
|
+
lines: List[str] = []
|
|
105
|
+
for key in sorted(table):
|
|
106
|
+
names = table[key]
|
|
107
|
+
# ("a",) — the trailing comma keeps 1-tuples valid; () renders bare.
|
|
108
|
+
rendered = "(" + "".join(f"{name!r}, " for name in names).rstrip() + ")" if names else "()"
|
|
109
|
+
lines.append(f" {key!r}: {rendered},\n")
|
|
110
|
+
return _HEADER_TEMPLATE.format(package=package_name, entries="".join(lines))
|
|
111
|
+
|
|
112
|
+
|
|
113
|
+
def baked_module_path(package_name: str) -> Path:
|
|
114
|
+
"""Filesystem location of the package's bake table (``<pkg>/_confluid_baked.py``)."""
|
|
115
|
+
package = importlib.import_module(package_name)
|
|
116
|
+
package_file = getattr(package, "__file__", None)
|
|
117
|
+
if package_file is None:
|
|
118
|
+
raise ConfigurationError(
|
|
119
|
+
f"confluid.bake: package {package_name!r} has no __file__ (namespace package?) — "
|
|
120
|
+
"the bake table needs a regular package directory to live in."
|
|
121
|
+
)
|
|
122
|
+
return Path(package_file).parent / f"{BAKED_MODULE_BASENAME}.py"
|
|
123
|
+
|
|
124
|
+
|
|
125
|
+
def bake_broadcast_attrs(package_names: List[str], check: bool = False) -> Dict[str, Any]:
|
|
126
|
+
"""Scan each package and write (or, with ``check=True``, verify) its bake table.
|
|
127
|
+
|
|
128
|
+
Returns a JSON-serializable summary per package:
|
|
129
|
+
``{package: {"path": str, "classes": int, "changed": bool}}`` — ``changed``
|
|
130
|
+
means the on-disk table differed from the fresh scan (in check mode
|
|
131
|
+
nothing is written; the caller decides whether drift fails the build).
|
|
132
|
+
"""
|
|
133
|
+
summary: Dict[str, Any] = {}
|
|
134
|
+
for package_name in package_names:
|
|
135
|
+
table = scan_package(package_name)
|
|
136
|
+
rendered = render_baked_module(package_name, table)
|
|
137
|
+
path = baked_module_path(package_name)
|
|
138
|
+
existing = path.read_text() if path.exists() else None
|
|
139
|
+
changed = existing != rendered
|
|
140
|
+
if changed and not check:
|
|
141
|
+
path.write_text(rendered)
|
|
142
|
+
logger.info(f"confluid.bake: wrote {path} ({len(table)} classes)")
|
|
143
|
+
summary[package_name] = {"path": str(path), "classes": len(table), "changed": changed}
|
|
144
|
+
return summary
|
|
145
|
+
|
|
146
|
+
|
|
147
|
+
def main(argv: Optional[List[str]] = None) -> int:
|
|
148
|
+
"""CLI entry point: ``python -m confluid.bake <package> [...] [--check]``."""
|
|
149
|
+
parser = argparse.ArgumentParser(
|
|
150
|
+
prog="python -m confluid.bake",
|
|
151
|
+
description="Bake __init__ body-slot names into <package>/_confluid_baked.py "
|
|
152
|
+
"so broadcasting keeps working in compiled/frozen deployments.",
|
|
153
|
+
)
|
|
154
|
+
parser.add_argument("packages", nargs="+", help="importable package names to scan")
|
|
155
|
+
parser.add_argument(
|
|
156
|
+
"--check",
|
|
157
|
+
action="store_true",
|
|
158
|
+
help="verify the existing bake tables are up to date (write nothing; exit 1 on drift)",
|
|
159
|
+
)
|
|
160
|
+
args = parser.parse_args(argv)
|
|
161
|
+
|
|
162
|
+
summary = bake_broadcast_attrs(args.packages, check=args.check)
|
|
163
|
+
drifted = [pkg for pkg, info in summary.items() if info["changed"]]
|
|
164
|
+
for pkg, info in summary.items():
|
|
165
|
+
status = "STALE" if info["changed"] and args.check else ("updated" if info["changed"] else "up to date")
|
|
166
|
+
print(f"{pkg}: {info['classes']} classes -> {info['path']} [{status}]")
|
|
167
|
+
if args.check and drifted:
|
|
168
|
+
print(f"bake tables out of date for: {', '.join(drifted)} — rerun python -m confluid.bake", file=sys.stderr)
|
|
169
|
+
return 1
|
|
170
|
+
return 0
|
|
171
|
+
|
|
172
|
+
|
|
173
|
+
if __name__ == "__main__":
|
|
174
|
+
sys.exit(main())
|