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
tgzr/cuisine/plugin.py
ADDED
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
from typing import TYPE_CHECKING, Type
|
|
3
|
+
|
|
4
|
+
from tgzr.package_management.plugin_manager import Plugin, PluginManager
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
if TYPE_CHECKING:
|
|
8
|
+
from .recipe import Recipe
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
class CuisinePlugin(Plugin):
|
|
12
|
+
@classmethod
|
|
13
|
+
def plugin_type_name(cls) -> str:
|
|
14
|
+
return "PipelinePlugin"
|
|
15
|
+
|
|
16
|
+
def get_recipe_types(self) -> list[Type[Recipe]]:
|
|
17
|
+
return []
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
class CuisinePluginManager(PluginManager[CuisinePlugin]):
|
|
21
|
+
|
|
22
|
+
EP_GROUP = "tgzr.cuisine.plugin"
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
# NB: we use a "global" plugin manager so it is automatically
|
|
26
|
+
# picked up by `tgzr plugins ls`:
|
|
27
|
+
cuisine_plugin_manager = CuisinePluginManager()
|
tgzr/cuisine/recipe.py
ADDED
|
@@ -0,0 +1,361 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
from typing import Type, TypeVar
|
|
3
|
+
|
|
4
|
+
from pathlib import Path
|
|
5
|
+
import dataclasses
|
|
6
|
+
import datetime
|
|
7
|
+
import importlib
|
|
8
|
+
|
|
9
|
+
import toml
|
|
10
|
+
|
|
11
|
+
from tgzr.package_management.pyproject import (
|
|
12
|
+
PyProject,
|
|
13
|
+
save_pyproject,
|
|
14
|
+
BuildSystem,
|
|
15
|
+
Project,
|
|
16
|
+
Tools,
|
|
17
|
+
ToolHatch,
|
|
18
|
+
ToolHatchVersion,
|
|
19
|
+
)
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
@dataclasses.dataclass
|
|
23
|
+
class RecipeTypeInfo:
|
|
24
|
+
type_name: str | None = None # only needed if it's not the class name
|
|
25
|
+
category: str | None = None # just for UIs (None means hidden category)
|
|
26
|
+
color: str | None = None # html format
|
|
27
|
+
icon: str | None = None # one of the google-font or fontawesome icons
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
@dataclasses.dataclass
|
|
31
|
+
class RecipeMetadata:
|
|
32
|
+
name: str
|
|
33
|
+
type_name: str
|
|
34
|
+
inputs: dict[str | None, list[str]] = dataclasses.field(default_factory=dict)
|
|
35
|
+
# Note: we should use a set for tags, but a set get converted to a list when savec in toml so:
|
|
36
|
+
tags: list[str] = dataclasses.field(default_factory=list)
|
|
37
|
+
|
|
38
|
+
@classmethod
|
|
39
|
+
def create_default(cls, name: str, type_name: str):
|
|
40
|
+
return cls(
|
|
41
|
+
name=name,
|
|
42
|
+
type_name=type_name,
|
|
43
|
+
)
|
|
44
|
+
|
|
45
|
+
@classmethod
|
|
46
|
+
def from_toml(cls, toml_path: Path):
|
|
47
|
+
asset_data = toml.load(toml_path)
|
|
48
|
+
return cls(**asset_data)
|
|
49
|
+
|
|
50
|
+
def write_toml(self, toml_path: Path):
|
|
51
|
+
toml_path.write_text(toml.dumps(dataclasses.asdict(self)))
|
|
52
|
+
print(f"Recipe {self.name} Metadata saved to {toml_path}")
|
|
53
|
+
|
|
54
|
+
|
|
55
|
+
class RecipeControler:
|
|
56
|
+
"""
|
|
57
|
+
This object contains methods that would totally fit in the Recipe interface,
|
|
58
|
+
but are kept separated to keep the Recipe namespace as empty as possible.
|
|
59
|
+
|
|
60
|
+
It makes subclassing a little bit harder, but only for subclasses that
|
|
61
|
+
are changing the relation between the Chef and the Recipe (which should
|
|
62
|
+
not be often outside of tgzr.cuisine, if we designed thing right :p)
|
|
63
|
+
"""
|
|
64
|
+
|
|
65
|
+
@classmethod
|
|
66
|
+
def default_dinit_content(cls, RecipeType: Type[Recipe]) -> str:
|
|
67
|
+
cls_module = RecipeType.__module__
|
|
68
|
+
cls_name = RecipeType.__name__
|
|
69
|
+
|
|
70
|
+
return f"""
|
|
71
|
+
from {cls_module} import {cls_name}
|
|
72
|
+
|
|
73
|
+
recipe = {cls_name}(__file__)
|
|
74
|
+
|
|
75
|
+
def cook() -> None:
|
|
76
|
+
recipe.cook()
|
|
77
|
+
|
|
78
|
+
"""
|
|
79
|
+
|
|
80
|
+
@classmethod
|
|
81
|
+
def init_recipe_files(cls, recipe: Recipe, dinit_file: Path) -> None:
|
|
82
|
+
"""
|
|
83
|
+
This is called by the chef after it created a new
|
|
84
|
+
reciep.
|
|
85
|
+
Subclasses may override this to add more files inside the
|
|
86
|
+
recipe package.
|
|
87
|
+
"""
|
|
88
|
+
pass
|
|
89
|
+
|
|
90
|
+
@classmethod
|
|
91
|
+
def make_pyproject(cls, recipe: Recipe) -> PyProject:
|
|
92
|
+
metadata = cls.read_metadata(recipe)
|
|
93
|
+
pyproject = PyProject()
|
|
94
|
+
|
|
95
|
+
name = recipe.name # this is the coninical package name
|
|
96
|
+
pyproject.project = project = Project()
|
|
97
|
+
project.name = name
|
|
98
|
+
project.dynamic = ["version"]
|
|
99
|
+
project.description = f"{metadata.name} - a Recipe created with tgzr.cuisine."
|
|
100
|
+
project.readme = "README.md"
|
|
101
|
+
project.dependencies = ["tgzr.cuisine"] + metadata.inputs.get(None, [])
|
|
102
|
+
# TODO: add extra input groups too!
|
|
103
|
+
|
|
104
|
+
# All Recipe have a cook() method
|
|
105
|
+
project.scripts[name] = f"{name}:cook"
|
|
106
|
+
|
|
107
|
+
project.classifiers = [
|
|
108
|
+
# We use classifiers as searchable hierarchy in private indexes. Must be stable.
|
|
109
|
+
# /!\ when building the package, we need HATCH_METADATA_CLASSIFIERS_NO_VERIFY env var to be set
|
|
110
|
+
# or these custom classifiers will be rejected.
|
|
111
|
+
"Private :: Do Not Upload", # prevents accidental upload to PyPI
|
|
112
|
+
"Framework :: tgzr.cuisine",
|
|
113
|
+
f"RecipeType :: {metadata.type_name}",
|
|
114
|
+
]
|
|
115
|
+
project.keywords = [
|
|
116
|
+
# We use keywords for fine search in private indexes. Can be arbitrary.
|
|
117
|
+
f"tgzr.cuisine.recipe_name:{metadata.name}",
|
|
118
|
+
f"tgzr.cuisine.recipe_type_name:{metadata.type_name}",
|
|
119
|
+
*[
|
|
120
|
+
f"tgzr.cuisine.tags:{tag}" for tag in metadata.tags
|
|
121
|
+
], # TODO: ensure tag are alpha+alphanum only ! (or valid identifiers)
|
|
122
|
+
]
|
|
123
|
+
project.entry_points = {
|
|
124
|
+
# Gives the hability to discover all installed Recipes of a given (final) type in the venv:
|
|
125
|
+
f"tgzr.pipeline.{metadata.type_name}": {"recipe": f"{name}:recipe"},
|
|
126
|
+
#
|
|
127
|
+
# These ep is the source of truth when getting name and type from the Distribution
|
|
128
|
+
# (not the classifers or keywords)
|
|
129
|
+
# Also: it give the hability to discover all installed Recipe names and types:
|
|
130
|
+
"tgzr.pipeline.asset_info_trick": {
|
|
131
|
+
"recipe_name": metadata.name,
|
|
132
|
+
"recipe_type": metadata.type_name,
|
|
133
|
+
},
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
pyproject.build_system = BuildSystem(
|
|
137
|
+
requires=["hatchling"], build_backend="hatchling.build"
|
|
138
|
+
)
|
|
139
|
+
|
|
140
|
+
pyproject.tool = tool = Tools()
|
|
141
|
+
tool.hatch = ToolHatch()
|
|
142
|
+
# does not work in current version,
|
|
143
|
+
# might in next version,
|
|
144
|
+
# for now we still need to have the envvar HATCH_METADATA_CLASSIFIERS_NO_VERIFY set !
|
|
145
|
+
tool.hatch.metadata = {"allow-custom-classifiers": True}
|
|
146
|
+
tool.hatch.envs = {"default": {"installer": "uv"}}
|
|
147
|
+
tool.hatch.version = ToolHatchVersion(path=f"src/{name}/__version__.py")
|
|
148
|
+
|
|
149
|
+
panel_names = recipe.nice_panel_names()
|
|
150
|
+
if panel_names:
|
|
151
|
+
nice_panels_ep = project.entry_points["tgzr.pipeline.asset.nice_panel"] = {}
|
|
152
|
+
for panel_name in panel_names:
|
|
153
|
+
nice_panels_ep[panel_name] = f"{recipe.name}:recipe.{panel_name}"
|
|
154
|
+
|
|
155
|
+
return pyproject
|
|
156
|
+
|
|
157
|
+
@classmethod
|
|
158
|
+
def write_pyproject_to(cls, recipe: Recipe, pyproject_path: Path):
|
|
159
|
+
pyproject = cls.make_pyproject(recipe)
|
|
160
|
+
print("Saving pyproject to:", pyproject_path)
|
|
161
|
+
save_pyproject(pyproject, pyproject_path)
|
|
162
|
+
|
|
163
|
+
@classmethod
|
|
164
|
+
def is_editable(cls, recipe: Recipe) -> bool:
|
|
165
|
+
for parent in recipe._init_file.parents:
|
|
166
|
+
if (parent / "pyproject.toml").exists():
|
|
167
|
+
# FIXME: this could be a uv workspace pyproject !!!
|
|
168
|
+
return True
|
|
169
|
+
return False
|
|
170
|
+
|
|
171
|
+
@classmethod
|
|
172
|
+
def read_metadata(cls, recipe: Recipe) -> RecipeMetadata:
|
|
173
|
+
data = RecipeMetadata.from_toml(recipe._recipe_toml)
|
|
174
|
+
return data
|
|
175
|
+
|
|
176
|
+
@classmethod
|
|
177
|
+
def write_metadata(cls, recipe: Recipe, metadata: RecipeMetadata):
|
|
178
|
+
metadata.write_toml(recipe._recipe_toml)
|
|
179
|
+
|
|
180
|
+
@classmethod
|
|
181
|
+
def create_editable(
|
|
182
|
+
cls, recipe: Recipe, workbench_path: Path | str, force: bool = False
|
|
183
|
+
):
|
|
184
|
+
if recipe.is_editable:
|
|
185
|
+
raise ValueError(
|
|
186
|
+
f"Are you sure you want to create an editable version of {recipe.name}? \n"
|
|
187
|
+
f"(it is already editable as {recipe._init_file})."
|
|
188
|
+
)
|
|
189
|
+
|
|
190
|
+
from .workbench import Workbench
|
|
191
|
+
|
|
192
|
+
workbench_path = Path(workbench_path)
|
|
193
|
+
wb = Workbench(workbench_path)
|
|
194
|
+
if wb.has_editable_recipe(recipe.name):
|
|
195
|
+
if not force:
|
|
196
|
+
raise ValueError(
|
|
197
|
+
f"The Recipe {recipe.name} is already an editable in workbench {workbench_path}."
|
|
198
|
+
)
|
|
199
|
+
print(
|
|
200
|
+
f"Overridding editable Recipe {recipe.name} in workbench {workbench_path}"
|
|
201
|
+
)
|
|
202
|
+
else:
|
|
203
|
+
print(
|
|
204
|
+
f"Creating editable Recipe {recipe.name} in workbench {workbench_path}"
|
|
205
|
+
)
|
|
206
|
+
|
|
207
|
+
version = recipe.get_version()
|
|
208
|
+
metadata = cls.read_metadata(recipe)
|
|
209
|
+
|
|
210
|
+
wb.chef.create_recipe(
|
|
211
|
+
folder=wb._editable_path,
|
|
212
|
+
recipe_name=recipe.name,
|
|
213
|
+
recipe_metadata=metadata,
|
|
214
|
+
version=version,
|
|
215
|
+
recipe_type_name=metadata.type_name,
|
|
216
|
+
)
|
|
217
|
+
|
|
218
|
+
|
|
219
|
+
RecipeType = TypeVar("RecipeType", bound="Recipe")
|
|
220
|
+
|
|
221
|
+
|
|
222
|
+
class Recipe:
|
|
223
|
+
RECIPE_TYPE_INFO = RecipeTypeInfo(
|
|
224
|
+
type_name=None,
|
|
225
|
+
category="AbstractRecipes",
|
|
226
|
+
color=None,
|
|
227
|
+
icon=None,
|
|
228
|
+
)
|
|
229
|
+
_CONTROLER = RecipeControler
|
|
230
|
+
|
|
231
|
+
def __init__(self, init_file: Path | str):
|
|
232
|
+
self._init_file = Path(init_file)
|
|
233
|
+
self._name = self._init_file.parent.name
|
|
234
|
+
self._recipe_toml = (self._init_file / ".." / "recipe.toml").resolve()
|
|
235
|
+
self._svg_cooking_log_path = self._init_file.parent / "cooking_log.svg"
|
|
236
|
+
self._txt_cooking_log_path = self._init_file.parent / "cooking_log.txt"
|
|
237
|
+
|
|
238
|
+
@property
|
|
239
|
+
def name(self) -> str:
|
|
240
|
+
return self._name
|
|
241
|
+
|
|
242
|
+
@property
|
|
243
|
+
def is_editable(self) -> bool:
|
|
244
|
+
return self._CONTROLER.is_editable(self)
|
|
245
|
+
|
|
246
|
+
@property
|
|
247
|
+
def svg_cooking_log_path(self):
|
|
248
|
+
return self._svg_cooking_log_path
|
|
249
|
+
|
|
250
|
+
@property
|
|
251
|
+
def txt_cooking_log_path(self):
|
|
252
|
+
return self._txt_cooking_log_path
|
|
253
|
+
|
|
254
|
+
def cook(self):
|
|
255
|
+
from rich.console import Console
|
|
256
|
+
|
|
257
|
+
console = Console(record=True)
|
|
258
|
+
try:
|
|
259
|
+
self._cook(print=console.print)
|
|
260
|
+
finally:
|
|
261
|
+
console.save_text(str(self._txt_cooking_log_path), clear=False)
|
|
262
|
+
title = f"{self.name} Cooking Log {datetime.datetime.now().isoformat(sep=' ', timespec='minutes')}"
|
|
263
|
+
console.save_svg(str(self._svg_cooking_log_path), title=title)
|
|
264
|
+
|
|
265
|
+
def _cook(self, print):
|
|
266
|
+
print(
|
|
267
|
+
f"Hello from Recipe {self.name} (version={self.get_version()}, {self.is_editable=})"
|
|
268
|
+
)
|
|
269
|
+
print(self._CONTROLER.read_metadata(self))
|
|
270
|
+
|
|
271
|
+
def get_version(self) -> str:
|
|
272
|
+
version_module = importlib.import_module(self.name + ".__version__")
|
|
273
|
+
return version_module.__version__
|
|
274
|
+
|
|
275
|
+
def nice_panel_names(self) -> list[str]:
|
|
276
|
+
"""
|
|
277
|
+
Asset subclasses implementing nice panel guis
|
|
278
|
+
must override this method to return the name of the method
|
|
279
|
+
implementing each panel.
|
|
280
|
+
|
|
281
|
+
NB: You panel method will **NOT** be executed in the asset
|
|
282
|
+
virtual env. That means you CAN NOT do things like cook them
|
|
283
|
+
or even import their input recipes with `self.get_inputs()`.
|
|
284
|
+
But you can access the recipe data with `self._CONTROLER.read_metadata(...)`
|
|
285
|
+
and save them with `self._CONTROLER.write_metadata(...)`.
|
|
286
|
+
If you **really** want to affect the recipe, you need to do so
|
|
287
|
+
using the Workspace provided with the `workspace` argument.
|
|
288
|
+
|
|
289
|
+
NB: GUIs will show your panels by alphabetic order, but you
|
|
290
|
+
can preprend some panel names with some "_" to bring them first.
|
|
291
|
+
|
|
292
|
+
Example:
|
|
293
|
+
|
|
294
|
+
from tgzr.cuisine.workspace import Workspace
|
|
295
|
+
|
|
296
|
+
class MyRecipe(Recipe):
|
|
297
|
+
def nice_panel_name(self)->list[str]:
|
|
298
|
+
return ['preview_panel', 'options_panel']
|
|
299
|
+
|
|
300
|
+
def preview_panel(self, workspace:Workspace)->None:
|
|
301
|
+
ui.label('This is the preview panel.')
|
|
302
|
+
|
|
303
|
+
def options_panel(self, workspace:Workspace)->None:
|
|
304
|
+
ui.label('This is the option panel.')
|
|
305
|
+
|
|
306
|
+
"""
|
|
307
|
+
return []
|
|
308
|
+
|
|
309
|
+
def get_inputs(self, group: str | None = None) -> list[Recipe]:
|
|
310
|
+
"""
|
|
311
|
+
Returns a dict like {recipe_name:Recipe} with all inputs in
|
|
312
|
+
the group `group`.
|
|
313
|
+
"""
|
|
314
|
+
assets = []
|
|
315
|
+
recipe_metadata = self._CONTROLER.read_metadata(self)
|
|
316
|
+
for package_name in recipe_metadata.inputs.get(group, []):
|
|
317
|
+
module = importlib.import_module(package_name)
|
|
318
|
+
try:
|
|
319
|
+
recipe: Recipe = module.recipe
|
|
320
|
+
except AttributeError:
|
|
321
|
+
print(f"!!Warning!! upstream {package_name} is not an Recipe ?!?")
|
|
322
|
+
else:
|
|
323
|
+
assets.append(module.asset)
|
|
324
|
+
return assets
|
|
325
|
+
|
|
326
|
+
def get_unique_input(self, group: str, required: bool = True):
|
|
327
|
+
"""
|
|
328
|
+
Return the one and only input in the group `group`.
|
|
329
|
+
If there's not exactly one input in this group, raises a
|
|
330
|
+
Exception.
|
|
331
|
+
"""
|
|
332
|
+
inputs = self.get_inputs(group=group)
|
|
333
|
+
if required and not inputs:
|
|
334
|
+
raise Exception(
|
|
335
|
+
f"You need to connect something in {group!r} dependency of asset {self.name!r}!"
|
|
336
|
+
)
|
|
337
|
+
|
|
338
|
+
if len(inputs) != 1:
|
|
339
|
+
raise Exception(
|
|
340
|
+
f"You need one and exactly one {group!r} dependency. (got: {inputs})"
|
|
341
|
+
)
|
|
342
|
+
return inputs[0]
|
|
343
|
+
|
|
344
|
+
def get_typed_inputs(
|
|
345
|
+
self, RecipeType: Type[RecipeType], group: str, raises: bool = True
|
|
346
|
+
) -> dict[str, RecipeType]:
|
|
347
|
+
"""
|
|
348
|
+
Returns a {name:recipe} dict with all recipes in input group `group` of
|
|
349
|
+
type `RecipeType`.
|
|
350
|
+
"""
|
|
351
|
+
inputs = self.get_inputs(group="params")
|
|
352
|
+
matching: dict[str, RecipeType] = {}
|
|
353
|
+
for input in inputs:
|
|
354
|
+
if not isinstance(input, RecipeType):
|
|
355
|
+
if raises:
|
|
356
|
+
raise TypeError(
|
|
357
|
+
f"The input {input.name} in group {group!r} of {self.name} is not a {RecipeType} but a {type(input)}!"
|
|
358
|
+
)
|
|
359
|
+
continue
|
|
360
|
+
matching[input.name] = input
|
|
361
|
+
return matching
|