devstuff 1.13.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.
- dev_setup/__init__.py +6 -0
- dev_setup/__main__.py +9 -0
- dev_setup/base.py +74 -0
- dev_setup/catalog.py +188 -0
- dev_setup/cli.py +49 -0
- dev_setup/commands/__init__.py +0 -0
- dev_setup/commands/add_cmd.py +334 -0
- dev_setup/commands/catalog_cmd.py +37 -0
- dev_setup/commands/delete_cmd.py +36 -0
- dev_setup/commands/docs_cmd.py +34 -0
- dev_setup/commands/functions_cmd.py +87 -0
- dev_setup/commands/help_cmd.py +59 -0
- dev_setup/commands/install_cmd.py +129 -0
- dev_setup/commands/list_cmd.py +76 -0
- dev_setup/commands/remove_cmd.py +53 -0
- dev_setup/commands/run_cmd.py +66 -0
- dev_setup/commands/update_cmd.py +149 -0
- dev_setup/function_runner.py +133 -0
- dev_setup/functions.schema.json +106 -0
- dev_setup/functions.yaml +111 -0
- dev_setup/functions_catalog.py +217 -0
- dev_setup/functions_registry.py +149 -0
- dev_setup/generic.py +719 -0
- dev_setup/registry.py +97 -0
- dev_setup/tools.yaml +679 -0
- dev_setup/ui.py +111 -0
- devstuff-1.13.1.dist-info/METADATA +617 -0
- devstuff-1.13.1.dist-info/RECORD +30 -0
- devstuff-1.13.1.dist-info/WHEEL +4 -0
- devstuff-1.13.1.dist-info/entry_points.txt +3 -0
|
@@ -0,0 +1,217 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import copy
|
|
4
|
+
import re
|
|
5
|
+
from importlib import resources
|
|
6
|
+
from pathlib import Path
|
|
7
|
+
from typing import Any
|
|
8
|
+
|
|
9
|
+
import yaml
|
|
10
|
+
|
|
11
|
+
from dev_setup.catalog import CONFIG_DIR, CatalogError
|
|
12
|
+
|
|
13
|
+
USER_CATALOG_PATH = CONFIG_DIR / "functions.yaml"
|
|
14
|
+
BUNDLED_CATALOG = "functions.yaml"
|
|
15
|
+
|
|
16
|
+
VERSION = 1
|
|
17
|
+
VALID_KEY = re.compile(r"^[a-z0-9][a-z0-9_-]*$")
|
|
18
|
+
VALID_PARAM_NAME = re.compile(r"^[a-zA-Z_][a-zA-Z0-9_]*$")
|
|
19
|
+
|
|
20
|
+
TYPES = {"script", "shell-eval"}
|
|
21
|
+
REGISTER_MODES = {"bashrc", "eval"}
|
|
22
|
+
|
|
23
|
+
SUPPORTED_FIELDS = {
|
|
24
|
+
"name",
|
|
25
|
+
"description",
|
|
26
|
+
"category",
|
|
27
|
+
"type",
|
|
28
|
+
"register",
|
|
29
|
+
"params",
|
|
30
|
+
"script",
|
|
31
|
+
"help_cmd",
|
|
32
|
+
"docs_url",
|
|
33
|
+
}
|
|
34
|
+
SUPPORTED_PARAM_FIELDS = {"name", "description", "required", "default"}
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
def bundled_catalog_path() -> str:
|
|
38
|
+
return f"dev_setup/{BUNDLED_CATALOG}"
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
def load_catalog_file(path: Path, *, required: bool = False) -> dict[str, dict[str, Any]]:
|
|
42
|
+
if not path.exists():
|
|
43
|
+
if required:
|
|
44
|
+
raise CatalogError(f"Catalog file not found: {path}")
|
|
45
|
+
return {}
|
|
46
|
+
|
|
47
|
+
try:
|
|
48
|
+
raw = yaml.safe_load(path.read_text()) or {}
|
|
49
|
+
except yaml.YAMLError as exc:
|
|
50
|
+
raise CatalogError(f"Invalid YAML in {path}: {exc}") from exc
|
|
51
|
+
|
|
52
|
+
return validate_catalog(raw, source=path)
|
|
53
|
+
|
|
54
|
+
|
|
55
|
+
def validate_catalog(raw: Any, *, source: Path | str = "<catalog>") -> dict[str, dict[str, Any]]:
|
|
56
|
+
if not isinstance(raw, dict):
|
|
57
|
+
raise CatalogError(f"{source}: catalog must be a mapping")
|
|
58
|
+
if raw.get("version") != VERSION:
|
|
59
|
+
raise CatalogError(f"{source}: version must be {VERSION}")
|
|
60
|
+
|
|
61
|
+
functions = raw.get("functions")
|
|
62
|
+
if functions is None:
|
|
63
|
+
return {}
|
|
64
|
+
if not isinstance(functions, dict):
|
|
65
|
+
raise CatalogError(f"{source}: functions must be a mapping")
|
|
66
|
+
|
|
67
|
+
validated: dict[str, dict[str, Any]] = {}
|
|
68
|
+
for key, data in functions.items():
|
|
69
|
+
if not isinstance(key, str) or not VALID_KEY.match(key):
|
|
70
|
+
raise CatalogError(f"{source}: invalid function key {key!r}")
|
|
71
|
+
if not isinstance(data, dict):
|
|
72
|
+
raise CatalogError(f"{source}: function {key!r} must be a mapping")
|
|
73
|
+
|
|
74
|
+
unknown = sorted(set(data) - SUPPORTED_FIELDS)
|
|
75
|
+
if unknown:
|
|
76
|
+
fields = ", ".join(unknown)
|
|
77
|
+
raise CatalogError(f"{source}: function {key!r} has unknown field(s): {fields}")
|
|
78
|
+
|
|
79
|
+
item = copy.deepcopy(data)
|
|
80
|
+
item.setdefault("name", key)
|
|
81
|
+
item.setdefault("description", "")
|
|
82
|
+
item.setdefault("category", "custom")
|
|
83
|
+
|
|
84
|
+
fn_type = item.get("type")
|
|
85
|
+
if fn_type not in TYPES:
|
|
86
|
+
raise CatalogError(
|
|
87
|
+
f"{source}: function {key!r} type must be one of {sorted(TYPES)}, got {fn_type!r}"
|
|
88
|
+
)
|
|
89
|
+
|
|
90
|
+
if not item.get("script"):
|
|
91
|
+
raise CatalogError(f"{source}: function {key!r} must set 'script'")
|
|
92
|
+
|
|
93
|
+
register = item.get("register")
|
|
94
|
+
if fn_type == "shell-eval":
|
|
95
|
+
if register is None:
|
|
96
|
+
item["register"] = "bashrc"
|
|
97
|
+
elif register not in REGISTER_MODES:
|
|
98
|
+
raise CatalogError(
|
|
99
|
+
f"{source}: function {key!r} register must be one of "
|
|
100
|
+
f"{sorted(REGISTER_MODES)}, got {register!r}"
|
|
101
|
+
)
|
|
102
|
+
elif register is not None:
|
|
103
|
+
raise CatalogError(
|
|
104
|
+
f"{source}: function {key!r} sets 'register' but type is {fn_type!r} "
|
|
105
|
+
"('register' only applies to type 'shell-eval')"
|
|
106
|
+
)
|
|
107
|
+
|
|
108
|
+
item["params"] = _validate_params(item.get("params"), key=key, source=source)
|
|
109
|
+
|
|
110
|
+
validated[key] = item
|
|
111
|
+
|
|
112
|
+
return validated
|
|
113
|
+
|
|
114
|
+
|
|
115
|
+
def _validate_params(params: Any, *, key: str, source: Path | str) -> list[dict[str, Any]]:
|
|
116
|
+
if params is None:
|
|
117
|
+
return []
|
|
118
|
+
if not isinstance(params, list):
|
|
119
|
+
raise CatalogError(f"{source}: function {key!r} params must be a list")
|
|
120
|
+
|
|
121
|
+
validated: list[dict[str, Any]] = []
|
|
122
|
+
seen: set[str] = set()
|
|
123
|
+
for i, param in enumerate(params):
|
|
124
|
+
if not isinstance(param, dict):
|
|
125
|
+
raise CatalogError(f"{source}: function {key!r} params[{i}] must be a mapping")
|
|
126
|
+
|
|
127
|
+
unknown = sorted(set(param) - SUPPORTED_PARAM_FIELDS)
|
|
128
|
+
if unknown:
|
|
129
|
+
fields = ", ".join(unknown)
|
|
130
|
+
raise CatalogError(
|
|
131
|
+
f"{source}: function {key!r} params[{i}] has unknown field(s): {fields}"
|
|
132
|
+
)
|
|
133
|
+
|
|
134
|
+
name = param.get("name")
|
|
135
|
+
if not isinstance(name, str) or not VALID_PARAM_NAME.match(name):
|
|
136
|
+
raise CatalogError(
|
|
137
|
+
f"{source}: function {key!r} params[{i}] name must be a valid shell "
|
|
138
|
+
f"identifier, got {name!r}"
|
|
139
|
+
)
|
|
140
|
+
if name in seen:
|
|
141
|
+
raise CatalogError(f"{source}: function {key!r} has duplicate param name {name!r}")
|
|
142
|
+
seen.add(name)
|
|
143
|
+
|
|
144
|
+
item = copy.deepcopy(param)
|
|
145
|
+
item.setdefault("description", "")
|
|
146
|
+
item.setdefault("required", True)
|
|
147
|
+
item.setdefault("default", "")
|
|
148
|
+
if not isinstance(item["required"], bool):
|
|
149
|
+
raise CatalogError(f"{source}: function {key!r} params[{i}] required must be a bool")
|
|
150
|
+
validated.append(item)
|
|
151
|
+
|
|
152
|
+
return validated
|
|
153
|
+
|
|
154
|
+
|
|
155
|
+
def catalog_document(functions: dict[str, dict[str, Any]]) -> dict[str, Any]:
|
|
156
|
+
return {"version": VERSION, "functions": functions}
|
|
157
|
+
|
|
158
|
+
|
|
159
|
+
def read_user_catalog() -> dict[str, dict[str, Any]]:
|
|
160
|
+
return load_catalog_file(USER_CATALOG_PATH)
|
|
161
|
+
|
|
162
|
+
|
|
163
|
+
def write_user_catalog(functions: dict[str, dict[str, Any]]) -> None:
|
|
164
|
+
CONFIG_DIR.mkdir(parents=True, exist_ok=True)
|
|
165
|
+
USER_CATALOG_PATH.write_text(_dump(catalog_document(functions)))
|
|
166
|
+
|
|
167
|
+
|
|
168
|
+
def load_bundled_catalog() -> dict[str, dict[str, Any]]:
|
|
169
|
+
resource = resources.files("dev_setup").joinpath(BUNDLED_CATALOG)
|
|
170
|
+
try:
|
|
171
|
+
raw = yaml.safe_load(resource.read_text()) or {}
|
|
172
|
+
except FileNotFoundError as exc:
|
|
173
|
+
raise CatalogError(f"Catalog file not found: {bundled_catalog_path()}") from exc
|
|
174
|
+
except yaml.YAMLError as exc:
|
|
175
|
+
raise CatalogError(f"Invalid YAML in {bundled_catalog_path()}: {exc}") from exc
|
|
176
|
+
return validate_catalog(raw, source=bundled_catalog_path())
|
|
177
|
+
|
|
178
|
+
|
|
179
|
+
def load_effective_catalog() -> tuple[
|
|
180
|
+
dict[str, dict[str, Any]],
|
|
181
|
+
dict[str, dict[str, Any]],
|
|
182
|
+
dict[str, dict[str, Any]],
|
|
183
|
+
]:
|
|
184
|
+
bundled = load_bundled_catalog()
|
|
185
|
+
user = read_user_catalog()
|
|
186
|
+
effective = merge_catalogs(bundled, user)
|
|
187
|
+
return effective, bundled, user
|
|
188
|
+
|
|
189
|
+
|
|
190
|
+
def merge_catalogs(
|
|
191
|
+
bundled: dict[str, dict[str, Any]],
|
|
192
|
+
user: dict[str, dict[str, Any]],
|
|
193
|
+
) -> dict[str, dict[str, Any]]:
|
|
194
|
+
merged = copy.deepcopy(bundled)
|
|
195
|
+
for key, data in user.items():
|
|
196
|
+
merged[key] = copy.deepcopy(data)
|
|
197
|
+
return merged
|
|
198
|
+
|
|
199
|
+
|
|
200
|
+
def save_user_function(key: str, data: dict[str, Any]) -> None:
|
|
201
|
+
validate_catalog(catalog_document({key: data}), source=USER_CATALOG_PATH)
|
|
202
|
+
user = read_user_catalog()
|
|
203
|
+
user[key] = copy.deepcopy(data)
|
|
204
|
+
write_user_catalog(user)
|
|
205
|
+
|
|
206
|
+
|
|
207
|
+
def delete_user_function(key: str) -> bool:
|
|
208
|
+
user = read_user_catalog()
|
|
209
|
+
if key not in user:
|
|
210
|
+
return False
|
|
211
|
+
del user[key]
|
|
212
|
+
write_user_catalog(user)
|
|
213
|
+
return True
|
|
214
|
+
|
|
215
|
+
|
|
216
|
+
def _dump(data: dict[str, Any]) -> str:
|
|
217
|
+
return yaml.safe_dump(data, sort_keys=False, default_flow_style=False)
|
|
@@ -0,0 +1,149 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from dataclasses import dataclass, field, fields
|
|
4
|
+
|
|
5
|
+
from dev_setup import functions_catalog as catalog
|
|
6
|
+
|
|
7
|
+
# field name -> catalog YAML key (only where they differ)
|
|
8
|
+
_YAML_KEY: dict[str, str] = {}
|
|
9
|
+
_NON_CATALOG = ("key", "builtin")
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
@dataclass
|
|
13
|
+
class FunctionParam:
|
|
14
|
+
name: str = ""
|
|
15
|
+
description: str = ""
|
|
16
|
+
required: bool = True
|
|
17
|
+
default: str = ""
|
|
18
|
+
|
|
19
|
+
@classmethod
|
|
20
|
+
def from_dict(cls, data: dict) -> FunctionParam:
|
|
21
|
+
return cls(
|
|
22
|
+
name=data.get("name", ""),
|
|
23
|
+
description=data.get("description", ""),
|
|
24
|
+
required=data.get("required", True),
|
|
25
|
+
default=data.get("default", ""),
|
|
26
|
+
)
|
|
27
|
+
|
|
28
|
+
def to_dict(self) -> dict:
|
|
29
|
+
return {
|
|
30
|
+
"name": self.name,
|
|
31
|
+
"description": self.description,
|
|
32
|
+
"required": self.required,
|
|
33
|
+
"default": self.default,
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
@dataclass
|
|
38
|
+
class FunctionDef:
|
|
39
|
+
key: str = ""
|
|
40
|
+
name: str = ""
|
|
41
|
+
description: str = ""
|
|
42
|
+
category: str = "custom"
|
|
43
|
+
type: str = "script"
|
|
44
|
+
register: str = ""
|
|
45
|
+
params: list[FunctionParam] = field(default_factory=list)
|
|
46
|
+
script: str = ""
|
|
47
|
+
help_cmd: str = ""
|
|
48
|
+
docs_url: str = ""
|
|
49
|
+
builtin: bool = False
|
|
50
|
+
|
|
51
|
+
def __post_init__(self) -> None:
|
|
52
|
+
if not self.name:
|
|
53
|
+
self.name = self.key
|
|
54
|
+
|
|
55
|
+
@classmethod
|
|
56
|
+
def from_dict(cls, data: dict, key: str) -> FunctionDef:
|
|
57
|
+
kwargs = {
|
|
58
|
+
f.name: data.get(_YAML_KEY.get(f.name, f.name), f.default)
|
|
59
|
+
for f in fields(cls)
|
|
60
|
+
if f.name not in _NON_CATALOG and f.name != "params"
|
|
61
|
+
}
|
|
62
|
+
kwargs["params"] = [FunctionParam.from_dict(p) for p in data.get("params", [])]
|
|
63
|
+
return cls(key=key, **kwargs)
|
|
64
|
+
|
|
65
|
+
def to_dict(self) -> dict:
|
|
66
|
+
d: dict = {
|
|
67
|
+
"name": self.name,
|
|
68
|
+
"description": self.description,
|
|
69
|
+
"category": self.category,
|
|
70
|
+
"type": self.type,
|
|
71
|
+
}
|
|
72
|
+
if self.register:
|
|
73
|
+
d["register"] = self.register
|
|
74
|
+
if self.params:
|
|
75
|
+
d["params"] = [p.to_dict() for p in self.params]
|
|
76
|
+
for f in ("script", "help_cmd", "docs_url"):
|
|
77
|
+
val = getattr(self, f)
|
|
78
|
+
if val:
|
|
79
|
+
d[f] = val
|
|
80
|
+
return d
|
|
81
|
+
|
|
82
|
+
def save(self) -> None:
|
|
83
|
+
catalog.save_user_function(self.key, self.to_dict())
|
|
84
|
+
|
|
85
|
+
|
|
86
|
+
_registry: dict[str, FunctionDef] = {}
|
|
87
|
+
_order: list[str] = []
|
|
88
|
+
_initialized = False
|
|
89
|
+
|
|
90
|
+
|
|
91
|
+
def _register(fn: FunctionDef) -> None:
|
|
92
|
+
if fn.key not in _registry:
|
|
93
|
+
_registry[fn.key] = fn
|
|
94
|
+
_order.append(fn.key)
|
|
95
|
+
else:
|
|
96
|
+
_registry[fn.key] = fn
|
|
97
|
+
|
|
98
|
+
|
|
99
|
+
def _load_builtins() -> None:
|
|
100
|
+
effective, bundled, user = catalog.load_effective_catalog()
|
|
101
|
+
for key, data in effective.items():
|
|
102
|
+
fn = FunctionDef.from_dict(data, key=key)
|
|
103
|
+
fn.builtin = key in bundled and key not in user
|
|
104
|
+
_register(fn)
|
|
105
|
+
|
|
106
|
+
|
|
107
|
+
def init() -> None:
|
|
108
|
+
global _initialized
|
|
109
|
+
if _initialized:
|
|
110
|
+
return
|
|
111
|
+
_initialized = True
|
|
112
|
+
_load_builtins()
|
|
113
|
+
|
|
114
|
+
|
|
115
|
+
def reload() -> None:
|
|
116
|
+
global _initialized
|
|
117
|
+
_registry.clear()
|
|
118
|
+
_order.clear()
|
|
119
|
+
_initialized = False
|
|
120
|
+
init()
|
|
121
|
+
|
|
122
|
+
|
|
123
|
+
def get(key: str) -> FunctionDef | None:
|
|
124
|
+
init()
|
|
125
|
+
return _registry.get(key)
|
|
126
|
+
|
|
127
|
+
|
|
128
|
+
def all_functions() -> list[FunctionDef]:
|
|
129
|
+
init()
|
|
130
|
+
return [_registry[k] for k in _order if k in _registry]
|
|
131
|
+
|
|
132
|
+
|
|
133
|
+
def exists(key: str) -> bool:
|
|
134
|
+
init()
|
|
135
|
+
return key in _registry
|
|
136
|
+
|
|
137
|
+
|
|
138
|
+
def register(fn: FunctionDef) -> None:
|
|
139
|
+
"""Register (or replace) a function in the live registry."""
|
|
140
|
+
init()
|
|
141
|
+
_register(fn)
|
|
142
|
+
|
|
143
|
+
|
|
144
|
+
def deregister(key: str) -> None:
|
|
145
|
+
"""Remove a function from the live registry by key."""
|
|
146
|
+
init()
|
|
147
|
+
_registry.pop(key, None)
|
|
148
|
+
if key in _order:
|
|
149
|
+
_order.remove(key)
|