zrb 1.0.0a2__py3-none-any.whl → 1.0.0a4__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.
Files changed (174) hide show
  1. zrb/__init__.py +49 -40
  2. zrb/__main__.py +5 -3
  3. zrb/attr/type.py +2 -1
  4. zrb/builtin/__init__.py +42 -2
  5. zrb/builtin/base64.py +34 -0
  6. zrb/builtin/git.py +156 -0
  7. zrb/builtin/git_subtree.py +88 -0
  8. zrb/builtin/group.py +34 -0
  9. zrb/builtin/llm/llm_chat.py +47 -0
  10. zrb/builtin/llm/tool/cli.py +9 -0
  11. zrb/builtin/llm/tool/rag.py +189 -0
  12. zrb/builtin/llm/tool/web.py +74 -0
  13. zrb/builtin/md5.py +36 -0
  14. zrb/builtin/project/add/fastapp.py +72 -0
  15. zrb/builtin/project/add/fastapp_template/.gitignore +4 -0
  16. zrb/builtin/project/add/fastapp_template/README.md +7 -0
  17. zrb/builtin/project/add/fastapp_template/_zrb/config.py +17 -0
  18. zrb/builtin/project/add/fastapp_template/_zrb/group.py +16 -0
  19. zrb/builtin/project/add/fastapp_template/_zrb/helper.py +97 -0
  20. zrb/builtin/project/add/fastapp_template/_zrb/main.py +132 -0
  21. zrb/builtin/project/add/fastapp_template/_zrb/venv_task.py +22 -0
  22. zrb/builtin/project/add/fastapp_template/common/app.py +18 -0
  23. zrb/builtin/project/add/fastapp_template/common/db_engine.py +5 -0
  24. zrb/builtin/project/add/fastapp_template/common/db_repository.py +134 -0
  25. zrb/builtin/project/add/fastapp_template/common/error.py +8 -0
  26. zrb/builtin/project/add/fastapp_template/common/schema.py +5 -0
  27. zrb/builtin/project/add/fastapp_template/common/usecase.py +232 -0
  28. zrb/builtin/project/add/fastapp_template/config.py +29 -0
  29. zrb/builtin/project/add/fastapp_template/main.py +7 -0
  30. zrb/builtin/project/add/fastapp_template/migrate.py +3 -0
  31. zrb/builtin/project/add/fastapp_template/module/__init__.py +0 -0
  32. zrb/builtin/project/add/fastapp_template/module/auth/alembic.ini +117 -0
  33. zrb/builtin/project/add/fastapp_template/module/auth/client/api_client.py +7 -0
  34. zrb/builtin/project/add/fastapp_template/module/auth/client/base_client.py +27 -0
  35. zrb/builtin/project/add/fastapp_template/module/auth/client/direct_client.py +6 -0
  36. zrb/builtin/project/add/fastapp_template/module/auth/client/factory.py +9 -0
  37. zrb/builtin/project/add/fastapp_template/module/auth/migration/README +1 -0
  38. zrb/builtin/project/add/fastapp_template/module/auth/migration/env.py +108 -0
  39. zrb/builtin/project/add/fastapp_template/module/auth/migration/script.py.mako +26 -0
  40. zrb/builtin/project/add/fastapp_template/module/auth/migration/versions/3093c7336477_add_user_table.py +37 -0
  41. zrb/builtin/project/add/fastapp_template/module/auth/migration_metadata.py +6 -0
  42. zrb/builtin/project/add/fastapp_template/module/auth/route.py +22 -0
  43. zrb/builtin/project/add/fastapp_template/module/auth/service/__init__.py +0 -0
  44. zrb/builtin/project/add/fastapp_template/module/auth/service/user/__init__.py +0 -0
  45. zrb/builtin/project/add/fastapp_template/module/auth/service/user/repository/__init__.py +0 -0
  46. zrb/builtin/project/add/fastapp_template/module/auth/service/user/repository/db_repository.py +39 -0
  47. zrb/builtin/project/add/fastapp_template/module/auth/service/user/repository/factory.py +13 -0
  48. zrb/builtin/project/add/fastapp_template/module/auth/service/user/repository/repository.py +34 -0
  49. zrb/builtin/project/add/fastapp_template/module/auth/service/user/usecase.py +45 -0
  50. zrb/builtin/project/add/fastapp_template/module/gateway/alembic.ini +117 -0
  51. zrb/builtin/project/add/fastapp_template/module/gateway/migration/README +1 -0
  52. zrb/builtin/project/add/fastapp_template/module/gateway/migration/env.py +108 -0
  53. zrb/builtin/project/add/fastapp_template/module/gateway/migration/script.py.mako +26 -0
  54. zrb/builtin/project/add/fastapp_template/module/gateway/migration/versions/.gitkeep +0 -0
  55. zrb/builtin/project/add/fastapp_template/module/gateway/migration_metadata.py +3 -0
  56. zrb/builtin/project/add/fastapp_template/module/gateway/route.py +27 -0
  57. zrb/builtin/project/add/fastapp_template/requirements.txt +6 -0
  58. zrb/builtin/project/add/fastapp_template/schema/__init__.py +0 -0
  59. zrb/builtin/project/add/fastapp_template/schema/role.py +31 -0
  60. zrb/builtin/project/add/fastapp_template/schema/user.py +31 -0
  61. zrb/builtin/project/add/fastapp_template/template.env +2 -0
  62. zrb/builtin/project/create/__init__.py +0 -0
  63. zrb/builtin/project/create/create.py +41 -0
  64. zrb/builtin/project/create/project-template/README.md +3 -0
  65. zrb/builtin/project/create/project-template/zrb_init.py +7 -0
  66. zrb/builtin/python.py +11 -0
  67. zrb/builtin/shell/__init__.py +0 -5
  68. zrb/builtin/shell/autocomplete/__init__.py +0 -9
  69. zrb/builtin/shell/autocomplete/bash.py +5 -6
  70. zrb/builtin/shell/autocomplete/subcmd.py +7 -8
  71. zrb/builtin/shell/autocomplete/zsh.py +5 -6
  72. zrb/builtin/todo.py +219 -0
  73. zrb/callback/any_callback.py +1 -1
  74. zrb/callback/callback.py +5 -5
  75. zrb/cmd/cmd_val.py +2 -2
  76. zrb/config.py +16 -3
  77. zrb/content_transformer/any_content_transformer.py +1 -1
  78. zrb/content_transformer/content_transformer.py +2 -2
  79. zrb/context/any_context.py +1 -1
  80. zrb/context/any_shared_context.py +3 -3
  81. zrb/context/context.py +10 -8
  82. zrb/context/shared_context.py +9 -8
  83. zrb/env/__init__.py +0 -3
  84. zrb/env/any_env.py +1 -1
  85. zrb/env/env.py +3 -4
  86. zrb/env/env_file.py +4 -4
  87. zrb/env/env_map.py +2 -2
  88. zrb/group/__init__.py +0 -3
  89. zrb/group/any_group.py +3 -3
  90. zrb/group/group.py +7 -6
  91. zrb/input/any_input.py +1 -1
  92. zrb/input/base_input.py +4 -4
  93. zrb/input/bool_input.py +5 -5
  94. zrb/input/float_input.py +3 -3
  95. zrb/input/int_input.py +3 -3
  96. zrb/input/option_input.py +51 -0
  97. zrb/input/password_input.py +2 -2
  98. zrb/input/str_input.py +1 -1
  99. zrb/input/text_input.py +12 -10
  100. zrb/runner/cli.py +80 -45
  101. zrb/runner/web_app.py +150 -0
  102. zrb/runner/web_controller/__init__.py +0 -0
  103. zrb/runner/web_controller/group_info_ui/__init__.py +0 -0
  104. zrb/runner/{web_app → web_controller}/group_info_ui/controller.py +7 -8
  105. zrb/runner/{web_app → web_controller}/group_info_ui/view.html +2 -2
  106. zrb/runner/web_controller/home_page/__init__.py +0 -0
  107. zrb/runner/{web_app → web_controller}/home_page/controller.py +7 -6
  108. zrb/runner/{web_app → web_controller}/home_page/view.html +2 -2
  109. zrb/runner/web_controller/task_ui/__init__.py +0 -0
  110. zrb/runner/{web_app → web_controller}/task_ui/controller.py +8 -12
  111. zrb/runner/{web_app → web_controller}/task_ui/view.html +2 -2
  112. zrb/runner/web_util.py +5 -35
  113. zrb/session/any_session.py +13 -7
  114. zrb/session/session.py +78 -40
  115. zrb/session_state_log/session_state_log.py +7 -5
  116. zrb/session_state_logger/any_session_state_logger.py +1 -1
  117. zrb/session_state_logger/default_session_state_logger.py +2 -2
  118. zrb/session_state_logger/file_session_state_logger.py +19 -27
  119. zrb/task/any_task.py +4 -4
  120. zrb/task/base_task.py +33 -23
  121. zrb/task/base_trigger.py +11 -12
  122. zrb/task/cmd_task.py +72 -65
  123. zrb/task/http_check.py +13 -13
  124. zrb/task/llm_task.py +215 -0
  125. zrb/task/make_task.py +9 -9
  126. zrb/task/rsync_task.py +25 -25
  127. zrb/task/scaffolder.py +18 -15
  128. zrb/task/scheduler.py +6 -7
  129. zrb/task/task.py +1 -1
  130. zrb/task/tcp_check.py +11 -13
  131. zrb/util/attr.py +19 -3
  132. zrb/util/cli/style.py +71 -2
  133. zrb/util/cli/subcommand.py +2 -2
  134. zrb/util/codemod/__init__.py +0 -0
  135. zrb/util/codemod/add_code_to_class.py +35 -0
  136. zrb/util/codemod/add_code_to_function.py +36 -0
  137. zrb/util/codemod/add_code_to_method.py +55 -0
  138. zrb/util/codemod/add_key_to_dict.py +51 -0
  139. zrb/util/codemod/add_param_to_function_call.py +39 -0
  140. zrb/util/codemod/add_property_to_class.py +55 -0
  141. zrb/util/git.py +156 -0
  142. zrb/util/git_subtree.py +94 -0
  143. zrb/util/group.py +2 -2
  144. zrb/util/llm/tool.py +63 -0
  145. zrb/util/string/conversion.py +7 -0
  146. zrb/util/todo.py +259 -0
  147. {zrb-1.0.0a2.dist-info → zrb-1.0.0a4.dist-info}/METADATA +13 -5
  148. zrb-1.0.0a4.dist-info/RECORD +197 -0
  149. zrb/builtin/shell/_group.py +0 -9
  150. zrb/builtin/shell/autocomplete/_group.py +0 -6
  151. zrb/runner/web_app/any_request_handler.py +0 -24
  152. zrb/runner/web_server.py +0 -224
  153. zrb-1.0.0a2.dist-info/RECORD +0 -120
  154. /zrb/{runner/web_app → builtin/project}/__init__.py +0 -0
  155. /zrb/{runner/web_app/group_info_ui → builtin/project/add}/__init__.py +0 -0
  156. /zrb/{runner/web_app/home_page → builtin/project/add/fastapp_template}/__init__.py +0 -0
  157. /zrb/{runner/web_app/task_ui → builtin/project/add/fastapp_template/common}/__init__.py +0 -0
  158. /zrb/runner/{web_app → web_controller}/group_info_ui/partial/group_info.html +0 -0
  159. /zrb/runner/{web_app → web_controller}/group_info_ui/partial/group_li.html +0 -0
  160. /zrb/runner/{web_app → web_controller}/group_info_ui/partial/task_info.html +0 -0
  161. /zrb/runner/{web_app → web_controller}/group_info_ui/partial/task_li.html +0 -0
  162. /zrb/runner/{web_app → web_controller}/home_page/partial/group_info.html +0 -0
  163. /zrb/runner/{web_app → web_controller}/home_page/partial/group_li.html +0 -0
  164. /zrb/runner/{web_app → web_controller}/home_page/partial/task_info.html +0 -0
  165. /zrb/runner/{web_app → web_controller}/home_page/partial/task_li.html +0 -0
  166. /zrb/runner/{web_app → web_controller}/static/favicon-32x32.png +0 -0
  167. /zrb/runner/{web_app → web_controller}/static/pico.min.css +0 -0
  168. /zrb/runner/{web_app → web_controller}/task_ui/partial/common-util.js +0 -0
  169. /zrb/runner/{web_app → web_controller}/task_ui/partial/input.html +0 -0
  170. /zrb/runner/{web_app → web_controller}/task_ui/partial/main.js +0 -0
  171. /zrb/runner/{web_app → web_controller}/task_ui/partial/show-existing-session.js +0 -0
  172. /zrb/runner/{web_app → web_controller}/task_ui/partial/visualize-history.js +0 -0
  173. {zrb-1.0.0a2.dist-info → zrb-1.0.0a4.dist-info}/WHEEL +0 -0
  174. {zrb-1.0.0a2.dist-info → zrb-1.0.0a4.dist-info}/entry_points.txt +0 -0
zrb/util/git.py ADDED
@@ -0,0 +1,156 @@
1
+ import subprocess
2
+
3
+ from pydantic import BaseModel
4
+
5
+
6
+ class DiffResult(BaseModel):
7
+ created: list[str]
8
+ removed: list[str]
9
+ updated: list[str]
10
+
11
+
12
+ def get_diff(source_commit: str, current_commit: str) -> DiffResult:
13
+ # git show b176b5a main
14
+ exit_status, output = subprocess.getstatusoutput(
15
+ f"git diff {source_commit} {current_commit}"
16
+ )
17
+ if exit_status != 0:
18
+ raise Exception(output)
19
+ lines = output.split("\n")
20
+ diff: dict[str, dict[str, bool]] = {}
21
+ for line in lines:
22
+ if not line.startswith("---") and not line.startswith("+++"):
23
+ continue
24
+ if line[4:6] != "a/" and line[4:6] != "b/":
25
+ continue
26
+ # line should contains something like `--- a/some-file.txt`
27
+ file = line[6:]
28
+ if file not in diff:
29
+ diff[file] = {"plus": False, "minus": False}
30
+ if line.startswith("---"):
31
+ diff[file]["minus"] = True
32
+ if line.startswith("+++"):
33
+ diff[file]["plus"] = True
34
+ return DiffResult(
35
+ created=[
36
+ file for file, state in diff.items() if state["plus"] and not state["minus"]
37
+ ],
38
+ removed=[
39
+ file for file, state in diff.items() if not state["plus"] and state["minus"]
40
+ ],
41
+ updated=[
42
+ file for file, state in diff.items() if state["plus"] and state["minus"]
43
+ ],
44
+ )
45
+
46
+
47
+ def get_repo_dir() -> str:
48
+ try:
49
+ # Run the Git command to get the repository's top-level directory
50
+ result = subprocess.run(
51
+ ["git", "rev-parse", "--show-toplevel"],
52
+ stdout=subprocess.PIPE,
53
+ stderr=subprocess.PIPE,
54
+ text=True,
55
+ check=True,
56
+ )
57
+ # Return the directory path
58
+ return result.stdout.strip()
59
+ except subprocess.CalledProcessError as e:
60
+ raise Exception(e.stderr or e.stdout)
61
+
62
+
63
+ def get_current_branch() -> str:
64
+ try:
65
+ result = subprocess.run(
66
+ ["git", "rev-parse", "--abbrev-ref", "HEAD"],
67
+ stdout=subprocess.PIPE,
68
+ stderr=subprocess.PIPE,
69
+ text=True,
70
+ check=True,
71
+ )
72
+ return result.stdout.strip()
73
+ except subprocess.CalledProcessError as e:
74
+ raise Exception(e.stderr or e.stdout)
75
+
76
+
77
+ def get_branches() -> list[str]:
78
+ try:
79
+ result = subprocess.run(
80
+ ["git", "branch"],
81
+ stdout=subprocess.PIPE,
82
+ stderr=subprocess.PIPE,
83
+ text=True,
84
+ check=True,
85
+ )
86
+ return [
87
+ branch.lstrip("*").strip() for branch in result.stdout.strip().split("\n")
88
+ ]
89
+ except subprocess.CalledProcessError as e:
90
+ raise Exception(e.stderr or e.stdout)
91
+
92
+
93
+ def delete_branch(branch_name: str) -> str:
94
+ try:
95
+ result = subprocess.run(
96
+ ["git", "branch", "-D", branch_name],
97
+ stdout=subprocess.PIPE,
98
+ stderr=subprocess.PIPE,
99
+ text=True,
100
+ check=True,
101
+ )
102
+ return result.stdout.strip()
103
+ except subprocess.CalledProcessError as e:
104
+ raise Exception(e.stderr or e.stdout)
105
+
106
+
107
+ def add() -> str:
108
+ try:
109
+ subprocess.run(
110
+ ["git", "add", ".", "-A"],
111
+ stdout=subprocess.PIPE,
112
+ stderr=subprocess.PIPE,
113
+ text=True,
114
+ check=True,
115
+ )
116
+ except subprocess.CalledProcessError as e:
117
+ raise Exception(e.stderr or e.stdout)
118
+
119
+
120
+ def commit(message: str) -> str:
121
+ try:
122
+ subprocess.run(
123
+ ["git", "commit", "-m", message],
124
+ stdout=subprocess.PIPE,
125
+ stderr=subprocess.PIPE,
126
+ text=True,
127
+ check=True,
128
+ )
129
+ except subprocess.CalledProcessError as e:
130
+ raise Exception(e.stderr or e.stdout)
131
+
132
+
133
+ def pull(remote: str, branch: str) -> str:
134
+ try:
135
+ subprocess.run(
136
+ ["git", "pull", remote, branch],
137
+ stdout=subprocess.PIPE,
138
+ stderr=subprocess.PIPE,
139
+ text=True,
140
+ check=True,
141
+ )
142
+ except subprocess.CalledProcessError as e:
143
+ raise Exception(e.stderr or e.stdout)
144
+
145
+
146
+ def push(remote: str, branch: str) -> str:
147
+ try:
148
+ subprocess.run(
149
+ ["git", "push", "-u", remote, branch],
150
+ stdout=subprocess.PIPE,
151
+ stderr=subprocess.PIPE,
152
+ text=True,
153
+ check=True,
154
+ )
155
+ except subprocess.CalledProcessError as e:
156
+ raise Exception(e.stderr or e.stdout)
@@ -0,0 +1,94 @@
1
+ import os
2
+ import subprocess
3
+
4
+ from pydantic import BaseModel
5
+
6
+ from zrb.util.git import get_repo_dir
7
+
8
+
9
+ class SingleSubTreeConfig(BaseModel):
10
+ repo_url: str
11
+ branch: str
12
+ prefix: str
13
+
14
+
15
+ class SubTreeConfig(BaseModel):
16
+ data: dict[str, SingleSubTreeConfig]
17
+
18
+
19
+ def load_config() -> SubTreeConfig:
20
+ file_path = os.path.join(get_repo_dir(), "subtrees.json")
21
+ if not os.path.exists(file_path):
22
+ return SubTreeConfig(data={})
23
+ with open(file_path, "r") as f:
24
+ return SubTreeConfig.model_validate_json(f.read())
25
+
26
+
27
+ def save_config(config: SubTreeConfig):
28
+ file_path = os.path.join(get_repo_dir(), "subtrees.json")
29
+ with open(file_path, "w") as f:
30
+ f.write(config.model_dump_json(indent=2))
31
+
32
+
33
+ def add_subtree(name: str, repo_url: str, branch: str, prefix: str):
34
+ config = load_config()
35
+ if os.path.isdir(prefix):
36
+ raise ValueError(f"Directory exists: {prefix}")
37
+ if name in config.data:
38
+ raise ValueError(f"Subtree config already exists: {name}")
39
+ try:
40
+ subprocess.run(
41
+ ["git", "subtree", "add", "--prefix", prefix, repo_url, branch],
42
+ stdout=subprocess.PIPE,
43
+ stderr=subprocess.PIPE,
44
+ text=True,
45
+ check=True,
46
+ )
47
+ except subprocess.CalledProcessError as e:
48
+ raise Exception(e.stderr or e.stdout)
49
+ config.data[name] = SingleSubTreeConfig(
50
+ repo_url=repo_url, branch=branch, prefix=prefix
51
+ )
52
+ save_config(config)
53
+
54
+
55
+ def pull_subtree(prefix: str, repo_url: str, branch: str):
56
+ try:
57
+ subprocess.run(
58
+ [
59
+ "git",
60
+ "subtree",
61
+ "pull",
62
+ "--prefix",
63
+ prefix,
64
+ repo_url,
65
+ branch,
66
+ ],
67
+ stdout=subprocess.PIPE,
68
+ stderr=subprocess.PIPE,
69
+ text=True,
70
+ check=True,
71
+ )
72
+ except subprocess.CalledProcessError as e:
73
+ raise Exception(e.stderr or e.stdout)
74
+
75
+
76
+ def push_subtree(prefix: str, repo_url: str, branch: str):
77
+ try:
78
+ subprocess.run(
79
+ [
80
+ "git",
81
+ "subtree",
82
+ "push",
83
+ "--prefix",
84
+ prefix,
85
+ repo_url,
86
+ branch,
87
+ ],
88
+ stdout=subprocess.PIPE,
89
+ stderr=subprocess.PIPE,
90
+ text=True,
91
+ check=True,
92
+ )
93
+ except subprocess.CalledProcessError as e:
94
+ raise Exception(e.stderr or e.stdout)
zrb/util/group.py CHANGED
@@ -1,5 +1,5 @@
1
- from ..group.any_group import AnyGroup
2
- from ..task.any_task import AnyTask
1
+ from zrb.group.any_group import AnyGroup
2
+ from zrb.task.any_task import AnyTask
3
3
 
4
4
 
5
5
  class NodeNotFoundError(ValueError):
zrb/util/llm/tool.py ADDED
@@ -0,0 +1,63 @@
1
+ import inspect
2
+ from collections.abc import Callable
3
+ from typing import Any, get_type_hints
4
+
5
+
6
+ def callable_to_tool_schema(
7
+ callable_obj: Callable, name: str | None = None, description: str | None = None
8
+ ) -> dict[str, Any]:
9
+ """
10
+ Convert a callable into a tool schema dictionary by deriving the parameter schema.
11
+
12
+ :param callable_obj: The callable object (e.g., a function).
13
+ :param name: The name to assign to the function in the schema.
14
+ :param description: A description of the function.
15
+ :return: A dictionary representing the tool schema.
16
+ """
17
+ if not callable(callable_obj):
18
+ raise ValueError("Provided object is not callable")
19
+ # Derive name and description
20
+ name = name or callable_obj.__name__
21
+ description = description or (callable_obj.__doc__ or "").strip()
22
+ # Get function signature
23
+ sig = inspect.signature(callable_obj)
24
+ hints = get_type_hints(callable_obj)
25
+ # Build parameter schema
26
+ param_schema = {"type": "object", "properties": {}, "required": []}
27
+ for param_name, param in sig.parameters.items():
28
+ param_type = hints.get(param_name, str) # Default type is string
29
+ param_schema["properties"][param_name] = {
30
+ "type": _python_type_to_json_type(param_type)
31
+ }
32
+ if param.default is inspect.Parameter.empty:
33
+ param_schema["required"].append(param_name)
34
+ return {
35
+ "type": "function",
36
+ "function": {
37
+ "name": name,
38
+ "description": description,
39
+ "parameters": param_schema,
40
+ },
41
+ }
42
+
43
+
44
+ def _python_type_to_json_type(py_type):
45
+ """
46
+ Map Python types to JSON Schema types.
47
+ """
48
+ if py_type is str:
49
+ return "string"
50
+ elif py_type is int:
51
+ return "integer"
52
+ elif py_type is float:
53
+ return "number"
54
+ elif py_type is bool:
55
+ return "boolean"
56
+ elif py_type is list:
57
+ return "array"
58
+ elif py_type is dict:
59
+ return "object"
60
+ elif py_type in {None, type(None)}:
61
+ return "null"
62
+ else:
63
+ return "string" # Default to string for unsupported types
@@ -5,6 +5,13 @@ TRUE_STRS = ["true", "1", "yes", "y", "active", "on"]
5
5
  FALSE_STRS = ["false", "0", "no", "n", "inactive", "off"]
6
6
 
7
7
 
8
+ def double_quote(input_string: str) -> str:
9
+ # Escape necessary characters: backslashes and double quotes
10
+ escaped_string = re.sub(r'([\\"])', r"\\\1", input_string)
11
+ # Wrap in double quotes
12
+ return f'"{escaped_string}"'
13
+
14
+
8
15
  def to_boolean(text: str) -> bool:
9
16
  if text.lower() in TRUE_STRS:
10
17
  return True
zrb/util/todo.py ADDED
@@ -0,0 +1,259 @@
1
+ import datetime
2
+ import re
3
+
4
+ from pydantic import BaseModel, Field, model_validator
5
+
6
+ from zrb.util.cli.style import (
7
+ stylize_bold_green,
8
+ stylize_cyan,
9
+ stylize_magenta,
10
+ stylize_yellow,
11
+ )
12
+ from zrb.util.string.name import get_random_name
13
+
14
+
15
+ class TodoTaskModel(BaseModel):
16
+ priority: str | None = Field("D", pattern=r"^[A-Z]$") # Priority like A, B, ...
17
+ completed: bool = False # True if completed, False otherwise
18
+ description: str # Main task description
19
+ projects: list[str] = [] # List of projects (e.g., +Project)
20
+ contexts: list[str] = [] # List of contexts (e.g., @Context)
21
+ keyval: dict[str, str] = {} # Special key (e.g., due:2016-05-30)
22
+ creation_date: datetime.date | None = None # Creation date
23
+ completion_date: datetime.date | None = None # Completion date
24
+
25
+ @model_validator(mode="before")
26
+ def validate_dates(cls, values):
27
+ completion_date = values.get("completion_date")
28
+ creation_date = values.get("creation_date")
29
+ if completion_date and not creation_date:
30
+ raise ValueError(
31
+ "creation_date must be specified if completion_date is set."
32
+ )
33
+ return values
34
+
35
+
36
+ TODO_TXT_PATTERN = re.compile(
37
+ r"^(?P<status>x)?\s*" # Optional completion mark ('x')
38
+ r"(?:\((?P<priority>[A-Z])\)\s+)?" # Optional priority (e.g., '(A)')
39
+ r"(?P<date1>\d{4}-\d{2}-\d{2})?\s*" # Optional first date
40
+ r"(?P<date2>\d{4}-\d{2}-\d{2})?\s*" # Optional second date
41
+ r"(?P<description>.*?)$" # Main description
42
+ )
43
+
44
+
45
+ def cascade_todo_task(todo_task: TodoTaskModel):
46
+ if todo_task.creation_date is None:
47
+ todo_task.creation_date = datetime.date.today()
48
+ if "id" not in todo_task.keyval:
49
+ todo_task.keyval["id"] = get_random_name()
50
+ return todo_task
51
+
52
+
53
+ def select_todo_task(
54
+ todo_list: list[TodoTaskModel], keyword: str
55
+ ) -> TodoTaskModel | None:
56
+ for todo_task in todo_list:
57
+ id = todo_task.keyval.get("id", "")
58
+ if keyword.lower().strip() == id.lower().strip():
59
+ return todo_task
60
+ for todo_task in todo_list:
61
+ description = todo_task.description
62
+ if keyword.lower().strip() == description.lower().strip():
63
+ return todo_task
64
+ for todo_task in todo_list:
65
+ id = todo_task.keyval.get("id", "")
66
+ if keyword.lower().strip() in id.lower().strip():
67
+ return todo_task
68
+ for todo_task in todo_list:
69
+ description = todo_task.description
70
+ if keyword.lower().strip() in description.lower().strip():
71
+ return todo_task
72
+ return None
73
+
74
+
75
+ def load_todo_list(todo_file_path: str) -> list[TodoTaskModel]:
76
+ with open(todo_file_path, "r") as f:
77
+ todo_lines = f.read().strip().split("\n")
78
+ todo_list: list[TodoTaskModel] = []
79
+ for todo_line in todo_lines:
80
+ todo_line = todo_line.strip()
81
+ if todo_line == "":
82
+ continue
83
+ todo_list.append(line_to_todo_task(todo_line))
84
+ todo_list.sort(
85
+ key=lambda task: (
86
+ task.completed,
87
+ task.priority if task.priority else "Z",
88
+ task.projects[0] if task.projects else "zzz",
89
+ task.creation_date if task.creation_date else datetime.date.max,
90
+ )
91
+ )
92
+ return todo_list
93
+
94
+
95
+ def save_todo_list(todo_file_path: str, todo_list: list[TodoTaskModel]):
96
+ with open(todo_file_path, "w") as f:
97
+ for todo_task in todo_list:
98
+ f.write(todo_task_to_line(todo_task) + "\n")
99
+
100
+
101
+ def line_to_todo_task(line: str) -> TodoTaskModel:
102
+ """Parses a single todo.txt line into a TodoTask model."""
103
+ match = TODO_TXT_PATTERN.match(line)
104
+ if not match:
105
+ raise ValueError(f"Invalid todo.txt line: {line}")
106
+ groups = match.groupdict()
107
+ # Extract completion status
108
+ is_completed = groups["status"] == "x"
109
+ # Extract dates
110
+ date1 = _parse_date(groups["date1"])
111
+ date2 = _parse_date(groups["date2"])
112
+ # Determine creation_date and completion_date
113
+ completion_date, creation_date = None, None
114
+ if date2 is None:
115
+ creation_date = date1
116
+ else:
117
+ completion_date = date1
118
+ creation_date = date2
119
+ # Extract and clean description
120
+ raw_description = groups["description"] or ""
121
+ projects = re.findall(r"\+(\S+)", raw_description)
122
+ contexts = re.findall(r"@(\S+)", raw_description)
123
+ keyval = {}
124
+ for keyval_str in re.findall(r"(\S+:\S+)", raw_description):
125
+ key, val = keyval_str.split(":", 1)
126
+ keyval[key] = val
127
+ description = re.sub(r"\s*\+\S+|\s*@\S+|\s*\S+:\S+", "", raw_description).strip()
128
+ return TodoTaskModel(
129
+ priority=groups["priority"],
130
+ completed=is_completed,
131
+ description=description,
132
+ projects=projects,
133
+ contexts=contexts,
134
+ keyval=keyval,
135
+ creation_date=creation_date,
136
+ completion_date=completion_date,
137
+ )
138
+
139
+
140
+ def _parse_date(date_str: str | None) -> datetime.date | None:
141
+ """Parses a date string in the format YYYY-MM-DD."""
142
+ if date_str:
143
+ return datetime.date.fromisoformat(date_str)
144
+ return None
145
+
146
+
147
+ def todo_task_to_line(task: TodoTaskModel) -> str:
148
+ """Converts a TodoTask instance back into a todo.txt formatted line."""
149
+ parts = []
150
+ # Add completion mark if task is completed
151
+ if task.completed:
152
+ parts.append("x")
153
+ # Add priority if present
154
+ if task.priority:
155
+ parts.append(f"({task.priority})")
156
+ # Add completion and creation dates if present
157
+ if task.completion_date:
158
+ parts.append(task.completion_date.isoformat())
159
+ if task.creation_date:
160
+ parts.append(task.creation_date.isoformat())
161
+ # Add description
162
+ parts.append(task.description)
163
+ # Append projects
164
+ for project in task.projects:
165
+ parts.append(f"+{project}")
166
+ # Append contexts
167
+ for context in task.contexts:
168
+ parts.append(f"@{context}")
169
+ # Append keyval
170
+ for key, val in task.keyval.items():
171
+ parts.append(f"{key}:{val}")
172
+ # Join all parts with a space
173
+ return " ".join(parts)
174
+
175
+
176
+ def get_visual_todo_list(todo_list: list[TodoTaskModel]) -> str:
177
+ if len(todo_list) == 0:
178
+ return "\n".join(["", " Empty todo list... 🌵🦖", ""])
179
+ max_desc_name_length = max(len(todo_task.description) for todo_task in todo_list)
180
+ if max_desc_name_length < len("DESCRIPTION"):
181
+ max_desc_name_length = len("DESCRIPTION")
182
+ # Headers
183
+ results = [
184
+ stylize_bold_green(
185
+ " ".join(
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
+ ]
194
+ )
195
+ )
196
+ ]
197
+ for todo_task in todo_list:
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
+ )
208
+ results.append(
209
+ " ".join(
210
+ [
211
+ completed,
212
+ priority,
213
+ completion_date,
214
+ creation_date,
215
+ description,
216
+ additions,
217
+ ]
218
+ )
219
+ )
220
+ return "\n".join(results)
221
+
222
+
223
+ def _date_to_str(date: datetime.date | None) -> str:
224
+ if date is None:
225
+ return "".ljust(14)
226
+ return date.strftime("%a %Y-%m-%d")
227
+
228
+
229
+ def add_durations(duration1: str, duration2: str) -> str:
230
+ total_seconds = _parse_duration(duration1) + _parse_duration(duration2)
231
+ # Format and return the result
232
+ return _format_duration(total_seconds)
233
+
234
+
235
+ def _parse_duration(duration: str) -> int:
236
+ """Parse a duration string into total seconds."""
237
+ units = {"M": 2592000, "w": 604800, "d": 86400, "h": 3600, "m": 60, "s": 1}
238
+ total_seconds = 0
239
+ match = re.findall(r"(\d+)([Mwdhms])", duration)
240
+ for value, unit in match:
241
+ total_seconds += int(value) * units[unit]
242
+ return total_seconds
243
+
244
+
245
+ def _format_duration(total_seconds: int) -> str:
246
+ """Format total seconds into a duration string."""
247
+ units = [
248
+ ("w", 604800), # 7 days in a week
249
+ ("d", 86400), # 24 hours in a day
250
+ ("h", 3600), # 60 minutes in an hour
251
+ ("m", 60), # 60 seconds in a minute
252
+ ("s", 1), # seconds
253
+ ]
254
+ result = []
255
+ for unit, value_in_seconds in units:
256
+ if total_seconds >= value_in_seconds:
257
+ amount, total_seconds = divmod(total_seconds, value_in_seconds)
258
+ result.append(f"{amount}{unit}")
259
+ return "".join(result) if result else "0s"
@@ -1,7 +1,7 @@
1
1
  Metadata-Version: 2.1
2
2
  Name: zrb
3
- Version: 1.0.0a2
4
- Summary: A Framework to Enhance Your Workflow
3
+ Version: 1.0.0a4
4
+ Summary: Your Automation Powerhouse
5
5
  Home-page: https://github.com/state-alchemists/zrb
6
6
  License: AGPL-3.0-or-later
7
7
  Keywords: Automation,Task Runner,Code Generator,Monorepo,Low Code
@@ -13,10 +13,18 @@ Classifier: Programming Language :: Python :: 3
13
13
  Classifier: Programming Language :: Python :: 3.10
14
14
  Classifier: Programming Language :: Python :: 3.11
15
15
  Classifier: Programming Language :: Python :: 3.12
16
- Requires-Dist: autopep8 (>=2.0.4,<2.1.0)
17
- Requires-Dist: python-dotenv (>=1.0.1,<1.1.0)
16
+ Provides-Extra: rag
17
+ Requires-Dist: autopep8 (>=2.0.4,<3.0.0)
18
+ Requires-Dist: beautifulsoup4 (>=4.12.3,<5.0.0)
19
+ Requires-Dist: black (>=24.10.0,<24.11.0)
20
+ Requires-Dist: chromadb (>=0.5.20,<0.6.0) ; extra == "rag"
21
+ Requires-Dist: fastapi[standard] (>=0.115.5,<0.116.0)
22
+ Requires-Dist: isort (>=5.13.2,<5.14.0)
23
+ Requires-Dist: libcst (>=1.5.0,<2.0.0)
24
+ Requires-Dist: litellm (>=1.52.12,<2.0.0)
25
+ Requires-Dist: pdfplumber (>=0.11.4,<0.12.0) ; extra == "rag"
26
+ Requires-Dist: python-dotenv (>=1.0.1,<2.0.0)
18
27
  Requires-Dist: requests (>=2.32.3,<3.0.0)
19
- Requires-Dist: tomlkit (>=0.12.4,<0.13.0)
20
28
  Project-URL: Documentation, https://github.com/state-alchemists/zrb
21
29
  Project-URL: Repository, https://github.com/state-alchemists/zrb
22
30
  Description-Content-Type: text/markdown