zrb 1.0.0a2__py3-none-any.whl → 1.0.0a3__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 (151) hide show
  1. zrb/__init__.py +48 -39
  2. zrb/__main__.py +3 -3
  3. zrb/attr/type.py +2 -1
  4. zrb/builtin/__init__.py +40 -2
  5. zrb/builtin/base64.py +32 -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.py +31 -0
  10. zrb/builtin/md5.py +34 -0
  11. zrb/builtin/project/__init__.py +0 -0
  12. zrb/builtin/project/add/__init__.py +0 -0
  13. zrb/builtin/project/add/fastapp.py +72 -0
  14. zrb/builtin/project/add/fastapp_template/.gitignore +4 -0
  15. zrb/builtin/project/add/fastapp_template/README.md +7 -0
  16. zrb/builtin/project/add/fastapp_template/__init__.py +0 -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/__init__.py +0 -0
  23. zrb/builtin/project/add/fastapp_template/common/app.py +18 -0
  24. zrb/builtin/project/add/fastapp_template/common/db_engine.py +5 -0
  25. zrb/builtin/project/add/fastapp_template/common/db_repository.py +134 -0
  26. zrb/builtin/project/add/fastapp_template/common/error.py +8 -0
  27. zrb/builtin/project/add/fastapp_template/common/schema.py +5 -0
  28. zrb/builtin/project/add/fastapp_template/common/usecase.py +232 -0
  29. zrb/builtin/project/add/fastapp_template/config.py +29 -0
  30. zrb/builtin/project/add/fastapp_template/main.py +7 -0
  31. zrb/builtin/project/add/fastapp_template/migrate.py +3 -0
  32. zrb/builtin/project/add/fastapp_template/module/__init__.py +0 -0
  33. zrb/builtin/project/add/fastapp_template/module/auth/alembic.ini +117 -0
  34. zrb/builtin/project/add/fastapp_template/module/auth/client/api_client.py +7 -0
  35. zrb/builtin/project/add/fastapp_template/module/auth/client/base_client.py +27 -0
  36. zrb/builtin/project/add/fastapp_template/module/auth/client/direct_client.py +6 -0
  37. zrb/builtin/project/add/fastapp_template/module/auth/client/factory.py +9 -0
  38. zrb/builtin/project/add/fastapp_template/module/auth/migration/README +1 -0
  39. zrb/builtin/project/add/fastapp_template/module/auth/migration/env.py +108 -0
  40. zrb/builtin/project/add/fastapp_template/module/auth/migration/script.py.mako +26 -0
  41. zrb/builtin/project/add/fastapp_template/module/auth/migration/versions/3093c7336477_add_user_table.py +37 -0
  42. zrb/builtin/project/add/fastapp_template/module/auth/migration_metadata.py +6 -0
  43. zrb/builtin/project/add/fastapp_template/module/auth/route.py +22 -0
  44. zrb/builtin/project/add/fastapp_template/module/auth/service/__init__.py +0 -0
  45. zrb/builtin/project/add/fastapp_template/module/auth/service/user/__init__.py +0 -0
  46. zrb/builtin/project/add/fastapp_template/module/auth/service/user/repository/__init__.py +0 -0
  47. zrb/builtin/project/add/fastapp_template/module/auth/service/user/repository/db_repository.py +39 -0
  48. zrb/builtin/project/add/fastapp_template/module/auth/service/user/repository/factory.py +13 -0
  49. zrb/builtin/project/add/fastapp_template/module/auth/service/user/repository/repository.py +34 -0
  50. zrb/builtin/project/add/fastapp_template/module/auth/service/user/usecase.py +45 -0
  51. zrb/builtin/project/add/fastapp_template/module/gateway/alembic.ini +117 -0
  52. zrb/builtin/project/add/fastapp_template/module/gateway/migration/README +1 -0
  53. zrb/builtin/project/add/fastapp_template/module/gateway/migration/env.py +108 -0
  54. zrb/builtin/project/add/fastapp_template/module/gateway/migration/script.py.mako +26 -0
  55. zrb/builtin/project/add/fastapp_template/module/gateway/migration/versions/.gitkeep +0 -0
  56. zrb/builtin/project/add/fastapp_template/module/gateway/migration_metadata.py +3 -0
  57. zrb/builtin/project/add/fastapp_template/module/gateway/route.py +27 -0
  58. zrb/builtin/project/add/fastapp_template/requirements.txt +6 -0
  59. zrb/builtin/project/add/fastapp_template/schema/__init__.py +0 -0
  60. zrb/builtin/project/add/fastapp_template/schema/role.py +31 -0
  61. zrb/builtin/project/add/fastapp_template/schema/user.py +31 -0
  62. zrb/builtin/project/add/fastapp_template/template.env +2 -0
  63. zrb/builtin/project/create/__init__.py +0 -0
  64. zrb/builtin/project/create/create.py +41 -0
  65. zrb/builtin/project/create/project-template/README.md +3 -0
  66. zrb/builtin/project/create/project-template/zrb_init.py +7 -0
  67. zrb/builtin/python.py +11 -0
  68. zrb/builtin/shell/__init__.py +0 -5
  69. zrb/builtin/shell/autocomplete/__init__.py +0 -9
  70. zrb/builtin/shell/autocomplete/bash.py +5 -6
  71. zrb/builtin/shell/autocomplete/subcmd.py +7 -8
  72. zrb/builtin/shell/autocomplete/zsh.py +5 -6
  73. zrb/builtin/todo.py +186 -0
  74. zrb/callback/any_callback.py +1 -1
  75. zrb/callback/callback.py +5 -5
  76. zrb/cmd/cmd_val.py +2 -2
  77. zrb/config.py +4 -1
  78. zrb/content_transformer/any_content_transformer.py +1 -1
  79. zrb/content_transformer/content_transformer.py +2 -2
  80. zrb/context/any_context.py +1 -1
  81. zrb/context/any_shared_context.py +3 -3
  82. zrb/context/context.py +10 -8
  83. zrb/context/shared_context.py +9 -8
  84. zrb/env/__init__.py +0 -3
  85. zrb/env/any_env.py +1 -1
  86. zrb/env/env.py +3 -4
  87. zrb/env/env_file.py +4 -4
  88. zrb/env/env_map.py +2 -2
  89. zrb/group/__init__.py +0 -3
  90. zrb/group/any_group.py +3 -3
  91. zrb/group/group.py +7 -6
  92. zrb/input/any_input.py +1 -1
  93. zrb/input/base_input.py +4 -4
  94. zrb/input/bool_input.py +5 -5
  95. zrb/input/float_input.py +3 -3
  96. zrb/input/int_input.py +3 -3
  97. zrb/input/option_input.py +51 -0
  98. zrb/input/password_input.py +2 -2
  99. zrb/input/str_input.py +1 -1
  100. zrb/input/text_input.py +12 -10
  101. zrb/runner/cli.py +79 -45
  102. zrb/runner/web_app/group_info_ui/controller.py +7 -8
  103. zrb/runner/web_app/group_info_ui/view.html +2 -2
  104. zrb/runner/web_app/home_page/controller.py +7 -6
  105. zrb/runner/web_app/home_page/view.html +2 -2
  106. zrb/runner/web_app/task_ui/controller.py +8 -12
  107. zrb/runner/web_app/task_ui/view.html +2 -2
  108. zrb/runner/web_server.py +137 -211
  109. zrb/runner/web_util.py +5 -35
  110. zrb/session/any_session.py +13 -7
  111. zrb/session/session.py +78 -40
  112. zrb/session_state_log/session_state_log.py +7 -5
  113. zrb/session_state_logger/any_session_state_logger.py +1 -1
  114. zrb/session_state_logger/default_session_state_logger.py +2 -2
  115. zrb/session_state_logger/file_session_state_logger.py +19 -27
  116. zrb/task/any_task.py +4 -4
  117. zrb/task/base_task.py +33 -23
  118. zrb/task/base_trigger.py +11 -12
  119. zrb/task/cmd_task.py +48 -39
  120. zrb/task/http_check.py +8 -8
  121. zrb/task/llm_task.py +160 -0
  122. zrb/task/make_task.py +9 -9
  123. zrb/task/rsync_task.py +7 -7
  124. zrb/task/scaffolder.py +14 -11
  125. zrb/task/scheduler.py +6 -7
  126. zrb/task/task.py +1 -1
  127. zrb/task/tcp_check.py +8 -8
  128. zrb/util/attr.py +19 -3
  129. zrb/util/cli/style.py +71 -2
  130. zrb/util/cli/subcommand.py +2 -2
  131. zrb/util/codemod/__init__.py +0 -0
  132. zrb/util/codemod/add_code_to_class.py +35 -0
  133. zrb/util/codemod/add_code_to_function.py +36 -0
  134. zrb/util/codemod/add_code_to_method.py +55 -0
  135. zrb/util/codemod/add_key_to_dict.py +51 -0
  136. zrb/util/codemod/add_param_to_function_call.py +39 -0
  137. zrb/util/codemod/add_property_to_class.py +55 -0
  138. zrb/util/git.py +156 -0
  139. zrb/util/git_subtree.py +94 -0
  140. zrb/util/group.py +2 -2
  141. zrb/util/llm/tool.py +63 -0
  142. zrb/util/string/conversion.py +7 -0
  143. zrb/util/todo.py +135 -0
  144. {zrb-1.0.0a2.dist-info → zrb-1.0.0a3.dist-info}/METADATA +8 -5
  145. zrb-1.0.0a3.dist-info/RECORD +194 -0
  146. zrb/builtin/shell/_group.py +0 -9
  147. zrb/builtin/shell/autocomplete/_group.py +0 -6
  148. zrb/runner/web_app/any_request_handler.py +0 -24
  149. zrb-1.0.0a2.dist-info/RECORD +0 -120
  150. {zrb-1.0.0a2.dist-info → zrb-1.0.0a3.dist-info}/WHEEL +0 -0
  151. {zrb-1.0.0a2.dist-info → zrb-1.0.0a3.dist-info}/entry_points.txt +0 -0
zrb/runner/cli.py CHANGED
@@ -1,16 +1,22 @@
1
1
  import sys
2
2
  from typing import Any
3
3
 
4
- from ..config import BANNER, WEB_HTTP_PORT
5
- from ..context.shared_context import SharedContext
6
- from ..group.group import Group
7
- from ..session.session import Session
8
- from ..task.any_task import AnyTask
9
- from ..task.task import Task
10
- from ..util.cli.style import stylize_bold_yellow, stylize_faint, stylize_section_header
11
- from ..util.group import extract_node_from_args, get_non_empty_subgroups, get_subtasks
12
- from ..util.load import load_zrb_init
13
- from .web_server import run_web_server
4
+ from zrb.config import BANNER, WEB_HTTP_PORT
5
+ from zrb.context.any_context import AnyContext
6
+ from zrb.context.shared_context import SharedContext
7
+ from zrb.group.group import Group
8
+ from zrb.runner.web_server import create_app, run_web_server
9
+ from zrb.session.session import Session
10
+ from zrb.task.any_task import AnyTask
11
+ from zrb.task.make_task import make_task
12
+ from zrb.util.cli.style import (
13
+ stylize_bold_yellow,
14
+ stylize_faint,
15
+ stylize_section_header,
16
+ )
17
+ from zrb.util.group import extract_node_from_args, get_non_empty_subgroups, get_subtasks
18
+ from zrb.util.load import load_zrb_init
19
+ from zrb.util.string.conversion import double_quote
14
20
 
15
21
 
16
22
  class Cli(Group):
@@ -25,12 +31,15 @@ class Cli(Group):
25
31
  if "h" in kwargs or "help" in kwargs:
26
32
  self._show_task_info(node)
27
33
  return
28
- result = self._run_task(node, args, kwargs)
29
- if result is not None:
30
- print(result)
31
- run_command = self._get_run_command(node_path, kwargs, args)
32
- self._print_run_command(run_command)
33
- return result
34
+ run_kwargs = self._get_run_kwargs(node, args, kwargs)
35
+ try:
36
+ result = self._run_task(node, args, run_kwargs)
37
+ if result is not None:
38
+ print(result)
39
+ return result
40
+ finally:
41
+ run_command = self._get_run_command(node_path, run_kwargs)
42
+ self._print_run_command(run_command)
34
43
 
35
44
  def _print_run_command(self, run_command: str):
36
45
  print(
@@ -39,34 +48,51 @@ class Cli(Group):
39
48
  file=sys.stderr,
40
49
  )
41
50
 
42
- def _get_run_command(
43
- self, node_path: list[str], kwargs: dict[str, Any], args: list[str]
44
- ) -> str:
51
+ def _get_run_command(self, node_path: list[str], run_kwargs: dict[str, str]) -> str:
45
52
  parts = [self.name] + node_path
46
- if len(kwargs) > 0:
47
- parts += [f"--{key}={val}" for key, val in kwargs.items()]
48
- if len(args) > 0:
49
- parts += args
53
+ if len(run_kwargs) > 0:
54
+ parts += [
55
+ self._get_run_command_param(key, val) for key, val in run_kwargs.items()
56
+ ]
50
57
  return " ".join(parts)
51
58
 
52
- def _run_task(self, task: AnyTask, args: list[str], options: list[str]) -> Any:
59
+ def _get_run_command_param(self, key: str, val: str) -> str:
60
+ if '"' in val or "'" in val or " " in val:
61
+ return f"--{key} {double_quote(val)}"
62
+ return f"--{key} {val}"
63
+
64
+ def _run_task(
65
+ self, task: AnyTask, args: list[str], run_kwargs: dict[str, str]
66
+ ) -> tuple[Any]:
67
+ shared_ctx = SharedContext(args=args)
68
+ for task_input in task.inputs:
69
+ if task_input.name in run_kwargs:
70
+ task_input.update_shared_context(
71
+ shared_ctx, run_kwargs[task_input.name]
72
+ )
73
+ continue
74
+ try:
75
+ return task.run(Session(shared_ctx=shared_ctx, root_group=self))
76
+ except KeyboardInterrupt:
77
+ pass
78
+
79
+ def _get_run_kwargs(
80
+ self, task: AnyTask, args: list[str], kwargs: dict[str, str]
81
+ ) -> tuple[Any]:
53
82
  arg_index = 0
54
- str_kwargs = {key: val for key, val in options.items()}
83
+ str_kwargs = {key: val for key, val in kwargs.items()}
84
+ run_kwargs = {**str_kwargs}
55
85
  shared_ctx = SharedContext(args=args)
56
86
  for task_input in task.inputs:
57
87
  if task_input.name in str_kwargs:
58
88
  continue
59
89
  if arg_index < len(args):
60
- str_kwargs[task_input.name] = args[arg_index]
90
+ run_kwargs[task_input.name] = args[arg_index]
61
91
  arg_index += 1
62
92
  continue
63
- str_kwargs[task_input.name] = task_input.prompt_cli_str(shared_ctx)
64
- try:
65
- return task.run(
66
- Session(shared_ctx=shared_ctx, root_group=self), str_kwargs=str_kwargs
67
- )
68
- except KeyboardInterrupt:
69
- pass
93
+ str_value = task_input.prompt_cli_str(shared_ctx)
94
+ run_kwargs[task_input.name] = str_value
95
+ return run_kwargs
70
96
 
71
97
  def _show_task_info(self, task: AnyTask):
72
98
  description = task.description
@@ -77,8 +103,9 @@ class Cli(Group):
77
103
  print()
78
104
  if len(inputs) > 0:
79
105
  print(stylize_section_header("INPUTS"))
106
+ max_input_name_length = max(len(task_input.name) for task_input in inputs)
80
107
  for task_input in inputs:
81
- task_input_name = task_input.name.ljust(20)
108
+ task_input_name = task_input.name.ljust(max_input_name_length + 1)
82
109
  print(f" --{task_input_name}: {task_input.description}")
83
110
  print()
84
111
 
@@ -93,15 +120,17 @@ class Cli(Group):
93
120
  subgroups = get_non_empty_subgroups(group)
94
121
  if len(subgroups) > 0:
95
122
  print(stylize_section_header("GROUPS"))
123
+ max_subgroup_alias_length = max(len(s) for s in subgroups)
96
124
  for alias, subgroup in subgroups.items():
97
- alias = alias.ljust(20)
125
+ alias = alias.ljust(max_subgroup_alias_length + 1)
98
126
  print(f" {alias}: {subgroup.description}")
99
127
  print()
100
128
  subtasks = get_subtasks(group)
101
129
  if len(subtasks) > 0:
102
130
  print(stylize_section_header("TASKS"))
131
+ max_subtask_alias_length = max(len(s) for s in subtasks)
103
132
  for alias, subtask in subtasks.items():
104
- alias = alias.ljust(20)
133
+ alias = alias.ljust(max_subtask_alias_length + 1)
105
134
  print(f" {alias}: {subtask.description}")
106
135
  print()
107
136
 
@@ -139,14 +168,19 @@ class Cli(Group):
139
168
 
140
169
 
141
170
  cli = Cli(name="zrb", description="Your Automation Powerhouse", banner=BANNER)
142
- server = cli.add_group(Group(name="server", description="Server related command"))
143
- server.add_task(
144
- Task(
145
- name="start-server",
146
- description="Make tasks available via HTTP Requests 🚀",
147
- action=lambda ctx: run_web_server(ctx=ctx, root_group=cli, port=WEB_HTTP_PORT),
148
- cli_only=True,
149
- retries=0,
150
- ),
171
+ server_group = cli.add_group(
172
+ Group(name="server", description="🌐 Server related command")
173
+ )
174
+
175
+
176
+ @make_task(
177
+ name="start-server",
178
+ description="🚀 Start Zrb Web Server",
179
+ cli_only=True,
180
+ retries=0,
181
+ group=server_group,
151
182
  alias="start",
152
183
  )
184
+ async def run(_: AnyContext):
185
+ app = create_app(cli, WEB_HTTP_PORT)
186
+ await run_web_server(app, WEB_HTTP_PORT)
@@ -1,9 +1,10 @@
1
1
  import os
2
2
 
3
- from ....group.any_group import AnyGroup
4
- from ....util.group import get_non_empty_subgroups, get_subtasks
5
- from ....util.string.format import fstring_format
6
- from ..any_request_handler import AnyRequestHandler
3
+ from fastapi.responses import HTMLResponse
4
+
5
+ from zrb.group.any_group import AnyGroup
6
+ from zrb.util.group import get_non_empty_subgroups, get_subtasks
7
+ from zrb.util.string.format import fstring_format
7
8
 
8
9
  _DIR = os.path.dirname(__file__)
9
10
 
@@ -23,9 +24,7 @@ with open(os.path.join(_DIR, "partial", "task_li.html")) as f:
23
24
  _TASK_LI_TEMPLATE = f.read()
24
25
 
25
26
 
26
- def handle_group_info_ui(
27
- handler: AnyRequestHandler, root_group: AnyGroup, group: AnyGroup, url: str
28
- ):
27
+ def handle_group_info_ui(root_group: AnyGroup, group: AnyGroup, url: str):
29
28
  url_parts = url.split("/")
30
29
  parent_url_parts = url_parts[:-2] + [""]
31
30
  parent_url = "/".join(parent_url_parts)
@@ -75,7 +74,7 @@ def handle_group_info_ui(
75
74
  },
76
75
  )
77
76
  )
78
- handler.send_html_response(
77
+ return HTMLResponse(
79
78
  fstring_format(
80
79
  _VIEW_TEMPLATE,
81
80
  {
@@ -4,8 +4,8 @@
4
4
  <meta charset="utf-8">
5
5
  <meta name="viewport" content="width=device-width, initial-scale=1">
6
6
  <meta name="color-scheme" content="light dark">
7
- <link rel="stylesheet" href="/pico.min.css">
8
- <link rel="icon" href="/favicon-32x32.png" sizes="32x32" type="image/png">
7
+ <link rel="stylesheet" href="/static/pico.min.css">
8
+ <link rel="icon" href="/static/favicon-32x32.png" sizes="32x32" type="image/png">
9
9
  <title>Zrb</title>
10
10
  </head>
11
11
  <body>
@@ -1,9 +1,10 @@
1
1
  import os
2
2
 
3
- from ....group.any_group import AnyGroup
4
- from ....util.group import get_non_empty_subgroups, get_subtasks
5
- from ....util.string.format import fstring_format
6
- from ..any_request_handler import AnyRequestHandler
3
+ from fastapi.responses import HTMLResponse
4
+
5
+ from zrb.group.any_group import AnyGroup
6
+ from zrb.util.group import get_non_empty_subgroups, get_subtasks
7
+ from zrb.util.string.format import fstring_format
7
8
 
8
9
  _DIR = os.path.dirname(__file__)
9
10
 
@@ -23,7 +24,7 @@ with open(os.path.join(_DIR, "partial", "task_li.html")) as f:
23
24
  _TASK_LI_TEMPLATE = f.read()
24
25
 
25
26
 
26
- def handle_home_page(handler: AnyRequestHandler, root_group: AnyGroup):
27
+ def handle_home_page(root_group: AnyGroup):
27
28
  subgroups = get_non_empty_subgroups(root_group, web_only=True)
28
29
  group_info = (
29
30
  ""
@@ -62,7 +63,7 @@ def handle_home_page(handler: AnyRequestHandler, root_group: AnyGroup):
62
63
  },
63
64
  )
64
65
  )
65
- handler.send_html_response(
66
+ return HTMLResponse(
66
67
  fstring_format(
67
68
  _VIEW_TEMPLATE,
68
69
  {
@@ -4,8 +4,8 @@
4
4
  <meta charset="utf-8">
5
5
  <meta name="viewport" content="width=device-width, initial-scale=1">
6
6
  <meta name="color-scheme" content="light dark">
7
- <link rel="stylesheet" href="/pico.min.css">
8
- <link rel="icon" href="/favicon-32x32.png" sizes="32x32" type="image/png">
7
+ <link rel="stylesheet" href="/static/pico.min.css">
8
+ <link rel="icon" href="/static/favicon-32x32.png" sizes="32x32" type="image/png">
9
9
  <title>Zrb</title>
10
10
  </head>
11
11
  <body>
@@ -1,10 +1,11 @@
1
1
  import os
2
2
 
3
- from ....group.any_group import AnyGroup
4
- from ....session.any_session import AnySession
5
- from ....task.any_task import AnyTask
6
- from ....util.string.format import fstring_format
7
- from ..any_request_handler import AnyRequestHandler
3
+ from fastapi.responses import HTMLResponse
4
+
5
+ from zrb.group.any_group import AnyGroup
6
+ from zrb.session.any_session import AnySession
7
+ from zrb.task.any_task import AnyTask
8
+ from zrb.util.string.format import fstring_format
8
9
 
9
10
  _DIR = os.path.dirname(__file__)
10
11
 
@@ -28,12 +29,7 @@ with open(os.path.join(_DIR, "partial", "common-util.js")) as f:
28
29
 
29
30
 
30
31
  def handle_task_ui(
31
- handler: AnyRequestHandler,
32
- root_group: AnyGroup,
33
- task: AnyTask,
34
- session: AnySession,
35
- url: str,
36
- args: list[str],
32
+ root_group: AnyGroup, task: AnyTask, session: AnySession, url: str, args: list[str]
37
33
  ):
38
34
  session.register_task(task)
39
35
  ctx = task.get_ctx(session)
@@ -56,7 +52,7 @@ def handle_task_ui(
56
52
  ]
57
53
  )
58
54
  session_name = args[0] if len(args) > 0 else ""
59
- handler.send_html_response(
55
+ return HTMLResponse(
60
56
  fstring_format(
61
57
  _VIEW_TEMPLATE,
62
58
  {
@@ -4,8 +4,8 @@
4
4
  <meta charset="utf-8">
5
5
  <meta name="viewport" content="width=device-width, initial-scale=1">
6
6
  <meta name="color-scheme" content="light dark">
7
- <link rel="stylesheet" href="/pico.min.css">
8
- <link rel="icon" href="/favicon-32x32.png" sizes="32x32" type="image/png">
7
+ <link rel="stylesheet" href="/static/pico.min.css">
8
+ <link rel="icon" href="/static/favicon-32x32.png" sizes="32x32" type="image/png">
9
9
  <title>Zrb</title>
10
10
  </head>
11
11
  <body>