Showing preview only (235K chars total). Download the full file or copy to clipboard to get everything.
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**: <https://github.com/ManiMozaffar/aioclock/>
## 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():
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
SYMBOL INDEX (127 symbols across 21 files)
FILE: aioclock/api.py
class TaskMetadata (line 38) | class TaskMetadata(BaseModel):
function run_specific_task (line 56) | async def run_specific_task(task_id: UUID, app: AioClock):
function run_with_injected_deps (line 86) | async def run_with_injected_deps(func: Callable[P, Awaitable[T]]) -> T:
function get_metadata_of_all_tasks (line 118) | async def get_metadata_of_all_tasks(app: AioClock) -> list[TaskMetadata]:
FILE: aioclock/app.py
class AioClock (line 48) | class AioClock:
method __init__ (line 146) | def __init__(
method dependencies (line 186) | def dependencies(self):
method override_dependencies (line 190) | def override_dependencies(
method include_group (line 218) | def include_group(self, group: Group) -> None:
method task (line 242) | def task(self, *, trigger: BaseTrigger, timeout: Optional[float] = None):
method _tasks (line 304) | def _tasks(self) -> list[Task]:
method _get_shutdown_task (line 309) | def _get_shutdown_task(self) -> list[Task]:
method _get_startup_task (line 312) | def _get_startup_task(self) -> list[Task]:
method _get_tasks (line 315) | def _get_tasks(self, exclude_type: Union[set[Triggers], None] = None) ...
method serve (line 324) | async def serve(self) -> None:
method _run_tasks (line 347) | async def _run_tasks(self) -> None:
FILE: aioclock/custom_types.py
class Triggers (line 26) | class Triggers(StrEnum):
FILE: aioclock/exceptions.py
class BaseAioClockException (line 1) | class BaseAioClockException(Exception):
class TaskIdNotFound (line 5) | class TaskIdNotFound(BaseAioClockException):
class TaskTimeoutError (line 9) | class TaskTimeoutError(BaseAioClockException, TimeoutError):
FILE: aioclock/ext/fast.py
function make_fastapi_router (line 28) | def make_fastapi_router(aioclock: AioClock, router: Union[APIRouter, Non...
FILE: aioclock/group.py
class Group (line 24) | class Group:
method __init__ (line 25) | def __init__(
method task (line 89) | def task(self, *, trigger: BaseTrigger, timeout: Optional[float] = None):
method _run (line 164) | async def _run(self):
FILE: aioclock/provider.py
function get_provider (line 7) | def get_provider():
FILE: aioclock/task.py
class Task (line 24) | class Task:
method run (line 44) | async def run(self):
FILE: aioclock/triggers.py
class BaseTrigger (line 55) | class BaseTrigger(BaseModel, ABC, Generic[TriggerTypeT]):
method trigger_next (line 102) | async def trigger_next(self) -> None:
method should_trigger (line 108) | def should_trigger(self) -> bool:
method get_waiting_time_till_next_trigger (line 119) | async def get_waiting_time_till_next_trigger(self) -> Union[float, None]:
class Forever (line 127) | class Forever(BaseTrigger[Literal[Triggers.FOREVER]]):
method should_trigger (line 161) | def should_trigger(self) -> bool:
method trigger_next (line 164) | async def trigger_next(self) -> None:
method get_waiting_time_till_next_trigger (line 167) | async def get_waiting_time_till_next_trigger(self):
class LoopController (line 171) | class LoopController(BaseTrigger, ABC, Generic[TriggerTypeT]):
method validate_loop_control (line 194) | def validate_loop_control(self):
method _increment_loop_counter (line 199) | def _increment_loop_counter(self) -> None:
method should_trigger (line 202) | def should_trigger(self) -> bool:
method get_waiting_time_till_next_trigger (line 209) | async def get_waiting_time_till_next_trigger(self):
class Once (line 213) | class Once(LoopController[Literal[Triggers.ONCE]]):
method trigger_next (line 230) | async def trigger_next(self) -> None:
method get_waiting_time_till_next_trigger (line 234) | async def get_waiting_time_till_next_trigger(self):
class OnStartUp (line 243) | class OnStartUp(LoopController[Literal[Triggers.ON_START_UP]]):
method trigger_next (line 260) | async def trigger_next(self) -> None:
method get_waiting_time_till_next_trigger (line 264) | async def get_waiting_time_till_next_trigger(self):
class OnShutDown (line 273) | class OnShutDown(LoopController[Literal[Triggers.ON_SHUT_DOWN]]):
method trigger_next (line 290) | async def trigger_next(self) -> None:
method get_waiting_time_till_next_trigger (line 294) | async def get_waiting_time_till_next_trigger(self):
class Every (line 300) | class Every(LoopController[Literal[Triggers.EVERY]]):
method validate_time_units (line 342) | def validate_time_units(self):
method to_seconds (line 355) | def to_seconds(self) -> float:
method trigger_next (line 368) | async def trigger_next(self) -> None:
method get_waiting_time_till_next_trigger (line 375) | async def get_waiting_time_till_next_trigger(self):
class At (line 391) | class At(LoopController[Literal[Triggers.AT]]):
method validate_time_units (line 439) | def validate_time_units(self):
method _shift_to_declared_weekday (line 451) | def _shift_to_declared_weekday(self, target_time: datetime, tz_aware_n...
method _get_next_ts (line 464) | def _get_next_ts(self, now: datetime) -> float:
method get_waiting_time_till_next_trigger (line 471) | async def get_waiting_time_till_next_trigger(self, now: Union[datetime...
method trigger_next (line 478) | async def trigger_next(self) -> None:
class Cron (line 483) | class Cron(LoopController[Literal[Triggers.CRON]]):
method validate_time_units (line 516) | def validate_time_units(self):
method get_waiting_time_till_next_trigger (line 527) | async def get_waiting_time_till_next_trigger(self, now: Union[datetime...
method trigger_next (line 535) | async def trigger_next(self) -> None:
class OrTrigger (line 540) | class OrTrigger(LoopController[Literal[Triggers.OR]]):
method should_trigger (line 588) | def should_trigger(self) -> bool:
method find_closest_trigger (line 594) | async def find_closest_trigger(self) -> tuple[BaseTrigger, float | None]:
method trigger_next (line 607) | async def trigger_next(self) -> None:
method get_waiting_time_till_next_trigger (line 613) | async def get_waiting_time_till_next_trigger(self):
FILE: aioclock/utils.py
function flatten_chain (line 8) | def flatten_chain(matrix: list[Iterable[T]]) -> list[T]:
class StrEnum (line 12) | class StrEnum(str, Enum):
method _generate_next_value_ (line 21) | def _generate_next_value_(name: str, start: int, count: int, last_valu...
method __str__ (line 28) | def __str__(self) -> str:
FILE: deploy_docs.py
function run_command (line 10) | def run_command(command: str) -> None:
FILE: docs/plugins.py
function on_pre_build (line 15) | def on_pre_build(config: Config):
function on_files (line 19) | def on_files(files: Files, config: Config) -> Files:
function remove_files (line 23) | def remove_files(files: Files) -> Files:
function on_page_markdown (line 37) | def on_page_markdown(markdown: str, page: Page, config: Config, files: F...
function add_version (line 42) | def add_version(markdown: str, page: Page) -> str:
function remove_code_fence_attributes (line 59) | def remove_code_fence_attributes(markdown: str) -> str:
FILE: examples/app.py
function dependency (line 14) | def dependency():
function sync_task_1 (line 19) | def sync_task_1(val: str = Depends(dependency)):
function sync_task_2 (line 25) | def sync_task_2(val: Annotated[str, Depends(dependency)]):
function async_task (line 35) | async def async_task(val: str = Depends(dependency)):
function lifespan (line 40) | async def lifespan(aio_clock: AioClock) -> AsyncGenerator[AioClock]:
FILE: examples/awesome_triggers.py
function more_useless_than_me (line 12) | def more_useless_than_me():
function every (line 17) | async def every():
function even_sync_works (line 22) | def even_sync_works():
function at (line 27) | async def at():
function forever (line 34) | async def forever(val: str = Depends(more_useless_than_me)):
function once (line 41) | async def once():
function lifespan (line 46) | async def lifespan(aio_clock: AioClock) -> AsyncGenerator[AioClock]:
FILE: examples/dependency_injection.py
function dependency (line 9) | def dependency():
function overwritten_dependency (line 13) | def overwritten_dependency():
function my_task (line 18) | async def my_task(val: str = Depends(dependency)):
FILE: examples/with_fast_api.py
function aioclock_lifespan (line 13) | async def aioclock_lifespan(aio_clock: AioClock) -> AsyncGenerator[AioCl...
function foo (line 23) | async def foo():
function lifespan (line 28) | async def lifespan(app: FastAPI):
FILE: tests/test_di.py
function some_dependency (line 9) | def some_dependency():
function main (line 14) | async def main(bar: int = Depends(some_dependency)):
function test_run_manual (line 20) | async def test_run_manual():
FILE: tests/test_examples.py
function extract_code_blocks (line 11) | def extract_code_blocks(docstring: str) -> list[str]:
function exec_example (line 34) | def exec_example(example: str) -> None:
function process_object (line 45) | def process_object(name: str, obj: object, module_name: str, library_nam...
function process_module (line 60) | def process_module(module_path: str) -> None:
function process_markdown (line 80) | def process_markdown(markdown_path: str) -> None:
function traverse_library (line 90) | def traverse_library(library_path: str) -> None:
function traverse_docs (line 104) | def traverse_docs(docs_path: str) -> None:
function test_examples (line 113) | def test_examples():
FILE: tests/test_lifespan.py
function lifespan (line 13) | async def lifespan(app: AioClock):
function main (line 23) | async def main():
function test_lifespan_e2e_async (line 30) | async def test_lifespan_e2e_async():
function lifespan_sync (line 43) | def lifespan_sync(sync_app: AioClock):
function sync_main (line 53) | def sync_main():
function test_lifespan_e2e_sync (line 60) | async def test_lifespan_e2e_sync():
FILE: tests/test_timeout.py
function assert_execution_time_below (line 13) | def assert_execution_time_below(target: float):
function main (line 20) | async def main():
function test_run_manual (line 25) | async def test_run_manual():
FILE: tests/test_triggers.py
function test_at_trigger (line 9) | def test_at_trigger():
function test_cross_timezone (line 72) | def test_cross_timezone():
function test_loop_controller (line 84) | async def test_loop_controller():
function test_forever (line 107) | async def test_forever():
function test_every (line 117) | async def test_every():
function test_cron (line 130) | async def test_cron():
function test_or_trigger_state (line 168) | async def test_or_trigger_state():
function test_or_trigger_next (line 178) | async def test_or_trigger_next():
Condensed preview — 50 files, each showing path, character count, and a content snippet. Download the .json file or copy for the full structured content (249K chars).
[
{
"path": ".github/workflows/deploy-docs-on-demand.yml",
"chars": 495,
"preview": "name: Deploy Docs On Demand\n\non:\n workflow_dispatch:\n\njobs:\n deploy-docs-on-demand:\n runs-on: ubuntu-latest\n ste"
},
{
"path": ".github/workflows/main.yml",
"chars": 812,
"preview": "name: Main\n\non:\n push:\n branches:\n - main\n pull_request:\n types: [opened, synchronize, reopened]\n\njobs:\n t"
},
{
"path": ".github/workflows/on-release-main.yml",
"chars": 947,
"preview": "name: release-main\n\non:\n release:\n types: [published]\n branches: [main]\n\njobs:\n publish:\n runs-on: ubuntu-lat"
},
{
"path": ".gitignore",
"chars": 2866,
"preview": "docs/source\n\n# From https://raw.githubusercontent.com/github/gitignore/main/Python.gitignore\n\n# Byte-compiled / optimize"
},
{
"path": ".python-version",
"chars": 7,
"preview": "3.11.9\n"
},
{
"path": "LICENSE",
"chars": 1072,
"preview": "MIT License\n\nCopyright (c) 2024, Mani Mozaffar\n\nPermission is hereby granted, free of charge, to any person obtaining a "
},
{
"path": "Makefile",
"chars": 927,
"preview": ".PHONY: install\ninstall: ## Install the rye environment\n\t@echo \"🚀 Creating virtual environment using rye and uv\"\n\trye sy"
},
{
"path": "README.md",
"chars": 3558,
"preview": "# aioclock\n\n[](https://img.shields.io/github/v/"
},
{
"path": "aioclock/__init__.py",
"chars": 392,
"preview": "from fast_depends import Depends\n\nfrom aioclock.app import AioClock\nfrom aioclock.group import Group\nfrom aioclock.trigg"
},
{
"path": "aioclock/api.py",
"chars": 4917,
"preview": "\"\"\"\nExternal API of the aioclock package, that can be used to interact with the AioClock instance.\nThis module could be "
},
{
"path": "aioclock/app.py",
"chars": 10965,
"preview": "\"\"\"\nTo initialize the AioClock instance, you need to import the AioClock class from the aioclock module.\nAioClock class "
},
{
"path": "aioclock/custom_types.py",
"chars": 1070,
"preview": "from enum import auto\nfrom typing import Annotated, Literal, Union\n\nfrom annotated_types import Interval\n\nfrom aioclock."
},
{
"path": "aioclock/exceptions.py",
"chars": 281,
"preview": "class BaseAioClockException(Exception):\n \"\"\"Base exception for aioclock.\"\"\"\n\n\nclass TaskIdNotFound(BaseAioClockExcept"
},
{
"path": "aioclock/ext/__init__.py",
"chars": 388,
"preview": "\"\"\"\nExtensions for aioclock.\n\nAioClock is very extensible, and you can add your own extensions to it.\nThe extension woul"
},
{
"path": "aioclock/ext/fast.py",
"chars": 2878,
"preview": "\"\"\"FastAPI extension to manage the tasks of the AioClock instance in HTTP Layer.\n\nUse cases:\n - Expose the tasks of t"
},
{
"path": "aioclock/group.py",
"chars": 5310,
"preview": "import asyncio\nimport sys\nfrom functools import wraps\nfrom typing import Optional, TypeVar\n\nfrom asyncer import asyncify"
},
{
"path": "aioclock/logger.py",
"chars": 55,
"preview": "import logging\n\nlogger = logging.getLogger(\"aioclock\")\n"
},
{
"path": "aioclock/provider.py",
"chars": 245,
"preview": "from functools import lru_cache\n\nfrom fast_depends import Provider\n\n\n@lru_cache\ndef get_provider():\n \"\"\"Return a Prov"
},
{
"path": "aioclock/py.typed",
"chars": 0,
"preview": ""
},
{
"path": "aioclock/task.py",
"chars": 3060,
"preview": "\"\"\"\nAioclock wrap your functions with a task object,\nand append the task to the list of tasks in the AioClock instance.\n"
},
{
"path": "aioclock/triggers.py",
"chars": 21457,
"preview": "\"\"\"\nTriggers are used to determine when the event should be triggered. It can be based on time, or some other condition."
},
{
"path": "aioclock/utils.py",
"chars": 862,
"preview": "from enum import Enum\nfrom itertools import chain\nfrom typing import Iterable, TypeVar\n\nT = TypeVar(\"T\")\n\n\ndef flatten_c"
},
{
"path": "deploy_docs.py",
"chars": 600,
"preview": "import logging\nimport subprocess\nimport sys\n\nfrom aioclock import __version__\n\nlogger = logging.getLogger(__name__)\n\n\nde"
},
{
"path": "docs/alternative.md",
"chars": 5740,
"preview": "# AioClock VS Alternatives\n\nThere are other alternatives for scheduling as well.\nThis section contains comparisons betwe"
},
{
"path": "docs/api/external_api.md",
"chars": 17,
"preview": "::: aioclock.api\n"
},
{
"path": "docs/api/getting_started.md",
"chars": 36,
"preview": "::: aioclock.app\n::: aioclock.group\n"
},
{
"path": "docs/api/plugin.md",
"chars": 39,
"preview": "::: aioclock.ext\n::: aioclock.ext.fast\n"
},
{
"path": "docs/api/task.md",
"chars": 18,
"preview": "::: aioclock.task\n"
},
{
"path": "docs/api/triggers.md",
"chars": 34,
"preview": "# Triggers\n\n::: aioclock.triggers\n"
},
{
"path": "docs/diagrams/aioclock.excalidraw",
"chars": 121616,
"preview": "{\n \"type\": \"excalidraw\",\n \"version\": 2,\n \"source\": \"https://excalidraw.com\",\n \"elements\": [\n {\n \"type\": \"arr"
},
{
"path": "docs/examples/brokers.md",
"chars": 1646,
"preview": "You can basically run any tasks on aioclock, it could be your redis broker or other kind of brokers listening to a queue"
},
{
"path": "docs/examples/fastapi.md",
"chars": 1299,
"preview": "To run AioClock with FastAPI, you can run it in the background with FastAPI lifespan, next to your asgi.\n\n```python\nfrom"
},
{
"path": "docs/extra/tweaks.css",
"chars": 162,
"preview": "/* Revert hue value to that of pre mkdocs-material v9.4.0 */\n[data-md-color-scheme='slate'] {\n --md-hue: 230;\n --md-de"
},
{
"path": "docs/images/README.md",
"chars": 146,
"preview": "Please check th diagrams folder if you're wondering where diagrams come from.\nYou can load them in [excalidraw](https://"
},
{
"path": "docs/index.md",
"chars": 2929,
"preview": "# AioClock\n\n## The Principle\n\nScheduling is annoying, stateful and hard to scale. But not anymore! AioClock is here as a"
},
{
"path": "docs/overview.md",
"chars": 4761,
"preview": "# AioClock: Overview\n\n## Introduction\n\nAioClock is a lightweight, asynchronous task scheduling framework designed for Py"
},
{
"path": "docs/plugins.py",
"chars": 2436,
"preview": "import os\nimport re\nfrom typing import Match\n\nfrom mkdocs.config import Config\nfrom mkdocs.structure.files import Files\n"
},
{
"path": "examples/app.py",
"chars": 1235,
"preview": "import asyncio\nfrom collections.abc import AsyncGenerator\nfrom contextlib import asynccontextmanager\nimport threading\nfr"
},
{
"path": "examples/awesome_triggers.py",
"chars": 1690,
"preview": "import asyncio\nfrom collections.abc import AsyncGenerator\nfrom contextlib import asynccontextmanager\n\nfrom aioclock impo"
},
{
"path": "examples/dependency_injection.py",
"chars": 488,
"preview": "import asyncio\n\nfrom aioclock import AioClock, Depends, Every, Group\n\n# service1.py\ngroup = Group()\n\n\ndef dependency():\n"
},
{
"path": "examples/with_fast_api.py",
"chars": 960,
"preview": "import asyncio\nfrom collections.abc import AsyncGenerator\nfrom contextlib import asynccontextmanager\n\nfrom fastapi impor"
},
{
"path": "mkdocs.yml",
"chars": 2968,
"preview": "site_name: AioClock\nsite_description: An asyncio-based scheduling framework designed for execution of periodic task with"
},
{
"path": "pyproject.toml",
"chars": 2529,
"preview": "[project]\nname = \"aioclock\"\nversion = \"0.3.1\"\ndescription = \"An asyncio-based scheduling framework designed for executio"
},
{
"path": "tests/__init__.py",
"chars": 0,
"preview": ""
},
{
"path": "tests/test_di.py",
"chars": 402,
"preview": "import pytest\n\nfrom aioclock import AioClock, Depends, Once\nfrom aioclock.api import run_with_injected_deps\n\napp = AioCl"
},
{
"path": "tests/test_examples.py",
"chars": 3875,
"preview": "import importlib.util\nimport inspect\nimport logging\nimport os\nimport sys\nimport textwrap\n\nlogger = logging.getLogger(__n"
},
{
"path": "tests/test_lifespan.py",
"chars": 1514,
"preview": "from contextlib import asynccontextmanager, contextmanager\n\nimport pytest\n\nfrom aioclock import AioClock\nfrom aioclock.t"
},
{
"path": "tests/test_timeout.py",
"chars": 591,
"preview": "import asyncio\nfrom contextlib import contextmanager\nfrom datetime import datetime\n\nimport pytest\n\nfrom aioclock import "
},
{
"path": "tests/test_triggers.py",
"chars": 5379,
"preview": "from datetime import datetime, timedelta\n\nimport pytest\nimport zoneinfo\n\nfrom aioclock.triggers import At, Cron, Every, "
},
{
"path": "tox.ini",
"chars": 566,
"preview": "[tox]\nskipsdist = true\nenvlist = py39, py310, py311\n\n[gh-actions]\npython =\n 3.9: py39\n 3.10: py310\n 3.11: py311"
}
]
About this extraction
This page contains the full source code of the ManiMozaffar/aioclock GitHub repository, extracted and formatted as plain text for AI agents and large language models (LLMs). The extraction includes 50 files (220.9 KB), approximately 66.9k tokens, and a symbol index with 127 extracted functions, classes, methods, constants, and types. Use this with OpenClaw, Claude, ChatGPT, Cursor, Windsurf, or any other AI tool that accepts text input. You can copy the full output to your clipboard or download it as a .txt file.
Extracted by GitExtract — free GitHub repo to text converter for AI. Built by Nikandr Surkov.