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,33 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from tgzr.cuisine.basics.buildable import Buildable, RecipeTypeInfo
|
|
4
|
+
from tgzr.cuisine.basics.recipe_contexts import CookContext
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
@CookContext.context_type
|
|
8
|
+
class ViewContext(CookContext):
|
|
9
|
+
to_view: Viewable
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
class Viewable(Buildable):
|
|
13
|
+
"""
|
|
14
|
+
A Buildable Recipe which cooks its 'viewer' input in its context after building
|
|
15
|
+
if if needed.
|
|
16
|
+
"""
|
|
17
|
+
|
|
18
|
+
RECIPE_TYPE_INFO = RecipeTypeInfo(
|
|
19
|
+
category="View",
|
|
20
|
+
color="#0000FF",
|
|
21
|
+
icon="sym_o_play_pause",
|
|
22
|
+
)
|
|
23
|
+
|
|
24
|
+
def view(self):
|
|
25
|
+
current_context = CookContext.current()
|
|
26
|
+
current_context.log(f"Opening viewer for {self.file_path()}")
|
|
27
|
+
viewer = self.get_unique_input("view")
|
|
28
|
+
with current_context.get(ViewContext, asset_to_view=self):
|
|
29
|
+
viewer.cook()
|
|
30
|
+
|
|
31
|
+
def cook(self):
|
|
32
|
+
super().cook()
|
|
33
|
+
self.view()
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from pathlib import Path
|
|
4
|
+
|
|
5
|
+
from tgzr.cuisine.recipe import Recipe, RecipeTypeInfo
|
|
6
|
+
from tgzr.cuisine.basics.recipe_contexts import CookContext
|
|
7
|
+
from tgzr.cuisine.basics.viewable import ViewContext
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
class Viewer(Recipe):
|
|
11
|
+
"""
|
|
12
|
+
An Recipe which will open the 'file' from the current Viewable asset with
|
|
13
|
+
a defined viewer when cooked.
|
|
14
|
+
"""
|
|
15
|
+
|
|
16
|
+
RECIPE_TYPE_INFO = RecipeTypeInfo(
|
|
17
|
+
category="View",
|
|
18
|
+
color="#0000FF",
|
|
19
|
+
icon="sym_o_play_circle",
|
|
20
|
+
)
|
|
21
|
+
|
|
22
|
+
def launch_viewer(self, file_path: Path):
|
|
23
|
+
ViewContext.current().log(f"Launching viewer for {file_path}")
|
|
24
|
+
|
|
25
|
+
def cook(self):
|
|
26
|
+
ctx = ViewContext.current(raises=False)
|
|
27
|
+
if ctx is None:
|
|
28
|
+
raise Exception("Cannot view an asset outside of a ViewContext.")
|
|
29
|
+
ctx.log(f"Viewer cooking: {self.name}")
|
|
30
|
+
self.launch_viewer(ctx.to_view.file_path())
|
tgzr/cuisine/chef.py
ADDED
|
@@ -0,0 +1,339 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
from typing import TYPE_CHECKING, Type, Callable, Any
|
|
3
|
+
|
|
4
|
+
import os
|
|
5
|
+
from pathlib import Path
|
|
6
|
+
from importlib_metadata import Distribution
|
|
7
|
+
import dataclasses
|
|
8
|
+
import datetime
|
|
9
|
+
|
|
10
|
+
import rich
|
|
11
|
+
import packaging.utils
|
|
12
|
+
import packaging.requirements
|
|
13
|
+
|
|
14
|
+
from tgzr.package_management.venv import Venv
|
|
15
|
+
from .recipe import Recipe, RecipeMetadata, RecipeTypeInfo
|
|
16
|
+
|
|
17
|
+
if TYPE_CHECKING:
|
|
18
|
+
from .recipe import RecipeControler
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
@dataclasses.dataclass
|
|
22
|
+
class RecipeInfo:
|
|
23
|
+
"""
|
|
24
|
+
Recipe metadata extracted from
|
|
25
|
+
a `importlib.metadata.Distribution()`.
|
|
26
|
+
|
|
27
|
+
These are created by Workbench.get_recipe_info
|
|
28
|
+
"""
|
|
29
|
+
|
|
30
|
+
dist: Distribution
|
|
31
|
+
is_asset: bool
|
|
32
|
+
asset_name: str
|
|
33
|
+
asset_type: str | None
|
|
34
|
+
tags: set[str]
|
|
35
|
+
is_editable: bool
|
|
36
|
+
editable_path: Path | None
|
|
37
|
+
nice_panel_names: list[str]
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
class Chef:
|
|
41
|
+
"""
|
|
42
|
+
Chef is doing things with recipes
|
|
43
|
+
"""
|
|
44
|
+
|
|
45
|
+
def __init__(self, venv_path: Path):
|
|
46
|
+
self._venv_path = venv_path
|
|
47
|
+
self._venv = Venv(venv_path)
|
|
48
|
+
self._recipe_types: list[Type[Recipe]] = []
|
|
49
|
+
|
|
50
|
+
def register_recipe_types(self, *recipe_types):
|
|
51
|
+
# fill empty 'type_name' with default:
|
|
52
|
+
for RecipeType in recipe_types:
|
|
53
|
+
if RecipeType.RECIPE_TYPE_INFO.type_name is None:
|
|
54
|
+
RecipeType.RECIPE_TYPE_INFO.type_name = RecipeType.__name__
|
|
55
|
+
self._recipe_types.extend(recipe_types)
|
|
56
|
+
|
|
57
|
+
def get_recipe_type(self, recipe_type_name: str) -> Type[Recipe]:
|
|
58
|
+
for RecipeType in self._recipe_types:
|
|
59
|
+
if RecipeType.RECIPE_TYPE_INFO.type_name == recipe_type_name:
|
|
60
|
+
return RecipeType
|
|
61
|
+
raise KeyError(f"No Recipe registered with type named {recipe_type_name!r}!")
|
|
62
|
+
|
|
63
|
+
def get_recipe_types(self) -> list[Type[Recipe]]:
|
|
64
|
+
return list(self._recipe_types)
|
|
65
|
+
|
|
66
|
+
def get_recipe_id(self, recipe_name: str) -> str:
|
|
67
|
+
"""
|
|
68
|
+
Return the recipe id for this recipe_name (the canonical name of the
|
|
69
|
+
recipe package).
|
|
70
|
+
|
|
71
|
+
if validate is True:
|
|
72
|
+
Raises a ValueError if name cannot be used as a Recipe name.
|
|
73
|
+
"""
|
|
74
|
+
try:
|
|
75
|
+
recipe_id = packaging.utils.canonicalize_name(recipe_name, validate=True)
|
|
76
|
+
except packaging.utils.InvalidName as err:
|
|
77
|
+
raise ValueError(f"Invalid Recipe name {recipe_name}.")
|
|
78
|
+
|
|
79
|
+
if not recipe_id.replace(".", "").replace("-", "").isidentifier():
|
|
80
|
+
suffix = ""
|
|
81
|
+
if recipe_id != recipe_name:
|
|
82
|
+
suffix = f" (canonicalized to {recipe_id}"
|
|
83
|
+
raise ValueError(f"Invalid Recipe name {recipe_name!r}{suffix}.")
|
|
84
|
+
|
|
85
|
+
return recipe_id
|
|
86
|
+
|
|
87
|
+
def create_recipe(
|
|
88
|
+
self,
|
|
89
|
+
folder: Path,
|
|
90
|
+
recipe_name: str,
|
|
91
|
+
recipe_metadata: RecipeMetadata | None,
|
|
92
|
+
version: str | None,
|
|
93
|
+
recipe_type_name: str,
|
|
94
|
+
):
|
|
95
|
+
"""
|
|
96
|
+
Creates an editable recipe in folder and returns
|
|
97
|
+
the id of the created recipe (the canonical name of the package).
|
|
98
|
+
"""
|
|
99
|
+
RecipeType = self.get_recipe_type(recipe_type_name)
|
|
100
|
+
|
|
101
|
+
recipe_metadata = recipe_metadata or RecipeMetadata.create_default(
|
|
102
|
+
name=recipe_name,
|
|
103
|
+
type_name=recipe_type_name,
|
|
104
|
+
)
|
|
105
|
+
version = version or "0.1.0"
|
|
106
|
+
package_name = self.get_recipe_id(recipe_name)
|
|
107
|
+
|
|
108
|
+
recipe_path = folder / package_name
|
|
109
|
+
rich.print(f"Creating Recipe {recipe_name!r} ({package_name=})")
|
|
110
|
+
rich.print(f"ReceipMetadata: {recipe_metadata}")
|
|
111
|
+
|
|
112
|
+
recipe_path.mkdir(exist_ok=True)
|
|
113
|
+
|
|
114
|
+
src = (
|
|
115
|
+
recipe_path / "src" / package_name
|
|
116
|
+
) # FIXME: support . here (package namespaces)
|
|
117
|
+
src.mkdir(parents=True, exist_ok=True)
|
|
118
|
+
|
|
119
|
+
recipe_toml = src / "recipe.toml"
|
|
120
|
+
recipe_metadata.write_toml(recipe_toml)
|
|
121
|
+
|
|
122
|
+
dinit = src / "__init__.py"
|
|
123
|
+
dinit_content = RecipeType._CONTROLER.default_dinit_content(RecipeType)
|
|
124
|
+
dinit.write_text(dinit_content)
|
|
125
|
+
|
|
126
|
+
dversion = src / "__version__.py"
|
|
127
|
+
dversion_content = f'__version__ = "{version}"'
|
|
128
|
+
dversion.write_text(dversion_content)
|
|
129
|
+
|
|
130
|
+
readme = recipe_path / "README.md"
|
|
131
|
+
readme_content = f"# tgzr.cuisine Recipe: {recipe_name}\n\nCreated on {datetime.datetime.now().strftime('%c')}\n"
|
|
132
|
+
readme.write_text(readme_content)
|
|
133
|
+
|
|
134
|
+
recipe = RecipeType(dinit)
|
|
135
|
+
recipe._CONTROLER.init_recipe_files(recipe, dinit)
|
|
136
|
+
|
|
137
|
+
pyproject = recipe_path / "pyproject.toml"
|
|
138
|
+
recipe._CONTROLER.write_pyproject_to(recipe, pyproject)
|
|
139
|
+
return package_name
|
|
140
|
+
|
|
141
|
+
def get_recipe_from_toml(self, toml_path) -> Recipe:
|
|
142
|
+
recipe_metadata = RecipeMetadata.from_toml(toml_path)
|
|
143
|
+
recipe_type_name = recipe_metadata.type_name
|
|
144
|
+
RecipeType = self.get_recipe_type(recipe_type_name)
|
|
145
|
+
dinit = toml_path.parent / "__init__.py"
|
|
146
|
+
return RecipeType(dinit)
|
|
147
|
+
|
|
148
|
+
def _edit_recipe_metadata(
|
|
149
|
+
self,
|
|
150
|
+
folder: Path,
|
|
151
|
+
recipe_name: str,
|
|
152
|
+
modifier: Callable[[RecipeMetadata], RecipeMetadata | None],
|
|
153
|
+
rebuild_pyproject: bool,
|
|
154
|
+
bump: str | None = "patch,a",
|
|
155
|
+
):
|
|
156
|
+
recipe_path = folder / recipe_name
|
|
157
|
+
if not recipe_path.exists():
|
|
158
|
+
raise ValueError(
|
|
159
|
+
f"Cannot edit recipe metadata for {recipe_name!r}: it was not found in {folder})."
|
|
160
|
+
)
|
|
161
|
+
|
|
162
|
+
toml_path = recipe_path / "src" / recipe_name / "recipe.toml"
|
|
163
|
+
recipe = self.get_recipe_from_toml(toml_path)
|
|
164
|
+
recipe_metadata = recipe._CONTROLER.read_metadata(recipe)
|
|
165
|
+
|
|
166
|
+
recipe_metadata = modifier(recipe_metadata)
|
|
167
|
+
|
|
168
|
+
if recipe_metadata is None:
|
|
169
|
+
print("Nothing to save.")
|
|
170
|
+
return
|
|
171
|
+
|
|
172
|
+
# rich.print(recipe_metadata)
|
|
173
|
+
|
|
174
|
+
recipe._CONTROLER.write_metadata(recipe, recipe_metadata)
|
|
175
|
+
|
|
176
|
+
if rebuild_pyproject:
|
|
177
|
+
pyproject = recipe_path / "pyproject.toml"
|
|
178
|
+
recipe._CONTROLER.write_pyproject_to(recipe, pyproject)
|
|
179
|
+
|
|
180
|
+
if bump is not None:
|
|
181
|
+
self.bump_recipe(folder, recipe_name, "patch,a")
|
|
182
|
+
|
|
183
|
+
def rebuild_pyproject(self, folder: Path, recipe_name: str):
|
|
184
|
+
recipe_path = folder / recipe_name
|
|
185
|
+
if not recipe_path.exists():
|
|
186
|
+
raise ValueError(
|
|
187
|
+
f"Cannot rebuild pyproject for {recipe_name!r}: it was not found in {folder})."
|
|
188
|
+
)
|
|
189
|
+
|
|
190
|
+
toml_path = recipe_path / "src" / recipe_name / "recipe.toml"
|
|
191
|
+
recipe = self.get_recipe_from_toml(toml_path)
|
|
192
|
+
pyproject = recipe_path / "pyproject.toml"
|
|
193
|
+
recipe._CONTROLER.write_pyproject_to(recipe, pyproject)
|
|
194
|
+
|
|
195
|
+
def rebuild_dinit(self, folder: Path, recipe_name: str):
|
|
196
|
+
recipe_path = folder / recipe_name
|
|
197
|
+
if not recipe_path.exists():
|
|
198
|
+
raise ValueError(
|
|
199
|
+
f"Cannot rebuild pyproject for {recipe_name!r}: it was not found in {folder})."
|
|
200
|
+
)
|
|
201
|
+
|
|
202
|
+
src = recipe_path / "src" / recipe_name
|
|
203
|
+
toml_path = src / "recipe.toml"
|
|
204
|
+
recipe = self.get_recipe_from_toml(toml_path)
|
|
205
|
+
dinit = src / "__init__.py"
|
|
206
|
+
dinit_content = recipe._CONTROLER.default_dinit_content(type(recipe))
|
|
207
|
+
dinit.write_text(dinit_content)
|
|
208
|
+
recipe._CONTROLER.init_recipe_files(recipe, dinit)
|
|
209
|
+
|
|
210
|
+
def add_tags(self, folder: Path, recipe_name: str, tags: set[str]):
|
|
211
|
+
|
|
212
|
+
def add_tags(recipe_metadata: RecipeMetadata) -> RecipeMetadata | None:
|
|
213
|
+
rich.print(f"Adding tags {tags!r} to {recipe_name!r}.")
|
|
214
|
+
old_tags = set(recipe_metadata.tags)
|
|
215
|
+
recipe_metadata.tags.extend([t for t in tags if t not in old_tags])
|
|
216
|
+
if set(recipe_metadata.tags) == set(old_tags):
|
|
217
|
+
print("No change in tags, no need to save.")
|
|
218
|
+
return
|
|
219
|
+
return recipe_metadata
|
|
220
|
+
|
|
221
|
+
self._edit_recipe_metadata(
|
|
222
|
+
folder, recipe_name, add_tags, rebuild_pyproject=True, bump="patch,a"
|
|
223
|
+
)
|
|
224
|
+
|
|
225
|
+
def add_inputs(
|
|
226
|
+
self,
|
|
227
|
+
folder: Path,
|
|
228
|
+
recipe_name: str,
|
|
229
|
+
group: str = "",
|
|
230
|
+
*input_requirements: str,
|
|
231
|
+
):
|
|
232
|
+
rich.print(f"Adding inputs {input_requirements!r} to {recipe_name!r}.")
|
|
233
|
+
|
|
234
|
+
def add_inputs(recipe_metadata: RecipeMetadata) -> RecipeMetadata | None:
|
|
235
|
+
existing_requirements = [
|
|
236
|
+
packaging.requirements.Requirement(i)
|
|
237
|
+
for i in recipe_metadata.inputs.get(group, [])
|
|
238
|
+
]
|
|
239
|
+
existing_named_requirements = dict(
|
|
240
|
+
[(req.name, req) for req in existing_requirements]
|
|
241
|
+
)
|
|
242
|
+
reqs_to_remove = []
|
|
243
|
+
reqs_to_add = []
|
|
244
|
+
for input_requirement in input_requirements:
|
|
245
|
+
input_req = packaging.requirements.Requirement(input_requirement)
|
|
246
|
+
if input_req.name in existing_named_requirements:
|
|
247
|
+
existing_req = existing_named_requirements[input_req.name]
|
|
248
|
+
if input_req == existing_req:
|
|
249
|
+
# this requirement is already there
|
|
250
|
+
pass
|
|
251
|
+
else:
|
|
252
|
+
reqs_to_remove.append(existing_req)
|
|
253
|
+
reqs_to_add.append(input_req)
|
|
254
|
+
else:
|
|
255
|
+
reqs_to_add.append(input_req)
|
|
256
|
+
|
|
257
|
+
new_dependencies = []
|
|
258
|
+
for req in existing_requirements:
|
|
259
|
+
if req in reqs_to_remove:
|
|
260
|
+
continue
|
|
261
|
+
new_dependencies.append(str(req))
|
|
262
|
+
|
|
263
|
+
new_dependencies += [str(req) for req in reqs_to_add]
|
|
264
|
+
if recipe_metadata.inputs.get(group, []) == new_dependencies:
|
|
265
|
+
# nothing changed, no need to save
|
|
266
|
+
return None
|
|
267
|
+
|
|
268
|
+
recipe_metadata.inputs[group] = new_dependencies
|
|
269
|
+
rich.print(recipe_metadata)
|
|
270
|
+
return recipe_metadata
|
|
271
|
+
|
|
272
|
+
self._edit_recipe_metadata(
|
|
273
|
+
folder, recipe_name, add_inputs, rebuild_pyproject=True, bump="patch,a"
|
|
274
|
+
)
|
|
275
|
+
|
|
276
|
+
def bump_recipe(self, folder: Path, recipe_name: str, bump: str = "minor"):
|
|
277
|
+
recipe_path = folder / recipe_name
|
|
278
|
+
self._venv.hatch_version_bump(recipe_path, bump)
|
|
279
|
+
|
|
280
|
+
def build_recipe(
|
|
281
|
+
self,
|
|
282
|
+
folder: Path,
|
|
283
|
+
recipe_name: str,
|
|
284
|
+
dist_folder: Path,
|
|
285
|
+
):
|
|
286
|
+
|
|
287
|
+
recipe_path = folder / recipe_name
|
|
288
|
+
dist_path = dist_folder / recipe_name
|
|
289
|
+
|
|
290
|
+
self._venv.hatch_build(recipe_path, dist_path, allow_custom_classifiers=True)
|
|
291
|
+
|
|
292
|
+
def publish_recipe(
|
|
293
|
+
self,
|
|
294
|
+
folder: Path,
|
|
295
|
+
recipe_name: str,
|
|
296
|
+
dist_folder: Path,
|
|
297
|
+
index_url: str,
|
|
298
|
+
**options: str,
|
|
299
|
+
):
|
|
300
|
+
"""
|
|
301
|
+
Provided options like:
|
|
302
|
+
option_name='option_value', target='bob'
|
|
303
|
+
will be passed to the publisher plugin like:
|
|
304
|
+
-o option_name=option_value -o default_target=blessed
|
|
305
|
+
|
|
306
|
+
"""
|
|
307
|
+
recipe_path = folder / recipe_name
|
|
308
|
+
dist_path = dist_folder / recipe_name
|
|
309
|
+
|
|
310
|
+
self._venv.hatch_publish(recipe_path, dist_path, index_url, **options)
|
|
311
|
+
|
|
312
|
+
def run_recipe_method(
|
|
313
|
+
self, folder: Path, recipe_name: str, method_name: str, *args, **kwargs
|
|
314
|
+
) -> Any:
|
|
315
|
+
"""
|
|
316
|
+
WARNING: the method is not executed in the workbench venv but from wherever this is
|
|
317
|
+
called (might by a GUI app or an automation script outside of the workspace).
|
|
318
|
+
|
|
319
|
+
"""
|
|
320
|
+
recipe_path = folder / recipe_name
|
|
321
|
+
if not recipe_path.exists():
|
|
322
|
+
raise ValueError(
|
|
323
|
+
f"Cannot run method {method_name} from {recipe_name!r}: it was not found in {folder})."
|
|
324
|
+
)
|
|
325
|
+
toml_path = recipe_path / "src" / recipe_name / "recipe.toml"
|
|
326
|
+
recipe = self.get_recipe_from_toml(toml_path)
|
|
327
|
+
|
|
328
|
+
try:
|
|
329
|
+
method = getattr(recipe, method_name)
|
|
330
|
+
except Exception as err:
|
|
331
|
+
raise AttributeError(
|
|
332
|
+
f"Cannot get method {method_name} from {recipe_name!r}: {err}."
|
|
333
|
+
)
|
|
334
|
+
try:
|
|
335
|
+
return method(*args, **kwargs)
|
|
336
|
+
except Exception as err:
|
|
337
|
+
print(
|
|
338
|
+
f"Error runnig recipr method {method_name} on {recipe_name} (a {recipe.RECIPE_TYPE_INFO.type_name}): {err}"
|
|
339
|
+
)
|
|
@@ -0,0 +1,232 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
from typing import Literal
|
|
3
|
+
|
|
4
|
+
import os
|
|
5
|
+
from pathlib import Path
|
|
6
|
+
import click
|
|
7
|
+
import dataclasses
|
|
8
|
+
import json
|
|
9
|
+
import rich, rich.table
|
|
10
|
+
|
|
11
|
+
from .._version import __version__
|
|
12
|
+
from ..workbench import Workbench
|
|
13
|
+
from .utils import ShortNameGroup
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
@click.group(
|
|
17
|
+
cls=ShortNameGroup,
|
|
18
|
+
context_settings={"help_option_names": ["-h", "--help"]},
|
|
19
|
+
)
|
|
20
|
+
@click.version_option(version=__version__, prog_name="cuisine")
|
|
21
|
+
@click.option(
|
|
22
|
+
"-w",
|
|
23
|
+
"--workbench",
|
|
24
|
+
metavar="PATH",
|
|
25
|
+
default=".",
|
|
26
|
+
help="Path to the cuisine workbench.",
|
|
27
|
+
)
|
|
28
|
+
@click.option(
|
|
29
|
+
"-c",
|
|
30
|
+
"--create-workbench",
|
|
31
|
+
default=None,
|
|
32
|
+
metavar="PY-VERSION",
|
|
33
|
+
help="Create the specified workbench if it doesn't exists, use the python version provided by this flag",
|
|
34
|
+
)
|
|
35
|
+
@click.pass_context
|
|
36
|
+
def cuisine_cli(ctx, workbench: str, create_workbench: str):
|
|
37
|
+
workbench_path = os.path.abspath(os.path.normpath(workbench))
|
|
38
|
+
wb = Workbench(workbench_path)
|
|
39
|
+
if create_workbench:
|
|
40
|
+
wb.ensure_exists(python_version=create_workbench)
|
|
41
|
+
|
|
42
|
+
ctx.obj = wb
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
#
|
|
46
|
+
# WORKBENCH
|
|
47
|
+
#
|
|
48
|
+
|
|
49
|
+
|
|
50
|
+
# @cuisine_cli.group(cls=ShortNameGroup, help="Manage the cuisine workbenche.")
|
|
51
|
+
# def workbench():
|
|
52
|
+
# pass
|
|
53
|
+
|
|
54
|
+
|
|
55
|
+
#
|
|
56
|
+
# RECIPES
|
|
57
|
+
#
|
|
58
|
+
|
|
59
|
+
|
|
60
|
+
@cuisine_cli.group(cls=ShortNameGroup, help="Manage Recipes")
|
|
61
|
+
def recipe():
|
|
62
|
+
pass
|
|
63
|
+
|
|
64
|
+
|
|
65
|
+
@recipe.command
|
|
66
|
+
@click.argument("name")
|
|
67
|
+
@click.option(
|
|
68
|
+
"-T",
|
|
69
|
+
"--recipe-type",
|
|
70
|
+
default="Recipe",
|
|
71
|
+
help="The recipe type to create, like Product or Workscene (valid values depend on the installed asset plugins)",
|
|
72
|
+
)
|
|
73
|
+
@click.pass_obj
|
|
74
|
+
def create(wb: Workbench, recipe_type: str, name: str):
|
|
75
|
+
wb.create_recipe(recipe_type_name=recipe_type, recipe_name=name)
|
|
76
|
+
|
|
77
|
+
|
|
78
|
+
@recipe.command
|
|
79
|
+
@click.option(
|
|
80
|
+
"-e",
|
|
81
|
+
"--editable-only",
|
|
82
|
+
is_flag=True,
|
|
83
|
+
help="List only the editable recipes.",
|
|
84
|
+
)
|
|
85
|
+
@click.option(
|
|
86
|
+
"-l",
|
|
87
|
+
"--libs",
|
|
88
|
+
is_flag=True,
|
|
89
|
+
help="List library package (python) too.",
|
|
90
|
+
)
|
|
91
|
+
@click.option(
|
|
92
|
+
"-f",
|
|
93
|
+
"--format",
|
|
94
|
+
default="human",
|
|
95
|
+
help="Output format: 'json', 'rich', 'human'. Default to `human`.",
|
|
96
|
+
)
|
|
97
|
+
@click.pass_obj
|
|
98
|
+
def ls(
|
|
99
|
+
wb: Workbench,
|
|
100
|
+
editable_only: bool = False,
|
|
101
|
+
libs: bool = False,
|
|
102
|
+
format: Literal["json", "rich", "human"] = "human",
|
|
103
|
+
):
|
|
104
|
+
"""List installed Recipes (and libs, optionnaly)."""
|
|
105
|
+
matched = wb.get_dist_infos(editable_only=editable_only, include_non_recipe=libs)
|
|
106
|
+
|
|
107
|
+
if format == "human":
|
|
108
|
+
table = rich.table.Table("Name", "Is Recipe", "Is Editable")
|
|
109
|
+
for dist_info in matched:
|
|
110
|
+
style = dist_info.is_editable and "yellow" or ""
|
|
111
|
+
style += dist_info.is_recipe and " on cyan" or ""
|
|
112
|
+
|
|
113
|
+
table.add_row(
|
|
114
|
+
dist_info.dist.name,
|
|
115
|
+
dist_info.is_recipe and "✔" or "",
|
|
116
|
+
dist_info.is_editable and "✔" or "",
|
|
117
|
+
style=style,
|
|
118
|
+
)
|
|
119
|
+
rich.print(table)
|
|
120
|
+
|
|
121
|
+
if format == "rich":
|
|
122
|
+
rich.print(matched)
|
|
123
|
+
|
|
124
|
+
elif format == "json":
|
|
125
|
+
|
|
126
|
+
def drop_undumpable(d: dict, encoder: json.JSONEncoder):
|
|
127
|
+
to_drop = []
|
|
128
|
+
to_dig = []
|
|
129
|
+
for k, v in d.items():
|
|
130
|
+
if v is None:
|
|
131
|
+
continue
|
|
132
|
+
if isinstance(v, dict):
|
|
133
|
+
to_dig.append(k)
|
|
134
|
+
elif isinstance(v, Path):
|
|
135
|
+
d[k] = str(v)
|
|
136
|
+
elif not isinstance(v, (str, bool, list, tuple)):
|
|
137
|
+
to_drop.append(k)
|
|
138
|
+
for k in to_drop:
|
|
139
|
+
del d[k]
|
|
140
|
+
for k in to_dig:
|
|
141
|
+
drop_undumpable(d[k], encoder)
|
|
142
|
+
|
|
143
|
+
data = [dataclasses.asdict(dist_info) for dist_info in matched]
|
|
144
|
+
for d in data:
|
|
145
|
+
drop_undumpable(d, json.JSONEncoder())
|
|
146
|
+
click.echo(json.dumps(data))
|
|
147
|
+
|
|
148
|
+
else:
|
|
149
|
+
raise click.UsageError(
|
|
150
|
+
f"Unsupported value for -f/--format: {format!r} (must be 'json', 'rich' or 'human')."
|
|
151
|
+
)
|
|
152
|
+
|
|
153
|
+
|
|
154
|
+
@recipe.command
|
|
155
|
+
@click.argument("name")
|
|
156
|
+
@click.pass_obj
|
|
157
|
+
def pyproject_rebuild(wb: Workbench, name: str):
|
|
158
|
+
"""Rebuild the recipe pyproject file, bump the version micro and (re)install."""
|
|
159
|
+
wb.rebuid_pyproject(name)
|
|
160
|
+
|
|
161
|
+
|
|
162
|
+
@recipe.command
|
|
163
|
+
@click.argument("name")
|
|
164
|
+
@click.pass_obj
|
|
165
|
+
def dinit_rebuild(wb: Workbench, name: str):
|
|
166
|
+
"""Rebuild the recipe __init__.py file, bump the version micro and (re)install."""
|
|
167
|
+
wb.rebuid_dinit(name)
|
|
168
|
+
|
|
169
|
+
|
|
170
|
+
@recipe.command
|
|
171
|
+
@click.argument("name")
|
|
172
|
+
@click.argument("tags", nargs=-1)
|
|
173
|
+
@click.pass_obj
|
|
174
|
+
def tag(wb: Workbench, name: str, tags: list[str]):
|
|
175
|
+
"""Add tags to recipe."""
|
|
176
|
+
if not tags:
|
|
177
|
+
click.echo("No tags given, nothing to do.")
|
|
178
|
+
wb.tag_recipe(name, *tags)
|
|
179
|
+
|
|
180
|
+
|
|
181
|
+
@recipe.command
|
|
182
|
+
@click.argument("name")
|
|
183
|
+
@click.option(
|
|
184
|
+
"-g",
|
|
185
|
+
"--group",
|
|
186
|
+
default="",
|
|
187
|
+
help="If specified, add to this group (like dev, build, edit,...)",
|
|
188
|
+
)
|
|
189
|
+
@click.argument("requirements", nargs=-1)
|
|
190
|
+
@click.pass_obj
|
|
191
|
+
def add_input(wb: Workbench, name, group, requirements):
|
|
192
|
+
if not requirements:
|
|
193
|
+
click.echo("No requirements given, nothing to do.")
|
|
194
|
+
wb.add_inputs(name, group, *requirements)
|
|
195
|
+
|
|
196
|
+
|
|
197
|
+
@recipe.command(help=f"\b\n {Workbench.bump_recipe.__doc__}")
|
|
198
|
+
@click.argument("name")
|
|
199
|
+
@click.argument("bump", required=False, default="minor")
|
|
200
|
+
@click.pass_obj
|
|
201
|
+
def bump(wb: Workbench, name, bump):
|
|
202
|
+
wb.bump_recipe(name, bump)
|
|
203
|
+
|
|
204
|
+
|
|
205
|
+
@recipe.command
|
|
206
|
+
@click.argument("name")
|
|
207
|
+
@click.option(
|
|
208
|
+
"-b",
|
|
209
|
+
"--bump",
|
|
210
|
+
default="minor",
|
|
211
|
+
help=(
|
|
212
|
+
"The part of the version to bump, like "
|
|
213
|
+
"'major', 'minor', 'micro', 'alpha', 'rc' or a combination like 'minor,rc'. "
|
|
214
|
+
"Defaults to 'minor'."
|
|
215
|
+
),
|
|
216
|
+
)
|
|
217
|
+
@click.pass_obj
|
|
218
|
+
def build(wb: Workbench, name, bump):
|
|
219
|
+
wb.build_recipe(name, bump)
|
|
220
|
+
|
|
221
|
+
|
|
222
|
+
@recipe.command
|
|
223
|
+
@click.argument("name")
|
|
224
|
+
@click.option(
|
|
225
|
+
"-r",
|
|
226
|
+
"--repo",
|
|
227
|
+
required=True,
|
|
228
|
+
help="Name of repo to upload to.",
|
|
229
|
+
)
|
|
230
|
+
@click.pass_obj
|
|
231
|
+
def publish(wb: Workbench, name, repo: str):
|
|
232
|
+
wb.publish_recipe(name, repo)
|
tgzr/cuisine/cli/main.py
ADDED
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import click
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
class ShortNameGroup(click.Group):
|
|
7
|
+
"""
|
|
8
|
+
This Group recognizes commands with less
|
|
9
|
+
than their full name when there is no ambiguity.
|
|
10
|
+
For example: 'wo'->'workspace' if no other command
|
|
11
|
+
starts with 'wo'
|
|
12
|
+
"""
|
|
13
|
+
|
|
14
|
+
def __init__(self, *args, **kwargs) -> None:
|
|
15
|
+
super().__init__(*args, **kwargs)
|
|
16
|
+
self.no_args_is_help = True
|
|
17
|
+
self.invoke_without_command = False
|
|
18
|
+
|
|
19
|
+
def get_command(self, ctx: click.Context, cmd_name: str) -> click.Command | None:
|
|
20
|
+
known_commands = self.list_commands(ctx)
|
|
21
|
+
if cmd_name not in known_commands:
|
|
22
|
+
found = [name for name in known_commands if name.startswith(cmd_name)]
|
|
23
|
+
if len(found) > 1:
|
|
24
|
+
candidats = " or ".join(found)
|
|
25
|
+
raise click.UsageError(
|
|
26
|
+
f'Ambiuous command "{cmd_name}" (could be {candidats}).'
|
|
27
|
+
)
|
|
28
|
+
elif found:
|
|
29
|
+
cmd_name = found[0]
|
|
30
|
+
|
|
31
|
+
return super().get_command(ctx, cmd_name)
|