├── .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 |
5 |
6 |
7 |
8 |
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 |
--------------------------------------------------------------------------------