Repository: ManiMozaffar/aioclock
Branch: main
Commit: c943c853ac9f
Files: 50
Total size: 220.9 KB
Directory structure:
gitextract_w_f8cus0/
├── .github/
│ └── workflows/
│ ├── deploy-docs-on-demand.yml
│ ├── main.yml
│ └── on-release-main.yml
├── .gitignore
├── .python-version
├── LICENSE
├── Makefile
├── README.md
├── aioclock/
│ ├── __init__.py
│ ├── api.py
│ ├── app.py
│ ├── custom_types.py
│ ├── exceptions.py
│ ├── ext/
│ │ ├── __init__.py
│ │ └── fast.py
│ ├── group.py
│ ├── logger.py
│ ├── provider.py
│ ├── py.typed
│ ├── task.py
│ ├── triggers.py
│ └── utils.py
├── deploy_docs.py
├── docs/
│ ├── alternative.md
│ ├── api/
│ │ ├── external_api.md
│ │ ├── getting_started.md
│ │ ├── plugin.md
│ │ ├── task.md
│ │ └── triggers.md
│ ├── diagrams/
│ │ └── aioclock.excalidraw
│ ├── examples/
│ │ ├── brokers.md
│ │ └── fastapi.md
│ ├── extra/
│ │ └── tweaks.css
│ ├── images/
│ │ └── README.md
│ ├── index.md
│ ├── overview.md
│ └── plugins.py
├── examples/
│ ├── app.py
│ ├── awesome_triggers.py
│ ├── dependency_injection.py
│ └── with_fast_api.py
├── mkdocs.yml
├── pyproject.toml
├── tests/
│ ├── __init__.py
│ ├── test_di.py
│ ├── test_examples.py
│ ├── test_lifespan.py
│ ├── test_timeout.py
│ └── test_triggers.py
└── tox.ini
================================================
FILE CONTENTS
================================================
================================================
FILE: .github/workflows/deploy-docs-on-demand.yml
================================================
name: Deploy Docs On Demand
on:
workflow_dispatch:
jobs:
deploy-docs-on-demand:
runs-on: ubuntu-latest
steps:
- name: Check out
uses: actions/checkout@v3
- name: Set up the environment
uses: ./.github/actions/setup-poetry-env
- name: Install the latest version of rye
uses: eifinger/setup-rye@v2
- name: Install dependencies
run: make install
- name: Deploy documentation
run: rye run python deploy_docs.py
================================================
FILE: .github/workflows/main.yml
================================================
name: Main
on:
push:
branches:
- main
pull_request:
types: [opened, synchronize, reopened]
jobs:
tox:
runs-on: ubuntu-latest
strategy:
matrix:
python-version: ["3.9", "3.10", "3.11"]
fail-fast: false
steps:
- name: Check out
uses: actions/checkout@v3
- name: Set up python
uses: actions/setup-python@v4
with:
python-version: ${{ matrix.python-version }}
- name: Install the latest version of rye
uses: eifinger/setup-rye@v2
- name: Pin python-version ${{ matrix.python-version }}
run: rye pin ${{ matrix.python-version }}
- name: Install dependencies
run: make install
- name: Run tests
run: make test
- name: Run check
run: make check
================================================
FILE: .github/workflows/on-release-main.yml
================================================
name: release-main
on:
release:
types: [published]
branches: [main]
jobs:
publish:
runs-on: ubuntu-latest
steps:
- name: Check out
uses: actions/checkout@v3
- name: Export tag
id: vars
run: echo tag=${GITHUB_REF#refs/*/} >> $GITHUB_OUTPUT
- name: Set up python
uses: actions/setup-python@v4
with:
python-version: ${{ matrix.python-version }}
- name: Install the latest version of rye
uses: eifinger/setup-rye@v2
# https://github.com/astral-sh/rye/issues/1180
- name: Patch Rye
run: |
echo "Patching Rye with Twine 5.1.1"
$RYE_HOME/self/bin/pip install twine==5.1.1
- name: Build and publish
run: |
rye build
rye publish --token $PYPI_TOKEN --yes
env:
PYPI_TOKEN: ${{ secrets.PYPI_TOKEN }}
RELEASE_VERSION: ${{ steps.vars.outputs.tag }}
================================================
FILE: .gitignore
================================================
docs/source
# From https://raw.githubusercontent.com/github/gitignore/main/Python.gitignore
# Byte-compiled / optimized / DLL files
__pycache__/
*.py[cod]
*$py.class
.DS_Store
# C extensions
*.so
# Distribution / packaging
.Python
build/
develop-eggs/
dist/
downloads/
eggs/
.eggs/
lib/
lib64/
parts/
sdist/
var/
wheels/
share/python-wheels/
*.egg-info/
.installed.cfg
*.egg
MANIFEST
# PyInstaller
# Usually these files are written by a python script from a template
# before PyInstaller builds the exe, so as to inject date/other infos into it.
*.manifest
*.spec
# Installer logs
pip-log.txt
pip-delete-this-directory.txt
# Unit test / coverage reports
htmlcov/
.tox/
.nox/
.coverage
.coverage.*
.cache
nosetests.xml
coverage.xml
*.cover
*.py,cover
.hypothesis/
.pytest_cache/
cover/
# Translations
*.mo
*.pot
# Django stuff:
*.log
local_settings.py
db.sqlite3
db.sqlite3-journal
# Flask stuff:
instance/
.webassets-cache
# Scrapy stuff:
.scrapy
# Sphinx documentation
docs/_build/
# PyBuilder
.pybuilder/
target/
# Jupyter Notebook
.ipynb_checkpoints
# IPython
profile_default/
ipython_config.py
# pyenv
# For a library or package, you might want to ignore these files since the code is
# intended to run in multiple environments; otherwise, check them in:
# .python-version
# pipenv
# According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control.
# However, in case of collaboration, if having platform-specific dependencies or dependencies
# having no cross-platform support, pipenv may install dependencies that don't work, or not
# install all needed dependencies.
#Pipfile.lock
# pdm
# Similar to Pipfile.lock, it is generally recommended to include pdm.lock in version control.
#pdm.lock
# pdm stores project-wide configurations in .pdm.toml, but it is recommended to not include it
# in version control.
# https://pdm.fming.dev/#use-with-ide
.pdm.toml
# PEP 582; used by e.g. github.com/David-OConnor/pyflow and github.com/pdm-project/pdm
__pypackages__/
# Celery stuff
celerybeat-schedule
celerybeat.pid
# SageMath parsed files
*.sage.py
# Environments
.env
.venv
env/
venv/
ENV/
env.bak/
venv.bak/
# Spyder project settings
.spyderproject
.spyproject
# Rope project settings
.ropeproject
# mkdocs documentation
/site
# mypy
.mypy_cache/
.dmypy.json
dmypy.json
# Pyre type checker
.pyre/
# pytype static type analyzer
.pytype/
# Cython debug symbols
cython_debug/
# Vscode config files
.vscode/
# PyCharm
# JetBrains specific template is maintained in a separate JetBrains.gitignore that can
# be found at https://github.com/github/gitignore/blob/main/Global/JetBrains.gitignore
# and can be added to the global gitignore or merged into this file. For a more nuclear
# option (not recommended) you can uncomment the following to ignore the entire idea folder.
.idea/
================================================
FILE: .python-version
================================================
3.11.9
================================================
FILE: LICENSE
================================================
MIT License
Copyright (c) 2024, Mani Mozaffar
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
================================================
FILE: Makefile
================================================
.PHONY: install
install: ## Install the rye environment
@echo "🚀 Creating virtual environment using rye and uv"
rye sync
.PHONY: check
check: ## Run the quality checks on the code
@echo "🚀 Running quality checks"
rye run ruff .
rye run pyright .
.PHONY: test
test: ## Test the code with pytest
@echo "🚀 Testing code: Running pytest"
rye run pytest
.PHONY: docs
docs: ## Build and serve the documentation
@echo "🚀 Testing documentation: Building and testing"
rye run mkdocs serve
.PHONY: deploy-docs
deploy-docs: ## Build and serve the documentation
@echo "🚀 Deploying documentation"
rye run python deploy_docs.py
.PHONY: docs-test
docs-test: ## Test if documentation can be built without warnings or errors
@rye run mkdocs build -s
.PHONY: help
help:
@grep -E '^[a-zA-Z_-]+:.*?## .*$$' $(MAKEFILE_LIST) | awk 'BEGIN {FS = ":.*?## "}; {printf "\033[36m%-20s\033[0m %s\n", $$1, $$2}'
.DEFAULT_GOAL := help
================================================
FILE: README.md
================================================
# aioclock
[](https://img.shields.io/github/v/release/ManiMozaffar/aioclock)
[](https://github.com/ManiMozaffar/aioclock/actions/workflows/main.yml?query=branch%3Amain)
[](https://img.shields.io/github/commit-activity/m/ManiMozaffar/aioclock)
[](https://img.shields.io/github/license/ManiMozaffar/aioclock)
An asyncio-based scheduling framework designed for execution of periodic task with integrated support for dependency injection, enabling efficient and flexiable task management
- **Github repository**:
## Features
Aioclock offers:
- Async: 100% Async, very light, fast and resource friendly
- Scheduling: Keep scheduling tasks for you
- Group: Group your task, for better code maintainability
- Trigger: Already defined and easily extendable triggers, to trigger your scheduler to be started
- Easy syntax: Library's syntax is very easy and enjoyable, no confusing hierarchy
- Pydantic v2 validation: Validate all your trigger on startup using pydantic 2. Fastest to fail possible!
- **Soon**: Running the task dispatcher (scheduler) on different process by default, so CPU intensive stuff on task won't delay the scheduling
- **Soon**: Backend support, to allow horizontal scalling, by synchronizing, maybe using Redis
## Getting started
To Install aioclock, simply do
```
pip install aioclock
```
## Help
See [documentation](https://ManiMozaffar.github.io/aioclock/) for more details.
## A Sample Example
```python
from collections.abc import AsyncGenerator
from contextlib import asynccontextmanager
import asyncio
from aioclock import AioClock, At, Depends, Every, Forever, Once
from aioclock.group import Group
# groups.py
group = Group()
def more_useless_than_me():
return "I'm a dependency. I'm more useless than a screen door on a submarine."
@group.task(trigger=Every(seconds=10))
async def every():
print("Every 10 seconds, I make a quantum leap. Where will I land next?")
@group.task(trigger=Every(seconds=5))
def even_sync_works():
print("I'm a synchronous task. I work even in async world.")
@group.task(trigger=At(tz="UTC", hour=0, minute=0, second=0))
async def at():
print(
"When the clock strikes midnight... I turn into a pumpkin. Just kidding, I run this task!"
)
@group.task(trigger=Forever())
async def forever(val: str = Depends(more_useless_than_me)):
await asyncio.sleep(2)
print("Heartbeat detected. Still not a zombie. Will check again in a bit.")
assert val == "I'm a dependency. I'm more useless than a screen door on a submarine."
@group.task(trigger=Once())
async def once():
print("Just once, I get to say something. Here it goes... I love lamp.")
@asynccontextmanager
async def lifespan(aio_clock: AioClock) -> AsyncGenerator[AioClock]:
# starting up
print(
"Welcome to the Async Chronicles! Did you know a group of unicorns is called a blessing? Well, now you do!"
)
yield aio_clock
# shuting down
print("Going offline. Remember, if your code is running, you better go catch it!")
# app.py
app = AioClock(lifespan=lifespan)
app.include_group(group)
# main.py
if __name__ == "__main__":
asyncio.run(app.serve())
```
================================================
FILE: aioclock/__init__.py
================================================
from fast_depends import Depends
from aioclock.app import AioClock
from aioclock.group import Group
from aioclock.triggers import At, Cron, Every, Forever, Once, OnShutDown, OnStartUp, OrTrigger
__all__ = [
"Depends",
"Once",
"OnStartUp",
"OnShutDown",
"Every",
"Forever",
"Group",
"AioClock",
"At",
"Cron",
"OrTrigger",
]
__version__ = "0.3.0"
================================================
FILE: aioclock/api.py
================================================
"""
External API of the aioclock package, that can be used to interact with the AioClock instance.
This module could be very useful if you intend to use aioclock in a web application or a CLI tool.
Other tools and extension are written from this tool.
!!! danger "Note when writing to aioclock API and changing its state."
Right now the state of AioClock instance is on the memory level,
so if you write an API and change a task's trigger time, it will not persist.
In the future, we might store the state of AioClock instance in a database, so that it always remains same.
But this is a bit tricky and implicit because then your code gets ignored
and database is preferred over the codebase.
For now, you may consider it as a way to change something without redeploying the application,
but it is not very recommended to write.
"""
import sys
from typing import Any, Awaitable, Callable, TypeVar, Union
from uuid import UUID
from fast_depends import inject
from pydantic import BaseModel
from aioclock.app import AioClock
from aioclock.exceptions import TaskIdNotFound
from aioclock.provider import get_provider
from aioclock.triggers import TriggerT
if sys.version_info < (3, 10):
from typing_extensions import ParamSpec
else:
from typing import ParamSpec
T = TypeVar("T")
P = ParamSpec("P")
class TaskMetadata(BaseModel):
"""Metadata of the task that is included in the AioClock instance.
Attributes:
id: UUID: Task ID that is unique for each task, and changes every time you run the aioclock app.
In the future, we might store task ID in a database, so that it always remains same.
trigger: Union[TriggerT, Any]: Trigger that is used to run the task,
type is also any to ease implementing new triggers.
task_name: str: Name of the task function.
"""
id: UUID
trigger: Union[TriggerT, Any]
task_name: str
async def run_specific_task(task_id: UUID, app: AioClock):
"""Run a specific task immediately by its ID, from the AioClock instance.
params:
task_id: Task ID that is unique for each task, and changes every time you run the aioclock app.
In the future, we might store task ID in a database, so that it always remains same.
app: AioClock instance to run the task from.
Example:
```python
from aioclock import AioClock, Once
from aioclock.api import run_specific_task
app = AioClock()
@app.task(trigger=Once())
async def main():
print("Hello World")
async def some_other_func():
await run_specific_task(app._tasks[0].id, app)
```
"""
task = next((task for task in app._tasks if task.id == task_id), None)
if not task:
raise TaskIdNotFound
return await run_with_injected_deps(task.func)
async def run_with_injected_deps(func: Callable[P, Awaitable[T]]) -> T:
"""Runs an aioclock decorated function, with all the dependencies injected.
Can be used to run a task function with all the dependencies injected.
params:
func: Function to run with all the dependencies injected. Must be decorated with `@app.task` decorator.
Example:
```python
from aioclock import Once, AioClock, Depends
from aioclock.api import run_with_injected_deps
app = AioClock()
def some_dependency():
return 1
@app.task(trigger=Once())
async def main(bar: int = Depends(some_dependency)):
print("Hello World")
return bar
async def some_other_func():
foo = await run_with_injected_deps(main)
assert foo == 1
```
"""
return await inject(func, dependency_overrides_provider=get_provider())() # type: ignore
async def get_metadata_of_all_tasks(app: AioClock) -> list[TaskMetadata]:
"""Get metadata of all tasks that are included in the AioClock instance.
This function can be used to mutate the `TaskMetadata` object, i.e. to change the trigger of a task.
But for now it is yet not recommended to do this, as you might experience some unexpected behavior.
But in next versions, I'd like to make it more stable and reliable on mutating the data.
params:
app: AioClock instance to get the metadata of all tasks.
Example:
```python
from aioclock import AioClock, Once
from aioclock.api import get_metadata_of_all_tasks
app = AioClock()
@app.task(trigger=Once())
async def main(): ...
async def some_other_func():
metadata = await get_metadata_of_all_tasks(app)
```
"""
return [
TaskMetadata(
id=task.id,
trigger=task.trigger,
task_name=task.func.__name__,
)
for task in app._get_tasks(exclude_type=set())
]
================================================
FILE: aioclock/app.py
================================================
"""
To initialize the AioClock instance, you need to import the AioClock class from the aioclock module.
AioClock class represent the aioclock, and handle the tasks and groups that will be run by the aioclock.
Another way to modularize your code is to use `Group` which is kinda the same idea as router in web frameworks.
"""
from __future__ import annotations
import asyncio
import sys
from functools import wraps
from typing import (
Any,
AsyncContextManager,
Callable,
ContextManager,
Optional,
TypeVar,
Union,
)
import anyio
if sys.version_info < (3, 10):
from typing_extensions import ParamSpec
else:
from typing import ParamSpec
if sys.version_info < (3, 11):
from typing_extensions import assert_never
else:
from typing import assert_never
from asyncer import asyncify
from fast_depends import inject
from aioclock.custom_types import Triggers
from aioclock.group import Group, Task
from aioclock.provider import get_provider
from aioclock.triggers import BaseTrigger
from aioclock.utils import flatten_chain
T = TypeVar("T")
P = ParamSpec("P")
class AioClock:
"""
AioClock is the main class that will be used to run the tasks.
It will be responsible for running the tasks in the right order.
Example:
```python
from aioclock import AioClock, Once
app = AioClock()
@app.task(trigger=Once())
async def main():
print("Hello World")
```
To run the aioclock final app simply do:
Example:
```python
from aioclock import AioClock, Once
import asyncio
app = AioClock()
# whatever next comes here
asyncio.run(app.serve())
```
## Lifespan
You can define this startup and shutdown logic using the lifespan parameter of the AioClock instance.
It should be as an AsyncContextManager which get AioClock application as argument.
You can find the example below.
Example:
```python
import asyncio
from contextlib import asynccontextmanager
from aioclock import AioClock
ML_MODEL = [] # just some imaginary component that needs to be started and stopped
@asynccontextmanager
async def lifespan(app: AioClock):
ML_MODEL.append(2)
print("UP!")
yield app
ML_MODEL.clear()
print("DOWN!")
app = AioClock(lifespan=lifespan)
if __name__ == "__main__":
asyncio.run(app.serve())
```
Here we are simulating the expensive startup operation of loading the model by putting the (fake)
model function in the dictionary with machine learning models before the yield.
This code will be executed before the application starts operating, during the startup.
And then, right after the yield, we unload the model.
This code will be executed after the application finishes handling requests, right before the shutdown.
This could, for example, release resources like memory, a GPU or some database connection.
It would also happen when you're stopping your application gracefully,
for example, when you're shutting down your container.
Lifespan could also be synchronous context manager. Check the example below.
Example:
```python
from contextlib import contextmanager
from aioclock import AioClock
ML_MODEL = []
@contextmanager
def lifespan_sync(sync_app: AioClock):
ML_MODEL.append(2)
print("UP!")
yield sync_app
ML_MODEL.clear()
print("DOWN!")
sync_app = AioClock(lifespan=lifespan_sync)
if __name__ == "__main__":
asyncio.run(app.serve())
```
"""
def __init__(
self,
*,
lifespan: Optional[
Callable[[AioClock], AsyncContextManager[AioClock] | ContextManager[AioClock]]
] = None,
limiter: Optional[anyio.CapacityLimiter] = None,
):
"""
Initialize AioClock instance.
No parameters are needed.
Attributes:
lifespan:
A context manager that will be used to handle the startup and shutdown of the application.
If not provided, the application will run without any startup and shutdown logic.
To understand it better, check the examples and documentation above.
limiter:
Anyio CapacityLimiter. capacity limiter to use to limit the total amount of threads running
Limiter that will be used to limit the number of tasks that are running at the same time.
If not provided, it will fall back to the default limiter set on Application level.
If no limiter is set on Application level, it will fall back to the default limiter set by AnyIO.
"""
self._groups: list[Group] = []
self._app_tasks: list[Task] = []
self._limiter = limiter
self.lifespan = lifespan
group = Group()
group._tasks = self._app_tasks
self.include_group(group)
_groups: list[Group]
"""List of groups that will be run by AioClock."""
_app_tasks: list[Task]
"""List of tasks that will be run by AioClock."""
@property
def dependencies(self):
"""Dependencies provider that will be used to inject dependencies in tasks."""
return get_provider()
def override_dependencies(
self, original: Callable[..., Any], override: Callable[..., Any]
) -> None:
"""Override a dependency with a new one.
params:
original:
Original dependency that will be overridden.
override:
New dependency that will override the original one.
Example:
```python
from aioclock import AioClock
def original_dependency():
return 1
def new_dependency():
return 2
app = AioClock()
app.override_dependencies(original=original_dependency, override=new_dependency)
```
"""
self.dependencies.override(original, override)
def include_group(self, group: Group) -> None:
"""Include a group of tasks that will be run by AioClock.
params:
group:
Group of tasks that will be run together.
Example:
```python
from aioclock import AioClock, Group, Once
app = AioClock()
group = Group()
@group.task(trigger=Once())
async def main():
print("Hello World")
app.include_group(group)
```
"""
self._groups.append(group)
return None
def task(self, *, trigger: BaseTrigger, timeout: Optional[float] = None):
"""
Decorator to add a task to the AioClock instance.
If decorated function is sync, aioclock will run it in a thread pool executor, using AnyIO.
But if you try to run the decorated function, it will run in the same thread, blocking the event loop.
It is intended to not change all your `sync functions` to coroutine functions,
and they can be used outside aioclock, if needed.
params:
trigger: BaseTrigger
Trigger that will trigger the task to be running.
timeout: float | None (defaults to None)
Set a timeout for the task.
If the task completion took longer than timeout,
it will be cancelled and a `TaskTimeoutError` be raised by the Application.
Example:
```python
from aioclock import AioClock, Once
app = AioClock()
@app.task(trigger=Once())
async def main():
print("Hello World")
```
Example:
```python
from aioclock import AioClock, Once
app = AioClock()
@app.task(trigger=Once(), timeout=3)
async def main():
await some_io_task()
```
"""
def decorator(func):
@wraps(func)
async def wrapped_function(*args, **kwargs):
if asyncio.iscoroutinefunction(func):
return await func(*args, **kwargs)
else: # run in threadpool to make sure it's not blocking the event loop
return await asyncify(func, limiter=self._limiter)(*args, **kwargs)
self._app_tasks.append(
Task(
func=inject(wrapped_function, dependency_overrides_provider=get_provider()),
trigger=trigger,
timeout=timeout,
)
)
return wrapped_function
return decorator
@property
def _tasks(self) -> list[Task]:
result = flatten_chain([group._tasks for group in self._groups])
return result
def _get_shutdown_task(self) -> list[Task]:
return [task for task in self._tasks if task.trigger.type_ == Triggers.ON_SHUT_DOWN]
def _get_startup_task(self) -> list[Task]:
return [task for task in self._tasks if task.trigger.type_ == Triggers.ON_START_UP]
def _get_tasks(self, exclude_type: Union[set[Triggers], None] = None) -> list[Task]:
exclude_type = (
exclude_type
if exclude_type is not None
else {Triggers.ON_START_UP, Triggers.ON_SHUT_DOWN}
)
return [task for task in self._tasks if task.trigger.type_ not in exclude_type]
async def serve(self) -> None:
"""
Serves AioClock
Run the tasks in the right order.
First, run the startup tasks, then run the tasks, and finally run the shutdown tasks.
"""
if self.lifespan is None:
await self._run_tasks()
return
ctx = self.lifespan(self)
if isinstance(ctx, AsyncContextManager):
async with ctx:
await self._run_tasks()
elif isinstance(ctx, ContextManager):
with ctx:
await self._run_tasks()
else:
assert_never(ctx)
async def _run_tasks(self) -> None:
try:
await asyncio.gather(
*(task.run() for task in self._get_startup_task()), return_exceptions=False
)
await asyncio.gather(
*(task.run() for task in self._get_tasks()), return_exceptions=False
)
finally:
shutdown_tasks = self._get_shutdown_task()
await asyncio.gather(*(task.run() for task in shutdown_tasks), return_exceptions=False)
================================================
FILE: aioclock/custom_types.py
================================================
from enum import auto
from typing import Annotated, Literal, Union
from annotated_types import Interval
from aioclock.utils import StrEnum
EveryT = Literal[
"every monday",
"every tuesday",
"every wednesday",
"every thursday",
"every friday",
"every saturday",
"every sunday",
"every day",
]
SecondT = Annotated[int, Interval(ge=0, le=59)]
MinuteT = Annotated[int, Interval(ge=0, le=59)]
HourT = Annotated[int, Interval(ge=0, le=24)]
PositiveNumber = Annotated[Union[int, float], Interval(ge=0)]
class Triggers(StrEnum):
CRON = auto()
"""Cron job trigger."""
EVERY = auto()
"""Every (x) time units, it gets triggered."""
ONCE = auto()
"""Trigger once, then stop."""
FOREVER = auto()
"""Keep running, as long as application is running."""
ON_START_UP = auto()
"""Trigger on application start up."""
ON_SHUT_DOWN = auto()
"""Trigger on application shut down."""
AT = auto()
"""Trigger at a specific time."""
OR = auto()
"""Trigger when any of the triggers are met."""
================================================
FILE: aioclock/exceptions.py
================================================
class BaseAioClockException(Exception):
"""Base exception for aioclock."""
class TaskIdNotFound(BaseAioClockException):
"""Task not found in the AioClock app."""
class TaskTimeoutError(BaseAioClockException, TimeoutError):
"""A task took longer than its timeout"""
================================================
FILE: aioclock/ext/__init__.py
================================================
"""
Extensions for aioclock.
AioClock is very extensible, and you can add your own extensions to it.
The extension would allow you to interact with your AioClock instance, from different layers of your application.
For instance, the FastAPI plugin allows you to run a specific task immediately from an HTTP API, or see your tasks in an HTTP API, and when they are going to run next.
"""
================================================
FILE: aioclock/ext/fast.py
================================================
"""FastAPI extension to manage the tasks of the AioClock instance in HTTP Layer.
Use cases:
- Expose the tasks of the AioClock instance in an HTTP API.
- Show to your client which task is going to be run next, and at which time.
- Run a specific task from an HTTP API immidiately if needed.
To use FastAPI Extension, please make sure you do `pip install aioclock[fastapi]`.
"""
from typing import Union
from uuid import UUID
from aioclock.api import TaskMetadata, get_metadata_of_all_tasks, run_specific_task
from aioclock.app import AioClock
from aioclock.exceptions import TaskIdNotFound
try:
from fastapi.exceptions import HTTPException
from fastapi.routing import APIRouter
except ImportError:
raise ImportError(
"You need to install fastapi to use aioclock with FastAPI. Please run `pip install aioclock[fastapi]`"
)
def make_fastapi_router(aioclock: AioClock, router: Union[APIRouter, None] = None):
"""Make a FastAPI router that exposes the tasks of the AioClock instance and its external python API in HTTP Layer.
You can pass a router to this function, and have dependencies injected in the router, or any authorization logic that you want to have.
params:
aioclock: AioClock instance to get the tasks from.
router: FastAPI router to add the routes to. If not provided, a new router will be created.
Example:
```python
import asyncio
from contextlib import asynccontextmanager
from fastapi import FastAPI
from aioclock import AioClock
from aioclock.ext.fast import make_fastapi_router
from aioclock.triggers import Every, OnStartUp
clock_app = AioClock()
@clock_app.task(trigger=OnStartUp())
async def startup():
print("Starting...")
@clock_app.task(trigger=Every(seconds=3600))
async def foo():
print("Foo is processing...")
@asynccontextmanager
async def lifespan(app: FastAPI):
task = asyncio.create_task(clock_app.serve())
yield
try:
task.cancel()
await task
except asyncio.CancelledError:
...
app = FastAPI(lifespan=lifespan)
app.include_router(make_fastapi_router(clock_app))
if __name__ == "__main__":
import uvicorn
# uvicorn.run(app)
```
"""
router = router or APIRouter()
@router.get("/tasks")
async def get_tasks() -> list[TaskMetadata]:
return await get_metadata_of_all_tasks(aioclock)
@router.post("/task/{task_id}")
async def run_task(task_id: UUID):
try:
await run_specific_task(task_id, aioclock)
except TaskIdNotFound:
raise HTTPException(status_code=404, detail="Task not found")
return router
================================================
FILE: aioclock/group.py
================================================
import asyncio
import sys
from functools import wraps
from typing import Optional, TypeVar
from asyncer import asyncify
if sys.version_info < (3, 10):
from typing_extensions import ParamSpec
else:
from typing import ParamSpec
import anyio
from fast_depends import inject
from aioclock.provider import get_provider
from aioclock.task import Task
from aioclock.triggers import BaseTrigger
T = TypeVar("T")
P = ParamSpec("P")
class Group:
def __init__(
self,
*,
limiter: Optional[anyio.CapacityLimiter] = None,
timeout: Optional[float] = None
):
"""
Group of tasks that will be run together.
Best use case is to have a good modularity and separation of concerns.
For example, you can have a group of tasks that are responsible for sending emails.
And another group of tasks that are responsible for sending notifications.
Params:
limiter:
Anyio CapacityLimiter. capacity limiter to use to limit the total amount of threads running
Limiter that will be used to limit the number of tasks that are running at the same time.
If not provided, it will fall back to the default limiter set on Application level.
If no limiter is set on Application level, it will fall back to the default limiter set by AnyIO.
timeout:
General timeout for the group's tasks.
If a task overrides this value, the new value will be used
for the task.
Example:
```python
from aioclock import Group, AioClock, Forever
email_group = Group()
# consider this as different file
@email_group.task(trigger=Forever())
async def send_email():
...
# app.py
aio_clock = AioClock()
aio_clock.include_group(email_group)
```
Example:
```python
from aioclock import Group, AioClock, Forever
email_group = Group(timeout=5)
# consider this as different file
@email_group.task(trigger=Forever())
async def send_email():
...
# app.py
aio_clock = AioClock()
aio_clock.include_group(email_group)
```
"""
self._tasks: list[Task] = []
self._limiter = limiter
self._timeout = timeout
def task(self, *, trigger: BaseTrigger, timeout: Optional[float] = None):
"""
Decorator to add a task to the group.
If decorated function is sync, aioclock will run it in a thread pool executor, using AnyIO.
But if you try to run the decorated function, it will run in the same thread, blocking the event loop.
It is intended to not change all your `sync functions` to coroutine functions,
and they can be used outside aioclock, if needed.
params:
trigger: BaseTrigger
Trigger that will trigger the task to be running.
timeout: float | None (defaults to None)
Set a timeout for the task.
If the task completion took longer than timeout,
it will be cancelled and a `TaskTimeoutError` be raised by the Application.
Example:
```python
from aioclock import AioClock, Group, Once
group = Group()
@group.task(trigger=Once())
async def main():
print("Hello World")
app = AioClock()
app.include_group(group)
```
Example:
```python
from aioclock import AioClock, Group, Once, Every
group = Group(timeout=5)
@group.task(trigger=Every(seconds=5))
async def main():
print("Hello World")
@group.task(trigger=Once(), timeout=4) # this task will get 4 as timeout
async def main():
print("Hello World")
app = AioClock()
app.include_group(group)
```
"""
def decorator(func):
@wraps(func)
async def wrapped_function(*args, **kwargs):
if asyncio.iscoroutinefunction(func):
return await func(*args, **kwargs)
else: # run in threadpool to make sure it's not blocking the event loop
return await asyncify(func, limiter=self._limiter)(*args, **kwargs)
to = self._timeout
if timeout is not None:
to = timeout
self._tasks.append(
Task(
func=inject(wrapped_function, dependency_overrides_provider=get_provider()),
trigger=trigger,
timeout=to,
)
)
return wrapped_function
return decorator
async def _run(self):
"""
Just for purpose of being able to run all task in group
Private method, should not be used outside the library
"""
await asyncio.gather(
*(task.run() for task in self._tasks),
return_exceptions=False,
)
================================================
FILE: aioclock/logger.py
================================================
import logging
logger = logging.getLogger("aioclock")
================================================
FILE: aioclock/provider.py
================================================
from functools import lru_cache
from fast_depends import Provider
@lru_cache
def get_provider():
"""Return a Provider instance, which is singleton.
This singleton is used to inject dependencies in tasks.
"""
return Provider()
================================================
FILE: aioclock/py.typed
================================================
================================================
FILE: aioclock/task.py
================================================
"""
Aioclock wrap your functions with a task object,
and append the task to the list of tasks in the AioClock instance.
After collecting all the tasks from decorated functions,
aioclock serve them in order it has to be (startup, normal, shutdown).
These tasks keep running forever until the trigger's method `should_trigger` returns False.
"""
import asyncio
from dataclasses import dataclass, field
from datetime import datetime, timedelta, timezone
from typing import Any, Awaitable, Callable, Optional
from uuid import UUID, uuid4
from aioclock.exceptions import TaskTimeoutError
from aioclock.logger import logger
from aioclock.triggers import BaseTrigger
UTC = timezone.utc
@dataclass
class Task:
"""Task that will be run by AioClock.
Which always has a function and a trigger.
This is internally used, when you decorate your function with `aioclock.task`.
Attributes:
func: Callable[..., Awaitable[Any]]: Decorated function that will be run by AioClock.
trigger: BaseTrigger: Trigger that will be used to run the function.
id: UUID: Task ID that is unique for each task, and changes every time you run the aioclock app.
In the future, we might store task ID in a database, so that it always remains same.
"""
func: Callable[..., Awaitable[Any]]
trigger: BaseTrigger
timeout: Optional[float] = None
id: UUID = field(default_factory=uuid4)
async def run(self):
"""
Run the task, and handle the exceptions.
If the task fails, log the error with exception, but keep running the tasks.
"""
while self.trigger.should_trigger():
try:
next_trigger = await self.trigger.get_waiting_time_till_next_trigger()
if next_trigger is not None:
logger.info(f"Triggering next task {self.func.__name__} in {next_trigger}")
self.trigger.expected_trigger_time = datetime.now(UTC) + timedelta(
seconds=next_trigger
)
await self.trigger.trigger_next()
if self.timeout is not None:
logger.debug(
f"Running task {self.func.__name__} with timeout of {self.timeout}"
)
try:
await asyncio.wait_for(self.func(), self.timeout)
except asyncio.TimeoutError:
raise TaskTimeoutError(
f"Task {self.func.__name__!r} took longer than {self.timeout} seconds to run!"
) from None
else:
logger.debug(f"Running task {self.func.__name__}")
await self.func()
except Exception as error:
# Log the error, but keep running the tasks.
# don't crash the whole application.
logger.exception(f"Error running task {self.func.__name__}: {error}")
self.trigger.expected_trigger_time = None
================================================
FILE: aioclock/triggers.py
================================================
"""
Triggers are used to determine when the event should be triggered. It can be based on time, or some other condition.
You can create custom triggers by inheriting from `BaseTrigger` class.
!!! info "Don't run CPU intensitve or thread-block IO task "
AioClock's trigger are all running in async, only on one CPU.
So, if you run a CPU intensive task, or a task that blocks the thread, then it will block the entire event loop.
If you have a sync IO task, then it's recommended to use `run_in_executor` to run the task in a separate thread.
Or use similiar libraries like `asyncer` or `trio` to run the task in a separate thread.
"""
from __future__ import annotations
import asyncio
from abc import ABC, abstractmethod
from copy import deepcopy
from datetime import datetime, timedelta
from typing import Annotated, Generic, Literal, TypeVar, Union
import zoneinfo
from annotated_types import Interval
from croniter import croniter
from dateutil.relativedelta import relativedelta
from pydantic import BaseModel, Field, PositiveInt, model_validator
from typing_extensions import deprecated
from aioclock.custom_types import PositiveNumber, Triggers
TriggerTypeT = TypeVar("TriggerTypeT")
WEEKDAY_MAPPER: dict[
Literal[
"every monday",
"every tuesday",
"every wednesday",
"every thursday",
"every friday",
"every saturday",
"every sunday",
"every day",
],
int,
] = {
"every monday": 0,
"every tuesday": 1,
"every wednesday": 2,
"every thursday": 3,
"every friday": 4,
"every saturday": 5,
"every sunday": 6,
}
class BaseTrigger(BaseModel, ABC, Generic[TriggerTypeT]):
"""
Base class for all triggers.
A trigger is a way to determine when the event should be triggered. It can be based on time, or some other condition.
The way trigger are used is as follows:
1. An async function which is a task, is decorated with framework, and trigger is the arguement for the decorator
2. `get_waiting_time_till_next_trigger` is called to get the time in seconds, after which the event should be triggered.
3. If the time is not None, then it logs the time that is predicted for the event to be triggered.
4. `trigger_next` is called immediately after that, which triggers the event.
You can create trigger by yourself, by inheriting from `BaseTrigger` class.
Example:
```python
from aioclock.triggers import BaseTrigger
from typing import Literal
class Forever(BaseTrigger[Literal["Forever"]]):
type_: Literal["Forever"] = "Forever"
def should_trigger(self) -> bool:
return True
async def trigger_next(self) -> None:
return None
async def get_waiting_time_till_next_trigger(self):
if self.should_trigger():
return 0
return None
```
Attributes:
type_: Type of the trigger. It is a string, which is used to identify the trigger's name.
You can change the type by using `Generic` type when inheriting from `BaseTrigger`.
expected_trigger_time: Expected time when the event should be triggered. This gets updated
by Task Runner. It can be used on API layer, to know when the event is expected to be triggered.
"""
type_: TriggerTypeT
expected_trigger_time: Union[datetime, None] = None
@abstractmethod
async def trigger_next(self) -> None:
"""
`trigger_next` keep track of the event, and triggers the event.
The function shall return when the event is triggered and should be executed.
"""
def should_trigger(self) -> bool:
"""
`should_trigger` checks if the event should be triggered or not.
If not, then the event will not be triggered anymore.
You can save the state of the trigger and task inside the instance, and then check if the event should be triggered or not.
For instance, in `LoopCounter` trigger, it keeps track of the number of times the event has been triggered,
and then checks if the event should be triggered or not.
"""
return True
@abstractmethod
async def get_waiting_time_till_next_trigger(self) -> Union[float, None]:
"""
Returns the time in seconds, after which the event should be triggered.
Returns None, if the event should not trigger anymore.
"""
...
class Forever(BaseTrigger[Literal[Triggers.FOREVER]]):
"""A trigger that is always triggered immediately.
Example:
```python
from aioclock import AioClock, Forever
app = AioClock()
# instead of this:
async def my_task():
while True:
try:
await asyncio.sleep(3)
1/0
except DivisionByZero:
pass
# use this:
@app.task(trigger=Forever())
async def my_task():
await asyncio.sleep(3)
1/0
```
Attributes:
type_: Type of the trigger. It is a string, which is used to identify the trigger's name.
You can change the type by using `Generic` type when inheriting from `BaseTrigger`.
"""
type_: Literal[Triggers.FOREVER] = Triggers.FOREVER
def should_trigger(self) -> bool:
return True
async def trigger_next(self) -> None:
return None
async def get_waiting_time_till_next_trigger(self):
return 0
class LoopController(BaseTrigger, ABC, Generic[TriggerTypeT]):
"""
Base class for all triggers that have loop control.
Attributes:
type_: Type of the trigger. It is a string, which is used to identify the trigger's name.
You can change the type by using `Generic` type when inheriting from `LoopController`.
max_loop_count: The maximum number of times the event should be triggered.
If set to 3, then 4th time the event will not be triggered.
If set to None, it will keep running forever.
This is available for all triggers that inherit from `LoopController`.
_current_loop_count: Current loop count, which is used to keep track of the number of times the event has been triggered.
Private attribute, should not be accessed directly.
This is available for all triggers that inherit from `LoopController`.
"""
type_: TriggerTypeT
_current_loop_count: int = 0
max_loop_count: Union[PositiveInt, None] = None
@model_validator(mode="after")
def validate_loop_control(self):
if "_current_loop_count" in self.model_fields_set:
raise ValueError("_current_loop_count is a private attribute, should not be provided.")
return self
def _increment_loop_counter(self) -> None:
self._current_loop_count += 1
def should_trigger(self) -> bool:
if self.max_loop_count is None:
return True
if self.max_loop_count > self._current_loop_count:
return True
return False
async def get_waiting_time_till_next_trigger(self):
return 0
class Once(LoopController[Literal[Triggers.ONCE]]):
"""A trigger that is triggered only once. It is used to trigger the event only once, and then stop.
Example:
```python
from aioclock import AioClock, Once
app = AioClock()
app.task(trigger=Once())
async def task():
print("Hello World!")
```
"""
type_: Literal[Triggers.ONCE] = Triggers.ONCE
max_loop_count: Literal[1] = 1
async def trigger_next(self) -> None:
self._increment_loop_counter()
return None
async def get_waiting_time_till_next_trigger(self):
if self._current_loop_count == 0:
return 0
return None
@deprecated(
"Use `lifespan` instead of using Triggers for startup/shutdown events. This feature be removed in version 1.0.0"
)
class OnStartUp(LoopController[Literal[Triggers.ON_START_UP]]):
"""Just like Once, but it triggers the event only once, when the application starts up.
Example:
```python
from aioclock import AioClock, OnStartUp
app = AioClock()
app.task(trigger=OnStartUp())
async def task():
print("Hello World!")
```
"""
type_: Literal[Triggers.ON_START_UP] = Triggers.ON_START_UP
max_loop_count: Literal[1] = 1
async def trigger_next(self) -> None:
self._increment_loop_counter()
return None
async def get_waiting_time_till_next_trigger(self):
if self._current_loop_count == 0:
return 0
return None
@deprecated(
"Use `lifespan` instead of using Triggers for startup/shutdown events. This feature be removed in version 1.0.0"
)
class OnShutDown(LoopController[Literal[Triggers.ON_SHUT_DOWN]]):
"""Just like Once, but it triggers the event only once, when the application shuts down.
Example:
```python
from aioclock import AioClock, OnShutDown
app = AioClock()
app.task(trigger=OnShutDown())
async def task():
print("Hello World!")
```
"""
type_: Literal[Triggers.ON_SHUT_DOWN] = Triggers.ON_SHUT_DOWN
max_loop_count: Literal[1] = 1
async def trigger_next(self) -> None:
self._increment_loop_counter()
return None
async def get_waiting_time_till_next_trigger(self):
if self._current_loop_count == 0:
return 0
return None
class Every(LoopController[Literal[Triggers.EVERY]]):
"""A trigger that is triggered every x time units.
Example:
```python
from aioclock import AioClock, Every
app = AioClock()
app.task(trigger=Every(seconds=3))
async def task():
print("Hello World!")
```
Attributes:
first_run_strategy: Strategy to use for the first run.
If `immediate`, then the event will be triggered immediately,
and then wait for the time to trigger the event again.
If `wait`, then the event will wait for the time to trigger the event for the first time.
seconds: Seconds to wait before triggering the event.
minutes: Minutes to wait before triggering the event.
hours: Hours to wait before triggering the event.
days: Days to wait before triggering the event.
weeks: Weeks to wait before triggering the event.
max_loop_count: The maximum number of times the event should be triggered.
If set to 3, then 4th time the event will not be triggered.
If set to None, it will keep running forever.
This is available for all triggers that inherit from `LoopController`.
"""
type_: Literal[Triggers.EVERY] = Triggers.EVERY
first_run_strategy: Literal["immediate", "wait"] = "wait"
seconds: Union[PositiveNumber, None] = None
minutes: Union[PositiveNumber, None] = None
hours: Union[PositiveNumber, None] = None
days: Union[PositiveNumber, None] = None
weeks: Union[PositiveNumber, None] = None
max_loop_count: Union[PositiveInt, None] = None
@model_validator(mode="after")
def validate_time_units(self):
if (
self.seconds is None
and self.minutes is None
and self.hours is None
and self.days is None
and self.weeks is None
):
raise ValueError("At least one time unit must be provided.")
return self
@property
def to_seconds(self) -> float:
result = self.seconds or 0
if self.weeks is not None:
result += self.weeks * WEEK_TO_SECOND
if self.days is not None:
result += self.days * DAY_TO_SECOND
if self.hours is not None:
result += self.hours * HOUR_TO_SECOND
if self.minutes is not None:
result += self.minutes * MINUTE_TO_SECOND
return result
async def trigger_next(self) -> None:
self._increment_loop_counter()
if self._current_loop_count == 1 and self.first_run_strategy == "immediate":
return None
await asyncio.sleep(self.to_seconds)
return None
async def get_waiting_time_till_next_trigger(self):
# not incremented yet, so the counter is 0
if self._current_loop_count == 0 and self.first_run_strategy == "immediate":
return 0
if self.should_trigger():
return self.to_seconds
return None
MINUTE_TO_SECOND = 60
HOUR_TO_SECOND = 60 * MINUTE_TO_SECOND
DAY_TO_SECOND = 24 * HOUR_TO_SECOND
WEEK_TO_SECOND = 7 * DAY_TO_SECOND
class At(LoopController[Literal[Triggers.AT]]):
"""A trigger that is triggered at a specific time.
Example:
```python
from aioclock import AioClock, At
app = AioClock()
@app.task(trigger=At(hour=12, minute=30, tz="Asia/Kolkata"))
async def task():
print("Hello World!")
```
Attributes:
second: Second to trigger the event.
minute: Minute to trigger the event.
hour: Hour to trigger the event.
at: Day of week to trigger the event. You would get the in-line typing support when using the trigger.
tz: Timezone to use for the event.
max_loop_count: The maximum number of times the event should be triggered.
If set to 3, then 4th time the event will not be triggered.
If set to None, it will keep running forever.
This is available for all triggers that inherit from `LoopController`.
"""
type_: Literal[Triggers.AT] = Triggers.AT
max_loop_count: Union[PositiveInt, None] = None
second: Annotated[int, Interval(ge=0, le=59)] = 0
minute: Annotated[int, Interval(ge=0, le=59)] = 0
hour: Annotated[int, Interval(ge=0, le=24)] = 0
at: Literal[
"every monday",
"every tuesday",
"every wednesday",
"every thursday",
"every friday",
"every saturday",
"every sunday",
"every day",
] = "every day"
tz: str
@model_validator(mode="after")
def validate_time_units(self):
if self.second is None and self.minute is None and self.hour is None:
raise ValueError("At least one time unit must be provided.")
if self.tz is not None:
try:
zoneinfo.ZoneInfo(self.tz)
except Exception as error:
raise ValueError(f"Invalid timezone provided: {error}")
return self
def _shift_to_declared_weekday(self, target_time: datetime, tz_aware_now: datetime):
if self.at == "every day":
if tz_aware_now > target_time: # if the time is already passed, then shift to next day
target_time += timedelta(days=1)
return target_time
target_weekday: int = WEEKDAY_MAPPER[self.at]
if tz_aware_now > target_time: # if the time is already passed, then shift to next week
return target_time + relativedelta(weeks=1)
days_ahead = abs(target_weekday - tz_aware_now.weekday())
return target_time + timedelta(days_ahead)
def _get_next_ts(self, now: datetime) -> float:
target_time = deepcopy(now).replace(
hour=self.hour, minute=self.minute, second=self.second, microsecond=0
)
target_time = self._shift_to_declared_weekday(target_time, now)
return (target_time - now).total_seconds()
async def get_waiting_time_till_next_trigger(self, now: Union[datetime, None] = None):
if now is None:
now = datetime.now(tz=zoneinfo.ZoneInfo(self.tz))
sleep_for = self._get_next_ts(now)
return sleep_for
async def trigger_next(self) -> None:
self._increment_loop_counter()
await asyncio.sleep(await self.get_waiting_time_till_next_trigger())
class Cron(LoopController[Literal[Triggers.CRON]]):
"""A trigger that is triggered at a specific time, using cron job format.
If you are not familiar with the cron format, you may read about in [this wikipedia article](https://en.wikipedia.org/wiki/Cron).
Or if you need an online tool to generate cron job, you may use [crontab.guru](https://crontab.guru/).
Example:
```python
from aioclock import AioClock, Cron
app = AioClock()
@app.task(trigger=Cron(cron="0 12 * * *", tz="Asia/Kolkata"))
async def task():
print("Hello World!")
```
Attributes:
cron: Cron job format to trigger the event.
tz: Timezone to use for the event.
max_loop_count: The maximum number of times the event should be triggered.
If set to 3, then 4th time the event will not be triggered.
If set to None, it will keep running forever.
This is available for all triggers that inherit from `LoopController`.
"""
type_: Literal[Triggers.CRON] = Triggers.CRON
max_loop_count: Union[PositiveInt, None] = None
cron: str
tz: str
@model_validator(mode="after")
def validate_time_units(self):
if self.tz is not None:
try:
zoneinfo.ZoneInfo(self.tz)
except Exception as error:
raise ValueError(f"Invalid timezone provided: {error}")
if croniter.is_valid(self.cron) is False:
raise ValueError("Invalid cron format provided.")
return self
async def get_waiting_time_till_next_trigger(self, now: Union[datetime, None] = None):
if now is None:
now = datetime.now(tz=zoneinfo.ZoneInfo(self.tz))
cron_iter = croniter(self.cron, now)
next_dt: datetime = cron_iter.get_next(datetime)
return (next_dt - now).total_seconds()
async def trigger_next(self) -> None:
self._increment_loop_counter()
await asyncio.sleep(await self.get_waiting_time_till_next_trigger())
class OrTrigger(LoopController[Literal[Triggers.OR]]):
"""
A trigger that triggers the event if any of the inner triggers are met.
Example:
```python
from aioclock import AioClock, OrTrigger, Every, At
app = AioClock()
@app.task(trigger=OrTrigger(triggers=[Every(seconds=3), At(hour=12, minute=30, tz="Asia/Kolkata")]))
async def task():
print("Hello World!")
```
Not that any trigger used with OrTrigger, is fully respected, hence if you have two trigger with `max_loop_count=1`,
then each trigger will be triggered only once, and then stop, which result in the OrTrigger run only twice.
Check example to understand this intended behaviour.
Example:
```python
from aioclock import AioClock, OrTrigger, Every, At
app = AioClock()
@app.task(trigger=OrTrigger( # this get triggered 20 times because :...
triggers=[
Every(seconds=3, max_loop_count=10), # will trigger the event 10 times
At(hour=12, minute=30, tz="Asia/Kolkata", max_loop_count=10) # will trigger the event 10 times
]
))
async def task():
print("Hello World!")
```
Attributes:
triggers: List of triggers to use.
max_loop_count: The maximum number of times the event should be triggered.
If set to 3, then 4th time the event will not be triggered.
If set to None, it will keep running forever.
This is available for all triggers that inherit from `LoopController`.
"""
type_: Literal[Triggers.OR] = Triggers.OR
triggers: list[TriggerT]
max_loop_count: Union[PositiveInt, None] = None
def should_trigger(self) -> bool:
all_triggers = {trigger.should_trigger() for trigger in self.triggers}
if all_triggers == {False}:
return False # if all inner triggers should not trigger, then this shouldn't too.
return super().should_trigger()
async def find_closest_trigger(self) -> tuple[BaseTrigger, float | None]:
triggers_with_next_trigger: list[tuple[BaseTrigger, float]] = []
for trigger in self.triggers:
if trigger.should_trigger():
next_trigger = await trigger.get_waiting_time_till_next_trigger()
if next_trigger is None:
# just return it as this should be executed immediately
return trigger, next_trigger
triggers_with_next_trigger.append((trigger, next_trigger))
return min(triggers_with_next_trigger, key=lambda x: x[1])
async def trigger_next(self) -> None:
self._increment_loop_counter()
next_trigger, _ = await self.find_closest_trigger()
await next_trigger.trigger_next()
return None
async def get_waiting_time_till_next_trigger(self):
_, to_sleep = await self.find_closest_trigger()
return to_sleep
TriggerT = Annotated[
Union[
Forever,
Once,
Every,
At,
OnStartUp,
OnShutDown,
Cron,
OrTrigger,
],
Field(discriminator="type_"),
]
================================================
FILE: aioclock/utils.py
================================================
from enum import Enum
from itertools import chain
from typing import Iterable, TypeVar
T = TypeVar("T")
def flatten_chain(matrix: list[Iterable[T]]) -> list[T]:
return list(chain.from_iterable(matrix))
class StrEnum(str, Enum):
"""
StrEnum subclasses that create variants using `auto()` will have values equal to their names
Enums inheriting from this class that set values using `enum.auto()` will have variant values
equal to their names
"""
@staticmethod
def _generate_next_value_(name: str, start: int, count: int, last_values: list[str]) -> str:
"""
Uses the name as the automatic value, rather than an integer
See https://docs.python.org/3/library/enum.html#using-automatic-values for reference
"""
return name
def __str__(self) -> str:
return str(self.value)
================================================
FILE: deploy_docs.py
================================================
import logging
import subprocess
import sys
from aioclock import __version__
logger = logging.getLogger(__name__)
def run_command(command: str) -> None:
try:
result = subprocess.run(command, shell=True, check=True, capture_output=True, text=True)
print(result.stdout)
except subprocess.CalledProcessError:
logger.exception("Error executing command")
sys.exit(1)
if __name__ == "__main__":
command = "mike set-default latest"
run_command(command)
command = f"mike deploy --push --update-aliases {__version__} latest"
run_command(command)
================================================
FILE: docs/alternative.md
================================================
# AioClock VS Alternatives
There are other alternatives for scheduling as well.
This section contains comparisons between AioClock
and other scheduling tools.
Credit to Rocketry library, as the comparison is inspired by that.
Features unique for **AioClock**:
- **Simplicity**: With being super-simple, it's very easy to extend the library as you wish, which is not the usual case with other solutions!
- **Trigger-based scheduling**: Trigger based scheduling allows flexibility, making it very easy to run a task at a certain time in future.
- **Dependency Injection System**: Just like FastAPI, AioClock has a very similiar injection system which you can use to decouple your dependency.
- **Declarative Syntax**: AioClock promotes declarative syntax which makes the library easy to use.
## AioClock vs Rocketry
Rocketry is a modern statement-based scheduling framework for Python. It is simple, clean and extensive. It is suitable for small and big projects.
When **AioClock** might be a better choice:
- You don't want to be dependent to other unnecessary libraries like [redbird](https://github.com/Miksus/red-bird)
- You need a truly light weight solution.
- You are using Pydantic v2.
- Type safety is important to you. All triggers are type safe, but some statements are stringly typed in rocketry.
- You need more reliable and preditcable time based scheduling that logs when the next event is going to be triggered.
When **Rocketry** might be a better choice:
- You need a task pipelining that is heavily cpu intensive.
- You have heavy cpu bound tasks
- You are still using Pydantic v1.
!!! success "Coming next..."
In future versions, aioclock will feature a more advanced architecture, leveraging multiprocessing to handle heavy tasks efficiently.
## AioClock vs Crontab
Crontab is a scheduler for Unix-like operating systems. It is light weight and it is able to run tasks (or jobs) periodically, ie. hourly, weekly or on fixed dates.
When **AioClock** might be a better choice:
- You are building a system and not just running individual scripts.
- You need task pipelining.
- You need more complex and custom scheduling.
- You are not familiar Unix-Linux or you work with Windows.
- You need dependency injection on top of your framework layer.
When **Crontab** might be a better choice:
- If you need a truly light weight solution.
- You are not familiar with Python.
- You only want to run scripts independently at given periods.
## AioClock vs APScheduler
APScheduler is a relatively simple scheduler library for Python.
It provides Cron-style scheduling and some interval based scheduling.
When **AioClock** might be a better choice:
- You are building an automation system.
- You need more complex and customized scheduling.
- You need to pipeline tasks.
- You need dependency injection on top of your framework layer.
When **APScheduler** might be a better choice:
- You wish to have the tasks stored in a database (and not in Python code)
!!! info "You can do this by yourself already..."
There is already External APIs from library that you can use, to implement storing task metadata on a database.
It is very easy, but aioclock might actually not do it, to not couple library to a dependency.
Read about [how to use the external API](api/external_api.md).
## AioClock vs Celery
Celery is a task queue system meant for distributed execution and
scheduling background tasks for web back-ends.
When **AioClock** might be a better choice:
- You are building an automation system.
- You need more complex and customized scheduling.
- You work with Windows.
- You want to fully control your broker behavior, and have high flexability.
- You need dependency injection on top of your framework layer.
When **Celery** might be a better choice:
- You are running background tasks for web servers.
- You are not very familiar with message brokers, and you need very easy solution that abstract away all details.
!!! info "Integrate broker is easier than you can imagine, with aioclock!"
Celery works via task queues but such mechanism could be implemented to AioClock as well by creating a `once trigger` that reads from queue. You may make this as decorator and even create new libraries using AioClock.
For implementation details, see [how to integrate a broker into AioClock App](examples/brokers.md).
## AioClock vs Airflow
Airflow is a a workflow management system used heavily
in data pipelines. It has a scheduler and a built-in monitor.
When **AioClock** might be a better choice:
- You work with Windows.
- You need something that is easy to set up and quick to get produtive with.
- You are building an application.
- You want more customization.
When **Airflow** might be a better choice:
- You are building standard data pipelines.
- You would like to have more out-of-the-box.
- You need distributed execution.
- You work in data engineering.
## AioClock vs FastStream
FastStream is a powerful and easy-to-use Python framework for building asynchronous services interacting with event streams such as Apache Kafka, RabbitMQ, NATS and Redis.
When **AioClock** might be a better choice:
- You need more complex and customized scheduling.
- You need high flexability and low level APIs of your broker.
When **FastStream** might be a better choice:
- You are not very familiar with message brokers, and you need very easy solution that abstract away all details.
- You need auto generated asyncapi documentation
- You are building a distributed data streaming application
!!! info "They can be used together..."
Note that you can use both beside each other, just like FastAPI. All you'd have to do is to serve both application at same time.
================================================
FILE: docs/api/external_api.md
================================================
::: aioclock.api
================================================
FILE: docs/api/getting_started.md
================================================
::: aioclock.app
::: aioclock.group
================================================
FILE: docs/api/plugin.md
================================================
::: aioclock.ext
::: aioclock.ext.fast
================================================
FILE: docs/api/task.md
================================================
::: aioclock.task
================================================
FILE: docs/api/triggers.md
================================================
# Triggers
::: aioclock.triggers
================================================
FILE: docs/diagrams/aioclock.excalidraw
================================================
{
"type": "excalidraw",
"version": 2,
"source": "https://excalidraw.com",
"elements": [
{
"type": "arrow",
"version": 1870,
"versionNonce": 562660048,
"index": "aR",
"isDeleted": false,
"id": "umIUdT2Pg_dV6Yj3OxRkn",
"fillStyle": "hachure",
"strokeWidth": 1,
"strokeStyle": "dashed",
"roughness": 1,
"opacity": 100,
"angle": 0,
"x": 631.8702637667776,
"y": 56.86698670706318,
"strokeColor": "#1e1e1e",
"backgroundColor": "#3bc9db",
"width": 260.23891777104006,
"height": 28.060608173103617,
"seed": 400598064,
"groupIds": [],
"frameId": null,
"roundness": {
"type": 2
},
"boundElements": [],
"updated": 1723757446539,
"link": null,
"locked": false,
"startBinding": {
"elementId": "URbMlcmauXmJNE4154eWu",
"focus": -0.14154036538490047,
"gap": 10.016775821461238,
"fixedPoint": null
},
"endBinding": {
"elementId": "b3EdqBYxnZK7_xrKoWpEZ",
"focus": -0.24081634913640015,
"gap": 1.0371662412074443,
"fixedPoint": null
},
"lastCommittedPoint": null,
"startArrowhead": null,
"endArrowhead": "arrow",
"points": [
[
0,
0
],
[
260.23891777104006,
28.060608173103617
]
],
"elbowed": false
},
{
"type": "text",
"version": 646,
"versionNonce": 2009813200,
"index": "aS",
"isDeleted": false,
"id": "jGMhw9Odj78Yt6mAP27K3",
"fillStyle": "hachure",
"strokeWidth": 1,
"strokeStyle": "solid",
"roughness": 1,
"opacity": 100,
"angle": 0.09974755060020257,
"x": 665.3042807320827,
"y": 36.30932819124689,
"strokeColor": "#1e1e1e",
"backgroundColor": "#3bc9db",
"width": 171.17919658226998,
"height": 21.098102858351695,
"seed": 3246640,
"groupIds": [],
"frameId": null,
"roundness": null,
"boundElements": [],
"updated": 1723757251178,
"link": null,
"locked": false,
"fontSize": 16.878482286681354,
"fontFamily": 5,
"text": "def include_group()",
"textAlign": "left",
"verticalAlign": "top",
"containerId": null,
"originalText": "def include_group()",
"autoResize": false,
"lineHeight": 1.25
},
{
"type": "rectangle",
"version": 1610,
"versionNonce": 2058722512,
"index": "aa4",
"isDeleted": false,
"id": "b3EdqBYxnZK7_xrKoWpEZ",
"fillStyle": "hachure",
"strokeWidth": 1,
"strokeStyle": "solid",
"roughness": 1,
"opacity": 100,
"angle": 0,
"x": 893.1463477790252,
"y": -116.12620250637502,
"strokeColor": "#1e1e1e",
"backgroundColor": "#ffec99",
"width": 891.0632487039327,
"height": 383.0330657293356,
"seed": 734090960,
"groupIds": [
"kz6Om3WP3ZIzj3WwTqs5f"
],
"frameId": null,
"roundness": {
"type": 3
},
"boundElements": [
{
"id": "umIUdT2Pg_dV6Yj3OxRkn",
"type": "arrow"
}
],
"updated": 1723757446539,
"link": null,
"locked": false
},
{
"type": "text",
"version": 1399,
"versionNonce": 1357949136,
"index": "aa8",
"isDeleted": false,
"id": "T8_mi4ZCb-ZGtGclGYrQb",
"fillStyle": "solid",
"strokeWidth": 2,
"strokeStyle": "solid",
"roughness": 1,
"opacity": 100,
"angle": 0,
"x": 969.6874368433648,
"y": -100.65652553224646,
"strokeColor": "#1e1e1e",
"backgroundColor": "transparent",
"width": 96.32254493525791,
"height": 41.85028122224372,
"seed": 1468254768,
"groupIds": [
"kz6Om3WP3ZIzj3WwTqs5f"
],
"frameId": null,
"roundness": null,
"boundElements": [],
"updated": 1723757446539,
"link": null,
"locked": false,
"fontSize": 33.48022497779499,
"fontFamily": 5,
"text": "Group",
"textAlign": "left",
"verticalAlign": "top",
"containerId": null,
"originalText": "Group",
"autoResize": true,
"lineHeight": 1.25
},
{
"type": "diamond",
"version": 1336,
"versionNonce": 344133328,
"index": "aaG",
"isDeleted": false,
"id": "1-nChtDmy9xBSz25x5Tdy",
"fillStyle": "hachure",
"strokeWidth": 1,
"strokeStyle": "solid",
"roughness": 1,
"opacity": 100,
"angle": 0,
"x": 919.9022234788671,
"y": -34.98141186613114,
"strokeColor": "#1e1e1e",
"backgroundColor": "#ff8787",
"width": 172.41095637580733,
"height": 78,
"seed": 1465639632,
"groupIds": [
"kz6Om3WP3ZIzj3WwTqs5f"
],
"frameId": null,
"roundness": {
"type": 2
},
"boundElements": [
{
"id": "Ug96RmiLOJxVdqw8JLOl4",
"type": "text"
},
{
"id": "ICGWc2_oP7ooflk821IPB",
"type": "arrow"
},
{
"id": "dREYVkyhboXiS53A17F-t",
"type": "arrow"
}
],
"updated": 1723757446539,
"link": null,
"locked": false
},
{
"type": "text",
"version": 1340,
"versionNonce": 1155052752,
"index": "aaO",
"isDeleted": false,
"id": "Ug96RmiLOJxVdqw8JLOl4",
"fillStyle": "hachure",
"strokeWidth": 1,
"strokeStyle": "solid",
"roughness": 1,
"opacity": 100,
"angle": 0,
"x": 968.5321079585611,
"y": -10.460148756903092,
"strokeColor": "#1e1e1e",
"backgroundColor": "#ff8787",
"width": 74.94570922851562,
"height": 28.957473781543904,
"seed": 1418761776,
"groupIds": [
"kz6Om3WP3ZIzj3WwTqs5f"
],
"frameId": null,
"roundness": null,
"boundElements": [],
"updated": 1723757446539,
"link": null,
"locked": false,
"fontSize": 23.165979025235124,
"fontFamily": 5,
"text": "Task 1",
"textAlign": "center",
"verticalAlign": "middle",
"containerId": "1-nChtDmy9xBSz25x5Tdy",
"originalText": "Task 1",
"autoResize": true,
"lineHeight": 1.25
},
{
"type": "diamond",
"version": 1640,
"versionNonce": 995960528,
"index": "aaV",
"isDeleted": false,
"id": "679z3nf1w9xVLbf3f8AdN",
"fillStyle": "hachure",
"strokeWidth": 1,
"strokeStyle": "solid",
"roughness": 1,
"opacity": 100,
"angle": 0,
"x": 1233.3981057694639,
"y": -67.91630887978769,
"strokeColor": "#1e1e1e",
"backgroundColor": "#ff8787",
"width": 190.0690779031065,
"height": 78,
"seed": 446189616,
"groupIds": [
"kz6Om3WP3ZIzj3WwTqs5f"
],
"frameId": null,
"roundness": {
"type": 2
},
"boundElements": [
{
"id": "ZovWm7KTns9gYTw6Alczq",
"type": "text"
},
{
"id": "gBeFANJTXm0QDmAM0WzrH",
"type": "arrow"
},
{
"id": "WYORfi6MQ5vqe_kGKqXHJ",
"type": "arrow"
}
],
"updated": 1723757446539,
"link": null,
"locked": false
},
{
"type": "text",
"version": 1612,
"versionNonce": 951045328,
"index": "aad",
"isDeleted": false,
"id": "ZovWm7KTns9gYTw6Alczq",
"fillStyle": "hachure",
"strokeWidth": 1,
"strokeStyle": "solid",
"roughness": 1,
"opacity": 100,
"angle": 0,
"x": 1287.7811818248304,
"y": -43.39504577055965,
"strokeColor": "#1e1e1e",
"backgroundColor": "#ff8787",
"width": 81.26838684082031,
"height": 28.957473781543904,
"seed": 1300154928,
"groupIds": [
"kz6Om3WP3ZIzj3WwTqs5f"
],
"frameId": null,
"roundness": null,
"boundElements": [],
"updated": 1723757446539,
"link": null,
"locked": false,
"fontSize": 23.165979025235124,
"fontFamily": 5,
"text": "Task 2",
"textAlign": "center",
"verticalAlign": "middle",
"containerId": "679z3nf1w9xVLbf3f8AdN",
"originalText": "Task 2",
"autoResize": true,
"lineHeight": 1.25
},
{
"type": "diamond",
"version": 1669,
"versionNonce": 2020294352,
"index": "aal",
"isDeleted": false,
"id": "vsA_CIfMv5pa_E8ntMeiT",
"fillStyle": "hachure",
"strokeWidth": 1,
"strokeStyle": "solid",
"roughness": 1,
"opacity": 100,
"angle": 0,
"x": 1340.6872159208458,
"y": 55.18372300040164,
"strokeColor": "#1e1e1e",
"backgroundColor": "#ff8787",
"width": 180.59766388142287,
"height": 85.51233077137486,
"seed": 65450544,
"groupIds": [
"kz6Om3WP3ZIzj3WwTqs5f"
],
"frameId": null,
"roundness": {
"type": 2
},
"boundElements": [
{
"id": "h9vDlzUov3hi0exBJmXql",
"type": "text"
},
{
"id": "QCOc4HbolRRMUaYbYgevA",
"type": "arrow"
},
{
"id": "43_r9mRXWwrQYCAcyRAFA",
"type": "arrow"
}
],
"updated": 1723757446539,
"link": null,
"locked": false
},
{
"type": "text",
"version": 1633,
"versionNonce": 1547363536,
"index": "aat",
"isDeleted": false,
"id": "h9vDlzUov3hi0exBJmXql",
"fillStyle": "hachure",
"strokeWidth": 1,
"strokeStyle": "solid",
"roughness": 1,
"opacity": 100,
"angle": 0,
"x": 1391.2677994937405,
"y": 83.5830688024734,
"strokeColor": "#1e1e1e",
"backgroundColor": "#ff8787",
"width": 79.13766479492188,
"height": 28.957473781543904,
"seed": 45149232,
"groupIds": [
"kz6Om3WP3ZIzj3WwTqs5f"
],
"frameId": null,
"roundness": null,
"boundElements": [],
"updated": 1723757446539,
"link": null,
"locked": false,
"fontSize": 23.165979025235124,
"fontFamily": 5,
"text": "Task 3",
"textAlign": "center",
"verticalAlign": "middle",
"containerId": "vsA_CIfMv5pa_E8ntMeiT",
"originalText": "Task 3",
"autoResize": true,
"lineHeight": 1.25
},
{
"type": "ellipse",
"version": 1231,
"versionNonce": 296572624,
"index": "ab",
"isDeleted": false,
"id": "PEU_XU8Sh3D4biyxvy4HU",
"fillStyle": "hachure",
"strokeWidth": 1,
"strokeStyle": "solid",
"roughness": 1,
"opacity": 100,
"angle": 0,
"x": 1514.9518100115115,
"y": -110.71170442618455,
"strokeColor": "#1e1e1e",
"backgroundColor": "#a5d8ff",
"width": 188.21171841881053,
"height": 56.75664861182606,
"seed": 1514127408,
"groupIds": [
"kz6Om3WP3ZIzj3WwTqs5f"
],
"frameId": null,
"roundness": {
"type": 2
},
"boundElements": [
{
"id": "RZxT1str5sU0IsNoY2jST",
"type": "text"
},
{
"id": "WYORfi6MQ5vqe_kGKqXHJ",
"type": "arrow"
}
],
"updated": 1723757446539,
"link": null,
"locked": false
},
{
"type": "text",
"version": 1264,
"versionNonce": 897187024,
"index": "ab8",
"isDeleted": false,
"id": "RZxT1str5sU0IsNoY2jST",
"fillStyle": "hachure",
"strokeWidth": 1,
"strokeStyle": "dashed",
"roughness": 1,
"opacity": 100,
"angle": 0,
"x": 1555.6078490084465,
"y": -96.8786225664656,
"strokeColor": "#1e1e1e",
"backgroundColor": "#a5d8ff",
"width": 106.81385803222656,
"height": 28.957473781543904,
"seed": 950688304,
"groupIds": [
"kz6Om3WP3ZIzj3WwTqs5f"
],
"frameId": null,
"roundness": null,
"boundElements": [],
"updated": 1723757446539,
"link": null,
"locked": false,
"fontSize": 23.165979025235124,
"fontFamily": 5,
"text": "Trigger 2",
"textAlign": "center",
"verticalAlign": "middle",
"containerId": "PEU_XU8Sh3D4biyxvy4HU",
"originalText": "Trigger 2",
"autoResize": true,
"lineHeight": 1.25
},
{
"type": "ellipse",
"version": 1391,
"versionNonce": 1597016624,
"index": "abG",
"isDeleted": false,
"id": "7nMEXGNJzk7AaKIUHZYgN",
"fillStyle": "hachure",
"strokeWidth": 1,
"strokeStyle": "solid",
"roughness": 1,
"opacity": 100,
"angle": 0,
"x": 1080.8975851818275,
"y": 99.49504911229374,
"strokeColor": "#1e1e1e",
"backgroundColor": "#a5d8ff",
"width": 188.21171841881053,
"height": 56.75664861182606,
"seed": 1550810160,
"groupIds": [
"kz6Om3WP3ZIzj3WwTqs5f"
],
"frameId": null,
"roundness": {
"type": 2
},
"boundElements": [
{
"id": "AagPjJF25n7fL0j1dkYKb",
"type": "text"
},
{
"id": "dREYVkyhboXiS53A17F-t",
"type": "arrow"
}
],
"updated": 1723757450575,
"link": null,
"locked": false
},
{
"type": "text",
"version": 1424,
"versionNonce": 117425200,
"index": "abV",
"isDeleted": false,
"id": "AagPjJF25n7fL0j1dkYKb",
"fillStyle": "hachure",
"strokeWidth": 1,
"strokeStyle": "dashed",
"roughness": 1,
"opacity": 100,
"angle": 0,
"x": 1124.7149629849148,
"y": 113.3281309720127,
"strokeColor": "#1e1e1e",
"backgroundColor": "#a5d8ff",
"width": 100.49118041992188,
"height": 28.957473781543904,
"seed": 596670000,
"groupIds": [
"kz6Om3WP3ZIzj3WwTqs5f"
],
"frameId": null,
"roundness": null,
"boundElements": [],
"updated": 1723757450575,
"link": null,
"locked": false,
"fontSize": 23.165979025235124,
"fontFamily": 5,
"text": "Trigger 1",
"textAlign": "center",
"verticalAlign": "middle",
"containerId": "7nMEXGNJzk7AaKIUHZYgN",
"originalText": "Trigger 1",
"autoResize": true,
"lineHeight": 1.25
},
{
"type": "ellipse",
"version": 1266,
"versionNonce": 2073164496,
"index": "abl",
"isDeleted": false,
"id": "L1oF-vTjx-r7uE4ozO6yW",
"fillStyle": "hachure",
"strokeWidth": 1,
"strokeStyle": "solid",
"roughness": 1,
"opacity": 100,
"angle": 0,
"x": 1578.0877317826946,
"y": 109.78177027891613,
"strokeColor": "#1e1e1e",
"backgroundColor": "#a5d8ff",
"width": 188.21171841881053,
"height": 56.75664861182606,
"seed": 1582500912,
"groupIds": [
"kz6Om3WP3ZIzj3WwTqs5f"
],
"frameId": null,
"roundness": {
"type": 2
},
"boundElements": [
{
"id": "7w2g1VYZInR5vuWaicPT-",
"type": "text"
},
{
"id": "QCOc4HbolRRMUaYbYgevA",
"type": "arrow"
}
],
"updated": 1723757446539,
"link": null,
"locked": false
},
{
"type": "text",
"version": 1306,
"versionNonce": 1126834384,
"index": "ac",
"isDeleted": false,
"id": "7w2g1VYZInR5vuWaicPT-",
"fillStyle": "hachure",
"strokeWidth": 1,
"strokeStyle": "dashed",
"roughness": 1,
"opacity": 100,
"angle": 0,
"x": 1619.8091318025788,
"y": 123.61485213863509,
"strokeColor": "#1e1e1e",
"backgroundColor": "#a5d8ff",
"width": 104.68313598632812,
"height": 28.957473781543904,
"seed": 1115817520,
"groupIds": [
"kz6Om3WP3ZIzj3WwTqs5f"
],
"frameId": null,
"roundness": null,
"boundElements": [],
"updated": 1723757446539,
"link": null,
"locked": false,
"fontSize": 23.165979025235124,
"fontFamily": 5,
"text": "Trigger 3",
"textAlign": "center",
"verticalAlign": "middle",
"containerId": "L1oF-vTjx-r7uE4ozO6yW",
"originalText": "Trigger 3",
"autoResize": true,
"lineHeight": 1.25
},
{
"type": "arrow",
"version": 2589,
"versionNonce": 618118704,
"index": "ad",
"isDeleted": false,
"id": "dREYVkyhboXiS53A17F-t",
"fillStyle": "hachure",
"strokeWidth": 1,
"strokeStyle": "solid",
"roughness": 1,
"opacity": 100,
"angle": 0,
"x": 1052.2934151720121,
"y": 31.6871389549543,
"strokeColor": "#1e1e1e",
"backgroundColor": "#a5d8ff",
"width": 72.65101292680833,
"height": 71.20679435860492,
"seed": 1357628464,
"groupIds": [
"kz6Om3WP3ZIzj3WwTqs5f"
],
"frameId": null,
"roundness": {
"type": 2
},
"boundElements": [],
"updated": 1723757450575,
"link": null,
"locked": false,
"startBinding": {
"elementId": "1-nChtDmy9xBSz25x5Tdy",
"focus": 0.021328620465046026,
"gap": 8.713122096692402,
"fixedPoint": null
},
"endBinding": {
"elementId": "7nMEXGNJzk7AaKIUHZYgN",
"focus": -0.2495745298142544,
"gap": 1,
"fixedPoint": null
},
"lastCommittedPoint": null,
"startArrowhead": null,
"endArrowhead": "arrow",
"points": [
[
0,
0
],
[
72.65101292680833,
71.20679435860492
]
],
"elbowed": false
},
{
"type": "arrow",
"version": 3150,
"versionNonce": 1788521008,
"index": "ae",
"isDeleted": false,
"id": "QCOc4HbolRRMUaYbYgevA",
"fillStyle": "hachure",
"strokeWidth": 1,
"strokeStyle": "solid",
"roughness": 1,
"opacity": 100,
"angle": 0,
"x": 1505.9138290121784,
"y": 115.04897709364587,
"strokeColor": "#1e1e1e",
"backgroundColor": "#a5d8ff",
"width": 68.45677489333957,
"height": 21.345873788386115,
"seed": 1524351024,
"groupIds": [
"kz6Om3WP3ZIzj3WwTqs5f"
],
"frameId": null,
"roundness": {
"type": 2
},
"boundElements": [],
"updated": 1723757446553,
"link": null,
"locked": false,
"startBinding": {
"elementId": "vsA_CIfMv5pa_E8ntMeiT",
"focus": -0.14628403082450295,
"gap": 8.885249593051313,
"fixedPoint": null
},
"endBinding": {
"elementId": "L1oF-vTjx-r7uE4ozO6yW",
"focus": -0.7039819943540665,
"gap": 3.843006315232685,
"fixedPoint": null
},
"lastCommittedPoint": null,
"startArrowhead": null,
"endArrowhead": "arrow",
"points": [
[
0,
0
],
[
68.45677489333957,
21.345873788386115
]
],
"elbowed": false
},
{
"type": "diamond",
"version": 1324,
"versionNonce": 1520916176,
"index": "at",
"isDeleted": false,
"id": "mp5cf4rOe7-TBZhYPXY93",
"fillStyle": "hachure",
"strokeWidth": 1,
"strokeStyle": "solid",
"roughness": 1,
"opacity": 100,
"angle": 0.006345883568884325,
"x": 644.4979240201953,
"y": 359.25214630499306,
"strokeColor": "#1e1e1e",
"backgroundColor": "#ff8787",
"width": 173.20703124999997,
"height": 79.8671875,
"seed": 426496208,
"groupIds": [
"EzkF_X72G6C04wpKYmGYo"
],
"frameId": null,
"roundness": {
"type": 2
},
"boundElements": [
{
"type": "text",
"id": "xyEhwiCsaF_GlM_qi4kL_"
},
{
"id": "jtmxjvYMT8IyTg6YsEhEo",
"type": "arrow"
},
{
"id": "qdG1HK_ivqJvkFxfXu_bj",
"type": "arrow"
},
{
"id": "HRt4IaHAZ1RGroBskO7Y4",
"type": "arrow"
}
],
"updated": 1723757424418,
"link": null,
"locked": false
},
{
"type": "text",
"version": 1261,
"versionNonce": 628108336,
"index": "au",
"isDeleted": false,
"id": "xyEhwiCsaF_GlM_qi4kL_",
"fillStyle": "hachure",
"strokeWidth": 1,
"strokeStyle": "solid",
"roughness": 1,
"opacity": 100,
"angle": 0.006345883568884325,
"x": 697.3597022794727,
"y": 386.71894317999306,
"strokeColor": "#1e1e1e",
"backgroundColor": "#ff8787",
"width": 67.87995910644531,
"height": 25,
"seed": 1935226576,
"groupIds": [
"EzkF_X72G6C04wpKYmGYo"
],
"frameId": null,
"roundness": null,
"boundElements": [],
"updated": 1723757418188,
"link": null,
"locked": false,
"fontSize": 20,
"fontFamily": 5,
"text": "Task 4",
"textAlign": "center",
"verticalAlign": "middle",
"containerId": "mp5cf4rOe7-TBZhYPXY93",
"originalText": "Task 4",
"autoResize": true,
"lineHeight": 1.25
},
{
"type": "ellipse",
"version": 1341,
"versionNonce": 383682096,
"index": "av",
"isDeleted": false,
"id": "l1-m9PMckNOmTsLOCicxB",
"fillStyle": "hachure",
"strokeWidth": 1,
"strokeStyle": "solid",
"roughness": 1,
"opacity": 100,
"angle": 0.03810438733463428,
"x": 861.8797925835937,
"y": 327.34831220020493,
"strokeColor": "#1e1e1e",
"backgroundColor": "#a5d8ff",
"width": 162.48975984463084,
"height": 49,
"seed": 917101776,
"groupIds": [
"EzkF_X72G6C04wpKYmGYo"
],
"frameId": null,
"roundness": {
"type": 2
},
"boundElements": [
{
"type": "text",
"id": "rmTtNCvSYi_osiDfmjP7W"
},
{
"id": "qdG1HK_ivqJvkFxfXu_bj",
"type": "arrow"
}
],
"updated": 1723757412156,
"link": null,
"locked": false
},
{
"type": "text",
"version": 1315,
"versionNonce": 260980784,
"index": "aw",
"isDeleted": false,
"id": "rmTtNCvSYi_osiDfmjP7W",
"fillStyle": "hachure",
"strokeWidth": 1,
"strokeStyle": "dashed",
"roughness": 1,
"opacity": 100,
"angle": 0.03810438733463428,
"x": 898.2059039024226,
"y": 339.5241960611345,
"strokeColor": "#1e1e1e",
"backgroundColor": "#a5d8ff",
"width": 89.93992614746094,
"height": 25,
"seed": 1425392336,
"groupIds": [
"EzkF_X72G6C04wpKYmGYo"
],
"frameId": null,
"roundness": null,
"boundElements": [],
"updated": 1723757412156,
"link": null,
"locked": false,
"fontSize": 20,
"fontFamily": 5,
"text": "Trigger 4",
"textAlign": "center",
"verticalAlign": "middle",
"containerId": "l1-m9PMckNOmTsLOCicxB",
"originalText": "Trigger 4",
"autoResize": true,
"lineHeight": 1.25
},
{
"type": "arrow",
"version": 2670,
"versionNonce": 890691632,
"index": "ax",
"isDeleted": false,
"id": "qdG1HK_ivqJvkFxfXu_bj",
"fillStyle": "hachure",
"strokeWidth": 1,
"strokeStyle": "solid",
"roughness": 1,
"opacity": 100,
"angle": 6.024769842411049,
"x": 792.7955223699694,
"y": 376.2767822063056,
"strokeColor": "#1e1e1e",
"backgroundColor": "#a5d8ff",
"width": 87.57268219796742,
"height": 0.5282969080340081,
"seed": 419426352,
"groupIds": [
"EzkF_X72G6C04wpKYmGYo"
],
"frameId": null,
"roundness": {
"type": 2
},
"boundElements": [],
"updated": 1723757418188,
"link": null,
"locked": false,
"startBinding": {
"elementId": "mp5cf4rOe7-TBZhYPXY93",
"focus": 0.11419552780004952,
"gap": 1.1291855844196732,
"fixedPoint": null
},
"endBinding": {
"elementId": "l1-m9PMckNOmTsLOCicxB",
"focus": -0.23292659823893222,
"gap": 1,
"fixedPoint": null
},
"lastCommittedPoint": null,
"startArrowhead": null,
"endArrowhead": "arrow",
"points": [
[
0,
0
],
[
87.57268219796742,
0.5282969080340081
]
],
"elbowed": false
},
{
"type": "ellipse",
"version": 742,
"versionNonce": 599753264,
"index": "b05",
"isDeleted": false,
"id": "WhZAKrfIBY1WfWup23z04",
"fillStyle": "hachure",
"strokeWidth": 1,
"strokeStyle": "solid",
"roughness": 1,
"opacity": 100,
"angle": 0,
"x": 1539.2381134992843,
"y": 8.612451809277985,
"strokeColor": "#1e1e1e",
"backgroundColor": "#12b886",
"width": 188.21171841881053,
"height": 56.75664861182606,
"seed": 754706128,
"groupIds": [
"i5wOMyEkj5AdEKIlQn9ba"
],
"frameId": null,
"roundness": {
"type": 2
},
"boundElements": [
{
"id": "r-dwj1uZUT6jpFw5M_7eg",
"type": "text"
},
{
"id": "gBeFANJTXm0QDmAM0WzrH",
"type": "arrow"
}
],
"updated": 1723753697539,
"link": null,
"locked": false
},
{
"type": "text",
"version": 793,
"versionNonce": 1324334128,
"index": "b06",
"isDeleted": false,
"id": "r-dwj1uZUT6jpFw5M_7eg",
"fillStyle": "hachure",
"strokeWidth": 1,
"strokeStyle": "dashed",
"roughness": 1,
"opacity": 100,
"angle": 0,
"x": 1579.4772747496372,
"y": 22.445533668996934,
"strokeColor": "#1e1e1e",
"backgroundColor": "#12b886",
"width": 107.64761352539062,
"height": 28.957473781543904,
"seed": 141987024,
"groupIds": [
"i5wOMyEkj5AdEKIlQn9ba"
],
"frameId": null,
"roundness": null,
"boundElements": [],
"updated": 1723753697539,
"link": null,
"locked": false,
"fontSize": 23.165979025235124,
"fontFamily": 5,
"text": "Callable 2",
"textAlign": "center",
"verticalAlign": "middle",
"containerId": "WhZAKrfIBY1WfWup23z04",
"originalText": "Callable 2",
"autoResize": true,
"lineHeight": 1.25
},
{
"type": "arrow",
"version": 907,
"versionNonce": 12432080,
"index": "b07",
"isDeleted": false,
"id": "gBeFANJTXm0QDmAM0WzrH",
"fillStyle": "hachure",
"strokeWidth": 1,
"strokeStyle": "solid",
"roughness": 1,
"opacity": 100,
"angle": 0,
"x": 1412.1801059262384,
"y": -14.87264932207451,
"strokeColor": "#1e1e1e",
"backgroundColor": "#12b886",
"width": 126.85949483626405,
"height": 47.810475354948395,
"seed": 1199146192,
"groupIds": [],
"frameId": null,
"roundness": {
"type": 2
},
"boundElements": [],
"updated": 1723757446539,
"link": null,
"locked": false,
"startBinding": {
"elementId": "679z3nf1w9xVLbf3f8AdN",
"focus": -0.4492016684540374,
"gap": 8.707041440834715,
"fixedPoint": null
},
"endBinding": {
"elementId": "WhZAKrfIBY1WfWup23z04",
"focus": -0.7112738678621304,
"gap": 1.1025252940076768,
"fixedPoint": null
},
"lastCommittedPoint": null,
"startArrowhead": null,
"endArrowhead": "arrow",
"points": [
[
0,
0
],
[
126.85949483626405,
47.810475354948395
]
],
"elbowed": false
},
{
"type": "ellipse",
"version": 926,
"versionNonce": 256996400,
"index": "b08",
"isDeleted": false,
"id": "s89xH-4yu5r6IAP8S9KE8",
"fillStyle": "hachure",
"strokeWidth": 1,
"strokeStyle": "solid",
"roughness": 1,
"opacity": 100,
"angle": 0,
"x": 921.0041842510022,
"y": 175.33420387831111,
"strokeColor": "#1e1e1e",
"backgroundColor": "#12b886",
"width": 188.21171841881053,
"height": 56.75664861182606,
"seed": 840094768,
"groupIds": [
"7T3F0-rwS5jf_PHTJroJR"
],
"frameId": null,
"roundness": {
"type": 2
},
"boundElements": [
{
"id": "ZF1CqhL5fwb4Ws-zY_XLW",
"type": "text"
},
{
"id": "ICGWc2_oP7ooflk821IPB",
"type": "arrow"
}
],
"updated": 1723757221361,
"link": null,
"locked": false
},
{
"type": "text",
"version": 981,
"versionNonce": 1583729872,
"index": "b09",
"isDeleted": false,
"id": "ZF1CqhL5fwb4Ws-zY_XLW",
"fillStyle": "hachure",
"strokeWidth": 1,
"strokeStyle": "dashed",
"roughness": 1,
"opacity": 100,
"angle": 0,
"x": 964.4046843075075,
"y": 189.16728573803007,
"strokeColor": "#1e1e1e",
"backgroundColor": "#12b886",
"width": 101.32493591308594,
"height": 28.957473781543904,
"seed": 453561904,
"groupIds": [
"7T3F0-rwS5jf_PHTJroJR"
],
"frameId": null,
"roundness": null,
"boundElements": [],
"updated": 1723757221361,
"link": null,
"locked": false,
"fontSize": 23.165979025235124,
"fontFamily": 5,
"text": "Callable 1",
"textAlign": "center",
"verticalAlign": "middle",
"containerId": "s89xH-4yu5r6IAP8S9KE8",
"originalText": "Callable 1",
"autoResize": true,
"lineHeight": 1.25
},
{
"type": "ellipse",
"version": 854,
"versionNonce": 801233104,
"index": "b0A",
"isDeleted": false,
"id": "KWb-DkqaKPnpVL7f6V4bL",
"fillStyle": "hachure",
"strokeWidth": 1,
"strokeStyle": "solid",
"roughness": 1,
"opacity": 100,
"angle": 0,
"x": 1370.069653799254,
"y": 191.40622146641806,
"strokeColor": "#1e1e1e",
"backgroundColor": "#12b886",
"width": 188.21171841881053,
"height": 56.75664861182606,
"seed": 332013104,
"groupIds": [
"PkHndZTqTLVxxXgSDZxvW"
],
"frameId": null,
"roundness": {
"type": 2
},
"boundElements": [
{
"id": "-Iu1CEC1DMo3CeL80AL-H",
"type": "text"
},
{
"id": "43_r9mRXWwrQYCAcyRAFA",
"type": "arrow"
}
],
"updated": 1723753719986,
"link": null,
"locked": false
},
{
"type": "text",
"version": 911,
"versionNonce": 609235504,
"index": "b0B",
"isDeleted": false,
"id": "-Iu1CEC1DMo3CeL80AL-H",
"fillStyle": "hachure",
"strokeWidth": 1,
"strokeStyle": "dashed",
"roughness": 1,
"opacity": 100,
"angle": 0,
"x": 1411.374176072556,
"y": 205.23930332613702,
"strokeColor": "#1e1e1e",
"backgroundColor": "#12b886",
"width": 105.51689147949219,
"height": 28.957473781543904,
"seed": 457627696,
"groupIds": [
"PkHndZTqTLVxxXgSDZxvW"
],
"frameId": null,
"roundness": null,
"boundElements": [],
"updated": 1723753715755,
"link": null,
"locked": false,
"fontSize": 23.165979025235124,
"fontFamily": 5,
"text": "Callable 3",
"textAlign": "center",
"verticalAlign": "middle",
"containerId": "KWb-DkqaKPnpVL7f6V4bL",
"originalText": "Callable 3",
"autoResize": true,
"lineHeight": 1.25
},
{
"type": "arrow",
"version": 621,
"versionNonce": 1737071312,
"index": "b0C",
"isDeleted": false,
"id": "ICGWc2_oP7ooflk821IPB",
"fillStyle": "hachure",
"strokeWidth": 1,
"strokeStyle": "solid",
"roughness": 1,
"opacity": 100,
"angle": 0,
"x": 1000.4847504534591,
"y": 44.85883247767296,
"strokeColor": "#1e1e1e",
"backgroundColor": "#12b886",
"width": 1.021474072241972,
"height": 126.31065788274074,
"seed": 1934117584,
"groupIds": [],
"frameId": null,
"roundness": {
"type": 2
},
"boundElements": [],
"updated": 1723757446539,
"link": null,
"locked": false,
"startBinding": {
"elementId": "1-nChtDmy9xBSz25x5Tdy",
"focus": 0.06905856012815444,
"gap": 3.99435700483167,
"fixedPoint": null
},
"endBinding": {
"elementId": "s89xH-4yu5r6IAP8S9KE8",
"focus": -0.14186582145959503,
"gap": 4.479043609614649,
"fixedPoint": null
},
"lastCommittedPoint": null,
"startArrowhead": null,
"endArrowhead": "arrow",
"points": [
[
0,
0
],
[
1.021474072241972,
126.31065788274074
]
],
"elbowed": false
},
{
"type": "arrow",
"version": 49,
"versionNonce": 1153557200,
"index": "b0D",
"isDeleted": false,
"id": "WYORfi6MQ5vqe_kGKqXHJ",
"fillStyle": "hachure",
"strokeWidth": 1,
"strokeStyle": "solid",
"roughness": 1,
"opacity": 100,
"angle": 0,
"x": 1423.6927813940347,
"y": -35.91522196403305,
"strokeColor": "#1e1e1e",
"backgroundColor": "#12b886",
"width": 93.20078197532757,
"height": 37.93587818765758,
"seed": 969630768,
"groupIds": [],
"frameId": null,
"roundness": {
"type": 2
},
"boundElements": [],
"updated": 1723757446539,
"link": null,
"locked": false,
"startBinding": {
"elementId": "679z3nf1w9xVLbf3f8AdN",
"focus": 0.815379469998369,
"gap": 6.560548878821066,
"fixedPoint": null
},
"endBinding": {
"elementId": "PEU_XU8Sh3D4biyxvy4HU",
"focus": 0.6089969123958293,
"gap": 1.9020541028933877,
"fixedPoint": null
},
"lastCommittedPoint": null,
"startArrowhead": null,
"endArrowhead": "arrow",
"points": [
[
0,
0
],
[
93.20078197532757,
-37.93587818765758
]
],
"elbowed": false
},
{
"type": "arrow",
"version": 52,
"versionNonce": 2009329872,
"index": "b0E",
"isDeleted": false,
"id": "43_r9mRXWwrQYCAcyRAFA",
"fillStyle": "hachure",
"strokeWidth": 1,
"strokeStyle": "solid",
"roughness": 1,
"opacity": 100,
"angle": 0,
"x": 1437.9674926622693,
"y": 140.24593205610267,
"strokeColor": "#1e1e1e",
"backgroundColor": "#12b886",
"width": 9.254842268716857,
"height": 52.1801369969441,
"seed": 1634053840,
"groupIds": [],
"frameId": null,
"roundness": {
"type": 2
},
"boundElements": [],
"updated": 1723757446539,
"link": null,
"locked": false,
"startBinding": {
"elementId": "vsA_CIfMv5pa_E8ntMeiT",
"focus": 0.005781863342302937,
"gap": 2.5808713031511914,
"fixedPoint": null
},
"endBinding": {
"elementId": "KWb-DkqaKPnpVL7f6V4bL",
"focus": -0.12844787041016997,
"gap": 1,
"fixedPoint": null
},
"lastCommittedPoint": null,
"startArrowhead": null,
"endArrowhead": "arrow",
"points": [
[
0,
0
],
[
9.254842268716857,
52.1801369969441
]
],
"elbowed": false
},
{
"type": "ellipse",
"version": 1354,
"versionNonce": 1998775504,
"index": "b0F",
"isDeleted": false,
"id": "g3vzES2f7nTxgtOAQHxlD",
"fillStyle": "hachure",
"strokeWidth": 1,
"strokeStyle": "solid",
"roughness": 1,
"opacity": 100,
"angle": 0.08267458081857892,
"x": 883.3984270653652,
"y": 445.3473162712001,
"strokeColor": "#1e1e1e",
"backgroundColor": "#12b886",
"width": 172.18810636886062,
"height": 51.92460878852706,
"seed": 602337488,
"groupIds": [
"Z2WNd7VHjMQwMoXLdzYAJ"
],
"frameId": null,
"roundness": {
"type": 2
},
"boundElements": [
{
"type": "text",
"id": "meTPYjzpBqS9qJaNPrOwm"
},
{
"id": "jtmxjvYMT8IyTg6YsEhEo",
"type": "arrow"
}
],
"updated": 1723757403784,
"link": null,
"locked": false
},
{
"type": "text",
"version": 1352,
"versionNonce": 424861392,
"index": "b0G",
"isDeleted": false,
"id": "meTPYjzpBqS9qJaNPrOwm",
"fillStyle": "hachure",
"strokeWidth": 1,
"strokeStyle": "dashed",
"roughness": 1,
"opacity": 100,
"angle": 0.08267458081857892,
"x": 921.58768418448,
"y": 458.205425502508,
"strokeColor": "#1e1e1e",
"backgroundColor": "#12b886",
"width": 96.05421447753906,
"height": 26.492147341085236,
"seed": 1097881296,
"groupIds": [
"Z2WNd7VHjMQwMoXLdzYAJ"
],
"frameId": null,
"roundness": null,
"boundElements": [],
"updated": 1723757402200,
"link": null,
"locked": false,
"fontSize": 21.193717872868188,
"fontFamily": 5,
"text": "Callable 4",
"textAlign": "center",
"verticalAlign": "middle",
"containerId": "g3vzES2f7nTxgtOAQHxlD",
"originalText": "Callable 4",
"autoResize": true,
"lineHeight": 1.25
},
{
"type": "arrow",
"version": 698,
"versionNonce": 1161193168,
"index": "b0H",
"isDeleted": false,
"id": "jtmxjvYMT8IyTg6YsEhEo",
"fillStyle": "hachure",
"strokeWidth": 1,
"strokeStyle": "solid",
"roughness": 1,
"opacity": 100,
"angle": 0,
"x": 762.4465004005942,
"y": 423.5712587597044,
"strokeColor": "#1e1e1e",
"backgroundColor": "#12b886",
"width": 123.52352373817519,
"height": 38.4442066961376,
"seed": 20683984,
"groupIds": [],
"frameId": null,
"roundness": {
"type": 2
},
"boundElements": [],
"updated": 1723757421788,
"link": null,
"locked": false,
"startBinding": {
"elementId": "mp5cf4rOe7-TBZhYPXY93",
"focus": 0.3656434333139051,
"gap": 1,
"fixedPoint": null
},
"endBinding": {
"elementId": "g3vzES2f7nTxgtOAQHxlD",
"focus": -0.506230526903665,
"gap": 1,
"fixedPoint": null
},
"lastCommittedPoint": null,
"startArrowhead": null,
"endArrowhead": "arrow",
"points": [
[
0,
0
],
[
123.52352373817519,
38.4442066961376
]
],
"elbowed": false
},
{
"type": "ellipse",
"version": 661,
"versionNonce": 383714512,
"index": "b0N",
"isDeleted": false,
"id": "_ov2Za4vMppZrq6E8DrGP",
"fillStyle": "hachure",
"strokeWidth": 2,
"strokeStyle": "solid",
"roughness": 1,
"opacity": 100,
"angle": 0,
"x": 151.76561742716592,
"y": 952.6445385915624,
"strokeColor": "#1e1e1e",
"backgroundColor": "#ffc9c9",
"width": 222.1171875,
"height": 153.02734374999997,
"seed": 1113625136,
"groupIds": [
"bkA1fovZjPU8kOsGeKQ3-"
],
"frameId": null,
"roundness": {
"type": 2
},
"boundElements": [
{
"id": "TaKI_Zsm5EtJ2RAoG-piT",
"type": "arrow"
}
],
"updated": 1723754636194,
"link": null,
"locked": false
},
{
"type": "text",
"version": 681,
"versionNonce": 803213520,
"index": "b0O",
"isDeleted": false,
"id": "1mKnrf8qs4l8Oo-0yvtPA",
"fillStyle": "solid",
"strokeWidth": 2,
"strokeStyle": "solid",
"roughness": 1,
"opacity": 100,
"angle": 0,
"x": 161.04686742716592,
"y": 1016.0898510915624,
"strokeColor": "#1e1e1e",
"backgroundColor": "#ffc9c9",
"width": 192.65985107421875,
"height": 25,
"seed": 384748592,
"groupIds": [
"bkA1fovZjPU8kOsGeKQ3-"
],
"frameId": null,
"roundness": null,
"boundElements": [],
"updated": 1723754636194,
"link": null,
"locked": false,
"fontSize": 20,
"fontFamily": 5,
"text": "AioClock Application",
"textAlign": "left",
"verticalAlign": "top",
"containerId": null,
"originalText": "AioClock Application",
"autoResize": true,
"lineHeight": 1.25
},
{
"type": "arrow",
"version": 600,
"versionNonce": 1145432784,
"index": "b0P",
"isDeleted": false,
"id": "TaKI_Zsm5EtJ2RAoG-piT",
"fillStyle": "hachure",
"strokeWidth": 1,
"strokeStyle": "solid",
"roughness": 1,
"opacity": 100,
"angle": 0,
"x": 366.722107585323,
"y": 1070.018365180686,
"strokeColor": "#1e1e1e",
"backgroundColor": "#fff9db",
"width": 201.3027018317324,
"height": 88.58508972177424,
"seed": 1313068752,
"groupIds": [],
"frameId": null,
"roundness": {
"type": 2
},
"boundElements": [],
"updated": 1723754636194,
"link": null,
"locked": false,
"startBinding": {
"elementId": "_ov2Za4vMppZrq6E8DrGP",
"focus": -0.05354174431970359,
"gap": 7.743665962249281,
"fixedPoint": null
},
"endBinding": {
"elementId": "pFHt3Tj9bXJpp0z2vze_z",
"focus": -0.05824999527775626,
"gap": 6.017602848102115,
"fixedPoint": null
},
"lastCommittedPoint": null,
"startArrowhead": null,
"endArrowhead": "arrow",
"points": [
[
0,
0
],
[
201.3027018317324,
88.58508972177424
]
],
"elbowed": false
},
{
"type": "rectangle",
"version": 363,
"versionNonce": 1642239184,
"index": "b0U",
"isDeleted": false,
"id": "pFHt3Tj9bXJpp0z2vze_z",
"fillStyle": "hachure",
"strokeWidth": 1,
"strokeStyle": "solid",
"roughness": 1,
"opacity": 100,
"angle": 0,
"x": 490.9445948885584,
"y": 1164.6210577505624,
"strokeColor": "#1e1e1e",
"backgroundColor": "#fff9db",
"width": 192.7956882911392,
"height": 126.8196202531644,
"seed": 761130192,
"groupIds": [],
"frameId": null,
"roundness": {
"type": 3
},
"boundElements": [
{
"type": "text",
"id": "97hUh-7kc56-J9lFD3Dlz"
},
{
"id": "TaKI_Zsm5EtJ2RAoG-piT",
"type": "arrow"
},
{
"id": "HUMibXR2jjlLuG3kA-AVo",
"type": "arrow"
}
],
"updated": 1723754634921,
"link": null,
"locked": false
},
{
"type": "text",
"version": 392,
"versionNonce": 268047568,
"index": "b0V",
"isDeleted": false,
"id": "97hUh-7kc56-J9lFD3Dlz",
"fillStyle": "hachure",
"strokeWidth": 1,
"strokeStyle": "solid",
"roughness": 1,
"opacity": 100,
"angle": 0,
"x": 499.052514412546,
"y": 1190.5308678771446,
"strokeColor": "#1e1e1e",
"backgroundColor": "#fff9db",
"width": 176.57984924316406,
"height": 75,
"seed": 1653197520,
"groupIds": [],
"frameId": null,
"roundness": null,
"boundElements": [],
"updated": 1723754844423,
"link": null,
"locked": false,
"fontSize": 20,
"fontFamily": 5,
"text": "Collect all \nassociated tasks \nto group or itself",
"textAlign": "center",
"verticalAlign": "middle",
"containerId": "pFHt3Tj9bXJpp0z2vze_z",
"originalText": "Collect all associated tasks to group or itself",
"autoResize": true,
"lineHeight": 1.25
},
{
"type": "diamond",
"version": 355,
"versionNonce": 379821616,
"index": "b0W",
"isDeleted": false,
"id": "PP-ng3MaLs-Vkm56oZm-j",
"fillStyle": "hachure",
"strokeWidth": 1,
"strokeStyle": "solid",
"roughness": 1,
"opacity": 100,
"angle": 0,
"x": 66.92703154630999,
"y": 1245.7362012582953,
"strokeColor": "#1e1e1e",
"backgroundColor": "#fff9db",
"width": 215.98101265822788,
"height": 220,
"seed": 729412304,
"groupIds": [],
"frameId": null,
"roundness": {
"type": 2
},
"boundElements": [
{
"id": "HUMibXR2jjlLuG3kA-AVo",
"type": "arrow"
},
{
"type": "text",
"id": "sHlsZZl8Ld-3G8tEpmfC8"
},
{
"id": "tFQlXQLJobY8BmOyCFy-C",
"type": "arrow"
},
{
"id": "bSYZ54fDSJlCiYxFIoqY3",
"type": "arrow"
}
],
"updated": 1723754297337,
"link": null,
"locked": false
},
{
"type": "text",
"version": 148,
"versionNonce": 1402237136,
"index": "b0WV",
"isDeleted": false,
"id": "sHlsZZl8Ld-3G8tEpmfC8",
"fillStyle": "hachure",
"strokeWidth": 1,
"strokeStyle": "solid",
"roughness": 1,
"opacity": 100,
"angle": 0,
"x": 130.18230973528102,
"y": 1305.7362012582953,
"strokeColor": "#1e1e1e",
"backgroundColor": "#fff9db",
"width": 89.47994995117188,
"height": 100,
"seed": 1549808848,
"groupIds": [],
"frameId": null,
"roundness": null,
"boundElements": [],
"updated": 1723754034163,
"link": null,
"locked": false,
"fontSize": 20,
"fontFamily": 5,
"text": "Is there \nany \nstartup \ntask?",
"textAlign": "center",
"verticalAlign": "middle",
"containerId": "PP-ng3MaLs-Vkm56oZm-j",
"originalText": "Is there any startup task?",
"autoResize": true,
"lineHeight": 1.25
},
{
"type": "arrow",
"version": 434,
"versionNonce": 1276378672,
"index": "b0X",
"isDeleted": false,
"id": "HUMibXR2jjlLuG3kA-AVo",
"fillStyle": "hachure",
"strokeWidth": 1,
"strokeStyle": "solid",
"roughness": 1,
"opacity": 100,
"angle": 0,
"x": 479.3000141923558,
"y": 1233.7891803622645,
"strokeColor": "#1e1e1e",
"backgroundColor": "#fff9db",
"width": 259.49965686083056,
"height": 31.466995486930045,
"seed": 890192080,
"groupIds": [],
"frameId": null,
"roundness": {
"type": 2
},
"boundElements": [],
"updated": 1723756062790,
"link": null,
"locked": false,
"startBinding": {
"elementId": "pFHt3Tj9bXJpp0z2vze_z",
"focus": -0.024001291159690904,
"gap": 11.644580696202638,
"fixedPoint": null
},
"endBinding": {
"elementId": "PP-ng3MaLs-Vkm56oZm-j",
"focus": -0.7375903662329082,
"gap": 18.35325627629757,
"fixedPoint": null
},
"lastCommittedPoint": null,
"startArrowhead": null,
"endArrowhead": "arrow",
"points": [
[
0,
0
],
[
-132.86603922833922,
5.100516049794578
],
[
-259.49965686083056,
31.466995486930045
]
],
"elbowed": false
},
{
"type": "arrow",
"version": 281,
"versionNonce": 1258562608,
"index": "b0Y",
"isDeleted": false,
"id": "tFQlXQLJobY8BmOyCFy-C",
"fillStyle": "hachure",
"strokeWidth": 1,
"strokeStyle": "solid",
"roughness": 1,
"opacity": 100,
"angle": 0,
"x": 287.56539981706385,
"y": 1370.7373442086102,
"strokeColor": "#1e1e1e",
"backgroundColor": "#fff9db",
"width": 186.4989873674889,
"height": 78.94752973813888,
"seed": 73693232,
"groupIds": [],
"frameId": null,
"roundness": {
"type": 2
},
"boundElements": [],
"updated": 1723756074327,
"link": null,
"locked": false,
"startBinding": {
"elementId": "PP-ng3MaLs-Vkm56oZm-j",
"focus": -0.2971292009789168,
"gap": 13.832646064717963,
"fixedPoint": null
},
"endBinding": {
"elementId": "n9ToP1sTho6bZwvzaSX1v",
"focus": 0.24979688288014204,
"gap": 1,
"fixedPoint": null
},
"lastCommittedPoint": null,
"startArrowhead": null,
"endArrowhead": "arrow",
"points": [
[
0,
0
],
[
186.4989873674889,
78.94752973813888
]
],
"elbowed": false
},
{
"type": "rectangle",
"version": 178,
"versionNonce": 1493577776,
"index": "b0Z",
"isDeleted": false,
"id": "n9ToP1sTho6bZwvzaSX1v",
"fillStyle": "hachure",
"strokeWidth": 1,
"strokeStyle": "solid",
"roughness": 1,
"opacity": 100,
"angle": 0,
"x": 474.5694147504952,
"y": 1446.1572874752337,
"strokeColor": "#1e1e1e",
"backgroundColor": "#fff9db",
"width": 186.3429588607594,
"height": 141.38647151898752,
"seed": 526215728,
"groupIds": [],
"frameId": null,
"roundness": {
"type": 3
},
"boundElements": [
{
"id": "tFQlXQLJobY8BmOyCFy-C",
"type": "arrow"
},
{
"type": "text",
"id": "xqy3QNcMbTQdr-7Y-CAPu"
},
{
"id": "K6tLFhiw5Uin1d-gyZMMa",
"type": "arrow"
}
],
"updated": 1723755086801,
"link": null,
"locked": false
},
{
"type": "text",
"version": 277,
"versionNonce": 1410606288,
"index": "b0ZG",
"isDeleted": false,
"id": "xqy3QNcMbTQdr-7Y-CAPu",
"fillStyle": "hachure",
"strokeWidth": 1,
"strokeStyle": "solid",
"roughness": 1,
"opacity": 100,
"angle": 0,
"x": 487.16095031533723,
"y": 1491.8505232347275,
"strokeColor": "#1e1e1e",
"backgroundColor": "#fff9db",
"width": 161.1598877310753,
"height": 50,
"seed": 1089100336,
"groupIds": [],
"frameId": null,
"roundness": null,
"boundElements": [],
"updated": 1723755913354,
"link": null,
"locked": false,
"fontSize": 20,
"fontFamily": 5,
"text": "Run the task, \nwith task runner",
"textAlign": "center",
"verticalAlign": "middle",
"containerId": "n9ToP1sTho6bZwvzaSX1v",
"originalText": "Run the task, with task runner",
"autoResize": true,
"lineHeight": 1.25
},
{
"type": "arrow",
"version": 7,
"versionNonce": 1431708368,
"index": "b0a",
"isDeleted": false,
"id": "toXI5aAPeghGS-UojT5Dr",
"fillStyle": "hachure",
"strokeWidth": 1,
"strokeStyle": "solid",
"roughness": 1,
"opacity": 100,
"angle": 0,
"x": 498.18197616656346,
"y": 1544.9994528405732,
"strokeColor": "#1e1e1e",
"backgroundColor": "#fff9db",
"width": 0,
"height": 0,
"seed": 45031120,
"groupIds": [],
"frameId": null,
"roundness": {
"type": 2
},
"boundElements": [],
"updated": 1723754060374,
"link": null,
"locked": false,
"startBinding": null,
"endBinding": null,
"lastCommittedPoint": null,
"startArrowhead": null,
"endArrowhead": "arrow",
"points": [
[
0,
0
],
[
0,
0
]
],
"elbowed": false
},
{
"type": "text",
"version": 189,
"versionNonce": 1948207152,
"index": "b0b",
"isDeleted": false,
"id": "HEyjXhFgmGSg4l_cVGwpo",
"fillStyle": "hachure",
"strokeWidth": 1,
"strokeStyle": "solid",
"roughness": 1,
"opacity": 100,
"angle": 0.38252949035119954,
"x": 367.22159326534563,
"y": 1362.8041428944932,
"strokeColor": "#1e1e1e",
"backgroundColor": "#fff9db",
"width": 40.7856642036145,
"height": 31.587420437145056,
"seed": 1679624912,
"groupIds": [],
"frameId": null,
"roundness": null,
"boundElements": [],
"updated": 1723756076678,
"link": null,
"locked": false,
"fontSize": 25.26993634971605,
"fontFamily": 5,
"text": "Yes",
"textAlign": "left",
"verticalAlign": "top",
"containerId": null,
"originalText": "Yes",
"autoResize": true,
"lineHeight": 1.25
},
{
"type": "arrow",
"version": 262,
"versionNonce": 108425776,
"index": "b0c",
"isDeleted": false,
"id": "bSYZ54fDSJlCiYxFIoqY3",
"fillStyle": "hachure",
"strokeWidth": 1,
"strokeStyle": "solid",
"roughness": 1,
"opacity": 100,
"angle": 0,
"x": 120.14961629042963,
"y": 1427.5564598860278,
"strokeColor": "#1e1e1e",
"backgroundColor": "#fff9db",
"width": 30.311807014930793,
"height": 138.16917102811112,
"seed": 1295594544,
"groupIds": [],
"frameId": null,
"roundness": {
"type": 2
},
"boundElements": [],
"updated": 1723754482167,
"link": null,
"locked": false,
"startBinding": {
"elementId": "PP-ng3MaLs-Vkm56oZm-j",
"focus": 0.6529344937699867,
"gap": 12.334950998376627,
"fixedPoint": null
},
"endBinding": {
"elementId": "62AnXeQAIeDdvmIAHLsql",
"focus": 0.18626692128435784,
"gap": 1,
"fixedPoint": null
},
"lastCommittedPoint": null,
"startArrowhead": null,
"endArrowhead": "arrow",
"points": [
[
0,
0
],
[
30.311807014930793,
138.16917102811112
]
],
"elbowed": false
},
{
"type": "text",
"version": 225,
"versionNonce": 1193407536,
"index": "b0d",
"isDeleted": false,
"id": "UB0T4WsJRhDst_ROu99CV",
"fillStyle": "hachure",
"strokeWidth": 1,
"strokeStyle": "solid",
"roughness": 1,
"opacity": 100,
"angle": 6.193731223285352,
"x": 69.15357436383988,
"y": 1481.907016430885,
"strokeColor": "#1e1e1e",
"backgroundColor": "#fff9db",
"width": 35.288696305989404,
"height": 35.68840598676002,
"seed": 1811917360,
"groupIds": [],
"frameId": null,
"roundness": null,
"boundElements": [],
"updated": 1723754219032,
"link": null,
"locked": false,
"fontSize": 28.550724789408026,
"fontFamily": 5,
"text": "No",
"textAlign": "left",
"verticalAlign": "top",
"containerId": null,
"originalText": "No",
"autoResize": true,
"lineHeight": 1.25
},
{
"type": "arrow",
"version": 202,
"versionNonce": 1599069744,
"index": "b0g",
"isDeleted": false,
"id": "K6tLFhiw5Uin1d-gyZMMa",
"fillStyle": "hachure",
"strokeWidth": 1,
"strokeStyle": "solid",
"roughness": 1,
"opacity": 100,
"angle": 0,
"x": 473.5694147504952,
"y": 1577.2840394159837,
"strokeColor": "#1e1e1e",
"backgroundColor": "#fff9db",
"width": 222.31777593586875,
"height": 74.27226068666982,
"seed": 658627120,
"groupIds": [],
"frameId": null,
"roundness": {
"type": 2
},
"boundElements": [],
"updated": 1723755086801,
"link": null,
"locked": false,
"startBinding": {
"elementId": "n9ToP1sTho6bZwvzaSX1v",
"focus": -0.2845466033490556,
"gap": 1,
"fixedPoint": null
},
"endBinding": {
"elementId": "62AnXeQAIeDdvmIAHLsql",
"focus": 0.09532978924614155,
"gap": 7.742822789853733,
"fixedPoint": null
},
"lastCommittedPoint": null,
"startArrowhead": null,
"endArrowhead": "arrow",
"points": [
[
0,
0
],
[
-222.31777593586875,
74.27226068666982
]
],
"elbowed": false
},
{
"type": "text",
"version": 522,
"versionNonce": 1066451664,
"index": "b0i",
"isDeleted": false,
"id": "NTr0E-uAaLqNxcqR-y62m",
"fillStyle": "hachure",
"strokeWidth": 1,
"strokeStyle": "solid",
"roughness": 1,
"opacity": 100,
"angle": 5.950908548183436,
"x": 259.0621979655733,
"y": 1572.151673564935,
"strokeColor": "#1e1e1e",
"backgroundColor": "#fff9db",
"width": 204.45667309363057,
"height": 24.853187128468736,
"seed": 393975504,
"groupIds": [],
"frameId": null,
"roundness": null,
"boundElements": [],
"updated": 1723754267288,
"link": null,
"locked": false,
"fontSize": 19.88254970277498,
"fontFamily": 5,
"text": "Eventually it finishes",
"textAlign": "left",
"verticalAlign": "top",
"containerId": null,
"originalText": "Eventually it finishes",
"autoResize": true,
"lineHeight": 1.25
},
{
"type": "diamond",
"version": 415,
"versionNonce": 1831616048,
"index": "b0j",
"isDeleted": false,
"id": "62AnXeQAIeDdvmIAHLsql",
"fillStyle": "hachure",
"strokeWidth": 1,
"strokeStyle": "solid",
"roughness": 1,
"opacity": 100,
"angle": 0,
"x": 46.010126614696446,
"y": 1563.5479955472367,
"strokeColor": "#1e1e1e",
"backgroundColor": "#fff9db",
"width": 215.98101265822788,
"height": 220,
"seed": 2045036240,
"groupIds": [],
"frameId": null,
"roundness": {
"type": 2
},
"boundElements": [
{
"type": "text",
"id": "-ThMjAXaf2xYE8np1dZMC"
},
{
"id": "bSYZ54fDSJlCiYxFIoqY3",
"type": "arrow"
},
{
"id": "K6tLFhiw5Uin1d-gyZMMa",
"type": "arrow"
},
{
"id": "F8dKp3225ek0XJx7j5nTi",
"type": "arrow"
}
],
"updated": 1723754482166,
"link": null,
"locked": false
},
{
"type": "text",
"version": 215,
"versionNonce": 1130690608,
"index": "b0k",
"isDeleted": false,
"id": "-ThMjAXaf2xYE8np1dZMC",
"fillStyle": "hachure",
"strokeWidth": 1,
"strokeStyle": "solid",
"roughness": 1,
"opacity": 100,
"angle": 0,
"x": 106.71540282479327,
"y": 1636.0479955472367,
"strokeColor": "#1e1e1e",
"backgroundColor": "#fff9db",
"width": 94.57995390892029,
"height": 75,
"seed": 88304848,
"groupIds": [],
"frameId": null,
"roundness": null,
"boundElements": [],
"updated": 1723754482166,
"link": null,
"locked": false,
"fontSize": 20,
"fontFamily": 5,
"text": "Is there \nany other\ntask?",
"textAlign": "center",
"verticalAlign": "middle",
"containerId": "62AnXeQAIeDdvmIAHLsql",
"originalText": "Is there any other task?",
"autoResize": true,
"lineHeight": 1.25
},
{
"type": "arrow",
"version": 73,
"versionNonce": 1371776560,
"index": "b0l",
"isDeleted": false,
"id": "F8dKp3225ek0XJx7j5nTi",
"fillStyle": "hachure",
"strokeWidth": 1,
"strokeStyle": "solid",
"roughness": 1,
"opacity": 100,
"angle": 0,
"x": 261.20140736398764,
"y": 1700.7333461944968,
"strokeColor": "#1e1e1e",
"backgroundColor": "#fff9db",
"width": 177.29937870676588,
"height": 111.56354900062706,
"seed": 1441168592,
"groupIds": [],
"frameId": null,
"roundness": {
"type": 2
},
"boundElements": [],
"updated": 1723754482167,
"link": null,
"locked": false,
"startBinding": {
"elementId": "62AnXeQAIeDdvmIAHLsql",
"focus": -0.36608614517760474,
"gap": 18.481385916632377,
"fixedPoint": null
},
"endBinding": null,
"lastCommittedPoint": null,
"startArrowhead": null,
"endArrowhead": "arrow",
"points": [
[
0,
0
],
[
177.29937870676588,
111.56354900062706
]
],
"elbowed": false
},
{
"type": "rectangle",
"version": 208,
"versionNonce": 978137296,
"index": "b0m",
"isDeleted": false,
"id": "pTPLz0pnwCGZc1m9XPrVU",
"fillStyle": "hachure",
"strokeWidth": 1,
"strokeStyle": "solid",
"roughness": 1,
"opacity": 100,
"angle": 0,
"x": 444.23109718020464,
"y": 1790.604336075719,
"strokeColor": "#1e1e1e",
"backgroundColor": "#fff9db",
"width": 186.3429588607594,
"height": 141.38647151898752,
"seed": 1098873904,
"groupIds": [],
"frameId": null,
"roundness": {
"type": 3
},
"boundElements": [
{
"type": "text",
"id": "Jx4puZXo9muudZA7Is-ul"
},
{
"id": "hsgEKlKrummvl8bI0o12-",
"type": "arrow"
}
],
"updated": 1723755916084,
"link": null,
"locked": false
},
{
"type": "text",
"version": 284,
"versionNonce": 1694913744,
"index": "b0n",
"isDeleted": false,
"id": "Jx4puZXo9muudZA7Is-ul",
"fillStyle": "hachure",
"strokeWidth": 1,
"strokeStyle": "solid",
"roughness": 1,
"opacity": 100,
"angle": 0,
"x": 456.8226327450467,
"y": 1836.2975718352127,
"strokeColor": "#1e1e1e",
"backgroundColor": "#fff9db",
"width": 161.1598877310753,
"height": 50,
"seed": 1721792048,
"groupIds": [],
"frameId": null,
"roundness": null,
"boundElements": [],
"updated": 1723755917136,
"link": null,
"locked": false,
"fontSize": 20,
"fontFamily": 5,
"text": "Run the task, \nwith task runner",
"textAlign": "center",
"verticalAlign": "middle",
"containerId": "pTPLz0pnwCGZc1m9XPrVU",
"originalText": "Run the task, with task runner",
"autoResize": true,
"lineHeight": 1.25
},
{
"type": "text",
"version": 233,
"versionNonce": 785521712,
"index": "b0o",
"isDeleted": false,
"id": "AZ8Lb1Qf-UBPg__N6Ni3k",
"fillStyle": "hachure",
"strokeWidth": 1,
"strokeStyle": "solid",
"roughness": 1,
"opacity": 100,
"angle": 0.5690110473277814,
"x": 354.8587843212615,
"y": 1727.2910370892278,
"strokeColor": "#1e1e1e",
"backgroundColor": "#fff9db",
"width": 40.7856642036145,
"height": 31.587420437145056,
"seed": 471380016,
"groupIds": [],
"frameId": null,
"roundness": null,
"boundElements": [],
"updated": 1723754472533,
"link": null,
"locked": false,
"fontSize": 25.26993634971605,
"fontFamily": 5,
"text": "Yes",
"textAlign": "left",
"verticalAlign": "top",
"containerId": null,
"originalText": "Yes",
"autoResize": true,
"lineHeight": 1.25
},
{
"type": "arrow",
"version": 308,
"versionNonce": 1701565648,
"index": "b0p",
"isDeleted": false,
"id": "tgY3B-VJozPI8jwqQqydk",
"fillStyle": "hachure",
"strokeWidth": 1,
"strokeStyle": "solid",
"roughness": 1,
"opacity": 100,
"angle": 0,
"x": 155.4336876594124,
"y": 1780.766675660775,
"strokeColor": "#1e1e1e",
"backgroundColor": "#fff9db",
"width": 30.611100340859906,
"height": 139.6509163254666,
"seed": 257645264,
"groupIds": [],
"frameId": null,
"roundness": {
"type": 2
},
"boundElements": [],
"updated": 1723754456616,
"link": null,
"locked": false,
"startBinding": null,
"endBinding": null,
"lastCommittedPoint": null,
"startArrowhead": null,
"endArrowhead": "arrow",
"points": [
[
0,
0
],
[
30.611100340859906,
139.6509163254666
]
],
"elbowed": false
},
{
"type": "text",
"version": 272,
"versionNonce": 1220066000,
"index": "b0q",
"isDeleted": false,
"id": "PcRpuw1zwSsRVNUhXieye",
"fillStyle": "hachure",
"strokeWidth": 1,
"strokeStyle": "solid",
"roughness": 1,
"opacity": 100,
"angle": 6.193731223285352,
"x": 104.43764573282266,
"y": 1835.1172322056318,
"strokeColor": "#1e1e1e",
"backgroundColor": "#fff9db",
"width": 35.288696305989404,
"height": 35.68840598676002,
"seed": 1455915216,
"groupIds": [],
"frameId": null,
"roundness": null,
"boundElements": [],
"updated": 1723754456617,
"link": null,
"locked": false,
"fontSize": 28.550724789408026,
"fontFamily": 5,
"text": "No",
"textAlign": "left",
"verticalAlign": "top",
"containerId": null,
"originalText": "No",
"autoResize": true,
"lineHeight": 1.25
},
{
"type": "diamond",
"version": 479,
"versionNonce": 1734766640,
"index": "b0r",
"isDeleted": false,
"id": "j6CjjAOcI4Pq1MVQ1M24r",
"fillStyle": "hachure",
"strokeWidth": 1,
"strokeStyle": "solid",
"roughness": 1,
"opacity": 100,
"angle": 0,
"x": 66.74944636156056,
"y": 1929.4260765331521,
"strokeColor": "#1e1e1e",
"backgroundColor": "#fff9db",
"width": 215.98101265822788,
"height": 220,
"seed": 909420752,
"groupIds": [],
"frameId": null,
"roundness": {
"type": 2
},
"boundElements": [
{
"type": "text",
"id": "n5-9AXrgT7uZ3e_HhcKwI"
},
{
"id": "hsgEKlKrummvl8bI0o12-",
"type": "arrow"
},
{
"id": "8-EVaSqf6ZR64L11bet06",
"type": "arrow"
}
],
"updated": 1723754520983,
"link": null,
"locked": false
},
{
"type": "text",
"version": 299,
"versionNonce": 515520560,
"index": "b0s",
"isDeleted": false,
"id": "n5-9AXrgT7uZ3e_HhcKwI",
"fillStyle": "hachure",
"strokeWidth": 1,
"strokeStyle": "solid",
"roughness": 1,
"opacity": 100,
"angle": 0,
"x": 128.39473156957456,
"y": 1989.4260765331521,
"strokeColor": "#1e1e1e",
"backgroundColor": "#fff9db",
"width": 92.69993591308594,
"height": 100,
"seed": 826740432,
"groupIds": [],
"frameId": null,
"roundness": null,
"boundElements": [],
"updated": 1723754452537,
"link": null,
"locked": false,
"fontSize": 20,
"fontFamily": 5,
"text": "Is there \nany \nshutdown\ntask?",
"textAlign": "center",
"verticalAlign": "middle",
"containerId": "j6CjjAOcI4Pq1MVQ1M24r",
"originalText": "Is there any shutdown task?",
"autoResize": true,
"lineHeight": 1.25
},
{
"type": "arrow",
"version": 264,
"versionNonce": 1454587088,
"index": "b0t",
"isDeleted": false,
"id": "hsgEKlKrummvl8bI0o12-",
"fillStyle": "hachure",
"strokeWidth": 1,
"strokeStyle": "solid",
"roughness": 1,
"opacity": 100,
"angle": 0,
"x": 487.1046231869103,
"y": 1939.953951382775,
"strokeColor": "#1e1e1e",
"backgroundColor": "#fff9db",
"width": 199.08000455324208,
"height": 98.80596639902069,
"seed": 371455696,
"groupIds": [],
"frameId": null,
"roundness": {
"type": 2
},
"boundElements": [],
"updated": 1723755916084,
"link": null,
"locked": false,
"startBinding": {
"elementId": "pTPLz0pnwCGZc1m9XPrVU",
"focus": -0.45916668906629854,
"gap": 7.963143788068464,
"fixedPoint": null
},
"endBinding": {
"elementId": "j6CjjAOcI4Pq1MVQ1M24r",
"focus": 0.5126982685313142,
"gap": 5.335906061696036,
"fixedPoint": null
},
"lastCommittedPoint": null,
"startArrowhead": null,
"endArrowhead": "arrow",
"points": [
[
0,
0
],
[
-199.08000455324208,
98.80596639902069
]
],
"elbowed": false
},
{
"type": "text",
"version": 697,
"versionNonce": 572080176,
"index": "b0u",
"isDeleted": false,
"id": "yww7vnZfDtkn23qZV_9MI",
"fillStyle": "hachure",
"strokeWidth": 1,
"strokeStyle": "solid",
"roughness": 1,
"opacity": 100,
"angle": 5.847034041833071,
"x": 250.532589632297,
"y": 1953.4169380927945,
"strokeColor": "#1e1e1e",
"backgroundColor": "#fff9db",
"width": 204.45667309363057,
"height": 24.853187128468736,
"seed": 840735952,
"groupIds": [],
"frameId": null,
"roundness": null,
"boundElements": [],
"updated": 1723754414914,
"link": null,
"locked": false,
"fontSize": 19.88254970277498,
"fontFamily": 5,
"text": "Eventually it finishes",
"textAlign": "left",
"verticalAlign": "top",
"containerId": null,
"originalText": "Eventually it finishes",
"autoResize": true,
"lineHeight": 1.25
},
{
"type": "text",
"version": 399,
"versionNonce": 361233616,
"index": "b0w",
"isDeleted": false,
"id": "X-0iInYecUG1BjmK_DZBY",
"fillStyle": "hachure",
"strokeWidth": 1,
"strokeStyle": "solid",
"roughness": 1,
"opacity": 100,
"angle": 5.811901839719698,
"x": 298.9133895614428,
"y": 2003.2861778230986,
"strokeColor": "#e03131",
"backgroundColor": "#fff9db",
"width": 229.47476490363073,
"height": 25.90321232416628,
"seed": 1632365104,
"groupIds": [],
"frameId": null,
"roundness": null,
"boundElements": [],
"updated": 1723756400828,
"link": null,
"locked": false,
"fontSize": 20.722569859333024,
"fontFamily": 5,
"text": "Or graceful shutdown",
"textAlign": "left",
"verticalAlign": "top",
"containerId": null,
"originalText": "Or graceful shutdown",
"autoResize": false,
"lineHeight": 1.25
},
{
"type": "arrow",
"version": 382,
"versionNonce": 1475396304,
"index": "b0x",
"isDeleted": false,
"id": "8-EVaSqf6ZR64L11bet06",
"fillStyle": "hachure",
"strokeWidth": 1,
"strokeStyle": "solid",
"roughness": 1,
"opacity": 100,
"angle": 0,
"x": 179.27769129424638,
"y": 2147.664404904931,
"strokeColor": "#1e1e1e",
"backgroundColor": "#fff9db",
"width": 17.031897197660243,
"height": 135.17219951404422,
"seed": 1723002064,
"groupIds": [],
"frameId": null,
"roundness": {
"type": 2
},
"boundElements": [],
"updated": 1723754619242,
"link": null,
"locked": false,
"startBinding": {
"elementId": "j6CjjAOcI4Pq1MVQ1M24r",
"focus": 0.13740406747958717,
"gap": 2.003952060075946,
"fixedPoint": null
},
"endBinding": {
"elementId": "mqiZadO7gUjCdwEG95rN-",
"focus": 0.08481186725898551,
"gap": 19.5792698927537,
"fixedPoint": null
},
"lastCommittedPoint": null,
"startArrowhead": null,
"endArrowhead": "arrow",
"points": [
[
0,
0
],
[
17.031897197660243,
135.17219951404422
]
],
"elbowed": false
},
{
"type": "text",
"version": 328,
"versionNonce": 874861264,
"index": "b0y",
"isDeleted": false,
"id": "sliZS3n4NM5VWGWLk07qL",
"fillStyle": "hachure",
"strokeWidth": 1,
"strokeStyle": "solid",
"roughness": 1,
"opacity": 100,
"angle": 6.193731223285352,
"x": 131.1640305530422,
"y": 2195.4529447085906,
"strokeColor": "#1e1e1e",
"backgroundColor": "#fff9db",
"width": 35.288696305989404,
"height": 35.68840598676002,
"seed": 1096602320,
"groupIds": [],
"frameId": null,
"roundness": null,
"boundElements": [],
"updated": 1723754523120,
"link": null,
"locked": false,
"fontSize": 28.550724789408026,
"fontFamily": 5,
"text": "No",
"textAlign": "left",
"verticalAlign": "top",
"containerId": null,
"originalText": "No",
"autoResize": true,
"lineHeight": 1.25
},
{
"type": "arrow",
"version": 611,
"versionNonce": 1697792048,
"index": "b0z",
"isDeleted": false,
"id": "jlL5CJ4DT5zCd_RjrZb1x",
"fillStyle": "hachure",
"strokeWidth": 1,
"strokeStyle": "solid",
"roughness": 1,
"opacity": 100,
"angle": 0,
"x": 259.14555139206345,
"y": 2089.8461994879863,
"strokeColor": "#1e1e1e",
"backgroundColor": "#fff9db",
"width": 185.6005502713267,
"height": 92.47327948141628,
"seed": 1638863568,
"groupIds": [],
"frameId": null,
"roundness": {
"type": 2
},
"boundElements": [],
"updated": 1723755096802,
"link": null,
"locked": false,
"startBinding": null,
"endBinding": {
"elementId": "SLDezLygbRJ2VvbEepnBf",
"focus": 0.2602874090619763,
"gap": 1,
"fixedPoint": null
},
"lastCommittedPoint": null,
"startArrowhead": null,
"endArrowhead": "arrow",
"points": [
[
0,
0
],
[
185.6005502713267,
92.47327948141628
]
],
"elbowed": false
},
{
"type": "rectangle",
"version": 452,
"versionNonce": 1390644272,
"index": "b10",
"isDeleted": false,
"id": "SLDezLygbRJ2VvbEepnBf",
"fillStyle": "hachure",
"strokeWidth": 1,
"strokeStyle": "solid",
"roughness": 1,
"opacity": 100,
"angle": 0,
"x": 434.2855099184445,
"y": 2183.3194789694026,
"strokeColor": "#1e1e1e",
"backgroundColor": "#fff9db",
"width": 186.3429588607594,
"height": 141.38647151898752,
"seed": 367278288,
"groupIds": [],
"frameId": null,
"roundness": {
"type": 3
},
"boundElements": [
{
"id": "jlL5CJ4DT5zCd_RjrZb1x",
"type": "arrow"
},
{
"type": "text",
"id": "QA47B66xhfSSWURPoI2Wj"
},
{
"id": "I3Wcv2heCBjVL8wJ7cxju",
"type": "arrow"
}
],
"updated": 1723755096802,
"link": null,
"locked": false
},
{
"type": "text",
"version": 525,
"versionNonce": 778602544,
"index": "b11",
"isDeleted": false,
"id": "QA47B66xhfSSWURPoI2Wj",
"fillStyle": "hachure",
"strokeWidth": 1,
"strokeStyle": "solid",
"roughness": 1,
"opacity": 100,
"angle": 0,
"x": 446.87704548328657,
"y": 2229.0127147288963,
"strokeColor": "#1e1e1e",
"backgroundColor": "#fff9db",
"width": 161.1598877310753,
"height": 50,
"seed": 344235728,
"groupIds": [],
"frameId": null,
"roundness": null,
"boundElements": [],
"updated": 1723755919085,
"link": null,
"locked": false,
"fontSize": 20,
"fontFamily": 5,
"text": "Run the task, \nwith task runner",
"textAlign": "center",
"verticalAlign": "middle",
"containerId": "SLDezLygbRJ2VvbEepnBf",
"originalText": "Run the task, with task runner",
"autoResize": true,
"lineHeight": 1.25
},
{
"type": "text",
"version": 339,
"versionNonce": 1846783696,
"index": "b12",
"isDeleted": false,
"id": "kcV9WES9r3Hgum1vq3lSr",
"fillStyle": "hachure",
"strokeWidth": 1,
"strokeStyle": "solid",
"roughness": 1,
"opacity": 100,
"angle": 0.49425269310474107,
"x": 399.1123171761782,
"y": 2116.967520578905,
"strokeColor": "#1e1e1e",
"backgroundColor": "#fff9db",
"width": 40.7856642036145,
"height": 31.587420437145056,
"seed": 1348176592,
"groupIds": [],
"frameId": null,
"roundness": null,
"boundElements": [],
"updated": 1723754505234,
"link": null,
"locked": false,
"fontSize": 25.26993634971605,
"fontFamily": 5,
"text": "Yes",
"textAlign": "left",
"verticalAlign": "top",
"containerId": null,
"originalText": "Yes",
"autoResize": true,
"lineHeight": 1.25
},
{
"type": "rectangle",
"version": 398,
"versionNonce": 915588656,
"index": "b13",
"isDeleted": false,
"id": "mqiZadO7gUjCdwEG95rN-",
"fillStyle": "hachure",
"strokeWidth": 1,
"strokeStyle": "solid",
"roughness": 1,
"opacity": 100,
"angle": 0,
"x": 120.53834260665997,
"y": 2302.415874311729,
"strokeColor": "#1e1e1e",
"backgroundColor": "#ffc9c9",
"width": 157.7848984637119,
"height": 127.39363519702648,
"seed": 699762384,
"groupIds": [],
"frameId": null,
"roundness": {
"type": 3
},
"boundElements": [
{
"type": "text",
"id": "Wn6LI4a9xruvOuYApEe8l"
},
{
"id": "8-EVaSqf6ZR64L11bet06",
"type": "arrow"
},
{
"id": "I3Wcv2heCBjVL8wJ7cxju",
"type": "arrow"
}
],
"updated": 1723754621541,
"link": null,
"locked": false
},
{
"type": "text",
"version": 472,
"versionNonce": 1023728848,
"index": "b14",
"isDeleted": false,
"id": "Wn6LI4a9xruvOuYApEe8l",
"fillStyle": "hachure",
"strokeWidth": 1,
"strokeStyle": "solid",
"roughness": 1,
"opacity": 100,
"angle": 0,
"x": 178.48080251966826,
"y": 2353.6126919102426,
"strokeColor": "#1e1e1e",
"backgroundColor": "#fff9db",
"width": 41.89997863769531,
"height": 25,
"seed": 2096901328,
"groupIds": [],
"frameId": null,
"roundness": null,
"boundElements": [],
"updated": 1723754619242,
"link": null,
"locked": false,
"fontSize": 20,
"fontFamily": 5,
"text": "Exit",
"textAlign": "center",
"verticalAlign": "middle",
"containerId": "mqiZadO7gUjCdwEG95rN-",
"originalText": "Exit",
"autoResize": true,
"lineHeight": 1.25
},
{
"type": "arrow",
"version": 399,
"versionNonce": 257604144,
"index": "b15",
"isDeleted": false,
"id": "I3Wcv2heCBjVL8wJ7cxju",
"fillStyle": "hachure",
"strokeWidth": 1,
"strokeStyle": "solid",
"roughness": 1,
"opacity": 100,
"angle": 0,
"x": 520.6298569442847,
"y": 2334.4068649622322,
"strokeColor": "#1e1e1e",
"backgroundColor": "#fff9db",
"width": 228.38910390384757,
"height": 45.70347845481592,
"seed": 1391728848,
"groupIds": [],
"frameId": null,
"roundness": {
"type": 2
},
"boundElements": [],
"updated": 1723755096802,
"link": null,
"locked": false,
"startBinding": {
"elementId": "SLDezLygbRJ2VvbEepnBf",
"focus": -0.8845951098115866,
"gap": 9.700914473842204,
"fixedPoint": null
},
"endBinding": {
"elementId": "mqiZadO7gUjCdwEG95rN-",
"focus": 0.4096596647723168,
"gap": 13.917511970065277,
"fixedPoint": null
},
"lastCommittedPoint": null,
"startArrowhead": null,
"endArrowhead": "arrow",
"points": [
[
0,
0
],
[
-228.38910390384757,
45.70347845481592
]
],
"elbowed": false
},
{
"type": "text",
"version": 841,
"versionNonce": 89012432,
"index": "b16",
"isDeleted": false,
"id": "UP2QFcgsa8fPXFOeQWORR",
"fillStyle": "hachure",
"strokeWidth": 1,
"strokeStyle": "solid",
"roughness": 1,
"opacity": 100,
"angle": 6.056032201216738,
"x": 324.87050612396354,
"y": 2388.028964675776,
"strokeColor": "#1e1e1e",
"backgroundColor": "#fff9db",
"width": 204.45667309363057,
"height": 24.853187128468736,
"seed": 1687715536,
"groupIds": [],
"frameId": null,
"roundness": null,
"boundElements": [],
"updated": 1723754533104,
"link": null,
"locked": false,
"fontSize": 19.88254970277498,
"fontFamily": 5,
"text": "Eventually it finishes",
"textAlign": "left",
"verticalAlign": "top",
"containerId": null,
"originalText": "Eventually it finishes",
"autoResize": true,
"lineHeight": 1.25
},
{
"type": "text",
"version": 225,
"versionNonce": 705319984,
"index": "b17",
"isDeleted": false,
"id": "AdDzyQ-BQk5kpMri0E72R",
"fillStyle": "hachure",
"strokeWidth": 1,
"strokeStyle": "solid",
"roughness": 1,
"opacity": 100,
"angle": 0.3899896189126908,
"x": 389.28497787797653,
"y": 1072.2854853849283,
"strokeColor": "#e03131",
"backgroundColor": "#fff9db",
"width": 238.16266248983007,
"height": 29.68216517673118,
"seed": 684030672,
"groupIds": [],
"frameId": null,
"roundness": null,
"boundElements": [],
"updated": 1723754643543,
"link": null,
"locked": false,
"fontSize": 23.74573214138495,
"fontFamily": 5,
"text": "await app.serve()",
"textAlign": "left",
"verticalAlign": "top",
"containerId": null,
"originalText": "await app.serve()",
"autoResize": false,
"lineHeight": 1.25
},
{
"type": "rectangle",
"version": 832,
"versionNonce": 1154620112,
"index": "b1X",
"isDeleted": false,
"id": "IQuQeqT5jHaGToLEJeScY",
"fillStyle": "hachure",
"strokeWidth": 1,
"strokeStyle": "solid",
"roughness": 1,
"opacity": 100,
"angle": 0,
"x": 1698.9602512692113,
"y": 871.1782058755693,
"strokeColor": "#1e1e1e",
"backgroundColor": "#d0bfff",
"width": 349.58787444021567,
"height": 241.12794293132154,
"seed": 737447472,
"groupIds": [],
"frameId": null,
"roundness": {
"type": 3
},
"boundElements": [
{
"id": "-GFO_l2VPtxG5Bc-I2kuy",
"type": "text"
}
],
"updated": 1723756979113,
"link": null,
"locked": false
},
{
"type": "text",
"version": 890,
"versionNonce": 795230416,
"index": "b1XV",
"isDeleted": false,
"id": "-GFO_l2VPtxG5Bc-I2kuy",
"fillStyle": "hachure",
"strokeWidth": 1,
"strokeStyle": "solid",
"roughness": 1,
"opacity": 100,
"angle": 0,
"x": 1816.0542373174442,
"y": 879.2421773412301,
"strokeColor": "#1e1e1e",
"backgroundColor": "#d0bfff",
"width": 115.39990234375,
"height": 225,
"seed": 1029764816,
"groupIds": [],
"frameId": null,
"roundness": null,
"boundElements": [],
"updated": 1723756979113,
"link": null,
"locked": false,
"fontSize": 20,
"fontFamily": 5,
"text": "Task runner\n\n\n\n\n\n\n\n",
"textAlign": "center",
"verticalAlign": "middle",
"containerId": "IQuQeqT5jHaGToLEJeScY",
"originalText": "Task runner\n\n\n\n\n\n\n\n",
"autoResize": true,
"lineHeight": 1.25
},
{
"type": "rectangle",
"version": 1207,
"versionNonce": 209652432,
"index": "b1XZ",
"isDeleted": false,
"id": "CqiCDYYzdeFbt7hIPh6Ej",
"fillStyle": "hachure",
"strokeWidth": 1,
"strokeStyle": "solid",
"roughness": 1,
"opacity": 100,
"angle": 0,
"x": 1705.7889969339021,
"y": 918.7187504179244,
"strokeColor": "#1e1e1e",
"backgroundColor": "#ffc9c9",
"width": 263.3592677540476,
"height": 184,
"seed": 1630399024,
"groupIds": [
"c6BWuvFGG24TyGmFHy9p8"
],
"frameId": null,
"roundness": {
"type": 3
},
"boundElements": [
{
"type": "text",
"id": "fDd6ctG9MLdC4BAe3UjnL"
},
{
"id": "fXI-ZUZHdOLNfBJ2k9IaU",
"type": "arrow"
}
],
"updated": 1723756982309,
"link": null,
"locked": false
},
{
"type": "text",
"version": 1228,
"versionNonce": 80235728,
"index": "b1Xd",
"isDeleted": false,
"id": "fDd6ctG9MLdC4BAe3UjnL",
"fillStyle": "hachure",
"strokeWidth": 1,
"strokeStyle": "solid",
"roughness": 1,
"opacity": 100,
"angle": 0,
"x": 1813.535235425184,
"y": 923.7484501700878,
"strokeColor": "#1e1e1e",
"backgroundColor": "#ffc9c9",
"width": 47.866790771484375,
"height": 173.940600495673,
"seed": 1530374864,
"groupIds": [
"c6BWuvFGG24TyGmFHy9p8"
],
"frameId": null,
"roundness": null,
"boundElements": [],
"updated": 1723756979114,
"link": null,
"locked": false,
"fontSize": 19.878925770934057,
"fontFamily": 5,
"text": "Task\n\n\n\n\n\n",
"textAlign": "center",
"verticalAlign": "middle",
"containerId": "CqiCDYYzdeFbt7hIPh6Ej",
"originalText": "Task\n\n\n\n\n\n",
"autoResize": true,
"lineHeight": 1.25
},
{
"type": "ellipse",
"version": 2108,
"versionNonce": 902181584,
"index": "b1Xh",
"isDeleted": false,
"id": "y2kyTs8L2fDyChRo01Siv",
"fillStyle": "hachure",
"strokeWidth": 1,
"strokeStyle": "solid",
"roughness": 1,
"opacity": 100,
"angle": 0.0790450627558883,
"x": 1791.3734901428243,
"y": 974.7988184591122,
"strokeColor": "#1e1e1e",
"backgroundColor": "#12b886",
"width": 161.5060937244159,
"height": 49,
"seed": 641814224,
"groupIds": [
"N_rYhw0CcYfJ8hWSOuPFa",
"c6BWuvFGG24TyGmFHy9p8"
],
"frameId": null,
"roundness": {
"type": 2
},
"boundElements": [
{
"type": "text",
"id": "7Q0nkPX6LxcrTKt8mS743"
}
],
"updated": 1723756979114,
"link": null,
"locked": false
},
{
"type": "text",
"version": 2137,
"versionNonce": 437737680,
"index": "b1Xl",
"isDeleted": false,
"id": "7Q0nkPX6LxcrTKt8mS743",
"fillStyle": "hachure",
"strokeWidth": 1,
"strokeStyle": "dashed",
"roughness": 1,
"opacity": 100,
"angle": 0.0790450627558883,
"x": 1836.7662050861936,
"y": 987.050373713208,
"strokeColor": "#1e1e1e",
"backgroundColor": "#12b886",
"width": 70.51860976219177,
"height": 24.848657213667572,
"seed": 80671952,
"groupIds": [
"N_rYhw0CcYfJ8hWSOuPFa",
"c6BWuvFGG24TyGmFHy9p8"
],
"frameId": null,
"roundness": null,
"boundElements": [],
"updated": 1723756979114,
"link": null,
"locked": false,
"fontSize": 19.878925770934057,
"fontFamily": 5,
"text": "Callable",
"textAlign": "center",
"verticalAlign": "middle",
"containerId": "y2kyTs8L2fDyChRo01Siv",
"originalText": "Callable",
"autoResize": true,
"lineHeight": 1.25
},
{
"type": "ellipse",
"version": 2022,
"versionNonce": 1699987152,
"index": "b1Xp",
"isDeleted": false,
"id": "XfSzjMLNwUPk9AVXRq5-c",
"fillStyle": "hachure",
"strokeWidth": 1,
"strokeStyle": "solid",
"roughness": 1,
"opacity": 100,
"angle": 0.039362398350647254,
"x": 1710.740412101144,
"y": 1026.350749168779,
"strokeColor": "#1e1e1e",
"backgroundColor": "#a5d8ff",
"width": 161.5060937244159,
"height": 49,
"seed": 79611952,
"groupIds": [
"6zcuGwXIAAuVEqpx03rbO",
"c6BWuvFGG24TyGmFHy9p8"
],
"frameId": null,
"roundness": {
"type": 2
},
"boundElements": [
{
"type": "text",
"id": "lt43g2NolrgpCin3XiLSR"
}
],
"updated": 1723756979114,
"link": null,
"locked": false
},
{
"type": "text",
"version": 2045,
"versionNonce": 403924176,
"index": "b1Xt",
"isDeleted": false,
"id": "lt43g2NolrgpCin3XiLSR",
"fillStyle": "hachure",
"strokeWidth": 1,
"strokeStyle": "dashed",
"roughness": 1,
"opacity": 100,
"angle": 0.039362398350647254,
"x": 1756.5007388103759,
"y": 1038.6023044228748,
"strokeColor": "#1e1e1e",
"backgroundColor": "#a5d8ff",
"width": 69.78338623046875,
"height": 24.848657213667572,
"seed": 471220784,
"groupIds": [
"6zcuGwXIAAuVEqpx03rbO",
"c6BWuvFGG24TyGmFHy9p8"
],
"frameId": null,
"roundness": null,
"boundElements": [],
"updated": 1723756979114,
"link": null,
"locked": false,
"fontSize": 19.878925770934057,
"fontFamily": 5,
"text": "Trigger",
"textAlign": "center",
"verticalAlign": "middle",
"containerId": "XfSzjMLNwUPk9AVXRq5-c",
"originalText": "Trigger",
"autoResize": true,
"lineHeight": 1.25
},
{
"type": "rectangle",
"version": 879,
"versionNonce": 1474880560,
"index": "b1i",
"isDeleted": false,
"id": "HyP7BZJVo6tJy8wPmNzbl",
"fillStyle": "hachure",
"strokeWidth": 1,
"strokeStyle": "solid",
"roughness": 1,
"opacity": 100,
"angle": 0,
"x": 1554.5497932146895,
"y": 1158.5511408762293,
"strokeColor": "#1e1e1e",
"backgroundColor": "#fff9db",
"width": 239.06313174761198,
"height": 126.8196202531644,
"seed": 1474292272,
"groupIds": [],
"frameId": null,
"roundness": {
"type": 3
},
"boundElements": [
{
"id": "LbyFqCoQTmvTdC5VlJe9P",
"type": "arrow"
},
{
"id": "iOCvXYA2L16R_vT2aAdFu",
"type": "text"
},
{
"id": "fXI-ZUZHdOLNfBJ2k9IaU",
"type": "arrow"
}
],
"updated": 1723757164556,
"link": null,
"locked": false
},
{
"type": "text",
"version": 993,
"versionNonce": 1000136240,
"index": "b1j",
"isDeleted": false,
"id": "iOCvXYA2L16R_vT2aAdFu",
"fillStyle": "hachure",
"strokeWidth": 1,
"strokeStyle": "solid",
"roughness": 1,
"opacity": 100,
"angle": 0,
"x": 1585.3614189029486,
"y": 1196.9609510028115,
"strokeColor": "#1e1e1e",
"backgroundColor": "#fff9db",
"width": 177.43988037109375,
"height": 50,
"seed": 1925391408,
"groupIds": [],
"frameId": null,
"roundness": null,
"boundElements": [],
"updated": 1723757164556,
"link": null,
"locked": false,
"fontSize": 20,
"fontFamily": 5,
"text": "Get a task as an \nargument",
"textAlign": "center",
"verticalAlign": "middle",
"containerId": "HyP7BZJVo6tJy8wPmNzbl",
"originalText": "Get a task as an argument",
"autoResize": true,
"lineHeight": 1.25
},
{
"type": "arrow",
"version": 849,
"versionNonce": 985765936,
"index": "b1m",
"isDeleted": false,
"id": "LbyFqCoQTmvTdC5VlJe9P",
"fillStyle": "hachure",
"strokeWidth": 1,
"strokeStyle": "solid",
"roughness": 1,
"opacity": 100,
"angle": 0,
"x": 1696.6044829064658,
"y": 1288.841905217944,
"strokeColor": "#1e1e1e",
"backgroundColor": "#d0bfff",
"width": 0.8193286928735688,
"height": 53.678002679886276,
"seed": 1633266224,
"groupIds": [],
"frameId": null,
"roundness": {
"type": 2
},
"boundElements": [],
"updated": 1723757164556,
"link": null,
"locked": false,
"startBinding": {
"elementId": "HyP7BZJVo6tJy8wPmNzbl",
"focus": -0.17844289194268018,
"gap": 3.471144088550318,
"fixedPoint": null
},
"endBinding": {
"elementId": "FCwcIAfeLbeJg6Zi8BVKM",
"focus": -0.009745033877159017,
"gap": 3.402997661688971,
"fixedPoint": null
},
"lastCommittedPoint": null,
"startArrowhead": null,
"endArrowhead": "arrow",
"points": [
[
0,
0
],
[
0.8193286928735688,
53.678002679886276
]
],
"elbowed": false
},
{
"type": "diamond",
"version": 701,
"versionNonce": 463820848,
"index": "b1s",
"isDeleted": false,
"id": "FCwcIAfeLbeJg6Zi8BVKM",
"fillStyle": "hachure",
"strokeWidth": 1,
"strokeStyle": "solid",
"roughness": 1,
"opacity": 100,
"angle": 0,
"x": 1536.3487797743226,
"y": 1345.0150380286043,
"strokeColor": "#1e1e1e",
"backgroundColor": "#fff9db",
"width": 327.0918259068212,
"height": 192.18989248354546,
"seed": 490909392,
"groupIds": [],
"frameId": null,
"roundness": {
"type": 2
},
"boundElements": [
{
"type": "text",
"id": "XIj9uOXTaxVQuFWHAh8t2"
},
{
"id": "STikmhs29bpStOM3nvBoD",
"type": "arrow"
},
{
"id": "3rHRwfJtVAuBBE1kyGmdt",
"type": "arrow"
},
{
"id": "2hRCzj2mZlHhWGt_WPdv1",
"type": "arrow"
},
{
"id": "LbyFqCoQTmvTdC5VlJe9P",
"type": "arrow"
}
],
"updated": 1723756891654,
"link": null,
"locked": false
},
{
"type": "text",
"version": 449,
"versionNonce": 308836560,
"index": "b1t",
"isDeleted": false,
"id": "XIj9uOXTaxVQuFWHAh8t2",
"fillStyle": "hachure",
"strokeWidth": 1,
"strokeStyle": "solid",
"roughness": 1,
"opacity": 100,
"angle": 0,
"x": 1630.9617936240747,
"y": 1403.5625111494905,
"strokeColor": "#1e1e1e",
"backgroundColor": "#fff9db",
"width": 138.31988525390625,
"height": 75,
"seed": 1420055760,
"groupIds": [],
"frameId": null,
"roundness": null,
"boundElements": [],
"updated": 1723756890860,
"link": null,
"locked": false,
"fontSize": 20,
"fontFamily": 5,
"text": "Check if task \nstill can be \ntriggered",
"textAlign": "center",
"verticalAlign": "middle",
"containerId": "FCwcIAfeLbeJg6Zi8BVKM",
"originalText": "Check if task still can be triggered",
"autoResize": true,
"lineHeight": 1.25
},
{
"type": "arrow",
"version": 1103,
"versionNonce": 1760627760,
"index": "b1u",
"isDeleted": false,
"id": "3rHRwfJtVAuBBE1kyGmdt",
"fillStyle": "hachure",
"strokeWidth": 1,
"strokeStyle": "solid",
"roughness": 1,
"opacity": 100,
"angle": 0,
"x": 1850.808229807619,
"y": 1461.3902677755318,
"strokeColor": "#1e1e1e",
"backgroundColor": "#d0bfff",
"width": 282.17700712382043,
"height": 305.359935601738,
"seed": 1682829008,
"groupIds": [],
"frameId": null,
"roundness": {
"type": 2
},
"boundElements": [],
"updated": 1723756891655,
"link": null,
"locked": false,
"startBinding": {
"elementId": "FCwcIAfeLbeJg6Zi8BVKM",
"focus": -0.12884470113660396,
"gap": 11.085840774530027,
"fixedPoint": null
},
"endBinding": {
"elementId": "w7B3jgdlWOsnxlvPngEQD",
"focus": 0.25339453076017165,
"gap": 9.103550868112734,
"fixedPoint": null
},
"lastCommittedPoint": null,
"startArrowhead": null,
"endArrowhead": "arrow",
"points": [
[
0,
0
],
[
282.17700712382043,
61.07043151696416
],
[
273.33977612188505,
305.359935601738
]
],
"elbowed": false
},
{
"type": "text",
"version": 517,
"versionNonce": 263527120,
"index": "b1y",
"isDeleted": false,
"id": "jkUc63EEwi0tX_KG4pnmP",
"fillStyle": "hachure",
"strokeWidth": 1,
"strokeStyle": "solid",
"roughness": 1,
"opacity": 100,
"angle": 0.06053892542880668,
"x": 1987.9618247165897,
"y": 1449.2329700567975,
"strokeColor": "#1e1e1e",
"backgroundColor": "#d0bfff",
"width": 24.720000326633453,
"height": 25,
"seed": 1910569520,
"groupIds": [],
"frameId": null,
"roundness": null,
"boundElements": [],
"updated": 1723756836518,
"link": null,
"locked": false,
"fontSize": 20,
"fontFamily": 5,
"text": "No",
"textAlign": "left",
"verticalAlign": "top",
"containerId": null,
"originalText": "No",
"autoResize": true,
"lineHeight": 1.25
},
{
"type": "text",
"version": 811,
"versionNonce": 1171962416,
"index": "b1z",
"isDeleted": false,
"id": "FFibWCRbM0ac15VquIccI",
"fillStyle": "hachure",
"strokeWidth": 1,
"strokeStyle": "solid",
"roughness": 1,
"opacity": 100,
"angle": 0.13743315829613412,
"x": 1856.1019957293356,
"y": 1506.6127252537526,
"strokeColor": "#1e1e1e",
"backgroundColor": "#d0bfff",
"width": 294.84916554571714,
"height": 66.19013114735047,
"seed": 77767216,
"groupIds": [],
"frameId": null,
"roundness": null,
"boundElements": [
{
"id": "3rHRwfJtVAuBBE1kyGmdt",
"type": "arrow"
}
],
"updated": 1723756836518,
"link": null,
"locked": false,
"fontSize": 17.650701639293455,
"fontFamily": 5,
"text": "1. graceful shutdown\n2. Sometimes task are n time run \nonly (like startup task run once)",
"textAlign": "left",
"verticalAlign": "top",
"containerId": null,
"originalText": "1. graceful shutdown\n2. Sometimes task are n time run only (like startup task run once)",
"autoResize": false,
"lineHeight": 1.25
},
{
"type": "rectangle",
"version": 913,
"versionNonce": 1087998512,
"index": "b21",
"isDeleted": false,
"id": "w7B3jgdlWOsnxlvPngEQD",
"fillStyle": "hachure",
"strokeWidth": 1,
"strokeStyle": "solid",
"roughness": 1,
"opacity": 100,
"angle": 0,
"x": 1996.7259811735935,
"y": 1775.8537542453826,
"strokeColor": "#1e1e1e",
"backgroundColor": "#ffc9c9",
"width": 198.5683942456583,
"height": 116.9088698549408,
"seed": 438287920,
"groupIds": [],
"frameId": null,
"roundness": {
"type": 3
},
"boundElements": [
{
"type": "text",
"id": "7XMxNA8RXkBPcSKKEXWIO"
},
{
"id": "3rHRwfJtVAuBBE1kyGmdt",
"type": "arrow"
}
],
"updated": 1723756836518,
"link": null,
"locked": false
},
{
"type": "text",
"version": 1045,
"versionNonce": 1529802448,
"index": "b22",
"isDeleted": false,
"id": "7XMxNA8RXkBPcSKKEXWIO",
"fillStyle": "hachure",
"strokeWidth": 1,
"strokeStyle": "solid",
"roughness": 1,
"opacity": 100,
"angle": 0,
"x": 2004.1902527497766,
"y": 1809.308189172853,
"strokeColor": "#1e1e1e",
"backgroundColor": "#fff9db",
"width": 183.63985109329224,
"height": 50,
"seed": 99121200,
"groupIds": [],
"frameId": null,
"roundness": null,
"boundElements": [],
"updated": 1723756836519,
"link": null,
"locked": false,
"fontSize": 20,
"fontFamily": 5,
"text": "Return in function.\nTask is finished",
"textAlign": "center",
"verticalAlign": "middle",
"containerId": "w7B3jgdlWOsnxlvPngEQD",
"originalText": "Return in function.\nTask is finished",
"autoResize": true,
"lineHeight": 1.25
},
{
"type": "text",
"version": 136,
"versionNonce": 1856546512,
"index": "b23",
"isDeleted": false,
"id": "Uq1j2EwKkxp5mcs2g9yQf",
"fillStyle": "hachure",
"strokeWidth": 1,
"strokeStyle": "solid",
"roughness": 1,
"opacity": 100,
"angle": 0,
"x": 1629.8841019714973,
"y": 1563.2230944946575,
"strokeColor": "#1e1e1e",
"backgroundColor": "#d0bfff",
"width": 49.21613603094943,
"height": 25,
"seed": 1579427536,
"groupIds": [],
"frameId": null,
"roundness": null,
"boundElements": [],
"updated": 1723756836518,
"link": null,
"locked": false,
"fontSize": 20,
"fontFamily": 5,
"text": "Yes",
"textAlign": "left",
"verticalAlign": "top",
"containerId": null,
"originalText": "Yes",
"autoResize": false,
"lineHeight": 1.25
},
{
"type": "rectangle",
"version": 804,
"versionNonce": 2007851728,
"index": "b26",
"isDeleted": false,
"id": "eYJWFucNr-z4ZnArvQ-Zr",
"fillStyle": "hachure",
"strokeWidth": 1,
"strokeStyle": "solid",
"roughness": 1,
"opacity": 100,
"angle": 0,
"x": 1571.2363939435845,
"y": 1626.8059576040923,
"strokeColor": "#1e1e1e",
"backgroundColor": "#fff9db",
"width": 273.58068072975556,
"height": 160,
"seed": 886513712,
"groupIds": [],
"frameId": null,
"roundness": {
"type": 3
},
"boundElements": [
{
"type": "text",
"id": "irv12xCSy44n3nsd-C1hX"
},
{
"id": "2hRCzj2mZlHhWGt_WPdv1",
"type": "arrow"
},
{
"id": "yd_IMWqbdG2nufqbzBVma",
"type": "arrow"
}
],
"updated": 1723756836518,
"link": null,
"locked": false
},
{
"type": "text",
"version": 1028,
"versionNonce": 1988840656,
"index": "b27",
"isDeleted": false,
"id": "irv12xCSy44n3nsd-C1hX",
"fillStyle": "hachure",
"strokeWidth": 1,
"strokeStyle": "solid",
"roughness": 1,
"opacity": 100,
"angle": 0,
"x": 1579.7768258611966,
"y": 1644.3059576040923,
"strokeColor": "#1e1e1e",
"backgroundColor": "#fff9db",
"width": 256.49981689453125,
"height": 125,
"seed": 1466145328,
"groupIds": [],
"frameId": null,
"roundness": null,
"boundElements": [],
"updated": 1723756836518,
"link": null,
"locked": false,
"fontSize": 20,
"fontFamily": 5,
"text": "Wait until the task get \ntriggered \n(e.x if cron job, then \nshould sleep for next cron\ninterval)",
"textAlign": "center",
"verticalAlign": "middle",
"containerId": "eYJWFucNr-z4ZnArvQ-Zr",
"originalText": "Wait until the task get triggered \n(e.x if cron job, then should sleep for next cron interval)",
"autoResize": true,
"lineHeight": 1.25
},
{
"type": "rectangle",
"version": 897,
"versionNonce": 1387080752,
"index": "b2A",
"isDeleted": false,
"id": "JxDqrhu671kLDTdcOGeyZ",
"fillStyle": "hachure",
"strokeWidth": 1,
"strokeStyle": "solid",
"roughness": 1,
"opacity": 100,
"angle": 0,
"x": 1598.090109389605,
"y": 1892.713229888806,
"strokeColor": "#1e1e1e",
"backgroundColor": "#fff9db",
"width": 222.06468710355526,
"height": 101.80408496883595,
"seed": 2117406928,
"groupIds": [],
"frameId": null,
"roundness": {
"type": 3
},
"boundElements": [
{
"type": "text",
"id": "fGfM8hH-3f8NYSSbkKzuB"
},
{
"id": "yd_IMWqbdG2nufqbzBVma",
"type": "arrow"
},
{
"id": "STikmhs29bpStOM3nvBoD",
"type": "arrow"
}
],
"updated": 1723756836518,
"link": null,
"locked": false
},
{
"type": "text",
"version": 1171,
"versionNonce": 1999188688,
"index": "b2B",
"isDeleted": false,
"id": "fGfM8hH-3f8NYSSbkKzuB",
"fillStyle": "hachure",
"strokeWidth": 1,
"strokeStyle": "solid",
"roughness": 1,
"opacity": 100,
"angle": 0,
"x": 1643.99249168917,
"y": 1931.115272373224,
"strokeColor": "#1e1e1e",
"backgroundColor": "#fff9db",
"width": 130.25992250442505,
"height": 25,
"seed": 1828353744,
"groupIds": [],
"frameId": null,
"roundness": null,
"boundElements": [],
"updated": 1723756836518,
"link": null,
"locked": false,
"fontSize": 20,
"fontFamily": 5,
"text": "Run the task",
"textAlign": "center",
"verticalAlign": "middle",
"containerId": "JxDqrhu671kLDTdcOGeyZ",
"originalText": "Run the task",
"autoResize": true,
"lineHeight": 1.25
},
{
"type": "arrow",
"version": 210,
"versionNonce": 1231029808,
"index": "b2C",
"isDeleted": false,
"id": "2hRCzj2mZlHhWGt_WPdv1",
"fillStyle": "hachure",
"strokeWidth": 1,
"strokeStyle": "solid",
"roughness": 1,
"opacity": 100,
"angle": 0,
"x": 1695.1683092267951,
"y": 1535.5876864571458,
"strokeColor": "#1e1e1e",
"backgroundColor": "#d0bfff",
"width": 2.505653222510091,
"height": 86.9826664940208,
"seed": 1489365040,
"groupIds": [],
"frameId": null,
"roundness": {
"type": 2
},
"boundElements": [],
"updated": 1723756891655,
"link": null,
"locked": false,
"startBinding": {
"elementId": "FCwcIAfeLbeJg6Zi8BVKM",
"focus": 0.04554036978314176,
"gap": 1.0000000000001137,
"fixedPoint": null
},
"endBinding": {
"elementId": "eYJWFucNr-z4ZnArvQ-Zr",
"focus": -0.05698450771445858,
"gap": 4.235604652925758,
"fixedPoint": null
},
"lastCommittedPoint": null,
"startArrowhead": null,
"endArrowhead": "arrow",
"points": [
[
0,
0
],
[
2.505653222510091,
86.9826664940208
]
],
"elbowed": false
},
{
"type": "arrow",
"version": 309,
"versionNonce": 274568240,
"index": "b2D",
"isDeleted": false,
"id": "yd_IMWqbdG2nufqbzBVma",
"fillStyle": "hachure",
"strokeWidth": 1,
"strokeStyle": "solid",
"roughness": 1,
"opacity": 100,
"angle": 0,
"x": 1693.8171318957795,
"y": 1797.3645905520277,
"strokeColor": "#1e1e1e",
"backgroundColor": "#d0bfff",
"width": 3.90076845842691,
"height": 84.28241401267951,
"seed": 1069469744,
"groupIds": [],
"frameId": null,
"roundness": {
"type": 2
},
"boundElements": [],
"updated": 1723756836520,
"link": null,
"locked": false,
"startBinding": {
"elementId": "eYJWFucNr-z4ZnArvQ-Zr",
"focus": 0.1380173553300253,
"gap": 10.558632947935394,
"fixedPoint": null
},
"endBinding": {
"elementId": "JxDqrhu671kLDTdcOGeyZ",
"focus": -0.07528587805114698,
"gap": 11.066225324098781,
"fixedPoint": null
},
"lastCommittedPoint": null,
"startArrowhead": null,
"endArrowhead": "arrow",
"points": [
[
0,
0
],
[
3.90076845842691,
84.28241401267951
]
],
"elbowed": false
},
{
"type": "arrow",
"version": 403,
"versionNonce": 933941808,
"index": "b2E",
"isDeleted": false,
"id": "STikmhs29bpStOM3nvBoD",
"fillStyle": "hachure",
"strokeWidth": 1,
"strokeStyle": "solid",
"roughness": 1,
"opacity": 100,
"angle": 0,
"x": 1587.0238840655074,
"y": 1944.041078311416,
"strokeColor": "#1e1e1e",
"backgroundColor": "#d0bfff",
"width": 134.6236982149312,
"height": 499.71671706185407,
"seed": 1382304976,
"groupIds": [],
"frameId": null,
"roundness": {
"type": 2
},
"boundElements": [],
"updated": 1723756891655,
"link": null,
"locked": false,
"startBinding": {
"elementId": "JxDqrhu671kLDTdcOGeyZ",
"focus": -0.8775469865039849,
"gap": 11.06622532409752,
"fixedPoint": null
},
"endBinding": {
"elementId": "FCwcIAfeLbeJg6Zi8BVKM",
"focus": 0.989821931698346,
"gap": 2.461394250283675,
"fixedPoint": null
},
"lastCommittedPoint": null,
"startArrowhead": null,
"endArrowhead": "arrow",
"points": [
[
0,
0
],
[
-134.6236982149312,
-241.50757913609573
],
[
-50.063194075934234,
-499.71671706185407
]
],
"elbowed": false
},
{
"type": "text",
"version": 532,
"versionNonce": 1101911760,
"index": "b2G",
"isDeleted": false,
"id": "Fo90EBjtmjCdnFISTCbzQ",
"fillStyle": "hachure",
"strokeWidth": 1,
"strokeStyle": "solid",
"roughness": 1,
"opacity": 100,
"angle": 6.278177951354555,
"x": 1258.6235230693567,
"y": 1663.7011971998286,
"strokeColor": "#1e1e1e",
"backgroundColor": "#d0bfff",
"width": 212.59902644016609,
"height": 28.165738862651683,
"seed": 886943792,
"groupIds": [],
"frameId": null,
"roundness": null,
"boundElements": [],
"updated": 1723755863197,
"link": null,
"locked": false,
"fontSize": 22.532591090121358,
"fontFamily": 5,
"text": "In a while loop....",
"textAlign": "left",
"verticalAlign": "top",
"containerId": null,
"originalText": "In a while loop....",
"autoResize": false,
"lineHeight": 1.25
},
{
"type": "arrow",
"version": 72,
"versionNonce": 691999280,
"index": "b2I",
"isDeleted": false,
"id": "fXI-ZUZHdOLNfBJ2k9IaU",
"fillStyle": "hachure",
"strokeWidth": 1,
"strokeStyle": "solid",
"roughness": 1,
"opacity": 100,
"angle": 0,
"x": 1765.7332540847406,
"y": 1117.859883036091,
"strokeColor": "#1e1e1e",
"backgroundColor": "#d0bfff",
"width": 8.997444931522296,
"height": 38.654428294499894,
"seed": 2004644048,
"groupIds": [],
"frameId": null,
"roundness": {
"type": 2
},
"boundElements": [],
"updated": 1723757164556,
"link": null,
"locked": false,
"startBinding": {
"elementId": "CqiCDYYzdeFbt7hIPh6Ej",
"focus": 0.42645184962372484,
"gap": 15.141132618166694,
"fixedPoint": null
},
"endBinding": {
"elementId": "HyP7BZJVo6tJy8wPmNzbl",
"focus": 0.5020483085399844,
"gap": 2.036829545638284,
"fixedPoint": null
},
"lastCommittedPoint": null,
"startArrowhead": null,
"endArrowhead": "arrow",
"points": [
[
0,
0
],
[
-8.997444931522296,
38.654428294499894
]
],
"elbowed": false
},
{
"type": "rectangle",
"version": 96,
"versionNonce": 995565616,
"index": "b2IG",
"isDeleted": false,
"id": "URbMlcmauXmJNE4154eWu",
"fillStyle": "hachure",
"strokeWidth": 1,
"strokeStyle": "solid",
"roughness": 1,
"opacity": 100,
"angle": 0,
"x": 251.14182229334358,
"y": -74.41583597634695,
"strokeColor": "#1e1e1e",
"backgroundColor": "#ffc9c9",
"width": 370.7116656519728,
"height": 263.40256087233087,
"seed": 1862224944,
"groupIds": [
"5K2ah22q0bV8H2rPle8Rd"
],
"frameId": null,
"roundness": {
"type": 3
},
"boundElements": [
{
"type": "text",
"id": "Zv7ejmqs54GHJIXOGOmWE"
},
{
"id": "umIUdT2Pg_dV6Yj3OxRkn",
"type": "arrow"
}
],
"updated": 1723757378149,
"link": null,
"locked": false
},
{
"type": "text",
"version": 125,
"versionNonce": 1580595760,
"index": "b2IV",
"isDeleted": false,
"id": "Zv7ejmqs54GHJIXOGOmWE",
"fillStyle": "hachure",
"strokeWidth": 1,
"strokeStyle": "solid",
"roughness": 1,
"opacity": 100,
"angle": 0,
"x": 341.4177295822206,
"y": -30.21455554018152,
"strokeColor": "#1e1e1e",
"backgroundColor": "#ffc9c9",
"width": 190.15985107421875,
"height": 175,
"seed": 1425153232,
"groupIds": [
"5K2ah22q0bV8H2rPle8Rd"
],
"frameId": null,
"roundness": null,
"boundElements": [],
"updated": 1723757375833,
"link": null,
"locked": false,
"fontSize": 20,
"fontFamily": 5,
"text": "Aioclock Application\n\n\n\n\n\n",
"textAlign": "center",
"verticalAlign": "middle",
"containerId": "URbMlcmauXmJNE4154eWu",
"originalText": "Aioclock Application\n\n\n\n\n\n",
"autoResize": true,
"lineHeight": 1.25
},
{
"type": "ellipse",
"version": 1064,
"versionNonce": 1690098896,
"index": "b2J",
"isDeleted": false,
"id": "r8Q1v_XO3gRI7vmMfsEXw",
"fillStyle": "hachure",
"strokeWidth": 1,
"strokeStyle": "solid",
"roughness": 1,
"opacity": 100,
"angle": 0,
"x": 325.2229077704774,
"y": 50.56508754088625,
"strokeColor": "#1e1e1e",
"backgroundColor": "#3bc9db",
"width": 213.32236457653954,
"height": 120,
"seed": 1818493488,
"groupIds": [
"5K2ah22q0bV8H2rPle8Rd"
],
"frameId": null,
"roundness": {
"type": 2
},
"boundElements": [
{
"id": "N6Qx_9If8fvD9nNkz3hcc",
"type": "text"
},
{
"id": "HRt4IaHAZ1RGroBskO7Y4",
"type": "arrow"
}
],
"updated": 1723757384370,
"link": null,
"locked": false
},
{
"type": "text",
"version": 954,
"versionNonce": 1047887920,
"index": "b2K",
"isDeleted": false,
"id": "N6Qx_9If8fvD9nNkz3hcc",
"fillStyle": "hachure",
"strokeWidth": 1,
"strokeStyle": "solid",
"roughness": 1,
"opacity": 100,
"angle": 0,
"x": 375.60329101223635,
"y": 73.1386806696934,
"strokeColor": "#1e1e1e",
"backgroundColor": "#3bc9db",
"width": 112.71990752220154,
"height": 75,
"seed": 2131910352,
"groupIds": [
"5K2ah22q0bV8H2rPle8Rd"
],
"frameId": null,
"roundness": null,
"boundElements": [],
"updated": 1723757375832,
"link": null,
"locked": false,
"fontSize": 20,
"fontFamily": 5,
"text": "Dependency\nInject\nSystem",
"textAlign": "center",
"verticalAlign": "middle",
"containerId": "r8Q1v_XO3gRI7vmMfsEXw",
"originalText": "Dependency\nInject\nSystem",
"autoResize": true,
"lineHeight": 1.25
},
{
"type": "text",
"version": 1687,
"versionNonce": 473918000,
"index": "b2L",
"isDeleted": false,
"id": "HC4jNhimd9IUoFr2xipV5",
"fillStyle": "hachure",
"strokeWidth": 1,
"strokeStyle": "solid",
"roughness": 1,
"opacity": 100,
"angle": 6.240271949811893,
"x": 441.847757819976,
"y": 380.6251438274418,
"strokeColor": "#1e1e1e",
"backgroundColor": "#3bc9db",
"width": 182.67791703977647,
"height": 22.515338081998202,
"seed": 2112564944,
"groupIds": [
"t7l2d8FW9kPWixWqQCSlC"
],
"frameId": null,
"roundness": null,
"boundElements": [],
"updated": 1723757435723,
"link": null,
"locked": false,
"fontSize": 18.01227046559856,
"fontFamily": 5,
"text": "@aioclock.task",
"textAlign": "left",
"verticalAlign": "top",
"containerId": null,
"originalText": "@aioclock.task",
"autoResize": false,
"lineHeight": 1.25
},
{
"type": "arrow",
"version": 2177,
"versionNonce": 812487728,
"index": "b2M",
"isDeleted": false,
"id": "HRt4IaHAZ1RGroBskO7Y4",
"fillStyle": "hachure",
"strokeWidth": 1,
"strokeStyle": "dashed",
"roughness": 1,
"opacity": 100,
"angle": 0,
"x": 414.225741641744,
"y": 196.96627523669537,
"strokeColor": "#1e1e1e",
"backgroundColor": "#3bc9db",
"width": 233.81396450213708,
"height": 212.0236995742859,
"seed": 1735212080,
"groupIds": [],
"frameId": null,
"roundness": {
"type": 2
},
"boundElements": [],
"updated": 1723757428340,
"link": null,
"locked": false,
"startBinding": {
"elementId": "r8Q1v_XO3gRI7vmMfsEXw",
"focus": 0.23301788409604482,
"gap": 27.124023203196707,
"fixedPoint": null
},
"endBinding": {
"elementId": "mp5cf4rOe7-TBZhYPXY93",
"focus": -0.31217152478063914,
"gap": 7.871960812977093,
"fixedPoint": null
},
"lastCommittedPoint": null,
"startArrowhead": null,
"endArrowhead": "arrow",
"points": [
[
0,
0
],
[
17.144056910559357,
205.07407135371665
],
[
233.81396450213708,
212.0236995742859
]
],
"elbowed": false
}
],
"appState": {
"gridSize": 20,
"gridStep": 5,
"gridModeEnabled": false,
"viewBackgroundColor": "#ffffff"
},
"files": {}
}
================================================
FILE: docs/examples/brokers.md
================================================
You can basically run any tasks on aioclock, it could be your redis broker or other kind of brokers listening to a queue. The benefit of doing so, is that you don't need to worry about dependency injection, shutdown or startup event.
AioClock offer you a unique easy way to spin up new services, without any overhead or perfomance issue!
```python
from collections.abc import AsyncGenerator
from contextlib import asynccontextmanager
from aioclock import AioClock, Forever, Depends
from functools import lru_cache
from typing import NewType
BrokerType = NewType("BrokerType", ...) # your broker type ...
# your singleton redis instance
@lru_cache
def get_redis():
...
@asynccontextmanager
async def lifespan(aio_clock: AioClock, redis: BrokerType = Depends(get_redis)) -> AsyncGenerator[AioClock]:
yield aio_clock
await redis.disconnect()
app = AioClock(lifespan=lifespan)
@app.task(trigger=Forever())
async def read_message_queue(redis: BrokerType = Depends(get_redis)):
async for message in redis.listen("..."):
...
```
One other way to do this, is to implement a trigger that automatically execute the function.
But to do so, I basically need to wrap redis in my own library, and that's not good for some reasons:
1. Complexity of framework increases.
2. Is not realy flexible, because native library and client are always way more flexible. I end up writing something like `Celery`.
3. The architecture I choose to handle interactions with broker may not satisfy your requirement.
[This repository is an example how you can write a message queue in aioclock.](https://github.com/ManiMozaffar/typed-redis)
================================================
FILE: docs/examples/fastapi.md
================================================
To run AioClock with FastAPI, you can run it in the background with FastAPI lifespan, next to your asgi.
```python
from aioclock import AioClock
from fastapi import FastAPI
import asyncio
from contextlib import asynccontextmanager
clock_app = AioClock()
@asynccontextmanager
async def lifespan(app: FastAPI):
task = asyncio.create_task(clock_app.serve())
yield
try:
task.cancel()
await task
except asyncio.CancelledError:
...
app = FastAPI(lifespan=lifespan)
# now serve this with uvicorn or anything else
```
!!! danger "This setup is not recommended at all"
Running AioClock with FastAPI is not a good practice in General, because:
FastAPI is a framework to write stateless API, but aioclock is still stateful component in your architecture.
In simpler terms, it means if you have 5 instances of aioclock running, they produce 5x tasks than you intended.
So you cannot easily scale up horizontally by adding more aioclock power!
Even in this case, if you serve FastAPI with multiple processes, you end up having one aioclock per process!
What I suggest doing is to spin one new service, that is responsible for processing the periodic tasks.
Try to avoid periodic tasks in general, but sometimes it's not easy to do so.
================================================
FILE: docs/extra/tweaks.css
================================================
/* Revert hue value to that of pre mkdocs-material v9.4.0 */
[data-md-color-scheme='slate'] {
--md-hue: 230;
--md-default-bg-color: hsla(230, 15%, 21%, 1);
}
================================================
FILE: docs/images/README.md
================================================
Please check th diagrams folder if you're wondering where diagrams come from.
You can load them in [excalidraw](https://excalidraw.com/) website.
================================================
FILE: docs/index.md
================================================
# AioClock
## The Principle
Scheduling is annoying, stateful and hard to scale. But not anymore! AioClock is here as an asyncio-based scheduling framework designed for execution of periodic task with integrated support for dependency injection, enabling efficient and flexiable task management.
Aioclock offers:
- Async: 100% Async, very light, fast and resource friendly
- Scheduling: Keep scheduling tasks for you
- Group: Group your task, for better code maintainability
- Trigger: Already defined and easily extendable triggers, to trigger your scheduler to be started
- Easy syntax: Library's syntax is very easy and enjoyable, no confusing hierarchy
- Pydantic v2 validation: Validate all your trigger on startup using pydantic 2. Fastest to fail possible!
- **Soon**: Running the task dispatcher (scheduler) on different process by default, so CPU intensive stuff on task won't delay the scheduling
- **Soon**: Backend support, to allow horizontal scalling, by synchronizing, maybe using Redis
## Getting started
To Install aioclock, simply do
```
pip install aioclock
```
AioClock is very user friendly and easy to use, it's type stated library to use easily.
AioClock always have a trigger, that trigger the events.
```python
from collections.abc import AsyncGenerator
from contextlib import asynccontextmanager
import asyncio
from aioclock import AioClock, At, Depends, Every, Forever, Once
from aioclock.group import Group
# groups.py
group = Group()
def more_useless_than_me():
return "I'm a dependency. I'm more useless than a screen door on a submarine."
@group.task(trigger=Every(seconds=10))
async def every():
print("Every 10 seconds, I make a quantum leap. Where will I land next?")
@group.task(trigger=Every(seconds=5))
def even_sync_works():
print("I'm a synchronous task. I work even in async world.")
@group.task(trigger=At(tz="UTC", hour=0, minute=0, second=0))
async def at():
print(
"When the clock strikes midnight... I turn into a pumpkin. Just kidding, I run this task!"
)
@group.task(trigger=Forever())
async def forever(val: str = Depends(more_useless_than_me)):
await asyncio.sleep(2)
print("Heartbeat detected. Still not a zombie. Will check again in a bit.")
assert val == "I'm a dependency. I'm more useless than a screen door on a submarine."
@group.task(trigger=Once())
async def once():
print("Just once, I get to say something. Here it goes... I love lamp.")
@asynccontextmanager
async def lifespan(aio_clock: AioClock) -> AsyncGenerator[AioClock]:
print(
"Welcome to the Async Chronicles! Did you know a group of unicorns is called a blessing? Well, now you do!"
)
yield aio_clock
print("Going offline. Remember, if your code is running, you better go catch it!")
app = AioClock(lifespan=lifespan)
app.include_group(group)
# main.py
if __name__ == "__main__":
asyncio.run(app.serve())
```
================================================
FILE: docs/overview.md
================================================
# AioClock: Overview
## Introduction
AioClock is a lightweight, asynchronous task scheduling framework designed for Python applications. It provides a structured approach to managing, scheduling, and executing tasks with integrated dependency injection, making it highly modular and testable.
## Key Components
### 1. AioClock Application
The central orchestrator for all tasks. It initializes, manages, and executes tasks based on their defined triggers.
### 2. Dependency Injection System
A built-in system for injecting dependencies into tasks, enabling loose coupling and improving testability. It uses [FastDepends](https://lancetnik.github.io/FastDepends/) internally, which is very fimiliar with [FastAPI Dependency Injection System](https://fastapi.tiangolo.com/tutorial/dependencies/)
### 3. Task
A unit of work that can be scheduled and executed asynchronously. Tasks are defined using specific triggers that determine their execution conditions.
### 4. Trigger
Conditions or events that initiate task execution. It includes
- **Every**: Repeats a task at regular intervals.
- **At**: Executes a task at a specified time.
- **Once**: Runs a task a single time.
- **OnStartUp**: Runs when the application starts. (DEPRECATED in favor of lifespan)
- **OnShutDown**: Executes during application shutdown. (DEPRECATED in favor of lifespan)
- **Forever**: Continuously runs a task in an infinite loop.
- **Cron**: Uses cron syntax for scheduling.
- **OrTrigger**: Initiate with a list of triggers, and executes when at least one of the included triggers activate.
### 5. Group
A logical collection of tasks. Groups allow related tasks to be bundled and managed together, simplifying task organization and execution. Group allow you to code with aioclock in modular way.
### 6. Task Runner
The engine that monitors and executes tasks according to their triggers, ensuring tasks run in the correct order and at the right time.
### 7. Serve
The entry point that starts the AioClock application. It initiates the task runner, which monitors and executes tasks based on their triggers.
### 8. Lifespan
A context manager that will be used to handle the startup and shutdown of the application. It is used inside AioClock application.
### 9. Callable
A function or method associated with a task, executed when the task's trigger condition is met.
## Diagrams
### Ownership

In AioClock, tasks are managed through clear ownership within groups, using dependency injection. Groups encapsulate related tasks, each with specific triggers and callables. The include_group() function integrates these groups into the AioClock application, while standalone tasks are managed with decorators like @aioclock.task. This structure ensures that tasks are organized, maintainable, and easy to scale, with each component having a defined responsibility within the application.
The dependency injection system in AioClock allows you to override a callable with another through the application interface, facilitating testing. For example, instead of returning a session from a PostgreSQL database with get_session, you can override it to use get_sqlite_session, which provides a SQLite session instead. This flexibility makes it easier to swap out components for testing or other purposes without changing the core logic.
### Aioclock LifeCycle

This diagram shows the lifecycle of an AioClock application. It starts with the app.serve() call, which gathers all tasks and groups. The application then checks for startup tasks, runs them, and proceeds to other tasks. If a shutdown task is detected, it's executed before the application gracefully exits. If no shutdown tasks are present, the application simply exits. The diagram ensures a clear, step-by-step process for task management within the AioClock framework, ensuring tasks are executed in the proper order.
P.S: Since startup and shutdown task are deprcated, lifespan has same side effect as them, with extra benefit of having them with a shared memory state. Please reffer to lifespan API Documentation to understand it better with examples.
### Task Runner Execution Flow

This diagram breaks down how a task is handled in the AioClock system. The task runner first receives the task, checks if it’s still valid to run, and then either waits for the appropriate trigger (like a scheduled time) or finishes if it’s no longer needed. If valid, the task is executed; otherwise, the system gracefully exits. This loop continues until all tasks are either completed or stopped, ensuring everything runs smoothly and on time.
================================================
FILE: docs/plugins.py
================================================
import os
import re
from typing import Match
from mkdocs.config import Config
from mkdocs.structure.files import Files
from mkdocs.structure.pages import Page
try:
import pytest
except ImportError:
pytest = None
def on_pre_build(config: Config):
pass
def on_files(files: Files, config: Config) -> Files:
return remove_files(files)
def remove_files(files: Files) -> Files:
to_remove = []
for file in files:
if file.src_path in {"plugins.py"}:
to_remove.append(file)
elif file.src_path.startswith("__pycache__/"):
to_remove.append(file)
for f in to_remove:
files.remove(f)
return files
def on_page_markdown(markdown: str, page: Page, config: Config, files: Files) -> str:
markdown = remove_code_fence_attributes(markdown)
return add_version(markdown, page)
def add_version(markdown: str, page: Page) -> str:
if page.file.src_uri == "index.md":
version_ref = os.getenv("GITHUB_REF")
if version_ref and version_ref.startswith("refs/tags/"):
version = re.sub("^refs/tags/", "", version_ref.lower())
url = f"https://ManiMozaffar.github.io/aioclock/releases/tag/{version}"
version_str = f"Documentation for version: [{version}]({url})"
elif sha := os.getenv("GITHUB_SHA"):
sha = sha[:7]
url = f"https://ManiMozaffar.github.io/aioclock/commit/{sha}"
version_str = f"Documentation for development version: [{sha}]({url})"
else:
version_str = "Documentation for development version"
markdown = re.sub(r"{{ *version *}}", version_str, markdown)
return markdown
def remove_code_fence_attributes(markdown: str) -> str:
"""
There's no way to add attributes to code fences that works with both pycharm and mkdocs, hence we use
`py key="value"` to provide attributes to pytest-examples, then remove those attributes here.
https://youtrack.jetbrains.com/issue/IDEA-297873 & https://python-markdown.github.io/extensions/fenced_code_blocks/
"""
def remove_attrs(match: Match[str]) -> str:
suffix = re.sub(
r' (?:test|lint|upgrade|group|requires|output|rewrite_assert)=".+?"',
"",
match.group(2),
flags=re.M,
)
return f"{match.group(1)}{suffix}"
return re.sub(r"^( *``` *py)(.*)", remove_attrs, markdown, flags=re.M)
================================================
FILE: examples/app.py
================================================
import asyncio
from collections.abc import AsyncGenerator
from contextlib import asynccontextmanager
import threading
from time import sleep
from typing import Annotated
from aioclock import AioClock, Depends, Every, Group
# service1.py
group = Group()
def dependency():
return "Hello from thread: "
@group.task(trigger=Every(seconds=2))
def sync_task_1(val: str = Depends(dependency)):
print(f"{val} `sync_task_1` {threading.current_thread().ident}")
sleep(1) # some blocking operation
@group.task(trigger=Every(seconds=2.01))
def sync_task_2(val: Annotated[str, Depends(dependency)]):
print(f"{val} `sync_task_2` {threading.current_thread().ident}")
sleep(1) # some blocking operation
return "3"
print(sync_task_2("Aioclock won't color your functions!"))
@group.task(trigger=Every(seconds=2))
async def async_task(val: str = Depends(dependency)):
print(f"{val} `async_task` {threading.current_thread().ident}")
@asynccontextmanager
async def lifespan(aio_clock: AioClock) -> AsyncGenerator[AioClock]:
print("Welcome!")
yield aio_clock
print("Bye!")
# app.py
app = AioClock(lifespan=lifespan)
app.include_group(group)
if __name__ == "__main__":
asyncio.run(app.serve())
================================================
FILE: examples/awesome_triggers.py
================================================
import asyncio
from collections.abc import AsyncGenerator
from contextlib import asynccontextmanager
from aioclock import AioClock, At, Depends, Every, Forever, Once
from aioclock.group import Group
# groups.py
group = Group()
def more_useless_than_me():
return "I'm a dependency. I'm more useless than a screen door on a submarine."
@group.task(trigger=Every(seconds=10))
async def every():
print("Every 10 seconds, I make a quantum leap. Where will I land next?")
@group.task(trigger=Every(seconds=5))
def even_sync_works():
print("I'm a synchronous task. I work even in async world.")
@group.task(trigger=At(tz="UTC", hour=0, minute=0, second=0))
async def at():
print(
"When the clock strikes midnight... I turn into a pumpkin. Just kidding, I run this task!"
)
@group.task(trigger=Forever())
async def forever(val: str = Depends(more_useless_than_me)):
await asyncio.sleep(2)
print("Heartbeat detected. Still not a zombie. Will check again in a bit.")
assert val == "I'm a dependency. I'm more useless than a screen door on a submarine."
@group.task(trigger=Once())
async def once():
print("Just once, I get to say something. Here it goes... I love lamp.")
@asynccontextmanager
async def lifespan(aio_clock: AioClock) -> AsyncGenerator[AioClock]:
print(
"Welcome to the Async Chronicles! Did you know a group of unicorns is called a blessing? Well, now you do!"
)
yield aio_clock
print("Going offline. Remember, if your code is running, you better go catch it!")
# app.py
app = AioClock(lifespan=lifespan)
app.include_group(group)
# main.py
if __name__ == "__main__":
asyncio.run(app.serve())
================================================
FILE: examples/dependency_injection.py
================================================
import asyncio
from aioclock import AioClock, Depends, Every, Group
# service1.py
group = Group()
def dependency():
return "Hello, world!"
def overwritten_dependency():
return "Goodbye, world!"
@group.task(trigger=Every(seconds=1))
async def my_task(val: str = Depends(dependency)):
print(val)
# app.py
app = AioClock()
app.include_group(group)
app.override_dependencies(dependency, overwritten_dependency)
if __name__ == "__main__":
asyncio.run(app.serve())
================================================
FILE: examples/with_fast_api.py
================================================
import asyncio
from collections.abc import AsyncGenerator
from contextlib import asynccontextmanager
from fastapi import FastAPI
from aioclock import AioClock
from aioclock.ext.fast import make_fastapi_router
from aioclock.triggers import Every
@asynccontextmanager
async def aioclock_lifespan(aio_clock: AioClock) -> AsyncGenerator[AioClock]:
print("Starting aiolcok app...")
yield aio_clock
print("Closing aiolcok app...")
clock_app = AioClock(lifespan=aioclock_lifespan)
@clock_app.task(trigger=Every(seconds=3600))
async def foo():
print("Foo is processing...")
@asynccontextmanager
async def lifespan(app: FastAPI):
task = asyncio.create_task(clock_app.serve())
yield
try:
task.cancel()
await task
except asyncio.CancelledError:
...
app = FastAPI(lifespan=lifespan)
app.include_router(make_fastapi_router(clock_app))
if __name__ == "__main__":
import uvicorn
uvicorn.run(app)
================================================
FILE: mkdocs.yml
================================================
site_name: AioClock
site_description: An asyncio-based scheduling framework designed for execution of periodic task with integrated support for dependency injection, enabling efficient and flexiable task management
repo_url: https://github.com/ManiMozaffar/aioclock
site_url: https://ManiMozaffar.github.io/aioclock
site_author: Mani Mozaffar
repo_name: ManiMozaffar/aioclock
copyright: Maintained by Mani Mozaffar.
theme:
name: "material"
palette:
- media: "(prefers-color-scheme)"
toggle:
icon: material/link
name: Switch to light mode
- media: "(prefers-color-scheme: light)"
scheme: default
primary: indigo
accent: indigo
toggle:
icon: material/toggle-switch
name: Switch to dark mode
- media: "(prefers-color-scheme: dark)"
scheme: slate
primary: black
accent: indigo
toggle:
icon: material/toggle-switch-off
name: Switch to system preference
features:
- navigation.tabs
- navigation.instant
- content.code.annotate
- content.tabs.link
- content.code.copy
- announce.dismiss
- search.suggest
- search.highlight
# logo: assets/logo-white.svg
# favicon: assets/favicon.png
edit_uri: ""
# https://www.mkdocs.org/user-guide/configuration/#validation
validation:
omitted_files: warn
absolute_links: warn
unrecognized_links: warn
extra:
navigation:
next: true
previous: true
extra_css:
- "extra/tweaks.css"
markdown_extensions:
- toc:
permalink: true
- admonition
- pymdownx.details
- pymdownx.extra
- pymdownx.superfences
- pymdownx.highlight:
anchor_linenums: true
- pymdownx.inlinehilite
- pymdownx.snippets
- attr_list
- md_in_html
watch:
- aioclock
plugins:
- mike:
alias_type: symlink
canonical_version: latest
- search:
- mkdocstrings:
handlers:
python:
paths:
- aioclock
options:
members_order: source
separate_signature: true
docstring_options:
ignore_init_summary: true
merge_init_into_class: true
show_signature_annotations: true
signature_crossrefs: true
- mkdocs-simple-hooks:
hooks:
on_pre_build: "docs.plugins:on_pre_build"
on_files: "docs.plugins:on_files"
on_page_markdown: "docs.plugins:on_page_markdown"
nav:
- Introduction: index.md
- Documentation:
- Overview: overview.md
- Basic Usage:
- AioClock Application: api/getting_started.md
- Triggers: api/triggers.md
- Advance Usage:
- Task: api/task.md
- External API: api/external_api.md
- Plugins (FastAPI included): api/plugin.md
- Using beside FastAPI: examples/fastapi.md
- Using beside Message Brokers: examples/brokers.md
- Alternatives: alternative.md
================================================
FILE: pyproject.toml
================================================
[project]
name = "aioclock"
version = "0.3.1"
description = "An asyncio-based scheduling framework designed for execution of periodic task with integrated support for dependency injection, enabling efficient and flexible task management"
authors = [{ name = "Mani Mozaffar", email = "mani.mozaffar@gmail.com" }]
readme = "README.md"
requires-python = ">= 3.8"
dependencies = [
"pydantic[timezone]>=2.9.0",
"fast-depends>=2.4.0",
"asyncer>=0.0.7",
"croniter>=2.0.5",
]
license = 'MIT'
[project.urls]
repository = "https://github.com/ManiMozaffar/aioclock"
Homepage = 'https://github.com/ManiMozaffar/aioclock'
Documentation = 'https://github.com/ManiMozaffar/aioclock'
Source = 'https://github.com/ManiMozaffar/aioclock'
[project.optional-dependencies]
fastapi = ["fastapi"]
[build-system]
requires = ["hatchling"]
build-backend = "hatchling.build"
[tool.rye]
dev-dependencies = [
"pytest>=7.2.0",
"pytest-asyncio>=0.23.6",
"tox>=4.11.1",
"rich>=13.7.1",
"pyright>=1.1.350",
"mike==2.0.0",
"pytest-asyncio>=0.23.7",
"mkdocs>=1.4.2",
"mkdocs-material>=9.2.7",
"mkdocstrings[python]>=0.25.1",
"mkdocs-simple-hooks>=0.1.5",
"black>=24.4.2",
"fastapi>=0.90",
"ruff>=0.3.5",
"mkdocs-material-extensions>=1.3.1",
]
[tool.behavior]
use-uv = true
[tool.hatch.metadata]
allow-direct-references = true
[tool.hatch.build.targets.wheel]
packages = ["aioclock"]
[tool.setuptools.package-data]
"aioclock" = ["py.typed"]
[tool.ruff]
lint.fixable = [
"A",
"B",
"C",
"D",
"E",
"F",
"G",
"I",
"N",
"Q",
"S",
"T",
"W",
"ANN",
"ARG",
"BLE",
"COM",
"DJ",
"DTZ",
"EM",
"ERA",
"EXE",
"FBT",
"ICN",
"INP",
"ISC",
"NPY",
"PD",
"PGH",
"PIE",
"PL",
"PT",
"PTH",
"PYI",
"RET",
"RSE",
"RUF",
"SIM",
"SLF",
"TCH",
"TID",
"TRY",
"UP",
"YTT",
]
lint.unfixable = []
lint.exclude = [
".bzr",
".direnv",
".eggs",
".git",
".git-rewrite",
".hg",
".mypy_cache",
".nox",
".pants.d",
".pytype",
".ruff_cache",
".svn",
".tox",
".venv",
"__pypackages__",
"_build",
"buck-out",
"build",
"dist",
"node_modules",
"venv",
]
line-length = 100
[tool.ruff.format]
preview = true
[tool.ruff.lint.per-file-ignores]
"tests/*" = ["S101"]
[tool.pyright]
typeCheckingMode = "basic"
[tool.pytest.ini_options]
testpaths = ["tests"]
================================================
FILE: tests/__init__.py
================================================
================================================
FILE: tests/test_di.py
================================================
import pytest
from aioclock import AioClock, Depends, Once
from aioclock.api import run_with_injected_deps
app = AioClock()
def some_dependency():
return 1
@app.task(trigger=Once())
async def main(bar: int = Depends(some_dependency)):
print("Hello World")
return bar
@pytest.mark.asyncio
async def test_run_manual():
foo = await run_with_injected_deps(main)
assert foo == 1
================================================
FILE: tests/test_examples.py
================================================
import importlib.util
import inspect
import logging
import os
import sys
import textwrap
logger = logging.getLogger(__name__)
def extract_code_blocks(docstring: str) -> list[str]:
"""Extract code blocks from a docstring."""
code_blocks = []
in_code_block = False
code_block = []
for line in docstring.split("\n"):
if line.strip().startswith("```python"):
in_code_block = True
continue
elif line.strip().startswith("```"):
in_code_block = False
if code_block:
code_blocks.append("\n".join(code_block))
code_block = []
continue
if in_code_block:
code_block.append(line)
return code_blocks
def exec_example(example: str) -> None:
"""Execute a code example."""
example = textwrap.dedent(example)
namespace = {"__name__": "__not_main__"}
try:
code = compile(example, "", "exec")
exec(code, namespace)
except Exception:
raise Exception(f"Failed to execute example:\n\n{example}")
def process_object(name: str, obj: object, module_name: str, library_name: str) -> None:
"""Process a single object, extracting and executing code examples."""
docstring = inspect.getdoc(obj)
if docstring:
examples = extract_code_blocks(docstring)
for example in examples:
exec_example(example)
# Recursively process class methods only if they belong to the same library
if inspect.isclass(obj):
for method_name, method_obj in inspect.getmembers(obj, inspect.isfunction):
if method_obj.__module__ and method_obj.__module__.startswith(library_name):
process_object(f"{name}.{method_name}", method_obj, module_name, library_name)
def process_module(module_path: str) -> None:
"""Process a single Python module."""
module_name = os.path.splitext(os.path.basename(module_path))[0]
spec = importlib.util.spec_from_file_location(module_name, module_path)
if spec is None or spec.loader is None:
raise Exception(f"Could not load module: {module_path}")
module = importlib.util.module_from_spec(spec)
sys.modules[module_name] = module
spec.loader.exec_module(module)
library_name = "aioclock"
for name, obj in inspect.getmembers(
module, lambda o: inspect.isfunction(o) or inspect.isclass(o)
):
if obj.__module__ and obj.__module__.startswith(library_name):
process_object(name, obj, module_name, library_name)
def process_markdown(markdown_path: str) -> None:
"""Process a markdown file and execute code blocks."""
with open(markdown_path, "r") as file:
content = file.read()
code_blocks = extract_code_blocks(content)
for example in code_blocks:
exec_example(example)
def traverse_library(library_path: str) -> None:
"""Traverse the library directory and process each Python module."""
original_sys_path = sys.path.copy()
sys.path.insert(0, os.path.abspath(library_path)) # Prioritize the library path
for root, _, files in os.walk(library_path):
for file in files:
if file.endswith(".py"):
module_path = os.path.join(root, file)
process_module(module_path)
sys.path = original_sys_path # Restore the original sys.path
def traverse_docs(docs_path: str) -> None:
"""Traverse the docs directory and process each markdown file."""
for root, _, files in os.walk(docs_path):
for file in files:
if file.endswith(".md"):
markdown_path = os.path.join(root, file)
process_markdown(markdown_path)
def test_examples():
docs_path = "../aioclock/docs"
traverse_docs(docs_path)
library_path = "../aioclock/aioclock"
traverse_library(library_path)
================================================
FILE: tests/test_lifespan.py
================================================
from contextlib import asynccontextmanager, contextmanager
import pytest
from aioclock import AioClock
from aioclock.triggers import Once
ML_MODEL_ASYNC = []
RAN_ONCE_TASK_ASYNC = False
@asynccontextmanager
async def lifespan(app: AioClock):
ML_MODEL_ASYNC.append(2)
yield app
ML_MODEL_ASYNC.clear()
app = AioClock(lifespan=lifespan)
@app.task(trigger=Once())
async def main():
assert len(ML_MODEL_ASYNC) == 1
global RAN_ONCE_TASK_ASYNC
RAN_ONCE_TASK_ASYNC = True
@pytest.mark.asyncio
async def test_lifespan_e2e_async():
assert len(ML_MODEL_ASYNC) == 0
assert RAN_ONCE_TASK_ASYNC is False
await app.serve() # asserts are in the task
assert len(ML_MODEL_ASYNC) == 0 # clean up done
assert RAN_ONCE_TASK_ASYNC is True # task ran
ML_MODEL_SYNC = [] # just some imaginary component that needs to be started and stopped
RAN_ONCE_TASK_SYNC = False
@contextmanager
def lifespan_sync(sync_app: AioClock):
ML_MODEL_SYNC.append(2)
yield sync_app
ML_MODEL_SYNC.clear()
sync_app = AioClock(lifespan=lifespan_sync)
@sync_app.task(trigger=Once())
def sync_main():
assert len(ML_MODEL_SYNC) == 1
global RAN_ONCE_TASK_SYNC
RAN_ONCE_TASK_SYNC = True
@pytest.mark.asyncio
async def test_lifespan_e2e_sync():
assert len(ML_MODEL_SYNC) == 0
assert RAN_ONCE_TASK_SYNC is False
await sync_app.serve() # asserts are in the task
assert len(ML_MODEL_SYNC) == 0 # clean up done
assert RAN_ONCE_TASK_SYNC is True # task ran
================================================
FILE: tests/test_timeout.py
================================================
import asyncio
from contextlib import contextmanager
from datetime import datetime
import pytest
from aioclock import AioClock, Once
app = AioClock()
@contextmanager
def assert_execution_time_below(target: float):
start_time = datetime.now()
yield
assert (datetime.now() - start_time).total_seconds() < target
@app.task(trigger=Once(), timeout=0.1)
async def main():
await asyncio.sleep(10)
@pytest.mark.asyncio
async def test_run_manual():
task_with_timeout = app._get_tasks()[0]
with assert_execution_time_below(0.5):
await task_with_timeout.run()
================================================
FILE: tests/test_triggers.py
================================================
from datetime import datetime, timedelta
import pytest
import zoneinfo
from aioclock.triggers import At, Cron, Every, Forever, LoopController, Once, OrTrigger
def test_at_trigger():
# test this sunday
trigger = At(at="every sunday", hour=14, minute=1, second=0, tz="Europe/Istanbul")
val = trigger._get_next_ts(
datetime(
year=2024,
month=3,
day=31,
hour=14,
minute=00,
second=0,
tzinfo=zoneinfo.ZoneInfo("Europe/Istanbul"),
)
)
assert val == 60
# test next week
trigger = At(at="every sunday", hour=14, second=59, tz="Europe/Istanbul")
val = trigger._get_next_ts(
datetime(
year=2024,
month=3,
day=31,
hour=14,
minute=0,
second=0,
tzinfo=zoneinfo.ZoneInfo("Europe/Istanbul"),
)
)
assert val == 59
# test every day
trigger = At(at="every day", hour=14, second=59, tz="Europe/Istanbul")
this_sunday = datetime(
year=2024,
month=3,
day=31,
hour=14,
minute=0,
second=0,
tzinfo=zoneinfo.ZoneInfo("Europe/Istanbul"),
)
val = trigger._get_next_ts(this_sunday)
assert val == 59
# test next week
trigger = At(at="every saturday", hour=14, second=0, tz="Europe/Istanbul")
val = trigger._get_next_ts(this_sunday)
assert val == 86400
# right NOW
trigger = At(at="every sunday", hour=14, tz="Europe/Istanbul")
val = trigger._get_next_ts(this_sunday)
assert val == 0
# next week but 1h before than now
trigger = At(at="every sunday", hour=13, tz="Europe/Istanbul")
val = trigger._get_next_ts(this_sunday)
assert val == timedelta(days=7).total_seconds() - timedelta(hours=1).total_seconds()
def test_cross_timezone():
# `NOW` is same day as the trigger, but trigger should be next week with different timezone
trigger = At(hour=3, tz="Europe/Berlin", at="every saturday")
assert (
trigger._get_next_ts(
datetime.fromtimestamp(1720296232, tz=zoneinfo.ZoneInfo("Europe/Istanbul"))
)
== 532568
)
@pytest.mark.asyncio
async def test_loop_controller():
# since once trigger is triggered, it should not trigger again.
trigger = Once()
assert trigger.should_trigger() is True
await trigger.trigger_next()
assert trigger.should_trigger() is False
class IterateFiveTime(LoopController):
type_: str = "foo"
async def trigger_next(self) -> None:
self._increment_loop_counter()
return None
trigger = IterateFiveTime(max_loop_count=5)
for _ in range(5):
assert trigger.should_trigger() is True
await trigger.trigger_next()
assert trigger.should_trigger() is False
@pytest.mark.asyncio
async def test_forever():
trigger = Forever()
assert trigger.should_trigger() is True
await trigger.trigger_next()
assert trigger.should_trigger() is True
await trigger.trigger_next()
assert trigger.should_trigger() is True
@pytest.mark.asyncio
async def test_every():
# wait should always wait for the period on first run
trigger = Every(seconds=1, first_run_strategy="wait")
assert await trigger.get_waiting_time_till_next_trigger() == 1
# immediate should always execute immediately, but wait for the period from second run.
trigger = Every(seconds=1, first_run_strategy="immediate")
assert await trigger.get_waiting_time_till_next_trigger() == 0
trigger._increment_loop_counter()
assert await trigger.get_waiting_time_till_next_trigger() == 1
@pytest.mark.asyncio
async def test_cron():
# it's dumb idea to test library, but I don't trust it 100%, and it might drop it in the future.
trigger = Cron(cron="* * * * *", tz="UTC")
val = await trigger.get_waiting_time_till_next_trigger(
datetime(
year=2024,
month=3,
day=31,
hour=14,
minute=0,
second=0,
tzinfo=zoneinfo.ZoneInfo("UTC"),
)
)
assert val == 60
trigger = Cron(cron="2-10 * * * *", tz="UTC")
assert (
await trigger.get_waiting_time_till_next_trigger(
datetime(
year=2024,
month=3,
day=31,
hour=11,
minute=48,
second=0,
tzinfo=zoneinfo.ZoneInfo("Europe/Berlin"),
)
)
== 14 * 60
)
with pytest.raises(ValueError):
Cron(cron="* * * * 65", tz="UTC")
@pytest.mark.asyncio
async def test_or_trigger_state():
trigger = OrTrigger(triggers=[Once(), Once()])
assert trigger.should_trigger() is True
await trigger.trigger_next()
assert trigger.should_trigger() is True
await trigger.trigger_next()
assert trigger.should_trigger() is False
@pytest.mark.asyncio
async def test_or_trigger_next():
trigger = OrTrigger(
triggers=[Every(seconds=0, max_loop_count=2), Every(seconds=0, max_loop_count=2)]
)
for _ in range(4):
assert trigger.should_trigger() is True
assert (await trigger.get_waiting_time_till_next_trigger()) == 0
await trigger.trigger_next()
assert trigger.should_trigger() is False
================================================
FILE: tox.ini
================================================
[tox]
skipsdist = true
envlist = py39, py310, py311
[gh-actions]
python =
3.9: py39
3.10: py310
3.11: py311
[testenv:py39]
passenv = PYTHON_VERSION
allowlist_externals = rye,pytest,pyright
commands =
rye pin 3.9
rye sync
rye run pytest tests
[testenv:py310]
passenv = PYTHON_VERSION
allowlist_externals = rye,pytest,pyright
commands =
rye pin 3.10
rye sync
rye run pytest tests
[testenv:py311]
passenv = PYTHON_VERSION
allowlist_externals = rye,pytest,pyright
commands =
rye pin 3.11
rye sync
rye run pytest tests