tgzr.cuisine 0.0.1__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.
- tgzr/cuisine/__init__.py +0 -0
- tgzr/cuisine/_version.py +34 -0
- tgzr/cuisine/basics/__init__.py +35 -0
- tgzr/cuisine/basics/buildable.py +38 -0
- tgzr/cuisine/basics/builder.py +72 -0
- tgzr/cuisine/basics/computable.py +33 -0
- tgzr/cuisine/basics/editable.py +28 -0
- tgzr/cuisine/basics/editor.py +29 -0
- tgzr/cuisine/basics/env.py +216 -0
- tgzr/cuisine/basics/files_recipes/__init__.py +18 -0
- tgzr/cuisine/basics/panels/__init__.py +0 -0
- tgzr/cuisine/basics/panels/params_panel.py +540 -0
- tgzr/cuisine/basics/product.py +32 -0
- tgzr/cuisine/basics/recipe_contexts.py +10 -0
- tgzr/cuisine/basics/recipe_with_params.py +119 -0
- tgzr/cuisine/basics/viewable.py +33 -0
- tgzr/cuisine/basics/viewer.py +30 -0
- tgzr/cuisine/basics/workscene.py +5 -0
- tgzr/cuisine/chef.py +339 -0
- tgzr/cuisine/cli/__init__.py +232 -0
- tgzr/cuisine/cli/main.py +7 -0
- tgzr/cuisine/cli/utils.py +31 -0
- tgzr/cuisine/plugin.py +27 -0
- tgzr/cuisine/recipe.py +361 -0
- tgzr/cuisine/workbench.py +455 -0
- tgzr_cuisine-0.0.1.dist-info/METADATA +34 -0
- tgzr_cuisine-0.0.1.dist-info/RECORD +30 -0
- tgzr_cuisine-0.0.1.dist-info/WHEEL +4 -0
- tgzr_cuisine-0.0.1.dist-info/entry_points.txt +8 -0
- tgzr_cuisine-0.0.1.dist-info/licenses/LICENSE +674 -0
|
@@ -0,0 +1,540 @@
|
|
|
1
|
+
# TDOD: most of this should be extracted to its own project like tgzr.nice_pydantic
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
from typing import TYPE_CHECKING, Union, Type, Literal, Callable, Any
|
|
5
|
+
from typing import get_args, get_origin
|
|
6
|
+
|
|
7
|
+
import sys
|
|
8
|
+
import inspect
|
|
9
|
+
import contextlib
|
|
10
|
+
|
|
11
|
+
import pydantic
|
|
12
|
+
from nicegui import ui
|
|
13
|
+
|
|
14
|
+
if TYPE_CHECKING:
|
|
15
|
+
from tgzr.cuisine.basics.recipe_with_params import RecipeParams
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
# This is to be compatible with python 3.9
|
|
19
|
+
# because it does not have types.UnionType
|
|
20
|
+
# so we can't just do "xx is UnionType"
|
|
21
|
+
# TODO: trash this when ToonBoom got working and we stop need to support py3.9 anymore.
|
|
22
|
+
def is_union_type(tp):
|
|
23
|
+
# 1. Check for the modern | syntax (Python 3.10+)
|
|
24
|
+
if sys.version_info >= (3, 10):
|
|
25
|
+
from types import UnionType
|
|
26
|
+
|
|
27
|
+
if tp is UnionType:
|
|
28
|
+
# TODO: I'm pretty sure this is wrong, but our usage doesn't work w/o it ¯\_(ツ)_/¯
|
|
29
|
+
return True
|
|
30
|
+
|
|
31
|
+
if isinstance(tp, UnionType):
|
|
32
|
+
return True
|
|
33
|
+
|
|
34
|
+
# 2. Check for the traditional Union[int, str] syntax
|
|
35
|
+
# In 3.9, we check the __origin__ of the type
|
|
36
|
+
return getattr(tp, "__origin__", None) is Union
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
class FieldRenderer:
|
|
40
|
+
@classmethod
|
|
41
|
+
def handles(cls, value_type) -> bool:
|
|
42
|
+
raise NotImplementedError(f"on {cls}")
|
|
43
|
+
|
|
44
|
+
def __init__(
|
|
45
|
+
self,
|
|
46
|
+
panel: ParamsPanel,
|
|
47
|
+
name: str,
|
|
48
|
+
getter: Callable[[], Any],
|
|
49
|
+
setter: Callable[[Any], None],
|
|
50
|
+
default_factory: Callable[[], Any] | None,
|
|
51
|
+
):
|
|
52
|
+
self._panel = panel
|
|
53
|
+
self._name = name
|
|
54
|
+
self._getter = getter
|
|
55
|
+
self._setter = setter
|
|
56
|
+
self._default_factory = default_factory
|
|
57
|
+
self._options: dict[str, Any] = {}
|
|
58
|
+
|
|
59
|
+
def set_optional(self, b: bool = False):
|
|
60
|
+
self._options["optional"] = b
|
|
61
|
+
|
|
62
|
+
def set_options(self, *args, **kwargs) -> None:
|
|
63
|
+
if args:
|
|
64
|
+
raise ValueError(
|
|
65
|
+
f"FiedlRenderer default set_option() implementation does not accept *args, but got {args} on field {self._name} ({self}) !"
|
|
66
|
+
)
|
|
67
|
+
self._options.update(kwargs)
|
|
68
|
+
|
|
69
|
+
def render(self):
|
|
70
|
+
# print("RENDER LABEL ON", self)
|
|
71
|
+
self.render_label()
|
|
72
|
+
# print("RENDER FIELD", self)
|
|
73
|
+
self.render_field()
|
|
74
|
+
|
|
75
|
+
def render_label(self):
|
|
76
|
+
with self._panel.field_label_parent():
|
|
77
|
+
if self._panel.editable:
|
|
78
|
+
self._render_label_editable()
|
|
79
|
+
else:
|
|
80
|
+
self._render_label_readonly()
|
|
81
|
+
|
|
82
|
+
def _render_label_editable(self):
|
|
83
|
+
ui.label(self._name.replace("_", " ").title())
|
|
84
|
+
|
|
85
|
+
def _render_label_readonly(self):
|
|
86
|
+
ui.label(self._name.replace("_", " ").title())
|
|
87
|
+
|
|
88
|
+
def render_field(self):
|
|
89
|
+
with self._panel.field_value_parent() as p:
|
|
90
|
+
p.classes("col-span-2")
|
|
91
|
+
if self._panel.editable:
|
|
92
|
+
e = self._render_editable_value()
|
|
93
|
+
if e is not None:
|
|
94
|
+
e.on("mousedown", js_handler="(e)=>{e.stopPropagation()}")
|
|
95
|
+
else:
|
|
96
|
+
self._render_readonly_value()
|
|
97
|
+
|
|
98
|
+
def _render_editable_value(self) -> ui.element | None:
|
|
99
|
+
raise NotImplementedError(f"on {self}")
|
|
100
|
+
|
|
101
|
+
def _render_readonly_value(self):
|
|
102
|
+
raise NotImplementedError(f"on {self}")
|
|
103
|
+
|
|
104
|
+
|
|
105
|
+
class StrField(FieldRenderer):
|
|
106
|
+
@classmethod
|
|
107
|
+
def handles(cls, value_type) -> bool:
|
|
108
|
+
return value_type is str
|
|
109
|
+
|
|
110
|
+
def _render_editable_value(self):
|
|
111
|
+
def on_key(event):
|
|
112
|
+
if event.args["key"] == "Enter":
|
|
113
|
+
self._setter(event.sender.value)
|
|
114
|
+
event.sender.run_method("blur")
|
|
115
|
+
|
|
116
|
+
e = (
|
|
117
|
+
ui.input(value=str(self._getter()))
|
|
118
|
+
.props("dense rounded standout")
|
|
119
|
+
.classes("w-full")
|
|
120
|
+
.on("keydown", on_key)
|
|
121
|
+
)
|
|
122
|
+
return e
|
|
123
|
+
|
|
124
|
+
def _render_readonly_value(self):
|
|
125
|
+
ui.label(self._getter()).classes("w-full")
|
|
126
|
+
|
|
127
|
+
|
|
128
|
+
class BoolField(FieldRenderer):
|
|
129
|
+
@classmethod
|
|
130
|
+
def handles(cls, value_type) -> bool:
|
|
131
|
+
return value_type is bool
|
|
132
|
+
|
|
133
|
+
def _render_editable_value(self):
|
|
134
|
+
e = ui.checkbox(
|
|
135
|
+
value=self._getter(), on_change=lambda e: self._setter(e.value)
|
|
136
|
+
).classes("w-full")
|
|
137
|
+
return e
|
|
138
|
+
|
|
139
|
+
def _render_readonly_value(self):
|
|
140
|
+
ui.icon(
|
|
141
|
+
self._getter() and "sym_o_check_box" or "sym_o_check_box_outline_blank",
|
|
142
|
+
size="sm",
|
|
143
|
+
)
|
|
144
|
+
|
|
145
|
+
|
|
146
|
+
class ChoiceField(FieldRenderer):
|
|
147
|
+
@classmethod
|
|
148
|
+
def handles(cls, value_type) -> bool:
|
|
149
|
+
return value_type is Literal
|
|
150
|
+
|
|
151
|
+
def set_options(
|
|
152
|
+
self,
|
|
153
|
+
*choices: Any,
|
|
154
|
+
) -> None:
|
|
155
|
+
super().set_options(choices=choices)
|
|
156
|
+
|
|
157
|
+
def _render_editable_value(self):
|
|
158
|
+
choices = self._options["choices"]
|
|
159
|
+
value = self._getter()
|
|
160
|
+
|
|
161
|
+
selectable = dict([(str(i), i) for i in choices])
|
|
162
|
+
if str(value) not in selectable:
|
|
163
|
+
selectable[str(value)] = value
|
|
164
|
+
e = (
|
|
165
|
+
ui.select(
|
|
166
|
+
selectable,
|
|
167
|
+
value=value,
|
|
168
|
+
new_value_mode="add",
|
|
169
|
+
key_generator=str,
|
|
170
|
+
on_change=lambda e: self._setter(e.value),
|
|
171
|
+
)
|
|
172
|
+
.props("dense rounded standout")
|
|
173
|
+
.classes("w-full")
|
|
174
|
+
)
|
|
175
|
+
|
|
176
|
+
return e
|
|
177
|
+
|
|
178
|
+
def _render_readonly_value(self):
|
|
179
|
+
ui.label(str(self._getter()))
|
|
180
|
+
|
|
181
|
+
|
|
182
|
+
class ListField(FieldRenderer):
|
|
183
|
+
@classmethod
|
|
184
|
+
def handles(cls, value_type) -> bool:
|
|
185
|
+
return value_type is list
|
|
186
|
+
|
|
187
|
+
def set_options(
|
|
188
|
+
self,
|
|
189
|
+
*args,
|
|
190
|
+
allow_reorder: bool = True,
|
|
191
|
+
open: bool = False,
|
|
192
|
+
default_open: bool | None = None,
|
|
193
|
+
) -> None:
|
|
194
|
+
self._item_type = args[0]
|
|
195
|
+
return super().set_options(
|
|
196
|
+
open=open, default_open=default_open, allow_reorder=allow_reorder
|
|
197
|
+
)
|
|
198
|
+
|
|
199
|
+
def on_add_item(self):
|
|
200
|
+
if self._default_factory is None:
|
|
201
|
+
raise ValueError("Cannot add item in list fiels without a default factory!")
|
|
202
|
+
list_values = self._getter()
|
|
203
|
+
list_values.append(self._item_type())
|
|
204
|
+
# list_values.append(self._default_factory())
|
|
205
|
+
self.render_content.refresh()
|
|
206
|
+
|
|
207
|
+
def move_item_up(self, index: int):
|
|
208
|
+
list_values = self._getter()
|
|
209
|
+
value = list_values.pop(index)
|
|
210
|
+
list_values.insert(index - 1, value)
|
|
211
|
+
self.render_content.refresh()
|
|
212
|
+
|
|
213
|
+
def move_item_down(self, index: int):
|
|
214
|
+
list_values = self._getter()
|
|
215
|
+
value = list_values.pop(index)
|
|
216
|
+
list_values.insert(index + 1, value)
|
|
217
|
+
self.render_content.refresh()
|
|
218
|
+
|
|
219
|
+
def delete_item(self, index: int):
|
|
220
|
+
list_values = self._getter()
|
|
221
|
+
list_values.pop(index)
|
|
222
|
+
self.render_content.refresh()
|
|
223
|
+
|
|
224
|
+
def toggle_open(self):
|
|
225
|
+
if self._content.visible:
|
|
226
|
+
self._open_close_btn.icon = "arrow_right"
|
|
227
|
+
self._content.visible = False
|
|
228
|
+
self._add_item_btn.visible = False
|
|
229
|
+
self._options["open"] = False
|
|
230
|
+
else:
|
|
231
|
+
self._open_close_btn.icon = "arrow_drop_down"
|
|
232
|
+
self._content.visible = True
|
|
233
|
+
if self._panel.editable:
|
|
234
|
+
self._add_item_btn.visible = True
|
|
235
|
+
self._options["open"] = True
|
|
236
|
+
|
|
237
|
+
def render_label(self):
|
|
238
|
+
with ui.row(align_items="center").classes(
|
|
239
|
+
f"gap-0 xmin-h-[{self._panel.min_h}]"
|
|
240
|
+
) as p:
|
|
241
|
+
p.classes("col-span-3")
|
|
242
|
+
self._render_header()
|
|
243
|
+
|
|
244
|
+
def _render_header(self):
|
|
245
|
+
self._open_close_btn = ui.button(icon="arrow_drop_down").props("flat dense")
|
|
246
|
+
ui.label(self._name.replace("_", " ").title())
|
|
247
|
+
|
|
248
|
+
with ui.row(align_items="center").classes("gap-0 col-span-2"):
|
|
249
|
+
self._add_item_btn = (
|
|
250
|
+
ui.button(icon="sym_o_list_alt_add")
|
|
251
|
+
.tooltip("Add Item")
|
|
252
|
+
.props("flat dense")
|
|
253
|
+
)
|
|
254
|
+
self._add_item_btn.set_visibility(self._panel.editable)
|
|
255
|
+
|
|
256
|
+
self._open_close_btn.on_click(self.toggle_open)
|
|
257
|
+
self._add_item_btn.on_click(self.on_add_item)
|
|
258
|
+
|
|
259
|
+
ui.space()
|
|
260
|
+
|
|
261
|
+
@ui.refreshable_method
|
|
262
|
+
def render_content(self):
|
|
263
|
+
list_values = self._getter()
|
|
264
|
+
with ui.column().classes(
|
|
265
|
+
f"w-full gap-1 col-span-2 pl-[{self._panel.min_h}]"
|
|
266
|
+
) as self._content:
|
|
267
|
+
for i, value in enumerate(list_values):
|
|
268
|
+
with ui.row(wrap=False).classes(
|
|
269
|
+
"w-full border border-neutral-500/50 gap-0"
|
|
270
|
+
):
|
|
271
|
+
with ui.column().classes("gap-0"):
|
|
272
|
+
b = (
|
|
273
|
+
ui.button(
|
|
274
|
+
icon="sym_o_arrow_upward_alt",
|
|
275
|
+
on_click=lambda e, i=i: self.move_item_up(i),
|
|
276
|
+
)
|
|
277
|
+
.props("flat dense")
|
|
278
|
+
.tooltip("Move Up")
|
|
279
|
+
)
|
|
280
|
+
b.set_visibility(self._panel.editable)
|
|
281
|
+
b = (
|
|
282
|
+
ui.button(
|
|
283
|
+
icon="sym_o_arrow_downward_alt",
|
|
284
|
+
on_click=lambda e, i=i: self.move_item_down(i),
|
|
285
|
+
)
|
|
286
|
+
.props("flat dense")
|
|
287
|
+
.tooltip("Move Down")
|
|
288
|
+
)
|
|
289
|
+
b.set_visibility(self._panel.editable)
|
|
290
|
+
name = "" # f"#{i}"
|
|
291
|
+
getter = lambda: value
|
|
292
|
+
self._panel.render_field(
|
|
293
|
+
name, getter, setter=None, value_type=self._item_type
|
|
294
|
+
)
|
|
295
|
+
with ui.column():
|
|
296
|
+
b = (
|
|
297
|
+
ui.button(
|
|
298
|
+
icon="sym_o_delete_forever",
|
|
299
|
+
on_click=lambda e, i=i: self.delete_item(i),
|
|
300
|
+
)
|
|
301
|
+
.props("dense flat")
|
|
302
|
+
.tooltip("Delete this Item")
|
|
303
|
+
)
|
|
304
|
+
b.set_visibility(self._panel.editable)
|
|
305
|
+
|
|
306
|
+
def _render_editable_value(self):
|
|
307
|
+
default_open = self._options.get("default_open")
|
|
308
|
+
if default_open is None:
|
|
309
|
+
self._options["open"] = True
|
|
310
|
+
self.render_content()
|
|
311
|
+
|
|
312
|
+
def _render_readonly_value(self):
|
|
313
|
+
default_open = self._options.get("default_open")
|
|
314
|
+
if default_open is None:
|
|
315
|
+
self._options["open"] = False
|
|
316
|
+
self.render_content()
|
|
317
|
+
|
|
318
|
+
|
|
319
|
+
class ModelField(FieldRenderer):
|
|
320
|
+
@classmethod
|
|
321
|
+
def handles(cls, value_type) -> bool:
|
|
322
|
+
return inspect.isclass(value_type) and issubclass(
|
|
323
|
+
value_type, pydantic.BaseModel
|
|
324
|
+
)
|
|
325
|
+
|
|
326
|
+
def _render_editable_value(self):
|
|
327
|
+
self._render_value()
|
|
328
|
+
|
|
329
|
+
def _render_readonly_value(self):
|
|
330
|
+
self._render_value()
|
|
331
|
+
|
|
332
|
+
def _render_value(self):
|
|
333
|
+
model: pydantic.BaseModel = self._getter()
|
|
334
|
+
model_type = type(model)
|
|
335
|
+
with ui.grid(columns="auto 1fr auto").classes(
|
|
336
|
+
"w-full gap-0 gap-x-1 col-span-2"
|
|
337
|
+
):
|
|
338
|
+
for name, field in model_type.model_fields.items():
|
|
339
|
+
getter = lambda m=model, n=name: getattr(m, n)
|
|
340
|
+
setter = lambda v, m=model, n=name: setattr(m, n, v)
|
|
341
|
+
self._panel.render_field(
|
|
342
|
+
name, getter, setter, value_type=field.annotation
|
|
343
|
+
)
|
|
344
|
+
# ui.label(str(model_type))
|
|
345
|
+
|
|
346
|
+
|
|
347
|
+
class DefaultField(FieldRenderer):
|
|
348
|
+
@classmethod
|
|
349
|
+
def handles(cls, value_type) -> bool:
|
|
350
|
+
return True
|
|
351
|
+
|
|
352
|
+
def set_options(self, *args, **kwargs) -> None:
|
|
353
|
+
kwargs["*args"] = args
|
|
354
|
+
self._options.update(kwargs)
|
|
355
|
+
|
|
356
|
+
def _render_editable_value(self):
|
|
357
|
+
self._render_value()
|
|
358
|
+
|
|
359
|
+
def _render_readonly_value(self):
|
|
360
|
+
self._render_value()
|
|
361
|
+
|
|
362
|
+
def _render_value(self):
|
|
363
|
+
value = self._getter()
|
|
364
|
+
ui.label(repr(value)).classes("w-full")
|
|
365
|
+
# ui.label(f"(!! {type(value)})")
|
|
366
|
+
|
|
367
|
+
|
|
368
|
+
class FieldRendererFactory:
|
|
369
|
+
def __init__(self):
|
|
370
|
+
self._field_renderers: list[Type[FieldRenderer]] = []
|
|
371
|
+
self._default_field_renderer = DefaultField
|
|
372
|
+
|
|
373
|
+
def add_renderer(self, FieldRendererType: Type[FieldRenderer]):
|
|
374
|
+
self._field_renderers.insert(0, FieldRendererType)
|
|
375
|
+
|
|
376
|
+
def register_renderers(self, *FieldRendererType: Type[FieldRenderer]):
|
|
377
|
+
self._field_renderers.extend(FieldRendererType)
|
|
378
|
+
|
|
379
|
+
def get_field_renderer(
|
|
380
|
+
self, panel, value_type, name, getter, setter
|
|
381
|
+
) -> FieldRenderer:
|
|
382
|
+
|
|
383
|
+
# Amazing trick from this comment:
|
|
384
|
+
# https://stackoverflow.com/questions/56832881/check-if-a-field-is-typing-optional/62641842#62641842
|
|
385
|
+
# optional = Union[value_type, None] == Union[value_type]
|
|
386
|
+
# (not using it tho bc I still need to find out the non-optional type, but you gotta read this! <3)
|
|
387
|
+
|
|
388
|
+
# Remove the optional None type and assert there's only one other valid type, then use it:
|
|
389
|
+
# if get_origin(value_type) is Union or get_origin(value_type) is UnionType:
|
|
390
|
+
if get_origin(value_type) is Union or is_union_type(get_origin(value_type)):
|
|
391
|
+
accepted_types = list(get_args(value_type))
|
|
392
|
+
if type(None) in accepted_types:
|
|
393
|
+
optional = True
|
|
394
|
+
accepted_types.remove(type(None))
|
|
395
|
+
if len(accepted_types) > 1:
|
|
396
|
+
raise TypeError(
|
|
397
|
+
f"Multiple type for field {name} ({value_type}) is not supported!"
|
|
398
|
+
)
|
|
399
|
+
value_type = accepted_types[0]
|
|
400
|
+
|
|
401
|
+
field_type = value_type
|
|
402
|
+
sub_types = ()
|
|
403
|
+
|
|
404
|
+
origin = get_origin(value_type)
|
|
405
|
+
if origin is not None:
|
|
406
|
+
field_type = origin
|
|
407
|
+
sub_types = get_args(value_type)
|
|
408
|
+
|
|
409
|
+
optional = False
|
|
410
|
+
args = get_args(field_type)
|
|
411
|
+
if args:
|
|
412
|
+
if type(None) in args:
|
|
413
|
+
optional = True
|
|
414
|
+
args = [i for i in args if i is not type(None)]
|
|
415
|
+
|
|
416
|
+
# print(" --->>", args)
|
|
417
|
+
if args:
|
|
418
|
+
if len(args) > 1:
|
|
419
|
+
raise ValueError("Unsuported dual-type field :/")
|
|
420
|
+
field_type = args[0] # type: ignore - if args + if len(args) >1 => we know args[0] exists!
|
|
421
|
+
|
|
422
|
+
# print(
|
|
423
|
+
# ">>>>>>>>",
|
|
424
|
+
# field_type,
|
|
425
|
+
# is_union_type(field_type),
|
|
426
|
+
# )
|
|
427
|
+
# if is_union_type(field_type):
|
|
428
|
+
# print("!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!")
|
|
429
|
+
# print(" >>", value_type)
|
|
430
|
+
# print(" >>", origin, get_args(origin))
|
|
431
|
+
# print(" >>", field_type)
|
|
432
|
+
# print(" >>", sub_types)
|
|
433
|
+
# print(" >>", optional)
|
|
434
|
+
|
|
435
|
+
for FieldRendererType in self._field_renderers:
|
|
436
|
+
if FieldRendererType.handles(field_type):
|
|
437
|
+
field_renderer = FieldRendererType(
|
|
438
|
+
panel, name, getter, setter, lambda: field_type()
|
|
439
|
+
)
|
|
440
|
+
field_renderer.set_optional(optional)
|
|
441
|
+
field_renderer.set_options(*sub_types)
|
|
442
|
+
# print(" Created field", field_renderer)
|
|
443
|
+
return field_renderer
|
|
444
|
+
print(f"!!!! No Field Renderer found for {value_type} ({name}), using default")
|
|
445
|
+
return self._default_field_renderer(panel, name, getter, setter, None)
|
|
446
|
+
|
|
447
|
+
|
|
448
|
+
class ParamsPanel:
|
|
449
|
+
def __init__(
|
|
450
|
+
self, params: RecipeParams, allow_edit: bool = True, editable: bool = False
|
|
451
|
+
):
|
|
452
|
+
self.params = params
|
|
453
|
+
self.min_h = "4em"
|
|
454
|
+
self.allow_edit = allow_edit
|
|
455
|
+
self.editable = editable
|
|
456
|
+
self._on_save = None
|
|
457
|
+
|
|
458
|
+
self._field_renderer_factory = FieldRendererFactory()
|
|
459
|
+
self._field_renderer_factory.register_renderers(
|
|
460
|
+
StrField, BoolField, ChoiceField, ListField, ModelField, DefaultField
|
|
461
|
+
)
|
|
462
|
+
|
|
463
|
+
def add_field_renderer(self, FieldRendererType: Type[FieldRenderer]):
|
|
464
|
+
self._field_renderer_factory.add_renderer(FieldRendererType)
|
|
465
|
+
|
|
466
|
+
def set_on_save(self, cb: Callable[[RecipeParams], None]):
|
|
467
|
+
self._on_save = cb
|
|
468
|
+
|
|
469
|
+
def save(self):
|
|
470
|
+
print("Saving:", self.params)
|
|
471
|
+
if self._on_save is not None:
|
|
472
|
+
self._on_save(self.params)
|
|
473
|
+
self.stop_edit()
|
|
474
|
+
|
|
475
|
+
def stop_edit(self):
|
|
476
|
+
self.editable = False
|
|
477
|
+
print("Edit mode", self.editable)
|
|
478
|
+
self.edit_save_btn.icon = "sym_o_edit"
|
|
479
|
+
self.render_all.refresh()
|
|
480
|
+
|
|
481
|
+
def start_edit(self):
|
|
482
|
+
if not self.allow_edit:
|
|
483
|
+
return
|
|
484
|
+
self.editable = True
|
|
485
|
+
print("Edit mode", self.editable)
|
|
486
|
+
self.edit_save_btn.icon = "sym_o_save"
|
|
487
|
+
self.render_all.refresh()
|
|
488
|
+
|
|
489
|
+
def _on_edit_save_btn(self):
|
|
490
|
+
if self.editable:
|
|
491
|
+
self.save()
|
|
492
|
+
else:
|
|
493
|
+
self.start_edit()
|
|
494
|
+
|
|
495
|
+
def render(self):
|
|
496
|
+
with ui.column().classes("gap-0"):
|
|
497
|
+
with (
|
|
498
|
+
ui.fab(icon="sym_o_menu", direction="left")
|
|
499
|
+
.props("padding=sm")
|
|
500
|
+
.classes("absolute right-3")
|
|
501
|
+
):
|
|
502
|
+
if self.editable:
|
|
503
|
+
icon = "sym_o_save"
|
|
504
|
+
else:
|
|
505
|
+
icon = "sym_o_edit"
|
|
506
|
+
self.edit_save_btn = ui.fab_action(
|
|
507
|
+
icon, on_click=self._on_edit_save_btn
|
|
508
|
+
).tooltip("Edit")
|
|
509
|
+
ui.fab_action("sym_o_content_copy").tooltip("Copy")
|
|
510
|
+
self.render_all()
|
|
511
|
+
|
|
512
|
+
@ui.refreshable_method
|
|
513
|
+
def render_all(self):
|
|
514
|
+
with ui.grid(columns="auto 1fr auto").classes("w-full gap-0 gap-x-1"):
|
|
515
|
+
self.render_field("", lambda: self.params, None, type(self.params))
|
|
516
|
+
|
|
517
|
+
@contextlib.contextmanager
|
|
518
|
+
def field_label_parent(self):
|
|
519
|
+
with ui.column(align_items="end"):
|
|
520
|
+
with ui.row(align_items="center").classes(
|
|
521
|
+
f"h-full xmin-h-[{self.min_h}]"
|
|
522
|
+
) as p:
|
|
523
|
+
yield p
|
|
524
|
+
|
|
525
|
+
@contextlib.contextmanager
|
|
526
|
+
def field_value_parent(self):
|
|
527
|
+
with ui.row(align_items="center").classes(
|
|
528
|
+
f"w-full py-1 xmin-h-[{self.min_h}]"
|
|
529
|
+
) as p:
|
|
530
|
+
yield p
|
|
531
|
+
|
|
532
|
+
def render_field_label(self, name: str):
|
|
533
|
+
with self.field_label_parent():
|
|
534
|
+
ui.label(name.replace("_", " ").title())
|
|
535
|
+
|
|
536
|
+
def render_field(self, name, getter, setter, value_type):
|
|
537
|
+
field_renderer = self._field_renderer_factory.get_field_renderer(
|
|
538
|
+
self, value_type, name, getter, setter
|
|
539
|
+
)
|
|
540
|
+
field_renderer.render()
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from tgzr.cuisine.basics.buildable import BuildContext, RecipeTypeInfo
|
|
4
|
+
from tgzr.cuisine.basics.viewable import Viewable
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
class Product(Viewable):
|
|
8
|
+
"""
|
|
9
|
+
A Viewable Recipe which include itself in the current scene when cooked in
|
|
10
|
+
a build context.
|
|
11
|
+
All dependency Products are cooked too.
|
|
12
|
+
"""
|
|
13
|
+
|
|
14
|
+
RECIPE_TYPE_INFO = RecipeTypeInfo(
|
|
15
|
+
category="Product",
|
|
16
|
+
color="#0000FF",
|
|
17
|
+
icon="sym_o_stockpot",
|
|
18
|
+
)
|
|
19
|
+
|
|
20
|
+
def cook(self):
|
|
21
|
+
ctx = BuildContext.current(raises=False)
|
|
22
|
+
if ctx is None:
|
|
23
|
+
raise Exception("Cannot cook a Product outside of a build context !")
|
|
24
|
+
|
|
25
|
+
inputs = self.get_inputs()
|
|
26
|
+
if inputs:
|
|
27
|
+
with ctx.log_group(f"Cooking inputs of {self.name!r}"):
|
|
28
|
+
for input in inputs:
|
|
29
|
+
input.cook()
|
|
30
|
+
|
|
31
|
+
print("!!! WARNING !!! Product.cook IS STILL A MOCKUP !!!")
|
|
32
|
+
ctx.log(f"Adding Product {self.name} to {ctx.to_build.file_path()}")
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
from tgzr.context import Context, LoggableContext
|
|
2
|
+
from tgzr.cuisine.recipe import Recipe
|
|
3
|
+
|
|
4
|
+
|
|
5
|
+
@Context.context_type
|
|
6
|
+
class CookContext(LoggableContext):
|
|
7
|
+
recipe: Recipe
|
|
8
|
+
|
|
9
|
+
def _log_section_summary(self) -> str:
|
|
10
|
+
return f"{self.__class__.__name__} recipe={self.recipe.name!r} ({self.recipe.__class__.__name__})"
|
|
@@ -0,0 +1,119 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
from typing import TYPE_CHECKING, Type, TypeVar, Generic, Any
|
|
3
|
+
from pathlib import Path
|
|
4
|
+
|
|
5
|
+
try:
|
|
6
|
+
from types import get_original_bases
|
|
7
|
+
except ImportError:
|
|
8
|
+
# Py 3.9 need that:
|
|
9
|
+
def get_original_bases(cls):
|
|
10
|
+
return getattr(cls, "__orig_bases__", cls.__bases__)
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
import pydantic
|
|
14
|
+
import json
|
|
15
|
+
|
|
16
|
+
from ..recipe import Recipe, RecipeControler
|
|
17
|
+
|
|
18
|
+
if TYPE_CHECKING:
|
|
19
|
+
from tgzr.cuisine.workbench import Workbench
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
class RecipeParams(pydantic.BaseModel):
|
|
23
|
+
"""
|
|
24
|
+
A pydantic model defining the params for an RecipeWithParams.
|
|
25
|
+
Subclass it and use it as generic base for you RecipeWithParams subclass.
|
|
26
|
+
|
|
27
|
+
!!! All field type **MUST** be json dumpable !!!
|
|
28
|
+
|
|
29
|
+
!!! If subclass has field of type list or sub-models, these submodels
|
|
30
|
+
need to have default values for all fields or the the Params panel
|
|
31
|
+
will not be able to create them.
|
|
32
|
+
"""
|
|
33
|
+
|
|
34
|
+
pass
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
RecipeParamsType = TypeVar("RecipeParamsType", bound=RecipeParams)
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
class RecipeWithParamControler(RecipeControler):
|
|
41
|
+
@classmethod
|
|
42
|
+
def init_recipe_files(cls, recipe: RecipeWithParams, dinit_file: Path) -> None:
|
|
43
|
+
params_file = dinit_file.parent / "params.json"
|
|
44
|
+
defaults = recipe.get_default_params()
|
|
45
|
+
cls.write_recipe_params(defaults, params_file)
|
|
46
|
+
|
|
47
|
+
@classmethod
|
|
48
|
+
def params_type(
|
|
49
|
+
cls, RecipeType: Type[RecipeWithParams[RecipeParamsType]]
|
|
50
|
+
) -> Type[RecipeParamsType]:
|
|
51
|
+
# Dont asks. That python generic class dark magic...
|
|
52
|
+
# (But if you know a better way w/o __args__ please tell me! ^_^)
|
|
53
|
+
return get_original_bases(RecipeType)[-1].__args__[0]
|
|
54
|
+
|
|
55
|
+
@classmethod
|
|
56
|
+
def read_recipe_params(
|
|
57
|
+
cls, recipe: RecipeWithParams[RecipeParamsType], params_file: Path
|
|
58
|
+
) -> RecipeParamsType:
|
|
59
|
+
with open(params_file, "r") as f:
|
|
60
|
+
data = json.load(f)
|
|
61
|
+
return cls.params_type(type(recipe))(**data)
|
|
62
|
+
|
|
63
|
+
@classmethod
|
|
64
|
+
def write_recipe_params(cls, params: RecipeParams, params_file: Path) -> None:
|
|
65
|
+
json_str = (
|
|
66
|
+
params.model_dump_json()
|
|
67
|
+
) # exclude_unset=True, exclude_defaults=True)
|
|
68
|
+
print("Saving RecipeParam to json:", json_str)
|
|
69
|
+
with open(params_file, "w") as fp:
|
|
70
|
+
fp.write(json_str)
|
|
71
|
+
|
|
72
|
+
|
|
73
|
+
class RecipeWithParams(Recipe, Generic[RecipeParamsType]):
|
|
74
|
+
_CONTROLER = RecipeWithParamControler
|
|
75
|
+
|
|
76
|
+
def get_default_params(self) -> RecipeParamsType:
|
|
77
|
+
return self._CONTROLER.params_type(type(self))()
|
|
78
|
+
|
|
79
|
+
@property
|
|
80
|
+
def params_file(self) -> Path:
|
|
81
|
+
return self._init_file.parent / "params.json"
|
|
82
|
+
|
|
83
|
+
def save_params(self, params: RecipeParamsType):
|
|
84
|
+
self._CONTROLER.write_recipe_params(params, self.params_file)
|
|
85
|
+
|
|
86
|
+
@property
|
|
87
|
+
def params(self) -> RecipeParamsType:
|
|
88
|
+
# TODO: maybe cache the param ?
|
|
89
|
+
return self._CONTROLER.read_recipe_params(self, self.params_file)
|
|
90
|
+
|
|
91
|
+
def get_params_dict(self) -> dict[str, Any]:
|
|
92
|
+
return self.params.model_dump()
|
|
93
|
+
|
|
94
|
+
def nice_panel_names(self) -> list[str]:
|
|
95
|
+
return ["params_panel"]
|
|
96
|
+
|
|
97
|
+
def _get_params_panel(self) -> Any:
|
|
98
|
+
"""
|
|
99
|
+
Subclasses can override this to fine tune the created ParamsPanel.
|
|
100
|
+
The returned value must be an instance of
|
|
101
|
+
.panels.params_panel.ParamsPanel
|
|
102
|
+
|
|
103
|
+
(We cant annotate it here bc it must be a lazy import...)
|
|
104
|
+
"""
|
|
105
|
+
|
|
106
|
+
from .panels.params_panel import ParamsPanel
|
|
107
|
+
|
|
108
|
+
panel = ParamsPanel(self.params)
|
|
109
|
+
return panel
|
|
110
|
+
|
|
111
|
+
def params_panel(self, workbench: Workbench) -> None:
|
|
112
|
+
|
|
113
|
+
def save_params(params):
|
|
114
|
+
self.save_params(params)
|
|
115
|
+
workbench.bump_recipe(self.name, bump="micro")
|
|
116
|
+
|
|
117
|
+
panel = self._get_params_panel()
|
|
118
|
+
panel.set_on_save(save_params)
|
|
119
|
+
panel.render()
|