levain-cli 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.
@@ -0,0 +1,114 @@
1
+ """Code generation for recipe packages.
2
+
3
+ Generates a Python ``GraphState`` TypedDict from the recipe's
4
+ spec. Node functions type their state parameter as this class,
5
+ so mypy catches any key not declared in the spec.
6
+ """
7
+
8
+ from __future__ import annotations
9
+
10
+ from .models import SPEC_TYPES, StateField, ValidatedSpec
11
+ from .reducers import REDUCERS
12
+
13
+ _HEADER_BODY = '''"""Generated GraphState TypedDict.
14
+
15
+ Do not edit by hand — regenerate via::
16
+
17
+ levain generate-state <dir>
18
+ """
19
+ '''
20
+
21
+
22
+ def generate_state_module(spec: ValidatedSpec) -> str:
23
+ """Return Python source for the recipe's state TypedDict."""
24
+ reducer_names = sorted(
25
+ {v.reducer for v in spec.variables if v.reducer is not None}
26
+ )
27
+ needs_annotated = bool(reducer_names) or any(
28
+ v.description for v in spec.variables
29
+ )
30
+
31
+ lines: list[str] = [_HEADER_BODY]
32
+
33
+ if reducer_names:
34
+ # Merge ``Any`` into the typing import when any reducer's
35
+ # element type references it (e.g. ``dict[str, Any]``);
36
+ # without this the generated file would use ``Any``
37
+ # without importing it.
38
+ typing_imports = ["Annotated", "TypedDict"]
39
+ if any("Any" in REDUCERS[n].element_type_name for n in reducer_names):
40
+ typing_imports.insert(1, "Any")
41
+ lines.append(f"from typing import {', '.join(typing_imports)}")
42
+ lines.append("")
43
+ # Group reducer imports by module so ruff/isort won't
44
+ # split them and cause CI drift.
45
+ by_module: dict[str, list[str]] = {}
46
+ for name in reducer_names:
47
+ raw = REDUCERS[name].import_line
48
+ mod, _, names = raw.partition(" import ")
49
+ by_module.setdefault(mod, []).append(names)
50
+ for mod, groups in by_module.items():
51
+ seen: set[str] = set()
52
+ merged_names: list[str] = []
53
+ for group in groups:
54
+ for n in group.split(","):
55
+ name = n.strip()
56
+ if name not in seen:
57
+ seen.add(name)
58
+ merged_names.append(name)
59
+ lines.append(f"{mod} import {', '.join(merged_names)}")
60
+ elif needs_annotated:
61
+ lines.append("from typing import Annotated, TypedDict")
62
+ else:
63
+ lines.append("from typing import TypedDict")
64
+ lines.append("")
65
+ lines.append("")
66
+ lines.append("class GraphState(TypedDict, total=False):")
67
+ lines.append(f' """State fields for the {spec.module_name} recipe.')
68
+ lines.append("")
69
+ lines.append(" ``total=False`` allows node functions to return partial")
70
+ lines.append(" updates — only the keys they actually change.")
71
+ lines.append(' """')
72
+ lines.append("")
73
+
74
+ if spec.variables:
75
+ for var in spec.variables:
76
+ lines.append(f" {var.name}: {_py_type_for(var)}")
77
+ else:
78
+ # TypedDict body must not be empty.
79
+ lines.append(" pass")
80
+
81
+ return "\n".join(lines) + "\n"
82
+
83
+
84
+ def _py_type_for(var: StateField) -> str:
85
+ """Map a spec variable type to a Python type name.
86
+
87
+ For list-typed fields with a blessed reducer, emit
88
+ ``Annotated[list[<element>], <reducer>]`` so LangGraph picks
89
+ the reducer up from the TypedDict annotation. When the variable
90
+ carries a description it's appended as the final metadata arg
91
+ so ``get_type_hints(..., include_extras=True)`` recovers it.
92
+ """
93
+ py_type = SPEC_TYPES.get(var.type)
94
+ if py_type is None:
95
+ raise ValueError(
96
+ f"Unknown type {var.type!r} for variable {var.name!r}"
97
+ )
98
+ desc_literal = repr(var.description) if var.description else None
99
+
100
+ if var.reducer is not None:
101
+ spec = REDUCERS.get(var.reducer)
102
+ if spec is None:
103
+ raise ValueError(
104
+ f"Unknown reducer {var.reducer!r} for variable {var.name!r}"
105
+ )
106
+ metadata = [spec.name]
107
+ if desc_literal is not None:
108
+ metadata.append(desc_literal)
109
+ return (
110
+ f"Annotated[list[{spec.element_type_name}], {', '.join(metadata)}]"
111
+ )
112
+ if desc_literal is not None:
113
+ return f"Annotated[{py_type.__name__}, {desc_literal}]"
114
+ return py_type.__name__