zrb 1.0.0a10__py3-none-any.whl → 1.0.0a14__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.
- zrb/builtin/__init__.py +15 -5
- zrb/builtin/project/add/fastapp.py +32 -17
- zrb/builtin/project/add/fastapp_template/_zrb/column/create_column_task.py +11 -0
- zrb/builtin/project/add/fastapp_template/_zrb/config.py +4 -4
- zrb/builtin/project/add/fastapp_template/_zrb/entity/create_entity_task.py +196 -0
- zrb/builtin/project/add/fastapp_template/_zrb/entity/module_template/service/my_entity/my_entity_usecase.py +66 -0
- zrb/builtin/project/add/fastapp_template/_zrb/entity/module_template/service/my_entity/repository/factory.py +13 -0
- zrb/builtin/project/add/fastapp_template/_zrb/entity/module_template/service/my_entity/repository/my_entity_db_repository.py +33 -0
- zrb/builtin/project/add/fastapp_template/_zrb/entity/module_template/service/my_entity/repository/my_entity_repository.py +39 -0
- zrb/builtin/project/add/fastapp_template/_zrb/entity/schema.template.py +29 -0
- zrb/builtin/project/add/fastapp_template/_zrb/group.py +9 -5
- zrb/builtin/project/add/fastapp_template/_zrb/helper.py +25 -11
- zrb/builtin/project/add/fastapp_template/_zrb/input.py +43 -0
- zrb/builtin/project/add/fastapp_template/_zrb/main.py +30 -21
- zrb/builtin/project/add/fastapp_template/_zrb/module/create_module_task.py +136 -0
- zrb/builtin/project/add/fastapp_template/_zrb/module/module_template/alembic.ini +117 -0
- zrb/builtin/project/add/fastapp_template/_zrb/module/module_template/client/api_client.py +6 -0
- zrb/builtin/project/add/fastapp_template/_zrb/module/module_template/client/direct_client.py +6 -0
- zrb/builtin/project/add/fastapp_template/_zrb/module/module_template/client/factory.py +9 -0
- zrb/builtin/project/add/fastapp_template/_zrb/module/module_template/migration/README +1 -0
- zrb/builtin/project/add/fastapp_template/_zrb/module/module_template/migration/env.py +108 -0
- zrb/builtin/project/add/fastapp_template/_zrb/module/module_template/migration/script.py.mako +26 -0
- zrb/builtin/project/add/fastapp_template/_zrb/module/module_template/migration/versions/3093c7336477_add_user_table.py +37 -0
- zrb/builtin/project/add/fastapp_template/_zrb/module/module_template/migration_metadata.py +3 -0
- zrb/builtin/project/add/fastapp_template/_zrb/module/module_template/route.py +19 -0
- zrb/builtin/project/add/fastapp_template/_zrb/module/run_module.template.py +26 -0
- zrb/builtin/project/add/fastapp_template/_zrb/venv_task.py +2 -5
- zrb/builtin/project/add/fastapp_template/common/app.py +3 -1
- zrb/builtin/project/add/fastapp_template/config.py +7 -7
- zrb/builtin/project/add/fastapp_template/module/auth/client/any_client.py +27 -0
- zrb/builtin/project/add/fastapp_template/module/auth/client/api_client.py +2 -2
- zrb/builtin/project/add/fastapp_template/module/auth/client/direct_client.py +2 -2
- zrb/builtin/project/add/fastapp_template/module/auth/client/factory.py +1 -1
- zrb/builtin/project/add/fastapp_template/module/auth/route.py +1 -1
- zrb/builtin/project/add/fastapp_template/module/auth/service/user/repository/factory.py +2 -2
- zrb/builtin/project/add/fastapp_template/module/auth/service/user/repository/{db_repository.py → user_db_repository.py} +2 -2
- zrb/builtin/project/add/fastapp_template/module/auth/service/user/{usecase.py → user_usecase.py} +15 -8
- zrb/builtin/project/add/fastapp_template/requirements.txt +5 -6
- zrb/builtin/project/add/fastapp_template/schema/permission.py +31 -0
- zrb/builtin/project/add/fastapp_template/template.env +2 -2
- zrb/builtin/project/create/create.py +2 -2
- zrb/builtin/project/create/project-template/zrb_init.py +0 -4
- zrb/builtin/setup/{dev → asdf}/asdf.py +6 -6
- zrb/builtin/setup/{system/latex → latex}/ubuntu.py +1 -1
- zrb/builtin/setup/{dev → tmux}/tmux.py +1 -1
- zrb/builtin/setup/tmux/tmux_config.sh +12 -0
- zrb/builtin/todo.py +101 -15
- zrb/config.py +2 -2
- zrb/content_transformer/content_transformer.py +14 -2
- zrb/context/context.py +13 -10
- zrb/input/base_input.py +3 -2
- zrb/input/bool_input.py +1 -1
- zrb/input/float_input.py +1 -1
- zrb/input/int_input.py +1 -1
- zrb/input/option_input.py +1 -1
- zrb/input/password_input.py +1 -1
- zrb/input/text_input.py +1 -1
- zrb/runner/cli.py +16 -5
- zrb/runner/web_app.py +30 -18
- zrb/runner/web_controller/task_ui/controller.py +8 -6
- zrb/session/session.py +4 -1
- zrb/task/scaffolder.py +7 -9
- zrb/util/cli/style.py +7 -0
- zrb/util/codemod/add_code_to_module.py +12 -0
- zrb/util/load.py +9 -7
- zrb/util/string/conversion.py +52 -0
- zrb/util/todo.py +152 -34
- {zrb-1.0.0a10.dist-info → zrb-1.0.0a14.dist-info}/METADATA +1 -2
- {zrb-1.0.0a10.dist-info → zrb-1.0.0a14.dist-info}/RECORD +79 -55
- {zrb-1.0.0a10.dist-info → zrb-1.0.0a14.dist-info}/WHEEL +1 -1
- /zrb/builtin/project/add/fastapp_template/{module/auth/client/base_client.py → _zrb/module/module_template/client/any_client.py} +0 -0
- /zrb/builtin/{setup/dev/tmux_config.sh → project/add/fastapp_template/_zrb/module/module_template/service/__init__.py} +0 -0
- /zrb/builtin/project/add/fastapp_template/common/{db_repository.py → base_db_repository.py} +0 -0
- /zrb/builtin/project/add/fastapp_template/common/{usecase.py → base_usecase.py} +0 -0
- /zrb/builtin/project/add/fastapp_template/module/auth/service/user/repository/{repository.py → user_repository.py} +0 -0
- /zrb/builtin/setup/{dev → asdf}/asdf_helper.py +0 -0
- /zrb/builtin/setup/{dev → tmux}/tmux_helper.py +0 -0
- /zrb/builtin/setup/{system/ubuntu.py → ubuntu.py} +0 -0
- {zrb-1.0.0a10.dist-info → zrb-1.0.0a14.dist-info}/entry_points.txt +0 -0
zrb/input/base_input.py
CHANGED
@@ -14,7 +14,7 @@ class BaseInput(AnyInput):
|
|
14
14
|
prompt: str | None = None,
|
15
15
|
default_str: StrAttr = "",
|
16
16
|
auto_render: bool = True,
|
17
|
-
allow_empty: bool =
|
17
|
+
allow_empty: bool = False,
|
18
18
|
):
|
19
19
|
self._name = name
|
20
20
|
self._description = description
|
@@ -66,7 +66,8 @@ class BaseInput(AnyInput):
|
|
66
66
|
default_value = self._get_default_str(shared_ctx)
|
67
67
|
if default_value != "":
|
68
68
|
prompt_message = f"{prompt_message} [{default_value}]"
|
69
|
-
|
69
|
+
print(f"{prompt_message}: ", end="")
|
70
|
+
value = input()
|
70
71
|
if value.strip() == "":
|
71
72
|
value = default_value
|
72
73
|
return value
|
zrb/input/bool_input.py
CHANGED
zrb/input/float_input.py
CHANGED
zrb/input/int_input.py
CHANGED
zrb/input/option_input.py
CHANGED
zrb/input/password_input.py
CHANGED
zrb/input/text_input.py
CHANGED
@@ -16,7 +16,7 @@ class TextInput(BaseInput):
|
|
16
16
|
prompt: str | None = None,
|
17
17
|
default_str: str | Callable[[AnySharedContext], str] = "",
|
18
18
|
auto_render: bool = True,
|
19
|
-
allow_empty: bool =
|
19
|
+
allow_empty: bool = False,
|
20
20
|
editor: str = DEFAULT_EDITOR,
|
21
21
|
extension: str = ".txt",
|
22
22
|
comment_start: str | None = None,
|
zrb/runner/cli.py
CHANGED
@@ -82,13 +82,24 @@ class Cli(Group):
|
|
82
82
|
shared_ctx = SharedContext(args=args)
|
83
83
|
for task_input in task.inputs:
|
84
84
|
if task_input.name in str_kwargs:
|
85
|
-
|
86
|
-
|
85
|
+
# Update shared context for next input default value
|
86
|
+
task_input.update_shared_context(
|
87
|
+
shared_ctx, str_kwargs[task_input.name]
|
88
|
+
)
|
89
|
+
elif arg_index < len(args):
|
87
90
|
run_kwargs[task_input.name] = args[arg_index]
|
91
|
+
# Update shared context for next input default value
|
92
|
+
task_input.update_shared_context(
|
93
|
+
shared_ctx, run_kwargs[task_input.name]
|
94
|
+
)
|
88
95
|
arg_index += 1
|
89
|
-
|
90
|
-
|
91
|
-
|
96
|
+
else:
|
97
|
+
str_value = task_input.prompt_cli_str(shared_ctx)
|
98
|
+
run_kwargs[task_input.name] = str_value
|
99
|
+
# Update shared context for next input default value
|
100
|
+
task_input.update_shared_context(
|
101
|
+
shared_ctx, run_kwargs[task_input.name]
|
102
|
+
)
|
92
103
|
return run_kwargs
|
93
104
|
|
94
105
|
def _show_task_info(self, task: AnyTask):
|
zrb/runner/web_app.py
CHANGED
@@ -2,7 +2,7 @@ import asyncio
|
|
2
2
|
import os
|
3
3
|
import sys
|
4
4
|
from datetime import datetime, timedelta
|
5
|
-
from typing import Any
|
5
|
+
from typing import Any
|
6
6
|
|
7
7
|
from zrb.config import BANNER, WEB_HTTP_PORT
|
8
8
|
from zrb.context.shared_context import SharedContext
|
@@ -23,7 +23,7 @@ from zrb.util.group import extract_node_from_args, get_node_path
|
|
23
23
|
def create_app(root_group: AnyGroup, port: int = WEB_HTTP_PORT):
|
24
24
|
from contextlib import asynccontextmanager
|
25
25
|
|
26
|
-
from fastapi import FastAPI, HTTPException, Request
|
26
|
+
from fastapi import FastAPI, HTTPException, Query, Request
|
27
27
|
from fastapi.responses import FileResponse, HTMLResponse
|
28
28
|
from fastapi.staticfiles import StaticFiles
|
29
29
|
|
@@ -64,7 +64,7 @@ def create_app(root_group: AnyGroup, port: int = WEB_HTTP_PORT):
|
|
64
64
|
# Avoid capturing '/ui' itself
|
65
65
|
if not path:
|
66
66
|
raise HTTPException(status_code=404, detail="Not Found")
|
67
|
-
args = path.split("/")
|
67
|
+
args = path.strip("/").split("/")
|
68
68
|
node, node_path, residual_args = extract_node_from_args(root_group, args)
|
69
69
|
url = f"/ui/{'/'.join(node_path)}/"
|
70
70
|
if isinstance(node, AnyTask):
|
@@ -82,7 +82,7 @@ def create_app(root_group: AnyGroup, port: int = WEB_HTTP_PORT):
|
|
82
82
|
"""
|
83
83
|
Creating new session
|
84
84
|
"""
|
85
|
-
args = path.split("/")
|
85
|
+
args = path.strip("/").split("/")
|
86
86
|
node, _, residual_args = extract_node_from_args(root_group, args)
|
87
87
|
if isinstance(node, AnyTask):
|
88
88
|
session_name = residual_args[0] if residual_args else None
|
@@ -97,33 +97,45 @@ def create_app(root_group: AnyGroup, port: int = WEB_HTTP_PORT):
|
|
97
97
|
raise HTTPException(status_code=404, detail="Not Found")
|
98
98
|
|
99
99
|
@app.get("/api/{path:path}", response_model=SessionStateLog | SessionStateLogList)
|
100
|
-
async def get_session(
|
100
|
+
async def get_session(
|
101
|
+
path: str,
|
102
|
+
min_start_query: str = Query(default=None, alias="from"),
|
103
|
+
max_start_query: str = Query(default=None, alias="to"),
|
104
|
+
page: int = Query(default=0, alias="page"),
|
105
|
+
limit: int = Query(default=10, alias="limit"),
|
106
|
+
):
|
101
107
|
"""
|
102
108
|
Getting existing session or sessions
|
103
109
|
"""
|
104
|
-
args = path.split("/")
|
110
|
+
args = path.strip("/").split("/")
|
105
111
|
node, _, residual_args = extract_node_from_args(root_group, args)
|
106
112
|
if isinstance(node, AnyTask) and residual_args:
|
107
113
|
if residual_args[0] == "list":
|
108
114
|
task_path = get_node_path(root_group, node)
|
109
|
-
|
115
|
+
max_start_time = (
|
116
|
+
datetime.now()
|
117
|
+
if max_start_query is None
|
118
|
+
else datetime.strptime(max_start_query, "%Y-%m-%d %H:%M:%S")
|
119
|
+
)
|
120
|
+
min_start_time = (
|
121
|
+
max_start_time - timedelta(hours=1)
|
122
|
+
if min_start_query is None
|
123
|
+
else datetime.strptime(min_start_query, "%Y-%m-%d %H:%M:%S")
|
124
|
+
)
|
125
|
+
return list_sessions(
|
126
|
+
task_path, min_start_time, max_start_time, page, limit
|
127
|
+
)
|
110
128
|
else:
|
111
129
|
return read_session(residual_args[0])
|
112
130
|
raise HTTPException(status_code=404, detail="Not Found")
|
113
131
|
|
114
132
|
def list_sessions(
|
115
|
-
task_path:
|
133
|
+
task_path: list[str],
|
134
|
+
min_start_time: datetime,
|
135
|
+
max_start_time: datetime,
|
136
|
+
page: int,
|
137
|
+
limit: int,
|
116
138
|
) -> SessionStateLogList:
|
117
|
-
max_start_time = datetime.now()
|
118
|
-
if "to" in query_params:
|
119
|
-
max_start_time = datetime.strptime(query_params["to"], "%Y-%m-%d %H:%M:%S")
|
120
|
-
min_start_time = max_start_time - timedelta(hours=1)
|
121
|
-
if "from" in query_params:
|
122
|
-
min_start_time = datetime.strptime(
|
123
|
-
query_params["from"], "%Y-%m-%d %H:%M:%S"
|
124
|
-
)
|
125
|
-
page = int(query_params.get("page", 0))
|
126
|
-
limit = int(query_params.get("limit", 10))
|
127
139
|
try:
|
128
140
|
return default_session_state_logger.list(
|
129
141
|
task_path,
|
@@ -1,5 +1,6 @@
|
|
1
1
|
import os
|
2
2
|
|
3
|
+
from zrb.context.shared_context import SharedContext
|
3
4
|
from zrb.group.any_group import AnyGroup
|
4
5
|
from zrb.session.any_session import AnySession
|
5
6
|
from zrb.task.any_task import AnyTask
|
@@ -45,12 +46,13 @@ def handle_task_ui(
|
|
45
46
|
ui_url_parts = list(api_url_parts)
|
46
47
|
ui_url_parts[1] = "ui"
|
47
48
|
ui_url = "/".join(ui_url_parts)
|
48
|
-
|
49
|
-
|
49
|
+
# Assemble task inputs
|
50
|
+
input_html_list = []
|
51
|
+
for task_input in task.inputs:
|
52
|
+
task_input.update_shared_context(ctx)
|
53
|
+
input_html_list.append(
|
50
54
|
fstring_format(_TASK_INPUT_TEMPLATE, {"task_input": task_input, "ctx": ctx})
|
51
|
-
|
52
|
-
]
|
53
|
-
)
|
55
|
+
)
|
54
56
|
session_name = args[0] if len(args) > 0 else ""
|
55
57
|
return HTMLResponse(
|
56
58
|
fstring_format(
|
@@ -62,7 +64,7 @@ def handle_task_ui(
|
|
62
64
|
"root_description": root_group.description,
|
63
65
|
"url": url,
|
64
66
|
"parent_url": parent_url,
|
65
|
-
"task_inputs":
|
67
|
+
"task_inputs": "\n".join(input_html_list),
|
66
68
|
"api_url": api_url,
|
67
69
|
"ui_url": ui_url,
|
68
70
|
"main_script": _MAIN_SCRIPT,
|
zrb/session/session.py
CHANGED
@@ -28,6 +28,7 @@ from zrb.util.cli.style import (
|
|
28
28
|
ICONS,
|
29
29
|
MAGENTA,
|
30
30
|
YELLOW,
|
31
|
+
remove_style,
|
31
32
|
)
|
32
33
|
from zrb.util.group import get_node_path
|
33
34
|
from zrb.util.string.name import get_random_name
|
@@ -165,7 +166,9 @@ class Session(AnySession):
|
|
165
166
|
main_task_name=self._main_task.name,
|
166
167
|
path=self.task_path,
|
167
168
|
final_result=(
|
168
|
-
f"{self.final_result}"
|
169
|
+
remove_style(f"{self.final_result}")
|
170
|
+
if self.final_result is not None
|
171
|
+
else ""
|
169
172
|
),
|
170
173
|
finished=self.is_terminated,
|
171
174
|
log=self.shared_ctx.shared_log,
|
zrb/task/scaffolder.py
CHANGED
@@ -92,8 +92,8 @@ class Scaffolder(BaseTask):
|
|
92
92
|
destination_path = self._get_destination_path(ctx)
|
93
93
|
self._copy_path(ctx, source_path, destination_path)
|
94
94
|
transformers = self._get_content_transformers()
|
95
|
-
|
96
|
-
for file_path in
|
95
|
+
file_path_list = self._get_all_file_paths(destination_path)
|
96
|
+
for file_path in file_path_list:
|
97
97
|
for transformer in transformers:
|
98
98
|
if transformer.match(ctx, file_path):
|
99
99
|
try:
|
@@ -118,14 +118,10 @@ class Scaffolder(BaseTask):
|
|
118
118
|
dest_dir, self._transform_path(ctx, file_name)
|
119
119
|
)
|
120
120
|
shutil.copy2(src_file, dest_file)
|
121
|
-
ctx.log_info(f"Copied
|
121
|
+
ctx.log_info(f"Copied {src_file} to {dest_file}")
|
122
122
|
else:
|
123
|
-
|
124
|
-
|
125
|
-
self._transform_path(ctx, os.path.basename(source_path)),
|
126
|
-
)
|
127
|
-
shutil.copy2(source_path, dest_file)
|
128
|
-
ctx.log_info(f"Copied and renamed {source_path} to {dest_file}")
|
123
|
+
shutil.copy2(source_path, destination_path)
|
124
|
+
ctx.log_info(f"Copied {source_path} to {destination_path}")
|
129
125
|
|
130
126
|
def _transform_path(self, ctx: AnyContext, file_path: str):
|
131
127
|
if callable(self._path_transformer):
|
@@ -139,6 +135,8 @@ class Scaffolder(BaseTask):
|
|
139
135
|
"""
|
140
136
|
Returns a list of absolute file paths for all files in the given path, recursively.
|
141
137
|
"""
|
138
|
+
if os.path.isfile(path):
|
139
|
+
return [os.path.abspath(path)]
|
142
140
|
file_paths = []
|
143
141
|
for root, _, files in os.walk(path):
|
144
142
|
for file in files:
|
zrb/util/cli/style.py
CHANGED
@@ -1,3 +1,5 @@
|
|
1
|
+
import re
|
2
|
+
|
1
3
|
BLACK = 30
|
2
4
|
RED = 31
|
3
5
|
GREEN = 32
|
@@ -118,6 +120,11 @@ ICONS = [
|
|
118
120
|
]
|
119
121
|
|
120
122
|
|
123
|
+
def remove_style(text):
|
124
|
+
ansi_escape = re.compile(r"\x1B[@-_][0-?]*[ -/]*[@-~]")
|
125
|
+
return ansi_escape.sub("", text)
|
126
|
+
|
127
|
+
|
121
128
|
def stylize(
|
122
129
|
text: str,
|
123
130
|
color: int | None = None,
|
@@ -0,0 +1,12 @@
|
|
1
|
+
def add_code_to_module(source_code: str, new_code: str) -> str:
|
2
|
+
lines = source_code.splitlines()
|
3
|
+
last_import_index = -1
|
4
|
+
for i, line in enumerate(lines):
|
5
|
+
stripped_line = line.strip()
|
6
|
+
if stripped_line.startswith("import") or stripped_line.startswith("from"):
|
7
|
+
last_import_index = i
|
8
|
+
elif stripped_line and not stripped_line.startswith("#"):
|
9
|
+
break
|
10
|
+
if last_import_index != -1:
|
11
|
+
lines.insert(last_import_index + 1, new_code)
|
12
|
+
return "\n".join(lines)
|
zrb/util/load.py
CHANGED
@@ -3,37 +3,39 @@ import importlib.util
|
|
3
3
|
import os
|
4
4
|
import re
|
5
5
|
import sys
|
6
|
+
from functools import lru_cache
|
6
7
|
from typing import Any
|
7
8
|
|
8
9
|
pattern = re.compile("[^a-zA-Z0-9]")
|
9
10
|
|
10
11
|
|
11
|
-
def load_zrb_init(dir_path: str | None = None):
|
12
|
+
def load_zrb_init(dir_path: str | None = None) -> Any | None:
|
12
13
|
if dir_path is None:
|
13
14
|
dir_path = os.getcwd()
|
14
15
|
script_path = os.path.join(dir_path, "zrb_init.py")
|
15
16
|
if os.path.isfile(script_path):
|
16
|
-
load_file(script_path)
|
17
|
-
return
|
17
|
+
return load_file(script_path, -1)
|
18
18
|
new_dir_path = os.path.dirname(dir_path)
|
19
19
|
if new_dir_path == dir_path:
|
20
20
|
return
|
21
|
-
load_zrb_init(new_dir_path)
|
21
|
+
return load_zrb_init(new_dir_path)
|
22
22
|
|
23
23
|
|
24
|
-
|
24
|
+
@lru_cache
|
25
|
+
def load_file(script_path: str, sys_path_index: int = 0) -> Any | None:
|
25
26
|
if not os.path.isfile(script_path):
|
26
|
-
return
|
27
|
+
return None
|
28
|
+
module_name = pattern.sub("", script_path)
|
27
29
|
# Append script dir path
|
28
30
|
script_dir_path = os.path.dirname(script_path)
|
29
31
|
if script_dir_path not in sys.path:
|
30
32
|
sys.path.insert(sys_path_index, script_dir_path)
|
31
33
|
# Add script dir path to Python path
|
32
34
|
os.environ["PYTHONPATH"] = _get_new_python_path(script_dir_path)
|
33
|
-
module_name = pattern.sub("", script_path)
|
34
35
|
spec = importlib.util.spec_from_file_location(module_name, script_path)
|
35
36
|
module = importlib.util.module_from_spec(spec)
|
36
37
|
spec.loader.exec_module(module)
|
38
|
+
return module
|
37
39
|
|
38
40
|
|
39
41
|
def _get_new_python_path(dir_path: str) -> str:
|
zrb/util/string/conversion.py
CHANGED
@@ -63,6 +63,58 @@ def to_human_case(text: str | None) -> str:
|
|
63
63
|
)
|
64
64
|
|
65
65
|
|
66
|
+
def pluralize(noun: str) -> str:
|
67
|
+
"""
|
68
|
+
Pluralize a given noun.
|
69
|
+
|
70
|
+
Args:
|
71
|
+
noun (str): The singular noun.
|
72
|
+
|
73
|
+
Returns:
|
74
|
+
str: The plural form of the noun.
|
75
|
+
"""
|
76
|
+
# Irregular plural forms
|
77
|
+
irregulars = {
|
78
|
+
"foot": "feet",
|
79
|
+
"tooth": "teeth",
|
80
|
+
"child": "children",
|
81
|
+
"person": "people",
|
82
|
+
"man": "men",
|
83
|
+
"woman": "women",
|
84
|
+
"mouse": "mice",
|
85
|
+
"goose": "geese",
|
86
|
+
"ox": "oxen",
|
87
|
+
"cactus": "cacti",
|
88
|
+
"focus": "foci",
|
89
|
+
"fungus": "fungi",
|
90
|
+
"nucleus": "nuclei",
|
91
|
+
"syllabus": "syllabi",
|
92
|
+
"analysis": "analyses",
|
93
|
+
"diagnosis": "diagnoses",
|
94
|
+
"thesis": "theses",
|
95
|
+
"crisis": "crises",
|
96
|
+
"phenomenon": "phenomena",
|
97
|
+
"criterion": "criteria",
|
98
|
+
}
|
99
|
+
|
100
|
+
# Handle irregular nouns
|
101
|
+
if noun.lower() in irregulars:
|
102
|
+
return irregulars[noun.lower()]
|
103
|
+
# Handle words ending in 'y' preceded by a consonant
|
104
|
+
if noun.endswith("y") and not re.match(r"[aeiou]y$", noun):
|
105
|
+
return re.sub(r"y$", "ies", noun)
|
106
|
+
# Handle words ending in 's', 'x', 'z', 'ch', or 'sh'
|
107
|
+
if re.search(r"(s|x|z|ch|sh)$", noun):
|
108
|
+
return noun + "es"
|
109
|
+
# Handle words ending in 'f' or 'fe'
|
110
|
+
if re.search(r"f$|fe$", noun):
|
111
|
+
if noun.endswith("fe"):
|
112
|
+
return re.sub(r"fe$", "ves", noun)
|
113
|
+
return re.sub(r"f$", "ves", noun)
|
114
|
+
# Handle general case for regular plurals
|
115
|
+
return noun + "s"
|
116
|
+
|
117
|
+
|
66
118
|
def _to_space_separated(text: str | None) -> str:
|
67
119
|
text = str(text) if text is not None else ""
|
68
120
|
text = text.replace("-", " ").replace("_", " ")
|
zrb/util/todo.py
CHANGED
@@ -1,11 +1,15 @@
|
|
1
1
|
import datetime
|
2
2
|
import re
|
3
|
+
import shutil
|
4
|
+
from typing import Any
|
3
5
|
|
4
6
|
from pydantic import BaseModel, Field, model_validator
|
5
7
|
|
6
8
|
from zrb.util.cli.style import (
|
7
9
|
stylize_bold_green,
|
10
|
+
stylize_bold_yellow,
|
8
11
|
stylize_cyan,
|
12
|
+
stylize_faint,
|
9
13
|
stylize_magenta,
|
10
14
|
stylize_yellow,
|
11
15
|
)
|
@@ -32,6 +36,16 @@ class TodoTaskModel(BaseModel):
|
|
32
36
|
)
|
33
37
|
return values
|
34
38
|
|
39
|
+
def get_additional_info_length(self):
|
40
|
+
results = []
|
41
|
+
for project in self.projects:
|
42
|
+
results.append(f"@{project}")
|
43
|
+
for context in self.contexts:
|
44
|
+
results.append(f"+{context}")
|
45
|
+
for key, val in self.keyval.items():
|
46
|
+
results.append(f"{key}:{val}")
|
47
|
+
return len(", ".join(results))
|
48
|
+
|
35
49
|
|
36
50
|
TODO_TXT_PATTERN = re.compile(
|
37
51
|
r"^(?P<status>x)?\s*" # Optional completion mark ('x')
|
@@ -80,7 +94,8 @@ def load_todo_list(todo_file_path: str) -> list[TodoTaskModel]:
|
|
80
94
|
todo_line = todo_line.strip()
|
81
95
|
if todo_line == "":
|
82
96
|
continue
|
83
|
-
|
97
|
+
todo_task = line_to_todo_task(todo_line)
|
98
|
+
todo_list.append(todo_task)
|
84
99
|
todo_list.sort(
|
85
100
|
key=lambda task: (
|
86
101
|
task.completed,
|
@@ -173,51 +188,154 @@ def todo_task_to_line(task: TodoTaskModel) -> str:
|
|
173
188
|
return " ".join(parts)
|
174
189
|
|
175
190
|
|
176
|
-
def get_visual_todo_list(todo_list: list[TodoTaskModel]) -> str:
|
177
|
-
|
191
|
+
def get_visual_todo_list(todo_list: list[TodoTaskModel], filter: str) -> str:
|
192
|
+
todo_filter = line_to_todo_task(filter)
|
193
|
+
filtered_todo_list = []
|
194
|
+
for todo_task in todo_list:
|
195
|
+
filter_description = todo_filter.description.lower().strip()
|
196
|
+
if (
|
197
|
+
filter_description != ""
|
198
|
+
and filter_description not in todo_task.description.lower()
|
199
|
+
):
|
200
|
+
continue
|
201
|
+
if not all(context in todo_task.contexts for context in todo_filter.contexts):
|
202
|
+
continue
|
203
|
+
if not all(project in todo_task.projects for project in todo_filter.projects):
|
204
|
+
continue
|
205
|
+
if not all(
|
206
|
+
key in todo_task.keyval and todo_task.keyval[key] == val
|
207
|
+
for key, val in todo_filter.keyval.items()
|
208
|
+
):
|
209
|
+
continue
|
210
|
+
filtered_todo_list.append(todo_task)
|
211
|
+
if len(filtered_todo_list) == 0:
|
178
212
|
return "\n".join(["", " Empty todo list... 🌵🦖", ""])
|
179
|
-
|
180
|
-
|
181
|
-
|
213
|
+
max_desc_length = max(
|
214
|
+
len(todo_task.description) for todo_task in filtered_todo_list
|
215
|
+
)
|
216
|
+
if max_desc_length < len("DESCRIPTION"):
|
217
|
+
max_desc_length = len("DESCRIPTION")
|
218
|
+
if max_desc_length > 70:
|
219
|
+
max_desc_length = 70
|
220
|
+
max_additional_info_length = max(
|
221
|
+
todo_task.get_additional_info_length() for todo_task in filtered_todo_list
|
222
|
+
)
|
223
|
+
if max_additional_info_length < len("PROJECT/CONTEXT/OTHERS"):
|
224
|
+
max_additional_info_length = len("PROJECT/CONTEXT/OTHERS")
|
225
|
+
terminal_width, _ = shutil.get_terminal_size()
|
182
226
|
# Headers
|
183
227
|
results = [
|
184
|
-
|
185
|
-
|
186
|
-
|
187
|
-
"".ljust(3), # priority
|
188
|
-
"".ljust(3), # completed
|
189
|
-
"COMPLETED AT".rjust(14), # completed date
|
190
|
-
"CREATED AT".rjust(14), # completed date
|
191
|
-
"DESCRIPTION".ljust(max_desc_name_length),
|
192
|
-
"PROJECT/CONTEXT/OTHERS",
|
193
|
-
]
|
228
|
+
stylize_faint(
|
229
|
+
get_visual_todo_header(
|
230
|
+
terminal_width, max_desc_length, max_additional_info_length
|
194
231
|
)
|
195
232
|
)
|
196
233
|
]
|
197
|
-
for todo_task in
|
198
|
-
completed = "[x]" if todo_task.completed else "[ ]"
|
199
|
-
priority = " " if todo_task.priority is None else f"({todo_task.priority})"
|
200
|
-
completion_date = stylize_yellow(_date_to_str(todo_task.completion_date))
|
201
|
-
creation_date = stylize_cyan(_date_to_str(todo_task.creation_date))
|
202
|
-
description = todo_task.description.ljust(max_desc_name_length)
|
203
|
-
additions = ", ".join(
|
204
|
-
[stylize_yellow(f"+{project}") for project in todo_task.projects]
|
205
|
-
+ [stylize_cyan(f"@{context}") for context in todo_task.contexts]
|
206
|
-
+ [stylize_magenta(f"{key}:{val}") for key, val in todo_task.keyval.items()]
|
207
|
-
)
|
234
|
+
for todo_task in filtered_todo_list:
|
208
235
|
results.append(
|
236
|
+
get_visual_todo_line(
|
237
|
+
terminal_width, max_desc_length, max_additional_info_length, todo_task
|
238
|
+
)
|
239
|
+
)
|
240
|
+
return "\n".join(results)
|
241
|
+
|
242
|
+
|
243
|
+
def get_visual_todo_header(
|
244
|
+
terminal_width: int, max_desc_length: int, max_additional_info_length: int
|
245
|
+
) -> str:
|
246
|
+
priority = "".ljust(3)
|
247
|
+
completed = "".ljust(3)
|
248
|
+
completed_at = "COMPLETED AT".rjust(14)
|
249
|
+
created_at = "CREATED_AT".ljust(14)
|
250
|
+
description = "DESCRIPTION".ljust(min(max_desc_length, 70))
|
251
|
+
additional_info = "PROJECT/CONTEXT/OTHERS"
|
252
|
+
if terminal_width <= 14 + max_desc_length + max_additional_info_length:
|
253
|
+
return " ".join([priority, completed, description])
|
254
|
+
if terminal_width <= 36 + max_desc_length + max_additional_info_length:
|
255
|
+
return " ".join([priority, completed, description, additional_info])
|
256
|
+
return " ".join(
|
257
|
+
[priority, completed, completed_at, created_at, description, additional_info]
|
258
|
+
)
|
259
|
+
|
260
|
+
|
261
|
+
def get_visual_todo_line(
|
262
|
+
terminal_width: int,
|
263
|
+
max_desc_length: int,
|
264
|
+
max_additional_info_length: int,
|
265
|
+
todo_task: TodoTaskModel,
|
266
|
+
) -> str:
|
267
|
+
completed = "[x]" if todo_task.completed else "[ ]"
|
268
|
+
priority = " " if todo_task.priority is None else f"({todo_task.priority})"
|
269
|
+
completed_at = stylize_yellow(_date_to_str(todo_task.completion_date))
|
270
|
+
created_at = stylize_cyan(_date_to_str(todo_task.creation_date))
|
271
|
+
description = todo_task.description
|
272
|
+
if len(description) > max_desc_length:
|
273
|
+
description = description[: max_desc_length - 4] + " ..."
|
274
|
+
description = description.ljust(max_desc_length)
|
275
|
+
description = description[:max_desc_length]
|
276
|
+
if todo_task.completed:
|
277
|
+
description = stylize_faint(description)
|
278
|
+
elif "duration" in todo_task.keyval:
|
279
|
+
description = stylize_bold_yellow(description)
|
280
|
+
additional_info = ", ".join(
|
281
|
+
[stylize_yellow(f"+{project}") for project in todo_task.projects]
|
282
|
+
+ [stylize_cyan(f"@{context}") for context in todo_task.contexts]
|
283
|
+
+ [stylize_magenta(f"{key}:{val}") for key, val in todo_task.keyval.items()]
|
284
|
+
)
|
285
|
+
if terminal_width <= 14 + max_desc_length + max_additional_info_length:
|
286
|
+
return " ".join([priority, completed, description])
|
287
|
+
if terminal_width <= 36 + max_desc_length + max_additional_info_length:
|
288
|
+
return " ".join([priority, completed, description, additional_info])
|
289
|
+
return " ".join(
|
290
|
+
[priority, completed, completed_at, created_at, description, additional_info]
|
291
|
+
)
|
292
|
+
|
293
|
+
|
294
|
+
def get_visual_todo_card(
|
295
|
+
todo_task: TodoTaskModel, log_work_list: list[dict[str, str]]
|
296
|
+
) -> str:
|
297
|
+
description = todo_task.description
|
298
|
+
status = "TODO"
|
299
|
+
if todo_task.completed:
|
300
|
+
status = "DONE"
|
301
|
+
elif "duration" in todo_task.keyval:
|
302
|
+
status = "DOING"
|
303
|
+
priority = todo_task.priority
|
304
|
+
completed_at = (
|
305
|
+
_date_to_str(todo_task.completion_date)
|
306
|
+
if todo_task.completion_date is not None
|
307
|
+
else ""
|
308
|
+
)
|
309
|
+
created_at = (
|
310
|
+
_date_to_str(todo_task.creation_date)
|
311
|
+
if todo_task.creation_date is not None
|
312
|
+
else ""
|
313
|
+
)
|
314
|
+
log_work_str = "\n".join(
|
315
|
+
[
|
209
316
|
" ".join(
|
210
317
|
[
|
211
|
-
|
212
|
-
|
213
|
-
|
214
|
-
creation_date,
|
215
|
-
description,
|
216
|
-
additions,
|
318
|
+
stylize_magenta(log_work.get("duration", "").strip().rjust(12)),
|
319
|
+
stylize_cyan(log_work.get("start", "").strip().rjust(20)),
|
320
|
+
log_work.get("log", "").strip(),
|
217
321
|
]
|
218
322
|
)
|
323
|
+
for log_work in log_work_list
|
324
|
+
]
|
325
|
+
)
|
326
|
+
detail = [
|
327
|
+
f"{'📄 Description'.ljust(16)}: {description}",
|
328
|
+
f"{'🎯 Priority'.ljust(16)}: {priority}",
|
329
|
+
f"{'📊 Status'.ljust(16)}: {status}",
|
330
|
+
f"{'📅 Created at'.ljust(16)}: {created_at}",
|
331
|
+
f"{'✅ Completed at'.ljust(16)}: {completed_at}",
|
332
|
+
]
|
333
|
+
if log_work_str != "":
|
334
|
+
detail.append(
|
335
|
+
stylize_faint(" ".join(["Time Spent".rjust(12), "Start".rjust(20), "Log"]))
|
219
336
|
)
|
220
|
-
|
337
|
+
detail.append(log_work_str)
|
338
|
+
return "\n".join(detail)
|
221
339
|
|
222
340
|
|
223
341
|
def _date_to_str(date: datetime.date | None) -> str:
|