chore(deps): upgrade apprise to 2.0 (#5102)

This commit is contained in:
Sam Dornan
2026-10-04 10:18:38 -04:00
committed by GitHub
parent 3204037797
commit 93c98c71c0
7 changed files with 145 additions and 11 deletions
@@ -118,7 +118,7 @@ _HOST: Final = re.compile(r"\[[0-9a-f:.]+\]|[\w.-]+", re.I)
# A token that is a URL path, which Apprise wants between slashes.
_PATH_TOKEN: Final = "path"
FieldType = Literal["string", "int", "float", "bool", "choice", "list"]
FieldType = Literal["string", "email", "int", "float", "bool", "choice", "list"]
FieldValue = str | int | float | bool | list[str]
# What Apprise logs about the send running in this context: its warnings, and
@@ -420,7 +420,8 @@ def _segment(field: AppriseField, value: FieldValue) -> str:
return f"/{quote(path)}/" if path else "/"
if isinstance(value, list):
return field.delimiter.join(quote(str(item), safe="") for item in value)
return quote(str(value), safe="")
# Apprise reads an address's @ as the URL's own.
return quote(str(value), safe="@" if field.type == "email" else "")
def _fill(
@@ -569,7 +570,9 @@ def _token_pattern(service: AppriseService, key: str) -> str:
if key in _TOKEN_PATTERNS:
return _TOKEN_PATTERNS[key]
field = service.field(key)
return r".+" if field is not None and field.type == "list" else r"[^/@?#]+"
if field is not None and field.type == "list":
return r".+"
return r"[^/?#]+" if field is not None and field.type == "email" else r"[^/@?#]+"
def _template_pattern(service: AppriseService, template: str) -> re.Pattern[str]:
@@ -26,6 +26,7 @@ from .fixtures import make_notification
_DISCORD = {"webhook_id": "1234567890", "webhook_token": "abcdefghijklmnop"}
_JSON = {"host": "hooks.example.com", "path": "romm"}
_SENDGRID = {"apikey": "abc123", "from_email": "[email protected]"}
def _message(**overrides: Any) -> OutboundMessage:
@@ -70,6 +71,16 @@ class TestCatalog:
assert targets is not None
assert (targets.type, targets.required) == ("list", True)
@pytest.mark.parametrize(
"service,key",
[("sendgrid", "from_email"), ("ses", "from_email"), ("resend", "from_addr")],
)
def test_offers_an_email_address(self, service, key):
field = find_service(service).field(key)
assert field is not None
assert (field.type, field.required) == ("email", True)
def test_offers_the_schema_only_when_there_is_a_choice(self):
schema = find_service("ntfy").field("schema")
@@ -154,6 +165,21 @@ class TestBuildUrl:
{"host": "hooks.example.com", "path": "/api/romm"},
"jsons://hooks.example.com/api/romm",
),
(
"sendgrid",
{**_SENDGRID, "targets": ["[email protected]"]},
"sendgrid://abc123:[email protected]/me%40example.com",
),
(
"ses",
{
"from_email": "[email protected]",
"access_key_id": "AKID",
"secret_access_key": "s3cr3t",
"region": "us-east-1",
},
"ses://[email protected]/AKID/s3cr3t/us-east-1",
),
],
)
def test_fills_in_the_template_the_fields_make_up(self, service, fields, url):
@@ -261,6 +287,11 @@ class TestFieldsFromUrl:
"json",
{"host": "hooks.example.com", "path": "api/romm"},
),
(
"sendgrid://abc123:[email protected]/me%40example.com",
"sendgrid",
{**_SENDGRID, "targets": ["[email protected]"]},
),
],
)
def test_reads_a_url_back_into_its_service_and_fields(self, url, service, fields):
@@ -269,6 +300,51 @@ class TestFieldsFromUrl:
assert (found.id, read) == (service, fields)
checked_fields(service, read)
@pytest.mark.parametrize(
"service,fields",
[
("sendgrid", {**_SENDGRID, "targets": ["[email protected]"]}),
("brevo", _SENDGRID),
("postmark", _SENDGRID),
("mailersend", _SENDGRID),
("resend", {"apikey": "abc123", "from_addr": "[email protected]"}),
(
"ses",
{
"from_email": "[email protected]",
"access_key_id": "AKID",
"secret_access_key": "s3cr3t",
"region": "us-east-1",
},
),
(
"octopush",
{
"api_login": "[email protected]",
"api_key": "k3y",
"targets": ["+15551234567"],
},
),
(
"twist",
{"password": "pw", "email": "[email protected]", "targets": ["#romm"]},
),
(
"voipms",
{
"password": "pw",
"email": "[email protected]",
"from_phone": "15551234567",
"targets": ["15557654321"],
},
),
],
)
def test_reads_back_the_url_an_email_field_makes(self, service, fields):
found, read = fields_from_url(build_url(find_service(service), fields))
assert (found.id, read) == (service, checked_fields(service, fields))
@pytest.mark.parametrize(
"url,reason",
[
+1 -1
View File
@@ -5,7 +5,7 @@
export type AppriseFieldSchema = {
key: string;
label: string;
type: 'string' | 'int' | 'float' | 'bool' | 'choice' | 'list';
type: 'string' | 'email' | 'int' | 'float' | 'bool' | 'choice' | 'list';
required: boolean;
private: boolean;
advanced: boolean;
@@ -10,7 +10,7 @@ import {
NOTIFICATION_CHANNEL_URL_MAX_LENGTH,
appriseFieldLabel,
} from "@/v2/utils/notificationChannels";
import { notBlank } from "@/v2/utils/validation";
import { email, notBlank } from "@/v2/utils/validation";
const props = defineProps<{
field: AppriseFieldSchema;
@@ -32,6 +32,11 @@ const hint = computed(() =>
const isNumber = computed(
() => props.field.type === "int" || props.field.type === "float",
);
const inputType = computed(() => {
if (props.field.private) return "password";
if (isNumber.value) return "number";
return props.field.type === "email" ? "email" : "text";
});
function inRange(text: string): true | string {
const { min, max, type } = props.field;
@@ -54,6 +59,9 @@ function inRange(text: string): true | string {
const rules = computed(() => [
...(required.value ? [notBlank()] : []),
...(isNumber.value ? [inRange] : []),
...(props.field.type === "email"
? [(text: string) => text === "" || email(text)]
: []),
]);
const listRules = computed(() =>
required.value
@@ -97,7 +105,7 @@ const listRules = computed(() =>
v-else
:model-value="String(value)"
:label="label"
:type="field.private ? 'password' : isNumber ? 'number' : 'text'"
:type="inputType"
:rules="rules"
:required="required"
:hint="hint"
@@ -178,6 +178,52 @@ describe("NotificationChannelDialog", () => {
wrapper.unmount();
});
it("takes an address in an email field and checks it", async () => {
const sendgrid = makeAppriseService({
id: "sendgrid",
name: "SendGrid",
fields: [
{
...makeAppriseService().fields[3]!,
key: "apikey",
label: "API Key",
required: true,
},
{
...makeAppriseService().fields[1]!,
key: "from_email",
label: "From Email",
type: "email",
required: true,
},
],
});
api.getAppriseServices.mockResolvedValue({ data: [sendgrid] });
api.create.mockResolvedValue({ data: channel({ type: "apprise" }) });
const wrapper = await open(null, { admin: true });
await pick(wrapper, "apprise:sendgrid");
await wrapper.findAll("input.r-text-field__input")[0]!.setValue("Mail");
await input(wrapper, '[type="password"]').setValue("abc123");
const address = input(wrapper, '[type="email"]');
await address.setValue("romm");
await save(wrapper);
expect(api.create).not.toHaveBeenCalled();
expect(wrapper.text()).toContain("Invalid email");
await address.setValue("[email protected]");
await save(wrapper);
expect(api.create).toHaveBeenCalledWith(
expect.objectContaining({
service: "sendgrid",
fields: { apikey: "abc123", from_email: "[email protected]" },
}),
);
wrapper.unmount();
});
const ntfyChannel = () =>
channel({
type: "apprise",
+1 -1
View File
@@ -18,7 +18,7 @@ dependencies = [
"asyncssh ~= 2.24",
"alembic ~= 1.16",
"anyio ~= 4.4",
"apprise ~= 1.13",
"apprise ~= 2.0",
"authlib ~= 1.8.0",
"bcrypt ~= 5.0",
"certifi >= 2024.7.4",
Generated
+5 -4
View File
@@ -154,7 +154,7 @@ wheels = [
[[package]]
name = "apprise"
version = "1.13.1"
version = "2.0.0"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "certifi" },
@@ -164,10 +164,11 @@ dependencies = [
{ name = "requests" },
{ name = "requests-oauthlib" },
{ name = "tzdata", marker = "sys_platform == 'win32'" },
{ name = "urllib3" },
]
sdist = { url = "https://files.pythonhosted.org/packages/81/44/5965c245998c72022297e2b515d9594281a97ed1442b3fa022c2c2324102/apprise-1.13.1.tar.gz", hash = "sha256:e7689dda71aaf739244d6c8690de13cb1361b8d0a79980fb48bb397455ca0bdd", size = 2520230, upload-time = "2026-08-31T02:12:01.472Z" }
sdist = { url = "https://files.pythonhosted.org/packages/38/b0/2f2e9b6d9e52f7530b47ba7a9259ebf2378e3953f445222f54412fb27a68/apprise-2.0.0.tar.gz", hash = "sha256:aeb321737f951860d7cb0a9574159090cbdbcb0f1ba01c6b49c69018380adf8d", size = 3268123, upload-time = "2026-09-26T18:49:55.113Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/45/7d/6852bd3fc2eb48fa432cb22f5df11d1c902137dcecab93e0d8319b8c8e16/apprise-1.13.1-py3-none-any.whl", hash = "sha256:2ded6d00562dd2ae9eb6f1e6cc8d3fb66e06f7ede6efac0ad029031b633780f4", size = 1826431, upload-time = "2026-08-31T02:11:58.263Z" },
{ url = "https://files.pythonhosted.org/packages/df/4e/9d3daa5cd16142b4077fbc3270b146ad965b8f81ffb4b538f781c4a1c858/apprise-2.0.0-py3-none-any.whl", hash = "sha256:945a2500a846b88b3c126a8d6e7c0472f6e3d94d8be52c836d8819f8db50b3d7", size = 2417371, upload-time = "2026-09-26T18:49:52.758Z" },
]
[[package]]
@@ -2492,7 +2493,7 @@ requires-dist = [
{ name = "aiohttp", specifier = "~=3.14" },
{ name = "alembic", specifier = "~=1.16" },
{ name = "anyio", specifier = "~=4.4" },
{ name = "apprise", specifier = "~=1.13" },
{ name = "apprise", specifier = "~=2.0" },
{ name = "asyncssh", specifier = "~=2.24" },
{ name = "authlib", specifier = "~=1.8.0" },
{ name = "bcrypt", specifier = "~=5.0" },