Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
30 changes: 14 additions & 16 deletions .github/workflows/release.yml
Original file line number Diff line number Diff line change
Expand Up @@ -11,39 +11,37 @@ jobs:
runs-on: ubuntu-latest
steps:
- name: Checkout code
uses: actions/checkout@v3
uses: actions/checkout@v6

- name: Set up Python 3.10
uses: actions/setup-python@v4
- name: Install uv and set Python 3.10
uses: astral-sh/setup-uv@v7
with:
python-version: "3.10"

- name: Install Poetry
uses: snok/install-poetry@v1
with:
version: 1.8.3
virtualenvs-create: true
virtualenvs-in-project: true
version: "0.10.9"
enable-cache: true

- name: Build project for distribution
run: poetry build
run: uv build

- name: Check Version
id: check-version
run: |
[[ "$(poetry version --short)" =~ ^[0-9]+\.[0-9]+\.[0-9]+$ ]] \
|| echo ::set-output name=prerelease::true
if ! [[ "$(uv version --short)" =~ ^[0-9]+\.[0-9]+\.[0-9]+$ ]]; then
echo "prerelease=true" >> "$GITHUB_OUTPUT"
fi

- name: Create Release
uses: ncipollo/release-action@v1
with:
artifacts: "dist/*"
token: ${{ secrets.GITHUB_TOKEN }}
draft: false
prerelease: steps.check-version.outputs.prerelease == 'true'
prerelease: ${{ steps.check-version.outputs.prerelease == 'true' }}
allowUpdates: true

- name: Publish to PyPI
env:
POETRY_PYPI_TOKEN_PYPI: ${{ secrets.PYPI_TOKEN }}
run: poetry publish
UV_PUBLISH_TOKEN: ${{ secrets.PYPI_TOKEN }}
run: uv publish --check-url https://pypi.org/simple
- name: Prune uv cache
run: uv cache prune --ci
33 changes: 10 additions & 23 deletions .github/workflows/test.yml
Original file line number Diff line number Diff line change
Expand Up @@ -14,29 +14,16 @@ jobs:
python-version: [ "3.8", "3.9", "3.10" ]
runs-on: ${{ matrix.os }}
steps:
- uses: actions/checkout@v2
- name: Set up Python ${{ matrix.python-version }}
uses: actions/setup-python@v2
- uses: actions/checkout@v6
- name: Install uv and set Python ${{ matrix.python-version }}
uses: astral-sh/setup-uv@v7
with:
python-version: ${{ matrix.python-version }}
- name: Install Poetry
uses: snok/install-poetry@v1
with:
version: 1.8.3
virtualenvs-create: true
virtualenvs-in-project: true
- name: Load cached venv
id: cached-poetry-dependencies
uses: actions/cache@v3
with:
path: .venv
key: venv-${{ runner.os }}-${{ matrix.python-version }}-${{ hashFiles('**/poetry.lock') }}
- name: Install dependencies
if: steps.cached-poetry-dependencies.outputs.cache-hit != 'true'
run: poetry install --no-interaction --no-root
- name: Install library
run: poetry install --no-interaction
version: "0.10.9"
enable-cache: true
- name: Install project dependencies
run: uv sync --locked --group dev
- name: Run tests
run: |
source .venv/bin/activate
pytest tests/
run: uv run pytest tests/
- name: Prune uv cache
run: uv cache prune --ci
1 change: 0 additions & 1 deletion .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -132,7 +132,6 @@ dmypy.json

# pycharm
.idea
poetry.lock
*.DS_Store

# docs/node_modules
Expand Down
19 changes: 10 additions & 9 deletions Makefile
Original file line number Diff line number Diff line change
@@ -1,17 +1,18 @@
install: ## Run `poetry install`
poetry install --no-root
install: ## Sync the project with uv
uv sync --group dev

lint:
poetry run isort --check .
poetry run black --check .
poetry run flake8 src tests
uv run --group dev isort --check .
uv run --group dev black --check .
uv run --group dev flake8 src tests

format: ## Formasts you code with Black
poetry run isort .
poetry run black .
uv run --group dev isort .
uv run --group dev black .

test:
poetry run pytest -v tests
uv run --group dev pytest -v tests

publish:
poetry publish --build
uv build
uv publish
42 changes: 22 additions & 20 deletions pyproject.toml
Original file line number Diff line number Diff line change
@@ -1,27 +1,29 @@
[tool.poetry]
[project]
name = "use-notify"
version = "0.3.5"
description = "一个简单可扩展的异步消息通知库"
authors = ["miclon <jcnd@163.com>"]
readme = "README.md"
packages = [{ include = 'use_notify', from = 'src' }]
requires-python = ">=3.8,<4.0"
authors = [
{ name = "miclon", email = "jcnd@163.com" },
]
dependencies = [
"httpx",
"usepy>=0.4.0",
]

[tool.poetry.dependencies]
python = "^3.8"
usepy = "^0.4.0"
httpx = "*"


[tool.poetry.group.test.dependencies]
pylint = "*"
pytest = "*"
black = "*"
flake8 = "*"
isort = "*"
pre-commit = "*"
pre-commit-hooks = "*"
pytest-asyncio = "0.18.3"
[dependency-groups]
dev = [
"black",
"flake8",
"isort",
"pre-commit",
"pre-commit-hooks",
"pylint",
"pytest",
"pytest-asyncio==0.18.3",
]

[build-system]
requires = ["poetry-core>=1.0.0"]
build-backend = "poetry.core.masonry.api"
requires = ["uv_build>=0.10.4,<0.11.0"]
build-backend = "uv_build"
8 changes: 7 additions & 1 deletion src/use_notify/__init__.py
Original file line number Diff line number Diff line change
@@ -1,12 +1,18 @@
# flake8: noqa: F401
from . import channels as useNotifyChannel
from .notification import Notify as useNotify
from .notification import (
Notify as useNotify,
NotificationPublishError,
RetryConfig,
)
from .decorator import notify, set_default_notify_instance, get_default_notify_instance, clear_default_notify_instance


__all__ = [
"useNotifyChannel",
"useNotify",
"NotificationPublishError",
"RetryConfig",
"notify",
"set_default_notify_instance",
"get_default_notify_instance",
Expand Down
101 changes: 93 additions & 8 deletions src/use_notify/decorator/core.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@
import inspect
import logging
from datetime import datetime
from typing import Any, Callable, Optional
from typing import Callable, Optional, Sequence, Type

from ..notification import Notify
from .context import ExecutionContext
Expand All @@ -21,6 +21,7 @@

# 全局默认通知实例
_default_notify_instance: Optional[Notify] = None
RetriableExceptionsInput = Optional[Sequence[Type[BaseException]]]


def set_default_notify_instance(notify_instance: Notify) -> None:
Expand Down Expand Up @@ -76,12 +77,17 @@ def __init__(
notify_on_error: bool = True,
include_args: bool = False,
include_result: bool = False,
timeout: Optional[float] = None
timeout: Optional[float] = None,
max_retries: Optional[int] = None,
retry_delay: Optional[float] = None,
retry_backoff: Optional[float] = None,
retriable_exceptions: RetriableExceptionsInput = None,
):
# 验证配置
self._validate_config(
notify_instance, title, success_template, error_template,
notify_on_success, notify_on_error, include_args, include_result, timeout
notify_on_success, notify_on_error, include_args, include_result, timeout,
max_retries, retry_delay, retry_backoff, retriable_exceptions,
)

# 如果没有提供 notify_instance,尝试使用全局默认实例
Expand All @@ -92,6 +98,14 @@ def __init__(
logger.warning("未提供 notify_instance 且未设置全局默认实例,创建了一个空的 Notify 实例。请确保添加通知渠道或设置默认实例。")
else:
logger.debug("使用全局默认通知实例")

notify_instance = self._apply_retry_overrides(
notify_instance=notify_instance,
max_retries=max_retries,
retry_delay=retry_delay,
retry_backoff=retry_backoff,
retriable_exceptions=retriable_exceptions,
)

self.notify_instance = notify_instance
self.title = title
Expand Down Expand Up @@ -242,7 +256,8 @@ async def _send_error_notification_async(self, context: ExecutionContext) -> Non
def _validate_config(self, *args) -> None:
"""验证配置参数"""
notify_instance, title, success_template, error_template, \
notify_on_success, notify_on_error, include_args, include_result, timeout = args
notify_on_success, notify_on_error, include_args, include_result, timeout, \
max_retries, retry_delay, retry_backoff, retriable_exceptions = args

if notify_instance is not None and not isinstance(notify_instance, Notify):
raise NotifyConfigError("notify_instance 必须是 Notify 类的实例")
Expand Down Expand Up @@ -270,10 +285,68 @@ def _validate_config(self, *args) -> None:

if timeout is not None and (not isinstance(timeout, (int, float)) or timeout <= 0):
raise NotifyConfigError("timeout 必须是正数")


if max_retries is not None and (not isinstance(max_retries, int) or max_retries < 0):
raise NotifyConfigError("max_retries 必须是大于等于 0 的整数")

if retry_delay is not None and (
not isinstance(retry_delay, (int, float)) or retry_delay < 0
):
raise NotifyConfigError("retry_delay 必须是大于等于 0 的数字")

if retry_backoff is not None and (
not isinstance(retry_backoff, (int, float)) or retry_backoff <= 0
):
raise NotifyConfigError("retry_backoff 必须是正数")

if retriable_exceptions is not None:
if not isinstance(retriable_exceptions, (list, tuple)):
raise NotifyConfigError("retriable_exceptions 必须是异常类型序列")
invalid_types = [
exception_type
for exception_type in retriable_exceptions
if not isinstance(exception_type, type)
or not issubclass(exception_type, BaseException)
]
if invalid_types:
raise NotifyConfigError("retriable_exceptions 必须只包含异常类型")

if not notify_on_success and not notify_on_error:
raise NotifyConfigError("notify_on_success 和 notify_on_error 不能同时为 False")

@staticmethod
def _apply_retry_overrides(
notify_instance: Notify,
max_retries: Optional[int],
retry_delay: Optional[float],
retry_backoff: Optional[float],
retriable_exceptions: RetriableExceptionsInput,
) -> Notify:
if (
max_retries is None
and retry_delay is None
and retry_backoff is None
and retriable_exceptions is None
):
return notify_instance

retry_config = notify_instance.retry_config
return Notify(
channels=list(notify_instance.channels),
max_retries=retry_config.max_retries if max_retries is None else max_retries,
retry_delay=retry_config.retry_delay if retry_delay is None else retry_delay,
retry_backoff=(
retry_config.retry_backoff
if retry_backoff is None
else retry_backoff
),
retriable_exceptions=(
retry_config.retriable_exceptions
if retriable_exceptions is None
else tuple(retriable_exceptions)
),
)


def notify(
notify_instance: Optional[Notify] = None,
Expand All @@ -284,7 +357,11 @@ def notify(
notify_on_error: bool = True,
include_args: bool = False,
include_result: bool = False,
timeout: Optional[float] = None
timeout: Optional[float] = None,
max_retries: Optional[int] = None,
retry_delay: Optional[float] = None,
retry_backoff: Optional[float] = None,
retriable_exceptions: RetriableExceptionsInput = None,
) -> Callable:
"""
创建通知装饰器的工厂函数
Expand All @@ -299,6 +376,10 @@ def notify(
include_args: 是否在消息中包含函数参数
include_result: 是否在消息中包含函数返回值
timeout: 通知发送超时时间(秒)
max_retries: 通知发送失败后的最大重试次数
retry_delay: 每次重试前的延迟(秒)
retry_backoff: 重试延迟的退避倍数
retriable_exceptions: 额外视为可重试的异常类型序列

Returns:
装饰器函数
Expand All @@ -325,5 +406,9 @@ def important_task():
notify_on_error=notify_on_error,
include_args=include_args,
include_result=include_result,
timeout=timeout
)
timeout=timeout,
max_retries=max_retries,
retry_delay=retry_delay,
retry_backoff=retry_backoff,
retriable_exceptions=retriable_exceptions,
)
Loading