From e49d3bb02ca338864905f860c161e799874857ce Mon Sep 17 00:00:00 2001 From: goFrendiAsgard Date: Thu, 30 Nov 2023 16:21:46 +0700 Subject: [PATCH 01/11] Version 0.0.119 --- docs/getting-started.md | 2 +- pyproject.toml | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/docs/getting-started.md b/docs/getting-started.md index 3ea34cf0..a775e786 100644 --- a/docs/getting-started.md +++ b/docs/getting-started.md @@ -282,7 +282,7 @@ Commands: ## Activating Virtual Environment -Working in a virtual environment is recommended in most cases. This encapsulates your project pip packages, ensuring better independence and reproducibility. +Although not required, working in a virtual environment is often recommended. This encapsulates your project pip packages, ensuring better independence and reproducibility. ### Activating Virtual Environment On A Generated Project diff --git a/pyproject.toml b/pyproject.toml index b727a725..e29800c1 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -4,7 +4,7 @@ build-backend = "flit_core.buildapi" [project] name = "zrb" -version = "0.0.118" +version = "0.0.119" authors = [ { name="Go Frendi Gunawan", email="gofrendiasgard@gmail.com" }, ] From 28843dbf6a1d8805f3fe27d304565c401c20655d Mon Sep 17 00:00:00 2001 From: goFrendiAsgard Date: Thu, 30 Nov 2023 16:48:01 +0700 Subject: [PATCH 02/11] update getting started --- docs/getting-started.md | 29 +++++++++++++++++++---------- 1 file changed, 19 insertions(+), 10 deletions(-) diff --git a/docs/getting-started.md b/docs/getting-started.md index a775e786..64909170 100644 --- a/docs/getting-started.md +++ b/docs/getting-started.md @@ -18,7 +18,7 @@ Welcome to Zrb's getting started guide. We will cover everything you need to kno - [Task Definition](#task-definition) - [Creating a Task Using Task Classes](#creating-a-task-using-task-classes) - [Creating a Task Using Python Decorator](#creating-a-task-using-python-decorator) - - [Task Parameters](#task-parameters) + - [Task Parameters](#task-parameters) - [Task Inputs](#task-inputs) - [Task Environments](#task-environments) - [Switching Environment](#switching-environment) @@ -282,17 +282,22 @@ Commands: ## Activating Virtual Environment -Although not required, working in a virtual environment is often recommended. This encapsulates your project pip packages, ensuring better independence and reproducibility. +Although not required, working in a virtual environment is often recommended. + +A virtual environment encapsulates your project pip packages from other projects, ensuring better independence and reproducibility. ### Activating Virtual Environment On A Generated Project -If you generate the project by invoking `zrb project create`, then you need to run the following command every time you start working on the project: +> __📝 NOTE:__ Using virtual environment is not required, but highly recommended. + + +If you generate the project by invoking `zrb project create`, you can create/activate the virtual environment by running the following command: ```bash source project.sh ``` -The command will activate the project's virtual environment and install necessary pip packages. +The command will activate the project's virtual environment and install the necessary pip packages. ### Activating Virtual Environment On A Manually Created Project @@ -530,14 +535,16 @@ from zrb import runner, TaskClass # Define a task, along with it's parameters task_name = TaskClass( name='task-name', - parameter=value, - other_parameter=other_value + description='the description' + # ... other task parameters ) # regiter the task to zrb runner runner.register(task_name) ``` +> __💡 HINT:__ Check out [task-parameter section](#task-parameters) to see the commonly used parameters + There are several built-in task classes. Each with its specific use case: - __CmdTask__: Run a CLI command/shell script. @@ -562,8 +569,8 @@ from zrb import runner, python_task # Decorate a function named `task_name` @python_task( name='task-name', - parameter=value, - other_parameter=other_value, + description='the description' + # ... other task parameters runner=runner # register the task to zrb runner ) def task_name(*args, **kwargs): @@ -572,12 +579,14 @@ def task_name(*args, **kwargs): # Note that python_task decorator turn your function into a task. So `task_name` is now a task, not a function. ``` +> __💡 HINT:__ Check out [task-parameter section](#task-parameters) to see the commonly used parameters + Using `@python_task` decorator is your best choice if you need to write complex logic in Python. -## Task Parameters +### Task Parameters -Each task has its specific parameter. However, the following tasks are typically available: +Each task has its specific parameter. However, the following parameters are typically available: - __name__: The name of the task. When you invoke the task using the CLI, you need to use this name. By convention, the name should-be written in `kebab-case` (i.e., separated by `-`) - __description__: The description of the task. From 90fccb8218b439d1cae599821aef5eb465042b96 Mon Sep 17 00:00:00 2001 From: goFrendiAsgard Date: Thu, 30 Nov 2023 17:19:12 +0700 Subject: [PATCH 03/11] remove arasaka references --- docs/getting-started.md | 60 +++++++---------------------------------- 1 file changed, 10 insertions(+), 50 deletions(-) diff --git a/docs/getting-started.md b/docs/getting-started.md index 64909170..f3adee73 100644 --- a/docs/getting-started.md +++ b/docs/getting-started.md @@ -282,15 +282,12 @@ Commands: ## Activating Virtual Environment -Although not required, working in a virtual environment is often recommended. +> __📝 NOTE:__ Using virtual environment is not required, but highly recommended. A virtual environment encapsulates your project pip packages from other projects, ensuring better independence and reproducibility. ### Activating Virtual Environment On A Generated Project -> __📝 NOTE:__ Using virtual environment is not required, but highly recommended. - - If you generate the project by invoking `zrb project create`, you can create/activate the virtual environment by running the following command: ```bash @@ -299,6 +296,8 @@ source project.sh The command will activate the project's virtual environment and install the necessary pip packages. +You should run the command every time you start working on the project. + ### Activating Virtual Environment On A Manually Created Project If you create the project manually (i.e., by creating `zrb_init.py`), you also need to make a virtual environment for your project. Creating a virtual environment is necessary if you work with non-standard Python libraries. @@ -315,10 +314,7 @@ Once you make the virtual environment, you can activate it by invoking the follo source .venv/bin/activate ``` -You need to run the command every time you start working on the project. - - -> __💡 HINT:__ Working with virtual environment is recommended whenever you work with any Python project, including Zrb project. +You should activate the virtual environment whenever you start working on the project. # Creating a Task @@ -882,7 +878,7 @@ Host: stalchmst.com # Creating a long-running task -Commonly, you can determine whether a task is successful/failed after it is completed. However, some tasks might run forever, and you can only see whether the task is completed or failed by checking other behaviors. For example, a web server is successfully running if you can get the expected HTTP response from the server. +Commonly, you can determine whether a task is successful/failed after the task is finished. However, some tasks might run forever, and you can only see whether the task is completed or failed by checking other behaviors. For example, a web server is successfully running if you can get the expected HTTP response from the server. Zrb has some checking mechanisms to handle this use case. @@ -892,54 +888,18 @@ Let's start by scaffolding a CmdTask named `run-jupyterlab`. zrb project add cmd-task --project-dir "." --task-name "run-jupyterlab" ``` -Once you do so, you can start modifying `_automate/` - -In some cases, your task has to run forever (i.e., web server). - -Arasaka is a data-driven (and family-driven) company. They need their data scientists to experiment a lot to present the most valuable information/knowledge. - -For this, they need to be able to create a lot of notebooks for experimentation. - -To make sure things work, you need to: -- Install jupyterlab. -- Add Jupyterlab to your `requirements.txt`. -- Create a `notebooks` directory under `src`. -- Create a `start-jupyter` task. - -Let's start by installing jupyterlab - -```bash -pip install jupyterlab -``` - -Once jupyterlab has been installed, you need to add it into requirements.txt. You can do so by typing `pip freeze | grep jupyterlab` and add the output to your `requirements.txt`. Or you can do it with a single command: +You will notice that Zrb automatically creates a file named `_automate/run_jupyterlab.py` -```bash -pip freeze | grep jupyterlab >> requirements.txt -``` +We will need to modify the file. -Now let's make a `notebooks` directory under `src`. - -```bash -mkdir -p src/notebooks -touch src/notebooks/.gitkeep -``` - -You need an empty `.gitkeep` file, to tell git to not ignore the directory. ## Adding start-jupyterlab We have a few requirements for `start-jupyterlab` task -- You should show Arasaka banner before starting jupyterlab. -- `start-jupyterlab` is considered completed only if the port is accessible. -- Arasaka employee can choose the port to serve jupyterlab in their computer. - -Let's start by adding the task to your project. - -```bash -zrb project add cmd-task --project-dir . --task-name start-jupyterlab -``` +- Before starting Jupyterlab, you need to make sure that Jupyterlab is already installed. +- Jupyterlab is considered completed once the port is accessible. +- Jupyterlab HTTP port should be `8080` by default, but users should be able to override the Jupyterlab HTTP port. Now, let's modify `_automate/start_jupyterlab.py` into the following: From 4c1be0191a86e1afd3cc4324b2aa412afc0ac975 Mon Sep 17 00:00:00 2001 From: goFrendiAsgard Date: Fri, 1 Dec 2023 08:07:29 +0700 Subject: [PATCH 04/11] make bell more reliable, add test --- src/zrb/task/base_task/base_task.py | 1 - .../base_task/component/base_task_model.py | 2 +- src/zrb/task/recurring_task.py | 18 +++++- .../generator/cmd_task/test_add_cmd_task.py | 12 ++-- .../test_add_docker_compose_task.py | 12 ++-- test/builtin/generator/fastapp/.gitignore | 1 + .../fastapp/test_add_fastapp_task.py | 61 +++++++++++++++++++ .../builtin/generator/fastapp_crud/.gitignore | 1 + .../fastapp_crud/test_add_fastapp_crud.py | 58 ++++++++++++++++++ .../generator/fastapp_module/.gitignore | 1 + .../fastapp_module/test_add_fastapp_module.py | 49 +++++++++++++++ .../pip_package/test_add_pip_package.py | 12 ++-- .../generator/project/test_create_project.py | 8 +-- .../python_task/test_add_python_task.py | 12 ++-- .../test_add_simple_python_app.py | 12 ++-- zrb_init.py | 2 +- 16 files changed, 222 insertions(+), 40 deletions(-) create mode 100644 test/builtin/generator/fastapp/.gitignore create mode 100644 test/builtin/generator/fastapp/test_add_fastapp_task.py create mode 100644 test/builtin/generator/fastapp_crud/.gitignore create mode 100644 test/builtin/generator/fastapp_crud/test_add_fastapp_crud.py create mode 100644 test/builtin/generator/fastapp_module/.gitignore create mode 100644 test/builtin/generator/fastapp_module/test_add_fastapp_module.py diff --git a/src/zrb/task/base_task/base_task.py b/src/zrb/task/base_task/base_task.py index 11c5e1ca..6b115a2d 100644 --- a/src/zrb/task/base_task/base_task.py +++ b/src/zrb/task/base_task/base_task.py @@ -245,7 +245,6 @@ async def _loop_check(self, show_done_info: bool = False) -> bool: selected_advertisement = get_advertisement(advertisements) selected_advertisement.show() self._show_done_info() - self._play_bell() await self.on_ready() return True diff --git a/src/zrb/task/base_task/component/base_task_model.py b/src/zrb/task/base_task/component/base_task_model.py index 22d1bbdc..0b3df42a 100644 --- a/src/zrb/task/base_task/component/base_task_model.py +++ b/src/zrb/task/base_task/component/base_task_model.py @@ -169,7 +169,7 @@ def print_result(self, result: Any): print(result) def _play_bell(self): - print('\a', end='', file=sys.stderr) + print('\a', end='', file=sys.stderr, flush=True) def _show_done_info(self): elapsed_time = self._get_elapsed_time() diff --git a/src/zrb/task/recurring_task.py b/src/zrb/task/recurring_task.py index 2810da5a..7d6db6b8 100644 --- a/src/zrb/task/recurring_task.py +++ b/src/zrb/task/recurring_task.py @@ -97,6 +97,7 @@ async def run(self, *args: Any, **kwargs: Any): key: kwargs[key] for key in kwargs if key not in ['_task'] } + is_first_time = True while True: # Create trigger functions trigger_functions = [] @@ -107,8 +108,12 @@ async def run(self, *args: Any, **kwargs: Any): trigger_functions.append(asyncio.create_task( trigger_function(*args, **task_kwargs) )) + self.print_out_dark('Waiting for next trigger') + # Mark task as done since trigger has been defined. + if is_first_time: + await self._mark_done() + is_first_time = False # Wait for the first task to complete - self.print_out_dark('Waiting for trigger') _, pending = await asyncio.wait( trigger_functions, return_when=asyncio.FIRST_COMPLETED ) @@ -123,5 +128,12 @@ async def run(self, *args: Any, **kwargs: Any): is_async=True, raise_error=False, show_done_info=False ) self.print_out_dark('Executing the task') - asyncio.create_task(fn(*args, **task_kwargs)) - self._play_bell() + asyncio.create_task( + self.__run_and_play_bell(fn, *args, **task_kwargs) + ) + + async def __run_and_play_bell( + self, fn: Callable[[Any], Any], *args: Any, **kwargs: Any + ): + await fn(*args, **kwargs) + self._play_bell() diff --git a/test/builtin/generator/cmd_task/test_add_cmd_task.py b/test/builtin/generator/cmd_task/test_add_cmd_task.py index ae740497..5aca507a 100644 --- a/test/builtin/generator/cmd_task/test_add_cmd_task.py +++ b/test/builtin/generator/cmd_task/test_add_cmd_task.py @@ -13,14 +13,14 @@ def test_add_cmd_task(): if os.path.exists(project_path): shutil.rmtree(project_path) - create_project_loop = create_project.to_function() - create_project_loop(project_dir=project_path) + create_project_fn = create_project.to_function() + create_project_fn(project_dir=project_path) automate_path = os.path.join(project_path, '_automate') # first attempt should success - first_attempt_loop = add_cmd_task.to_function() - first_attempt_loop( + first_attempt_fn = add_cmd_task.to_function() + first_attempt_fn( project_dir=project_path, task_name='cmdTask' ) @@ -38,8 +38,8 @@ def test_add_cmd_task(): # second attempt should fail is_error = False try: - second_attempt_loop = add_cmd_task.to_function() - second_attempt_loop( + second_attempt_fn = add_cmd_task.to_function() + second_attempt_fn( project_dir=project_path, task_name='cmdTask' ) except Exception: diff --git a/test/builtin/generator/docker_compose_task/test_add_docker_compose_task.py b/test/builtin/generator/docker_compose_task/test_add_docker_compose_task.py index 33b766cf..7082f643 100644 --- a/test/builtin/generator/docker_compose_task/test_add_docker_compose_task.py +++ b/test/builtin/generator/docker_compose_task/test_add_docker_compose_task.py @@ -15,15 +15,15 @@ def test_add_docker_compose_task(): if os.path.exists(project_path): shutil.rmtree(project_path) - create_project_loop = create_project.to_function() - create_project_loop(project_dir=project_path) + create_project_fn = create_project.to_function() + create_project_fn(project_dir=project_path) automate_path = os.path.join(project_path, '_automate') src_path = os.path.join(project_path, 'src') # first attempt should success - first_attempt_loop = add_docker_compose_task.to_function() - first_attempt_loop( + first_attempt_fn = add_docker_compose_task.to_function() + first_attempt_fn( project_dir=project_path, task_name='composeTask' ) @@ -44,8 +44,8 @@ def test_add_docker_compose_task(): # second attempt should fail is_error = False try: - second_attempt_loop = add_docker_compose_task.to_function() - second_attempt_loop( + second_attempt_fn = add_docker_compose_task.to_function() + second_attempt_fn( project_dir=project_path, task_name='composeTask' ) except Exception: diff --git a/test/builtin/generator/fastapp/.gitignore b/test/builtin/generator/fastapp/.gitignore new file mode 100644 index 00000000..7a0b7f00 --- /dev/null +++ b/test/builtin/generator/fastapp/.gitignore @@ -0,0 +1 @@ +app \ No newline at end of file diff --git a/test/builtin/generator/fastapp/test_add_fastapp_task.py b/test/builtin/generator/fastapp/test_add_fastapp_task.py new file mode 100644 index 00000000..4004b71d --- /dev/null +++ b/test/builtin/generator/fastapp/test_add_fastapp_task.py @@ -0,0 +1,61 @@ +from zrb.builtin.generator.project.create import create_project +from zrb.builtin.generator.fastapp.add import add_fastapp +import os +import pathlib +import shutil + + +def test_add_fastapp_task(): + # prepare path + dir_path = pathlib.Path(__file__).parent.absolute() + project_path = os.path.join(dir_path, 'app') + # remove destination if exists + if os.path.exists(project_path): + shutil.rmtree(project_path) + + create_project_fn = create_project.to_function() + create_project_fn(project_dir=project_path) + + automate_path = os.path.join(project_path, '_automate') + + # first attempt should success + first_attempt_fn = add_fastapp.to_function() + first_attempt_fn( + project_dir=project_path, app_name='fastapp', http_port=3000 + ) + + # automate file should exists + assert os.path.isfile( + os.path.join(automate_path, 'fastapp', 'local.py') + ) + assert os.path.isfile( + os.path.join(automate_path, 'fastapp', 'container.py') + ) + + # automate tasks should be imported + with open(os.path.join(project_path, 'zrb_init.py')) as f: + content = f.read() + assert 'import _automate.fastapp.local as fastapp_local' in content + assert 'import _automate.fastapp.container as fastapp_container' in content + assert 'assert fastapp_local' in content + assert 'assert fastapp_container' in content + + # inspect main.py content + main_py_path = os.path.join( + project_path, 'src', 'fastapp', 'src', 'main.py' + ) + with open(main_py_path) as f: + content = f.read() + assert 'from component.app import app' in content + assert 'assert app' in content + + # second attempt should fail + is_error = False + try: + second_attempt_fn = add_fastapp.to_function() + second_attempt_fn( + project_dir=project_path, app_name='fastapp', http_port=3000 + ) + except Exception: + is_error = True + assert is_error diff --git a/test/builtin/generator/fastapp_crud/.gitignore b/test/builtin/generator/fastapp_crud/.gitignore new file mode 100644 index 00000000..7a0b7f00 --- /dev/null +++ b/test/builtin/generator/fastapp_crud/.gitignore @@ -0,0 +1 @@ +app \ No newline at end of file diff --git a/test/builtin/generator/fastapp_crud/test_add_fastapp_crud.py b/test/builtin/generator/fastapp_crud/test_add_fastapp_crud.py new file mode 100644 index 00000000..ce64ebba --- /dev/null +++ b/test/builtin/generator/fastapp_crud/test_add_fastapp_crud.py @@ -0,0 +1,58 @@ +from zrb.builtin.generator.project.create import create_project +from zrb.builtin.generator.fastapp.add import add_fastapp +from zrb.builtin.generator.fastapp_module.add import add_fastapp_module +from zrb.builtin.generator.fastapp_crud.add import add_fastapp_crud +import os +import pathlib +import shutil + + +def test_add_fastapp_crud(): + # prepare path + dir_path = pathlib.Path(__file__).parent.absolute() + project_path = os.path.join(dir_path, 'app') + # remove destination if exists + if os.path.exists(project_path): + shutil.rmtree(project_path) + + create_project_fn = create_project.to_function() + create_project_fn(project_dir=project_path) + add_fastapp_fn = add_fastapp.to_function() + add_fastapp_fn( + project_dir=project_path, app_name='fastapp', http_port=3000 + ) + add_fastapp_module_fn = add_fastapp_module.to_function() + add_fastapp_module_fn( + project_dir=project_path, app_name='fastapp', module_name='library' + ) + + # first attempt should success + first_attempt_fn = add_fastapp_crud.to_function() + first_attempt_fn( + project_dir=project_path, + app_name='fastapp', + module_name='library', + entity_name='book', + plural_entity_name='books', + column_name='code' + ) + + assert os.path.isdir(os.path.join( + project_path, 'src', 'fastapp', 'src', 'module', 'library', 'entity', 'book' # noqa + )) + + # second attempt should fail + is_error = False + try: + second_attempt_fn = add_fastapp_crud.to_function() + second_attempt_fn( + project_dir=project_path, + app_name='fastapp', + module_name='library', + entity_name='book', + plural_entity_name='books', + column_name='code' + ) + except Exception: + is_error = True + assert is_error diff --git a/test/builtin/generator/fastapp_module/.gitignore b/test/builtin/generator/fastapp_module/.gitignore new file mode 100644 index 00000000..7a0b7f00 --- /dev/null +++ b/test/builtin/generator/fastapp_module/.gitignore @@ -0,0 +1 @@ +app \ No newline at end of file diff --git a/test/builtin/generator/fastapp_module/test_add_fastapp_module.py b/test/builtin/generator/fastapp_module/test_add_fastapp_module.py new file mode 100644 index 00000000..24238d6e --- /dev/null +++ b/test/builtin/generator/fastapp_module/test_add_fastapp_module.py @@ -0,0 +1,49 @@ +from zrb.builtin.generator.project.create import create_project +from zrb.builtin.generator.fastapp.add import add_fastapp +from zrb.builtin.generator.fastapp_module.add import add_fastapp_module +import os +import pathlib +import shutil + + +def test_add_fastapp_module(): + # prepare path + dir_path = pathlib.Path(__file__).parent.absolute() + project_path = os.path.join(dir_path, 'app') + # remove destination if exists + if os.path.exists(project_path): + shutil.rmtree(project_path) + + create_project_fn = create_project.to_function() + create_project_fn(project_dir=project_path) + add_fastapp_fn = add_fastapp.to_function() + add_fastapp_fn( + project_dir=project_path, app_name='fastapp', http_port=3000 + ) + + # first attempt should success + first_attempt_fn = add_fastapp_module.to_function() + first_attempt_fn( + project_dir=project_path, app_name='fastapp', module_name='library' + ) + + # inspect main.py content + main_py_path = os.path.join( + project_path, 'src', 'fastapp', 'src', 'main.py' + ) + with open(main_py_path) as f: + content = f.read() + assert 'from component.app import app' in content + assert 'from module.library.register_module import register_library' in content # noqa + assert 'register_library()' in content + + # second attempt should fail + is_error = False + try: + second_attempt_fn = add_fastapp_module.to_function() + second_attempt_fn( + project_dir=project_path, app_name='fastapp', module_name='library' + ) + except Exception: + is_error = True + assert is_error diff --git a/test/builtin/generator/pip_package/test_add_pip_package.py b/test/builtin/generator/pip_package/test_add_pip_package.py index baaff147..3c188995 100644 --- a/test/builtin/generator/pip_package/test_add_pip_package.py +++ b/test/builtin/generator/pip_package/test_add_pip_package.py @@ -15,15 +15,15 @@ def test_add_pip_package(): if os.path.exists(project_path): shutil.rmtree(project_path) - create_project_loop = create_project.to_function() - create_project_loop(project_dir=project_path) + create_project_fn = create_project.to_function() + create_project_fn(project_dir=project_path) automate_path = os.path.join(project_path, '_automate') src_path = os.path.join(project_path, 'src') # first attempt should success - first_attempt_loop = add_pip_package.to_function() - first_attempt_loop( + first_attempt_fn = add_pip_package.to_function() + first_attempt_fn( project_dir=project_path, package_name='pip_package' ) @@ -44,8 +44,8 @@ def test_add_pip_package(): # second attempt should fail is_error = False try: - second_attempt_loop = add_pip_package.to_function() - second_attempt_loop( + second_attempt_fn = add_pip_package.to_function() + second_attempt_fn( project_dir=project_path, package_name='pip_package' ) except Exception: diff --git a/test/builtin/generator/project/test_create_project.py b/test/builtin/generator/project/test_create_project.py index 53bcd13f..db2530b4 100644 --- a/test/builtin/generator/project/test_create_project.py +++ b/test/builtin/generator/project/test_create_project.py @@ -14,8 +14,8 @@ def test_create_project(): shutil.rmtree(destination_path) # first attempt should success - first_attempt_loop = create_project.to_function() - result = first_attempt_loop(project_dir=destination_path) + first_attempt_fn = create_project.to_function() + result = first_attempt_fn(project_dir=destination_path) assert result # .git directory should exists @@ -38,8 +38,8 @@ def test_create_project(): # second attempt should failed is_error = False try: - second_attempt_loop = create_project.to_function() - result = second_attempt_loop(project_dir=destination_path) + second_attempt_fn = create_project.to_function() + result = second_attempt_fn(project_dir=destination_path) except Exception: is_error = True assert is_error diff --git a/test/builtin/generator/python_task/test_add_python_task.py b/test/builtin/generator/python_task/test_add_python_task.py index c60b0b01..f3be80aa 100644 --- a/test/builtin/generator/python_task/test_add_python_task.py +++ b/test/builtin/generator/python_task/test_add_python_task.py @@ -13,14 +13,14 @@ def test_add_python_task(): if os.path.exists(project_path): shutil.rmtree(project_path) - create_project_loop = create_project.to_function() - create_project_loop(project_dir=project_path) + create_project_fn = create_project.to_function() + create_project_fn(project_dir=project_path) automate_path = os.path.join(project_path, '_automate') # first attempt should success - first_attempt_loop = add_python_task.to_function() - first_attempt_loop( + first_attempt_fn = add_python_task.to_function() + first_attempt_fn( project_dir=project_path, task_name='pythonTask' ) @@ -38,8 +38,8 @@ def test_add_python_task(): # second attempt should fail is_error = False try: - second_attempt_loop = add_python_task.to_function() - second_attempt_loop( + second_attempt_fn = add_python_task.to_function() + second_attempt_fn( project_dir=project_path, task_name='pythonTask' ) except Exception: diff --git a/test/builtin/generator/simple-python-app/test_add_simple_python_app.py b/test/builtin/generator/simple-python-app/test_add_simple_python_app.py index 7138ffc4..c6a4ace3 100644 --- a/test/builtin/generator/simple-python-app/test_add_simple_python_app.py +++ b/test/builtin/generator/simple-python-app/test_add_simple_python_app.py @@ -15,15 +15,15 @@ def test_add_simple_python_app(): if os.path.exists(project_path): shutil.rmtree(project_path) - create_project_loop = create_project.to_function() - create_project_loop(project_dir=project_path) + create_project_fn = create_project.to_function() + create_project_fn(project_dir=project_path) automate_path = os.path.join(project_path, '_automate') src_path = os.path.join(project_path, 'src') # first attempt should success - first_attempt_loop = add_simple_python_app.to_function() - first_attempt_loop( + first_attempt_fn = add_simple_python_app.to_function() + first_attempt_fn( project_dir=project_path, app_name='simpleApp' ) @@ -64,8 +64,8 @@ def test_add_simple_python_app(): # second attempt should fail is_error = False try: - second_attempt_loop = add_simple_python_app.to_function() - second_attempt_loop( + second_attempt_fn = add_simple_python_app.to_function() + second_attempt_fn( project_dir=project_path, app_name='simpleApp' ) except Exception: diff --git a/zrb_init.py b/zrb_init.py index 47d2d6f7..7bfde1f4 100644 --- a/zrb_init.py +++ b/zrb_init.py @@ -297,7 +297,7 @@ 'set -e', f'cd {CURRENT_DIR}', 'echo "🤖 Perform test"', - 'pytest --ignore-glob="**/template/**/test" --ignore=playground --cov=zrb --cov-config=".coveragerc" --cov-report=html --cov-report=term --cov-report=term-missing {{input.test}}' # noqa + 'pytest --ignore-glob="**/template/**/test" --ignore-glob="**/generator/**/app" --ignore=playground --cov=zrb --cov-config=".coveragerc" --cov-report=html --cov-report=term --cov-report=term-missing {{input.test}}' # noqa ], retry=0, checking_interval=1 From 2d916169d5998682cb693ebccca31077b160432f Mon Sep 17 00:00:00 2001 From: goFrendiAsgard Date: Fri, 1 Dec 2023 08:50:17 +0700 Subject: [PATCH 05/11] add test --- .../generator/fastapp_field/.gitignore | 1 + .../fastapp_field/test_add_fastapp_field.py | 56 +++++++++++++++++++ 2 files changed, 57 insertions(+) create mode 100644 test/builtin/generator/fastapp_field/.gitignore create mode 100644 test/builtin/generator/fastapp_field/test_add_fastapp_field.py diff --git a/test/builtin/generator/fastapp_field/.gitignore b/test/builtin/generator/fastapp_field/.gitignore new file mode 100644 index 00000000..7a0b7f00 --- /dev/null +++ b/test/builtin/generator/fastapp_field/.gitignore @@ -0,0 +1 @@ +app \ No newline at end of file diff --git a/test/builtin/generator/fastapp_field/test_add_fastapp_field.py b/test/builtin/generator/fastapp_field/test_add_fastapp_field.py new file mode 100644 index 00000000..87ca3919 --- /dev/null +++ b/test/builtin/generator/fastapp_field/test_add_fastapp_field.py @@ -0,0 +1,56 @@ +from zrb.builtin.generator.project.create import create_project +from zrb.builtin.generator.fastapp.add import add_fastapp +from zrb.builtin.generator.fastapp_module.add import add_fastapp_module +from zrb.builtin.generator.fastapp_crud.add import add_fastapp_crud +from zrb.builtin.generator.fastapp_field.add import add_fastapp_field +import os +import pathlib +import shutil + + +def test_add_fastapp_field(): + # prepare path + dir_path = pathlib.Path(__file__).parent.absolute() + project_path = os.path.join(dir_path, 'app') + # remove destination if exists + if os.path.exists(project_path): + shutil.rmtree(project_path) + + create_project_fn = create_project.to_function() + create_project_fn(project_dir=project_path) + add_fastapp_fn = add_fastapp.to_function() + add_fastapp_fn( + project_dir=project_path, app_name='fastapp', http_port=3000 + ) + add_fastapp_module_fn = add_fastapp_module.to_function() + add_fastapp_module_fn( + project_dir=project_path, app_name='fastapp', module_name='library' + ) + add_fastapp_crud_fn = add_fastapp_crud.to_function() + add_fastapp_crud_fn( + project_dir=project_path, + app_name='fastapp', + module_name='library', + entity_name='book', + plural_entity_name='books', + column_name='code' + ) + + # first attempt should success + first_attempt_fn = add_fastapp_field.to_function() + first_attempt_fn( + project_dir=project_path, + app_name='fastapp', + module_name='library', + entity_name='book', + column_name='title', + column_type='str' + ) + + book_schema_py = os.path.join( + project_path, 'src', 'fastapp', 'src', 'module', 'library', 'schema', 'book.py' # noqa + ) + + with open(book_schema_py, 'r') as file: + content = file.read() + assert 'title' in content From 222bc5f5632e2fd6071811f35a30669810c7a1c9 Mon Sep 17 00:00:00 2001 From: goFrendiAsgard Date: Fri, 1 Dec 2023 09:08:35 +0700 Subject: [PATCH 06/11] add summary --- docs/getting-started.md | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/docs/getting-started.md b/docs/getting-started.md index f3adee73..c07e4471 100644 --- a/docs/getting-started.md +++ b/docs/getting-started.md @@ -947,6 +947,9 @@ Let's run the task: zrb project run-jupyterlab ``` +
+Show output + ``` Jupyterlab port [8080]: 🤖 ○ ◷ 2023-11-12 10:26:32.759 ❁ 58728 → 1/3 🐨 zrb project install-jupyterlab • Run script: pip install jupyterlab @@ -980,6 +983,8 @@ Support zrb growth and development! 🤖 ○ ◷ 2023-11-12 10:26:36.807 ❁ 58920 → 1/3 🐹 zrb project run-jupyterlab • Completed in 4.050489664077759 seconds ``` +
+ Open up your browser on [http://localhost:8080](http://localhost:8080) to start working with the notebook. # Now you are ready From 3f196738a29df902b217e5ada3aea7f816af1a6d Mon Sep 17 00:00:00 2001 From: goFrendiAsgard Date: Sat, 2 Dec 2023 11:50:22 +0700 Subject: [PATCH 07/11] introduce notifier --- src/zrb/builtin/helper/__init__.py | 0 src/zrb/builtin/helper/reccuring_action.py | 38 +++++ src/zrb/builtin/schedule.py | 24 +--- src/zrb/builtin/watch.py | 24 +--- src/zrb/shell-scripts/notify.ps1 | 16 +++ src/zrb/task/base_remote_cmd_task.py | 3 + src/zrb/task/checker.py | 3 + src/zrb/task/notifier.py | 155 +++++++++++++++++++++ src/zrb/task/recurring_task.py | 5 +- src/zrb/task/time_watcher.py | 2 +- 10 files changed, 224 insertions(+), 46 deletions(-) create mode 100644 src/zrb/builtin/helper/__init__.py create mode 100644 src/zrb/builtin/helper/reccuring_action.py create mode 100644 src/zrb/shell-scripts/notify.ps1 create mode 100644 src/zrb/task/notifier.py diff --git a/src/zrb/builtin/helper/__init__.py b/src/zrb/builtin/helper/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/src/zrb/builtin/helper/reccuring_action.py b/src/zrb/builtin/helper/reccuring_action.py new file mode 100644 index 00000000..97ebfdf3 --- /dev/null +++ b/src/zrb/builtin/helper/reccuring_action.py @@ -0,0 +1,38 @@ +from zrb.task.task import Task +from zrb.task.cmd_task import CmdTask +from zrb.task.notifier import Notifier +from zrb.task_input.str_input import StrInput + + +def create_recurring_action(title: str) -> Task: + # define inputs + message_input = StrInput( + name='message', + default='👋', + prompt='Message to be shown', + ) + command_input = StrInput( + name='command', + default='', + prompt='Command to be executed', + ) + # define tasks + run_command = CmdTask( + name='run-command', + inputs=[command_input], + should_execute='{{ input.command != "" }}' + ) + notify = Notifier( + name='notify', + inputs=[message_input], + title=title, + message='{{ input.message }}', + should_execute='{{ input.message != "" }}', + ) + # return aggregator task + return Task( + name='recurring-action', + inputs=[message_input, command_input], + upstreams=[run_command, notify], + retry=0 + ) diff --git a/src/zrb/builtin/schedule.py b/src/zrb/builtin/schedule.py index 48be8cc3..0857c0ed 100644 --- a/src/zrb/builtin/schedule.py +++ b/src/zrb/builtin/schedule.py @@ -1,8 +1,8 @@ from zrb.task.recurring_task import RecurringTask -from zrb.task.cmd_task import CmdTask from zrb.task.time_watcher import TimeWatcher from zrb.task_input.str_input import StrInput from zrb.runner import runner +from zrb.builtin.helper.reccuring_action import create_recurring_action schedule = RecurringTask( @@ -19,26 +19,6 @@ triggers=[ TimeWatcher(name='watch-schedule', schedule='{{input.schedule}}') ], - task=CmdTask( - name='run-task', - inputs=[ - StrInput( - name='message', - default='👋', - prompt='Message to be shown', - description='Message to be shown on schedule' - ), - StrInput( - name='command', - default='', - prompt='Command to be executed', - description='Command to be executed on schedule' - ), - ], - cmd=[ - '{% if input.message != "" %}echo {{ input.message }}{% endif %}', - '{% if input.command != "" %}{{ input.command }}{% endif %}', - ] - ) + task=create_recurring_action(title='schedule') ) runner.register(schedule) diff --git a/src/zrb/builtin/watch.py b/src/zrb/builtin/watch.py index 3777e910..408da46b 100644 --- a/src/zrb/builtin/watch.py +++ b/src/zrb/builtin/watch.py @@ -1,8 +1,8 @@ from zrb.task.recurring_task import RecurringTask -from zrb.task.cmd_task import CmdTask from zrb.task.path_watcher import PathWatcher from zrb.task_input.str_input import StrInput from zrb.runner import runner +from zrb.builtin.helper.reccuring_action import create_recurring_action watch = RecurringTask( @@ -19,26 +19,6 @@ triggers=[ PathWatcher(name='watch-path', path='{{input.pattern}}') ], - task=CmdTask( - name='run-task', - inputs=[ - StrInput( - name='message', - default='👋', - prompt='Message to be shown', - description='Message to be shown when changes detected' - ), - StrInput( - name='command', - default='', - prompt='Command to be executed', - description='Command to be executed when changes detected' - ), - ], - cmd=[ - '{% if input.message != "" %}echo {{ input.message }}{% endif %}', - '{% if input.command != "" %}{{ input.command }}{% endif %}', - ] - ) + task=create_recurring_action(title='schedule') ) runner.register(watch) diff --git a/src/zrb/shell-scripts/notify.ps1 b/src/zrb/shell-scripts/notify.ps1 new file mode 100644 index 00000000..d54fadcc --- /dev/null +++ b/src/zrb/shell-scripts/notify.ps1 @@ -0,0 +1,16 @@ +param ( + [string]$Title, + [string]$Message +) + +[Windows.UI.Notifications.ToastNotificationManager, Windows.UI.Notifications, ContentType = WindowsRuntime] > $null +$template = [Windows.UI.Notifications.ToastNotificationManager]::GetTemplateContent([Windows.UI.Notifications.ToastTemplateType]::ToastText02) +$toastXml = [xml] $template.GetXml() +$toastTextElements = $toastXml.GetElementsByTagName("text") +$toastTextElements[0].AppendChild($toastXml.CreateTextNode($Title)) # Title +$toastTextElements[1].AppendChild($toastXml.CreateTextNode($Message)) # Message +$xml = New-Object Windows.Data.Xml.Dom.XmlDocument +$xml.LoadXml($toastXml.OuterXml) +$newToast = [Windows.UI.Notifications.ToastNotification]::new($xml) +$toastNotifier = [Windows.UI.Notifications.ToastNotificationManager]::CreateToastNotifier("WSL") +$toastNotifier.Show($newToast) \ No newline at end of file diff --git a/src/zrb/task/base_remote_cmd_task.py b/src/zrb/task/base_remote_cmd_task.py index c628d41d..ff456ee1 100644 --- a/src/zrb/task/base_remote_cmd_task.py +++ b/src/zrb/task/base_remote_cmd_task.py @@ -262,3 +262,6 @@ def __init__( retry=0, return_upstream_result=True ) + + def __repr__(self) -> str: + return f'' diff --git a/src/zrb/task/checker.py b/src/zrb/task/checker.py index ca7c8f0b..23b7db6f 100644 --- a/src/zrb/task/checker.py +++ b/src/zrb/task/checker.py @@ -87,3 +87,6 @@ def show_progress(self, message: str): self.print_out_dark(message) return self.log_debug(message) + + def __repr__(self) -> str: + return f'' diff --git a/src/zrb/task/notifier.py b/src/zrb/task/notifier.py new file mode 100644 index 00000000..8ae35569 --- /dev/null +++ b/src/zrb/task/notifier.py @@ -0,0 +1,155 @@ +from zrb.helper.typing import Any, Callable, Iterable, Optional, Union +from zrb.helper.typecheck import typechecked +from zrb.task.base_task.base_task import BaseTask +from zrb.task.any_task import AnyTask +from zrb.task.any_task_event_handler import ( + OnTriggered, OnWaiting, OnSkipped, OnStarted, OnReady, OnRetry, OnFailed +) +from zrb.task_env.env import Env +from zrb.task_env.env_file import EnvFile +from zrb.task_group.group import Group +from zrb.task_input.any_input import AnyInput +from zrb.helper.accessories.icon import get_random_icon +from zrb.helper.string.modification import double_quote + +import os +import subprocess + +CURRENT_DIR = os.path.dirname(__file__) +NOTIFY_PS1_PATH = os.path.realpath(os.path.abspath(os.path.join( + os.path.dirname(CURRENT_DIR), 'shell-scripts', 'notify.ps1' +))) + + +@typechecked +class Notifier(BaseTask): + def __init__( + self, + name: str = 'port-check', + group: Optional[Group] = None, + inputs: Iterable[AnyInput] = [], + envs: Iterable[Env] = [], + env_files: Iterable[EnvFile] = [], + icon: Optional[str] = None, + color: Optional[str] = None, + description: str = '', + title: str = '', + message: str = '', + upstreams: Iterable[AnyTask] = [], + on_triggered: Optional[OnTriggered] = None, + on_waiting: Optional[OnWaiting] = None, + on_skipped: Optional[OnSkipped] = None, + on_started: Optional[OnStarted] = None, + on_ready: Optional[OnReady] = None, + on_retry: Optional[OnRetry] = None, + on_failed: Optional[OnFailed] = None, + checking_interval: Union[int, float] = 0, + retry: int = 2, + retry_interval: Union[float, int] = 1, + should_execute: Union[bool, str, Callable[..., bool]] = True + ): + BaseTask.__init__( + self, + name=name, + group=group, + inputs=inputs, + envs=envs, + env_files=env_files, + icon=icon, + color=color, + description=description, + upstreams=upstreams, + on_triggered=on_triggered, + on_waiting=on_waiting, + on_skipped=on_skipped, + on_started=on_started, + on_ready=on_ready, + on_retry=on_retry, + on_failed=on_failed, + checkers=[], + checking_interval=checking_interval, + retry=retry, + retry_interval=retry_interval, + should_execute=should_execute, + ) + self._title = title if title != '' else name + self._message = message if message != '' else get_random_icon() + + async def run(self, *args: Any, **kwargs: Any) -> str: + title = self.render_str(self._title) + message = self.render_str(self._message) + notify_kwargs = { + **kwargs, + 'title': title, + 'message': message, + } + await self.notify(*args, **notify_kwargs) + return message + + async def notify(self, *args: Any, **kwargs: Any) -> None: + title = kwargs.get('title') + message = kwargs.get('message') + task: BaseTask = kwargs.get('_task') + if _is_powershell_available(): + cmd = [ + 'powershell.exe', + '-ExecutionPolicy', 'Bypass', + '-File', NOTIFY_PS1_PATH, + '-Title', title, + '-Message', message + ] + subprocess.run(cmd, stdout=subprocess.DEVNULL) + if _is_osascript_available(): + q_message = double_quote(message) + q_title = double_quote(title) + cmd = [ + 'osascript', + '-e', + f'display notification "{q_message}" with title "{q_title}"' + ] + if _is_notify_send_available(): + cmd = ['notify-send', title, message] + subprocess.run(cmd, stdout=subprocess.DEVNULL) + task.print_out(message) + task._play_bell() + + def __repr__(self) -> str: + return f'' + + +def _is_powershell_available(): + try: + subprocess.run( + ['powershell.exe', '-Command', 'echo "Checking PowerShell"'], + check=True, stdout=subprocess.DEVNULL, + stderr=subprocess.DEVNULL + ) + return True + except (subprocess.CalledProcessError, FileNotFoundError, PermissionError): + return False + + +def _is_notify_send_available(): + try: + subprocess.run( + ['notify-send', '--version'], + check=True, + stdout=subprocess.DEVNULL, + stderr=subprocess.DEVNULL + ) + return True + except (subprocess.CalledProcessError, FileNotFoundError, PermissionError): + return False + + +def _is_osascript_available(): + try: + subprocess.run( + ['osascript', '-e', 'return'], + check=True, + stdout=subprocess.DEVNULL, + stderr=subprocess.DEVNULL + ) + return True + except (subprocess.CalledProcessError, FileNotFoundError, PermissionError): + return False diff --git a/src/zrb/task/recurring_task.py b/src/zrb/task/recurring_task.py index 7d6db6b8..7e0bb987 100644 --- a/src/zrb/task/recurring_task.py +++ b/src/zrb/task/recurring_task.py @@ -46,7 +46,7 @@ def __init__( should_execute: Union[bool, str, Callable[..., bool]] = True, return_upstream_result: bool = False ): - inputs = list(inputs) + task._get_inputs() + inputs = list(inputs) + task._get_combined_inputs() envs = list(envs) + task._get_envs() env_files = list(env_files) + task._get_env_files() BaseTask.__init__( @@ -137,3 +137,6 @@ async def __run_and_play_bell( ): await fn(*args, **kwargs) self._play_bell() + + def __repr__(self) -> str: + return f'' diff --git a/src/zrb/task/time_watcher.py b/src/zrb/task/time_watcher.py index 0cf76c9d..22ac2b77 100644 --- a/src/zrb/task/time_watcher.py +++ b/src/zrb/task/time_watcher.py @@ -108,4 +108,4 @@ async def inspect(self, *args: Any, **kwargs: Any) -> bool: return False def __repr__(self) -> str: - return f'' + return f'' From 63ac2f8962f3b6fdb0393da969ff7aa7118440bd Mon Sep 17 00:00:00 2001 From: goFrendiAsgard Date: Sat, 2 Dec 2023 21:21:40 +0700 Subject: [PATCH 08/11] update notify signature --- src/zrb/builtin/helper/reccuring_action.py | 14 +++++++--- src/zrb/builtin/schedule.py | 14 ++++++++-- .../builtin/{watch.py => watch_changes.py} | 17 ++++++++---- src/zrb/shell-scripts/notify.ps1 | 2 +- src/zrb/task/notifier.py | 26 ++++++++++--------- src/zrb/task/time_watcher.py | 2 +- 6 files changed, 51 insertions(+), 24 deletions(-) rename src/zrb/builtin/{watch.py => watch_changes.py} (61%) diff --git a/src/zrb/builtin/helper/reccuring_action.py b/src/zrb/builtin/helper/reccuring_action.py index 97ebfdf3..ff9f9319 100644 --- a/src/zrb/builtin/helper/reccuring_action.py +++ b/src/zrb/builtin/helper/reccuring_action.py @@ -4,11 +4,14 @@ from zrb.task_input.str_input import StrInput -def create_recurring_action(title: str) -> Task: +def create_recurring_action( + title: str, + default_message: str = '👋' +) -> Task: # define inputs message_input = StrInput( name='message', - default='👋', + default=default_message, prompt='Message to be shown', ) command_input = StrInput( @@ -19,11 +22,16 @@ def create_recurring_action(title: str) -> Task: # define tasks run_command = CmdTask( name='run-command', + icon='⚙️', + color='blue', inputs=[command_input], - should_execute='{{ input.command != "" }}' + should_execute='{{ input.command != "" }}', + cmd='{{ input.command }}' ) notify = Notifier( name='notify', + icon='📢', + color='green', inputs=[message_input], title=title, message='{{ input.message }}', diff --git a/src/zrb/builtin/schedule.py b/src/zrb/builtin/schedule.py index 0857c0ed..59f635fd 100644 --- a/src/zrb/builtin/schedule.py +++ b/src/zrb/builtin/schedule.py @@ -7,6 +7,8 @@ schedule = RecurringTask( name='schedule', + icon='📅', + color='yellow', description='Show message/run command periodically', inputs=[ StrInput( @@ -17,8 +19,16 @@ ), ], triggers=[ - TimeWatcher(name='watch-schedule', schedule='{{input.schedule}}') + TimeWatcher( + name='watch-schedule', + color='cyan', + icon='⏰', + schedule='{{input.schedule}}' + ) ], - task=create_recurring_action(title='schedule') + task=create_recurring_action( + title='Schedule', + default_message='{{datetime.datetime.now().strftime("%Y-%m-%d %H:%M:%S")}}', # noqa + ) ) runner.register(schedule) diff --git a/src/zrb/builtin/watch.py b/src/zrb/builtin/watch_changes.py similarity index 61% rename from src/zrb/builtin/watch.py rename to src/zrb/builtin/watch_changes.py index 408da46b..4a8470eb 100644 --- a/src/zrb/builtin/watch.py +++ b/src/zrb/builtin/watch_changes.py @@ -5,8 +5,10 @@ from zrb.builtin.helper.reccuring_action import create_recurring_action -watch = RecurringTask( - name='watch', +watch_changes = RecurringTask( + name='watch-changes', + icon='🕵️', + color='yellow', description='Watch changes and show message/run command', inputs=[ StrInput( @@ -17,8 +19,13 @@ ), ], triggers=[ - PathWatcher(name='watch-path', path='{{input.pattern}}') + PathWatcher( + name='watch-path', + color='cyan', + icon='👀', + path='{{input.pattern}}' + ) ], - task=create_recurring_action(title='schedule') + task=create_recurring_action(title='Watch') ) -runner.register(watch) +runner.register(watch_changes) diff --git a/src/zrb/shell-scripts/notify.ps1 b/src/zrb/shell-scripts/notify.ps1 index d54fadcc..8acc7419 100644 --- a/src/zrb/shell-scripts/notify.ps1 +++ b/src/zrb/shell-scripts/notify.ps1 @@ -12,5 +12,5 @@ $toastTextElements[1].AppendChild($toastXml.CreateTextNode($Message)) # Message $xml = New-Object Windows.Data.Xml.Dom.XmlDocument $xml.LoadXml($toastXml.OuterXml) $newToast = [Windows.UI.Notifications.ToastNotification]::new($xml) -$toastNotifier = [Windows.UI.Notifications.ToastNotificationManager]::CreateToastNotifier("WSL") +$toastNotifier = [Windows.UI.Notifications.ToastNotificationManager]::CreateToastNotifier("Zrb") $toastNotifier.Show($newToast) \ No newline at end of file diff --git a/src/zrb/task/notifier.py b/src/zrb/task/notifier.py index 8ae35569..796040d2 100644 --- a/src/zrb/task/notifier.py +++ b/src/zrb/task/notifier.py @@ -35,6 +35,8 @@ def __init__( description: str = '', title: str = '', message: str = '', + show_toast: bool = True, + show_stdout: bool = True, upstreams: Iterable[AnyTask] = [], on_triggered: Optional[OnTriggered] = None, on_waiting: Optional[OnWaiting] = None, @@ -74,23 +76,22 @@ def __init__( ) self._title = title if title != '' else name self._message = message if message != '' else get_random_icon() + self._show_toast = show_toast + self._show_stdout = show_stdout async def run(self, *args: Any, **kwargs: Any) -> str: title = self.render_str(self._title) message = self.render_str(self._message) notify_kwargs = { - **kwargs, - 'title': title, - 'message': message, + key: value + for key, value in kwargs.items() if key not in ('title', 'message') } - await self.notify(*args, **notify_kwargs) + await self.notify(title, message, **notify_kwargs) return message - async def notify(self, *args: Any, **kwargs: Any) -> None: - title = kwargs.get('title') - message = kwargs.get('message') + async def notify(self, title: str, message: str, **kwargs: Any) -> None: task: BaseTask = kwargs.get('_task') - if _is_powershell_available(): + if self._show_toast and _is_powershell_available(): cmd = [ 'powershell.exe', '-ExecutionPolicy', 'Bypass', @@ -99,7 +100,7 @@ async def notify(self, *args: Any, **kwargs: Any) -> None: '-Message', message ] subprocess.run(cmd, stdout=subprocess.DEVNULL) - if _is_osascript_available(): + if self._show_toast and _is_osascript_available(): q_message = double_quote(message) q_title = double_quote(title) cmd = [ @@ -107,11 +108,12 @@ async def notify(self, *args: Any, **kwargs: Any) -> None: '-e', f'display notification "{q_message}" with title "{q_title}"' ] - if _is_notify_send_available(): + if self._show_toast and _is_notify_send_available(): cmd = ['notify-send', title, message] subprocess.run(cmd, stdout=subprocess.DEVNULL) - task.print_out(message) - task._play_bell() + if self._show_stdout: + task.print_out(message) + task._play_bell() def __repr__(self) -> str: return f'' diff --git a/src/zrb/task/time_watcher.py b/src/zrb/task/time_watcher.py index 22ac2b77..9d86aad3 100644 --- a/src/zrb/task/time_watcher.py +++ b/src/zrb/task/time_watcher.py @@ -40,7 +40,7 @@ def __init__( on_retry: Optional[OnRetry] = None, on_failed: Optional[OnFailed] = None, schedule: str = '', - checking_interval: Union[int, float] = 0.1, + checking_interval: Union[int, float] = 1, progress_interval: Union[int, float] = 30, should_execute: Union[bool, str, Callable[..., bool]] = True ): From 211ce1b68fe9f724cfca4b6e55b9d436400dfe35 Mon Sep 17 00:00:00 2001 From: goFrendiAsgard Date: Sat, 2 Dec 2023 21:25:56 +0700 Subject: [PATCH 09/11] update signature --- src/zrb/builtin/__init__.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/zrb/builtin/__init__.py b/src/zrb/builtin/__init__.py index f09c2397..e581013e 100644 --- a/src/zrb/builtin/__init__.py +++ b/src/zrb/builtin/__init__.py @@ -12,7 +12,7 @@ from zrb.builtin import generator from zrb.builtin import process from zrb.builtin import say -from zrb.builtin import watch +from zrb.builtin import watch_changes from zrb.builtin import schedule assert base64 @@ -29,5 +29,5 @@ assert generator assert process assert say -assert watch +assert watch_changes assert schedule From 48638b27493c8708db4b722196887447c440dba3 Mon Sep 17 00:00:00 2001 From: goFrendiAsgard Date: Sun, 3 Dec 2023 08:09:36 +0700 Subject: [PATCH 10/11] update docs --- docs/faq/README.md | 2 +- docs/faq/does-zrb-has-a-scheduler.md | 20 ---------- docs/faq/does-zrb-have-a-scheduler.md | 20 ++++++++++ docs/tutorials/running-task-by-schedule.md | 46 +++++++++++++++++++++- 4 files changed, 65 insertions(+), 23 deletions(-) delete mode 100644 docs/faq/does-zrb-has-a-scheduler.md create mode 100644 docs/faq/does-zrb-have-a-scheduler.md diff --git a/docs/faq/README.md b/docs/faq/README.md index b07cfd73..47a54555 100644 --- a/docs/faq/README.md +++ b/docs/faq/README.md @@ -3,7 +3,7 @@ # FAQ - [Why Python](why-python.md) -- [Does Zrb has A Scheduler](does-zrb-has-a-scheduler.md) +- [Does Zrb have a scheduler](does-zrb-have-a-scheduler.md) - [How to get data from other tasks](how-to-get-data-from-other-tasks.md) 🔖 [Table of Contents](../README.md) diff --git a/docs/faq/does-zrb-has-a-scheduler.md b/docs/faq/does-zrb-has-a-scheduler.md deleted file mode 100644 index 24d37e79..00000000 --- a/docs/faq/does-zrb-has-a-scheduler.md +++ /dev/null @@ -1,20 +0,0 @@ -🔖 [Table of Contents](../README.md) / [FAQ](README.md) - -# Do Zrb has a Scheduler? - -No. Zrb focus is to help you run complicated tasks in a single run. You will need third-party alternatives to make your tasks run by schedule. - -# Why Is No Scheduler? - -Implementing a Scheduler seems to be easy at the first glance. - -However, there are a few reasons why we don't build our own internal scheduler: - -- There are a lot of battle-proven tools and strategy you can already use. -- Maintaining internal scheduler might distract us from the main goal. - -# What Can I Do to Make a Scheduled Task? - -Don't worry, there are some [tricks](../tutorials/running-task-by-schedule.md) you can use. For example you can use infinite loop, Cronjob, or even orchestrator like Airflow. - -🔖 [Table of Contents](../README.md) / [FAQ](README.md) diff --git a/docs/faq/does-zrb-have-a-scheduler.md b/docs/faq/does-zrb-have-a-scheduler.md new file mode 100644 index 00000000..fa640f37 --- /dev/null +++ b/docs/faq/does-zrb-have-a-scheduler.md @@ -0,0 +1,20 @@ +🔖 [Table of Contents](../README.md) / [FAQ](README.md) + + +# Does Zrb Have a Scheduler? + +No, but you can use `RecurringTask` and `TimeWatcher` + + +# How To Make A Scheduled Task? + +There are some [tricks](../tutorials/running-task-by-schedule.md) you can use. + +For example, you can use: + +- RecurringTask +- Infinite loop +- Cronjob +- Orchestrator like Airflow. + +🔖 [Table of Contents](../README.md) / [FAQ](README.md) diff --git a/docs/tutorials/running-task-by-schedule.md b/docs/tutorials/running-task-by-schedule.md index b836f711..0342713b 100644 --- a/docs/tutorials/running-task-by-schedule.md +++ b/docs/tutorials/running-task-by-schedule.md @@ -1,16 +1,57 @@ 🔖 [Table of Contents](../README.md) / [Tutorials](README.md) -# Run Task by Schedule +# Running Task by Schedule Zrb doesn't have any built-in scheduler. However, there are some workarounds you can use: +- Using RecurringTask - Creating an infinite loop - Using Cronjob - Using Airflow or other orchestrator +# Using RecurringTask + +The best approach is by turning your task into a recurring task. + +```python +from zrb import CmdTask, TimeWatcher, RecurringTask, runner + +# Your original task +hello = CmdTask( + name='hello', + cmd='echo "hello world"' +) +runner.register(hello) + + +# Your recurring task +scheduled_hello = RecurringTask( + name='scheduled-hello', + inputs=[ + StrInput(name='schedule', default='* * * * *') + ], + triggers=[ + TimeWatcher(schedule='{{input.schedule}}') + ], + task=hello +) +runner.register(scheduled_hello) +``` + +Notice that `TimeWatcher`'s `schedule` is a [cron schedule expression](https://crontab.guru/). + +The expression `* * * * *` means that the task will be executed every minute. + +To execute `scheduled-hello`, you can invoke: + +```bash +zrb scheduled-hello +``` + + # Creating an Infinite Loop -The simplest approach is by turning your task into a function, and call your function after some interval. +Another simple approach is by turning your task into a function, and call your function after some interval. For example, you want to run `hello` task every 5 seconds. Then you can create the following Python script and run it. @@ -38,6 +79,7 @@ while True: python scheduled_hello.py ``` + # Using CronJob From 55189a4763d64d286a6efda0ba0fd32291f3f5ca Mon Sep 17 00:00:00 2001 From: goFrendiAsgard Date: Sun, 3 Dec 2023 08:11:11 +0700 Subject: [PATCH 11/11] change description --- pyproject.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pyproject.toml b/pyproject.toml index e29800c1..7ddd7ae6 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -8,7 +8,7 @@ version = "0.0.119" authors = [ { name="Go Frendi Gunawan", email="gofrendiasgard@gmail.com" }, ] -description = "Super framework for your super app" +description = "A Framework to Enhance Your Workflow" readme = "README.md" requires-python = ">=3.10.0" classifiers = [