pernox 0.1.0__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- pernox-0.1.0/.github/workflows/release.yml +93 -0
- pernox-0.1.0/.github/workflows/test.yml +21 -0
- pernox-0.1.0/.gitignore +5 -0
- pernox-0.1.0/LICENSE +21 -0
- pernox-0.1.0/PKG-INFO +53 -0
- pernox-0.1.0/README.md +37 -0
- pernox-0.1.0/pyproject.toml +33 -0
- pernox-0.1.0/src/pernox/__init__.py +96 -0
- pernox-0.1.0/src/pernox/py.typed +0 -0
- pernox-0.1.0/tests/test_notify.py +86 -0
|
@@ -0,0 +1,93 @@
|
|
|
1
|
+
# Releases pernox on every merge to main that changes the package: works out the next
|
|
2
|
+
# version from the latest v* tag, builds it, publishes it to PyPI, and then tags the commit.
|
|
3
|
+
#
|
|
4
|
+
# The bump is a patch (0.1.3 -> 0.1.4) unless the merge's commit message says otherwise:
|
|
5
|
+
# put #minor or #major in the pull request's title or description. The first release is
|
|
6
|
+
# 0.1.0.
|
|
7
|
+
#
|
|
8
|
+
# Nothing is committed back to main: the version lives only in the tag, which hatch-vcs
|
|
9
|
+
# reads when building. The tag is pushed after PyPI accepts the release, so a failed
|
|
10
|
+
# publish leaves no tag behind and the next merge tries the same version again.
|
|
11
|
+
#
|
|
12
|
+
# Publishing uses PyPI's trusted publishing, so no PyPI token is stored here: PyPI trusts
|
|
13
|
+
# this workflow, in the "pypi" environment, to upload pernox.
|
|
14
|
+
name: release
|
|
15
|
+
|
|
16
|
+
on:
|
|
17
|
+
push:
|
|
18
|
+
branches: [main]
|
|
19
|
+
paths: [src/**, pyproject.toml, README.md, LICENSE]
|
|
20
|
+
|
|
21
|
+
# One release at a time, in order, so two quick merges don't claim the same version.
|
|
22
|
+
concurrency:
|
|
23
|
+
group: release
|
|
24
|
+
cancel-in-progress: false
|
|
25
|
+
|
|
26
|
+
jobs:
|
|
27
|
+
test:
|
|
28
|
+
uses: ./.github/workflows/test.yml
|
|
29
|
+
|
|
30
|
+
build:
|
|
31
|
+
needs: test
|
|
32
|
+
runs-on: ubuntu-latest
|
|
33
|
+
outputs:
|
|
34
|
+
version: ${{ steps.next.outputs.version }}
|
|
35
|
+
steps:
|
|
36
|
+
- uses: actions/checkout@v7
|
|
37
|
+
with:
|
|
38
|
+
fetch-depth: 0 # every tag, to find the latest version
|
|
39
|
+
- uses: actions/setup-python@v7
|
|
40
|
+
with:
|
|
41
|
+
python-version: "3.14"
|
|
42
|
+
- name: Work out the next version
|
|
43
|
+
id: next
|
|
44
|
+
env:
|
|
45
|
+
MESSAGE: ${{ github.event.head_commit.message }}
|
|
46
|
+
run: |
|
|
47
|
+
latest=$(git tag --list 'v[0-9]*.[0-9]*.[0-9]*' --sort=-version:refname | head -n 1)
|
|
48
|
+
if [ -z "$latest" ]; then
|
|
49
|
+
next=0.1.0
|
|
50
|
+
else
|
|
51
|
+
IFS=. read -r major minor patch <<< "${latest#v}"
|
|
52
|
+
case "$MESSAGE" in
|
|
53
|
+
*"#major"*) next="$((major + 1)).0.0" ;;
|
|
54
|
+
*"#minor"*) next="$major.$((minor + 1)).0" ;;
|
|
55
|
+
*) next="$major.$minor.$((patch + 1))" ;;
|
|
56
|
+
esac
|
|
57
|
+
fi
|
|
58
|
+
echo "Releasing $next (latest was ${latest:-none})"
|
|
59
|
+
echo "version=$next" >> "$GITHUB_OUTPUT"
|
|
60
|
+
# Tagged here only so hatch-vcs builds this version; it's pushed once PyPI has it.
|
|
61
|
+
- run: git tag "v${{ steps.next.outputs.version }}"
|
|
62
|
+
- run: python -m pip install build
|
|
63
|
+
- run: python -m build
|
|
64
|
+
- name: Check the build has the right version
|
|
65
|
+
run: ls dist/ | grep -q "pernox-${{ steps.next.outputs.version }}-py3-none-any.whl"
|
|
66
|
+
- uses: actions/upload-artifact@v7
|
|
67
|
+
with:
|
|
68
|
+
name: dist
|
|
69
|
+
path: dist/
|
|
70
|
+
|
|
71
|
+
publish:
|
|
72
|
+
needs: build
|
|
73
|
+
runs-on: ubuntu-latest
|
|
74
|
+
environment: pypi
|
|
75
|
+
permissions:
|
|
76
|
+
id-token: write
|
|
77
|
+
steps:
|
|
78
|
+
- uses: actions/download-artifact@v8
|
|
79
|
+
with:
|
|
80
|
+
name: dist
|
|
81
|
+
path: dist/
|
|
82
|
+
- uses: pypa/gh-action-pypi-publish@release/v1
|
|
83
|
+
|
|
84
|
+
tag:
|
|
85
|
+
needs: [build, publish]
|
|
86
|
+
runs-on: ubuntu-latest
|
|
87
|
+
permissions:
|
|
88
|
+
contents: write
|
|
89
|
+
steps:
|
|
90
|
+
- uses: actions/checkout@v7
|
|
91
|
+
- run: |
|
|
92
|
+
git tag "v${{ needs.build.outputs.version }}" "$GITHUB_SHA"
|
|
93
|
+
git push origin "v${{ needs.build.outputs.version }}"
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
# Tests every pull request and push. The release workflow runs it too, before publishing.
|
|
2
|
+
name: test
|
|
3
|
+
|
|
4
|
+
on:
|
|
5
|
+
push:
|
|
6
|
+
pull_request:
|
|
7
|
+
workflow_call:
|
|
8
|
+
|
|
9
|
+
jobs:
|
|
10
|
+
test:
|
|
11
|
+
runs-on: ubuntu-latest
|
|
12
|
+
strategy:
|
|
13
|
+
matrix:
|
|
14
|
+
# The oldest Python pernox supports, and the newest.
|
|
15
|
+
python: ["3.10", "3.14"]
|
|
16
|
+
steps:
|
|
17
|
+
- uses: actions/checkout@v7
|
|
18
|
+
- uses: actions/setup-python@v7
|
|
19
|
+
with:
|
|
20
|
+
python-version: ${{ matrix.python }}
|
|
21
|
+
- run: PYTHONPATH=src python -m unittest discover -s tests -v
|
pernox-0.1.0/.gitignore
ADDED
pernox-0.1.0/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Sebastien Williams-Wynn
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
pernox-0.1.0/PKG-INFO
ADDED
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
Metadata-Version: 2.5
|
|
2
|
+
Name: pernox
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: Email yourself from a script running in nox.
|
|
5
|
+
Project-URL: Homepage, https://pernox.dev
|
|
6
|
+
Project-URL: Documentation, https://pernox.dev/docs#notify
|
|
7
|
+
License-Expression: MIT
|
|
8
|
+
License-File: LICENSE
|
|
9
|
+
Keywords: cron,email,notifications,nox,scheduler
|
|
10
|
+
Classifier: Operating System :: OS Independent
|
|
11
|
+
Classifier: Programming Language :: Python :: 3
|
|
12
|
+
Classifier: Topic :: Communications
|
|
13
|
+
Classifier: Typing :: Typed
|
|
14
|
+
Requires-Python: >=3.10
|
|
15
|
+
Description-Content-Type: text/markdown
|
|
16
|
+
|
|
17
|
+
# pernox
|
|
18
|
+
|
|
19
|
+
Email yourself from a script running in [nox](https://pernox.dev).
|
|
20
|
+
|
|
21
|
+
```
|
|
22
|
+
pip install pernox
|
|
23
|
+
```
|
|
24
|
+
|
|
25
|
+
```python
|
|
26
|
+
from pernox import notify
|
|
27
|
+
|
|
28
|
+
notify("3 new flats under £1,500", title="Flat search")
|
|
29
|
+
```
|
|
30
|
+
|
|
31
|
+
In nox, add `pernox` to the script's requirements. There's nothing else to set up: each run
|
|
32
|
+
is given its own token, which stops working when the run ends, and the email goes to your
|
|
33
|
+
nox account's address.
|
|
34
|
+
|
|
35
|
+
Notifications come with nox Pro, and go only to a verified address: verify yours in
|
|
36
|
+
[Settings](https://pernox.dev/settings#profile).
|
|
37
|
+
|
|
38
|
+
Run the script anywhere else and `notify` prints the message instead of sending it, so the
|
|
39
|
+
same script works on your own machine.
|
|
40
|
+
|
|
41
|
+
`notify` returns `True` once the email is sent, and `False` when it was only printed. If nox
|
|
42
|
+
can't send it, it raises `pernox.NotifyError`, which fails the run like any other exception.
|
|
43
|
+
Catch it if a notification failing shouldn't.
|
|
44
|
+
|
|
45
|
+
## Arguments
|
|
46
|
+
|
|
47
|
+
| | |
|
|
48
|
+
|---|---|
|
|
49
|
+
| `message` | The text, up to 1,024 characters. |
|
|
50
|
+
| `title` | The subject line, up to 250 characters. Without one, it's the script's name. |
|
|
51
|
+
| `url` | A link to include. |
|
|
52
|
+
|
|
53
|
+
No dependencies beyond the standard library. Python 3.10 or later. MIT licensed.
|
pernox-0.1.0/README.md
ADDED
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
# pernox
|
|
2
|
+
|
|
3
|
+
Email yourself from a script running in [nox](https://pernox.dev).
|
|
4
|
+
|
|
5
|
+
```
|
|
6
|
+
pip install pernox
|
|
7
|
+
```
|
|
8
|
+
|
|
9
|
+
```python
|
|
10
|
+
from pernox import notify
|
|
11
|
+
|
|
12
|
+
notify("3 new flats under £1,500", title="Flat search")
|
|
13
|
+
```
|
|
14
|
+
|
|
15
|
+
In nox, add `pernox` to the script's requirements. There's nothing else to set up: each run
|
|
16
|
+
is given its own token, which stops working when the run ends, and the email goes to your
|
|
17
|
+
nox account's address.
|
|
18
|
+
|
|
19
|
+
Notifications come with nox Pro, and go only to a verified address: verify yours in
|
|
20
|
+
[Settings](https://pernox.dev/settings#profile).
|
|
21
|
+
|
|
22
|
+
Run the script anywhere else and `notify` prints the message instead of sending it, so the
|
|
23
|
+
same script works on your own machine.
|
|
24
|
+
|
|
25
|
+
`notify` returns `True` once the email is sent, and `False` when it was only printed. If nox
|
|
26
|
+
can't send it, it raises `pernox.NotifyError`, which fails the run like any other exception.
|
|
27
|
+
Catch it if a notification failing shouldn't.
|
|
28
|
+
|
|
29
|
+
## Arguments
|
|
30
|
+
|
|
31
|
+
| | |
|
|
32
|
+
|---|---|
|
|
33
|
+
| `message` | The text, up to 1,024 characters. |
|
|
34
|
+
| `title` | The subject line, up to 250 characters. Without one, it's the script's name. |
|
|
35
|
+
| `url` | A link to include. |
|
|
36
|
+
|
|
37
|
+
No dependencies beyond the standard library. Python 3.10 or later. MIT licensed.
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
[build-system]
|
|
2
|
+
requires = ["hatchling>=1.25", "hatch-vcs>=0.4"]
|
|
3
|
+
build-backend = "hatchling.build"
|
|
4
|
+
|
|
5
|
+
[project]
|
|
6
|
+
name = "pernox"
|
|
7
|
+
# The version is the latest v* git tag, which the release workflow creates on each merge.
|
|
8
|
+
dynamic = ["version"]
|
|
9
|
+
description = "Email yourself from a script running in nox."
|
|
10
|
+
readme = "README.md"
|
|
11
|
+
license = "MIT"
|
|
12
|
+
license-files = ["LICENSE"]
|
|
13
|
+
requires-python = ">=3.10"
|
|
14
|
+
# No dependencies: it only needs the standard library, so it installs instantly in a run.
|
|
15
|
+
dependencies = []
|
|
16
|
+
keywords = ["nox", "notifications", "email", "scheduler", "cron"]
|
|
17
|
+
classifiers = [
|
|
18
|
+
"Programming Language :: Python :: 3",
|
|
19
|
+
"Operating System :: OS Independent",
|
|
20
|
+
"Topic :: Communications",
|
|
21
|
+
"Typing :: Typed",
|
|
22
|
+
]
|
|
23
|
+
|
|
24
|
+
[project.urls]
|
|
25
|
+
Homepage = "https://pernox.dev"
|
|
26
|
+
Documentation = "https://pernox.dev/docs#notify"
|
|
27
|
+
|
|
28
|
+
[tool.hatch.version]
|
|
29
|
+
source = "vcs"
|
|
30
|
+
|
|
31
|
+
[tool.hatch.version.raw-options]
|
|
32
|
+
# Outside a git checkout with tags, as in a bare copy of the source.
|
|
33
|
+
fallback_version = "0.0.0"
|
|
@@ -0,0 +1,96 @@
|
|
|
1
|
+
"""Send yourself a notification from a script running in nox.
|
|
2
|
+
|
|
3
|
+
from pernox import notify
|
|
4
|
+
|
|
5
|
+
notify("3 new flats under £1,500", title="Flat search")
|
|
6
|
+
|
|
7
|
+
Inside nox, each run is given a token (NOX_TOKEN) and nox's address (NOX_API_URL), so
|
|
8
|
+
there's nothing to set up. Anywhere else, notify prints the message instead of sending it,
|
|
9
|
+
so the same script runs on your own machine unchanged.
|
|
10
|
+
"""
|
|
11
|
+
|
|
12
|
+
from __future__ import annotations
|
|
13
|
+
|
|
14
|
+
import json
|
|
15
|
+
import os
|
|
16
|
+
import sys
|
|
17
|
+
import urllib.error
|
|
18
|
+
import urllib.request
|
|
19
|
+
from importlib.metadata import PackageNotFoundError, version
|
|
20
|
+
from typing import Optional
|
|
21
|
+
|
|
22
|
+
__all__ = ["notify", "NotifyError"]
|
|
23
|
+
try:
|
|
24
|
+
__version__ = version("pernox")
|
|
25
|
+
except PackageNotFoundError: # run from the source tree without installing
|
|
26
|
+
__version__ = "0.0.0"
|
|
27
|
+
|
|
28
|
+
_DEFAULT_API_URL = "https://pernox.dev"
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
class NotifyError(Exception):
|
|
32
|
+
"""nox couldn't send the notification. status is the HTTP status, if nox answered."""
|
|
33
|
+
|
|
34
|
+
def __init__(self, message: str, status: Optional[int] = None) -> None:
|
|
35
|
+
super().__init__(message)
|
|
36
|
+
self.status = status
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
def notify(
|
|
40
|
+
message: str,
|
|
41
|
+
title: Optional[str] = None,
|
|
42
|
+
*,
|
|
43
|
+
url: Optional[str] = None,
|
|
44
|
+
timeout: float = 60,
|
|
45
|
+
) -> bool:
|
|
46
|
+
"""Email the script's owner, at their nox account's address.
|
|
47
|
+
|
|
48
|
+
message: the text, up to 1,024 characters.
|
|
49
|
+
title: the subject line, up to 250 characters. Without one, it's the script's name.
|
|
50
|
+
url: a link to include.
|
|
51
|
+
|
|
52
|
+
Returns True once sent, or False if it was printed instead because the script isn't
|
|
53
|
+
running in nox. Raises NotifyError if nox couldn't send it.
|
|
54
|
+
"""
|
|
55
|
+
token = os.environ.get("NOX_TOKEN")
|
|
56
|
+
if not token:
|
|
57
|
+
heading = f"{title}: " if title else ""
|
|
58
|
+
print(f"[pernox] {heading}{message} (not sent: runs outside nox)", file=sys.stderr)
|
|
59
|
+
return False
|
|
60
|
+
|
|
61
|
+
body = {"channel": "email", "message": message}
|
|
62
|
+
if title is not None:
|
|
63
|
+
body["title"] = title
|
|
64
|
+
if url is not None:
|
|
65
|
+
body["url"] = url
|
|
66
|
+
api = os.environ.get("NOX_API_URL", _DEFAULT_API_URL).rstrip("/")
|
|
67
|
+
request = urllib.request.Request(
|
|
68
|
+
api + "/api/v1/notify",
|
|
69
|
+
data=json.dumps(body).encode(),
|
|
70
|
+
method="POST",
|
|
71
|
+
headers={
|
|
72
|
+
"Authorization": f"Bearer {token}",
|
|
73
|
+
"Content-Type": "application/json",
|
|
74
|
+
"User-Agent": f"pernox/{__version__}",
|
|
75
|
+
},
|
|
76
|
+
)
|
|
77
|
+
try:
|
|
78
|
+
with urllib.request.urlopen(request, timeout=timeout) as response:
|
|
79
|
+
response.read()
|
|
80
|
+
except urllib.error.HTTPError as e:
|
|
81
|
+
raise NotifyError(_error_message(e), e.code) from None
|
|
82
|
+
except (urllib.error.URLError, TimeoutError) as e:
|
|
83
|
+
reason = getattr(e, "reason", e)
|
|
84
|
+
raise NotifyError(f"Couldn't reach nox at {api}: {reason}") from None
|
|
85
|
+
return True
|
|
86
|
+
|
|
87
|
+
|
|
88
|
+
def _error_message(e: urllib.error.HTTPError) -> str:
|
|
89
|
+
"""nox explains what went wrong in the body's "error"; fall back to the status."""
|
|
90
|
+
try:
|
|
91
|
+
message = json.loads(e.read())["error"]
|
|
92
|
+
if isinstance(message, str) and message:
|
|
93
|
+
return message
|
|
94
|
+
except (ValueError, KeyError, TypeError, OSError):
|
|
95
|
+
pass
|
|
96
|
+
return f"nox answered {e.code} {e.reason}"
|
|
File without changes
|
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
import io
|
|
2
|
+
import json
|
|
3
|
+
import os
|
|
4
|
+
import threading
|
|
5
|
+
import unittest
|
|
6
|
+
from contextlib import redirect_stderr
|
|
7
|
+
from http.server import BaseHTTPRequestHandler, HTTPServer
|
|
8
|
+
from unittest import mock
|
|
9
|
+
|
|
10
|
+
import pernox
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
class FakeNox(BaseHTTPRequestHandler):
|
|
14
|
+
"""Answers /api/v1/notify like nox: the token "good" works, anything else doesn't."""
|
|
15
|
+
|
|
16
|
+
received = []
|
|
17
|
+
|
|
18
|
+
def do_POST(self):
|
|
19
|
+
body = json.loads(self.rfile.read(int(self.headers["Content-Length"])))
|
|
20
|
+
FakeNox.received.append((self.path, self.headers["Authorization"], body))
|
|
21
|
+
if self.headers["Authorization"] != "Bearer good":
|
|
22
|
+
self.reply(401, {"error": "This token isn't valid."})
|
|
23
|
+
else:
|
|
24
|
+
self.reply(200, {"sent": True, "channel": body["channel"]})
|
|
25
|
+
|
|
26
|
+
def reply(self, status, body):
|
|
27
|
+
data = json.dumps(body).encode()
|
|
28
|
+
self.send_response(status)
|
|
29
|
+
self.send_header("Content-Type", "application/json")
|
|
30
|
+
self.send_header("Content-Length", str(len(data)))
|
|
31
|
+
self.end_headers()
|
|
32
|
+
self.wfile.write(data)
|
|
33
|
+
|
|
34
|
+
def log_message(self, *args):
|
|
35
|
+
pass
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
class NotifyTest(unittest.TestCase):
|
|
39
|
+
@classmethod
|
|
40
|
+
def setUpClass(cls):
|
|
41
|
+
cls.server = HTTPServer(("127.0.0.1", 0), FakeNox)
|
|
42
|
+
threading.Thread(target=cls.server.serve_forever, daemon=True).start()
|
|
43
|
+
cls.url = f"http://127.0.0.1:{cls.server.server_port}"
|
|
44
|
+
|
|
45
|
+
@classmethod
|
|
46
|
+
def tearDownClass(cls):
|
|
47
|
+
cls.server.shutdown()
|
|
48
|
+
|
|
49
|
+
def setUp(self):
|
|
50
|
+
FakeNox.received.clear()
|
|
51
|
+
|
|
52
|
+
def env(self, token):
|
|
53
|
+
return mock.patch.dict(os.environ, {"NOX_TOKEN": token, "NOX_API_URL": self.url + "/"})
|
|
54
|
+
|
|
55
|
+
def test_sends_inside_nox(self):
|
|
56
|
+
with self.env("good"):
|
|
57
|
+
sent = pernox.notify("All done", title="Backup", url="https://example.com")
|
|
58
|
+
self.assertTrue(sent)
|
|
59
|
+
path, auth, body = FakeNox.received[0]
|
|
60
|
+
self.assertEqual(path, "/api/v1/notify")
|
|
61
|
+
self.assertEqual(auth, "Bearer good")
|
|
62
|
+
self.assertEqual(body, {"channel": "email", "message": "All done", "title": "Backup", "url": "https://example.com"})
|
|
63
|
+
|
|
64
|
+
def test_raises_nox_error_message(self):
|
|
65
|
+
with self.env("bad"), self.assertRaises(pernox.NotifyError) as caught:
|
|
66
|
+
pernox.notify("All done")
|
|
67
|
+
self.assertEqual(str(caught.exception), "This token isn't valid.")
|
|
68
|
+
self.assertEqual(caught.exception.status, 401)
|
|
69
|
+
|
|
70
|
+
def test_raises_when_nox_is_unreachable(self):
|
|
71
|
+
with mock.patch.dict(os.environ, {"NOX_TOKEN": "good", "NOX_API_URL": "http://127.0.0.1:9"}):
|
|
72
|
+
with self.assertRaises(pernox.NotifyError) as caught:
|
|
73
|
+
pernox.notify("All done")
|
|
74
|
+
self.assertIn("Couldn't reach nox", str(caught.exception))
|
|
75
|
+
|
|
76
|
+
def test_prints_outside_nox(self):
|
|
77
|
+
env = {k: v for k, v in os.environ.items() if k != "NOX_TOKEN"}
|
|
78
|
+
with mock.patch.dict(os.environ, env, clear=True), redirect_stderr(io.StringIO()) as err:
|
|
79
|
+
sent = pernox.notify("All done", title="Backup")
|
|
80
|
+
self.assertFalse(sent)
|
|
81
|
+
self.assertEqual(FakeNox.received, [])
|
|
82
|
+
self.assertIn("[pernox] Backup: All done (not sent: runs outside nox)", err.getvalue())
|
|
83
|
+
|
|
84
|
+
|
|
85
|
+
if __name__ == "__main__":
|
|
86
|
+
unittest.main()
|