clidevkit 0.1.0__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.
- clidev/__init__.py +67 -0
- clidev/actions.py +73 -0
- clidev/app.py +210 -0
- clidev/builtins/__init__.py +1 -0
- clidev/builtins/plugins.py +26 -0
- clidev/cards.py +19 -0
- clidev/cli.py +42 -0
- clidev/colors.py +27 -0
- clidev/config.py +36 -0
- clidev/dashboard.py +28 -0
- clidev/dialogs.py +24 -0
- clidev/events.py +39 -0
- clidev/exceptions.py +35 -0
- clidev/forms.py +97 -0
- clidev/generators/__init__.py +13 -0
- clidev/generators/form.py +11 -0
- clidev/generators/menu.py +11 -0
- clidev/generators/project.py +150 -0
- clidev/generators/workflow.py +17 -0
- clidev/icons.py +16 -0
- clidev/inputs.py +90 -0
- clidev/logger.py +50 -0
- clidev/menus.py +69 -0
- clidev/notifications.py +26 -0
- clidev/pages.py +42 -0
- clidev/plugins.py +51 -0
- clidev/progress.py +42 -0
- clidev/router.py +37 -0
- clidev/scheduler.py +59 -0
- clidev/shell.py +84 -0
- clidev/spinner.py +25 -0
- clidev/state.py +47 -0
- clidev/statusbar.py +20 -0
- clidev/storage.py +221 -0
- clidev/table.py +39 -0
- clidev/tasks.py +28 -0
- clidev/themes.py +96 -0
- clidev/tree.py +36 -0
- clidev/utils.py +20 -0
- clidev/validators.py +90 -0
- clidev/workflow.py +52 -0
- clidevkit-0.1.0.dist-info/METADATA +449 -0
- clidevkit-0.1.0.dist-info/RECORD +46 -0
- clidevkit-0.1.0.dist-info/WHEEL +5 -0
- clidevkit-0.1.0.dist-info/entry_points.txt +2 -0
- clidevkit-0.1.0.dist-info/top_level.txt +1 -0
clidev/themes.py
ADDED
|
@@ -0,0 +1,96 @@
|
|
|
1
|
+
"""Theming support for clidev applications."""
|
|
2
|
+
|
|
3
|
+
from .colors import resolve
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
class Theme:
|
|
7
|
+
"""A mutable theme object.
|
|
8
|
+
|
|
9
|
+
Usage:
|
|
10
|
+
theme = Theme()
|
|
11
|
+
theme.primary("blue")
|
|
12
|
+
theme.success("green")
|
|
13
|
+
|
|
14
|
+
Or start from a built-in preset:
|
|
15
|
+
theme = Theme.preset("dark")
|
|
16
|
+
"""
|
|
17
|
+
|
|
18
|
+
_DEFAULTS = {
|
|
19
|
+
"primary": "cyan",
|
|
20
|
+
"secondary": "blue",
|
|
21
|
+
"success": "green",
|
|
22
|
+
"warning": "yellow",
|
|
23
|
+
"error": "red",
|
|
24
|
+
"info": "cyan",
|
|
25
|
+
"text": "white",
|
|
26
|
+
"muted": "gray",
|
|
27
|
+
"background": "black",
|
|
28
|
+
"accent": "magenta",
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
def __init__(self):
|
|
32
|
+
self._colors = dict(self._DEFAULTS)
|
|
33
|
+
|
|
34
|
+
# --- fluent setters -------------------------------------------------
|
|
35
|
+
def _set(self, key, color):
|
|
36
|
+
self._colors[key] = resolve(color)
|
|
37
|
+
return self
|
|
38
|
+
|
|
39
|
+
def primary(self, color):
|
|
40
|
+
return self._set("primary", color)
|
|
41
|
+
|
|
42
|
+
def secondary(self, color):
|
|
43
|
+
return self._set("secondary", color)
|
|
44
|
+
|
|
45
|
+
def success(self, color):
|
|
46
|
+
return self._set("success", color)
|
|
47
|
+
|
|
48
|
+
def warning(self, color):
|
|
49
|
+
return self._set("warning", color)
|
|
50
|
+
|
|
51
|
+
def error(self, color):
|
|
52
|
+
return self._set("error", color)
|
|
53
|
+
|
|
54
|
+
def info(self, color):
|
|
55
|
+
return self._set("info", color)
|
|
56
|
+
|
|
57
|
+
def text(self, color):
|
|
58
|
+
return self._set("text", color)
|
|
59
|
+
|
|
60
|
+
def muted(self, color):
|
|
61
|
+
return self._set("muted", color)
|
|
62
|
+
|
|
63
|
+
def background(self, color):
|
|
64
|
+
return self._set("background", color)
|
|
65
|
+
|
|
66
|
+
def accent(self, color):
|
|
67
|
+
return self._set("accent", color)
|
|
68
|
+
|
|
69
|
+
# --- access -----------------------------------------------------------
|
|
70
|
+
def get(self, key, fallback=None):
|
|
71
|
+
return self._colors.get(key, fallback)
|
|
72
|
+
|
|
73
|
+
def __getitem__(self, key):
|
|
74
|
+
return self._colors[key]
|
|
75
|
+
|
|
76
|
+
def as_dict(self):
|
|
77
|
+
return dict(self._colors)
|
|
78
|
+
|
|
79
|
+
@classmethod
|
|
80
|
+
def preset(cls, name: str) -> "Theme":
|
|
81
|
+
theme = cls()
|
|
82
|
+
if name == "dark":
|
|
83
|
+
theme.primary("cyan").secondary("blue").success("green")
|
|
84
|
+
theme.warning("yellow").error("red").text("white")
|
|
85
|
+
theme.muted("gray").background("black").accent("magenta")
|
|
86
|
+
elif name == "light":
|
|
87
|
+
theme.primary("blue").secondary("purple").success("green")
|
|
88
|
+
theme.warning("orange").error("red").text("black")
|
|
89
|
+
theme.muted("gray").background("white").accent("magenta")
|
|
90
|
+
else:
|
|
91
|
+
# Unknown theme name -> defaults
|
|
92
|
+
pass
|
|
93
|
+
return theme
|
|
94
|
+
|
|
95
|
+
def __repr__(self):
|
|
96
|
+
return f"Theme({self._colors})"
|
clidev/tree.py
ADDED
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
"""Tree rendering widget, wraps rich.tree.Tree."""
|
|
2
|
+
|
|
3
|
+
from rich.console import Console
|
|
4
|
+
from rich.tree import Tree as RichTree
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
class TreeNode:
|
|
8
|
+
def __init__(self, label: str):
|
|
9
|
+
self.label = label
|
|
10
|
+
self.children: list["TreeNode"] = []
|
|
11
|
+
|
|
12
|
+
def add(self, label: str) -> "TreeNode":
|
|
13
|
+
node = TreeNode(label)
|
|
14
|
+
self.children.append(node)
|
|
15
|
+
return node
|
|
16
|
+
|
|
17
|
+
def _build(self, rich_node):
|
|
18
|
+
for child in self.children:
|
|
19
|
+
branch = rich_node.add(child.label)
|
|
20
|
+
child._build(branch)
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
class Tree:
|
|
24
|
+
"""app.tree("Project"); root.add("src").add("main.py")"""
|
|
25
|
+
|
|
26
|
+
def __init__(self, label: str):
|
|
27
|
+
self.root = TreeNode(label)
|
|
28
|
+
self._console = Console()
|
|
29
|
+
|
|
30
|
+
def add(self, label: str) -> TreeNode:
|
|
31
|
+
return self.root.add(label)
|
|
32
|
+
|
|
33
|
+
def show(self):
|
|
34
|
+
rich_tree = RichTree(self.root.label)
|
|
35
|
+
self.root._build(rich_tree)
|
|
36
|
+
self._console.print(rich_tree)
|
clidev/utils.py
ADDED
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
"""Small shared utility helpers."""
|
|
2
|
+
|
|
3
|
+
import re
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
def slugify(text: str) -> str:
|
|
7
|
+
text = text.strip().lower()
|
|
8
|
+
text = re.sub(r"[^a-z0-9]+", "_", text)
|
|
9
|
+
return text.strip("_")
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
def truncate(text: str, length: int = 40) -> str:
|
|
13
|
+
return text if len(text) <= length else text[: length - 1] + "\u2026"
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
def merge_dicts(*dicts: dict) -> dict:
|
|
17
|
+
result = {}
|
|
18
|
+
for d in dicts:
|
|
19
|
+
result.update(d)
|
|
20
|
+
return result
|
clidev/validators.py
ADDED
|
@@ -0,0 +1,90 @@
|
|
|
1
|
+
"""Built-in field validators used by forms.py / inputs.py."""
|
|
2
|
+
|
|
3
|
+
import re
|
|
4
|
+
from datetime import datetime
|
|
5
|
+
|
|
6
|
+
from .exceptions import ValidationError
|
|
7
|
+
|
|
8
|
+
EMAIL_RE = re.compile(r"^[^@\s]+@[^@\s]+\.[^@\s]+$")
|
|
9
|
+
URL_RE = re.compile(r"^(https?|ftp)://[^\s/$.?#].[^\s]*$", re.IGNORECASE)
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
def required(value):
|
|
13
|
+
if value is None or (isinstance(value, str) and value.strip() == ""):
|
|
14
|
+
raise ValidationError("This field is required.")
|
|
15
|
+
return value
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
def is_email(value):
|
|
19
|
+
if value and not EMAIL_RE.match(value):
|
|
20
|
+
raise ValidationError("Enter a valid email address.")
|
|
21
|
+
return value
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
def is_url(value):
|
|
25
|
+
if value and not URL_RE.match(value):
|
|
26
|
+
raise ValidationError("Enter a valid URL.")
|
|
27
|
+
return value
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
def is_number(value):
|
|
31
|
+
try:
|
|
32
|
+
if isinstance(value, (int, float)):
|
|
33
|
+
return value
|
|
34
|
+
return float(value) if "." in str(value) else int(value)
|
|
35
|
+
except (ValueError, TypeError) as e:
|
|
36
|
+
raise ValidationError("Enter a valid number.") from e
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
def min_length(n):
|
|
40
|
+
def _validator(value):
|
|
41
|
+
if value is not None and len(str(value)) < n:
|
|
42
|
+
raise ValidationError(f"Must be at least {n} characters.")
|
|
43
|
+
return value
|
|
44
|
+
|
|
45
|
+
return _validator
|
|
46
|
+
|
|
47
|
+
|
|
48
|
+
def max_length(n):
|
|
49
|
+
def _validator(value):
|
|
50
|
+
if value is not None and len(str(value)) > n:
|
|
51
|
+
raise ValidationError(f"Must be at most {n} characters.")
|
|
52
|
+
return value
|
|
53
|
+
|
|
54
|
+
return _validator
|
|
55
|
+
|
|
56
|
+
|
|
57
|
+
def min_value(n):
|
|
58
|
+
def _validator(value):
|
|
59
|
+
if value is not None and float(value) < n:
|
|
60
|
+
raise ValidationError(f"Must be at least {n}.")
|
|
61
|
+
return value
|
|
62
|
+
|
|
63
|
+
return _validator
|
|
64
|
+
|
|
65
|
+
|
|
66
|
+
def max_value(n):
|
|
67
|
+
def _validator(value):
|
|
68
|
+
if value is not None and float(value) > n:
|
|
69
|
+
raise ValidationError(f"Must be at most {n}.")
|
|
70
|
+
return value
|
|
71
|
+
|
|
72
|
+
return _validator
|
|
73
|
+
|
|
74
|
+
|
|
75
|
+
def is_date(fmt="%Y-%m-%d"):
|
|
76
|
+
def _validator(value):
|
|
77
|
+
try:
|
|
78
|
+
datetime.strptime(str(value), fmt)
|
|
79
|
+
except ValueError as e:
|
|
80
|
+
raise ValidationError(f"Enter a valid date ({fmt}).") from e
|
|
81
|
+
return value
|
|
82
|
+
|
|
83
|
+
return _validator
|
|
84
|
+
|
|
85
|
+
|
|
86
|
+
def run_validators(value, validators):
|
|
87
|
+
"""Run a list of validator callables against value, raising on first failure."""
|
|
88
|
+
for validator in validators:
|
|
89
|
+
value = validator(value)
|
|
90
|
+
return value
|
clidev/workflow.py
ADDED
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
"""Sequential step-based workflow engine."""
|
|
2
|
+
|
|
3
|
+
from .exceptions import WorkflowError
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
class Workflow:
|
|
7
|
+
"""A sequence of steps run in order, sharing a context dict between them.
|
|
8
|
+
|
|
9
|
+
workflow = app.workflow()
|
|
10
|
+
workflow.step(login)
|
|
11
|
+
workflow.step(select_project)
|
|
12
|
+
workflow.step(build)
|
|
13
|
+
workflow.step(deploy)
|
|
14
|
+
workflow.start()
|
|
15
|
+
|
|
16
|
+
Each step function may optionally accept a single `context` dict argument;
|
|
17
|
+
its return value (if a dict) is merged into the shared context for
|
|
18
|
+
subsequent steps.
|
|
19
|
+
"""
|
|
20
|
+
|
|
21
|
+
def __init__(self, app=None, name: str = "workflow"):
|
|
22
|
+
self.app = app
|
|
23
|
+
self.name = name
|
|
24
|
+
self.steps: list = []
|
|
25
|
+
self.context: dict = {}
|
|
26
|
+
|
|
27
|
+
def step(self, fn) -> "Workflow":
|
|
28
|
+
self.steps.append(fn)
|
|
29
|
+
return self
|
|
30
|
+
|
|
31
|
+
def start(self, initial_context: dict | None = None):
|
|
32
|
+
self.context = dict(initial_context or {})
|
|
33
|
+
for i, fn in enumerate(self.steps):
|
|
34
|
+
try:
|
|
35
|
+
result = self._call_step(fn)
|
|
36
|
+
except Exception as e:
|
|
37
|
+
if self.app is not None:
|
|
38
|
+
self.app.events.emit("error", e)
|
|
39
|
+
raise WorkflowError(
|
|
40
|
+
f"Workflow '{self.name}' failed at step {i} ({getattr(fn, '__name__', fn)}): {e}"
|
|
41
|
+
) from e
|
|
42
|
+
if isinstance(result, dict):
|
|
43
|
+
self.context.update(result)
|
|
44
|
+
return self.context
|
|
45
|
+
|
|
46
|
+
def _call_step(self, fn):
|
|
47
|
+
import inspect
|
|
48
|
+
|
|
49
|
+
sig = inspect.signature(fn)
|
|
50
|
+
if len(sig.parameters) >= 1:
|
|
51
|
+
return fn(self.context)
|
|
52
|
+
return fn()
|