├── .github └── workflows │ ├── release.yml │ └── test.yml ├── .gitignore ├── Makefile ├── README.md ├── example ├── demo.py └── from_setting.py ├── pyproject.toml ├── src └── use_notify │ ├── __init__.py │ ├── channels │ ├── __init__.py │ ├── bark.py │ ├── base.py │ ├── chanify.py │ ├── ding.py │ ├── email.py │ ├── pushdeer.py │ ├── pushover.py │ └── wechat.py │ └── notification.py └── tests ├── test_from_settings.py └── test_publisher.py /.github/workflows/release.yml: -------------------------------------------------------------------------------- 1 | name: Release 2 | 3 | on: 4 | push: 5 | tags: 6 | - '*.*.*' 7 | 8 | jobs: 9 | release: 10 | name: Release 11 | runs-on: ubuntu-latest 12 | steps: 13 | - name: Checkout code 14 | uses: actions/checkout@v3 15 | 16 | - name: Set up Python 3.10 17 | uses: actions/setup-python@v4 18 | with: 19 | python-version: "3.10" 20 | 21 | - name: Install Poetry 22 | uses: snok/install-poetry@v1 23 | with: 24 | virtualenvs-create: true 25 | virtualenvs-in-project: true 26 | installer-parallel: true 27 | 28 | - name: Build project for distribution 29 | run: poetry build 30 | 31 | - name: Check Version 32 | id: check-version 33 | run: | 34 | [[ "$(poetry version --short)" =~ ^[0-9]+\.[0-9]+\.[0-9]+$ ]] \ 35 | || echo ::set-output name=prerelease::true 36 | 37 | - name: Create Release 38 | uses: ncipollo/release-action@v1 39 | with: 40 | artifacts: "dist/*" 41 | token: ${{ secrets.GITHUB_TOKEN }} 42 | draft: false 43 | prerelease: steps.check-version.outputs.prerelease == 'true' 44 | allowUpdates: true 45 | 46 | - name: Publish to PyPI 47 | env: 48 | POETRY_PYPI_TOKEN_PYPI: ${{ secrets.PYPI_TOKEN }} 49 | run: poetry publish -------------------------------------------------------------------------------- /.github/workflows/test.yml: -------------------------------------------------------------------------------- 1 | name: test suite 2 | 3 | on: 4 | push: 5 | branches: [ main ] 6 | pull_request: 7 | 8 | jobs: 9 | test: 10 | strategy: 11 | fail-fast: true 12 | matrix: 13 | os: [ "ubuntu-latest" ] 14 | python-version: [ "3.8", "3.9", "3.10" ] 15 | runs-on: ${{ matrix.os }} 16 | steps: 17 | - uses: actions/checkout@v2 18 | - name: Set up Python ${{ matrix.python-version }} 19 | uses: actions/setup-python@v2 20 | with: 21 | python-version: ${{ matrix.python-version }} 22 | - name: Install Poetry 23 | uses: snok/install-poetry@v1 24 | with: 25 | virtualenvs-create: true 26 | virtualenvs-in-project: true 27 | installer-parallel: true 28 | - name: Load cached venv 29 | id: cached-poetry-dependencies 30 | uses: actions/cache@v2 31 | with: 32 | path: .venv 33 | key: venv-${{ runner.os }}-${{ matrix.python-version }}-${{ hashFiles('**/poetry.lock') }} 34 | - name: Install dependencies 35 | if: steps.cached-poetry-dependencies.outputs.cache-hit != 'true' 36 | run: poetry install --no-interaction --no-root 37 | - name: Install library 38 | run: poetry install --no-interaction 39 | - name: Run tests 40 | run: | 41 | source .venv/bin/activate 42 | pytest tests/ -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # created by virtualenv automatically 2 | # Byte-compiled / optimized / DLL files 3 | __pycache__/ 4 | *.py[cod] 5 | *$py.class 6 | 7 | # C extensions 8 | *.so 9 | 10 | # Distribution / packaging 11 | .Python 12 | build/ 13 | develop-eggs/ 14 | dist/ 15 | downloads/ 16 | eggs/ 17 | .eggs/ 18 | lib/ 19 | lib64/ 20 | parts/ 21 | sdist/ 22 | var/ 23 | wheels/ 24 | pip-wheel-metadata/ 25 | share/python-wheels/ 26 | *.egg-info/ 27 | .installed.cfg 28 | *.egg 29 | MANIFEST 30 | 31 | # PyInstaller 32 | # Usually these files are written by a python script from a template 33 | # before PyInstaller builds the exe, so as to inject date/other infos into it. 34 | *.manifest 35 | *.spec 36 | 37 | # Installer logs 38 | pip-log.txt 39 | pip-delete-this-directory.txt 40 | 41 | # Unit test / coverage reports 42 | htmlcov/ 43 | .tox/ 44 | .nox/ 45 | .coverage 46 | .coverage.* 47 | .cache 48 | nosetests.xml 49 | coverage.xml 50 | *.cover 51 | *.py,cover 52 | .hypothesis/ 53 | .pytest_cache/ 54 | 55 | # Translations 56 | *.mo 57 | *.pot 58 | 59 | # Django stuff: 60 | *.log 61 | local_settings.py 62 | db.sqlite3 63 | db.sqlite3-journal 64 | 65 | # Flask stuff: 66 | instance/ 67 | .webassets-cache 68 | 69 | # Scrapy stuff: 70 | .scrapy 71 | 72 | # Sphinx documentation 73 | docs/_build/ 74 | 75 | # PyBuilder 76 | target/ 77 | 78 | # Jupyter Notebook 79 | .ipynb_checkpoints 80 | 81 | # IPython 82 | profile_default/ 83 | ipython_config.py 84 | 85 | # pyenv 86 | .python-version 87 | 88 | # pipenv 89 | # According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control. 90 | # However, in case of collaboration, if having platform-specific dependencies or dependencies 91 | # having no cross-platform support, pipenv may install dependencies that don't work, or not 92 | # install all needed dependencies. 93 | #Pipfile.lock 94 | 95 | # PEP 582; used by e.g. github.com/David-OConnor/pyflow 96 | __pypackages__/ 97 | 98 | # Celery stuff 99 | celerybeat-schedule 100 | celerybeat.pid 101 | 102 | # SageMath parsed files 103 | *.sage.py 104 | 105 | # Environments 106 | .env 107 | .venv 108 | env/ 109 | venv/ 110 | ENV/ 111 | env.bak/ 112 | venv.bak/ 113 | 114 | # Spyder project settings 115 | .spyderproject 116 | .spyproject 117 | 118 | # Rope project settings 119 | .ropeproject 120 | 121 | # mkdocs documentation 122 | /site 123 | 124 | # mypy 125 | .mypy_cache/ 126 | .dmypy.json 127 | dmypy.json 128 | 129 | # Pyre type checker 130 | .pyre/ 131 | 132 | # pycharm 133 | .idea 134 | poetry.lock 135 | *.DS_Store 136 | 137 | # docs/node_modules 138 | docs/node_modules -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | install: ## Run `poetry install` 2 | poetry install --no-root 3 | 4 | lint: 5 | poetry run isort --check . 6 | poetry run black --check . 7 | poetry run flake8 src tests 8 | 9 | format: ## Formasts you code with Black 10 | poetry run isort . 11 | poetry run black . 12 | 13 | test: 14 | poetry run pytest -v tests 15 | 16 | publish: 17 | poetry publish --build 18 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | ### 一个简单可扩展的异步消息通知库 2 | 3 | 4 | Package version 5 | 6 | 7 | 8 | Supported Python versions 9 | 10 | 11 | #### 安装 12 | 13 | > pip install use-notify 14 | 15 | #### 使用 16 | 17 | ```python 18 | from use_notify import useNotify, useNotifyChannel 19 | # if you use usepy, also can use `usepy.plugin` 20 | # from usepy.plugin import useNotify, useNotifyChannel 21 | 22 | notify = useNotify() 23 | notify.add( 24 | # 添加多个通知渠道 25 | useNotifyChannel.Bark({"token": "xxxxxx"}), 26 | useNotifyChannel.Ding({ 27 | "token": "xxxxx", 28 | "at_all": True 29 | }) 30 | ) 31 | 32 | notify.publish(title="消息标题", content="消息正文") 33 | 34 | ``` 35 | 36 | #### 支持的消息通知渠道列表 37 | 38 | - Wechat 39 | - Ding 40 | - Bark 41 | - Email 42 | - Chanify 43 | - Pushdeer 44 | - Pushover 45 | 46 | #### 自己开发消息通知 47 | 48 | ```python 49 | from use_notify import useNotifyChannel 50 | 51 | 52 | class Custom(useNotifyChannel.BaseChannel): 53 | """自定义消息通知""" 54 | 55 | def send(self, *args, **kwargs): 56 | ... 57 | 58 | async def send_async(self, *args, **kwargs): 59 | ... 60 | ``` 61 | -------------------------------------------------------------------------------- /example/demo.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | import asyncio 3 | 4 | from use_notify import useNotify, useNotifyChannel 5 | 6 | notify = useNotify() 7 | notify.add( 8 | # 添加多个通知渠道 9 | useNotifyChannel.Bark({"token": "your token"}), 10 | ) 11 | 12 | asyncio.run(notify.publish_async(title="消息标题", content="消息正文")) 13 | -------------------------------------------------------------------------------- /example/from_setting.py: -------------------------------------------------------------------------------- 1 | from use_notify import useNotify 2 | 3 | settings = { 4 | "bark": { 5 | "token": "YOUR_BARK_TOKEN", 6 | } 7 | } 8 | 9 | notify = useNotify.from_settings(settings) 10 | notify.publish("content") -------------------------------------------------------------------------------- /pyproject.toml: -------------------------------------------------------------------------------- 1 | [tool.poetry] 2 | name = "use-notify" 3 | version = "0.3.2" 4 | description = "一个简单可扩展的异步消息通知库" 5 | authors = ["miclon "] 6 | readme = "README.md" 7 | packages = [ 8 | { include = 'use_notify', from = 'src' } 9 | ] 10 | 11 | [tool.poetry.dependencies] 12 | python = "^3.8" 13 | usepy = "^0.2.6" 14 | httpx = "^0.23.3" 15 | 16 | 17 | [tool.poetry.group.test.dependencies] 18 | pylint = "*" 19 | pytest = "*" 20 | black = "*" 21 | flake8 = "*" 22 | isort = "*" 23 | pre-commit = "*" 24 | pre-commit-hooks = "*" 25 | 26 | [build-system] 27 | requires = ["poetry-core>=1.0.0"] 28 | build-backend = "poetry.core.masonry.api" 29 | -------------------------------------------------------------------------------- /src/use_notify/__init__.py: -------------------------------------------------------------------------------- 1 | # flake8: noqa: F401 2 | from . import channels as useNotifyChannel 3 | from .notification import Notify as useNotify 4 | -------------------------------------------------------------------------------- /src/use_notify/channels/__init__.py: -------------------------------------------------------------------------------- 1 | # flake8: noqa: F401 2 | from .bark import Bark 3 | from .base import BaseChannel 4 | from .chanify import Chanify 5 | from .ding import Ding 6 | from .email import Email 7 | from .pushdeer import PushDeer 8 | from .pushover import PushOver 9 | from .wechat import WeChat 10 | -------------------------------------------------------------------------------- /src/use_notify/channels/bark.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | import logging 3 | 4 | import httpx 5 | 6 | from .base import BaseChannel 7 | 8 | logger = logging.getLogger(__name__) 9 | 10 | 11 | class Bark(BaseChannel): 12 | """Bark app 消息通知""" 13 | 14 | @property 15 | def api_url(self): 16 | return f"https://api.day.app/{self.config.token}/{{title}}/{{content}}" 17 | 18 | @property 19 | def headers(self): 20 | return {"Content-Type": "application/x-www-form-urlencoded"} 21 | 22 | def send(self, content, title=None): 23 | api_url = self.api_url.format_map({"content": content, "title": title}) 24 | with httpx.Client() as client: 25 | client.get(api_url, headers=self.headers) 26 | logger.debug("`bark` send successfully") 27 | 28 | async def send_async(self, content, title=None): 29 | api_url = self.api_url.format_map({"content": content, "title": title}) 30 | async with httpx.AsyncClient() as client: 31 | await client.get(api_url, headers=self.headers) 32 | logger.debug("`bark` send successfully") 33 | -------------------------------------------------------------------------------- /src/use_notify/channels/base.py: -------------------------------------------------------------------------------- 1 | from abc import ABCMeta, abstractmethod 2 | 3 | from usepy import useAdDict 4 | 5 | 6 | class BaseChannel(metaclass=ABCMeta): 7 | def __init__(self, config: dict): 8 | self.config = useAdDict(config) 9 | 10 | @abstractmethod 11 | def send(self, message): 12 | raise NotImplementedError 13 | 14 | @abstractmethod 15 | async def send_async(self, message): 16 | raise NotImplementedError 17 | -------------------------------------------------------------------------------- /src/use_notify/channels/chanify.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | import logging 3 | 4 | import httpx 5 | 6 | from .base import BaseChannel 7 | 8 | logger = logging.getLogger(__name__) 9 | 10 | 11 | class Chanify(BaseChannel): 12 | """chanify 消息通知""" 13 | 14 | @property 15 | def api_url(self): 16 | return f"https://api.chanify.net/v1/sender/{self.config.token}" 17 | 18 | @property 19 | def headers(self): 20 | return {"Content-Type": "application/x-www-form-urlencoded"} 21 | 22 | @staticmethod 23 | def build_api_body(content, title=None): 24 | return {"text": f"{title}\n{content}"} 25 | 26 | def send(self, content, title=None): 27 | api_body = self.build_api_body(content, title) 28 | with httpx.Client() as client: 29 | client.post(self.api_url, data=api_body, headers=self.headers) 30 | logger.debug("`chanify` send successfully") 31 | 32 | async def send_async(self, content, title=None): 33 | api_body = self.build_api_body(content, title) 34 | async with httpx.AsyncClient() as client: 35 | await client.post(self.api_url, data=api_body, headers=self.headers) 36 | logger.debug("`chanify` send successfully") 37 | -------------------------------------------------------------------------------- /src/use_notify/channels/ding.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | import logging 3 | 4 | import httpx 5 | 6 | from .base import BaseChannel 7 | 8 | logger = logging.getLogger(__name__) 9 | 10 | 11 | class Ding(BaseChannel): 12 | """钉钉消息通知 13 | https://developers.dingtalk.com/document/app/custom-robot-access?spm=ding_open_doc.document.0.0.6d9d28e1QcCPII#topic-2026027 14 | """ 15 | 16 | @property 17 | def api_url(self): 18 | return f"https://oapi.dingtalk.com/robot/send?access_token={self.config.token}" 19 | 20 | @property 21 | def headers(self): 22 | return {"Content-Type": "application/json"} 23 | 24 | def build_api_body(self, content, title=None): 25 | title = title or "消息提醒" 26 | return { 27 | "msgtype": "markdown", 28 | "markdown": {"title": title, "text": content}, 29 | "at": {"isAtAll": self.config.at_all}, 30 | } 31 | 32 | def send(self, content, title=None): 33 | api_body = self.build_api_body(content, title) 34 | with httpx.Client() as client: 35 | client.post(self.api_url, json=api_body, headers=self.headers) 36 | logger.debug("`钉钉` send successfully") 37 | 38 | async def send_async(self, content, title=None): 39 | api_body = self.build_api_body(content, title) 40 | async with httpx.AsyncClient() as client: 41 | await client.post(self.api_url, json=api_body, headers=self.headers) 42 | logger.debug("`钉钉` send successfully") 43 | -------------------------------------------------------------------------------- /src/use_notify/channels/email.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | import asyncio 3 | import logging 4 | import smtplib 5 | from email.header import Header 6 | from email.mime.text import MIMEText 7 | from functools import partial 8 | 9 | from .base import BaseChannel 10 | 11 | logger = logging.getLogger(__name__) 12 | 13 | 14 | class Email(BaseChannel): 15 | """邮件消息通知""" 16 | 17 | def __init__(self, config): 18 | super().__init__(config) 19 | self.smtp = smtplib.SMTP_SSL(self.config.server, self.config.port) 20 | self.smtp.connect(self.config.server, self.config.port) 21 | self.smtp.login(self.config.username, self.config.password) 22 | 23 | @staticmethod 24 | def build_message(content, title=None): 25 | message = MIMEText(content, "html", "utf-8") 26 | message["From"] = Header("notify", "utf-8") 27 | subject = title or "消息提醒" 28 | message["Subject"] = Header(subject, "utf-8") 29 | return message.as_string() 30 | 31 | def send(self, content, title=None): 32 | if not self.config.receivers: 33 | logger.error("请先设置接收邮箱") 34 | return 35 | message = self.build_message(content, title) 36 | 37 | self.smtp.sendmail(self.config.sender, self.config.receivers, message) 38 | logger.debug("邮件通知推送成功") 39 | 40 | async def send_async(self, content, title=None): 41 | if not self.config.receivers: 42 | logger.error("请先设置接收邮箱") 43 | return 44 | message = self.build_message(content, title) 45 | 46 | loop = asyncio.get_event_loop() 47 | sendmail_func = partial( 48 | self.smtp.sendmail, self.config.sender, self.config.receivers, message 49 | ) 50 | await loop.run_in_executor(None, sendmail_func) 51 | logger.debug("邮件通知推送成功") 52 | -------------------------------------------------------------------------------- /src/use_notify/channels/pushdeer.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | import logging 3 | 4 | import httpx 5 | 6 | from .base import BaseChannel 7 | 8 | logger = logging.getLogger(__name__) 9 | 10 | 11 | class PushDeer(BaseChannel): 12 | """pushdeer app 消息通知""" 13 | 14 | @property 15 | def api_url(self): 16 | return f"https://api2.pushdeer.com/message/push?pushkey={self.config.token}&text={{text}}" 17 | 18 | @property 19 | def headers(self): 20 | return {"Content-Type": "application/x-www-form-urlencoded"} 21 | 22 | def build_api_body(self, content, title=None): 23 | return self.api_url.format_map({"text": f"{title}\n{content}"}) 24 | 25 | def send(self, content, title=None): 26 | api_url = self.build_api_body(content, title) 27 | with httpx.Client() as client: 28 | client.get(api_url, headers=self.headers) 29 | logger.debug("`pushdeer` send successfully") 30 | 31 | async def send_async(self, content, title=None): 32 | api_url = self.build_api_body(content, title) 33 | async with httpx.AsyncClient() as client: 34 | await client.get(api_url, headers=self.headers) 35 | logger.debug("`pushdeer` send successfully") 36 | -------------------------------------------------------------------------------- /src/use_notify/channels/pushover.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | import logging 3 | 4 | import httpx 5 | 6 | from .base import BaseChannel 7 | 8 | logger = logging.getLogger(__name__) 9 | 10 | 11 | class PushOver(BaseChannel): 12 | """pushover app 消息通知""" 13 | 14 | @property 15 | def api_url(self): 16 | return "https://api.pushover.net/1/messages.json" 17 | 18 | @property 19 | def headers(self): 20 | return {"Content-Type": "application/x-www-form-urlencoded"} 21 | 22 | def build_api_body(self, content, title=None): 23 | return { 24 | "token": self.config.token, 25 | "user": self.config.user, 26 | "title": title, 27 | "message": content, 28 | } 29 | 30 | def send(self, content, title=None): 31 | api_body = self.build_api_body(content, title) 32 | with httpx.Client() as client: 33 | client.post(self.api_url, data=api_body, headers=self.headers) 34 | logger.debug("`pushover` send successfully") 35 | 36 | async def send_async(self, content, title=None): 37 | api_body = self.build_api_body(content, title) 38 | async with httpx.AsyncClient() as client: 39 | await client.post(self.api_url, data=api_body, headers=self.headers) 40 | logger.debug("`pushover` send successfully") 41 | -------------------------------------------------------------------------------- /src/use_notify/channels/wechat.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | import json 3 | import logging 4 | 5 | import httpx 6 | 7 | from .base import BaseChannel 8 | 9 | logger = logging.getLogger(__name__) 10 | 11 | 12 | class WeChat(BaseChannel): 13 | """企业微信消息通知""" 14 | 15 | @property 16 | def api_url(self): 17 | return ( 18 | f"https://qyapi.weixin.qq.com/cgi-bin/webhook/send?key={self.config.token}" 19 | ) 20 | 21 | @property 22 | def headers(self): 23 | return {"Content-Type": "application/json"} 24 | 25 | @staticmethod 26 | def build_api_body(content): 27 | api_body = {"markdown": {"content": content}, "msgtype": "markdown"} 28 | return json.dumps(api_body).encode("utf-8") 29 | 30 | def send(self, content, title=None): 31 | api_body = self.build_api_body(content) 32 | with httpx.Client() as client: 33 | client.post(self.api_url, json=api_body, headers=self.headers) 34 | logger.debug("`WeChat` send successfully") 35 | 36 | async def send_async(self, content, title=None): 37 | api_body = self.build_api_body(content) 38 | async with httpx.AsyncClient() as client: 39 | await client.post(self.api_url, json=api_body, headers=self.headers) 40 | logger.debug("`WeChat` send successfully") 41 | -------------------------------------------------------------------------------- /src/use_notify/notification.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | import asyncio 3 | from typing import List, Optional 4 | 5 | from use_notify import channels as channels_models 6 | 7 | 8 | class Publisher: 9 | """A class that publishes notifications to multiple channels.""" 10 | 11 | def __init__(self, channels: Optional[List[channels_models.BaseChannel]] = None): 12 | if channels is None: 13 | channels = [] 14 | self.channels = channels 15 | 16 | def add(self, *channels): 17 | """ 18 | Add channels to the Publisher. 19 | 20 | Args: 21 | *channels: Variable number of BaseChannel objects. 22 | """ 23 | for channel in channels: 24 | self.channels.append(channel) 25 | 26 | def publish(self, *args, **kwargs): 27 | """ 28 | Publish a notification to all channels. 29 | """ 30 | for channel in self.channels: 31 | channel.send(*args, **kwargs) 32 | 33 | async def publish_async(self, *args, **kwargs): 34 | """ 35 | Publish a notification asynchronously to all channels. 36 | """ 37 | tasks = [channel.send_async(*args, **kwargs) for channel in self.channels] 38 | await asyncio.gather(*tasks) 39 | 40 | 41 | class Notify(Publisher): 42 | """A subclass of Publisher that represents a notification publisher.""" 43 | 44 | @classmethod 45 | def from_settings(cls, settings: dict): 46 | """ 47 | Create a Notify instance from a settings object. 48 | 49 | Args: 50 | settings: A settings object. 51 | Example: 52 | settings = { 53 | ... "BARK": {"token": "your token"}, 54 | ... "DINGTALK": {"access_token": "your access token"}, 55 | ... } 56 | notify = Notify.from_settings(settings) 57 | notify.publish(title="消息标题", content="消息正文") 58 | 59 | Returns: 60 | A Notify instance. 61 | """ 62 | channels = [] 63 | for channel, cfg in settings.items(): 64 | channel_cls = getattr(channels_models, channel.title(), None) 65 | if not channel_cls: 66 | raise ValueError(f"Unknown channel {channel}") 67 | channel = channel_cls(cfg) 68 | channels.append(channel) 69 | return cls(channels) 70 | -------------------------------------------------------------------------------- /tests/test_from_settings.py: -------------------------------------------------------------------------------- 1 | import pytest 2 | 3 | from use_notify import useNotify, useNotifyChannel 4 | 5 | 6 | def test_from_settings(): 7 | settings = { 8 | "bark": {"token": "1"} 9 | } 10 | notify = useNotify.from_settings(settings) 11 | assert len(notify.channels) == 1 12 | assert notify.channels.pop().config.token == "1" 13 | 14 | 15 | def test_two_from_settings(): 16 | settings = { 17 | "bark": {"token": "1"} 18 | } 19 | notify = useNotify.from_settings(settings) 20 | notify.add(useNotifyChannel.Bark({"token": "2"})) 21 | assert len(notify.channels) == 2 22 | 23 | 24 | def test_unknown_from_settings(): 25 | settings = { 26 | "unknown": {"token": "1"} 27 | } 28 | with pytest.raises(ValueError): 29 | useNotify.from_settings(settings) 30 | -------------------------------------------------------------------------------- /tests/test_publisher.py: -------------------------------------------------------------------------------- 1 | import pytest 2 | 3 | from use_notify.notification import Notify, Publisher 4 | 5 | 6 | @pytest.fixture 7 | def publisher(): 8 | return Publisher() 9 | 10 | 11 | @pytest.fixture 12 | def notify(): 13 | return Notify() 14 | 15 | 16 | def test_publisher_add(publisher): 17 | channel1 = MockChannel() 18 | channel2 = MockChannel() 19 | publisher.add(channel1, channel2) 20 | assert len(publisher.channels) == 2 21 | 22 | 23 | def test_publisher_publish(publisher): 24 | channel = MockChannel() 25 | publisher.add(channel) 26 | publisher.publish("Test message") 27 | assert channel.sent_message == "Test message" 28 | 29 | 30 | def test_notify_inherits_publisher_methods(notify): 31 | assert hasattr(notify, "add") 32 | assert hasattr(notify, "publish") 33 | 34 | 35 | class MockChannel: 36 | def __init__(self): 37 | self.sent_message = None 38 | 39 | def send(self, message): 40 | self.sent_message = message 41 | 42 | async def send_async(self, message): 43 | self.sent_message = message 44 | --------------------------------------------------------------------------------