diff --git a/LAST_VERSION b/LAST_VERSION index d33c3a2..6bd1e2a 100644 --- a/LAST_VERSION +++ b/LAST_VERSION @@ -1 +1 @@ -0.12.0 \ No newline at end of file +0.13.0.dev1 \ No newline at end of file diff --git a/docs/blog/index.md b/docs/blog/index.md new file mode 100644 index 0000000..c58f16c --- /dev/null +++ b/docs/blog/index.md @@ -0,0 +1,2 @@ +# Blog + diff --git a/docs/nav/development/release-notes.md b/docs/nav/development/release-notes.md index a259d89..e47b1ec 100644 --- a/docs/nav/development/release-notes.md +++ b/docs/nav/development/release-notes.md @@ -4,6 +4,7 @@ - [📦 PyPI - Build 0.13.0](https://github.com/dotflow-io/dotflow/releases/tag/v0.13.0) - [📌 Action with timeout, retry_delay and backoff](https://github.com/dotflow-io/dotflow/pull/56) +- [📌 Notification with Telegram](https://github.com/dotflow-io/dotflow/pull/59) ## v0.12.0 diff --git a/docs/nav/learn/dotflow-cli.md b/docs/nav/learn/dotflow-cli.md new file mode 100644 index 0000000..ae16856 --- /dev/null +++ b/docs/nav/learn/dotflow-cli.md @@ -0,0 +1,39 @@ +# Dotflow CLI + +May 4, 2025 + +--- + +The Dotflow CLI (Command-Line Interface) is a feature that simplifies the execution of workflows in environments that require running through terminal commands. + +### Simple Start + +```bash +dotflow start --step examples.cli_with_mode.simple_step +``` + +### With Initial Context + +```bash +dotflow start --step examples.cli_with_initial_context.simple_step --initial-context abc +``` + +### With Callback + +```bash +dotflow start --step examples.cli_with_callback.simple_step --callback examples.cli_with_callback.callback +``` + +### With Mode + +```bash +dotflow start --step examples.cli_with_mode.simple_step --mode sequential +``` + +```bash +dotflow start --step examples.cli_with_mode.simple_step --mode background +``` + +```bash +dotflow start --step examples.cli_with_mode.simple_step --mode parallel +``` \ No newline at end of file diff --git a/docs/nav/getting-started.md b/docs/nav/learn/getting-started.md similarity index 100% rename from docs/nav/getting-started.md rename to docs/nav/learn/getting-started.md diff --git a/docs/nav/learn/notify-with-telegram.md b/docs/nav/learn/notify-with-telegram.md new file mode 100644 index 0000000..2d4c4cf --- /dev/null +++ b/docs/nav/learn/notify-with-telegram.md @@ -0,0 +1,104 @@ +# Notify with Telegram + +May 4, 2025 + +--- + +## Bot Telegram + +### Create a bot on Telegram: + +1 - Open the **Telegram** app on your mobile device. + +2 - Tap the **search** icon and look for **BotFather**. + +3 - Open the contact and type `/newbot`. + +4 - Enter a name for your bot. + +### Generate Bot Token: + +1 - Type the command `/token`. + +2 - Select the bot you just created from the list. + +3 - **BotFather** will return an access token — copy and save this token securely. It will be required to authenticate your bot. + +### Retrieving Your Telegram Chat ID + +1 - Send a message to your bot in Telegram to ensure it appears in the bot's update log. + +2 - Run the following `curl` command to fetch the latest updates from your bot: + +```bash +curl --location --globoff 'https://api.telegram.org/bot/getUpdates' +``` +> Replace `` with the token provided by BotFather. + +3 - Inspect the API response, and locate the following key `result[0].channel_post.chat.id`. This is your chat ID. + +4 - Copy and store the chat ID in a secure location. You will need it to configure the `NotifyTelegram` instance. + + +## DotFlow Config + +### Import + +Start with the basics, which is importing the necessary classes and methods. + +```python +import os + +from dotflow import Config, DotFlow, action +from dotflow.notify import NotifyTelegram +from dotflow.types import TypeStatus +``` + +### Task function + +Use the `@action` decorator to define a simple task that will be executed in the workflow: + +```python +@action +def simple_task(): + return "ok" +``` + + +### Notify Class + +Instantiate the `NotifyTelegram` class with your Telegram bot credentials. You can use environment variables for security: + +```python +notify = NotifyTelegram( + token=os.getenv("TOKEN"), + chat_id=os.getenv("CHAT_ID"), + notification_type=TypeStatus.FAILED # Notify only on failure +) +``` + +### Dotflow Class + +Pass the `notify` instance into the `Config` class and initialize the `DotFlow` workflow: + +```python +workflow = DotFlow( + config=Config(notify=notify) +) +``` + +### Add Task + +Add your defined task as a step in the workflow: + +```python +workflow.task.add(step=simple_task) +``` + +### Start + +Start the workflow execution: + +```python +workflow.start() +``` diff --git a/docs/nav/reference/abc-log.md b/docs/nav/reference/abc-log.md new file mode 100644 index 0000000..6a669c6 --- /dev/null +++ b/docs/nav/reference/abc-log.md @@ -0,0 +1,3 @@ +# Log + +::: dotflow.abc.log.Log diff --git a/docs/nav/reference/abc-notify.md b/docs/nav/reference/abc-notify.md new file mode 100644 index 0000000..651f5d8 --- /dev/null +++ b/docs/nav/reference/abc-notify.md @@ -0,0 +1,3 @@ +# Notify + +::: dotflow.abc.notify.Notify diff --git a/docs/nav/reference/type-status.md b/docs/nav/reference/type-status.md index 743597a..a37f95b 100644 --- a/docs/nav/reference/type-status.md +++ b/docs/nav/reference/type-status.md @@ -1,6 +1,6 @@ -# TaskStatus +# TypeStatus -::: dotflow.core.types.status.TaskStatus +::: dotflow.core.types.status.TypeStatus options: members: - NOT_STARTED diff --git a/dotflow/__init__.py b/dotflow/__init__.py index 4ab947c..2698bad 100644 --- a/dotflow/__init__.py +++ b/dotflow/__init__.py @@ -1,6 +1,6 @@ """Dotflow __init__ module.""" -__version__ = "0.12.0" +__version__ = "0.13.0.dev1" __description__ = "🎲 Dotflow turns an idea into flow!" from .core.action import Action as action diff --git a/dotflow/abc/log.py b/dotflow/abc/log.py new file mode 100644 index 0000000..5f22904 --- /dev/null +++ b/dotflow/abc/log.py @@ -0,0 +1,17 @@ +"""Notify ABC""" + +from typing import Any + +from abc import ABC, abstractmethod + + +class Log(ABC): + """Log""" + + @abstractmethod + def info(self, task: Any) -> None: + """Info""" + + @abstractmethod + def error(self, task: Any) -> None: + """Error""" diff --git a/dotflow/abc/notify.py b/dotflow/abc/notify.py new file mode 100644 index 0000000..2a81f17 --- /dev/null +++ b/dotflow/abc/notify.py @@ -0,0 +1,13 @@ +"""Notify ABC""" + +from typing import Any + +from abc import ABC, abstractmethod + + +class Notify(ABC): + """Notify""" + + @abstractmethod + def send(self, task: Any) -> None: + """Send""" diff --git a/dotflow/core/config.py b/dotflow/core/config.py index 4d0a12f..4112d6a 100644 --- a/dotflow/core/config.py +++ b/dotflow/core/config.py @@ -1,7 +1,14 @@ """Config module""" +from typing import Optional + +from dotflow.abc.log import Log from dotflow.abc.storage import Storage +from dotflow.abc.notify import Notify + +from dotflow.providers.log_default import LogDefault from dotflow.providers.storage_default import StorageDefault +from dotflow.providers.notify_default import NotifyDefault class Config: @@ -10,19 +17,38 @@ class Config: You can import the **Config** class with: from dotflow import Config - from dotflow.storage import StorageDefault + from dotflow.providers import ( + StorageDefault, + NotifyDefault, + LogDefault + ) Example: `class` dotflow.core.config.Config - config = Config(storage=StorageDefault) + config = Config( + storage=StorageFile(path=".output"), + notify=NotifyDefault(), + log=LogDefault() + ) Args: - storage (Storage): Type of the storage. + storage (Optional[Storage]): Type of the storage. + notify (Optional[Notify]): Type of the notify. + log (Optional[Log]): Type of the notify. Attributes: - storage (Storage): + storage (Optional[Storage]): + notify (Optional[Notify]): + log (Optional[Log]): """ - def __init__(self, storage: Storage = StorageDefault()) -> None: - self.storage = storage + def __init__( + self, + storage: Optional[Storage] = None, + notify: Optional[Notify] = None, + log: Optional[Log] = None, + ) -> None: + self.storage = storage if storage else StorageDefault() + self.notify = notify if notify else NotifyDefault() + self.log = log if log else LogDefault() diff --git a/dotflow/core/dotflow.py b/dotflow/core/dotflow.py index 801bcee..6a1aab6 100644 --- a/dotflow/core/dotflow.py +++ b/dotflow/core/dotflow.py @@ -2,6 +2,7 @@ from uuid import uuid4 from functools import partial +from typing import Optional from dotflow.core.config import Config from dotflow.core.workflow import Manager @@ -26,7 +27,7 @@ class DotFlow: workflow = DotFlow(config=config) Args: - config (Config): Configuration class. + config (Optional[Config]): Configuration class. Attributes: workflow_id (UUID): @@ -38,9 +39,10 @@ class DotFlow: def __init__( self, - config: Config = Config() + config: Optional[Config] = None ) -> None: self.workflow_id = uuid4() + config = config if config else Config() self.task = TaskBuilder( config=config, diff --git a/dotflow/core/execution.py b/dotflow/core/execution.py index bbac826..8d33149 100644 --- a/dotflow/core/execution.py +++ b/dotflow/core/execution.py @@ -1,6 +1,7 @@ """Execution module""" from uuid import UUID +from datetime import datetime from typing import Callable, List, Tuple from inspect import getsourcelines from types import FunctionType @@ -15,9 +16,8 @@ from dotflow.core.action import Action from dotflow.core.context import Context from dotflow.core.task import Task -from dotflow.core.types import TaskStatus +from dotflow.core.types import TypeStatus -from dotflow.core.decorators import time from dotflow.utils import basic_callback @@ -50,7 +50,7 @@ def __init__( _flow_callback: Callable = basic_callback, ) -> None: self.task = task - self.task.status = TaskStatus.IN_PROGRESS + self.task.status = TypeStatus.IN_PROGRESS self.task.previous_context = previous_context self.task.workflow_id = workflow_id @@ -106,14 +106,15 @@ def _execution_with_class(self, class_instance: Callable): callable_list=callable_list, class_instance=class_instance ) - for _, new in ordered_list: - new_object = getattr(class_instance, new) + for index, new in enumerate(ordered_list): + new_object = getattr(class_instance, new[1]) try: subcontext = new_object( initial_context=self.task.initial_context, previous_context=previous_context, task=self.task, ) + subcontext.task_id = index new_context.storage.append(subcontext) previous_context = subcontext @@ -127,6 +128,7 @@ def _execution_with_class(self, class_instance: Callable): previous_context=previous_context, task=self.task, ) + subcontext.task_id = index new_context.storage.append(subcontext) previous_context = subcontext @@ -135,9 +137,9 @@ def _execution_with_class(self, class_instance: Callable): return new_context - @time def _excution(self, _flow_callback): try: + start = datetime.now() current_context = self.task.step( initial_context=self.task.initial_context, previous_context=self.task.previous_context, @@ -149,16 +151,17 @@ def _excution(self, _flow_callback): class_instance=current_context.storage ) - self.task.status = TaskStatus.COMPLETED self.task.current_context = current_context + self.task.duration = (datetime.now() - start).total_seconds() + self.task.status = TypeStatus.COMPLETED except AssertionError as err: raise err except Exception as err: - self.task.status = TaskStatus.FAILED - self.task.current_context = None self.task.error = err + self.task.current_context = None + self.task.status = TypeStatus.FAILED finally: self.task.callback(task=self.task) diff --git a/dotflow/core/serializers/task.py b/dotflow/core/serializers/task.py index fc232b8..dd38aa8 100644 --- a/dotflow/core/serializers/task.py +++ b/dotflow/core/serializers/task.py @@ -5,7 +5,9 @@ from typing import Any, Optional from uuid import UUID -from pydantic import BaseModel, Field, ConfigDict, field_validator # type: ignore +from pydantic import BaseModel, Field, ConfigDict, field_validator + +from dotflow.core.context import Context class SerializerTaskError(BaseModel): @@ -26,6 +28,22 @@ class SerializerTask(BaseModel): current_context: Any = Field(default=None, alias="_current_context") previous_context: Any = Field(default=None, alias="_previous_context") group_name: str = Field(default=None) + max: Optional[int] = Field(default=None, exclude=True) + size_message: Optional[str] = Field(default="Context size exceeded", exclude=True) + + def model_dump_json(self, **kwargs) -> str: + dump_json = super().model_dump_json(serialize_as_any=True, **kwargs) + + if self.max and len(dump_json) > self.max: + self.initial_context = self.size_message + self.current_context = self.size_message + self.previous_context = self.size_message + + dump_json = super().model_dump_json(serialize_as_any=True, **kwargs) + + return dump_json[0:self.max] + + return dump_json @field_validator("error", mode="before") @classmethod @@ -40,8 +58,26 @@ def error_validator(cls, value: str) -> str: @classmethod def context_validator(cls, value: str) -> str: if value and value.storage: - try: - return json.dumps(value.storage) - except Exception: - return str(value) + context = cls.context_loop(value=value) + return context return None + + @classmethod + def format_context(cls, value): + try: + return json.dumps(value.storage) + except TypeError: + return str(value.storage) + + @classmethod + def context_loop(cls, value): + if isinstance(value.storage, list): + contexts = {} + if any(isinstance(context, Context) for context in value.storage): + for context in value.storage: + if isinstance(context, Context): + contexts[context.task_id] = cls.context_loop(context) + else: + contexts[context.task_id] = cls.format_context(context) + return contexts + return cls.format_context(value=value) diff --git a/dotflow/core/task.py b/dotflow/core/task.py index 6529ab0..e96d20e 100644 --- a/dotflow/core/task.py +++ b/dotflow/core/task.py @@ -5,9 +5,6 @@ from uuid import UUID from typing import Any, Callable, List -from rich.console import Console # type: ignore - -from dotflow.logging import logger from dotflow.core.config import Config from dotflow.core.action import Action from dotflow.core.context import Context @@ -15,7 +12,7 @@ from dotflow.core.serializers.task import SerializerTask from dotflow.core.serializers.workflow import SerializerWorkflow from dotflow.core.exception import MissingActionDecorator, NotCallableObject -from dotflow.core.types.status import TaskStatus +from dotflow.core.types.status import TypeStatus from dotflow.utils import ( basic_callback, traceback_error, @@ -103,14 +100,14 @@ def __init__( config, group_name ) + self.config = config + self.group_name = group_name self.task_id = task_id self.workflow_id = workflow_id self.step = step self.callback = callback self.initial_context = initial_context - self.status = TaskStatus.NOT_STARTED - self.config = config - self.group_name = group_name + self.status = TypeStatus.NOT_STARTED @property def step(self): @@ -206,33 +203,20 @@ def error(self, value: Exception) -> None: task_error = TaskError(value) self._error = task_error - logger.error( - "ID %s - %s - %s \n %s", - self.workflow_id, - self.task_id, - self.status, - task_error.traceback, - ) - - console = Console() - console.print_exception(show_locals=True) + self.config.log.error(task=self) @property def status(self): if not self._status: - return TaskStatus.NOT_STARTED + return TypeStatus.NOT_STARTED return self._status @status.setter - def status(self, value: TaskStatus) -> None: + def status(self, value: TypeStatus) -> None: self._status = value - logger.info( - "ID %s - %s - %s", - self.workflow_id, - self.task_id, - self.status, - ) + self.config.notify.send(task=self) + self.config.log.info(task=self) @property def config(self): @@ -244,13 +228,11 @@ def config(self): def config(self, value: Config): self._config = value - def schema(self) -> SerializerTask: - return SerializerTask( - **self.__dict__ - ) + def schema(self, max: int = None) -> SerializerTask: + return SerializerTask(**self.__dict__, max=max) - def result(self) -> SerializerWorkflow: - item = self.schema().model_dump_json() + def result(self, max: int = None) -> SerializerWorkflow: + item = self.schema(max=max).model_dump_json() return json.loads(item) diff --git a/dotflow/core/types/__init__.py b/dotflow/core/types/__init__.py index fd9fe08..d11f57f 100644 --- a/dotflow/core/types/__init__.py +++ b/dotflow/core/types/__init__.py @@ -1,12 +1,12 @@ """Types __init__ module.""" from dotflow.core.types.execution import TypeExecution -from dotflow.core.types.status import TaskStatus +from dotflow.core.types.status import TypeStatus from dotflow.core.types.storage import TypeStorage __all__ = [ "TypeExecution", - "TaskStatus", + "TypeStatus", "TypeStorage" ] diff --git a/dotflow/core/types/status.py b/dotflow/core/types/status.py index ba30527..83e7758 100644 --- a/dotflow/core/types/status.py +++ b/dotflow/core/types/status.py @@ -1,14 +1,14 @@ -"""Type TaskStatus mode module""" +"""Type TypeStatus mode module""" from typing_extensions import Annotated, Doc -class TaskStatus: +class TypeStatus: """ Import: - You can import the **TaskStatus** class with: + You can import the **TypeStatus** class with: - from dotflow.core.types import TaskStatus + from dotflow.core.types import TypeStatus """ NOT_STARTED: Annotated[str, Doc("Status not started.")] = "Not started" @@ -17,3 +17,15 @@ class TaskStatus: PAUSED: Annotated[str, Doc("Status paused.")] = "Paused" RETRY: Annotated[str, Doc("Status retry.")] = "Retry" FAILED: Annotated[str, Doc("Status failed.")] = "Failed" + + @classmethod + def get_symbol(cls, value: str) -> str: + status = { + TypeStatus.NOT_STARTED: "⚪", + TypeStatus.IN_PROGRESS: "🔵", + TypeStatus.COMPLETED: "✅", + TypeStatus.PAUSED: "◼️", + TypeStatus.RETRY: "❗", + TypeStatus.FAILED: "❌" + } + return status.get(value) diff --git a/dotflow/core/workflow.py b/dotflow/core/workflow.py index 4c08274..d06b38e 100644 --- a/dotflow/core/workflow.py +++ b/dotflow/core/workflow.py @@ -14,7 +14,7 @@ from dotflow.core.context import Context from dotflow.core.execution import Execution from dotflow.core.exception import ExecutionModeNotExist -from dotflow.core.types import TypeExecution, TaskStatus +from dotflow.core.types import TypeExecution, TypeStatus from dotflow.core.task import Task from dotflow.utils import basic_callback @@ -117,7 +117,7 @@ def __init__( def _callback_workflow(self, tasks: List[Task]): final_status = [task.status for task in tasks] - if TaskStatus.FAILED in final_status: + if TypeStatus.FAILED in final_status: self.failure(tasks=tasks) else: self.success(tasks=tasks) @@ -179,7 +179,7 @@ def run(self) -> None: key=task.config.storage.key(task=task) ) - if not self.ignore and task.status == TaskStatus.FAILED: + if not self.ignore and task.status == TypeStatus.FAILED: break @@ -256,7 +256,7 @@ def _run_group(self, groups: List[Task]) -> None: key=task.config.storage.key(task=task) ) - if not self.ignore and task.status == TaskStatus.FAILED: + if not self.ignore and task.status == TypeStatus.FAILED: break diff --git a/dotflow/notify.py b/dotflow/notify.py new file mode 100644 index 0000000..fdfef1c --- /dev/null +++ b/dotflow/notify.py @@ -0,0 +1,6 @@ +"""Notify module""" + +from .providers.notify_telegram import NotifyTelegram +from .providers.notify_default import NotifyDefault + +__all__ = ["NotifyTelegram", "NotifyDefault"] diff --git a/dotflow/providers.py b/dotflow/providers.py new file mode 100644 index 0000000..351e8b4 --- /dev/null +++ b/dotflow/providers.py @@ -0,0 +1,15 @@ +"""Providers module""" + +from dotflow.providers.log_default import LogDefault +from dotflow.providers.notify_default import NotifyDefault +from dotflow.providers.notify_telegram import NotifyTelegram +from dotflow.providers.storage_default import StorageDefault +from dotflow.providers.storage_file import StorageFile + +__all__ = [ + "LogDefault", + "NotifyDefault", + "NotifyTelegram", + "StorageDefault", + "StorageFile" +] diff --git a/dotflow/providers/log_default.py b/dotflow/providers/log_default.py new file mode 100644 index 0000000..053406b --- /dev/null +++ b/dotflow/providers/log_default.py @@ -0,0 +1,30 @@ +"""Notify Default""" + +from typing import Any + +from rich.console import Console # type: ignore + +from dotflow.abc.log import Log +from dotflow.logging import logger + + +class LogDefault(Log): + + def info(self, task: Any) -> None: + logger.info( + "ID %s - %s - %s", + task.workflow_id, + task.task_id, + task.status, + ) + + def error(self, task: Any) -> None: + logger.error( + "ID %s - %s - %s \n %s", + task.workflow_id, + task.task_id, + task.status, + task.error.traceback, + ) + console = Console() + console.print_exception(show_locals=True) diff --git a/dotflow/providers/notify_default.py b/dotflow/providers/notify_default.py new file mode 100644 index 0000000..0584615 --- /dev/null +++ b/dotflow/providers/notify_default.py @@ -0,0 +1,11 @@ +"""Notify Default""" + +from typing import Any + +from dotflow.abc.notify import Notify + + +class NotifyDefault(Notify): + + def send(self, task: Any) -> None: + pass diff --git a/dotflow/providers/notify_telegram.py b/dotflow/providers/notify_telegram.py new file mode 100644 index 0000000..ecc395f --- /dev/null +++ b/dotflow/providers/notify_telegram.py @@ -0,0 +1,58 @@ +"""Notify Default""" + +from json import dumps +from typing import Any, Optional + +from requests import post + +from dotflow.core.types.status import TypeStatus +from dotflow.abc.notify import Notify +from dotflow.logging import logger + +MESSAGE = "{symbol} {status}\n```json\n{task}```\n{workflow_id}-{task_id}" +API_TELEGRAM = "https://api.telegram.org/bot{token}/sendMessage" + + +class NotifyTelegram(Notify): + + def __init__( + self, + token: str, + chat_id: int, + notification_type: Optional[TypeStatus] = None, + timeout: int = 1.5 + ): + self.token = token + self.chat_id = chat_id + self.notification_type = notification_type + self.timeout = timeout + + def send(self, task: Any) -> None: + if not self.notification_type or self.notification_type == task.status: + data = { + "chat_id": self.chat_id, + "text": self._get_text(task=task), + "parse_mode": "markdown", + } + try: + response = post( + url=API_TELEGRAM.format(token=self.token), + headers={"Content-Type": "application/json"}, + data=dumps(data), + timeout=self.timeout + ) + response.raise_for_status() + except Exception as error: + logger.error( + "Internal problem sending notification on Telegram: %s", + str(error), + ) + + def _get_text(self, task: Any) -> str: + return MESSAGE.format( + symbol=TypeStatus.get_symbol(task.status), + status=task.status, + workflow_id=task.workflow_id, + task_id=task.task_id, + task=task.result(max=4000), + ) diff --git a/dotflow/types.py b/dotflow/types.py new file mode 100644 index 0000000..b5f320d --- /dev/null +++ b/dotflow/types.py @@ -0,0 +1,9 @@ +"""Types module""" + +from dotflow.core.types.status import TypeStatus +from dotflow.core.types.storage import TypeStorage + +__all__ = [ + "TypeStatus", + "TypeStorage" +] diff --git a/examples b/examples index 9fcf3f8..fedfba6 160000 --- a/examples +++ b/examples @@ -1 +1 @@ -Subproject commit 9fcf3f800dc0fb47aa713a456703719d8adcadda +Subproject commit fedfba6d7e1aafa7bf8c2bb6ff3a3edf3db1257c diff --git a/mkdocs.yml b/mkdocs.yml index 79ca180..65f4902 100644 --- a/mkdocs.yml +++ b/mkdocs.yml @@ -7,7 +7,9 @@ copyright: Fernando Celmer nav: - Home: index.md - Learn: - - nav/getting-started.md + - nav/learn/getting-started.md + - nav/learn/notify-with-telegram.md + - nav/learn/dotflow-cli.md - Reference: - nav/reference/action.md - nav/reference/config.md @@ -22,9 +24,11 @@ nav: - nav/reference/exception.md - ABC: - nav/reference/abc-flow.md + - nav/reference/abc-notify.md - nav/reference/abc-storage.md - nav/reference/abc-http.md - nav/reference/abc-tcp.md + - nav/reference/abc-log.md - Storages: - nav/reference/storage-init.md - nav/reference/storage-file.md @@ -40,6 +44,23 @@ nav: theme: name: simple-blog + # palette: + # - media: (prefers-color-scheme) + # toggle: + # icon: material/brightness-7 + # name: Switch to light mode + # - media: '(prefers-color-scheme: light)' + # scheme: default + # primary: black + # toggle: + # icon: material/brightness-4 + # name: Switch to dark mode + # - media: '(prefers-color-scheme: dark)' + # scheme: slate + # primary: black + # toggle: + # icon: material/lightbulb-outline + # name: Switch to system preference theme_style: light favicon: assets/logo.ico logo: assets/logo.png @@ -68,6 +89,7 @@ extra_css: - assets/style.css plugins: +- search - mkdocstrings: handlers: python: diff --git a/poetry.lock b/poetry.lock index 325f603..149f0e1 100644 --- a/poetry.lock +++ b/poetry.lock @@ -61,6 +61,17 @@ files = [ {file = "cachetools-5.5.2.tar.gz", hash = "sha256:1a661caa9175d26759571b2e19580f9d6393969e5dfca11fdb1f947a23e640d4"}, ] +[[package]] +name = "certifi" +version = "2025.4.26" +description = "Python package for providing Mozilla's CA Bundle." +optional = false +python-versions = ">=3.6" +files = [ + {file = "certifi-2025.4.26-py3-none-any.whl", hash = "sha256:30350364dfe371162649852c63336a15c70c6510c2ad5015b21c2345311805f3"}, + {file = "certifi-2025.4.26.tar.gz", hash = "sha256:0a816057ea3cdefcef70270d2c515e4506bbc954f417fa5ade2021213bb8f0c6"}, +] + [[package]] name = "cffi" version = "1.17.1" @@ -151,6 +162,107 @@ files = [ {file = "chardet-5.2.0.tar.gz", hash = "sha256:1b3b6ff479a8c414bc3fa2c0852995695c4a026dcd6d0633b2dd092ca39c1cf7"}, ] +[[package]] +name = "charset-normalizer" +version = "3.4.2" +description = "The Real First Universal Charset Detector. Open, modern and actively maintained alternative to Chardet." +optional = false +python-versions = ">=3.7" +files = [ + {file = "charset_normalizer-3.4.2-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:7c48ed483eb946e6c04ccbe02c6b4d1d48e51944b6db70f697e089c193404941"}, + {file = "charset_normalizer-3.4.2-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:b2d318c11350e10662026ad0eb71bb51c7812fc8590825304ae0bdd4ac283acd"}, + {file = "charset_normalizer-3.4.2-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:9cbfacf36cb0ec2897ce0ebc5d08ca44213af24265bd56eca54bee7923c48fd6"}, + {file = "charset_normalizer-3.4.2-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:18dd2e350387c87dabe711b86f83c9c78af772c748904d372ade190b5c7c9d4d"}, + {file = "charset_normalizer-3.4.2-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8075c35cd58273fee266c58c0c9b670947c19df5fb98e7b66710e04ad4e9ff86"}, + {file = "charset_normalizer-3.4.2-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:5bf4545e3b962767e5c06fe1738f951f77d27967cb2caa64c28be7c4563e162c"}, + {file = "charset_normalizer-3.4.2-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:7a6ab32f7210554a96cd9e33abe3ddd86732beeafc7a28e9955cdf22ffadbab0"}, + {file = "charset_normalizer-3.4.2-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:b33de11b92e9f75a2b545d6e9b6f37e398d86c3e9e9653c4864eb7e89c5773ef"}, + {file = "charset_normalizer-3.4.2-cp310-cp310-musllinux_1_2_ppc64le.whl", hash = "sha256:8755483f3c00d6c9a77f490c17e6ab0c8729e39e6390328e42521ef175380ae6"}, + {file = "charset_normalizer-3.4.2-cp310-cp310-musllinux_1_2_s390x.whl", hash = "sha256:68a328e5f55ec37c57f19ebb1fdc56a248db2e3e9ad769919a58672958e8f366"}, + {file = "charset_normalizer-3.4.2-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:21b2899062867b0e1fde9b724f8aecb1af14f2778d69aacd1a5a1853a597a5db"}, + {file = "charset_normalizer-3.4.2-cp310-cp310-win32.whl", hash = "sha256:e8082b26888e2f8b36a042a58307d5b917ef2b1cacab921ad3323ef91901c71a"}, + {file = "charset_normalizer-3.4.2-cp310-cp310-win_amd64.whl", hash = "sha256:f69a27e45c43520f5487f27627059b64aaf160415589230992cec34c5e18a509"}, + {file = "charset_normalizer-3.4.2-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:be1e352acbe3c78727a16a455126d9ff83ea2dfdcbc83148d2982305a04714c2"}, + {file = "charset_normalizer-3.4.2-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:aa88ca0b1932e93f2d961bf3addbb2db902198dca337d88c89e1559e066e7645"}, + {file = "charset_normalizer-3.4.2-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:d524ba3f1581b35c03cb42beebab4a13e6cdad7b36246bd22541fa585a56cccd"}, + {file = "charset_normalizer-3.4.2-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:28a1005facc94196e1fb3e82a3d442a9d9110b8434fc1ded7a24a2983c9888d8"}, + {file = "charset_normalizer-3.4.2-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:fdb20a30fe1175ecabed17cbf7812f7b804b8a315a25f24678bcdf120a90077f"}, + {file = "charset_normalizer-3.4.2-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:0f5d9ed7f254402c9e7d35d2f5972c9bbea9040e99cd2861bd77dc68263277c7"}, + {file = "charset_normalizer-3.4.2-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:efd387a49825780ff861998cd959767800d54f8308936b21025326de4b5a42b9"}, + {file = "charset_normalizer-3.4.2-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:f0aa37f3c979cf2546b73e8222bbfa3dc07a641585340179d768068e3455e544"}, + {file = "charset_normalizer-3.4.2-cp311-cp311-musllinux_1_2_ppc64le.whl", hash = "sha256:e70e990b2137b29dc5564715de1e12701815dacc1d056308e2b17e9095372a82"}, + {file = "charset_normalizer-3.4.2-cp311-cp311-musllinux_1_2_s390x.whl", hash = "sha256:0c8c57f84ccfc871a48a47321cfa49ae1df56cd1d965a09abe84066f6853b9c0"}, + {file = "charset_normalizer-3.4.2-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:6b66f92b17849b85cad91259efc341dce9c1af48e2173bf38a85c6329f1033e5"}, + {file = "charset_normalizer-3.4.2-cp311-cp311-win32.whl", hash = "sha256:daac4765328a919a805fa5e2720f3e94767abd632ae410a9062dff5412bae65a"}, + {file = "charset_normalizer-3.4.2-cp311-cp311-win_amd64.whl", hash = "sha256:e53efc7c7cee4c1e70661e2e112ca46a575f90ed9ae3fef200f2a25e954f4b28"}, + {file = "charset_normalizer-3.4.2-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:0c29de6a1a95f24b9a1aa7aefd27d2487263f00dfd55a77719b530788f75cff7"}, + {file = "charset_normalizer-3.4.2-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:cddf7bd982eaa998934a91f69d182aec997c6c468898efe6679af88283b498d3"}, + {file = "charset_normalizer-3.4.2-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:fcbe676a55d7445b22c10967bceaaf0ee69407fbe0ece4d032b6eb8d4565982a"}, + {file = "charset_normalizer-3.4.2-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:d41c4d287cfc69060fa91cae9683eacffad989f1a10811995fa309df656ec214"}, + {file = "charset_normalizer-3.4.2-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:4e594135de17ab3866138f496755f302b72157d115086d100c3f19370839dd3a"}, + {file = "charset_normalizer-3.4.2-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:cf713fe9a71ef6fd5adf7a79670135081cd4431c2943864757f0fa3a65b1fafd"}, + {file = "charset_normalizer-3.4.2-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:a370b3e078e418187da8c3674eddb9d983ec09445c99a3a263c2011993522981"}, + {file = "charset_normalizer-3.4.2-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:a955b438e62efdf7e0b7b52a64dc5c3396e2634baa62471768a64bc2adb73d5c"}, + {file = "charset_normalizer-3.4.2-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:7222ffd5e4de8e57e03ce2cef95a4c43c98fcb72ad86909abdfc2c17d227fc1b"}, + {file = "charset_normalizer-3.4.2-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:bee093bf902e1d8fc0ac143c88902c3dfc8941f7ea1d6a8dd2bcb786d33db03d"}, + {file = "charset_normalizer-3.4.2-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:dedb8adb91d11846ee08bec4c8236c8549ac721c245678282dcb06b221aab59f"}, + {file = "charset_normalizer-3.4.2-cp312-cp312-win32.whl", hash = "sha256:db4c7bf0e07fc3b7d89ac2a5880a6a8062056801b83ff56d8464b70f65482b6c"}, + {file = "charset_normalizer-3.4.2-cp312-cp312-win_amd64.whl", hash = "sha256:5a9979887252a82fefd3d3ed2a8e3b937a7a809f65dcb1e068b090e165bbe99e"}, + {file = "charset_normalizer-3.4.2-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:926ca93accd5d36ccdabd803392ddc3e03e6d4cd1cf17deff3b989ab8e9dbcf0"}, + {file = "charset_normalizer-3.4.2-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:eba9904b0f38a143592d9fc0e19e2df0fa2e41c3c3745554761c5f6447eedabf"}, + {file = "charset_normalizer-3.4.2-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:3fddb7e2c84ac87ac3a947cb4e66d143ca5863ef48e4a5ecb83bd48619e4634e"}, + {file = "charset_normalizer-3.4.2-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:98f862da73774290f251b9df8d11161b6cf25b599a66baf087c1ffe340e9bfd1"}, + {file = "charset_normalizer-3.4.2-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6c9379d65defcab82d07b2a9dfbfc2e95bc8fe0ebb1b176a3190230a3ef0e07c"}, + {file = "charset_normalizer-3.4.2-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:e635b87f01ebc977342e2697d05b56632f5f879a4f15955dfe8cef2448b51691"}, + {file = "charset_normalizer-3.4.2-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:1c95a1e2902a8b722868587c0e1184ad5c55631de5afc0eb96bc4b0d738092c0"}, + {file = "charset_normalizer-3.4.2-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:ef8de666d6179b009dce7bcb2ad4c4a779f113f12caf8dc77f0162c29d20490b"}, + {file = "charset_normalizer-3.4.2-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:32fc0341d72e0f73f80acb0a2c94216bd704f4f0bce10aedea38f30502b271ff"}, + {file = "charset_normalizer-3.4.2-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:289200a18fa698949d2b39c671c2cc7a24d44096784e76614899a7ccf2574b7b"}, + {file = "charset_normalizer-3.4.2-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:4a476b06fbcf359ad25d34a057b7219281286ae2477cc5ff5e3f70a246971148"}, + {file = "charset_normalizer-3.4.2-cp313-cp313-win32.whl", hash = "sha256:aaeeb6a479c7667fbe1099af9617c83aaca22182d6cf8c53966491a0f1b7ffb7"}, + {file = "charset_normalizer-3.4.2-cp313-cp313-win_amd64.whl", hash = "sha256:aa6af9e7d59f9c12b33ae4e9450619cf2488e2bbe9b44030905877f0b2324980"}, + {file = "charset_normalizer-3.4.2-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:1cad5f45b3146325bb38d6855642f6fd609c3f7cad4dbaf75549bf3b904d3184"}, + {file = "charset_normalizer-3.4.2-cp37-cp37m-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:b2680962a4848b3c4f155dc2ee64505a9c57186d0d56b43123b17ca3de18f0fa"}, + {file = "charset_normalizer-3.4.2-cp37-cp37m-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:36b31da18b8890a76ec181c3cf44326bf2c48e36d393ca1b72b3f484113ea344"}, + {file = "charset_normalizer-3.4.2-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f4074c5a429281bf056ddd4c5d3b740ebca4d43ffffe2ef4bf4d2d05114299da"}, + {file = "charset_normalizer-3.4.2-cp37-cp37m-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:c9e36a97bee9b86ef9a1cf7bb96747eb7a15c2f22bdb5b516434b00f2a599f02"}, + {file = "charset_normalizer-3.4.2-cp37-cp37m-musllinux_1_2_aarch64.whl", hash = "sha256:1b1bde144d98e446b056ef98e59c256e9294f6b74d7af6846bf5ffdafd687a7d"}, + {file = "charset_normalizer-3.4.2-cp37-cp37m-musllinux_1_2_i686.whl", hash = "sha256:915f3849a011c1f593ab99092f3cecfcb4d65d8feb4a64cf1bf2d22074dc0ec4"}, + {file = "charset_normalizer-3.4.2-cp37-cp37m-musllinux_1_2_ppc64le.whl", hash = "sha256:fb707f3e15060adf5b7ada797624a6c6e0138e2a26baa089df64c68ee98e040f"}, + {file = "charset_normalizer-3.4.2-cp37-cp37m-musllinux_1_2_s390x.whl", hash = "sha256:25a23ea5c7edc53e0f29bae2c44fcb5a1aa10591aae107f2a2b2583a9c5cbc64"}, + {file = "charset_normalizer-3.4.2-cp37-cp37m-musllinux_1_2_x86_64.whl", hash = "sha256:770cab594ecf99ae64c236bc9ee3439c3f46be49796e265ce0cc8bc17b10294f"}, + {file = "charset_normalizer-3.4.2-cp37-cp37m-win32.whl", hash = "sha256:6a0289e4589e8bdfef02a80478f1dfcb14f0ab696b5a00e1f4b8a14a307a3c58"}, + {file = "charset_normalizer-3.4.2-cp37-cp37m-win_amd64.whl", hash = "sha256:6fc1f5b51fa4cecaa18f2bd7a003f3dd039dd615cd69a2afd6d3b19aed6775f2"}, + {file = "charset_normalizer-3.4.2-cp38-cp38-macosx_10_9_universal2.whl", hash = "sha256:76af085e67e56c8816c3ccf256ebd136def2ed9654525348cfa744b6802b69eb"}, + {file = "charset_normalizer-3.4.2-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e45ba65510e2647721e35323d6ef54c7974959f6081b58d4ef5d87c60c84919a"}, + {file = "charset_normalizer-3.4.2-cp38-cp38-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:046595208aae0120559a67693ecc65dd75d46f7bf687f159127046628178dc45"}, + {file = "charset_normalizer-3.4.2-cp38-cp38-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:75d10d37a47afee94919c4fab4c22b9bc2a8bf7d4f46f87363bcf0573f3ff4f5"}, + {file = "charset_normalizer-3.4.2-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6333b3aa5a12c26b2a4d4e7335a28f1475e0e5e17d69d55141ee3cab736f66d1"}, + {file = "charset_normalizer-3.4.2-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:e8323a9b031aa0393768b87f04b4164a40037fb2a3c11ac06a03ffecd3618027"}, + {file = "charset_normalizer-3.4.2-cp38-cp38-musllinux_1_2_aarch64.whl", hash = "sha256:24498ba8ed6c2e0b56d4acbf83f2d989720a93b41d712ebd4f4979660db4417b"}, + {file = "charset_normalizer-3.4.2-cp38-cp38-musllinux_1_2_i686.whl", hash = "sha256:844da2b5728b5ce0e32d863af26f32b5ce61bc4273a9c720a9f3aa9df73b1455"}, + {file = "charset_normalizer-3.4.2-cp38-cp38-musllinux_1_2_ppc64le.whl", hash = "sha256:65c981bdbd3f57670af8b59777cbfae75364b483fa8a9f420f08094531d54a01"}, + {file = "charset_normalizer-3.4.2-cp38-cp38-musllinux_1_2_s390x.whl", hash = "sha256:3c21d4fca343c805a52c0c78edc01e3477f6dd1ad7c47653241cf2a206d4fc58"}, + {file = "charset_normalizer-3.4.2-cp38-cp38-musllinux_1_2_x86_64.whl", hash = "sha256:dc7039885fa1baf9be153a0626e337aa7ec8bf96b0128605fb0d77788ddc1681"}, + {file = "charset_normalizer-3.4.2-cp38-cp38-win32.whl", hash = "sha256:8272b73e1c5603666618805fe821edba66892e2870058c94c53147602eab29c7"}, + {file = "charset_normalizer-3.4.2-cp38-cp38-win_amd64.whl", hash = "sha256:70f7172939fdf8790425ba31915bfbe8335030f05b9913d7ae00a87d4395620a"}, + {file = "charset_normalizer-3.4.2-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:005fa3432484527f9732ebd315da8da8001593e2cf46a3d817669f062c3d9ed4"}, + {file = "charset_normalizer-3.4.2-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e92fca20c46e9f5e1bb485887d074918b13543b1c2a1185e69bb8d17ab6236a7"}, + {file = "charset_normalizer-3.4.2-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:50bf98d5e563b83cc29471fa114366e6806bc06bc7a25fd59641e41445327836"}, + {file = "charset_normalizer-3.4.2-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:721c76e84fe669be19c5791da68232ca2e05ba5185575086e384352e2c309597"}, + {file = "charset_normalizer-3.4.2-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:82d8fd25b7f4675d0c47cf95b594d4e7b158aca33b76aa63d07186e13c0e0ab7"}, + {file = "charset_normalizer-3.4.2-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:b3daeac64d5b371dea99714f08ffc2c208522ec6b06fbc7866a450dd446f5c0f"}, + {file = "charset_normalizer-3.4.2-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:dccab8d5fa1ef9bfba0590ecf4d46df048d18ffe3eec01eeb73a42e0d9e7a8ba"}, + {file = "charset_normalizer-3.4.2-cp39-cp39-musllinux_1_2_i686.whl", hash = "sha256:aaf27faa992bfee0264dc1f03f4c75e9fcdda66a519db6b957a3f826e285cf12"}, + {file = "charset_normalizer-3.4.2-cp39-cp39-musllinux_1_2_ppc64le.whl", hash = "sha256:eb30abc20df9ab0814b5a2524f23d75dcf83cde762c161917a2b4b7b55b1e518"}, + {file = "charset_normalizer-3.4.2-cp39-cp39-musllinux_1_2_s390x.whl", hash = "sha256:c72fbbe68c6f32f251bdc08b8611c7b3060612236e960ef848e0a517ddbe76c5"}, + {file = "charset_normalizer-3.4.2-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:982bb1e8b4ffda883b3d0a521e23abcd6fd17418f6d2c4118d257a10199c0ce3"}, + {file = "charset_normalizer-3.4.2-cp39-cp39-win32.whl", hash = "sha256:43e0933a0eff183ee85833f341ec567c0980dae57c464d8a508e1b2ceb336471"}, + {file = "charset_normalizer-3.4.2-cp39-cp39-win_amd64.whl", hash = "sha256:d11b54acf878eef558599658b0ffca78138c8c3655cf4f3a4a673c437e67732e"}, + {file = "charset_normalizer-3.4.2-py3-none-any.whl", hash = "sha256:7f56930ab0abd1c45cd15be65cc741c28b1c9a34876ce8c17a2fa107810c0af0"}, + {file = "charset_normalizer-3.4.2.tar.gz", hash = "sha256:5baececa9ecba31eff645232d59845c07aa030f0c81ee70184a90d35099a0e63"}, +] + [[package]] name = "click" version = "8.1.8" @@ -372,6 +484,20 @@ files = [ griffe = ">=0.49" typing-extensions = ">=4.7" +[[package]] +name = "idna" +version = "3.10" +description = "Internationalized Domain Names in Applications (IDNA)" +optional = false +python-versions = ">=3.6" +files = [ + {file = "idna-3.10-py3-none-any.whl", hash = "sha256:946d195a0d259cbba61165e88e65941f16e9b36ea6ddb97f00452bae8b1287d3"}, + {file = "idna-3.10.tar.gz", hash = "sha256:12f65c9b470abda6dc35cf8e63cc574b1c52b11df2c86030af0ac09b01b13ea9"}, +] + +[package.extras] +all = ["flake8 (>=7.1.1)", "mypy (>=1.11.2)", "pytest (>=8.3.2)", "ruff (>=0.6.2)"] + [[package]] name = "importlib-metadata" version = "8.6.1" @@ -1068,6 +1194,20 @@ files = [ [package.dependencies] six = ">=1.5" +[[package]] +name = "python-dotenv" +version = "1.1.0" +description = "Read key-value pairs from a .env file and set them as environment variables" +optional = false +python-versions = ">=3.9" +files = [ + {file = "python_dotenv-1.1.0-py3-none-any.whl", hash = "sha256:d7c01d9e2293916c18baf562d95698754b0dbbb5e74d457c45d4f6561fb9d55d"}, + {file = "python_dotenv-1.1.0.tar.gz", hash = "sha256:41f90bc6f5f177fb41f53e87666db362025010eb28f60a01c9143bfa33a2b2d5"}, +] + +[package.extras] +cli = ["click (>=5.0)"] + [[package]] name = "pyyaml" version = "6.0.2" @@ -1249,6 +1389,27 @@ files = [ [package.dependencies] cffi = {version = "*", markers = "implementation_name == \"pypy\""} +[[package]] +name = "requests" +version = "2.32.3" +description = "Python HTTP for Humans." +optional = false +python-versions = ">=3.8" +files = [ + {file = "requests-2.32.3-py3-none-any.whl", hash = "sha256:70761cfe03c773ceb22aa2f671b4757976145175cdfca038c02654d061d6dcc6"}, + {file = "requests-2.32.3.tar.gz", hash = "sha256:55365417734eb18255590a9ff9eb97e9e1da868d4ccd6402399eaf68af20a760"}, +] + +[package.dependencies] +certifi = ">=2017.4.17" +charset-normalizer = ">=2,<4" +idna = ">=2.5,<4" +urllib3 = ">=1.21.1,<3" + +[package.extras] +socks = ["PySocks (>=1.5.6,!=1.5.7)"] +use-chardet-on-py3 = ["chardet (>=3.0.2,<6)"] + [[package]] name = "rich" version = "13.9.4" @@ -1383,6 +1544,23 @@ files = [ [package.dependencies] typing-extensions = ">=4.12.0" +[[package]] +name = "urllib3" +version = "2.4.0" +description = "HTTP library with thread-safe connection pooling, file post, and more." +optional = false +python-versions = ">=3.9" +files = [ + {file = "urllib3-2.4.0-py3-none-any.whl", hash = "sha256:4e16665048960a0900c702d4a66415956a584919c03361cac9f1df5c5dd7e813"}, + {file = "urllib3-2.4.0.tar.gz", hash = "sha256:414bc6535b787febd7567804cc015fee39daab8ad86268f1310a9250697de466"}, +] + +[package.extras] +brotli = ["brotli (>=1.0.9)", "brotlicffi (>=0.8.0)"] +h2 = ["h2 (>=4,<5)"] +socks = ["pysocks (>=1.5.6,!=1.5.7,<2.0)"] +zstd = ["zstandard (>=0.18.0)"] + [[package]] name = "virtualenv" version = "20.30.0" @@ -1467,4 +1645,4 @@ type = ["pytest-mypy"] [metadata] lock-version = "2.0" python-versions = ">=3.9.0" -content-hash = "9ef8c5bb81f8079006b9bf10ee48ca8e65fb9fdeadabff35b9ecb48bb670c2f4" +content-hash = "a225b9861a7e9eb69f2b651c07b0f691879d5747e54c3e14023c3b68143ca054" diff --git a/pyproject.toml b/pyproject.toml index 30af728..2f4760c 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,6 +1,6 @@ [project] name = "dotflow" -version = "0.12.0" +version = "0.13.0.dev1" authors = [ { name="Fernando Celmer", email="email@fernandocelmer.com" }, ] @@ -11,7 +11,8 @@ requires-python = ">=3.9" dependencies = [ "rich", "pydantic", - "typing-extensions" + "typing-extensions", + "requests" ] classifiers = [ 'Development Status :: 4 - Beta', @@ -35,7 +36,7 @@ mongodb = ["dotflow-mongodb"] [tool.poetry] name = "dotflow" -version = "0.12.0" +version = "0.13.0.dev1" description = "🎲 Dotflow turns an idea into flow!" authors = ["Fernando Celmer "] readme = "README.md" @@ -63,6 +64,8 @@ python = ">=3.9.0" rich = "^13.9.4" pydantic = "^2.10.6" typing-extensions = "^4.12.2" +python-dotenv = "^1.1.0" +requests = "^2.32.3" [tool.poetry.group.dev.dependencies] build = "^1.2.2.post1" diff --git a/tests/core/test_dotflow.py b/tests/core/test_dotflow.py index fe7752c..19fb404 100644 --- a/tests/core/test_dotflow.py +++ b/tests/core/test_dotflow.py @@ -6,7 +6,7 @@ from dotflow.core.context import Context from dotflow.core.task import Task, TaskBuilder from dotflow.core.dotflow import DotFlow -from dotflow.core.types.status import TaskStatus +from dotflow.core.types.status import TypeStatus from tests.mocks import action_step @@ -27,7 +27,7 @@ def test_result_task_with_start(self): self.assertEqual(len(result), 1) self.assertIsInstance(result[0], Task) - self.assertEqual(result[0].status, TaskStatus.COMPLETED) + self.assertEqual(result[0].status, TypeStatus.COMPLETED) def test_result_context_with_start(self): self.workflow.start() @@ -48,7 +48,7 @@ def test_result_task_without_start(self): self.assertEqual(len(result), 1) self.assertIsInstance(result[0], Task) - self.assertEqual(result[0].status, TaskStatus.NOT_STARTED) + self.assertEqual(result[0].status, TypeStatus.NOT_STARTED) def test_result_context_without_start(self): result = self.workflow.result_context() diff --git a/tests/core/test_execution.py b/tests/core/test_execution.py index 3a0496a..39da73e 100644 --- a/tests/core/test_execution.py +++ b/tests/core/test_execution.py @@ -9,7 +9,7 @@ from dotflow.core.context import Context from dotflow.core.execution import Execution -from dotflow.core.types import TaskStatus +from dotflow.core.types import TypeStatus from dotflow.core.task import Task from tests.mocks import ( @@ -49,7 +49,7 @@ def test_execution_with_function_completed(self): task=task, workflow_id=workflow_id, previous_context=Context() ) - self.assertEqual(controller.task.status, TaskStatus.COMPLETED) + self.assertEqual(controller.task.status, TypeStatus.COMPLETED) self.assertEqual(controller.task.workflow_id, workflow_id) def test_execution_with_function_failed(self): @@ -59,7 +59,7 @@ def test_execution_with_function_failed(self): task=task, workflow_id=workflow_id, previous_context=Context() ) - self.assertEqual(controller.task.status, TaskStatus.FAILED) + self.assertEqual(controller.task.status, TypeStatus.FAILED) self.assertEqual(controller.task.workflow_id, workflow_id) def test_execution_with_class_completed(self): @@ -78,7 +78,7 @@ def test_execution_with_class_completed(self): execution_log = log.message self.assertEqual(execution_log, "ActionStep: Run function executed") - self.assertEqual(controller.task.status, TaskStatus.COMPLETED) + self.assertEqual(controller.task.status, TypeStatus.COMPLETED) self.assertEqual(controller.task.workflow_id, workflow_id) def test_execution_with_class_failed(self): @@ -97,7 +97,7 @@ def test_execution_with_class_failed(self): execution_log = log.message self.assertEqual(execution_log, "ActionStepWithError: Run function executed") - self.assertEqual(controller.task.status, TaskStatus.FAILED) + self.assertEqual(controller.task.status, TypeStatus.FAILED) self.assertEqual(controller.task.workflow_id, workflow_id) def test_execution_function_with_initial_context(self): @@ -111,7 +111,7 @@ def test_execution_function_with_initial_context(self): task=task, workflow_id=self.workflow_id, previous_context=None ) - self.assertEqual(controller.task.status, TaskStatus.COMPLETED) + self.assertEqual(controller.task.status, TypeStatus.COMPLETED) self.assertEqual(controller.task.initial_context.storage, self.context) def test_execution_function_with_previous_context(self): @@ -122,7 +122,7 @@ def test_execution_function_with_previous_context(self): task=task, workflow_id=self.workflow_id, previous_context=self.context ) - self.assertEqual(controller.task.status, TaskStatus.COMPLETED) + self.assertEqual(controller.task.status, TypeStatus.COMPLETED) self.assertEqual(controller.task._previous_context.storage, self.context) def test_execution_function_with_contexts(self): @@ -136,7 +136,7 @@ def test_execution_function_with_contexts(self): task=task, workflow_id=self.workflow_id, previous_context=self.context ) - self.assertEqual(controller.task.status, TaskStatus.COMPLETED) + self.assertEqual(controller.task.status, TypeStatus.COMPLETED) self.assertEqual(controller.task.initial_context.storage, self.context) self.assertEqual(controller.task.previous_context.storage, self.context) @@ -151,7 +151,7 @@ def test_execution_class_with_initial_context(self): task=task, workflow_id=self.workflow_id, previous_context=None ) - self.assertEqual(controller.task.status, TaskStatus.COMPLETED) + self.assertEqual(controller.task.status, TypeStatus.COMPLETED) self.assertEqual(controller.task.initial_context.storage, self.context) def test_execution_class_with_previous_context(self): @@ -162,7 +162,7 @@ def test_execution_class_with_previous_context(self): task=task, workflow_id=self.workflow_id, previous_context=self.context ) - self.assertEqual(controller.task.status, TaskStatus.COMPLETED) + self.assertEqual(controller.task.status, TypeStatus.COMPLETED) self.assertEqual(controller.task.previous_context.storage, self.context) self.assertEqual( @@ -183,7 +183,7 @@ def test_execution_class_with_contexts(self): task=task, workflow_id=self.workflow_id, previous_context=self.context ) - self.assertEqual(controller.task.status, TaskStatus.COMPLETED) + self.assertEqual(controller.task.status, TypeStatus.COMPLETED) self.assertEqual(controller.task.initial_context.storage, self.context) self.assertEqual(controller.task.previous_context.storage, self.context) @@ -289,5 +289,5 @@ def test_valid_objects(self): previous_context=None ) - self.assertEqual(controller.task.status, TaskStatus.COMPLETED) + self.assertEqual(controller.task.status, TypeStatus.COMPLETED) self.assertEqual(controller.task.current_context.storage, input_value) diff --git a/tests/core/test_task.py b/tests/core/test_task.py index 411c67c..b8ddaed 100644 --- a/tests/core/test_task.py +++ b/tests/core/test_task.py @@ -8,7 +8,7 @@ from dotflow.core.action import Action from dotflow.core.config import Config from dotflow.core.context import Context -from dotflow.core.types.status import TaskStatus +from dotflow.core.types.status import TypeStatus from dotflow.core.serializers.task import SerializerTaskError, SerializerTask from dotflow.core.exception import ( MissingActionDecorator, @@ -92,7 +92,7 @@ def test_task_schema(self): self.assertEqual(schema.task_id, 0) self.assertEqual(schema.workflow_id, expected_workflow_id) - self.assertEqual(schema.status, TaskStatus.NOT_STARTED) + self.assertEqual(schema.status, TypeStatus.NOT_STARTED) self.assertEqual(schema.error.message, expected_error_message) self.assertEqual(schema.duration, expected_duration) self.assertEqual(schema.initial_context, json.dumps(self.content)) @@ -221,7 +221,7 @@ def test_set_error(self): self.assertIsInstance(self.task.error.exception, Exception) def test_set_status(self): - expected_value = TaskStatus.COMPLETED + expected_value = TypeStatus.COMPLETED self.task.status = expected_value self.assertEqual(self.task.status, expected_value) diff --git a/tests/core/test_workflow.py b/tests/core/test_workflow.py index dbc40e1..682eecb 100644 --- a/tests/core/test_workflow.py +++ b/tests/core/test_workflow.py @@ -7,7 +7,7 @@ from types import FunctionType from dotflow.core.workflow import Manager -from dotflow.core.types import TypeExecution, TaskStatus +from dotflow.core.types import TypeExecution, TypeStatus from dotflow.core.exception import ExecutionModeNotExist from dotflow.core.task import Task @@ -45,7 +45,7 @@ def test_workflow_with_function_completed(self): ) controller = Manager(tasks=[task]) - self.assertEqual(controller.tasks[0].status, TaskStatus.COMPLETED) + self.assertEqual(controller.tasks[0].status, TypeStatus.COMPLETED) def test_workflow_with_function_failed(self): task = Task( @@ -55,7 +55,7 @@ def test_workflow_with_function_failed(self): ) controller = Manager(tasks=[task]) - self.assertEqual(controller.tasks[0].status, TaskStatus.FAILED) + self.assertEqual(controller.tasks[0].status, TypeStatus.FAILED) def test_with_execution_mode_that_does_not_exist(self): with self.assertRaises(ExecutionModeNotExist): @@ -100,4 +100,4 @@ def test_workflow_with_class_completed(self): ) controller = Manager(tasks=[task]) - self.assertEqual(controller.tasks[0].status, TaskStatus.COMPLETED) + self.assertEqual(controller.tasks[0].status, TypeStatus.COMPLETED) diff --git a/tests/core/test_workflow_background.py b/tests/core/test_workflow_background.py index 2c6a792..8b01d5b 100644 --- a/tests/core/test_workflow_background.py +++ b/tests/core/test_workflow_background.py @@ -5,7 +5,7 @@ from uuid import uuid4 from dotflow.core.workflow import Background, grouper -from dotflow.core.types import TaskStatus +from dotflow.core.types import TypeStatus from dotflow.core.task import Task, TaskError from tests.mocks import ( @@ -51,7 +51,7 @@ def test_workflow_with_background_function_completed(self): tasks = execution.get_tasks() - self.assertEqual(tasks[0].status, TaskStatus.COMPLETED) + self.assertEqual(tasks[0].status, TypeStatus.COMPLETED) self.assertEqual(tasks[0].current_context.storage, {"foo": "bar"}) self.assertIsInstance(tasks[0].error, TaskError) self.assertEqual(tasks[0].error.message, "") @@ -68,7 +68,7 @@ def test_workflow_with_background_function_failed(self): tasks = execution.get_tasks() - self.assertEqual(tasks[0].status, TaskStatus.FAILED) + self.assertEqual(tasks[0].status, TypeStatus.FAILED) self.assertIsNone(tasks[0].current_context.storage) self.assertIsInstance(tasks[0].error, TaskError) self.assertEqual(tasks[0].error.message, "Fail!") diff --git a/tests/core/test_workflow_parallel.py b/tests/core/test_workflow_parallel.py index 038ead5..48c0341 100644 --- a/tests/core/test_workflow_parallel.py +++ b/tests/core/test_workflow_parallel.py @@ -6,7 +6,7 @@ from multiprocessing.queues import Queue from dotflow.core.workflow import Parallel, grouper -from dotflow.core.types import TaskStatus +from dotflow.core.types import TypeStatus from dotflow.core.task import Task, TaskError from tests.mocks import ( @@ -52,7 +52,7 @@ def test_workflow_with_parallel_function_completed(self): tasks = execution.get_tasks() - self.assertEqual(tasks[0].status, TaskStatus.COMPLETED) + self.assertEqual(tasks[0].status, TypeStatus.COMPLETED) self.assertEqual(tasks[0].current_context.storage, {"foo": "bar"}) self.assertIsInstance(tasks[0].error, TaskError) self.assertEqual(tasks[0].error.message, "") @@ -69,7 +69,7 @@ def test_workflow_with_parallel_function_failed(self): tasks = execution.get_tasks() - self.assertEqual(tasks[0].status, TaskStatus.FAILED) + self.assertEqual(tasks[0].status, TypeStatus.FAILED) self.assertIsNone(tasks[0].current_context.storage) self.assertIsInstance(tasks[0].error, TaskError) self.assertEqual(tasks[0].error.message, "Fail!") diff --git a/tests/core/test_workflow_sequential.py b/tests/core/test_workflow_sequential.py index 2d0bbba..beddac0 100644 --- a/tests/core/test_workflow_sequential.py +++ b/tests/core/test_workflow_sequential.py @@ -6,7 +6,7 @@ from unittest.mock import Mock from dotflow.core.workflow import Sequential, grouper -from dotflow.core.types import TaskStatus +from dotflow.core.types import TypeStatus from dotflow.core.task import Task, TaskError from tests.mocks import ( @@ -61,7 +61,7 @@ def test_workflow_with_function_completed(self): groups=grouper(tasks=tasks), ) - self.assertEqual(execution.tasks[0].status, TaskStatus.COMPLETED) + self.assertEqual(execution.tasks[0].status, TypeStatus.COMPLETED) self.assertEqual(execution.tasks[0].current_context.storage, {"foo": "bar"}) self.assertIsInstance(execution.tasks[0].error, TaskError) self.assertEqual(execution.tasks[0].error.message, "") @@ -76,7 +76,7 @@ def test_workflow_with_function_failed(self): groups=grouper(tasks=tasks), ) - self.assertEqual(execution.tasks[0].status, TaskStatus.FAILED) + self.assertEqual(execution.tasks[0].status, TypeStatus.FAILED) self.assertIsNone(execution.tasks[0].current_context.storage) self.assertIsInstance(execution.tasks[0].error, TaskError) self.assertEqual(execution.tasks[0].error.message, "Fail!") diff --git a/tests/core/test_workflow_sequential_group.py b/tests/core/test_workflow_sequential_group.py index 7bd39b7..6348d90 100644 --- a/tests/core/test_workflow_sequential_group.py +++ b/tests/core/test_workflow_sequential_group.py @@ -7,7 +7,7 @@ from multiprocessing.queues import Queue from dotflow.core.workflow import SequentialGroup, grouper -from dotflow.core.types import TaskStatus +from dotflow.core.types import TypeStatus from dotflow.core.task import Task, TaskError from tests.mocks import ( @@ -53,7 +53,7 @@ def test_workflow_with_sequential_group_function_completed(self): tasks = execution.get_tasks() - self.assertEqual(tasks[0].status, TaskStatus.COMPLETED) + self.assertEqual(tasks[0].status, TypeStatus.COMPLETED) self.assertEqual(tasks[0].current_context.storage, {"foo": "bar"}) self.assertIsInstance(tasks[0].error, TaskError) self.assertEqual(tasks[0].error.message, "") @@ -70,7 +70,7 @@ def test_workflow_with_sequential_group_function_failed(self): tasks = execution.get_tasks() - self.assertEqual(tasks[0].status, TaskStatus.FAILED) + self.assertEqual(tasks[0].status, TypeStatus.FAILED) self.assertIsNone(tasks[0].current_context.storage) self.assertIsInstance(tasks[0].error, TaskError) self.assertEqual(tasks[0].error.message, "Fail!")