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.
@@ -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)