Full Code of caronc/apprise for AI

master aa9988b174dd cached
431 files
5.8 MB
1.5M tokens
2670 symbols
1 requests
Download .txt
Showing preview only (6,160K chars total). Download the full file or copy to clipboard to get everything.
Repository: caronc/apprise
Branch: master
Commit: aa9988b174dd
Files: 431
Total size: 5.8 MB

Directory structure:
gitextract_a_8zomhz/

├── .codecov.yml
├── .github/
│   ├── FUNDING.yml
│   ├── ISSUE_TEMPLATE/
│   │   ├── 1_bug_report.md
│   │   ├── 2_enhancement_request.md
│   │   ├── 3_new-notification-request.md
│   │   ├── 4_question.md
│   │   └── config.yaml
│   ├── PULL_REQUEST_TEMPLATE.md
│   └── workflows/
│       ├── codeql-analysis.yml
│       ├── lint.yml
│       ├── loc-badge.yml
│       ├── pkgbuild.yml
│       └── tests.yml
├── .gitignore
├── .vscode/
│   └── settings.json
├── ACKNOWLEDGEMENTS.md
├── CONTRIBUTING.md
├── LICENSE
├── MANIFEST.in
├── README.md
├── SECURITY.md
├── all-plugin-requirements.txt
├── apprise/
│   ├── __init__.py
│   ├── apprise.py
│   ├── apprise_attachment.py
│   ├── apprise_config.py
│   ├── asset.py
│   ├── assets/
│   │   ├── NotifyXML-1.0.xsd
│   │   └── NotifyXML-1.1.xsd
│   ├── attachment/
│   │   ├── __init__.py
│   │   ├── base.py
│   │   ├── file.py
│   │   ├── http.py
│   │   └── memory.py
│   ├── cli.py
│   ├── common.py
│   ├── compat.py
│   ├── config/
│   │   ├── __init__.py
│   │   ├── base.py
│   │   ├── file.py
│   │   ├── http.py
│   │   └── memory.py
│   ├── conversion.py
│   ├── decorators/
│   │   ├── __init__.py
│   │   ├── base.py
│   │   └── notify.py
│   ├── emojis.py
│   ├── exception.py
│   ├── i18n/
│   │   ├── __init__.py
│   │   └── en/
│   │       └── LC_MESSAGES/
│   │           └── apprise.po
│   ├── locale.py
│   ├── logger.py
│   ├── manager.py
│   ├── manager_attachment.py
│   ├── manager_config.py
│   ├── manager_plugins.py
│   ├── persistent_store.py
│   ├── plugins/
│   │   ├── __init__.py
│   │   ├── africas_talking.py
│   │   ├── apprise_api.py
│   │   ├── aprs.py
│   │   ├── bark.py
│   │   ├── base.py
│   │   ├── bluesky.py
│   │   ├── brevo.py
│   │   ├── bulksms.py
│   │   ├── bulkvs.py
│   │   ├── burstsms.py
│   │   ├── chanify.py
│   │   ├── clickatell.py
│   │   ├── clicksend.py
│   │   ├── custom_form.py
│   │   ├── custom_json.py
│   │   ├── custom_xml.py
│   │   ├── d7networks.py
│   │   ├── dapnet.py
│   │   ├── dbus.py
│   │   ├── dingtalk.py
│   │   ├── discord.py
│   │   ├── dot.py
│   │   ├── email/
│   │   │   ├── __init__.py
│   │   │   ├── base.py
│   │   │   ├── common.py
│   │   │   └── templates.py
│   │   ├── emby.py
│   │   ├── enigma2.py
│   │   ├── fcm/
│   │   │   ├── __init__.py
│   │   │   ├── color.py
│   │   │   ├── common.py
│   │   │   ├── oauth.py
│   │   │   └── priority.py
│   │   ├── feishu.py
│   │   ├── flock.py
│   │   ├── fluxer.py
│   │   ├── fortysixelks.py
│   │   ├── freemobile.py
│   │   ├── glib.py
│   │   ├── gnome.py
│   │   ├── google_chat.py
│   │   ├── gotify.py
│   │   ├── growl.py
│   │   ├── guilded.py
│   │   ├── home_assistant.py
│   │   ├── httpsms.py
│   │   ├── ifttt.py
│   │   ├── irc/
│   │   │   ├── __init__.py
│   │   │   ├── base.py
│   │   │   ├── client.py
│   │   │   ├── protocol.py
│   │   │   ├── state.py
│   │   │   └── templates.py
│   │   ├── jellyfin.py
│   │   ├── join.py
│   │   ├── kavenegar.py
│   │   ├── kodi.py
│   │   ├── kumulos.py
│   │   ├── lametric.py
│   │   ├── lark.py
│   │   ├── line.py
│   │   ├── macosx.py
│   │   ├── mailgun.py
│   │   ├── mastodon.py
│   │   ├── matrix.py
│   │   ├── mattermost.py
│   │   ├── messagebird.py
│   │   ├── misskey.py
│   │   ├── mqtt.py
│   │   ├── msg91.py
│   │   ├── msteams.py
│   │   ├── nextcloud.py
│   │   ├── nextcloudtalk.py
│   │   ├── notica.py
│   │   ├── notifiarr.py
│   │   ├── notificationapi.py
│   │   ├── notifico.py
│   │   ├── ntfy.py
│   │   ├── office365.py
│   │   ├── one_signal.py
│   │   ├── opsgenie.py
│   │   ├── pagerduty.py
│   │   ├── pagertree.py
│   │   ├── parseplatform.py
│   │   ├── plivo.py
│   │   ├── popcorn_notify.py
│   │   ├── prowl.py
│   │   ├── pushbullet.py
│   │   ├── pushdeer.py
│   │   ├── pushed.py
│   │   ├── pushjet.py
│   │   ├── pushme.py
│   │   ├── pushover.py
│   │   ├── pushplus.py
│   │   ├── pushsafer.py
│   │   ├── pushy.py
│   │   ├── qq.py
│   │   ├── reddit.py
│   │   ├── resend.py
│   │   ├── revolt.py
│   │   ├── rocketchat.py
│   │   ├── rsyslog.py
│   │   ├── ryver.py
│   │   ├── sendgrid.py
│   │   ├── sendpulse.py
│   │   ├── serverchan.py
│   │   ├── ses.py
│   │   ├── seven.py
│   │   ├── sfr.py
│   │   ├── signal_api.py
│   │   ├── signl4.py
│   │   ├── simplepush.py
│   │   ├── sinch.py
│   │   ├── slack.py
│   │   ├── smpp.py
│   │   ├── smseagle.py
│   │   ├── smsmanager.py
│   │   ├── smtp2go.py
│   │   ├── sns.py
│   │   ├── sparkpost.py
│   │   ├── spike.py
│   │   ├── splunk.py
│   │   ├── spugpush.py
│   │   ├── streamlabs.py
│   │   ├── synology.py
│   │   ├── syslog.py
│   │   ├── techuluspush.py
│   │   ├── telegram.py
│   │   ├── threema.py
│   │   ├── twilio.py
│   │   ├── twist.py
│   │   ├── twitter.py
│   │   ├── vapid/
│   │   │   ├── __init__.py
│   │   │   └── subscription.py
│   │   ├── viber.py
│   │   ├── voipms.py
│   │   ├── vonage.py
│   │   ├── webexteams.py
│   │   ├── wecombot.py
│   │   ├── whatsapp.py
│   │   ├── windows.py
│   │   ├── workflows.py
│   │   ├── wxpusher.py
│   │   ├── xmpp/
│   │   │   ├── __init__.py
│   │   │   ├── adapter.py
│   │   │   ├── base.py
│   │   │   └── common.py
│   │   └── zulip.py
│   ├── py.typed
│   ├── url.py
│   └── utils/
│       ├── __init__.py
│       ├── base64.py
│       ├── cwe312.py
│       ├── disk.py
│       ├── format.py
│       ├── json.py
│       ├── logic.py
│       ├── module.py
│       ├── parse.py
│       ├── pem.py
│       ├── pgp.py
│       ├── sanitize.py
│       ├── singleton.py
│       ├── socket.py
│       ├── templates.py
│       └── time.py
├── babel.cfg
├── bin/
│   ├── README.md
│   ├── apprise
│   ├── build-rpm.sh
│   ├── checkdone.sh
│   └── test.sh
├── dev-requirements.txt
├── docker-compose.yml
├── packaging/
│   ├── README.md
│   ├── i18n_normalize.sh
│   ├── man/
│   │   ├── apprise.1
│   │   ├── apprise.1.html
│   │   └── apprise.md
│   └── redhat/
│       ├── python-apprise.rpmlintrc.el10
│       ├── python-apprise.rpmlintrc.el9
│       └── python-apprise.spec
├── pyproject.toml
├── requirements.txt
├── setup.py
├── tests/
│   ├── conftest.py
│   ├── docker/
│   │   ├── Dockerfile.el10
│   │   ├── Dockerfile.el9
│   │   ├── Dockerfile.f42
│   │   ├── Dockerfile.py310
│   │   ├── Dockerfile.py311
│   │   ├── Dockerfile.py312
│   │   ├── Dockerfile.py39
│   │   └── Dockerfile.rawhide
│   ├── helpers/
│   │   ├── __init__.py
│   │   ├── asyncio.py
│   │   ├── environment.py
│   │   ├── module.py
│   │   └── rest.py
│   ├── test_api.py
│   ├── test_apprise_asset.py
│   ├── test_apprise_attachments.py
│   ├── test_apprise_cli.py
│   ├── test_apprise_config.py
│   ├── test_apprise_emojis.py
│   ├── test_apprise_helpers.py
│   ├── test_apprise_jsonencoder.py
│   ├── test_apprise_pickle.py
│   ├── test_apprise_translations.py
│   ├── test_apprise_utils.py
│   ├── test_asyncio.py
│   ├── test_attach_base.py
│   ├── test_attach_file.py
│   ├── test_attach_http.py
│   ├── test_attach_memory.py
│   ├── test_compat_py39.py
│   ├── test_config_base.py
│   ├── test_config_file.py
│   ├── test_config_http.py
│   ├── test_config_memory.py
│   ├── test_conversion.py
│   ├── test_decorator_notify.py
│   ├── test_escapes.py
│   ├── test_logger.py
│   ├── test_notification_manager.py
│   ├── test_notify_base.py
│   ├── test_persistent_store.py
│   ├── test_plugin_africas_talking.py
│   ├── test_plugin_apprise_api.py
│   ├── test_plugin_aprs.py
│   ├── test_plugin_bark.py
│   ├── test_plugin_base_formatting.py
│   ├── test_plugin_bluesky.py
│   ├── test_plugin_brevo.py
│   ├── test_plugin_bulksms.py
│   ├── test_plugin_bulkvs.py
│   ├── test_plugin_burstsms.py
│   ├── test_plugin_chanify.py
│   ├── test_plugin_clickatell.py
│   ├── test_plugin_clicksend.py
│   ├── test_plugin_custom_form.py
│   ├── test_plugin_custom_json.py
│   ├── test_plugin_custom_xml.py
│   ├── test_plugin_d7networks.py
│   ├── test_plugin_dapnet.py
│   ├── test_plugin_dbus.py
│   ├── test_plugin_dingtalk.py
│   ├── test_plugin_discord.py
│   ├── test_plugin_dot.py
│   ├── test_plugin_email.py
│   ├── test_plugin_emby.py
│   ├── test_plugin_enigma2.py
│   ├── test_plugin_fcm.py
│   ├── test_plugin_feishu.py
│   ├── test_plugin_flock.py
│   ├── test_plugin_fluxer.py
│   ├── test_plugin_fortysixelks.py
│   ├── test_plugin_freemobile.py
│   ├── test_plugin_glib.py
│   ├── test_plugin_gnome.py
│   ├── test_plugin_google_chat.py
│   ├── test_plugin_gotify.py
│   ├── test_plugin_growl.py
│   ├── test_plugin_guilded.py
│   ├── test_plugin_homeassistant.py
│   ├── test_plugin_httpsms.py
│   ├── test_plugin_ifttt.py
│   ├── test_plugin_irc.py
│   ├── test_plugin_irc_state.py
│   ├── test_plugin_jellyfin.py
│   ├── test_plugin_join.py
│   ├── test_plugin_kavenegar.py
│   ├── test_plugin_kumulos.py
│   ├── test_plugin_lametric.py
│   ├── test_plugin_lark.py
│   ├── test_plugin_line.py
│   ├── test_plugin_macosx.py
│   ├── test_plugin_mailgun.py
│   ├── test_plugin_mastodon.py
│   ├── test_plugin_matrix.py
│   ├── test_plugin_mattermost.py
│   ├── test_plugin_messagebird.py
│   ├── test_plugin_misskey.py
│   ├── test_plugin_mqtt.py
│   ├── test_plugin_msg91.py
│   ├── test_plugin_msteams.py
│   ├── test_plugin_nextcloud.py
│   ├── test_plugin_nextcloudtalk.py
│   ├── test_plugin_notica.py
│   ├── test_plugin_notifiarr.py
│   ├── test_plugin_notificationapi.py
│   ├── test_plugin_notifico.py
│   ├── test_plugin_ntfy.py
│   ├── test_plugin_office365.py
│   ├── test_plugin_onesignal.py
│   ├── test_plugin_opsgenie.py
│   ├── test_plugin_pagerduty.py
│   ├── test_plugin_pagertree.py
│   ├── test_plugin_parse_platform.py
│   ├── test_plugin_plivo.py
│   ├── test_plugin_popcorn_notify.py
│   ├── test_plugin_prowl.py
│   ├── test_plugin_pushbullet.py
│   ├── test_plugin_pushdeer.py
│   ├── test_plugin_pushed.py
│   ├── test_plugin_pushjet.py
│   ├── test_plugin_pushme.py
│   ├── test_plugin_pushover.py
│   ├── test_plugin_pushplus.py
│   ├── test_plugin_pushsafer.py
│   ├── test_plugin_pushy.py
│   ├── test_plugin_qq.py
│   ├── test_plugin_reddit.py
│   ├── test_plugin_resend.py
│   ├── test_plugin_revolt.py
│   ├── test_plugin_rocket_chat.py
│   ├── test_plugin_rsyslog.py
│   ├── test_plugin_ryver.py
│   ├── test_plugin_sendgrid.py
│   ├── test_plugin_sendpulse.py
│   ├── test_plugin_serverchan.py
│   ├── test_plugin_ses.py
│   ├── test_plugin_seven.py
│   ├── test_plugin_sfr.py
│   ├── test_plugin_signal.py
│   ├── test_plugin_signl4.py
│   ├── test_plugin_simplepush.py
│   ├── test_plugin_sinch.py
│   ├── test_plugin_slack.py
│   ├── test_plugin_smpp.py
│   ├── test_plugin_sms_manager.py
│   ├── test_plugin_smseagle.py
│   ├── test_plugin_smtp2go.py
│   ├── test_plugin_sns.py
│   ├── test_plugin_sparkpost.py
│   ├── test_plugin_spike.py
│   ├── test_plugin_splunk.py
│   ├── test_plugin_spugpush.py
│   ├── test_plugin_streamlabs.py
│   ├── test_plugin_synology.py
│   ├── test_plugin_syslog.py
│   ├── test_plugin_techululs_push.py
│   ├── test_plugin_telegram.py
│   ├── test_plugin_threema.py
│   ├── test_plugin_title_maxlen.py
│   ├── test_plugin_twilio.py
│   ├── test_plugin_twist.py
│   ├── test_plugin_twitter.py
│   ├── test_plugin_vapid.py
│   ├── test_plugin_viber.py
│   ├── test_plugin_voipms.py
│   ├── test_plugin_vonage.py
│   ├── test_plugin_webex_teams.py
│   ├── test_plugin_wecombot.py
│   ├── test_plugin_whatsapp.py
│   ├── test_plugin_windows.py
│   ├── test_plugin_workflows.py
│   ├── test_plugin_wxpusher.py
│   ├── test_plugin_xbmc_kodi.py
│   ├── test_plugin_xmpp.py
│   ├── test_plugin_zulip.py
│   ├── test_utils_format.py
│   ├── test_utils_pem.py
│   ├── test_utils_sanitize.py
│   ├── test_utils_socket.py
│   └── var/
│       ├── 01_test_example.html
│       ├── fcm/
│       │   ├── service_account-bad-type.json
│       │   └── service_account.json
│       ├── mime.types
│       └── pgp/
│           ├── corrupt-pub.asc
│           └── valid-pub.asc
├── tox.ini
└── win-requirements.txt

================================================
FILE CONTENTS
================================================

================================================
FILE: .codecov.yml
================================================
codecov:
  require_ci_to_pass: false

comment:
  layout: "diff, flags, files"
  behavior: default
  require_changes: false  # if true: only post the comment if coverage changes

coverage:
  status:
    project:
      default:
        target: auto
        threshold: 1%
    patch:
      default:
        target: auto
        threshold: 1%


================================================
FILE: .github/FUNDING.yml
================================================
github: caronc
custom:
  - 'https://www.paypal.com/donate/?hosted_button_id=CR6YF7KLQWQ5E'


================================================
FILE: .github/ISSUE_TEMPLATE/1_bug_report.md
================================================
---
name: 🐛 Bug Report
about: Report errors and problems in Apprise
title: ''
labels: 'bug'
assignees: ''

---

## Notification Service(s) Impacted
<!-- Example: Discord, Telegram, Pushbullet, etc -->
<!-- If unknown, write "Unknown" and include your failing URL schema below -->

## What happened
<!-- Describe the bug and what you expected to happen -->

## Apprise URL(s) involved (redact secrets)
<!-- Include schema and structure, redact tokens/passwords -->
<!-- Example: discord://webhook_id/webhook_token -->
<!-- Example: mailto://user:****@example.com?to=a@example.com -->

## Steps to reproduce
1.
2.
3.

## Environment
- Apprise version: <!-- apprise --version OR pip show apprise -->
- Python version: <!-- python --version -->
- OS and distribution: <!-- e.g. Ubuntu 24.04, Rocky Linux 9, Windows 11 -->
- Install method: <!-- pip, distro package, docker, source -->
- If using Docker: image/tag:

## Logs (redact secrets)
<!-- Include -vv output if possible -->
```text
paste logs here
```


================================================
FILE: .github/ISSUE_TEMPLATE/2_enhancement_request.md
================================================
---
name: 💡 Enhancement Request
about: Suggest an improvement to Apprise
title: ''
labels: 'enhancement'
assignees: ''

---

## The idea
<!-- What should Apprise do, and why is it valuable? -->

## Use-case
<!-- Real-world scenario, who benefits, and how -->

## Proposed change
<!-- API, CLI, URL format, configuration, behaviours -->

## Compatibility impact
- Would this be a breaking change? Yes / No
- If yes, describe what breaks and any migration path.

## Alternatives considered
<!-- Any other approaches you considered -->

## Documentation impact
- Does appriseit.com require updates for this change? Yes / No
- If yes, please also open (or link) a documentation ticket/PR in apprise-docs.

## Additional context
<!-- Links, screenshots, logs, references -->


================================================
FILE: .github/ISSUE_TEMPLATE/3_new-notification-request.md
================================================
---
name: 📣 New Notification Request
about: Request a new notification service integration
title: ''
labels: ['enhancement', 'new-notification']
assignees: ''

---

## What is the name of the service?
<!-- Name of Service -->

## Proposed Apprise schema (service id)
<!-- Example: foo:// or foos:// -->
<!-- If unsure, leave blank and we will suggest one -->

## Proposed Appriseit service slug
<!-- Example: foo (maps to https://appriseit.com/services/foo/) -->
<!-- If unsure, leave blank and we will suggest one -->

## Provide details that help development
- Homepage:
- Official API docs:
- Authentication method: <!-- API key, OAuth, webhook, etc -->
- Rate limits (if known):
- Message limits (if known): <!-- title/body length, attachments, etc -->
- Attachments supported: Yes / No / Unknown

## Example payload or curl snippet (optional)
<!-- Redact secrets -->
```text
paste example here
```

## Anything else?
<!-- Features you would like supported, URL parameters, batching, attachments, etc -->

## ☝️ Documentation note
If this integration is accepted, it must also include an apprise-docs update so the service page exists on appriseit.com.
If you can contribute docs, open a ticket or PR in: https://github.com/caronc/apprise-docs


================================================
FILE: .github/ISSUE_TEMPLATE/4_question.md
================================================
---
name: ❓ Support Question
about: Ask a question about Apprise (prefer Discussions)
title: ''
labels: 'question'
assignees: ''

---

## Please use Discussions for support questions
https://github.com/caronc/apprise/discussions

If you are filing an issue anyway, include:

## Question
<!-- Ask your question here -->

## Apprise version and environment
- Apprise version:
- Python version:
- OS and distribution:
- Install method:


================================================
FILE: .github/ISSUE_TEMPLATE/config.yaml
================================================
blank_issues_enabled: false
contact_links:
  - name: Documentation (Apprise Docs)
    url: https://appriseit.com
    about: Documentation
  - name: Documentation Source (Apprise Docs)
    url: https://github.com/caronc/apprise-docs
    about: Documentation updates, fixes, and translation work belong here.
  - name: Support and Questions
    url: https://github.com/caronc/apprise/discussions
    about: Please use Discussions for questions and general support.



================================================
FILE: .github/PULL_REQUEST_TEMPLATE.md
================================================
## Description
**Related issue (if applicable):** #<!-- apprise issue number goes here -->

<!--
  -- Have anything else to describe?
  -- Define it here; this helps build the documentation site later
-->

<!-- START OF NEW PLUGIN SECTION
  -- Delete this section if you are not creating a new plugin --

## *ServiceName* Notifications
* **Source**: https://official.website.example.ca
* **Image Support**: Yes / No
* **Message Format**: Plain Text / HTML / Markdown
* **Message Limit**: nn characters

Describe your service here.

## Account Setup
1. Visit the service portal and sign in using your account credentials.
2. Generate and copy your token, key, or credentials.

## Syntax

Valid syntax is as follows:
- `service://{variable}`

## Parameter Breakdown

| Variable  | Required |  Description   |
|-----------|----------|----------------|
| variable1 | Yes      | Your variable1 |
| variable2 | No       | Your variable2 |

## Examples

Sends a simple example:
```bash
apprise -vv -t "Title" -b "Message content" \
    service://token
```

## New Service Completion Status
* [ ] apprise/plugins/--new_plugin_name.py
* [ ] pyproject.toml
    - Update keywords section to identify the new service (alphabetically).
* [ ] README.md
    - Add entry for the new service (quick reference only).
* [ ] packaging/redhat/python-apprise.spec
    - add new service into the `%global common_description`

END OF NEW PLUGIN SECTION -->

<!-- The following must be completed or your PR cannot be merged -->
## Checklist
* [ ] Documentation ticket created (if applicable):** [apprise-docs/##](https://github.com/caronc/apprise-docs/issue/<!--apprise-docs issue number goes here-->)
* [ ] The change is tested and works locally.
* [ ] No commented-out code in this PR.
* [ ] No lint errors (use `tox -e lint` and optionally `tox -e format`).
* [ ] Test coverage added or updated (use `tox -e minimal`).

## Testing
<!-- If your change is testable by others, define how to validate it here -->
Anyone can help test as follows:
```bash
# Create a virtual environment
python3 -m venv apprise

# Change into our new directory
cd apprise

# Activate our virtual environment
source bin/activate

# Install the branch
pip install git+https://github.com/caronc/apprise.git@<this.branch-name>

# If you have cloned the branch and have tox available to you:
tox -e apprise -- -t "Test Title" -b "Test Message" \
    <apprise url related to this change>
```


================================================
FILE: .github/workflows/codeql-analysis.yml
================================================
name: "CodeQL"

on:
  push:
    branches: [ master ]
  pull_request:
    branches: [ master ]
  schedule:
    - cron: '42 15 * * 5'

# Cancel in-progress jobs when pushing to the same branch.
concurrency:
  cancel-in-progress: true
  group: ${{ github.workflow }}-${{ github.ref }}

jobs:
  analyze:
    name: Analyze
    runs-on: ubuntu-latest
    permissions:
      actions: read
      contents: read
      security-events: write

    strategy:
      fail-fast: false
      matrix:
        language: [ 'python' ]

    steps:
    - name: Checkout repository
      uses: actions/checkout@v4

    # Initializes the CodeQL tools for scanning.
    - name: Initialize CodeQL
      uses: github/codeql-action/init@v3
      with:
        languages: ${{ matrix.language }}

    - name: Autobuild
      uses: github/codeql-action/autobuild@v3

    - name: Perform CodeQL Analysis
      uses: github/codeql-action/analyze@v3


================================================
FILE: .github/workflows/lint.yml
================================================
# .github/workflows/lint.yml
name: Run Lint Checks

on:
  push:
    paths:
      - '**.py'
  pull_request:
    paths:
      - '**.py'

jobs:
  lint:
    runs-on: ubuntu-latest

    steps:
      - uses: actions/checkout@v4

      - name: Set up Python
        uses: actions/setup-python@v5
        with:
          python-version: '3.12'

      - name: Install tox
        run: python -m pip install tox

      - name: Run Ruff lint check
        run: tox -e lint


================================================
FILE: .github/workflows/loc-badge.yml
================================================
# LoC = Lines of Code
name: LoC Badge

on:
  # Manual only
  workflow_dispatch:

permissions:
  contents: write
  pull-requests: write

jobs:
  loc-badge:
    runs-on: ubuntu-latest
    steps:
      - name: Checkout
        uses: actions/checkout@v4

      - name: Generate LoC badge (Apprise library only)
        uses: alexispurslane/GHA-LoC-Badge@v2.0.0

        id: badge
        with:
          debug: true
          # Run from the root of the repo so the pattern paths are exact
          directory: ./
          badge: .github/badges/loc.svg
          patterns: "apprise/**/*.py"
          ignore: "__pycache__"

      - name: Print the output
        run: |
          echo "Scanned: ${{ steps.badge.outputs.counted_files }}";
          echo "Line Count: ${{ steps.badge.outputs.total_lines }}";

      - name: Create PR (if changed)
        uses: peter-evans/create-pull-request@v7
        with:
          commit-message: "Update Lines of Code (LoC) badge"
          title: "Update LoC badge"
          body: "Automated update of the Lines of Code badge."
          branch: "automation/loc-badge"
          delete-branch: true
          add-paths: ".github/badges/loc.svg"


================================================
FILE: .github/workflows/pkgbuild.yml
================================================
#
# Verify on CI/GHA that RPM package building works.
#
name: RPM Packaging

on:
  push:
    branches: [main, master, 'release/**']
  pull_request:
    branches: [main, master]
  workflow_dispatch:

jobs:

  build:
    name: Build RPMs (${{ matrix.dist }})
    runs-on: ubuntu-latest

    strategy:
      fail-fast: false
      matrix:
        dist: [el9, el10]

    container:
      image: ghcr.io/caronc/apprise-rpmbuild:${{ matrix.dist }}
      options: --user root

    steps:
      - name: Checkout source
        uses: actions/checkout@v4

      - name: Build RPMs
        run: ./bin/build-rpm.sh
        env:
          APPRISE_DIR: ${{ github.workspace }}
          # Drop RPMs into dist/<dist> inside the workspace
          DIST_DIR: ${{ github.workspace }}/dist/${{ matrix.dist }}

      - name: Show RPMs found for upload
        run: |
          echo "Listing dist/${{ matrix.dist }}/**/*.rpm:"
          find dist/${{ matrix.dist }} -type f -name '*.rpm'

      - name: Upload RPM Artifacts
        uses: actions/upload-artifact@v4
        with:
          name: built-rpms-${{ matrix.dist }}
          path: ./dist/${{ matrix.dist }}
          if-no-files-found: error
          retention-days: 5

      - name: Upload rpmlint config files
        uses: actions/upload-artifact@v4
        with:
          # Upload the specific files needed for verification
          name: rpmlint-config-${{ matrix.dist }}
          path: ./packaging/redhat/python-apprise.rpmlintrc.${{ matrix.dist }}
          retention-days: 5

  verify:
    name: Verify RPMs (${{ matrix.dist }})
    needs: build
    runs-on: ubuntu-latest

    strategy:
      fail-fast: false
      matrix:
        dist: [el9, el10]

    container:
      image: ghcr.io/caronc/apprise-rpmbuild:${{ matrix.dist }}
      options: --user root

    steps:
      - name: Download built RPMs
        uses: actions/download-artifact@v4
        with:
          name: built-rpms-${{ matrix.dist }}
          path: ./dist

      - name: Download rpmlint config files
        uses: actions/download-artifact@v4
        with:
          name: rpmlint-config-${{ matrix.dist }}
          # Download files directly into the correct directory
          path: ./packaging/redhat

      - name: Lint RPMs
        run: |
          set -e
          RC_FILE="./packaging/redhat/python-apprise.rpmlintrc.${{ matrix.dist }}"

          if rpmlint --help 2>&1 | grep -q -- '--rpmlintrc'; then
            echo "Using rpmlint --rpmlintrc with $RC_FILE"
            rpmlint --rpmlintrc "$RC_FILE" ./dist/**/*.rpm
          else
            echo "Using rpmlint v1.x on older distribution"
            rpmlint -f "$RC_FILE" ./dist/**/*.rpm
          fi

      - name: Install and verify RPMs
        run: |
          echo "Installing RPMs from: ./dist/"
          find ./dist -name '*.rpm'
          dnf install -y ./dist/**/*.rpm
          apprise --version

      - name: Check Installed Files
        run: rpm -qlp ./dist/**/*.rpm


================================================
FILE: .github/workflows/tests.yml
================================================
name: Run Tests

on:
  # Run tests on push to main, master, or any release/ branch
  push:
    branches: [main, master, 'release/**']
  # Always test on pull requests targeting main/master
  pull_request:
    branches: [main, master]
  # Allow manual triggering via GitHub UI
  workflow_dispatch:

jobs:
  test:
    name: Python ${{ matrix.python-version }} – ${{ matrix.tox_env }} on ${{ matrix.os }}
    runs-on: ${{ matrix.os || 'ubuntu-latest' }}

    strategy:
      fail-fast: false  # Let all jobs run, even if one fails
      matrix:
        include:
          - python-version: "3.9"
            tox_env: qa
          - python-version: "3.10"
            tox_env: qa
          - python-version: "3.11"
            tox_env: qa
          - python-version: "3.12"
            tox_env: qa

          # Pre-release testing (won't fail entire workflow if this fails)
          - python-version: "3.13-dev"
            tox_env: qa
            continue-on-error: true

          - python-version: "3.14-dev"
            tox_env: qa
            continue-on-error: true

          - python-version: "3.15-dev"
            tox_env: qa
            continue-on-error: true

          # Platform validation only (one version)
          - os: windows-latest
            python-version: "3.12"
            tox_env: qa

          - os: macos-latest
            python-version: "3.12"
            tox_env: qa

          # Minimal test run on latest Python only
          # this verifies Apprise still works when extra libraries are not available
          - python-version: "3.12"
            tox_env: minimal

    steps:
      - uses: actions/checkout@v4

      # Install tox for isolated environment and plugin test orchestration
      - name: Install tox
        run: python -m pip install tox

      # Run tox with the specified environment (qa, minimal, etc.)
      - name: Run tox for ${{ matrix.tox_env }}
        run: tox -e ${{ matrix.tox_env }}

      - name: Upload coverage report
        if: always()
        uses: actions/upload-artifact@v4
        with:
          name: coverage-${{ matrix.os }}-${{ matrix.python-version }}-${{ matrix.tox_env }}
          path: .coverage
          include-hidden-files: true

  codecov:
    name: Upload merged coverage to Codecov
    runs-on: ubuntu-latest
    needs: test  # Waits for all matrix jobs to complete
    if: always()  # Even if a test fails, still attempt to upload what we have

    steps:
      - uses: actions/checkout@v4

      - name: Download all coverage reports
        uses: actions/download-artifact@v4
        with:
          path: coverage-artifacts

      - name: Combine and generate coverage
        run: |
          pip install coverage
      
          # Create a consistent temp dir
          mkdir -p coverage-inputs
      
          # Copy and rename each coverage file to .coverage.job_name
          i=0
          for f in $(find coverage-artifacts -name .coverage); do
            cp "$f" "coverage-inputs/.coverage.$i"
            i=$((i+1))
          done
      
          # Confirm files staged
          ls -alh coverage-inputs
      
          # Combine them all
          coverage combine coverage-inputs
          coverage report
          coverage xml -o coverage.xml

      # Upload merged coverage results to Codecov for visualization
      - name: Upload to Codecov
        uses: codecov/codecov-action@v4
        with:
          token: ${{ secrets.CODECOV_TOKEN }}
          file: coverage.xml
          verbose: false  # Used for debugging only
          fail_ci_if_error: false  # Avoid failing job if Codecov is down
        env:
          CODECOV_PR:  ${{ github.event.pull_request.number }}
          CODECOV_SHA: ${{ github.sha }}


================================================
FILE: .gitignore
================================================
# Byte-compiled / optimized / DLL files
__pycache__/
*.py[cod]
*$py.class

# left-over-files from conflicts/merges
*.orig

# C extensions
*.so

# vi swap files
.*.sw?

# Distribution / packaging
.Python
env/
.venv*
build/
BUILD/
BUILDROOT/
SOURCES/
SRPMS/
develop-eggs/
dist/
downloads/
eggs/
.eggs/
lib64/
parts/
sdist/
*.egg-info/
.installed.cfg
*.egg
.local

# Generated from Docker Instance
.bash_history
.python_history

# Installer logs
pip-log.txt
pip-delete-this-directory.txt

# Unit test / coverage reports
htmlcov/
.tox/
.coverage
.coverage.*
.cache
nosetests.xml
coverage.xml
*,cover
.hypothesis/

# Translations
*.mo
*.pot

# Django stuff:
*.log

# Sphinx documentation
docs/_build/

# PyBuilder
target/

#Ipython Notebook
.ipynb_checkpoints

#PyCharm
.idea

#PyDev (Eclipse)
.project
.pydevproject
.settings

# Others
.DS_Store


================================================
FILE: .vscode/settings.json
================================================
{
  "python.testing.pytestArgs": [
    "tests"
  ],
  "python.linting.enabled": true,
  "python.linting.ruffEnabled": true,
  "python.linting.ruffPath": "ruff",
  "python.formatting.provider": "none",
  "editor.formatOnSave": true,
  "python.testing.cwd": "${workspaceFolder}",
  "python.testing.unittestEnabled": false,
  "python.testing.pytestEnabled": true,
  "python.envFile": "${workspaceFolder}/.env",
  "terminal.integrated.env.linux": {
    "PYTHONPATH": "${workspaceFolder}"
  }
}

================================================
FILE: ACKNOWLEDGEMENTS.md
================================================
# Contributions to the Apprise Project

## Creator & Maintainer

* Chris Caron <lead2gold@gmail.com>

## Contributors

The following users have contributed to this project and their deserved
recognition has been identified here.  If you have contributed and wish
to be acknowledged for it, the syntax is as follows:

```
* [Your name or handle] <[email or website]>
  * [Month Year] - [Brief summary of your contribution]
```

The contributors have been listed in chronological order:
* Wim de With <wf@dewith.io>
  * Dec 2018 - Added Matrix Support

* Hitesh Sondhi <hitesh@cropsly.com>
  * Mar 2019 - Added Flock Support

* Andreas Motl <andreas.motl@panodata.org>
  * Mar 2020 - Fix XMPP Support
  * Oct 2022 - Drop support for Python 2
  * Oct 2022 - Add support for Python 3.11
  * Oct 2022 - Improve efficiency of NotifyEmail

* Joey Espinosa <@particledecay>
  * Apr 3rd 2022 - Added Ntfy Support

* Kate Ward <https://kate.pet>
  * 6th Feb 2024 - Add Revolt Support

* Han Wang <freddie.wanah@gmail.com>
  * Apr 2024 - Refactored test cases

* Toni Wells <@isometimescode>
  * May 2024 - Fixed token length with apprise://


================================================
FILE: CONTRIBUTING.md
================================================
# 🤝 Contributing to Apprise

Thank you for your interest in contributing to Apprise!

We welcome bug reports, feature requests, documentation improvements, and new
notification plugins. Please follow the guidelines below to help us review and
merge your contributions smoothly.

---

## ✅ Quick Checklist Before You Submit

- ✔️ Your code passes all lint checks:
  ```bash
  tox -e lint
  ```

- ✔️ Your changes are covered by tests:
  ```bash
  tox -e qa
  ```

- ✔️ Your code is clean and consistently formatted:
  ```bash
  tox -e format
  ```


- ✔️ You followed the plugin template (if adding a new plugin).
- ✔️ You included inline docstrings and respected the BSD 2-Clause license.
- ✔️ Your commit message is descriptive.

---

## 📦 Local Development Setup

To get started with development:

### 🧰 System Requirements

- Python >= 3.9
- `pip`
- `git`
- Optional: `VS Code` with the Python extension

### 🚀 One-Time Setup

```bash
git clone https://github.com/caronc/apprise.git
cd apprise

# Install all runtime + dev dependencies
pip install .[dev]
```

(Optional, but recommended if actively developing):
```bash
pip install -e .[dev]
```

---

## 🧪 Running Tests

```bash
pytest               # Run all tests
pytest tests/foo.py  # Run a specific test file
```

Run with coverage:
```bash
pytest --cov=apprise --cov-report=term
```

---

## 🧹 Linting & Formatting with ruff

```bash
ruff check apprise tests           # Check linting
ruff check apprise tests --fix     # Auto-fix
ruff format apprise tests          # Format files
```

---

## 🧰 Optional: Using VS Code

1. Open the repo: `code .`
2. Press `Ctrl+Shift+P → Python: Select Interpreter`
3. Choose the same interpreter you used for `pip install .[dev]`
4. Press `Ctrl+Shift+P → Python: Discover Tests`

`.vscode/settings.json` is pre-configured with:

- pytest as the test runner
- ruff for linting
- PYTHONPATH set to project root

No `.venv` is required unless you choose to use one.

---

## 📌 How to Contribute

1. **Fork the repository** and create a new branch.
2. Make your changes.
3. Run the checks listed above.
4. Submit a pull request (PR) to the `main` branch.

GitHub Actions will run tests and lint checks on your PR automatically.

---

## 🧪 Need Help with Testing or Plugins?

See [DEVELOPMENT.md](./DEVELOPMENT.md) for:
- Full setup instructions
- Tox environment descriptions
- RPM testing
- Plugin development guidance

---

## 🙏 Thank You

Your contributions make Apprise better for everyone — thank you!

📝 See [ACKNOWLEDGEMENTS.md](./ACKNOWLEDGEMENTS.md) for a list of contributors.


================================================
FILE: LICENSE
================================================
BSD 2-Clause License

Copyright (c) 2026, Chris Caron <lead2gold@gmail.com>
All rights reserved.

Redistribution and use in source and binary forms, with or without
modification, are permitted provided that the following conditions are met:

1. Redistributions of source code must retain the above copyright notice, this
   list of conditions and the following disclaimer.

2. Redistributions in binary form must reproduce the above copyright notice,
   this list of conditions and the following disclaimer in the documentation
   and/or other materials provided with the distribution.

THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.


================================================
FILE: MANIFEST.in
================================================
include LICENSE
include README.md
include CONTRIBUTING.md
include ACKNOWLEDGEMENTS.md
include SECURITY.md
include pyproject.toml
include tox.ini
include babel.cfg
include requirements.txt
include win-requirements.txt
include dev-requirements.txt
include all-plugin-requirements.txt
include apprise/py.typed
recursive-include tests *
recursive-include packaging *
recursive-include apprise/i18n *.pot
recursive-include apprise/i18n *.po
recursive-include apprise/i18n */LC_MESSAGES/*.po
global-exclude *.pyc
global-exclude *.pyo
global-exclude __pycache__


================================================
FILE: README.md
================================================
![Apprise Logo](https://raw.githubusercontent.com/caronc/apprise/master/apprise/assets/themes/default/apprise-logo.png)

<hr/>

**ap·prise** / *verb*<br/>
To inform or tell (someone). To make one aware of something.
<hr/>

*Apprise* allows you to send a notification to *almost* all of the most popular *notification* services available to us today such as: Telegram, Discord, Slack, Amazon SNS, Gotify, etc.

* One notification library to rule them all.
* A common and intuitive notification syntax.
* Supports the handling of images and attachments (_to the notification services that will accept them_).
* It's incredibly lightweight.
* Amazing response times because all messages sent asynchronously.

Developers who wish to provide a notification service no longer need to research each and every one out there. They no longer need to try to adapt to the new ones that comeout thereafter. They just need to include this one library and then they can immediately gain access to almost all of the notifications services available to us today.

System Administrators and DevOps who wish to send a notification now no longer need to find the right tool for the job. Everything is already wrapped and supported within the `apprise` command line tool (CLI) that ships with this product.

[![Paypal](https://img.shields.io/badge/paypal-donate-green.svg)](https://www.paypal.com/donate/?hosted_button_id=CR6YF7KLQWQ5E)
[![Follow](https://img.shields.io/twitter/follow/l2gnux)](https://twitter.com/l2gnux/)<br/>
[![Discord](https://img.shields.io/discord/558793703356104724.svg?colorB=7289DA&label=Discord&logo=Discord&logoColor=7289DA&style=flat-square)](https://discord.gg/MMPeN2D)
[![Python](https://img.shields.io/pypi/pyversions/apprise.svg?style=flat-square)](https://pypi.org/project/apprise/)
[![Build Status](https://github.com/caronc/apprise/actions/workflows/tests.yml/badge.svg)](https://github.com/caronc/apprise/actions/workflows/tests.yml)
[![Lines of Code](https://raw.githubusercontent.com/caronc/apprise/master/.github/badges/loc.svg)](https://github.com/caronc/apprise/actions/workflows/loc-badge.yml)
[![CodeCov Status](https://codecov.io/github/caronc/apprise/branch/master/graph/badge.svg)](https://codecov.io/github/caronc/apprise)
[![PyPi Downloads](https://img.shields.io/pepy/dt/apprise.svg?style=flat-square)](https://pypi.org/project/apprise/)

# Table of Contents
<!--ts-->
* [Supported Notifications](#supported-notifications)
  * [Productivity Based Notifications](#productivity-based-notifications)
  * [SMS Notifications](#sms-notifications)
  * [Desktop Notifications](#desktop-notifications)
  * [Email Notifications](#email-notifications)
  * [Custom Notifications](#custom-notifications)
* [Installation](#installation)
* [Command Line Usage](#command-line-usage)
  * [Configuration Files](#cli-configuration-files)
  * [File Attachments](#cli-file-attachments)
  * [Loading Custom Notifications/Hooks](#cli-loading-custom-notificationshooks)
  * [Environment Variables](#cli-environment-variables)
* [Developer API Usage](#developer-api-usage)
  * [Configuration Files](#api-configuration-files)
  * [File Attachments](#api-file-attachments)
  * [Loading Custom Notifications/Hooks](#api-loading-custom-notificationshooks)
* [Persistent Storage](#persistent-storage)
* [More Supported Links and Documentation](#want-to-learn-more)
<!--te-->

Visit the [Official Documentation](https://appriseit.com/getting-started/) site for more information on Apprise.

# Supported Notifications

The section identifies all of the services supported by this library. [Check out the wiki for more information on the supported modules here](https://appriseit.com/).

## Productivity Based Notifications

The table below identifies the services this tool supports and some example service urls you need to use in order to take advantage of it. Click on any of the services listed below to get more details on how you can configure Apprise to access them. If you're having trouble constructing your own URL; try our [Apprise URL Builder](https://appriseit.com/tools/url-builder/) out.

| Notification Service | Service ID | Default Port | Example Syntax |
| -------------------- | ---------- | ------------ | -------------- |
| [Apprise API](https://appriseit.com/services/apprise_api/)  | apprise:// or apprises:// | (TCP) 80 or 443 | apprise://hostname/Token
| [AWS SES](https://appriseit.com/services/ses/)  | ses://   | (TCP) 443   | ses://user@domain/AccessKeyID/AccessSecretKey/RegionName<br/>ses://user@domain/AccessKeyID/AccessSecretKey/RegionName/email1/email2/emailN
| [Bark](https://appriseit.com/services/bark/)  | bark://   | (TCP) 80 or 443   | bark://hostname<br />bark://hostname/device_key<br />bark://hostname/device_key1/device_key2/device_keyN<br/>barks://hostname<br />barks://hostname/device_key<br />barks://hostname/device_key1/device_key2/device_keyN
| [BlueSky](https://appriseit.com/services/bluesky/) | bluesky://  | (TCP) 443   | bluesky://Handle:AppPw<br />bluesky://Handle:AppPw/TargetHandle<br />bluesky://Handle:AppPw/TargetHandle1/TargetHandle2/TargetHandleN
| [Brevo](https://appriseit.com/services/brevo/) | brevo://  | (TCP) 443   | brevo://APIToken:FromEmail/<br />brevo://APIToken:FromEmail/ToEmail<br />brevo://APIToken:FromEmail/ToEmail1/ToEmail2/ToEmailN/
| [Chanify](https://appriseit.com/services/chanify/) | chantify://    | (TCP) 443    | chantify://token
| [Discord](https://appriseit.com/services/discord/)  | discord://   | (TCP) 443   | discord://webhook_id/webhook_token<br />discord://avatar@webhook_id/webhook_token
| [Dot.](https://appriseit.com/services/dot/)  | dot:// | (TCP) 443 | dot://apikey@device_id/text/<br />dot://apikey@device_id/image/<br />**Note**: `device_id` is the Quote/0 hardware serial
| [Emby](https://appriseit.com/services/emby/)  | emby:// or embys:// | (TCP) 8096 | emby://user@hostname/<br />emby://user:password@hostname
| [Enigma2](https://appriseit.com/services/enigma2/)  | enigma2:// or enigma2s:// | (TCP) 80 or 443 | enigma2://hostname
| [FCM](https://appriseit.com/services/fcm/) | fcm://    | (TCP) 443    | fcm://project@apikey/DEVICE_ID<br />fcm://project@apikey/#TOPIC<br/>fcm://project@apikey/DEVICE_ID1/#topic1/#topic2/DEVICE_ID2/
| [Feishu](https://appriseit.com/services/feishu/) | feishu://    | (TCP) 443    | feishu://token
| [Flock](https://appriseit.com/services/flock/) | flock://    | (TCP) 443    | flock://token<br/>flock://botname@token<br/>flock://app_token/u:userid<br/>flock://app_token/g:channel_id<br/>flock://app_token/u:userid/g:channel_id
| [Google Chat](https://appriseit.com/services/googlechat/) | gchat://    | (TCP) 443    | gchat://workspace/key/token
| [Gotify](https://appriseit.com/services/gotify/) | gotify:// or gotifys://   | (TCP) 80 or 443    | gotify://hostname/token<br />gotifys://hostname/token?priority=high
| [Growl](https://appriseit.com/services/growl/)  | growl://   | (UDP) 23053   | growl://hostname<br />growl://hostname:portno<br />growl://password@hostname<br />growl://password@hostname:port</br>**Note**: you can also use the get parameter _version_ which can allow the growl request to behave using the older v1.x protocol. An example would look like: growl://hostname?version=1
| [Guilded](https://appriseit.com/services/guilded/)  | guilded://   | (TCP) 443   | guilded://webhook_id/webhook_token<br />guilded://avatar@webhook_id/webhook_token
| [Home Assistant](https://appriseit.com/services/homeassistant/)       | hassio:// or hassios://   | (TCP) 8123 or 443 | hassio://hostname/accesstoken<br />hassio://user@hostname/accesstoken<br />hassio://user:password@hostname:port/accesstoken<br />hassio://hostname/optional/path/accesstoken
| [IFTTT](https://appriseit.com/services/ifttt/) | ifttt://    | (TCP) 443    | ifttt://webhooksID/Event<br />ifttt://webhooksID/Event1/Event2/EventN<br/>ifttt://webhooksID/Event1/?+Key=Value<br/>ifttt://webhooksID/Event1/?-Key=value1
| [IRC](https://appriseit.com/services/irc/) | irc:// or ircs://   | (TCP) 6667 or 6697 | ircs://user:pass@irc.server/@user<br /> ircs://user:pass@irc.server/#channel?join=true&mode=nickserv<br/>ircs://user:pass@znc.server/@user1/@user2/@user3/#channel1
| [Jellyfin](https://appriseit.com/services/jellyfin/)  | jellyfin:// or jellyfins:// | (TCP) 8096 | jellyfin://user@hostname/<br />jellyfins://user:password@hostname
| [Join](https://appriseit.com/services/join/) | join://   | (TCP) 443    | join://apikey/device<br />join://apikey/device1/device2/deviceN/<br />join://apikey/group<br />join://apikey/groupA/groupB/groupN<br />join://apikey/DeviceA/groupA/groupN/DeviceN/
| [KODI](https://appriseit.com/services/kodi/) | kodi:// or kodis://    | (TCP) 8080 or 443   | kodi://hostname<br />kodi://user@hostname<br />kodi://user:password@hostname:port
| [Kumulos](https://appriseit.com/services/kumulos/) | kumulos:// | (TCP) 443 | kumulos://apikey/serverkey
| [LaMetric Time](https://appriseit.com/services/lametric/) | lametric:// | (TCP) 443 | lametric://apikey@device_ipaddr<br/>lametric://apikey@hostname:port<br/>lametric://client_id@client_secret
| [Lark](https://appriseit.com/services/lark/) | lark://  | (TCP) 443   | lark://BotToken
| [Line](https://appriseit.com/services/line/) | line:// | (TCP) 443 | line://Token@User<br/>line://Token/User1/User2/UserN
| [Mailgun](https://appriseit.com/services/mailgun/) | mailgun:// | (TCP) 443 | mailgun://user@hostname/apikey<br />mailgun://user@hostname/apikey/email<br />mailgun://user@hostname/apikey/email1/email2/emailN<br />mailgun://user@hostname/apikey/?name="From%20User"
| [Mastodon](https://appriseit.com/services/mastodon/) | mastodon:// or mastodons://| (TCP) 80 or 443  | mastodon://access_key@hostname<br />mastodon://access_key@hostname/@user<br />mastodon://access_key@hostname/@user1/@user2/@userN
| [Matrix](https://appriseit.com/services/matrix/) | matrix:// or matrixs://  | (TCP) 80 or 443 | matrix://hostname<br />matrix://user@hostname<br />matrixs://user:pass@hostname:port/#room_alias<br />matrixs://user:pass@hostname:port/!room_id<br />matrixs://user:pass@hostname:port/#room_alias/!room_id/#room2<br />matrixs://token@hostname:port/?webhook=matrix<br />matrix://user:token@hostname/?webhook=slack&format=markdown
| [Mattermost](https://appriseit.com/services/mattermost/) | mmost:// or mmosts:// | (TCP) 8065 | mmost://hostname/authkey<br />mmost://hostname:80/authkey<br />mmost://user@hostname:80/authkey<br />mmost://hostname/authkey?channel=channel<br />mmosts://hostname/authkey<br />mmosts://user@hostname/authkey<br />
| [Microsoft Power Automate / Workflows (MSTeams)](https://appriseit.com/services/workflows/) | workflows://  | (TCP) 443   | workflows://WorkflowID/Signature/
| [Microsoft Teams](https://appriseit.com/services/msteams/) | msteams://  | (TCP) 443   | msteams://TokenA/TokenB/TokenC/
| [Misskey](https://appriseit.com/services/misskey/) | misskey:// or misskeys://| (TCP) 80 or 443  | misskey://access_token@hostname
| [MQTT](https://appriseit.com/services/mqtt/) | mqtt://  or mqtts:// | (TCP) 1883 or 8883   | mqtt://hostname/topic<br />mqtt://user@hostname/topic<br />mqtts://user:pass@hostname:9883/topic
| [Nextcloud](https://appriseit.com/services/nextcloud/) | ncloud:// or nclouds:// | (TCP) 80 or 443 | ncloud://adminuser:pass@host/User<br/>nclouds://adminuser:pass@host/User1/User2/UserN
| [NextcloudTalk](https://appriseit.com/services/nextcloudtalk/) | nctalk:// or nctalks:// | (TCP) 80 or 443 | nctalk://user:pass@host/RoomId<br/>nctalks://user:pass@host/RoomId1/RoomId2/RoomIdN
| [Notica](https://appriseit.com/services/notica/) | notica://  | (TCP) 443   | notica://Token/
| [NotificationAPI](https://appriseit.com/services/notificationapi/) | napi://  | (TCP) 443   | napi://ClientID/ClientSecret/Target<br />napi://ClientID/ClientSecret/Target1/Target2/TargetN<br />napi://MessageType@ClientID/ClientSecret/Target
| [Notifiarr](https://appriseit.com/services/notifiarr/) | notifiarr:// | (TCP) 443 | notifiarr://apikey/#channel<br />notifiarr://apikey/#channel1/#channel2/#channeln
| [Notifico](https://appriseit.com/services/notifico/) | notifico://  | (TCP) 443   | notifico://ProjectID/MessageHook/
| [ntfy](https://appriseit.com/services/ntfy/) | ntfy://  | (TCP) 80 or 443   | ntfy://topic/<br/>ntfys://topic/
| [Office 365](https://appriseit.com/services/office365/) | o365://  | (TCP) 443   | o365://TenantID:AccountEmail/ClientID/ClientSecret<br />o365://TenantID:AccountEmail/ClientID/ClientSecret/TargetEmail<br />o365://TenantID:AccountEmail/ClientID/ClientSecret/TargetEmail1/TargetEmail2/TargetEmailN
| [OneSignal](https://appriseit.com/services/onesignal/) | onesignal:// | (TCP) 443 | onesignal://AppID@APIKey/PlayerID<br/>onesignal://TemplateID:AppID@APIKey/UserID<br/>onesignal://AppID@APIKey/#IncludeSegment<br/>onesignal://AppID@APIKey/Email
| [Opsgenie](https://appriseit.com/services/opsgenie/) | opsgenie:// | (TCP) 443 | opsgenie://APIKey<br/>opsgenie://APIKey/UserID<br/>opsgenie://APIKey/#Team<br/>opsgenie://APIKey/\*Schedule<br/>opsgenie://APIKey/^Escalation
| [PagerDuty](https://appriseit.com/services/pagerduty/) | pagerduty:// | (TCP) 443 | pagerduty://IntegrationKey@ApiKey<br/>pagerduty://IntegrationKey@ApiKey/Source/Component
| [PagerTree](https://appriseit.com/services/pagertree/) | pagertree:// | (TCP) 443 | pagertree://integration_id
| [ParsePlatform](https://appriseit.com/services/parseplatform/) | parsep:// or parseps:// | (TCP) 80 or 443 | parsep://AppID:MasterKey@Hostname<br/>parseps://AppID:MasterKey@Hostname
| [PopcornNotify](https://appriseit.com/services/popcornnotify/) | popcorn://  | (TCP) 443   | popcorn://ApiKey/ToPhoneNo<br/>popcorn://ApiKey/ToPhoneNo1/ToPhoneNo2/ToPhoneNoN/<br/>popcorn://ApiKey/ToEmail<br/>popcorn://ApiKey/ToEmail1/ToEmail2/ToEmailN/<br/>popcorn://ApiKey/ToPhoneNo1/ToEmail1/ToPhoneNoN/ToEmailN
| [Prowl](https://appriseit.com/services/prowl/) | prowl://   | (TCP) 443    | prowl://apikey<br />prowl://apikey/providerkey
| [PushBullet](https://appriseit.com/services/pushbullet/) | pbul://    | (TCP) 443    | pbul://accesstoken<br />pbul://accesstoken/#channel<br/>pbul://accesstoken/A_DEVICE_ID<br />pbul://accesstoken/email@address.com<br />pbul://accesstoken/#channel/#channel2/email@address.net/DEVICE
| [Pushjet](https://appriseit.com/services/pushjet/) | pjet:// or pjets:// | (TCP) 80 or 443 | pjet://hostname/secret<br />pjet://hostname:port/secret<br />pjets://secret@hostname/secret<br />pjets://hostname:port/secret
| [Push (Techulus)](https://appriseit.com/services/techulus/) | push://    | (TCP) 443    | push://apikey/
| [Pushed](https://appriseit.com/services/pushed/) | pushed://    | (TCP) 443    | pushed://appkey/appsecret/<br/>pushed://appkey/appsecret/#ChannelAlias<br/>pushed://appkey/appsecret/#ChannelAlias1/#ChannelAlias2/#ChannelAliasN<br/>pushed://appkey/appsecret/@UserPushedID<br/>pushed://appkey/appsecret/@UserPushedID1/@UserPushedID2/@UserPushedIDN
| [PushMe](https://appriseit.com/services/pushme/) | pushme://  | (TCP) 443   | pushme://Token/
| [Pushover](https://appriseit.com/services/pushover/)  | pover://   | (TCP) 443   | pover://user@token<br />pover://user@token/DEVICE<br />pover://user@token/DEVICE1/DEVICE2/DEVICEN<br />**Note**: you must specify both your user_id and token
| [Pushplus](https://appriseit.com/services/pushplus/) | pushplus://  | (TCP) 443   | pushplus://Token
| [PushSafer](https://appriseit.com/services/pushsafer/)  | psafer:// or psafers://  | (TCP) 80 or 443  | psafer://privatekey<br />psafers://privatekey/DEVICE<br />psafer://privatekey/DEVICE1/DEVICE2/DEVICEN
| [Pushy](https://appriseit.com/services/pushy/)  | pushy://  | (TCP) 443  | pushy://apikey/DEVICE<br />pushy://apikey/DEVICE1/DEVICE2/DEVICEN<br />pushy://apikey/TOPIC<br />pushy://apikey/TOPIC1/TOPIC2/TOPICN
| [PushDeer](https://appriseit.com/services/pushdeer/) | pushdeer:// or pushdeers:// | (TCP) 80 or 443 | pushdeer://pushKey<br />pushdeer://hostname/pushKey<br />pushdeer://hostname:port/pushKey
| [QQ Push](https://appriseit.com/services/qq/) | qq://  | (TCP) 443   | qq://Token
| [Reddit](https://appriseit.com/services/reddit/) | reddit:// | (TCP) 443   | reddit://user:password@app_id/app_secret/subreddit<br />reddit://user:password@app_id/app_secret/sub1/sub2/subN
| [Resend](https://appriseit.com/services/resend/) | resend://  | (TCP) 443   | resend://APIToken:FromEmail/<br />resend://APIToken:FromEmail/ToEmail<br />resend://APIToken:FromEmail/ToEmail1/ToEmail2/ToEmailN/
| [Revolt](https://appriseit.com/services/revolt/) | revolt:// | (TCP) 443   |  revolt://bottoken/ChannelID<br />revolt://bottoken/ChannelID1/ChannelID2/ChannelIDN |
| [Rocket.Chat](https://appriseit.com/services/rocketchat/) | rocket:// or rockets://  | (TCP) 80 or 443   | rocket://user:password@hostname/RoomID/Channel<br />rockets://user:password@hostname:443/#Channel1/#Channel1/RoomID<br />rocket://user:password@hostname/#Channel<br />rocket://webhook@hostname<br />rockets://webhook@hostname/@User/#Channel
| [RSyslog](https://appriseit.com/services/rsyslog/) | rsyslog://  | (UDP) 514 | rsyslog://hostname<br />rsyslog://hostname/Facility
| [Ryver](https://appriseit.com/services/ryver/) | ryver://  | (TCP) 443   | ryver://Organization/Token<br />ryver://botname@Organization/Token
| [SendGrid](https://appriseit.com/services/sendgrid/) | sendgrid://  | (TCP) 443   | sendgrid://APIToken:FromEmail/<br />sendgrid://APIToken:FromEmail/ToEmail<br />sendgrid://APIToken:FromEmail/ToEmail1/ToEmail2/ToEmailN/
| [SendPulse](https://appriseit.com/services/sendpulse/) | sendpulse://  | (TCP) 443   | sendpulse://user@host/ClientId/ClientSecret<br />sendpulse://user@host/ClientId/clientSecret/ToEmail<br />sendpulse://user@host/ClientId/ClientSecret/ToEmail1/ToEmail2/ToEmailN/
| [ServerChan](https://appriseit.com/services/serverchan/) | schan://   | (TCP) 443    | schan://sendkey/
| [Signal API](https://appriseit.com/services/signal/) | signal://  or signals:// | (TCP) 80 or 443  | signal://hostname:port/FromPhoneNo<br/>signal://hostname:port/FromPhoneNo/ToPhoneNo<br/>signal://hostname:port/FromPhoneNo/ToPhoneNo1/ToPhoneNo2/ToPhoneNoN/
| [SIGNL4](https://appriseit.com/services/signl4/) | signl4://  | (TCP) 80 or 443  | signl4://hostname
| [SimplePush](https://appriseit.com/services/simplepush/) | spush://   | (TCP) 443    | spush://apikey<br />spush://salt:password@apikey<br />spush://apikey?event=Apprise
| [Slack](https://appriseit.com/services/slack/) | slack://  | (TCP) 443   | slack://TokenA/TokenB/TokenC/<br />slack://TokenA/TokenB/TokenC/Channel<br />slack://botname@TokenA/TokenB/TokenC/Channel<br />slack://user@TokenA/TokenB/TokenC/Channel1/Channel2/ChannelN
| [SMTP2Go](https://appriseit.com/services/smtp2go/) | smtp2go:// | (TCP) 443 | smtp2go://user@hostname/apikey<br />smtp2go://user@hostname/apikey/email<br />smtp2go://user@hostname/apikey/email1/email2/emailN<br />smtp2go://user@hostname/apikey/?name="From%20User"
| [SparkPost](https://appriseit.com/services/sparkpost/) | sparkpost:// | (TCP) 443 | sparkpost://user@hostname/apikey<br />sparkpost://user@hostname/apikey/email<br />sparkpost://user@hostname/apikey/email1/email2/emailN<br />sparkpost://user@hostname/apikey/?name="From%20User"
| [Spike.sh](https://appriseit.com/services/spike/) | spike://  | (TCP) 443   | spike://Token
| [Splunk](https://appriseit.com/services/splunk/) | splunk:// or victorops:/ | (TCP) 443 | splunk://route_key@apikey<br />splunk://route_key@apikey/entity_id
| [Spug Push](https://appriseit.com/services/spugpush/) | spugpush://  | (TCP) 443   | spugpush://Token
| [Streamlabs](https://appriseit.com/services/streamlabs/) | strmlabs:// | (TCP) 443 | strmlabs://AccessToken/<br/>strmlabs://AccessToken/?name=name&identifier=identifier&amount=0&currency=USD
| [Synology Chat](https://appriseit.com/services/synology_chat/) | synology:// or synologys:// |  (TCP) 80 or 443 | synology://hostname/token<br />synology://hostname:port/token
| [Syslog](https://appriseit.com/services/syslog/) | syslog://  | n/a | syslog://<br />syslog://Facility
| [Telegram](https://appriseit.com/services/telegram/) | tgram://  | (TCP) 443   | tgram://bottoken/ChatID<br />tgram://bottoken/ChatID1/ChatID2/ChatIDN
| [Twitter](https://appriseit.com/services/twitter/) | twitter://  | (TCP) 443   | twitter://CKey/CSecret/AKey/ASecret<br/>twitter://user@CKey/CSecret/AKey/ASecret<br/>twitter://CKey/CSecret/AKey/ASecret/User1/User2/User2<br/>twitter://CKey/CSecret/AKey/ASecret?mode=tweet
| [Twist](https://appriseit.com/services/twist/) | twist://  | (TCP) 443   | twist://pasword:login<br/>twist://password:login/#channel<br/>twist://password:login/#team:channel<br/>twist://password:login/#team:channel1/channel2/#team3:channel
| [Vapid (WebPush)](https://appriseit.com/services/vapid/) | vapid://    | (TCP) 443    | vapid://subscriber/target<br/>vapid://subscriber/target?subfile=path&keyfile=path
| [Viber](https://appriseit.com/services/viber/) | viber://    | (TCP) 443    | viber://token/target
| [Webex Teams (Cisco)](https://appriseit.com/services/wxteams/) | wxteams://  | (TCP) 443   | wxteams://Token
| [WeCom Bot](https://appriseit.com/services/wecombot/) | wecombot://  | (TCP) 443   | wecombot://BotKey
| [WhatsApp](https://appriseit.com/services/whatsapp/) | whatsapp://  | (TCP) 443   | whatsapp://AccessToken@FromPhoneID/ToPhoneNo<br/>whatsapp://Template:AccessToken@FromPhoneID/ToPhoneNo
| [WxPusher](https://appriseit.com/services/wxpusher/) | wxpusher://  | (TCP) 443   | wxpusher://AppToken@UserID1/UserID2/UserIDN<br/>wxpusher://AppToken@Topic1/Topic2/Topic3<br/>wxpusher://AppToken@UserID1/Topic1/
| [XBMC](https://appriseit.com/services/xbmc/) | xbmc:// or xbmcs://    | (TCP) 8080 or 443   | xbmc://hostname<br />xbmc://user@hostname<br />xbmc://user:password@hostname:port
| [XMPP](https://appriseit.com/services/xmpp/) | xmpp:// or xmpps://    | (TCP) 5222 or 5223   | xmpp://user:pass@hostname<br />xmpps://user:pass@hostname/jid<br />xmpps://user:pass@hostname/jid1/jid2@example.ca
| [Zulip Chat](https://appriseit.com/services/zulip/) | zulip://  | (TCP) 443   | zulip://botname@Organization/Token<br />zulip://botname@Organization/Token/Stream<br />zulip://botname@Organization/Token/Email

## SMS Notifications

SMS Notifications for the most part do not have a both a `title` and `body`.  They consist of a single `body` which is usually no more then 160 characters in length.  When using Apprise, the `title` and `body` are therefore combined into a single message prior to their transmission.

| Notification Service | Service ID | Default Port | Example Syntax |
| -------------------- | ---------- | ------------ | -------------- |
| [46elks](https://appriseit.com/services/46elks/) | 46elks://  | (TCP) 443   | 46elks://user:password@FromPhoneNo<br/>46elks://user:password@FromPhoneNo/ToPhoneNo<br/>46elks://user:password@FromPhoneNo/ToPhoneNo1/ToPhoneNo2/ToPhoneNoN/
| [Africas Talking](https://appriseit.com/services/africas_talking/) | atalk://  | (TCP) 443   | atalk://AppUser@ApiKey/ToPhoneNo<br/>atalk://AppUser@ApiKey/ToPhoneNo1/ToPhoneNo2/ToPhoneNoN/
| [Automated Packet Reporting System (ARPS)](https://appriseit.com/services/aprs/)  | aprs:// | (TCP) 10152 | aprs://user:pass@callsign<br/>aprs://user:pass@callsign1/callsign2/callsignN
| [AWS SNS](https://appriseit.com/services/sns/)  | sns://   | (TCP) 443   | sns://AccessKeyID/AccessSecretKey/RegionName/+PhoneNo<br/>sns://AccessKeyID/AccessSecretKey/RegionName/+PhoneNo1/+PhoneNo2/+PhoneNoN<br/>sns://AccessKeyID/AccessSecretKey/RegionName/Topic<br/>sns://AccessKeyID/AccessSecretKey/RegionName/Topic1/Topic2/TopicN
| [BulkSMS](https://appriseit.com/services/bulksms/) | bulksms://  | (TCP) 443   | bulksms://user:password@ToPhoneNo<br/>bulksms://User:Password@ToPhoneNo1/ToPhoneNo2/ToPhoneNoN/
| [BulkVS](https://appriseit.com/services/bulkvs/) | bulkvs://  | (TCP) 443   | bulkvs://user:password@FromPhoneNo<br/>bulkvs://user:password@FromPhoneNo/ToPhoneNo<br/>bulkvs://user:password@FromPhoneNo/ToPhoneNo1/ToPhoneNo2/ToPhoneNoN/
| [Burst SMS](https://appriseit.com/services/burstsms/) | burstsms://  | (TCP) 443   | burstsms://ApiKey:ApiSecret@FromPhoneNo/ToPhoneNo<br/>burstsms://ApiKey:ApiSecret@FromPhoneNo/ToPhoneNo1/ToPhoneNo2/ToPhoneNoN/
| [Clickatell](https://appriseit.com/services/clickatell/)                         | clickatell://               | (TCP) 443       | clickatell://ApiKey/ToPhoneNo<br/>clickatell://FromPhoneNo@ApiKey/ToPhoneNo1/ToPhoneNo2/ToPhoneNoN
| [ClickSend](https://appriseit.com/services/clicksend/) | clicksend://  | (TCP) 443   | clicksend://user:pass@PhoneNo<br/>clicksend://user:pass@ToPhoneNo1/ToPhoneNo2/ToPhoneNoN
| [DAPNET](https://appriseit.com/services/dapnet/) | dapnet://  | (TCP) 80   | dapnet://user:pass@callsign<br/>dapnet://user:pass@callsign1/callsign2/callsignN
| [D7 Networks](https://appriseit.com/services/d7networks/) | d7sms://  | (TCP) 443   | d7sms://token@PhoneNo<br/>d7sms://token@ToPhoneNo1/ToPhoneNo2/ToPhoneNoN
| [DingTalk](https://appriseit.com/services/dingtalk/)  | dingtalk://   | (TCP) 443   | dingtalk://token/<br />dingtalk://token/ToPhoneNo<br />dingtalk://token/ToPhoneNo1/ToPhoneNo2/ToPhoneNo1/
| [Free-Mobile](https://appriseit.com/services/freemobile/)  | freemobile://   | (TCP) 443   | freemobile://user@password/
| [httpSMS](https://appriseit.com/services/httpsms/) | httpsms://  | (TCP) 443   | httpsms://ApiKey@FromPhoneNo<br/>httpsms://ApiKey@FromPhoneNo/ToPhoneNo<br/>httpsms://ApiKey@FromPhoneNo/ToPhoneNo1/ToPhoneNo2/ToPhoneNoN/
| [Kavenegar](https://appriseit.com/services/kavenegar/) | kavenegar://  | (TCP) 443   | kavenegar://ApiKey/ToPhoneNo<br/>kavenegar://FromPhoneNo@ApiKey/ToPhoneNo<br/>kavenegar://ApiKey/ToPhoneNo1/ToPhoneNo2/ToPhoneNoN
| [MessageBird](https://appriseit.com/services/messagebird/) | msgbird://  | (TCP) 443   | msgbird://ApiKey/FromPhoneNo<br/>msgbird://ApiKey/FromPhoneNo/ToPhoneNo<br/>msgbird://ApiKey/FromPhoneNo/ToPhoneNo1/ToPhoneNo2/ToPhoneNoN/
| [MSG91](https://appriseit.com/services/msg91/) | msg91://  | (TCP) 443   | msg91://TemplateID@AuthKey/ToPhoneNo<br/>msg91://TemplateID@AuthKey/ToPhoneNo1/ToPhoneNo2/ToPhoneNoN/
| [Plivo](https://appriseit.com/services/plivo/) | plivo://  | (TCP) 443   | plivo://AuthID@Token@FromPhoneNo<br/>plivo://AuthID@Token/FromPhoneNo/ToPhoneNo<br/>plivo://AuthID@Token/FromPhoneNo/ToPhoneNo1/ToPhoneNo2/ToPhoneNoN/
| [Seven](https://appriseit.com/services/seven/)                                   | seven://                    | (TCP) 443   | seven://ApiKey/FromPhoneNo<br/>seven://ApiKey/FromPhoneNo/ToPhoneNo<br/>seven://ApiKey/FromPhoneNo/ToPhoneNo1/ToPhoneNo2/ToPhoneNoN/
| [Société Française du Radiotéléphone (SFR)](https://appriseit.com/services/sfr/) | sfr://   | (TCP) 443    | sfr://user:password>@spaceId/ToPhoneNo<br/>sfr://user:password>@spaceId/ToPhoneNo1/ToPhoneNo2/ToPhoneNoN/
| [Signal API](https://appriseit.com/services/signal/) | signal://  or signals:// | (TCP) 80 or 443  | signal://hostname:port/FromPhoneNo<br/>signal://hostname:port/FromPhoneNo/ToPhoneNo<br/>signal://hostname:port/FromPhoneNo/ToPhoneNo1/ToPhoneNo2/ToPhoneNoN/
| [Sinch](https://appriseit.com/services/sinch/) | sinch://  | (TCP) 443   | sinch://ServicePlanId:ApiToken@FromPhoneNo<br/>sinch://ServicePlanId:ApiToken@FromPhoneNo/ToPhoneNo<br/>sinch://ServicePlanId:ApiToken@FromPhoneNo/ToPhoneNo1/ToPhoneNo2/ToPhoneNoN/<br/>sinch://ServicePlanId:ApiToken@ShortCode/ToPhoneNo<br/>sinch://ServicePlanId:ApiToken@ShortCode/ToPhoneNo1/ToPhoneNo2/ToPhoneNoN/
| [SMPP](https://appriseit.com/services/smpp/)    | smpp:// or smpps://  | (TCP) 443  | smpp://user:password@hostname:port/FromPhoneNo/ToPhoneNo<br/>smpps://user:password@hostname:port/FromPhoneNo/ToPhoneNo1/ToPhoneNo2/ToPhoneNoN
| [SMSEagle](https://appriseit.com/services/smseagle/) | smseagle:// or smseagles:// | (TCP) 80 or 443  | smseagles://hostname:port/ToPhoneNo<br/>smseagles://hostname:port/@ToContact<br/>smseagles://hostname:port/#ToGroup<br/>smseagles://hostname:port/ToPhoneNo1/#ToGroup/@ToContact/
| [SMS Manager](https://appriseit.com/services/sms_manager/) | smsmgr://  | (TCP) 443   | smsmgr://ApiKey@ToPhoneNo<br/>smsmgr://ApiKey@ToPhoneNo1/ToPhoneNo2/ToPhoneNoN/
| [Threema Gateway](https://appriseit.com/services/threema/) | threema://  | (TCP) 443   | threema://GatewayID@secret/ToPhoneNo<br/>threema://GatewayID@secret/ToEmail<br/>threema://GatewayID@secret/ToThreemaID/<br/>threema://GatewayID@secret/ToEmail/ToThreemaID/ToPhoneNo/...
| [Twilio](https://appriseit.com/services/twilio/) | twilio://  | (TCP) 443   | twilio://AccountSid:AuthToken@FromPhoneNo<br/>twilio://AccountSid:AuthToken@FromPhoneNo/ToPhoneNo<br/>twilio://AccountSid:AuthToken@FromPhoneNo/ToPhoneNo1/ToPhoneNo2/ToPhoneNoN/<br/>twilio://AccountSid:AuthToken@FromPhoneNo/ToPhoneNo?apikey=Key<br/>twilio://AccountSid:AuthToken@ShortCode/ToPhoneNo<br/>twilio://AccountSid:AuthToken@ShortCode/ToPhoneNo1/ToPhoneNo2/ToPhoneNoN/<br/>twilio://AccountSid:AuthToken@FromPhoneNo/ToPhoneNo?method=call<br/>twilio://AccountSid:AuthToken@FromPhoneNo/ToPhoneNo1/ToPhoneNo2/ToPhoneNoN?method=call
| [Voipms](https://appriseit.com/services/voipms/) | voipms://  | (TCP) 443   | voipms://password:email/FromPhoneNo<br/>voipms://password:email/FromPhoneNo/ToPhoneNo<br/>voipms://password:email/FromPhoneNo/ToPhoneNo1/ToPhoneNo2/ToPhoneNoN/
| [Vonage](https://appriseit.com/services/vonage/) (formerly Nexmo) | vonage://  | (TCP) 443   | vonage://ApiKey:ApiSecret@FromPhoneNo<br/>vonage://ApiKey:ApiSecret@FromPhoneNo/ToPhoneNo<br/>vonage://ApiKey:ApiSecret@FromPhoneNo/ToPhoneNo1/ToPhoneNo2/ToPhoneNoN/

## Desktop Notifications

| Notification Service | Service ID | Default Port | Example Syntax |
| -------------------- | ---------- | ------------ | -------------- |
| [Linux DBus Notifications](https://appriseit.com/services/dbus/)  | dbus://<br />qt://<br />glib://<br />kde://  | n/a  | dbus://<br />qt://<br />glib://<br />kde://
| [Linux Gnome Notifications](https://appriseit.com/services/gnome/) | gnome://    |        n/a          | gnome://
| [MacOS X Notifications](https://appriseit.com/services/macosx/) | macosx://    |        n/a          | macosx://
| [Windows Notifications](https://appriseit.com/services/windows/) | windows://    |        n/a          | windows://

## Email Notifications

| Service ID | Default Port | Example Syntax |
| ---------- | ------------ | -------------- |
| [mailto://](https://appriseit.com/services/email/)  |  (TCP) 25    | mailto://userid:pass@domain.com<br />mailto://domain.com?user=userid&pass=password<br/>mailto://domain.com:2525?user=userid&pass=password<br />mailto://user@gmail.com&pass=password<br />mailto://mySendingUsername:mySendingPassword@example.com?to=receivingAddress@example.com<br />mailto://userid:password@example.com?smtp=mail.example.com&from=noreply@example.com&name=no%20reply
| [mailtos://](https://appriseit.com/services/email/) |  (TCP) 587   | mailtos://userid:pass@domain.com<br />mailtos://domain.com?user=userid&pass=password<br/>mailtos://domain.com:465?user=userid&pass=password<br />mailtos://user@hotmail.com&pass=password<br />mailtos://mySendingUsername:mySendingPassword@example.com?to=receivingAddress@example.com<br />mailtos://userid:password@example.com?smtp=mail.example.com&from=noreply@example.com&name=no%20reply

Apprise have some email services built right into it (such as yahoo, fastmail, hotmail, gmail, etc) that greatly simplify the mailto:// service.  See more details [here](https://appriseit.com/services/email/).

## Custom Notifications

| Post Method          | Service ID | Default Port | Example Syntax |
| -------------------- | ---------- | ------------ | -------------- |
| [Form](https://appriseit.com/services/form/)       | form:// or forms://   | (TCP) 80 or 443 | form://hostname<br />form://user@hostname<br />form://user:password@hostname:port<br />form://hostname/a/path/to/post/to
| [JSON](https://appriseit.com/services/json/)       | json:// or jsons://   | (TCP) 80 or 443 | json://hostname<br />json://user@hostname<br />json://user:password@hostname:port<br />json://hostname/a/path/to/post/to
| [XML](https://appriseit.com/services/xml/)         | xml:// or xmls://   | (TCP) 80 or 443 | xml://hostname<br />xml://user@hostname<br />xml://user:password@hostname:port<br />xml://hostname/a/path/to/post/to

# Installation

The easiest way is to install Apprise from PyPI:
```bash
pip install apprise
```

Apprise is also packaged as an RPM and available through [EPEL](https://docs.fedoraproject.org/en-US/epel/) supporting CentOS, Redhat, Rocky, Oracle Linux, etc.
```bash
# Follow instructions on https://docs.fedoraproject.org/en-US/epel
# to get your system connected up to EPEL and then:
# Redhat/CentOS 7.x users
yum install apprise

# Redhat/Rocky Linux 8.x+ and/or Fedora Users
dnf install apprise
```

You can also check out the [Graphical version of Apprise](https://github.com/caronc/apprise-api) to centralize your configuration and notifications through a manageable webpage.

# Command Line Usage

A small command line interface (CLI) tool is also provided with this package called *apprise*. If you know the server urls you wish to notify, you can simply provide them all on the command line and send your notifications that way:
```bash
# Send a notification to as many servers as you want
# as you can easily chain one after another (the -vv provides some
# additional verbosity to help let you know what is going on):
apprise -vv -t 'my title' -b 'my notification body' \
   'mailto://myemail:mypass@gmail.com' \
   'pbul://o.gn5kj6nfhv736I7jC3cj3QLRiyhgl98b'

# If you don't specify a --body (-b) then stdin is used allowing
# you to use the tool as part of your every day administration:
cat /proc/cpuinfo | apprise -vv -t 'cpu info' \
   'mailto://myemail:mypass@gmail.com'

# The title field is totally optional
uptime | apprise -vv \
   'discord:///4174216298/JHMHI8qBe7bk2ZwO5U711o3dV_js'
```

## CLI Configuration Files

No one wants to put their credentials out for everyone to see on the command line.  No problem *apprise* also supports configuration files.  It can handle both a specific YAML format or a very simple TEXT format. You can also pull these configuration files via an HTTP query too! Read more about the expected structure of the configuration files [here](https://appriseit.com/config/).

```bash
# By default if no url or configuration is specified apprise will attempt to load
# configuration files (if present) from:
#  ~/.apprise
#  ~/.apprise.yaml
#  ~/.config/apprise.conf
#  ~/.config/apprise.yaml
#  /etc/apprise.conf
#  /etc/apprise.yaml

# Also a subdirectory handling allows you to leverage plugins
#  ~/.apprise/apprise
#  ~/.apprise/apprise.yaml
#  ~/.config/apprise/apprise.conf
#  ~/.config/apprise/apprise.yaml
#  /etc/apprise/apprise.yaml
#  /etc/apprise/apprise.conf

# Windows users can store their default configuration files here:
#  %APPDATA%/Apprise/apprise.conf
#  %APPDATA%/Apprise/apprise.yaml
#  %LOCALAPPDATA%/Apprise/apprise.conf
#  %LOCALAPPDATA%/Apprise/apprise.yaml
#  %ALLUSERSPROFILE%\Apprise\apprise.conf
#  %ALLUSERSPROFILE%\Apprise\apprise.yaml
#  %PROGRAMFILES%\Apprise\apprise.conf
#  %PROGRAMFILES%\Apprise\apprise.yaml
#  %COMMONPROGRAMFILES%\Apprise\apprise.conf
#  %COMMONPROGRAMFILES%\Apprise\apprise.yaml

# The configuration files specified above can also be identified with a `.yml`
# extension or even just entirely removing the `.conf` extension altogether.

# If you loaded one of those files, your command line gets really easy:
apprise -vv -t 'my title' -b 'my notification body'

# If you want to deviate from the default paths or specify more than one,
# just specify them using the --config switch:
apprise -vv -t 'my title' -b 'my notification body' \
   --config=/path/to/my/config.yml

# Got lots of configuration locations? No problem, you can specify them all:
# Apprise can even fetch the configuration from over a network!
apprise -vv -t 'my title' -b 'my notification body' \
   --config=/path/to/my/config.yml \
   --config=https://localhost/my/apprise/config
```

## CLI Tagging Support

Apprise allows you to tag your services in your configuration to organize them (e.g., `family`, `devops`, `critical`). You can then filter which services to notify using the `--tag` (`-g`) switch.

It is important to understand how Apprise handles multiple tags:

* **OR Logic (Union)**: To notify services that have *either* Tag A **OR** Tag B, specify the `-g` switch multiple times.
* **AND Logic (Intersection)**: To notify services that have *both* Tag A **AND** Tag B, separate the tags with a comma.

```bash
# OR Logic: Notify any service tagged 'devops' OR 'admin'
apprise -vv -t "Union Test" \
   --config=~/apprise.yml \
   -g devops -g admin

# AND Logic: Notify only services tagged with BOTH 'devops' AND 'critical'
apprise -vv -t "Intersection Test" \
   --config=~/apprise.yml \
   -g devops,critical

## CLI File Attachments

Apprise also supports file attachments too! Specify as many attachments to a notification as you want.
```bash
# Send a funny image you found on the internet to a colleague:
apprise -vv --title 'Agile Joke' \
        --body 'Did you see this one yet?' \
        --attach https://i.redd.it/my2t4d2fx0u31.jpg \
        'mailto://myemail:mypass@gmail.com'

# Easily send an update from a critical server to your dev team
apprise -vv --title 'system crash' \
        --body 'I do not think Jim fixed the bug; see attached...' \
        --attach /var/log/myprogram.log \
        --attach /var/debug/core.2345 \
        --tag devteam
```

## CLI Loading Custom Notifications/Hooks

To create your own custom `schema://` hook so that you can trigger your own custom code,
simply include the `@notify` decorator to wrap your function.
```python
from apprise.decorators import notify
#
# The below assumes you want to catch foobar:// calls:
#
@notify(on="foobar", name="My Custom Foobar Plugin")
def my_custom_notification_wrapper(body, title, notify_type, *args, **kwargs):
    """My custom notification function that triggers on all foobar:// calls
    """
    # Write all of your code here... as an example...
    print("{}: {} - {}".format(notify_type.upper(), title, body))

    # Returning True/False is a way to relay your status back to Apprise.
    # Returning nothing (None by default) is always interpreted as a Success
```

Once you've defined your custom hook, you just need to tell Apprise where it is at runtime.
```bash
# By default if no plugin path is specified apprise will attempt to load
# all plugin files (if present) from the following directory paths:
#  ~/.apprise/plugins
#  ~/.config/apprise/plugins
#  /var/lib/apprise/plugins

# Windows users can store their default plugin files in these directories:
#  %APPDATA%/Apprise/plugins
#  %LOCALAPPDATA%/Apprise/plugins
#  %ALLUSERSPROFILE%\Apprise\plugins
#  %PROGRAMFILES%\Apprise\plugins
#  %COMMONPROGRAMFILES%\Apprise\plugins

# If you placed your plugin file within one of the directories already defined
# above, then your call simply needs to look like:
apprise -vv --title 'custom override' \
        --body 'the body of my message' \
        foobar:\\

# However you can override the path like so
apprise -vv --title 'custom override' \
        --body 'the body of my message' \
        --plugin-path /path/to/my/plugin.py \
        foobar:\\
```

You can read more about creating your own custom notifications and/or hooks [here](https://appriseit.com/library/extending/decorator/).

## CLI Environment Variables

Those using the Command Line Interface (CLI) can also leverage environment variables to pre-set the default settings:

| Variable                | Description       |
|------------------------ | ----------------- |
| `APPRISE_URLS`          |  Specify the default URLs to notify IF none are otherwise specified on the command line explicitly. If the `--config` (`-c`) is specified, then this will overrides any reference to this variable. Use white space and/or a comma (`,`) to delimit multiple entries.
|  `APPRISE_CONFIG_PATH`  | Explicitly specify the config search path to use (overriding the default). The path(s) defined here must point to the absolute filename to open/reference. Use a semi-colon (`;`), line-feed (`\n`), and/or carriage return (`\r`) to delimit multiple entries.
|  `APPRISE_PLUGIN_PATH`  | Explicitly specify the custom plugin search path to use (overriding the default). Use a semi-colon (`;`), line-feed (`\n`), and/or carriage return (`\r`) to delimit multiple entries.
|  `APPRISE_STORAGE_PATH` | Explicitly specify the persistent storage path to use (overriding the default).

# Developer API Usage

To send a notification from within your python application, just do the following:
```python
import apprise

# Create an Apprise instance
apobj = apprise.Apprise()

# Add all of the notification services by their server url.
# A sample email notification:
apobj.add('mailto://myuserid:mypass@gmail.com')

# A sample pushbullet notification
apobj.add('pbul://o.gn5kj6nfhv736I7jC3cj3QLRiyhgl98b')

# Then notify these services any time you desire. The below would
# notify all of the services loaded into our Apprise object.
apobj.notify(
    body='what a great notification service!',
    title='my notification title',
)
```

## API Configuration Files

Developers need access to configuration files too. The good news is their use just involves declaring another object (called *AppriseConfig*) that the *Apprise* object can ingest.  You can also freely mix and match config and notification entries as often as you wish! You can read more about the expected structure of the configuration files [here](https://appriseit.com/getting-started/configuration/).
```python
import apprise

# Create an Apprise instance
apobj = apprise.Apprise()

# Create an Config instance
config = apprise.AppriseConfig()

# Add a configuration source:
config.add('/path/to/my/config.yml')

# Add another...
config.add('https://myserver:8080/path/to/config')

# Make sure to add our config into our apprise object
apobj.add(config)

# You can mix and match; add an entry directly if you want too
# In this entry we associate the 'admin' tag with our notification
apobj.add('mailto://myuser:mypass@hotmail.com', tag='admin')

# Then notify these services any time you desire. The below would
# notify all of the services that have not been bound to any specific
# tag.
apobj.notify(
    body='what a great notification service!',
    title='my notification title',
)

# Tagging allows you to specifically target only specific notification
# services you've loaded:
apobj.notify(
    body='send a notification to our admin group',
    title='Attention Admins',
    # notify any services tagged with the 'admin' tag
    tag='admin',
)

# If you want to notify absolutely everything (regardless of whether
# it's been tagged or not), just use the reserved tag of 'all':
apobj.notify(
    body='send a notification to our admin group',
    title='Attention Admins',
    # notify absolutely everything loaded, regardless on whether
    # it has a tag associated with it or not:
    tag='all',
)
```

## API File Attachments

Attachments are very easy to send using the Apprise API:
```python
import apprise

# Create an Apprise instance
apobj = apprise.Apprise()

# Add at least one service you want to notify
apobj.add('mailto://myuser:mypass@hotmail.com')

# Then send your attachment.
apobj.notify(
    title='A great photo of our family',
    body='The flash caused Jane to close her eyes! hah! :)',
    attach='/local/path/to/my/DSC_003.jpg',
)

# Send a web based attachment too! In the below example, we connect to a home
# security camera and send a live image to an email. By default remote web
# content is cached, but for a security camera we might want to call notify
# again later in our code, so we want our last image retrieved to expire(in
# this case after 3 seconds).
apobj.notify(
    title='Latest security image',
    attach='http://admin:password@hikvision-cam01/ISAPI/Streaming/channels/101/picture?cache=3'
)
```

To send more than one attachment, just use a list, set, or tuple instead:
```python
import apprise

# Create an Apprise instance
apobj = apprise.Apprise()

# Add at least one service you want to notify
apobj.add('mailto://myuser:mypass@hotmail.com')

# Now add all of the entries we're interested in:
attach = (
    # ?name= allows us to rename the actual jpeg as found on the site
    # to be another name when sent to our receipient(s)
    'https://i.redd.it/my2t4d2fx0u31.jpg?name=FlyingToMars.jpg',

    # Now add another:
    '/path/to/funny/joke.gif',
)

# Send your multiple attachments with a single notify call:
apobj.notify(
    title='Some good jokes.',
    body='Hey guys, check out these!',
    attach=attach,
)
```

## API Loading Custom Notifications/Hooks

By default, no custom plugins are loaded at all for those building from within the Apprise API.
It's at the developers discretion to load custom modules. But should you choose to do so, it's as easy
as including the path reference in the `AppriseAsset()` object prior to the initialization of your `Apprise()`
instance.

For example:
```python
from apprise import Apprise
from apprise import AppriseAsset

# Prepare your Asset object so that you can enable the custom plugins to
# be loaded for your instance of Apprise...
asset = AppriseAsset(plugin_paths="/path/to/scan")

# OR You can also generate scan more then one file too:
asset = AppriseAsset(
    plugin_paths=[
        # Iterate over all python libraries found in the root of the
        # specified path. This is NOT a recursive (directory) scan; only
        # the first level is parsed. HOWEVER, if a directory containing
        # an __init__.py is found, it will be included in the load.
        "/dir/containing/many/python/libraries",

        # An absolute path to a plugin.py to exclusively load
        "/path/to/plugin.py",

        # if you point to a directory that has an __init__.py file found in
        # it, then only that file is loaded (it's similar to point to a
        # absolute .py file. Hence, there is no (level 1) scanning at all
        # within the directory specified.
        "/path/to/dir/library"
    ]
)

# Now that we've got our asset, we just work with our Apprise object as we
# normally do
aobj = Apprise(asset=asset)

# If our new custom `foobar://` library was loaded (presuming we prepared
# one like in the examples above).  then you would be able to safely add it
# into Apprise at this point
aobj.add('foobar://')

# Send our notification out through our foobar://
aobj.notify("test")
```

You can read more about creating your own custom notifications and/or hooks [here](https://appriseit.com/library/extending/decorator/).

# Persistent Storage

Persistent storage allows Apprise to cache re-occurring actions optionaly to disk. This can greatly reduce the overhead used to send a notification.

There are 3 Persistent Storage operational states Apprise can operate using:
1. `auto`:  Flush gathered cache information to the filesystem on demand.  This option is incredibly light weight.  This is the default behavior for all CLI usage.
   * Developers who choose to use this operational mode can also force cached information manually if they choose.
   * The CLI will use this operational mode by default.
1. `flush`: Flushes any cache information to the filesystem during every transaction.
1. `memory`: Effectively disable Persistent Storage.  Any caching of data required by each plugin used is done in memory.  Apprise effectively operates as it always did before peristent storage was available. This setting ensures no content is every written to disk.
   * By default this is the mode Apprise will operate under for those developing with it unless they configure it to otherwise operate as `auto` or `flush`.  This is done through the `AppriseAsset()` object and is explained further on in this documentation.

## CLI Persistent Storage Commands

You can provide the keyword `storage` on your CLI call to see the persistent storage options available to you.
```bash
# List all of the occupied space used by Apprise's Persistent Storage:
apprise storage list

# list is the default option, so the following does the same thing:
apprise storage

# You can prune all of your storage older then 30 days
# and not accessed for this period like so:
apprise storage prune

# You can do a hard reset (and wipe all persistent storage) with:
apprise storage clean

```

You can also filter your results by adding tags and/or URL Identifiers.  When you get a listing (`apprise storage list`), you may see:
```
   # example output of 'apprise storage list':
   1. f7077a65                                             0.00B    unused
      - matrixs://abcdef:****@synapse.example12.com/%23general?image=no&mode=off&version=3&msgtype...
      tags: team

   2. 0e873a46                                            81.10B    active
      - tgram://W...U//?image=False&detect=yes&silent=no&preview=no&content=before&mdv=v1&format=m...
      tags: personal

   3. abcd123                                             12.00B    stale

```
The (persistent storage) cache states are:
 - `unused`: This plugin has not commited anything to disk for reuse/cache purposes
 - `active`: This plugin has written content to disk.  Or at the very least, it has prepared a persistent storage location it can write into.
 - `stale`: The system detected a location where a URL may have possibly written to in the past, but there is nothing linking to it using the URLs provided.  It is likely wasting space or is no longer of any use.

You can use this information to filter your results by specifying _URL ID_ (UID) values after your command.  For example:
```bash
# The below commands continue with the example already identified above
# the following would match abcd123 (even though just ab was provided)
# The output would only list the 'stale' entry above
apprise storage list ab

# knowing our filter is safe, we could remove it
# the below command would not obstruct our other to URLs and would only
# remove our stale one:
apprise storage clean ab

# Entries can be filtered by tag as well:
apprise storage list --tag=team

# You can match on multiple URL ID's as well:
# The followin would actually match the URL ID's of 1. and .2 above
apprise storage list f 0
```
When using the CLI, Persistent storage is set to the operational mode of `auto` by default, you can change this by providing `--storage-mode=` (`-SM`) during your calls.  If you want to ensure it's always set to a value of your choice.

For more information on persistent storage, [visit here](https://appriseit.com/cli/persistent-storage/).

## API Persistent Storage Commands
For developers, persistent storage is set in the operational mode of `memory` by default.

It's at the developers discretion to enable it (by switching it to either `auto` or `flush`). Should you choose to do so: it's as easy as including the information in the `AppriseAsset()` object prior to the initialization of your `Apprise()` instance.

For example:
```python
from apprise import Apprise
from apprise import AppriseAsset
from apprise import PersistentStoreMode

# Prepare a location the persistent storage can write it's cached content to.
# By setting this path, this immediately assumes you wish to operate the
# persistent storage in the operational 'auto' mode
asset = AppriseAsset(storage_path="/path/to/save/data")

# If you want to be more explicit and set more options, then you may do the
# following
asset = AppriseAsset(
    # Set our storage path directory (minimum requirement to enable it)
    storage_path="/path/to/save/data",

    # Set the mode... the options are:
    # 1. PersistentStoreMode.MEMORY
    #       - disable persistent storage from writing to disk
    # 2. PersistentStoreMode.AUTO
    #       - write to disk on demand
    # 3. PersistentStoreMode.FLUSH
    #       - write to disk always and often
    storage_mode=PersistentStoreMode.FLUSH

    # The URL IDs are by default 8 characters in length. You can increase and
    # decrease it's value here.  The value must be > 2. The default value is 8
    # if not otherwise specified
    storage_idlen=8,
)

# Now that we've got our asset, we just work with our Apprise object as we
# normally do
aobj = Apprise(asset=asset)
```

For more information on persistent storage, [visit here](https://appriseit.com/library/persistent-storage/).

# Want To Learn More?

If you're interested in reading more about this and other methods on how to customize your own notifications, please check out the following links:
* 📣 [Using the CLI](https://appriseit.com/cli/)
* 🛠️ [Development API](https://appriseit.com/library/)
* ⚙️ [Configuration File Help](https://appriseit.com/getting-started/configuration/)
* ⚡ [Create Your Own Custom Notifications](https://appriseit.com/library/extending/decorator/)
* 🌎 [Apprise API/Web Interface](https://github.com/caronc/apprise-api/)
* 📖 [Apprise Documentation Source](https://github.com/caronc/apprise-docs/)
* 🔧 [Troubleshooting](https://appriseit.com/qa/)
* 🎉 [Showcase](https://appriseit.com/contributing/showcase/)

Want to help make Apprise better?
* 💡 [Contribute to the Apprise Code Base](https://appriseit.com/contributing/)
* ❤️ [Sponsorship and Donations](https://appriseit.com/contributing/sponsors/)


================================================
FILE: SECURITY.md
================================================
# Security Policy

## Supported Versions

| Version | Supported          |
| ------- | ------------------ |
| 0.9.x   | :white_check_mark: |
| < 0.9.x | :x:                |

## Reporting a Vulnerability

If you find a vunerability, please notify me at lead2gold@gmail.com. If the vunerability
is severe then please just open a ticket at https://github.com/caronc/apprise/issues


================================================
FILE: all-plugin-requirements.txt
================================================
#
# Note: This file is being kept for backwards compatibility with
#       legacy systems that point here.  All future changes should
#       occur in pyproject.toml.  Contents of this file can be found
#       in [project.optional-dependencies].all-plugins

# Provides fcm:// and spush://
cryptography

# Provides growl:// support
gntp

# Provides mqtt:// support
# use any version other than 2.0.x due to https://github.com/eclipse/paho.mqtt.python/issues/814
paho-mqtt != 2.0.*

# Pretty Good Privacy (PGP) Provides mailto:// and deltachat:// support
PGPy

# Provides smpp:// support
smpplib

# For xmpp:// support
slixmpp >= 1.10.0


================================================
FILE: apprise/__init__.py
================================================
# BSD 2-Clause License
#
# Apprise - Push Notification Library.
# Copyright (c) 2026, Chris Caron <lead2gold@gmail.com>
#
# Redistribution and use in source and binary forms, with or without
# modification, are permitted provided that the following conditions are met:
#
# 1. Redistributions of source code must retain the above copyright notice,
#    this list of conditions and the following disclaimer.
#
# 2. Redistributions in binary form must reproduce the above copyright notice,
#    this list of conditions and the following disclaimer in the documentation
#    and/or other materials provided with the distribution.
#
# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
# AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
# IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
# ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE
# LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
# CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
# SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
# INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
# CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
# ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
# POSSIBILITY OF SUCH DAMAGE.

__title__ = "Apprise"
__description__: str = \
    "Push Notifications that work with just about every platform!"
__version__ = "1.9.8"
__author__ = "Chris Caron"
__email__ = "lead2gold@gmail.com"
__license__ = "BSD 2-Clause"
__copyright__ = "Copyright (c) 2026, Chris Caron <lead2gold@gmail.com>"
__status__ = "Production"

from . import decorators, exception
from .apprise import Apprise
from .apprise_attachment import AppriseAttachment
from .apprise_config import AppriseConfig
from .asset import AppriseAsset
from .attachment.base import AttachBase
from .common import (
    CONFIG_FORMATS,
    CONTENT_INCLUDE_MODES,
    CONTENT_LOCATIONS,
    NOTIFY_FORMATS,
    NOTIFY_IMAGE_SIZES,
    NOTIFY_TYPES,
    OVERFLOW_MODES,
    PERSISTENT_STORE_MODES,
    PERSISTENT_STORE_STATES,
    ConfigFormat,
    ContentIncludeMode,
    ContentLocation,
    NotifyFormat,
    NotifyImageSize,
    NotifyType,
    OverflowMode,
    PersistentStoreMode,
)
from .config.base import ConfigBase
from .locale import AppriseLocale

# Inherit our logging with our additional entries added to it
from .logger import LOGGER_NAME, LogCapture, logger, logging
from .manager_attachment import AttachmentManager
from .manager_config import ConfigurationManager
from .manager_plugins import NotificationManager
from .persistent_store import PersistentStore
from .plugins.base import NotifyBase
from .url import PrivacyMode, URLBase

# Set default logging handler to avoid "No handler found" warnings.
logging.getLogger(__name__).addHandler(logging.NullHandler())

__all__ = [
    "CONFIG_FORMATS",
    "CONTENT_INCLUDE_MODES",
    "CONTENT_LOCATIONS",
    "LOGGER_NAME",
    "NOTIFY_FORMATS",
    "NOTIFY_IMAGE_SIZES",
    "NOTIFY_TYPES",
    "OVERFLOW_MODES",
    "PERSISTENT_STORE_MODES",
    "PERSISTENT_STORE_STATES",
    # Core
    "Apprise",
    "AppriseAsset",
    "AppriseAttachment",
    "AppriseConfig",
    "AppriseLocale",
    "AttachBase",
    "AttachmentManager",
    "ConfigBase",
    "ConfigFormat",
    "ConfigurationManager",
    "ContentIncludeMode",
    "ContentLocation",
    "LogCapture",
    # Managers
    "NotificationManager",
    "NotifyBase",
    "NotifyFormat",
    "NotifyImageSize",
    # Reference
    "NotifyType",
    "OverflowMode",
    "PersistentStore",
    "PersistentStoreMode",
    "PrivacyMode",
    "URLBase",
    # Decorator
    "decorators",
    # Exceptions
    "exception",
    # Logging
    "logger",
    "logging",
]


================================================
FILE: apprise/apprise.py
================================================
# BSD 2-Clause License
#
# Apprise - Push Notification Library.
# Copyright (c) 2026, Chris Caron <lead2gold@gmail.com>
#
# Redistribution and use in source and binary forms, with or without
# modification, are permitted provided that the following conditions are met:
#
# 1. Redistributions of source code must retain the above copyright notice,
#    this list of conditions and the following disclaimer.
#
# 2. Redistributions in binary form must reproduce the above copyright notice,
#    this list of conditions and the following disclaimer in the documentation
#    and/or other materials provided with the distribution.
#
# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
# AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
# IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
# ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE
# LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
# CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
# SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
# INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
# CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
# ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
# POSSIBILITY OF SUCH DAMAGE.

from __future__ import annotations

import asyncio
from collections.abc import Iterator
import concurrent.futures as cf
from itertools import chain
import json
import os
from typing import Any, Optional, Union

from . import __version__, common, plugins
from .apprise_attachment import AppriseAttachment
from .apprise_config import AppriseConfig
from .asset import AppriseAsset
from .common import ContentLocation
from .config.base import ConfigBase
from .conversion import convert_between
from .emojis import apply_emojis
from .locale import AppriseLocale
from .logger import logger
from .manager_plugins import NotificationManager
from .plugins.base import NotifyBase
from .utils.cwe312 import cwe312_url
from .utils.json import AppriseJSONEncoder
from .utils.logic import is_exclusive_match
from .utils.parse import parse_list, parse_urls

# Grant access to our Notification Manager Singleton
N_MGR = NotificationManager()


class Apprise:
    """Our Notification Manager."""

    def __init__(
        self,
        servers: Optional[
            Union[
                str,
                dict,
                NotifyBase,
                AppriseConfig,
                ConfigBase,
                list[Union[str, dict, NotifyBase, AppriseConfig, ConfigBase]],
            ]
        ] = None,
        asset: Optional[AppriseAsset] = None,
        location: Optional[ContentLocation] = None,
        debug: bool = False,
    ) -> None:
        """Loads a set of server urls while applying the Asset() module to each
        if specified.

        If no asset is provided, then the default asset is used.

        Optionally specify a global ContentLocation for a more strict means of
        handling Attachments.
        """

        # Initialize a server list of URLs
        self.servers = []

        # Assigns an central asset object that will be later passed into each
        # notification plugin.  Assets contain information such as the local
        # directory images can be found in. It can also identify remote
        # URL paths that contain the images you want to present to the end
        # user. If no asset is specified, then the default one is used.
        self.asset = (
            asset if isinstance(asset, AppriseAsset) else AppriseAsset()
        )

        if servers:
            self.add(servers)

        # Initialize our locale object
        self.locale = AppriseLocale()

        # Set our debug flag
        self.debug = debug

        # Store our hosting location for optional strict rule handling
        # of Attachments.  Setting this to None removes any attachment
        # restrictions.
        self.location = location

    @staticmethod
    def instantiate(
        url: Union[str, dict],
        asset: Optional[AppriseAsset] = None,
        tag: Optional[Union[str, list[str]]] = None,
        suppress_exceptions: bool = True,
    ) -> Optional[NotifyBase]:
        """Returns the instance of a instantiated plugin based on the provided
        Server URL.  If the url fails to be parsed, then None is returned.

        The specified url can be either a string (the URL itself) or a
        dictionary containing all of the components needed to istantiate
        the notification service.  If identifying a dictionary, at the bare
        minimum, one must specify the schema.

        An example of a url dictionary object might look like:
          {
            schema: 'mailto',
            host: 'google.com',
            user: 'myuser',
            password: 'mypassword',
          }

        Alternatively the string is much easier to specify:
          mailto://user:mypassword@google.com

        The dictionary works well for people who are calling details() to
        extract the components they need to build the URL manually.
        """

        # Initialize our result set
        results = None

        # Prepare our Asset Object
        asset = asset if isinstance(asset, AppriseAsset) else AppriseAsset()

        if isinstance(url, str):
            # Acquire our url tokens
            results = plugins.url_to_dict(
                url, secure_logging=asset.secure_logging
            )

            if results is None:
                # Failed to parse the server URL; detailed logging handled
                # inside url_to_dict - nothing to report here.
                return None

        elif isinstance(url, dict):
            # We already have our result set
            results = url

            if results.get("schema") not in N_MGR:
                # schema is a mandatory dictionary item as it is the only way
                # we can index into our loaded plugins
                logger.error('Dictionary does not include a "schema" entry.')
                logger.trace(
                    "Invalid dictionary unpacked as:{}{}".format(
                        os.linesep,
                        os.linesep.join(
                            [f'{k}="{v}"' for k, v in results.items()]
                        ),
                    )
                )
                return None

            logger.trace(
                "Dictionary unpacked as:{}{}".format(
                    os.linesep,
                    os.linesep.join(
                        [f'{k}="{v}"' for k, v in results.items()]
                    ),
                )
            )

        # Otherwise we handle the invalid input specified
        else:
            logger.error(
                "An invalid URL type (%s) was specified for instantiation",
                type(url),
            )
            return None

        if not N_MGR[results["schema"]].enabled:
            #
            # First Plugin Enable Check (Pre Initialization)
            #

            # Plugin has been disabled at a global level
            logger.error(
                "%s:// is disabled on this system.", results["schema"]
            )
            return None

        # Build a list of tags to associate with the newly added notifications
        results["tag"] = set(parse_list(tag))

        # Set our Asset Object
        results["asset"] = asset

        if suppress_exceptions:
            try:
                # Attempt to create an instance of our plugin using the parsed
                # URL information
                plugin = N_MGR[results["schema"]](**results)

                # Create log entry of loaded URL
                logger.debug(
                    "Loaded {} URL: {}".format(
                        N_MGR[results["schema"]].service_name,
                        plugin.url(privacy=asset.secure_logging),
                    )
                )

            except Exception:
                # CWE-312 (Secure Logging) Handling
                loggable_url = (
                    url if not asset.secure_logging else cwe312_url(url)
                )

                # the arguments are invalid or can not be used.
                logger.error(
                    "Could not load {} URL: {}".format(
                        N_MGR[results["schema"]].service_name, loggable_url
                    )
                )
                return None

        else:
            # Attempt to create an instance of our plugin using the parsed
            # URL information but don't wrap it in a try catch
            plugin = N_MGR[results["schema"]](**results)

        if not plugin.enabled:
            #
            # Second Plugin Enable Check (Post Initialization)
            #

            # Service/Plugin is disabled (on a more local level).  This is a
            # case where the plugin was initially enabled but then after the
            # __init__() was called under the hood something pre-determined
            # that it could no longer be used.

            # The only downside to doing it this way is services are
            # initialized prior to returning the details() if 3rd party tools
            # are polling what is available. These services that become
            # disabled thereafter are shown initially that they can be used.
            logger.error(
                "%s:// has become disabled on this system.", results["schema"]
            )
            return None

        return plugin

    def add(
        self,
        servers: Union[
            str,
            dict,
            NotifyBase,
            AppriseConfig,
            ConfigBase,
            list[Union[str, dict, NotifyBase, AppriseConfig, ConfigBase]],
        ],
        asset: Optional[AppriseAsset] = None,
        tag: Optional[Union[str, list[str]]] = None,
    ) -> bool:
        """Adds one or more server URLs into our list.

        You can override the global asset if you wish by including it with the
        server(s) that you add.

        The tag allows you to associate 1 or more tag values to the server(s)
        being added. tagging a service allows you to exclusively access them
        when calling the notify() function.
        """

        # Initialize our return status
        return_status = True

        if asset is None:
            # prepare default asset
            asset = self.asset

        if isinstance(servers, str):
            # build our server list
            servers = parse_urls(servers)
            if len(servers) == 0:
                return False

        elif isinstance(servers, dict):
            # no problem, we support kwargs, convert it to a list
            servers = [servers]

        elif isinstance(servers, (ConfigBase, NotifyBase, AppriseConfig)):
            # Go ahead and just add our plugin into our list
            self.servers.append(servers)
            return True

        elif not isinstance(servers, (tuple, set, list)):
            logger.error(
                f"An invalid notification (type={type(servers)}) was"
                " specified."
            )
            return False

        for server in servers:

            if isinstance(server, (ConfigBase, NotifyBase, AppriseConfig)):
                # Go ahead and just add our plugin into our list
                self.servers.append(server)
                continue

            elif not isinstance(server, (str, dict)):
                logger.error(
                    f"An invalid notification (type={type(server)}) was"
                    " specified."
                )
                return_status = False
                continue

            # Instantiate ourselves an object, this function throws or
            # returns None if it fails
            instance = Apprise.instantiate(server, asset=asset, tag=tag)
            if not isinstance(instance, NotifyBase):
                # No logging is required as instantiate() handles failure
                # and/or success reasons for us
                return_status = False
                continue

            # Add our initialized plugin to our server listings
            self.servers.append(instance)

        # Return our status
        return return_status

    def clear(self) -> None:
        """Empties our server list."""
        self.servers[:] = []

    def find(
        self,
        tag: Any = common.MATCH_ALL_TAG,
        match_always: bool = True,
    ) -> Iterator[NotifyBase]:
        """Returns a list of all servers matching against the tag specified."""

        # Build our tag setup
        #   - top level entries are treated as an 'or'
        #   - second level (or more) entries are treated as 'and'
        #
        #   examples:
        #     tag="tagA, tagB"                = tagA or tagB
        #     tag=['tagA', 'tagB']            = tagA or tagB
        #     tag=[('tagA', 'tagC'), 'tagB']  = (tagA and tagC) or tagB
        #     tag=[('tagB', 'tagC')]          = tagB and tagC

        # A match_always flag allows us to pick up on our 'any' keyword
        # and notify these services under all circumstances
        match_always = common.MATCH_ALWAYS_TAG if match_always else None

        # Iterate over our loaded plugins
        for entry in self.servers:

            if isinstance(entry, (ConfigBase, AppriseConfig)):
                # load our servers
                servers = entry.servers()

            else:
                servers = [
                    entry,
                ]

            for server in servers:
                # Apply our tag matching based on our defined logic
                if is_exclusive_match(
                    logic=tag,
                    data=server.tags,
                    match_all=common.MATCH_ALL_TAG,
                    match_always=match_always,
                ):
                    yield server
        return

    def notify(
        self,
        body: Union[str, bytes],
        title: Union[str, bytes] = "",
        notify_type: Union[str, common.NotifyType] = common.NotifyType.INFO,
        body_format: Optional[str] = None,
        tag: Any = common.MATCH_ALL_TAG,
        match_always: bool = True,
        attach: Any = None,
        interpret_escapes: Optional[bool] = None,
    ) -> Optional[bool]:
        """Send a notification to all the plugins previously loaded.

        If the body_format specified is NotifyFormat.MARKDOWN, it will be
        converted to HTML if the Notification type expects this.

        if the tag is specified (either a string or a set/list/tuple of
        strings), then only the notifications flagged with that tagged value
        are notified.  By default, all added services are notified
        (tag=MATCH_ALL_TAG)

        This function returns True if all notifications were successfully sent,
        False if even just one of them fails, and None if no notifications were
        sent at all as a result of tag filtering and/or simply having empty
        configuration files that were read.

        Attach can contain a list of attachment URLs.  attach can also be
        represented by an AttachBase() (or list of) object(s). This identifies
        the products you wish to notify

        Set interpret_escapes to True if you want to pre-escape a string such
        as turning a \n into an actual new line, etc.
        """

        try:
            # Process arguments and build synchronous and asynchronous calls
            # (this step can throw internal errors).
            sequential_calls, parallel_calls = self._create_notify_calls(
                body,
                title,
                notify_type=notify_type,
                body_format=body_format,
                tag=tag,
                match_always=match_always,
                attach=attach,
                interpret_escapes=interpret_escapes,
            )

        except TypeError:
            # No notifications sent, and there was an internal error.
            return False

        if not sequential_calls and not parallel_calls:
            # Nothing to send
            return None

        sequential_result = Apprise._notify_sequential(*sequential_calls)
        parallel_result = Apprise._notify_parallel_threadpool(*parallel_calls)
        return sequential_result and parallel_result

    async def async_notify(
        self,
        *args: Any,
        **kwargs: Any
    ) -> Optional[bool]:
        """Send a notification to all the plugins previously loaded, for
        asynchronous callers.

        The arguments are identical to those of Apprise.notify().
        """
        try:
            # Process arguments and build synchronous and asynchronous calls
            # (this step can throw internal errors).
            sequential_calls, parallel_calls = self._create_notify_calls(
                *args, **kwargs
            )

        except TypeError:
            # No notifications sent, and there was an internal error.
            return False

        if not sequential_calls and not parallel_calls:
            # Nothing to send
            return None

        sequential_result = Apprise._notify_sequential(*sequential_calls)
        parallel_result = await Apprise._notify_parallel_asyncio(
            *parallel_calls
        )
        return sequential_result and parallel_result

    def _create_notify_calls(self, *args, **kwargs):
        """Creates notifications for all the plugins loaded.

        Returns a list of (server, notify() kwargs) tuples for plugins with
        parallelism disabled and another list for plugins with parallelism
        enabled.
        """

        all_calls = list(self._create_notify_gen(*args, **kwargs))

        # Split into sequential and parallel notify() calls.
        sequential, parallel = [], []
        for server, notify_kwargs in all_calls:
            if server.asset.async_mode:
                parallel.append((server, notify_kwargs))
            else:
                sequential.append((server, notify_kwargs))

        return sequential, parallel

    def _create_notify_gen(
        self,
        body,
        title="",
        notify_type=common.NotifyType.INFO,
        body_format=None,
        tag=common.MATCH_ALL_TAG,
        match_always=True,
        attach=None,
        interpret_escapes=None,
    ):
        """Internal generator function for _create_notify_calls()."""

        if len(self) == 0:
            # Nothing to notify
            msg = "There are no service(s) to notify"
            logger.error(msg)
            raise TypeError(msg)

        if not (title or body or attach):
            msg = "No message content specified to deliver"
            logger.error(msg)
            raise TypeError(msg)

        try:
            notify_type = (
                notify_type if isinstance(notify_type, common.NotifyType)
                else common.NotifyType(notify_type.lower())
            )

        except (AttributeError, ValueError, TypeError):
            err = (
                f"An invalid notification type ({notify_type}) was "
                "specified.")
            raise TypeError(err) from None

        try:
            if title and isinstance(title, bytes):
                title = title.decode(self.asset.encoding)

            if body and isinstance(body, bytes):
                body = body.decode(self.asset.encoding)

        except UnicodeDecodeError:
            msg = (
                "The content passed into Apprise was not of encoding "
                f"type: {self.asset.encoding}"
            )
            logger.error(msg)
            raise TypeError(msg) from None

        # Tracks conversions
        conversion_body_map = {}
        conversion_title_map = {}

        # Prepare attachments if required
        if attach is not None and not isinstance(attach, AppriseAttachment):
            attach = AppriseAttachment(
                attach, asset=self.asset, location=self.location
            )

        # Allow Asset default value
        body_format = (
            self.asset.body_format if body_format is None else body_format
        )

        # Allow Asset default value
        interpret_escapes = (
            self.asset.interpret_escapes
            if interpret_escapes is None
            else interpret_escapes
        )

        # Iterate over our loaded plugins
        for server in self.find(tag, match_always=match_always):
            # If our code reaches here, we either did not define a tag (it
            # was set to None), or we did define a tag and the logic above
            # determined we need to notify the service it's associated with

            # First we need to generate a key we will use to determine if we
            # need to build our data out.  Entries without are merged with
            # the body at this stage.
            key = (
                server.notify_format
                if server.title_maxlen > 0
                else f"_{server.notify_format}"
            )

            if server.interpret_emojis:
                # alter our key slightly to handle emojis since their value is
                # pulled out of the notification
                key += "-emojis"

            if key not in conversion_title_map:

                # Prepare our title
                conversion_title_map[key] = title if title else ""

                # Conversion of title only occurs for services where the title
                # is blended with the body (title_maxlen <= 0)
                if conversion_title_map[key] and server.title_maxlen <= 0:
                    conversion_title_map[key] = convert_between(
                        body_format,
                        server.notify_format,
                        content=conversion_title_map[key],
                    )

                # Our body is always converted no matter what
                conversion_body_map[key] = convert_between(
                    body_format, server.notify_format, content=body
                )

                if interpret_escapes:
                    #
                    # Escape our content
                    #

                    try:
                        # Added overhead required due to Python 3 Encoding Bug
                        # identified here: https://bugs.python.org/issue21331
                        conversion_body_map[key] = (
                            conversion_body_map[key]
                            .encode("ascii", "backslashreplace")
                            .decode("unicode-escape")
                        )

                        conversion_title_map[key] = (
                            conversion_title_map[key]
                            .encode("ascii", "backslashreplace")
                            .decode("unicode-escape")
                        )

                    except AttributeError:
                        # Must be of string type
                        msg = "Failed to escape message body"
                        logger.error(msg)
                        raise TypeError(msg) from None

                if server.interpret_emojis:
                    #
                    # Convert our :emoji: definitions
                    #

                    conversion_body_map[key] = apply_emojis(
                        conversion_body_map[key]
                    )
                    conversion_title_map[key] = apply_emojis(
                        conversion_title_map[key]
                    )

            kwargs = {
                "body": conversion_body_map[key],
                "title": conversion_title_map[key],
                "notify_type": notify_type,
                "attach": attach,
                "body_format": body_format,
            }
            yield (server, kwargs)

    @staticmethod
    def _notify_sequential(*servers_kwargs):
        """Process a list of notify() calls sequentially and synchronously."""

        success = True

        for server, kwargs in servers_kwargs:
            try:
                # Send notification
                result = server.notify(**kwargs)
                success = success and result

            except TypeError:
                # These are our internally thrown notifications.
                success = False

            except Exception:
                # A catch all so we don't have to abort early
                # just because one of our plugins has a bug in it.
                logger.exception("Unhandled Notification Exception")
                success = False

        return success

    @staticmethod
    def _notify_parallel_threadpool(*servers_kwargs):
        """Process a list of notify() calls in parallel and synchronously."""

        n_calls = len(servers_kwargs)

        # 0-length case
        if n_calls == 0:
            return True

        # There's no need to use a thread pool for just a single notification
        if n_calls == 1:
            return Apprise._notify_sequential(servers_kwargs[0])

        # Create log entry
        logger.info(
            "Notifying %d service(s) with threads.", len(servers_kwargs)
        )

        with cf.ThreadPoolExecutor() as executor:
            success = True
            futures = [
                executor.submit(server.notify, **kwargs)
                for (server, kwargs) in servers_kwargs
            ]

            for future in cf.as_completed(futures):
                try:
                    result = future.result()
                    success = success and result

                except TypeError:
                    # These are our internally thrown notifications.
                    success = False

                except Exception:
                    # A catch all so we don't have to abort early
                    # just because one of our plugins has a bug in it.
                    logger.exception("Unhandled Notification Exception")
                    success = False

            return success

    @staticmethod
    async def _notify_parallel_asyncio(*servers_kwargs):
        """Process a list of async_notify() calls in parallel and
        asynchronously."""

        n_calls = len(servers_kwargs)

        # 0-length case
        if n_calls == 0:
            return True

        # (Unlike with the thread pool, we don't optimize for the single-
        # notification case because asyncio can do useful work while waiting
        # for that thread to complete)

        # Create log entry
        logger.info(
            "Notifying %d service(s) asynchronously.", len(servers_kwargs)
        )

        async def do_call(server, kwargs):
            return await server.async_notify(**kwargs)

        cors = (do_call(server, kwargs) for (server, kwargs) in servers_kwargs)
        results = await asyncio.gather(*cors, return_exceptions=True)

        if any(
            isinstance(status, Exception) and not isinstance(status, TypeError)
            for status in results
        ):
            # A catch all so we don't have to abort early just because
            # one of our plugins has a bug in it.
            logger.exception("Unhandled Notification Exception")
            return False

        if any(isinstance(status, TypeError) for status in results):
            # These are our internally thrown notifications.
            return False

        return all(results)

    def json(
        self,
        lang: Optional[str] = None,
        show_requirements: bool = False,
        show_disabled: bool = False,
        indent: Optional[int] = None,
        path: Optional[str] = None,
    ) -> Union[str, bool]:
        """Returns a json response associated with the Apprise object."""
        details = self.details(
            lang=lang,
            show_requirements=show_requirements,
            show_disabled=show_disabled,
        )

        if not path:
            return json.dumps(
                details,
                separators=(",", ":"),
                indent=indent,
                cls=AppriseJSONEncoder,
            )

        with open(path, "w") as fp:
            try:
                json.dump(
                    details,
                    fp,
                    separators=(",", ":"),
                    indent=indent,
                    cls=AppriseJSONEncoder,
                    ensure_ascii=False,
                )

            except (OSError, EOFError) as e:
                logger.error(
                    "Apprise details dumpfile inaccessible: %s", path
                )
                logger.debug("Apprise details dump Exception: %s", e)

                # Early Exit
                return False

            finally:
                # Reduce memory
                del details

        return True

    def details(
        self,
        lang: Optional[str] = None,
        show_requirements: bool = False,
        show_disabled: bool = False,
    ) -> dict[str, Any]:
        """Returns the details associated with the Apprise object."""

        # general object returned
        response = {
            # Defines the current version of Apprise
            "version": __version__,
            # Lists all of the currently supported Notifications
            "schemas": [],
            # Includes the configured asset details
            "asset": self.asset.details(),
        }

        for plugin in N_MGR.plugins():
            # Iterate over our hashed plugins and dynamically build details on
            # their status:

            content = {
                "service_name": getattr(plugin, "service_name", None),
                "service_url": getattr(plugin, "service_url", None),
                "setup_url": getattr(plugin, "setup_url", None),
                # Placeholder - populated below
                "details": None,
                # Let upstream service know of the plugins that support
                # attachments
                "attachment_support": getattr(
                    plugin, "attachment_support", False
                ),
                # Differentiat between what is a custom loaded plugin and
                # which is native.
                "category": getattr(plugin, "category", None),
            }

            # Standard protocol(s) should be None or a tuple
            enabled = getattr(plugin, "enabled", True)
            if not show_disabled and not enabled:
                # Do not show inactive plugins
                continue

            elif show_disabled:
                # Add current state to response
                content["enabled"] = enabled

            # Standard protocol(s) should be None or a tuple
            protocols = getattr(plugin, "protocol", None)
            if isinstance(protocols, str):
                protocols = (protocols,)

            # Secure protocol(s) should be None or a tuple
            secure_protocols = getattr(plugin, "secure_protocol", None)
            if isinstance(secure_protocols, str):
                secure_protocols = (secure_protocols,)

            # Add our protocol details to our content
            content.update({
                "protocols": protocols,
                "secure_protocols": secure_protocols,
            })

            if not lang:
                # Simply return our results
                content["details"] = plugins.details(plugin)
                if show_requirements:
                    content["requirements"] = plugins.requirements(plugin)

            else:
                # Emulate the specified language when returning our results
                with self.locale.lang_at(lang):
                    content["details"] = plugins.details(plugin)
                    if show_requirements:
                        content["requirements"] = plugins.requirements(plugin)

            # Build our response object
            response["schemas"].append(content)

        return response

    def urls(self, privacy: bool = False) -> list[str]:
        """Returns all of the loaded URLs defined in this apprise object."""
        urls = []
        for s in self.servers:
            if isinstance(s, (ConfigBase, AppriseConfig)):
                for s_ in s.servers():
                    urls.append(s_.url(privacy=privacy))
            else:
                urls.append(s.url(privacy=privacy))
        return urls

    def pop(self, index: int) -> NotifyBase:
        """Removes an indexed Notification Service from the stack and returns
        it.

        The thing is we can never pop AppriseConfig() entries, only what was
        loaded within them. So pop needs to carefully iterate over our list and
        only track actual entries.
        """

        # Tracking variables
        prev_offset = -1
        offset = prev_offset

        for idx, s in enumerate(self.servers):
            if isinstance(s, (ConfigBase, AppriseConfig)):
                servers = s.servers()
                if len(servers) > 0:
                    # Acquire a new maximum offset to work with
                    offset = prev_offset + len(servers)

                    if offset >= index:
                        # we can pop an element from our config stack
                        fn = (
                            s.pop
                            if isinstance(s, ConfigBase)
                            else s.server_pop
                        )

                        return fn(
                            index
                            if prev_offset == -1
                            else (index - prev_offset - 1)
                        )

            else:
                offset = prev_offset + 1
                if offset == index:
                    return self.servers.pop(idx)

            # Update our old offset
            prev_offset = offset

        # If we reach here, then we indexed out of range
        raise IndexError("list index out of range")

    def __getitem__(self, index: int) -> NotifyBase:
        """Returns the indexed server entry of a loaded notification server."""
        # Tracking variables
        prev_offset = -1
        offset = prev_offset

        for idx, s in enumerate(self.servers):
            if isinstance(s, (ConfigBase, AppriseConfig)):
                # Get our list of servers associate with our config object
                servers = s.servers()
                if len(servers) > 0:
                    # Acquire a new maximum offset to work with
                    offset = prev_offset + len(servers)

                    if offset >= index:
                        return servers[(
                            index
                            if prev_offset == -1
                            else (index - prev_offset - 1)
                        )]

            else:
                offset = prev_offset + 1
                if offset == index:
                    return self.servers[idx]

            # Update our old offset
            prev_offset = offset

        # If we reach here, then we indexed out of range
        raise IndexError("list index out of range")

    def __getstate__(self) -> dict[str, object]:
        """Pickle Support dumps()"""
        attributes = {
            "asset": self.asset,
            # Prepare our URL list as we need to extract the associated tags
            # and asset details associated with it
            "urls": [
                {
                    "url": server.url(privacy=False),
                    "tag": server.tags if server.tags else None,
                    "asset": server.asset,
                }
                for server in self.servers
            ],
            "locale": self.locale,
            "debug": self.debug,
            "location": self.location.value if self.location else None,
        }

        return attributes

    def __setstate__(self, state: dict[str, object]) -> None:
        """Pickle Support loads()"""
        self.servers = []
        self.asset = state["asset"]
        self.locale = state["locale"]

        location = state.get("location")
        self.location = (
            location if isinstance(location, ContentLocation)
            else ContentLocation(location)
            if location is not None
            else None
        )

        for entry in state["urls"]:
            self.add(entry["url"], asset=entry["asset"], tag=entry["tag"])

    def __bool__(self) -> bool:
        """Allows the Apprise object to be wrapped in an 'if statement'.

        True is returned if at least one service has been loaded.
        """
        return len(self) > 0

    def __iter__(self) -> Iterator[NotifyBase]:
        """Returns an iterator to each of our servers loaded.

        This includes those found inside configuration.
        """
        return chain(*[
            (
                [s]
                if not isinstance(s, (ConfigBase, AppriseConfig))
                else iter(s.servers())
            )
            for s in self.servers
        ])

    def __len__(self) -> int:
        """Returns the number of servers loaded; this includes those found
        within loaded configuration.

        This funtion nnever actually counts the Config entry themselves (if
        they exist), only what they contain.
        """
        return sum((
                1
                if not isinstance(s, (ConfigBase, AppriseConfig))
                else len(s.servers())
            )
            for s in self.servers)


================================================
FILE: apprise/apprise_attachment.py
================================================
# BSD 2-Clause License
#
# Apprise - Push Notification Library.
# Copyright (c) 2026, Chris Caron <lead2gold@gmail.com>
#
# Redistribution and use in source and binary forms, with or without
# modification, are permitted provided that the following conditions are met:
#
# 1. Redistributions of source code must retain the above copyright notice,
#    this list of conditions and the following disclaimer.
#
# 2. Redistributions in binary form must reproduce the above copyright notice,
#    this list of conditions and the following disclaimer in the documentation
#    and/or other materials provided with the distribution.
#
# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
# AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
# IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
# ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE
# LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
# CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
# SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
# INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
# CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
# ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
# POSSIBILITY OF SUCH DAMAGE.

from collections.abc import Iterator
from typing import Any, Optional, Union

from .asset import AppriseAsset
from .attachment.base import AttachBase
from .common import ContentLocation
from .logger import logger
from .manager_attachment import AttachmentManager
from .url import URLBase
from .utils.parse import GET_SCHEMA_RE

# Grant access to our Notification Manager Singleton
A_MGR = AttachmentManager()


class AppriseAttachment:
    """Our Apprise Attachment File Manager."""

    def __init__(
        self,
        paths: Optional[Union[str, list[
            Union[str, AttachBase, "AppriseAttachment"]]]] = None,
        asset: Optional[AppriseAsset] = None,
        cache: Union[bool, int] = True,
        location: Optional[Union[str, ContentLocation]] = None,
        **kwargs: Any,
    ) -> None:
        """Loads all of the paths/urls specified (if any).

        The path can either be a single string identifying one explicit
        location, otherwise you can pass in a series of locations to scan
        via a list.

        By default we cache our responses so that subsiquent calls does not
        cause the content to be retrieved again.  For local file references
        this makes no difference at all.  But for remote content, this does
        mean more then one call can be made to retrieve the (same) data.  This
        method can be somewhat inefficient if disabled.  Only disable caching
        if you understand the consequences.

        You can alternatively set the cache value to an int identifying the
        number of seconds the previously retrieved can exist for before it
        should be considered expired.

        It's also worth nothing that the cache value is only set to elements
        that are not already of subclass AttachBase()

        Optionally set your current ContentLocation in the location argument.
        This is used to further handle attachments. The rules are as follows:
          - INACCESSIBLE: You simply have disabled use of the object; no
                          attachments will be retrieved/handled.
          - HOSTED:       You are hosting an attachment service for others.
                          In these circumstances all attachments that are LOCAL
                          based (such as file://) will not be allowed.
          - LOCAL:        The least restrictive mode as local files can be
                          referenced in addition to hosted.

        In all but HOSTED and LOCAL modes, INACCESSIBLE attachment types will
        continue to be inaccessible.  However if you set this field (location)
        to None (it's default value) the attachment location category will not
        be tested in any way (all attachment types will be allowed).

        The location field is also a global option that can be set when
        initializing the Apprise object.
        """

        # Initialize our attachment listings
        self.attachments = []

        # Set our cache flag
        self.cache = cache

        # Prepare our Asset Object
        self.asset = (
            asset if isinstance(asset, AppriseAsset) else AppriseAsset()
        )

        if location:
            try:
                self.location = (
                    location if isinstance(location, ContentLocation)
                    else ContentLocation(location.lower())
                )

            except (AttributeError, ValueError):
                err = (
                    f"An invalid Attachment location ({location}) was "
                    "specified.",
                )
                logger.warning(err)
                raise TypeError(err) from None
        else:
            # do not set location if no initialization was made for it
            self.location = None

        # Now parse any paths specified
        if paths is not None and not self.add(paths):
            raise TypeError("One or more attachments could not be added.")

    def add(
        self,
        attachments: Union[
            str,
            AttachBase,
            "AppriseAttachment",
            list[Union[str, AttachBase, "AppriseAttachment"]],
        ],
        asset: Optional[AppriseAsset] = None,
        cache: Optional[Union[bool, int]] = None,
    ) -> bool:
        """Adds one or more attachments into our list.

        By default we cache our responses so that subsiquent calls does not
        cause the content to be retrieved again.  For local file references
        this makes no difference at all.  But for remote content, this does
        mean more then one call can be made to retrieve the (same) data.  This
        method can be somewhat inefficient if disabled.  Only disable caching
        if you understand the consequences.

        You can alternatively set the cache value to an int identifying the
        number of seconds the previously retrieved can exist for before it
        should be considered expired.

        It's also worth nothing that the cache value is only set to elements
        that are not already of subclass AttachBase()
        """
        # Initialize our return status
        return_status = True

        # Initialize our default cache value
        cache = cache if cache is not None else self.cache

        if asset is None:
            # prepare default asset
            asset = self.asset

        if isinstance(attachments, (AttachBase, str)):
            # store our instance
            attachments = (attachments,)

        elif not isinstance(attachments, (tuple, set, list)):
            logger.error(
                f"An invalid attachment url (type={type(attachments)}) was "
                "specified."
            )
            return False

        # Iterate over our attachments
        for attachment in attachments:
            if self.location == ContentLocation.INACCESSIBLE:
                logger.warning(
                    f"Attachments are disabled; ignoring {attachment}"
                )
                return_status = False
                continue

            if isinstance(attachment, str):
                logger.debug(f"Loading attachment: {attachment}")
                # Instantiate ourselves an object, this function throws or
                # returns None if it fails
                instance = AppriseAttachment.instantiate(
                    attachment, asset=asset, cache=cache
                )
                if not isinstance(instance, AttachBase):
                    return_status = False
                    continue

            elif isinstance(attachment, AppriseAttachment):
                # We were provided a list of Apprise Attachments
                # append our content together
                instance = attachment.attachments

            elif not isinstance(attachment, AttachBase):
                logger.warning(
                    f"An invalid attachment (type={type(attachment)}) was"
                    " specified."
                )
                return_status = False
                continue

            else:
                # our entry is of type AttachBase, so just go ahead and point
                # our instance to it for some post processing below
                instance = attachment

            # Apply some simple logic if our location flag is set
            if self.location and (
                (
                    self.location == ContentLocation.HOSTED
                    and instance.location != ContentLocation.HOSTED
                )
                or instance.location == ContentLocation.INACCESSIBLE
            ):
                logger.warning(
                    "Attachment was disallowed due to accessibility"
                    f" restrictions ({self.location}->{instance.location}):"
                    f" {instance.url(privacy=True)}"
                )
                return_status = False
                continue

            # Add our initialized plugin to our server listings
            if isinstance(instance, list):
                self.attachments.extend(instance)

            else:
                self.attachments.append(instance)

        # Return our status
        return return_status

    @staticmethod
    def instantiate(
        url: str,
        asset: Optional[AppriseAsset] = None,
        cache: Optional[Union[bool, int]] = None,
        suppress_exceptions: bool = True,
    ) -> Optional[AttachBase]:
        """Returns the instance of a instantiated attachment plugin based on
        the provided Attachment URL.  If the url fails to be parsed, then None
        is returned.

        A specified cache value will over-ride anything set
        """
        # Attempt to acquire the schema at the very least to allow our
        # attachment based urls.
        schema = GET_SCHEMA_RE.match(url)
        if schema is None:
            # Plan B is to assume we're dealing with a file
            schema = "file"
            url = f"{schema}://{URLBase.quote(url)}"

        else:
            # Ensure our schema is always in lower case
            schema = schema.group("schema").lower()

            # Some basic validation
            if schema not in A_MGR:
                logger.warning(f"Unsupported schema {schema}.")
                return None

        # Parse our url details of the server object as dictionary containing
        # all of the information parsed from our URL
        results = A_MGR[schema].parse_url(url)

        if not results:
            # Failed to parse the server URL
            logger.warning(f"Unparseable URL {url}.")
            return None

        # Prepare our Asset Object
        results["asset"] = (
            asset if isinstance(asset, AppriseAsset) else AppriseAsset()
        )

        if cache is not None:
            # Force an over-ride of the cache value to what we have specified
            results["cache"] = cache

        if suppress_exceptions:
            try:
                # Attempt to create an instance of our plugin using the parsed
                # URL information
                attach_plugin = A_MGR[results["schema"]](**results)

            except Exception:
                # the arguments are invalid or can not be used.
                logger.warning(f"Could not load URL: {url}")
                return None

        else:
            # Attempt to create an instance of our plugin using the parsed
            # URL information but don't wrap it in a try catch
            attach_plugin = A_MGR[results["schema"]](**results)

        return attach_plugin

    def sync(
        self,
        abort_on_error: bool = True,
        abort_if_empty: bool = True,
    ) -> bool:
        """Itereates over all of the attachments and retrieves them."""
        return (
            False
            if abort_if_empty and not self.attachments
            else (
                next((False for a in self.attachments if not a), True)
                if abort_on_error
                else next((True for a in self.attachments), True)
            )
        )

    def clear(self) -> None:
        """Empties our attachment list."""
        self.attachments[:] = []

    def size(self) -> int:
        """Returns the total size of accumulated attachments."""
        return sum(len(a) for a in self.attachments if len(a) > 0)

    def pop(self, index: int = -1) -> AttachBase:
        """Removes an indexed Apprise Attachment from the stack and returns it.

        by default the last element is poped from the list
        """
        # Remove our entry
        return self.attachments.pop(index)

    def __getitem__(self, index: int) -> AttachBase:
        """Returns the indexed entry of a loaded apprise attachments."""
        return self.attachments[index]

    def __bool__(self) -> bool:
        """Allows the Apprise object to be wrapped in an 'if statement'.

        True is returned if at least one service has been loaded.
        """
        return bool(self.attachments)

    def __iter__(self) -> Iterator[AttachBase]:
        """Returns an iterator to our attachment list."""
        return iter(self.attachments)

    def __len__(self) -> int:
        """Returns the number of attachment entries loaded."""
        return len(self.attachments)


================================================
FILE: apprise/apprise_config.py
================================================
# BSD 2-Clause License
#
# Apprise - Push Notification Library.
# Copyright (c) 2026, Chris Caron <lead2gold@gmail.com>
#
# Redistribution and use in source and binary forms, with or without
# modification, are permitted provided that the following conditions are met:
#
# 1. Redistributions of source code must retain the above copyright notice,
#    this list of conditions and the following disclaimer.
#
# 2. Redistributions in binary form must reproduce the above copyright notice,
#    this list of conditions and the following disclaimer in the documentation
#    and/or other materials provided with the distribution.
#
# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
# AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
# IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
# ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE
# LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
# CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
# SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
# INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
# CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
# ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
# POSSIBILITY OF SUCH DAMAGE.

from __future__ import annotations

from typing import TYPE_CHECKING, Any

from . import common
from .asset import AppriseAsset
from .config.base import ConfigBase
from .logger import logger
from .manager_config import ConfigurationManager
from .url import URLBase
from .utils.logic import is_exclusive_match
from .utils.parse import GET_SCHEMA_RE, parse_list

if TYPE_CHECKING:
    from .plugins.base import NotifyBase

# Grant access to our Configuration Manager Singleton
C_MGR = ConfigurationManager()


class AppriseConfig:
    """Our Apprise Configuration File Manager.

    - Supports a list of URLs defined one after another (text format)
    - Supports a destinct YAML configuration format
    """

    def __init__(
        self,
        paths: str | list[str] | None = None,
        asset: AppriseAsset | None = None,
        cache: bool | int = True,
        recursion: int = 0,
        insecure_includes: bool = False,
        **kwargs: Any,
    ) -> None:
        """Loads all of the paths specified (if any).

        The path can either be a single string identifying one explicit
        location, otherwise you can pass in a series of locations to scan
        via a list.

        If no path is specified then a default list is used.

        By default we cache our responses so that subsiquent calls does not
        cause the content to be retrieved again. Setting this to False does
        mean more then one call can be made to retrieve the (same) data.  This
        method can be somewhat inefficient if disabled and you're set up to
        make remote calls.  Only disable caching if you understand the
        consequences.

        You can alternatively set the cache value to an int identifying the
        number of seconds the previously retrieved can exist for before it
        should be considered expired.

        It's also worth nothing that the cache value is only set to elements
        that are not already of subclass ConfigBase()

        recursion defines how deep we recursively handle entries that use the
        `import` keyword. This keyword requires us to fetch more configuration
        from another source and add it to our existing compilation. If the
        file we remotely retrieve also has an `import` reference, we will only
        advance through it if recursion is set to 2 deep.  If set to zero
        it is off.  There is no limit to how high you set this value. It would
        be recommended to keep it low if you do intend to use it.

        insecure includes by default are disabled. When set to True, all
        Apprise Config files marked to be in STRICT mode are treated as being
        in ALWAYS mode.

        Take a file:// based configuration for example, only a file:// based
        configuration can import another file:// based one. because it is set
        to STRICT mode. If an http:// based configuration file attempted to
        import a file:// one it woul fail. However this import would be
        possible if insecure_includes is set to True.

        There are cases where a self hosting apprise developer may wish to load
        configuration from memory (in a string format) that contains import
        entries (even file:// based ones).  In these circumstances if you want
        these includes to be honored, this value must be set to True.
        """

        # Initialize a server list of URLs
        self.configs = []

        # Prepare our Asset Object
        self.asset = (
            asset if isinstance(asset, AppriseAsset) else AppriseAsset()
        )

        # Set our cache flag
        self.cache = cache

        # Initialize our recursion value
        self.recursion = recursion

        # Initialize our insecure_includes flag
        self.insecure_includes = insecure_includes

        if paths is not None:
            # Store our path(s)
            self.add(paths)

        return

    def add(
        self,
        configs: str | ConfigBase | list[str | ConfigBase],
        asset: AppriseAsset | None = None,
        tag: str | list[str] | None = None,
        cache: bool | int = True,
        recursion: int | None = None,
        insecure_includes: bool | None = None,
    ) -> bool:
        """Adds one or more config URLs into our list.

        You can override the global asset if you wish by including it with the
        config(s) that you add.

        By default we cache our responses so that subsiquent calls does not
        cause the content to be retrieved again. Setting this to False does
        mean more then one call can be made to retrieve the (same) data.  This
        method can be somewhat inefficient if disabled and you're set up to
        make remote calls.  Only disable caching if you understand the
        consequences.

        You can alternatively set the cache value to an int identifying the
        number of seconds the previously retrieved can exist for before it
        should be considered expired.

        It's also worth nothing that the cache value is only set to elements
        that are not already of subclass ConfigBase()

        Optionally override the default recursion value.

        Optionally override the insecure_includes flag. if insecure_includes is
        set to True then all plugins that are set to a STRICT mode will be a
        treated as ALWAYS.
        """

        # Initialize our return status
        return_status = True

        # Initialize our default cache value
        cache = cache if cache is not None else self.cache

        # Initialize our default recursion value
        recursion = recursion if recursion is not None else self.recursion

        # Initialize our default insecure_includes value
        insecure_includes = (
            insecure_includes
            if insecure_includes is not None
            else self.insecure_includes
        )

        if asset is None:
            # prepare default asset
            asset = self.asset

        if isinstance(configs, ConfigBase):
            # Go ahead and just add our configuration into our list
            self.configs.append(configs)
            return True

        elif isinstance(configs, str):
            # Save our path
            configs = (configs,)

        elif not isinstance(configs, (tuple, set, list)):
            logger.error(
                f"An invalid configuration path (type={type(configs)}) was "
                "specified."
            )
            return False

        # Iterate over our configuration
        for config in configs:

            if isinstance(config, ConfigBase):
                # Go ahead and just add our configuration into our list
                self.configs.append(config)
                continue

            elif not isinstance(config, str):
                logger.warning(
                    f"An invalid configuration (type={type(config)}) was"
                    " specified."
                )
                return_status = False
                continue

            logger.debug(f"Loading configuration: {config}")

            # Instantiate ourselves an object, this function throws or
            # returns None if it fails
            instance = AppriseConfig.instantiate(
                config,
                asset=asset,
                tag=tag,
                cache=cache,
                recursion=recursion,
                insecure_includes=insecure_includes,
            )
            if not isinstance(instance, ConfigBase):
                return_status = False
                continue

            # Add our initialized plugin to our server listings
            self.configs.append(instance)

        # Return our status
        return return_status

    def add_config(
        self,
        content: str,
        asset: AppriseAsset | None = None,
        tag: str | list[str] | None = None,
        format: str | None = None,
        recursion: int | None = None,
        insecure_includes: bool | None = None,
    ) -> bool:
        """Adds one configuration file in it's raw format. Content gets loaded
        as a memory based object and only exists for the life of this
        AppriseConfig object it was loaded into.

        If you know the format ('yaml' or 'text') you can specify it for
        slightly less overhead during this call.  Otherwise the configuration
        is auto-detected.

        Optionally override the default recursion value.

        Optionally override the insecure_includes flag. if insecure_includes is
        set to True then all plugins that are set to a STRICT mode will be a
        treated as ALWAYS.
        """

        # Initialize our default recursion value
        recursion = recursion if recursion is not None else self.recursion

        # Initialize our default insecure_includes value
        insecure_includes = (
            insecure_includes
            if insecure_includes is not None
            else self.insecure_includes
        )

        if asset is None:
            # prepare default asset
            asset = self.asset

        if not isinstance(content, str):
            logger.warning(
                f"An invalid configuration (type={type(content)}) was"
                " specified."
            )
            return False

        logger.debug(f"Loading raw configuration: {content}")

        # Create ourselves a ConfigMemory Object to store our configuration
        instance = C_MGR["memory"](
            content=content,
            format=format,
            asset=asset,
            tag=tag,
            recursion=recursion,
            insecure_includes=insecure_includes,
        )

        if not (instance.config_format and
                instance.config_format.value in common.CONFIG_FORMATS):
            logger.warning(
                "The format of the configuration could not be detected."
            )
            return False

        # Add our initialized plugin to our server listings
        self.configs.append(instance)

        # Return our status
        return True

    def servers(
        self,
        tag: str | list[str] = common.MATCH_ALL_TAG,
        match_always: bool = True,
        *args: Any,
        **kwargs: Any,
    ) -> list[NotifyBase]:
        """Returns all of our servers dynamically build based on parsed
        configuration.

        If a tag is specified, it applies to the configuration sources
        themselves and not the notification services inside them.

        This is for filtering the configuration files polled for results.

        If the anytag is set, then any notification that is found set with that
        tag are included in the response.
        """

        # A match_always flag allows us to pick up on our 'any' keyword
        # and notify these services under all circumstances
        match_always = common.MATCH_ALWAYS_TAG if match_always else None

        # Build our tag setup
        #   - top level entries are treated as an 'or'
        #   - second level (or more) entries are treated as 'and'
        #
        #   examples:
        #     tag="tagA, tagB"                = tagA or tagB
        #     tag=['tagA', 'tagB']            = tagA or tagB
        #     tag=[('tagA', 'tagC'), 'tagB']  = (tagA and tagC) or tagB
        #     tag=[('tagB', 'tagC')]          = tagB and tagC

        response = []

        for entry in self.configs:

            # Apply our tag matching based on our defined logic
            if is_exclusive_match(
                logic=tag,
                data=entry.tags,
                match_all=common.MATCH_ALL_TAG,
                match_always=match_always,
            ):
                # Build ourselves a list of services dynamically and return the
                # as a list
                response.extend(entry.servers())

        return response

    @staticmethod
    def instantiate(
        url: str,
        asset: AppriseAsset | None = None,
        tag: str | list[str] | None = None,
        cache: bool | int | None = None,
        recursion: int = 0,
        insecure_includes: bool = False,
        suppress_exceptions: bool = True,
    ) -> ConfigBase | None:
        """Returns the instance of a instantiated configuration plugin based on
        the provided Config URL.

        If the url fails to be parsed, then None is returned.
        """
        # Attempt to acquire the schema at the very least to allow our
        # configuration based urls.
        schema = GET_SCHEMA_RE.match(url)
        if schema is None:
            # Plan B is to assume we're dealing with a file
            schema = "file"
            url = f"{schema}://{URLBase.quote(url)}"

        else:
            # Ensure our schema is always in lower case
            schema = schema.group("schema").lower()

            # Some basic validation
            if schema not in C_MGR:
                logger.warning(f"Unsupported schema {schema}.")
                return None

        # Parse our url details of the server object as dictionary containing
        # all of the information parsed from our URL
        results = C_MGR[schema].parse_url(url)

        if not results:
            # Failed to parse the server URL
            logger.warning(f"Unparseable URL {url}.")
            return None

        # Build a list of tags to associate with the newly added notifications
        results["tag"] = set(parse_list(tag))

        # Prepare our Asset Object
        results["asset"] = (
            asset if isinstance(asset, AppriseAsset) else AppriseAsset()
        )

        if cache is not None:
            # Force an over-ride of the cache value to what we have specified
            results["cache"] = cache

        # Recursion can never be parsed from the URL
        results["recursion"] = recursion

        # Insecure includes flag can never be parsed from the URL
        results["insecure_includes"] = insecure_includes

        if suppress_exceptions:
            try:
                # Attempt to create an instance of our plugin using the parsed
                # URL information
                cfg_plugin = C_MGR[results["schema"]](**results)

            except Exception:
                # the arguments are invalid or can not be used.
                logger.warning(f"Could not load URL: {url}")
                return None

        else:
            # Attempt to create an instance of our plugin using the parsed
            # URL information but don't wrap it in a try catch
            cfg_plugin = C_MGR[results["schema"]](**results)

        return cfg_plugin

    def clear(self) -> None:
        """Empties our configuration list."""
        self.configs[:] = []

    def server_pop(self, index: int) -> NotifyBase:
        """Removes an indexed Apprise Notification from the servers."""

        # Tracking variables
        prev_offset = -1
        offset = prev_offset

        for entry in self.configs:
            servers = entry.servers(cache=True)
            if len(servers) > 0:
                # Acquire a new maximum offset to work with
                offset = prev_offset + len(servers)

                if offset >= index:
                    # we can pop an notification from our config stack
                    return entry.pop(
                        index
                        if prev_offset == -1
                        else (index - prev_offset - 1)
                    )

                # Update our old offset
                prev_offset = offset

        # If we reach here, then we indexed out of range
        raise IndexError("list index out of range")

    def pop(self, index: int = -1) -> ConfigBase:
        """Removes an indexed Apprise Configuration from the stack and returns
        it.

        By default, the last element is removed from the list
        """
        # Remove our entry
        return self.configs.pop(index)

    def __getitem__(self, index: int) -> ConfigBase:
        """Returns the indexed config entry of a loaded apprise
        configuration."""
        return self.configs[index]

    def __bool__(self) -> bool:
        """Allows the Apprise object to be wrapped in an 'if statement'.

        True is returned if at least one service has been loaded.
        """
        return bool(self.configs)

    def __iter__(self):  # type: () -> Iterator[ConfigBase]
        """Returns an iterator to our config list."""
        return iter(self.configs)

    def __len__(self) -> int:
        """Returns the number of config entries loaded."""
        return len(self.configs)


================================================
FILE: apprise/asset.py
================================================
# BSD 2-Clause License
#
# Apprise - Push Notification Library.
# Copyright (c) 2026, Chris Caron <lead2gold@gmail.com>
#
# Redistribution and use in source and binary forms, with or without
# modification, are permitted provided that the following conditions are met:
#
# 1. Redistributions of source code must retain the above copyright notice,
#    this list of conditions and the following disclaimer.
#
# 2. Redistributions in binary form must reproduce the above copyright notice,
#    this list of conditions and the following disclaimer in the documentation
#    and/or other materials provided with the distribution.
#
# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
# AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
# IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
# ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE
# LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
# CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
# SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
# INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
# CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
# ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
# POSSIBILITY OF SUCH DAMAGE.

from datetime import datetime, tzinfo
from os.path import abspath, dirname, isfile, join
import re
from typing import Any, Optional, Union
from uuid import uuid4

from .common import (
    NotifyFormat,
    NotifyImageSize,
    NotifyType,
    PersistentStoreMode,
)
from .manager_plugins import NotificationManager
from .utils.time import zoneinfo

# Grant access to our Notification Manager Singleton
N_MGR = NotificationManager()


class AppriseAsset:
    """Provides a supplimentary class that can be used to provide extra
    information and details that can be used by Apprise such as providing an
    alternate location to where images/icons can be found and the URL masks.

    Any variable that starts with an underscore (_) can only be initialized by
    this class manually and will/can not be parsed from a configuration file.
    """

    # Application Identifier
    app_id = "Apprise"

    # Application Description
    app_desc = "Apprise Notifications"

    # Provider URL
    app_url = "https://github.com/caronc/apprise"

    # A Simple Mapping of Colors; For every NOTIFY_TYPE identified,
    # there should be a mapping to it's color here:
    html_notify_map = {
        NotifyType.INFO: "#3AA3E3",
        NotifyType.SUCCESS: "#3AA337",
        NotifyType.FAILURE: "#A32037",
        NotifyType.WARNING: "#CACF29",
    }

    # The default color to return if a mapping isn't found in our table above
    default_html_color = "#888888"

    # Ascii Notification
    ascii_notify_map = {
        NotifyType.INFO: "[i]",
        NotifyType.SUCCESS: "[+]",
        NotifyType.FAILURE: "[!]",
        NotifyType.WARNING: "[~]",
    }

    # The default ascii to return if a mapping isn't found in our table above
    default_ascii_chars = "[?]"

    # The default image extension to use
    default_extension = ".png"

    # The default image size if one isn't specified
    default_image_size = NotifyImageSize.XY_256

    # The default theme
    theme = "default"

    # Image URL Mask
    image_url_mask = (
        "https://github.com/caronc/apprise/raw/master/apprise/assets/"
        "themes/{THEME}/apprise-{TYPE}-{XY}{EXTENSION}"
    )

    # Application Logo
    image_url_logo = (
        "https://github.com/caronc/apprise/raw/master/apprise/assets/"
        "themes/{THEME}/apprise-logo.png"
    )

    # Image Path Mask
    image_path_mask = abspath(
        join(
            dirname(__file__),
            "assets",
            "themes",
            "{THEME}",
            "apprise-{TYPE}-{XY}{EXTENSION}",
        )
    )

    # This value can also be set on calls to Apprise.notify(). This allows
    # you to let Apprise upfront the type of data being passed in.  This
    # must be of type NotifyFormat. Possible values could be:
    # - NotifyFormat.TEXT
    # - NotifyFormat.MARKDOWN
    # - NotifyFormat.HTML
    # - None
    #
    # If no format is specified (hence None), then no special pre-formatting
    # actions will take place during a notification. This has been and always
    # will be the default.
    body_format = None

    # Always attempt to send notifications asynchronous (as the same time
    # if possible)
    # This is a Python 3 supported option only. If set to False, then
    # notifications are sent sequentially (one after another)
    async_mode = True

    # Support :smile:, and other alike keywords swapping them for their
    # unicode value. A value of None leaves the interpretation up to the
    # end user to control (allowing them to specify emojis=yes on the
    # URL)
    interpret_emojis = None

    # Whether or not to interpret escapes found within the input text prior
    # to passing it upstream. Such as converting \t to an actual tab and \n
    # to a new line.
    interpret_escapes = False

    # Defines the encoding of the content passed into Apprise
    encoding = "utf-8"

    # Automatically generate our Pretty Good Privacy (PGP) keys if one isn't
    # present and our environment configuration allows for it.
    # For example, a case where the environment wouldn't allow for it would be
    # if Persistent Storage was set to `memory`
    pgp_autogen = True

    # Automatically generate our Privacy Enhanced Mail (PEM) keys if one isn't
    # present and our environment configuration allows for it.
    # For example, a case where the environment wouldn't allow for it would be
    # if Persistent Storage was set to `memory`
    pem_autogen = True

    # For more detail see CWE-312 @
    #    https://cwe.mitre.org/data/definitions/312.html
    #
    # By enabling this, the logging output has additional overhead applied to
    # it preventing secure password and secret information from being
    # displayed in the logging. Since there is overhead involved in performing
    # this cleanup; system owners who run in a very isolated environment may
    # choose to disable this for a slight performance bump. It is recommended
    # that you leave this option as is otherwise.
    secure_logging = True

    # Optionally specify one or more path to attempt to scan for Python modules
    # By default, no paths are scanned.
    __plugin_paths = []

    # Optionally set the location of the persistent storage
    # By default there is no path and thus persistent storage is not used
    __storage_path = None

    # Optionally define the default salt to apply to all persistent storage
    # namespace generation (unless over-ridden)
    __storage_salt = b""

    # Optionally define the namespace length of the directories created by
    # the storage. If this is set to zero, then the length is pre-determined
    # by the generator (sha1, md5, sha256, etc)
    __storage_idlen = 8

    # Set storage to auto
    __storage_mode = PersistentStoreMode.AUTO

    # All internal/system flags are prefixed with an underscore (_)
    # These can only be initialized using Python libraries and are not picked
    # up from (yaml) configuration files (if set)

    # An internal counter that is used by AppriseAPI
    # (https://github.com/caronc/apprise-api). The idea is to allow one
    # instance of AppriseAPI to call another, but to track how many times
    # this occurs. It's intent is to prevent a loop where an AppriseAPI
    # Server calls itself (or loops indefinitely)
    _recursion = 0

    # A unique identifer we can use to associate our calling source
    _uid = str(uuid4())

    # Default timezone to use (pass in timezone value)
    # A list of timezones can be found here:
    # https://en.wikipedia.org/wiki/List_of_tz_database_time_zones
    # You can specify things such as 'America/Montreal'
    # If no timezone is specified, then the one detected on the system
    # is uzed
    _tzinfo = None

    def __init__(
        self,
        plugin_paths: Optional[list[str]] = None,
        storage_path: Optional[str] = None,
        storage_mode: Optional[Union[str, PersistentStoreMode]] = None,
        storage_salt: Optional[Union[str, bytes]] = None,
        storage_idlen: Optional[int] = None,
        timezone: Optional[Union[str, tzinfo]] = None,
        **kwargs: Any
    ) -> None:
        """Asset Initialization."""
        # Assign default arguments if specified
        for key, value in kwargs.items():
            if not hasattr(AppriseAsset, key):
                raise AttributeError(
                    f"AppriseAsset init(): An invalid key {key} was specified."
                )

            setattr(self, key, value)

        if plugin_paths:
            # Load any decorated modules if defined
            self.__plugin_paths = plugin_paths
            N_MGR.module_detection(plugin_paths)

        if storage_path:
            # Define our persistent storage path
            self.__storage_path = storage_path

        if storage_mode:
            # Define how our persistent storage behaves
            try:
                self.__storage_mode = (
                    storage_mode if isinstance(storage_mode, NotifyFormat)
                    else PersistentStoreMode(storage_mode.lower())
                )

            except (AttributeError, ValueError, TypeError):
                err = (
                    f"An invalid persistent store mode ({storage_mode}) was "
                    "specified.")
                raise AttributeError(err) from None

        if isinstance(storage_idlen, int):
            # Define the number of characters utilized from our namespace lengh
            if storage_idlen < 0:
                # Unsupported type
                raise ValueError(
                    "AppriseAsset storage_idlen(): Value must "
                    "be an integer and > 0"
                )

            # Store value
            self.__storage_idlen = storage_idlen

        if isinstance(timezone, tzinfo):
            self._tzinfo = timezone

        elif timezone is not None:
            self._tzinfo = zoneinfo(timezone)
            if not self._tzinfo:
                raise AttributeError(
                    "AppriseAsset timezone provided is invalid") from None
        else:
            # Default our timezone to what is detected on the system
            self._tzinfo = datetime.now().astimezone().tzinfo

        if storage_salt is not None:
            # Define the number of characters utilized from our namespace lengh

            if isinstance(storage_salt, bytes):
                self.__storage_salt = storage_salt

            elif isinstance(storage_salt, str):
                try:
                    self.__storage_salt = storage_salt.encode(self.encoding)

                except UnicodeEncodeError:
                    # Bad data; don't pass it along
                    raise ValueError(
                        "AppriseAsset namespace_salt(): "
                        "Value provided could not be encoded"
                    ) from None

            else:  # Unsupported
                raise ValueError(
                    "AppriseAsset namespace_salt(): Value provided must be "
                    "string or bytes object"
                )

    def color(
        self,
        notify_type: NotifyType,
        color_type: Optional[type] = None,
    ) -> Union[str, int, tuple[int, int, int]]:
        """Returns an HTML mapped color based on passed in notify type.

        if color_type is:
           None    then a standard hex string is returned as
                   a string format ('#000000').

           int     then the integer representation is returned
           tuple   then the the red, green, blue is returned in a tuple
        """

        # Attempt to get the type, otherwise return a default grey
        # if we couldn't look up the entry
        color = self.html_notify_map.get(
            notify_type, self.default_html_color)
        if color_type is None:
            # This is the default return type
            return color

        elif color_type is int:
            # Convert the color to integer
            return AppriseAsset.hex_to_int(color)

        # The only other type is tuple
        elif color_type is tuple:
            return AppriseAsset.hex_to_rgb(color)

        # Unsupported type
        raise ValueError(
            "AppriseAsset html_color(): An invalid color_type was specified."
        )

    def ascii(self, notify_type: NotifyType) -> str:
        """Returns an ascii representation based on passed in notify type."""
        # look our response up
        return self.ascii_notify_map.get(
            notify_type, self.default_ascii_chars)

    def image_url(
        self,
        notify_type: NotifyType,
        image_size: Optional[NotifyImageSize] = None,
        logo: bool = False,
        extension: Optional[str] = None,
    ) -> Optional[str]:
        """Apply our mask to our image URL.

        if logo is set to True, then the logo_url is used instead
        """

        url_mask = self.image_url_logo if logo else self.image_url_mask
        if not url_mask:
            # No image to return
            return None

        if extension is None:
            extension = self.default_extension

        if image_size is None:
            image_size = self.default_image_size

        re_map = {
            "{THEME}": self.theme if self.theme else "",
            "{TYPE}": notify_type.value,
            "{XY}": image_size.value,
            "{EXTENSION}": extension,
        }

        # Iterate over above list and store content accordingly
        re_table = re.compile(
            r"(" + "|".join(re_map.keys()) + r")",
            re.IGNORECASE,
        )

        return re_table.sub(lambda x: re_map[x.group()], url_mask)

    def image_path(
        self,
        notify_type: NotifyType,
        image_size: NotifyImageSize,
        must_exist: bool = True,
        extension: Optional[str] = None,
    ) -> Optional[str]:
        """Apply our mask to our image file path."""

        if not self.image_path_mask:
            # No image to return
            return None

        if extension is None:
            extension = self.default_extension

        re_map = {
            "{THEME}": self.theme if self.theme else "",
            "{TYPE}": notify_type.value,
            "{XY}": image_size.value,
            "{EXTENSION}": extension,
        }

        # Iterate over above list and store content accordingly
        re_table = re.compile(
            r"(" + "|".join(re_map.keys()) + r")",
            re.IGNORECASE,
        )

        # Acquire our path
        path = re_table.sub(lambda x: re_map[x.group()], self.image_path_mask)
        if must_exist and not isfile(path):
            return None

        # Return what we parsed
        return path

    def image_raw(
        self,
        notify_type: NotifyType,
        image_size: NotifyImageSize,
        extension: Optional[str] = None,
    ) -> Optional[bytes]:
        """Returns the raw image if it can (otherwise the function returns
        None)"""

        path = self.image_path(
            notify_type=notify_type,
            image_size=image_size,
            extension=extension,
        )
        if path:
            try:
                with open(path, "rb") as fd:
                    return fd.read()

            except OSError:
                # We can't access the file
                return None

        return None

    def details(self) -> dict[str, str]:
        """Returns the details associated with the AppriseAsset object."""
        return {
            "app_id": self.app_id,
            "app_desc": self.app_desc,
            "default_extension": self.default_extension,
            "theme": self.theme,
            "image_path_mask": self.image_path_mask,
            "image_url_mask": self.image_url_mask,
            "image_url_logo": self.image_url_logo,
        }

    @staticmethod
    def hex_to_rgb(value: str) -> tuple[int, int, int]:
        """Takes a hex string (such as #00ff00) and returns a tuple in the form
        of (red, green, blue)

        eg: #00ff00 becomes : (0, 65535, 0)
        """
        value = value.lstrip("#")
        lv = len(value)
        return tuple(
            int(value[i : i + lv // 3], 16) for i in range(0, lv, lv // 3)
        )

    @staticmethod
    def hex_to_int(value: str) -> int:
        """Takes a hex string (such as #00ff00) and returns its integer
        equivalent.

        eg: #00000f becomes : 15
        """
        return int(value.lstrip("#"), 16)

    @property
    def plugin_paths(self) -> list[str]:
        """Return the plugin paths defined."""
        return self.__plugin_paths

    @property
    def storage_path(self) -> Optional[str]:
        """Return the persistent storage path defined."""
        return self.__storage_path

    @property
    def storage_mode(self) -> PersistentStoreMode:
        """Return the persistent storage mode defined."""

        return self.__storage_mode

    @property
    def storage_salt(self) -> bytes:
        """Return the provided namespace salt; this is always of type bytes."""
        return self.__storage_salt

    @property
    def storage_idlen(self) -> int:
        """Return the persistent storage id length."""

        return self.__storage_idlen

    @property
    def tzinfo(self) -> tzinfo:
        """Return the timezone object"""
        return self._tzinfo


================================================
FILE: apprise/assets/NotifyXML-1.0.xsd
================================================
<?xml version="1.0" encoding="utf-8"?>
<xs:schema attributeFormDefault="unqualified" elementFormDefault="qualified" xmlns:xs="http://www.w3.org/2001/XMLSchema">
  <xs:import namespace="http://schemas.xmlsoap.org/soap/envelope/" schemaLocation="http://schemas.xmlsoap.org/soap/envelope/"/>
  <xs:element name="Notification">
    <xs:complexType>
      <xs:sequence>
        <xs:element name="Version" type="xs:string" />
        <xs:element name="Subject" type="xs:string" />
        <xs:element name="MessageType">
          <xs:simpleType>
            <xs:restriction base="xs:string">
              <xs:enumeration value="success" />
              <xs:enumeration value="failure" />
              <xs:enumeration value="info" />
              <xs:enumeration value="warning" />
            </xs:restriction>
          </xs:simpleType>
        </xs:element>
        <xs:element name="Message" type="xs:string" />
      </xs:sequence>
    </xs:complexType>
  </xs:element>
</xs:schema>


================================================
FILE: apprise/assets/NotifyXML-1.1.xsd
================================================
<?xml version="1.0" encoding="utf-8"?>
<xs:schema attributeFormDefault="unqualified" elementFormDefault="qualified" xmlns:xs="http://www.w3.org/2001/XMLSchema">
  <xs:import namespace="http://schemas.xmlsoap.org/soap/envelope/" schemaLocation="http://schemas.xmlsoap.org/soap/envelope/"/>
  <xs:element name="Notification">
    <xs:complexType>
      <xs:sequence>
        <xs:element name="Version" type="xs:string" />
        <xs:element name="Subject" type="xs:string" />
        <xs:element name="MessageType">
          <xs:simpleType>
            <xs:restriction base="xs:string">
              <xs:enumeration value="success" />
              <xs:enumeration value="failure" />
              <xs:enumeration value="info" />
              <xs:enumeration value="warning" />
            </xs:restriction>
          </xs:simpleType>
        </xs:element>
        <xs:element name="Message" type="xs:string" />
        <xs:element name="Attachments" minOccurs="0">
          <xs:complexType>
            <xs:sequence>
              <xs:element name="Attachment" minOccurs="0" maxOccurs="unbounded">
                <xs:complexType>
                  <xs:simpleContent>
                    <xs:extension base="xs:string">
                      <xs:attribute name="mimetype" type="xs:string" use="required"/>
                      <xs:attribute name="filename" type="xs:string" use="required"/>
                    </xs:extension>
                  </xs:simpleContent>
                </xs:complexType>
              </xs:element> 
            </xs:sequence>
            <xs:attribute name="encoding" type="xs:string" use="required"/>
          </xs:complexType>
        </xs:element>
      </xs:sequence>
    </xs:complexType>
  </xs:element>
</xs:schema>


================================================
FILE: apprise/attachment/__init__.py
================================================
# BSD 2-Clause License
#
# Apprise - Push Notification Library.
# Copyright (c) 2026, Chris Caron <lead2gold@gmail.com>
#
# Redistribution and use in source and binary forms, with or without
# modification, are permitted provided that the following conditions are met:
#
# 1. Redistributions of source code must retain the above copyright notice,
#    this list of conditions and the following disclaimer.
#
# 2. Redistributions in binary form must reproduce the above copyright notice,
#    this list of conditions and the following disclaimer in the documentation
#    and/or other materials provided with the distribution.
#
# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
# AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
# IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
# ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE
# LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
# CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
# SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
# INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
# CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
# ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
# POSSIBILITY OF SUCH DAMAGE.

# Used for testing
from ..manager_attachment import AttachmentManager
from .base import AttachBase

# Initalize our Attachment Manager Singleton
A_MGR = AttachmentManager()

__all__ = [
    # Reference
    "AttachBase",
    "AttachmentManager",
]


================================================
FILE: apprise/attachment/base.py
================================================
# BSD 2-Clause License
#
# Apprise - Push Notification Library.
# Copyright (c) 2026, Chris Caron <lead2gold@gmail.com>
#
# Redistribution and use in source and binary forms, with or without
# modification, are permitted provided that the following conditions are met:
#
# 1. Redistributions of source code must retain the above copyright notice,
#    this list of conditions and the following disclaimer.
#
# 2. Redistributions in binary form must reproduce the above copyright notice,
#    this list of conditions and the following disclaimer in the documentation
#    and/or other materials provided with the distribution.
#
# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
# AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
# IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
# ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE
# LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
# CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
# SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
# INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
# CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
# ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
# POSSIBILITY OF SUCH DAMAGE.

import base64
import contextlib
import mimetypes
import os
import time

from .. import exception
from ..common import ContentLocation
from ..locale import gettext_lazy as _
from ..url import URLBase
from ..utils.parse import parse_bool


class AttachBase(URLBase):
    """This is the base class for all supported attachment types."""

    # For attachment type detection; this amount of data is read into memory
    # 128KB (131072B)
    max_detect_buffer_size = 131072

    # Unknown mimetype
    unknown_mimetype = "application/octet-stream"

    # Our filename when we can't otherwise determine one
    unknown_filename = "apprise-attachment"

    # Our filename extension when we can't otherwise determine one
    unknown_filename_extension = ".obj"

    # The strict argument is a flag specifying whether the list of known MIME
    # types is limited to only the official types registered with IANA. When
    # strict is True, only the IANA types are supported; when strict is False
    # (the default), some additional non-standard but commonly used MIME types
    # are also recognized.
    strict = False

    # The maximum file-size we will accept for an attachment size. If this is
    # set to zero (0), then no check is performed
    # 1 MB = 1048576 bytes
    # 5 MB = 5242880 bytes
    # 1 GB = 1048576000 bytes
    max_file_size = 1048576000

    # By default all attachments types are inaccessible.
    # Developers of items identified in the attachment plugin directory
    # are requried to set a location
    location = ContentLocation.INACCESSIBLE

    # Here is where we define all of the arguments we accept on the url
    # such as: schema://whatever/?overflow=upstream&format=text
    # These act the same way as tokens except they are optional and/or
    # have default values set if mandatory. This rule must be followed
    template_args = {
        "cache": {
            "name": _("Cache Age"),
            "type": "int",
            # We default to (600) which means we cache for 10 minutes
            "default": 600,
        },
        "mime": {
            "name": _("Forced Mime Type"),
            "type": "string",
        },
        "name": {
            "name": _("Forced File Name"),
            "type": "string",
        },
        "verify": {
            "name": _("Verify SSL"),
            # SSL Certificate Authority Verification
            "type": "bool",
            # Provide a default
            "default": True,
        },
    }

    def __init__(self, name=None, mimetype=None, cache=None, **kwargs):
        """Initialize some general logging and common server arguments that
        will keep things consistent when working with the configurations that
        inherit this class.

        Optionally provide a filename to over-ride name associated with the
        actual file retrieved (from where-ever).

        The mime-type is automatically detected, but you can over-ride this by
        explicitly stating what it should be.

        By default we cache our responses so that subsiquent calls does not
        cause the content to be retrieved again.  For local file references
        this makes no difference at all.  But for remote content, this does
        mean more then one call can be made to retrieve the (same) data.  This
        method can be somewhat inefficient if disabled.  Only disable caching
        if you understand the consequences.

        You can alternatively set the cache value to an int identifying the
        number of seconds the previously retrieved can exist for before it
        should be considered expired.
        """

        super().__init__(**kwargs)

        if not mimetypes.inited:
            # Ensure mimetypes has been initialized
            mimetypes.init()

        # Attach Filename (does not have to be the same as path)
        self._name = name

        # The mime type of the attached content.  This is detected if not
        # otherwise specified.
        self._mimetype = mimetype

        # The detected_mimetype, this is only used as a fallback if the
        # mimetype wasn't forced by the user
        self.detected_mimetype = None

        # The detected filename by calling child class. A detected filename
        # is always used if no force naming was specified.
        self.detected_name = None

        # Absolute path to attachment
        self.download_path = None

        # Track open file pointers
        self.__pointers = set()

        # Set our cache flag; it can be True, False, None, or a (positive)
        # integer... nothing else
        if cache is not None:
            try:
                self.cache = cache if isinstance(cache, bool) else int(cache)

            except (TypeError, ValueError):
                err = f"An invalid cache value ({cache}) was specified."
                self.logger.warning(err)
                raise TypeError(err) from None

            # Some simple error checking
            if self.cache < 0:
                err = f"A negative cache value ({cache}) was specified."
                self.logger.warning(err)
                raise TypeError(err)

        else:
            self.cache = None

        # Validate mimetype if specified
        if self._mimetype and (
                next(
                    (
                        t
                        for t in mimetypes.types_map.values()
                        if self._mimetype == t
                    ),
                    None,
                )
                is None):
            err = f"An invalid mime-type ({mimetype}) was specified."
            self.logger.warning(err)
            raise TypeError(err)

        return

    @property
    def path(self):
        """Returns the absolute path to the filename.

        If this is not known or is know but has been considered expired (due to
        cache setting), then content is re-retrieved prior to returning.
        """

        if not self.exists():
            # we could not obtain our path
            return None

        return self.download_path

    @property
    def name(self):
        """Returns the filename."""
        if self._name:
            # return our fixed content
            return self._name

        if not self.exists():
            # we could not obtain our name
            return None

        if not self.detected_name:
            # If we get here, our download was successful but we don't have a
            # filename based on our content.
            ext = mimetypes.guess_extension(self.mimetype)
            self.detected_name = (
                f"{self.unknown_filename}"
                f"{ext if ext else self.unknown_filename_extension}"
            )

        return self.detected_name

    @property
    def mimetype(self):
        """Returns mime type (if one is present).

        Content is cached once determied to prevent overhead of future calls.
        """
        if not self.exists():
            # we could not obtain our attachment
            return None

        if self._mimetype:
            # return our pre-calculated cached content
            return self._mimetype

        if not self.detected_mimetype:
            # guess_type() returns: (type, encoding) and sets type to None
            # if it can't otherwise determine it.
            with contextlib.suppress(TypeError):
                # Directly reference _name and detected_name to prevent
                # recursion loop (as self.name calls this function)
                self.detected_mimetype, _ = mimetypes.guess_type(
                    self._name if self._name else self.detected_name,
                    strict=self.strict,
                )

        # Return our mime type
        return (
            self.detected_mimetype
            if self.detected_mimetype
            else self.unknown_mimetype
        )

    def exists(self, retrieve_if_missing=True):
        """Simply returns true if the object has downloaded and stored the
        attachment AND the attachment has not expired."""
        if self.location == ContentLocation.INACCESSIBLE:
            # our content is inaccessible
            return False

        cache = (
            self.template_args["cache"]["default"]
            if self.cache is None
            else self.cache
        )

        try:
            if (
                self.download_path
                and os.path.isfile(self.download_path)
                and cache
            ):

                # We have enough reason to look further into our cached content
                # and verify it has not expired.
                if cache is True:
                    # return our fixed content as is; we will always cache it
                    return True

                # Verify our cache time to determine whether we will get our
                # content again.
                age_in_sec = time.time() - os.stat(self.download_path).st_mtime
                if age_in_sec <= cache:
                    return True

        except OSError:
            # The file is not present
            pass

        return False if not retrieve_if_missing else self.download()

    def base64(self, encoding="ascii"):
        """Returns the attachment object as a base64 string otherwise None is
        returned if an error occurs.

        If encoding is set to None, then it is not encoded when returned
        """
        if not self:
            # We could not access the attachment
            self.logger.error(
                f"Could not access attachment {self.url(privacy=True)}."
            )
            raise exception.AppriseFileNotFound("Attachment Missing")

        try:
            with self.open() as f:
                # Prepare our Attachment in Base64
                return (
                    base64.b64encode(f.read()).decode(encoding)
                    if encoding
                    else base64.b64encode(f.read())
                )

        except (FileNotFoundError):
            # We no longer have a path to open
            raise exception.AppriseFileNotFound("Attachment Missing") from None

        except (TypeError, OSError) as e:
            self.logger.warning(
                "An I/O error occurred while reading {}.".format(
                    self.name if self else "attachment"
                )
            )
            self.logger.debug(f"I/O Exception: {e!s}")
            raise exception.AppriseDiskIOError(
                "Attachment Access Error") from e

    def invalidate(self):
        """Release any temporary data that may be open by child classes.
        Externally fetched content should be automatically cleaned up when this
        function is called.

        This function should also reset the following entries to None:
          - detected_name : Should identify a human readable filename
          - download_path: Must contain a absolute path to content
          - detected_mimetype: Should identify mimetype of content
        """

        # Remove all open pointers
        while self.__pointers:
            self.__pointers.pop().close()

        self.detected_name = None
        self.download_path = None
        self.detected_mimetype = None
        return

    def download(self):
        """This function must be over-ridden by inheriting classes.

        Inherited classes MUST populate:
          - detected_name: Should identify a human readable filename
          - download_path: Must contain a absolute path to content
          - detected_mimetype: Should identify mimetype of content

        If a download fails, you should ensure these values are set to None.
        """
        raise NotImplementedError(
            "download() is implimented by the child class."
        )

    def open(self, mode="rb"):
        """Return our file pointer and track it (we'll auto close later)"""
        pointer = open(self.path, mode=mode)  # noqa: SIM115
        self.__pointers.add(pointer)
        return pointer

    def chunk(self, size=5242880):
        """A Generator that yield chunks of a file with the specified size.

        By default the chunk size is set to 5MB (5242880 bytes)
        """

        with self.open() as file:
            while True:
                chunk = file.read(size)
                if not chunk:
                    break

                yield chunk

    def __enter__(self):
        """Support with keyword."""
        return self.open()

    def __exit__(self, value_type, value, traceback):
        """Stub to do nothing; but support exit of with statement
        gracefully."""
        return

    @staticmethod
    def parse_url(url, verify_host=True, mimetype_db=None, sanitize=True):
        """Parses the URL and returns it broken apart into a dictionary.

        This is very specific and customized for Apprise.

        Args:
            url (str): The URL you want to fully parse.
            verify_host (:obj:`bool`, optional): a flag kept with the parsed
                 URL which some child classes will later use to verify SSL
                 keys (if SSL transactions take place).  Unless under very
                 specific circumstances, it is strongly recomended that
                 you leave this default value set to True.

        Returns:
            A dictionary is returned containing the URL fully parsed if
            successful, otherwise None is returned.
        """

        results = URLBase.parse_url(
            url, verify_host=verify_host, sanitize=sanitize
        )

        if not results:
            # We're done; we failed to parse our url
            return results

        # Allow overriding the default config mime type
        if "mime" in results["qsd"]:
            results["mimetype"] = (
                results["qsd"].get("mime", "").strip().lower()
            )

        # Allow overriding the default file name
        if "name" in results["qsd"]:
            results["name"] = results["qsd"].get("name", "").strip().lower()

        # Our cache value
        if "cache" in results["qsd"]:
            # First try to get it's integer value
            try:
                results["cache"] = int(results["qsd"]["cache"])

            except (ValueError, TypeError):
                # No problem, it just isn't an integer; now treat it as a bool
                # instead:
                results["cache"] = parse_bool(results["qsd"]["cache"])

        return results

    def __len__(self):
        """Returns the filesize of the attachment."""
        if not self:
            return 0

        try:
            return os.path.getsize(self.path) if self.path else 0

        except OSError:
            # OSError can occur if the file is inaccessible
            return 0

    def __bool__(self):
        """Allows the Apprise object to be wrapped in an based 'if statement'.

        True is returned if our content was downloaded correctly.
        """
        return bool(self.path)

    def __del__(self):
        """Perform any house cleaning."""
        self.invalidate()


================================================
FILE: apprise/attachment/file.py
================================================
# BSD 2-Clause License
#
# Apprise - Push Notification Library.
# Copyright (c) 2026, Chris Caron <lead2gold@gmail.com>
#
# Redistribution and use in source and binary forms, with or without
# modification, are permitted provided that the following conditions are met:
#
# 1. Redistributions of source code must retain the above copyright notice,
#    this list of conditions and the following disclaimer.
#
# 2. Redistributions in binary form must reproduce the above copyright notice,
#    this list of conditions and the following disclaimer in the documentation
#    and/or other materials provided with the distribution.
#
# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
# AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
# IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
# ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE
# LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
# CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
# SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
# INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
# CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
# ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
# POSSIBILITY OF SUCH DAMAGE.

import os
import re

from ..common import ContentLocation
from ..locale import gettext_lazy as _
from ..utils.disk import path_decode
from .base import AttachBase


class AttachFile(AttachBase):
    """A wrapper for File based attachment sources."""

    # The default descriptive name associated with the service
    service_name = _("Local File")

    # The default protocol
    protocol = "file"

    # Content is local to the same location as the apprise instance
    # being called (server-side)
    location = ContentLocation.LOCAL

    def __init__(self, path, **kwargs):
        """Initialize Local File Attachment Object."""
        super().__init__(**kwargs)

        # Store path but mark it dirty since we have not performed any
        # verification at this point.
        self.dirty_path = path_decode(path)

        # Track our file as it was saved
        self.__original_path = os.path.normpath(path)
        return

    def url(self, privacy=False, *args, **kwargs):
        """Returns the URL built dynamically based on specified arguments."""

        # Define any URL parameters
        params = {}

        if self._mimetype:
            # A mime-type was enforced
            params["mime"] = self._mimetype

        if self._name:
            # A name was enforced
            params["name"] = self._name

        return "file://{path}{params}".format(
            path=self.quote(self.__original_path),
            params=(
                "?{}".format(self.urlencode(params, safe="/"))
                if params
                else ""
            ),
        )

    def download(self, **kwargs):
        """Perform retrieval of our data.

        For file base attachments, our data already exists, so we only need to
        validate it.
        """

        if self.location == ContentLocation.INACCESSIBLE:
            # our content is inaccessible
            return False

        # Ensure any existing content set has been invalidated
        self.invalidate()

        try:
            if not os.path.isfile(self.dirty_path):
                return False

        except OSError:
            return False

        if (
            self.max_file_size > 0
            and os.path.getsize(self.dirty_path) > self.max_file_size
        ):

            # The content to attach is to large
            self.logger.error(
                "Content exceeds allowable maximum file length"
                f" ({int(self.max_file_size / 1024)}KB):"
                f" {self.url(privacy=True)}"
            )

            # Return False (signifying a failure)
            return False

        # We're good to go if we get here. Set our minimum requirements of
        # a call do download() before returning a success
        self.download_path = self.dirty_path
        self.detected_name = os.path.basename(self.download_path)

        # We don't need to set our self.detected_mimetype as it can be
        # pulled at the time it's needed based on the detected_name
        return True

    @staticmethod
    def parse_url(url):
        """Parses the URL so that we can handle all different file paths and
        return it as our path object."""

        results = AttachBase.parse_url(url, verify_host=False)
        if not results:
            # We're done early; it's not a good URL
            return results

        match = re.match(r"file://(?P<path>[^?]+)(\?.*)?", url, re.I)
        if not match:
            return None

        results["path"] = AttachFile.unquote(match.group("path"))
        return results


================================================
FILE: apprise/attachment/http.py
================================================
# BSD 2-Clause License
#
# Apprise - Push Notification Library.
# Copyright (c) 2026, Chris Caron <lead2gold@gmail.com>
#
# Redistribution and use in source and binary forms, with or without
# modification, are permitted provided that the following conditions are met:
#
# 1. Redistributions of source code must retain the above copyright notice,
#    this list of conditions and the following disclaimer.
#
# 2. Redistributions in binary form must reproduce the above copyright notice,
#    this list of conditions and the following disclaimer in the documentation
#    and/or other materials provided with the distribution.
#
# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
# AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
# IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
# ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE
# LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
# CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
# SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
# INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
# CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
# ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
# POSSIBILITY OF SUCH DAMAGE.

import contextlib
import os
import re
from tempfile import NamedTemporaryFile
import threading

import requests

from ..common import ContentLocation
from ..locale import gettext_lazy as _
from ..url import PrivacyMode
from .base import AttachBase


class AttachHTTP(AttachBase):
    """A wrapper for HTTP based a
Download .txt
gitextract_a_8zomhz/

├── .codecov.yml
├── .github/
│   ├── FUNDING.yml
│   ├── ISSUE_TEMPLATE/
│   │   ├── 1_bug_report.md
│   │   ├── 2_enhancement_request.md
│   │   ├── 3_new-notification-request.md
│   │   ├── 4_question.md
│   │   └── config.yaml
│   ├── PULL_REQUEST_TEMPLATE.md
│   └── workflows/
│       ├── codeql-analysis.yml
│       ├── lint.yml
│       ├── loc-badge.yml
│       ├── pkgbuild.yml
│       └── tests.yml
├── .gitignore
├── .vscode/
│   └── settings.json
├── ACKNOWLEDGEMENTS.md
├── CONTRIBUTING.md
├── LICENSE
├── MANIFEST.in
├── README.md
├── SECURITY.md
├── all-plugin-requirements.txt
├── apprise/
│   ├── __init__.py
│   ├── apprise.py
│   ├── apprise_attachment.py
│   ├── apprise_config.py
│   ├── asset.py
│   ├── assets/
│   │   ├── NotifyXML-1.0.xsd
│   │   └── NotifyXML-1.1.xsd
│   ├── attachment/
│   │   ├── __init__.py
│   │   ├── base.py
│   │   ├── file.py
│   │   ├── http.py
│   │   └── memory.py
│   ├── cli.py
│   ├── common.py
│   ├── compat.py
│   ├── config/
│   │   ├── __init__.py
│   │   ├── base.py
│   │   ├── file.py
│   │   ├── http.py
│   │   └── memory.py
│   ├── conversion.py
│   ├── decorators/
│   │   ├── __init__.py
│   │   ├── base.py
│   │   └── notify.py
│   ├── emojis.py
│   ├── exception.py
│   ├── i18n/
│   │   ├── __init__.py
│   │   └── en/
│   │       └── LC_MESSAGES/
│   │           └── apprise.po
│   ├── locale.py
│   ├── logger.py
│   ├── manager.py
│   ├── manager_attachment.py
│   ├── manager_config.py
│   ├── manager_plugins.py
│   ├── persistent_store.py
│   ├── plugins/
│   │   ├── __init__.py
│   │   ├── africas_talking.py
│   │   ├── apprise_api.py
│   │   ├── aprs.py
│   │   ├── bark.py
│   │   ├── base.py
│   │   ├── bluesky.py
│   │   ├── brevo.py
│   │   ├── bulksms.py
│   │   ├── bulkvs.py
│   │   ├── burstsms.py
│   │   ├── chanify.py
│   │   ├── clickatell.py
│   │   ├── clicksend.py
│   │   ├── custom_form.py
│   │   ├── custom_json.py
│   │   ├── custom_xml.py
│   │   ├── d7networks.py
│   │   ├── dapnet.py
│   │   ├── dbus.py
│   │   ├── dingtalk.py
│   │   ├── discord.py
│   │   ├── dot.py
│   │   ├── email/
│   │   │   ├── __init__.py
│   │   │   ├── base.py
│   │   │   ├── common.py
│   │   │   └── templates.py
│   │   ├── emby.py
│   │   ├── enigma2.py
│   │   ├── fcm/
│   │   │   ├── __init__.py
│   │   │   ├── color.py
│   │   │   ├── common.py
│   │   │   ├── oauth.py
│   │   │   └── priority.py
│   │   ├── feishu.py
│   │   ├── flock.py
│   │   ├── fluxer.py
│   │   ├── fortysixelks.py
│   │   ├── freemobile.py
│   │   ├── glib.py
│   │   ├── gnome.py
│   │   ├── google_chat.py
│   │   ├── gotify.py
│   │   ├── growl.py
│   │   ├── guilded.py
│   │   ├── home_assistant.py
│   │   ├── httpsms.py
│   │   ├── ifttt.py
│   │   ├── irc/
│   │   │   ├── __init__.py
│   │   │   ├── base.py
│   │   │   ├── client.py
│   │   │   ├── protocol.py
│   │   │   ├── state.py
│   │   │   └── templates.py
│   │   ├── jellyfin.py
│   │   ├── join.py
│   │   ├── kavenegar.py
│   │   ├── kodi.py
│   │   ├── kumulos.py
│   │   ├── lametric.py
│   │   ├── lark.py
│   │   ├── line.py
│   │   ├── macosx.py
│   │   ├── mailgun.py
│   │   ├── mastodon.py
│   │   ├── matrix.py
│   │   ├── mattermost.py
│   │   ├── messagebird.py
│   │   ├── misskey.py
│   │   ├── mqtt.py
│   │   ├── msg91.py
│   │   ├── msteams.py
│   │   ├── nextcloud.py
│   │   ├── nextcloudtalk.py
│   │   ├── notica.py
│   │   ├── notifiarr.py
│   │   ├── notificationapi.py
│   │   ├── notifico.py
│   │   ├── ntfy.py
│   │   ├── office365.py
│   │   ├── one_signal.py
│   │   ├── opsgenie.py
│   │   ├── pagerduty.py
│   │   ├── pagertree.py
│   │   ├── parseplatform.py
│   │   ├── plivo.py
│   │   ├── popcorn_notify.py
│   │   ├── prowl.py
│   │   ├── pushbullet.py
│   │   ├── pushdeer.py
│   │   ├── pushed.py
│   │   ├── pushjet.py
│   │   ├── pushme.py
│   │   ├── pushover.py
│   │   ├── pushplus.py
│   │   ├── pushsafer.py
│   │   ├── pushy.py
│   │   ├── qq.py
│   │   ├── reddit.py
│   │   ├── resend.py
│   │   ├── revolt.py
│   │   ├── rocketchat.py
│   │   ├── rsyslog.py
│   │   ├── ryver.py
│   │   ├── sendgrid.py
│   │   ├── sendpulse.py
│   │   ├── serverchan.py
│   │   ├── ses.py
│   │   ├── seven.py
│   │   ├── sfr.py
│   │   ├── signal_api.py
│   │   ├── signl4.py
│   │   ├── simplepush.py
│   │   ├── sinch.py
│   │   ├── slack.py
│   │   ├── smpp.py
│   │   ├── smseagle.py
│   │   ├── smsmanager.py
│   │   ├── smtp2go.py
│   │   ├── sns.py
│   │   ├── sparkpost.py
│   │   ├── spike.py
│   │   ├── splunk.py
│   │   ├── spugpush.py
│   │   ├── streamlabs.py
│   │   ├── synology.py
│   │   ├── syslog.py
│   │   ├── techuluspush.py
│   │   ├── telegram.py
│   │   ├── threema.py
│   │   ├── twilio.py
│   │   ├── twist.py
│   │   ├── twitter.py
│   │   ├── vapid/
│   │   │   ├── __init__.py
│   │   │   └── subscription.py
│   │   ├── viber.py
│   │   ├── voipms.py
│   │   ├── vonage.py
│   │   ├── webexteams.py
│   │   ├── wecombot.py
│   │   ├── whatsapp.py
│   │   ├── windows.py
│   │   ├── workflows.py
│   │   ├── wxpusher.py
│   │   ├── xmpp/
│   │   │   ├── __init__.py
│   │   │   ├── adapter.py
│   │   │   ├── base.py
│   │   │   └── common.py
│   │   └── zulip.py
│   ├── py.typed
│   ├── url.py
│   └── utils/
│       ├── __init__.py
│       ├── base64.py
│       ├── cwe312.py
│       ├── disk.py
│       ├── format.py
│       ├── json.py
│       ├── logic.py
│       ├── module.py
│       ├── parse.py
│       ├── pem.py
│       ├── pgp.py
│       ├── sanitize.py
│       ├── singleton.py
│       ├── socket.py
│       ├── templates.py
│       └── time.py
├── babel.cfg
├── bin/
│   ├── README.md
│   ├── apprise
│   ├── build-rpm.sh
│   ├── checkdone.sh
│   └── test.sh
├── dev-requirements.txt
├── docker-compose.yml
├── packaging/
│   ├── README.md
│   ├── i18n_normalize.sh
│   ├── man/
│   │   ├── apprise.1
│   │   ├── apprise.1.html
│   │   └── apprise.md
│   └── redhat/
│       ├── python-apprise.rpmlintrc.el10
│       ├── python-apprise.rpmlintrc.el9
│       └── python-apprise.spec
├── pyproject.toml
├── requirements.txt
├── setup.py
├── tests/
│   ├── conftest.py
│   ├── docker/
│   │   ├── Dockerfile.el10
│   │   ├── Dockerfile.el9
│   │   ├── Dockerfile.f42
│   │   ├── Dockerfile.py310
│   │   ├── Dockerfile.py311
│   │   ├── Dockerfile.py312
│   │   ├── Dockerfile.py39
│   │   └── Dockerfile.rawhide
│   ├── helpers/
│   │   ├── __init__.py
│   │   ├── asyncio.py
│   │   ├── environment.py
│   │   ├── module.py
│   │   └── rest.py
│   ├── test_api.py
│   ├── test_apprise_asset.py
│   ├── test_apprise_attachments.py
│   ├── test_apprise_cli.py
│   ├── test_apprise_config.py
│   ├── test_apprise_emojis.py
│   ├── test_apprise_helpers.py
│   ├── test_apprise_jsonencoder.py
│   ├── test_apprise_pickle.py
│   ├── test_apprise_translations.py
│   ├── test_apprise_utils.py
│   ├── test_asyncio.py
│   ├── test_attach_base.py
│   ├── test_attach_file.py
│   ├── test_attach_http.py
│   ├── test_attach_memory.py
│   ├── test_compat_py39.py
│   ├── test_config_base.py
│   ├── test_config_file.py
│   ├── test_config_http.py
│   ├── test_config_memory.py
│   ├── test_conversion.py
│   ├── test_decorator_notify.py
│   ├── test_escapes.py
│   ├── test_logger.py
│   ├── test_notification_manager.py
│   ├── test_notify_base.py
│   ├── test_persistent_store.py
│   ├── test_plugin_africas_talking.py
│   ├── test_plugin_apprise_api.py
│   ├── test_plugin_aprs.py
│   ├── test_plugin_bark.py
│   ├── test_plugin_base_formatting.py
│   ├── test_plugin_bluesky.py
│   ├── test_plugin_brevo.py
│   ├── test_plugin_bulksms.py
│   ├── test_plugin_bulkvs.py
│   ├── test_plugin_burstsms.py
│   ├── test_plugin_chanify.py
│   ├── test_plugin_clickatell.py
│   ├── test_plugin_clicksend.py
│   ├── test_plugin_custom_form.py
│   ├── test_plugin_custom_json.py
│   ├── test_plugin_custom_xml.py
│   ├── test_plugin_d7networks.py
│   ├── test_plugin_dapnet.py
│   ├── test_plugin_dbus.py
│   ├── test_plugin_dingtalk.py
│   ├── test_plugin_discord.py
│   ├── test_plugin_dot.py
│   ├── test_plugin_email.py
│   ├── test_plugin_emby.py
│   ├── test_plugin_enigma2.py
│   ├── test_plugin_fcm.py
│   ├── test_plugin_feishu.py
│   ├── test_plugin_flock.py
│   ├── test_plugin_fluxer.py
│   ├── test_plugin_fortysixelks.py
│   ├── test_plugin_freemobile.py
│   ├── test_plugin_glib.py
│   ├── test_plugin_gnome.py
│   ├── test_plugin_google_chat.py
│   ├── test_plugin_gotify.py
│   ├── test_plugin_growl.py
│   ├── test_plugin_guilded.py
│   ├── test_plugin_homeassistant.py
│   ├── test_plugin_httpsms.py
│   ├── test_plugin_ifttt.py
│   ├── test_plugin_irc.py
│   ├── test_plugin_irc_state.py
│   ├── test_plugin_jellyfin.py
│   ├── test_plugin_join.py
│   ├── test_plugin_kavenegar.py
│   ├── test_plugin_kumulos.py
│   ├── test_plugin_lametric.py
│   ├── test_plugin_lark.py
│   ├── test_plugin_line.py
│   ├── test_plugin_macosx.py
│   ├── test_plugin_mailgun.py
│   ├── test_plugin_mastodon.py
│   ├── test_plugin_matrix.py
│   ├── test_plugin_mattermost.py
│   ├── test_plugin_messagebird.py
│   ├── test_plugin_misskey.py
│   ├── test_plugin_mqtt.py
│   ├── test_plugin_msg91.py
│   ├── test_plugin_msteams.py
│   ├── test_plugin_nextcloud.py
│   ├── test_plugin_nextcloudtalk.py
│   ├── test_plugin_notica.py
│   ├── test_plugin_notifiarr.py
│   ├── test_plugin_notificationapi.py
│   ├── test_plugin_notifico.py
│   ├── test_plugin_ntfy.py
│   ├── test_plugin_office365.py
│   ├── test_plugin_onesignal.py
│   ├── test_plugin_opsgenie.py
│   ├── test_plugin_pagerduty.py
│   ├── test_plugin_pagertree.py
│   ├── test_plugin_parse_platform.py
│   ├── test_plugin_plivo.py
│   ├── test_plugin_popcorn_notify.py
│   ├── test_plugin_prowl.py
│   ├── test_plugin_pushbullet.py
│   ├── test_plugin_pushdeer.py
│   ├── test_plugin_pushed.py
│   ├── test_plugin_pushjet.py
│   ├── test_plugin_pushme.py
│   ├── test_plugin_pushover.py
│   ├── test_plugin_pushplus.py
│   ├── test_plugin_pushsafer.py
│   ├── test_plugin_pushy.py
│   ├── test_plugin_qq.py
│   ├── test_plugin_reddit.py
│   ├── test_plugin_resend.py
│   ├── test_plugin_revolt.py
│   ├── test_plugin_rocket_chat.py
│   ├── test_plugin_rsyslog.py
│   ├── test_plugin_ryver.py
│   ├── test_plugin_sendgrid.py
│   ├── test_plugin_sendpulse.py
│   ├── test_plugin_serverchan.py
│   ├── test_plugin_ses.py
│   ├── test_plugin_seven.py
│   ├── test_plugin_sfr.py
│   ├── test_plugin_signal.py
│   ├── test_plugin_signl4.py
│   ├── test_plugin_simplepush.py
│   ├── test_plugin_sinch.py
│   ├── test_plugin_slack.py
│   ├── test_plugin_smpp.py
│   ├── test_plugin_sms_manager.py
│   ├── test_plugin_smseagle.py
│   ├── test_plugin_smtp2go.py
│   ├── test_plugin_sns.py
│   ├── test_plugin_sparkpost.py
│   ├── test_plugin_spike.py
│   ├── test_plugin_splunk.py
│   ├── test_plugin_spugpush.py
│   ├── test_plugin_streamlabs.py
│   ├── test_plugin_synology.py
│   ├── test_plugin_syslog.py
│   ├── test_plugin_techululs_push.py
│   ├── test_plugin_telegram.py
│   ├── test_plugin_threema.py
│   ├── test_plugin_title_maxlen.py
│   ├── test_plugin_twilio.py
│   ├── test_plugin_twist.py
│   ├── test_plugin_twitter.py
│   ├── test_plugin_vapid.py
│   ├── test_plugin_viber.py
│   ├── test_plugin_voipms.py
│   ├── test_plugin_vonage.py
│   ├── test_plugin_webex_teams.py
│   ├── test_plugin_wecombot.py
│   ├── test_plugin_whatsapp.py
│   ├── test_plugin_windows.py
│   ├── test_plugin_workflows.py
│   ├── test_plugin_wxpusher.py
│   ├── test_plugin_xbmc_kodi.py
│   ├── test_plugin_xmpp.py
│   ├── test_plugin_zulip.py
│   ├── test_utils_format.py
│   ├── test_utils_pem.py
│   ├── test_utils_sanitize.py
│   ├── test_utils_socket.py
│   └── var/
│       ├── 01_test_example.html
│       ├── fcm/
│       │   ├── service_account-bad-type.json
│       │   └── service_account.json
│       ├── mime.types
│       └── pgp/
│           ├── corrupt-pub.asc
│           └── valid-pub.asc
├── tox.ini
└── win-requirements.txt
Download .txt
Showing preview only (235K chars total). Download the full file or copy to clipboard to get everything.
SYMBOL INDEX (2670 symbols across 359 files)

FILE: apprise/apprise.py
  class Apprise (line 59) | class Apprise:
    method __init__ (line 62) | def __init__(
    method instantiate (line 114) | def instantiate(
    method add (line 266) | def add(
    method clear (line 348) | def clear(self) -> None:
    method find (line 352) | def find(
    method notify (line 396) | def notify(
    method async_notify (line 456) | async def async_notify(
    method _create_notify_calls (line 487) | def _create_notify_calls(self, *args, **kwargs):
    method _create_notify_gen (line 507) | def _create_notify_gen(
    method _notify_sequential (line 667) | def _notify_sequential(*servers_kwargs):
    method _notify_parallel_threadpool (line 691) | def _notify_parallel_threadpool(*servers_kwargs):
    method _notify_parallel_asyncio (line 734) | async def _notify_parallel_asyncio(*servers_kwargs):
    method json (line 774) | def json(
    method details (line 823) | def details(
    method urls (line 905) | def urls(self, privacy: bool = False) -> list[str]:
    method pop (line 916) | def pop(self, index: int) -> NotifyBase:
    method __getitem__ (line 961) | def __getitem__(self, index: int) -> NotifyBase:
    method __getstate__ (line 993) | def __getstate__(self) -> dict[str, object]:
    method __setstate__ (line 1014) | def __setstate__(self, state: dict[str, object]) -> None:
    method __bool__ (line 1031) | def __bool__(self) -> bool:
    method __iter__ (line 1038) | def __iter__(self) -> Iterator[NotifyBase]:
    method __len__ (line 1052) | def __len__(self) -> int:

FILE: apprise/apprise_attachment.py
  class AppriseAttachment (line 43) | class AppriseAttachment:
    method __init__ (line 46) | def __init__(
    method add (line 127) | def add(
    method instantiate (line 240) | def instantiate(
    method sync (line 305) | def sync(
    method clear (line 321) | def clear(self) -> None:
    method size (line 325) | def size(self) -> int:
    method pop (line 329) | def pop(self, index: int = -1) -> AttachBase:
    method __getitem__ (line 337) | def __getitem__(self, index: int) -> AttachBase:
    method __bool__ (line 341) | def __bool__(self) -> bool:
    method __iter__ (line 348) | def __iter__(self) -> Iterator[AttachBase]:
    method __len__ (line 352) | def __len__(self) -> int:

FILE: apprise/apprise_config.py
  class AppriseConfig (line 48) | class AppriseConfig:
    method __init__ (line 55) | def __init__(
    method add (line 133) | def add(
    method add_config (line 242) | def add_config(
    method servers (line 312) | def servers(
    method instantiate (line 363) | def instantiate(
    method clear (line 439) | def clear(self) -> None:
    method server_pop (line 443) | def server_pop(self, index: int) -> NotifyBase:
    method pop (line 470) | def pop(self, index: int = -1) -> ConfigBase:
    method __getitem__ (line 479) | def __getitem__(self, index: int) -> ConfigBase:
    method __bool__ (line 484) | def __bool__(self) -> bool:
    method __iter__ (line 491) | def __iter__(self):  # type: () -> Iterator[ConfigBase]
    method __len__ (line 495) | def __len__(self) -> int:

FILE: apprise/asset.py
  class AppriseAsset (line 47) | class AppriseAsset:
    method __init__ (line 218) | def __init__(
    method color (line 308) | def color(
    method ascii (line 344) | def ascii(self, notify_type: NotifyType) -> str:
    method image_url (line 350) | def image_url(
    method image_path (line 388) | def image_path(
    method image_raw (line 425) | def image_raw(
    method details (line 450) | def details(self) -> dict[str, str]:
    method hex_to_rgb (line 463) | def hex_to_rgb(value: str) -> tuple[int, int, int]:
    method hex_to_int (line 476) | def hex_to_int(value: str) -> int:
    method plugin_paths (line 485) | def plugin_paths(self) -> list[str]:
    method storage_path (line 490) | def storage_path(self) -> Optional[str]:
    method storage_mode (line 495) | def storage_mode(self) -> PersistentStoreMode:
    method storage_salt (line 501) | def storage_salt(self) -> bytes:
    method storage_idlen (line 506) | def storage_idlen(self) -> int:
    method tzinfo (line 512) | def tzinfo(self) -> tzinfo:

FILE: apprise/attachment/base.py
  class AttachBase (line 41) | class AttachBase(URLBase):
    method __init__ (line 104) | def __init__(self, name=None, mimetype=None, cache=None, **kwargs):
    method path (line 192) | def path(self):
    method name (line 206) | def name(self):
    method mimetype (line 228) | def mimetype(self):
    method exists (line 259) | def exists(self, retrieve_if_missing=True):
    method base64 (line 297) | def base64(self, encoding="ascii"):
    method invalidate (line 333) | def invalidate(self):
    method download (line 353) | def download(self):
    method open (line 367) | def open(self, mode="rb"):
    method chunk (line 373) | def chunk(self, size=5242880):
    method __enter__ (line 387) | def __enter__(self):
    method __exit__ (line 391) | def __exit__(self, value_type, value, traceback):
    method parse_url (line 397) | def parse_url(url, verify_host=True, mimetype_db=None, sanitize=True):
    method __len__ (line 446) | def __len__(self):
    method __bool__ (line 458) | def __bool__(self):
    method __del__ (line 465) | def __del__(self):

FILE: apprise/attachment/file.py
  class AttachFile (line 37) | class AttachFile(AttachBase):
    method __init__ (line 50) | def __init__(self, path, **kwargs):
    method url (line 62) | def url(self, privacy=False, *args, **kwargs):
    method download (line 85) | def download(self, **kwargs):
    method parse_url (line 131) | def parse_url(url):

FILE: apprise/attachment/http.py
  class AttachHTTP (line 42) | class AttachHTTP(AttachBase):
    method __init__ (line 63) | def __init__(self, headers=None, **kwargs):
    method download (line 95) | def download(self, **kwargs):
    method invalidate (line 284) | def invalidate(self):
    method __del__ (line 300) | def __del__(self):
    method url (line 304) | def url(self, privacy=False, *args, **kwargs):
    method parse_url (line 363) | def parse_url(url):

FILE: apprise/attachment/memory.py
  class AttachMemory (line 40) | class AttachMemory(AttachBase):
    method __init__ (line 53) | def __init__(
    method url (line 100) | def url(self, privacy=False, *args, **kwargs):
    method open (line 113) | def open(self, *args, **kwargs):
    method __enter__ (line 119) | def __enter__(self):
    method download (line 125) | def download(self, **kwargs):
    method base64 (line 145) | def base64(self, encoding="ascii"):
    method invalidate (line 163) | def invalidate(self):
    method exists (line 168) | def exists(self):
    method parse_url (line 181) | def parse_url(url):
    method path (line 201) | def path(self):
    method __len__ (line 209) | def __len__(self):
    method __bool__ (line 213) | def __bool__(self):

FILE: apprise/cli.py
  class PersistentStorageMode (line 223) | class PersistentStorageMode:
  function print_version_msg (line 248) | def print_version_msg():
  class CustomHelpCommand (line 257) | class CustomHelpCommand(click.Command):
    method format_help (line 258) | def format_help(self, ctx, formatter):
  function main (line 601) | def main(

FILE: apprise/common.py
  class NotifyType (line 35) | class NotifyType(str, Enum):
  class NotifyImageSize (line 49) | class NotifyImageSize(str, Enum):
  class NotifyFormat (line 64) | class NotifyFormat(str, Enum):
  class OverflowMode (line 77) | class OverflowMode(str, Enum):
  class ConfigFormat (line 99) | class ConfigFormat(str, Enum):
  class ContentIncludeMode (line 117) | class ContentIncludeMode(str, Enum):
  class ContentLocation (line 141) | class ContentLocation(str, Enum):
  class PersistentStoreMode (line 168) | class PersistentStoreMode(str, Enum):
  class PersistentStoreState (line 185) | class PersistentStoreState(str, Enum):

FILE: apprise/compat.py
  function dataclass_compat (line 36) | def dataclass_compat(*dargs: Any, **dkwargs: Any) -> Callable[[_T], _T]:

FILE: apprise/config/base.py
  class ConfigBase (line 56) | class ConfigBase(URLBase):
    method __init__ (line 82) | def __init__(
    method servers (line 176) | def servers(
    method read (line 346) | def read(self) -> str | None:
    method expired (line 350) | def expired(self) -> bool:
    method __normalize_tag_groups (line 372) | def __normalize_tag_groups(group_tags: dict[str, set[str]]) -> None:
    method parse_url (line 447) | def parse_url(
    method detect_config_format (line 501) | def detect_config_format(
    method config_parse (line 579) | def config_parse(
    method config_parse_text (line 620) | def config_parse_text(
    method config_parse_yaml (line 836) | def config_parse_yaml(
    method pop (line 1347) | def pop(self, index: int = -1) -> object:
    method clear_cache (line 1361) | def clear_cache(self) -> None:
    method _special_token_handler (line 1367) | def _special_token_handler(
    method __getitem__ (line 1494) | def __getitem__(self, index: int) -> object:
    method __iter__ (line 1503) | def __iter__(self) -> object:
    method __len__ (line 1511) | def __len__(self) -> int:
    method __bool__ (line 1519) | def __bool__(self) -> bool:

FILE: apprise/config/file.py
  class ConfigFile (line 37) | class ConfigFile(ConfigBase):
    method __init__ (line 49) | def __init__(self, path, **kwargs):
    method url (line 68) | def url(self, privacy=False, *args, **kwargs):
    method read (line 93) | def read(self, **kwargs):
    method parse_url (line 160) | def parse_url(url):

FILE: apprise/config/http.py
  class ConfigHTTP (line 50) | class ConfigHTTP(ConfigBase):
    method __init__ (line 71) | def __init__(self, headers=None, **kwargs):
    method url (line 92) | def url(self, privacy=False, *args, **kwargs):
    method read (line 146) | def read(self, **kwargs):
    method parse_url (line 261) | def parse_url(url):

FILE: apprise/config/memory.py
  class ConfigMemory (line 32) | class ConfigMemory(ConfigBase):
    method __init__ (line 42) | def __init__(self, content, **kwargs):
    method url (line 61) | def url(self, privacy=False, *args, **kwargs):
    method read (line 66) | def read(self, **kwargs):
    method parse_url (line 72) | def parse_url(url):

FILE: apprise/conversion.py
  function convert_between (line 37) | def convert_between(from_format, to_format, content):
  function markdown_to_html (line 56) | def markdown_to_html(content):
  function text_to_html (line 64) | def text_to_html(content):
  function html_to_text (line 71) | def html_to_text(content):
  class HTMLConverter (line 80) | class HTMLConverter(HTMLParser):
    method __init__ (line 125) | def __init__(self, **kwargs):
    method close (line 138) | def close(self):
    method _finalize (line 142) | def _finalize(self, result):
    method handle_data (line 174) | def handle_data(self, data, *args, **kwargs):
    method handle_starttag (line 184) | def handle_starttag(self, tag, attrs):
    method handle_endtag (line 207) | def handle_endtag(self, tag):

FILE: apprise/decorators/base.py
  class CustomNotifyPlugin (line 42) | class CustomNotifyPlugin(NotifyBase):
    method parse_url (line 66) | def parse_url(url):
    method url (line 70) | def url(self, privacy=False, *args, **kwargs):
    method instantiate_plugin (line 75) | def instantiate_plugin(url, send_func, name=None):

FILE: apprise/decorators/notify.py
  function notify (line 31) | def notify(on, name=None):

FILE: apprise/emojis.py
  function apply_emojis (line 2153) | def apply_emojis(content):

FILE: apprise/exception.py
  class AppriseException (line 30) | class AppriseException(Exception):
    method __init__ (line 33) | def __init__(self, message, error_code=0):
  class ApprisePluginException (line 38) | class ApprisePluginException(AppriseException):
    method __init__ (line 41) | def __init__(self, message, error_code=600):
  class AppriseDiskIOError (line 45) | class AppriseDiskIOError(AppriseException):
    method __init__ (line 48) | def __init__(self, message, error_code=errno.EIO):
  class AppriseInvalidData (line 52) | class AppriseInvalidData(AppriseException):
    method __init__ (line 55) | def __init__(self, message, error_code=errno.EINVAL):
  class AppriseFileNotFound (line 59) | class AppriseFileNotFound(AppriseDiskIOError, FileNotFoundError):
    method __init__ (line 62) | def __init__(self, message):

FILE: apprise/locale.py
  class AppriseLocale (line 54) | class AppriseLocale:
    method __init__ (line 80) | def __init__(self, language=None):
    method add (line 104) | def add(self, lang=None, set_default=True):
    method lang_at (line 144) | def lang_at(self, lang, mapto=_fn):
    method gettext (line 172) | def gettext(self):
    method detect_language (line 180) | def detect_language(lang=None, detect_fallback=True):
    method __getstate__ (line 233) | def __getstate__(self):
    method __setstate__ (line 242) | def __setstate__(self, state):
  class LazyTranslation (line 257) | class LazyTranslation:
    method __init__ (line 261) | def __init__(self, text, *args, **kwargs):
    method __str__ (line 267) | def __str__(self):
  function gettext_lazy (line 272) | def gettext_lazy(text):

FILE: apprise/logger.py
  function trace (line 48) | def trace(self, message, *args, **kwargs):
  function deprecate (line 56) | def deprecate(self, message, *args, **kwargs):
  class LogCapture (line 70) | class LogCapture:
    method __init__ (line 85) | def __init__(
    method __enter__ (line 138) | def __enter__(self):
    method __exit__ (line 171) | def __exit__(self, exc_type, exc_value, tb):

FILE: apprise/manager.py
  class PluginManager (line 45) | class PluginManager(metaclass=Singleton):
    method __init__ (line 67) | def __init__(self, *args, **kwargs):
    method unload_modules (line 111) | def unload_modules(self, disable_native=False):
    method load_modules (line 150) | def load_modules(self, path=None, name=None, force=False):
    method module_detection (line 291) | def module_detection(self, paths, cache=True):
    method add (line 454) | def add(self, plugin, schemas=None, url=None, send_func=None, force=Fa...
    method remove (line 581) | def remove(self, *schemas, unload=True):
    method plugins (line 591) | def plugins(self, include_disabled=True):
    method schemas (line 603) | def schemas(self, include_disabled=True):
    method disable (line 620) | def disable(self, *schemas):
    method enable_only (line 637) | def enable_only(self, *schemas):
    method __contains__ (line 667) | def __contains__(self, schema):
    method __delitem__ (line 675) | def __delitem__(self, schema):
    method __setitem__ (line 681) | def __setitem__(self, schema, plugin):
    method _unmap_schema (line 709) | def _unmap_schema(self, schema, *, unload=True):
    method __getitem__ (line 779) | def __getitem__(self, schema):
    method __iter__ (line 787) | def __iter__(self):
    method __len__ (line 795) | def __len__(self):
    method __bool__ (line 803) | def __bool__(self):

FILE: apprise/manager_attachment.py
  class AttachmentManager (line 34) | class AttachmentManager(PluginManager):

FILE: apprise/manager_config.py
  class ConfigurationManager (line 34) | class ConfigurationManager(PluginManager):

FILE: apprise/manager_plugins.py
  class NotificationManager (line 34) | class NotificationManager(PluginManager):

FILE: apprise/persistent_store.py
  function _ntf_tidy (line 58) | def _ntf_tidy(ntf):
  class CacheObject (line 81) | class CacheObject:
    method __init__ (line 86) | def __init__(
    method set (line 104) | def set(
    method set_expiry (line 127) | def set_expiry(self, expires:
    method hash (line 152) | def hash(self) -> str:
    method json (line 158) | def json(self) -> Optional[dict[str, Any]]:
    method instantiate (line 181) | def instantiate(
    method value (line 253) | def value(self) -> Any:
    method persistent (line 258) | def persistent(self) -> bool:
    method expires (line 263) | def expires(self) -> Optional[datetime]:
    method expires_sec (line 268) | def expires_sec(self) -> Optional[float]:
    method __bool__ (line 282) | def __bool__(self) -> bool:
    method __eq__ (line 291) | def __eq__(self, other) -> bool:
    method __str__ (line 298) | def __str__(self) -> str:
  class CacheJSONEncoder (line 308) | class CacheJSONEncoder(json.JSONEncoder):
    method default (line 311) | def default(self, entry):
  class PersistentStore (line 328) | class PersistentStore:
    method __init__ (line 373) | def __init__(
    method read (line 447) | def read(
    method write (line 487) | def write(
    method __move (line 652) | def __move(self, src, dst):
    method open (line 747) | def open(
    method get (line 813) | def get(
    method set (line 835) | def set(
    method clear (line 870) | def clear(self, *args: str) -> Optional[bool]:
    method prune (line 907) | def prune(self) -> bool:
    method __load_cache (line 931) | def __load_cache(self, _recovery=False):
    method __prepare (line 1017) | def __prepare(self, flush=True):
    method flush (line 1082) | def flush(
    method files (line 1275) | def files(
    method disk_scan (line 1351) | def disk_scan(
    method disk_prune (line 1411) | def disk_prune(
    method size (line 1603) | def size(
    method __del__ (line 1630) | def __del__(self) -> None:
    method __delitem__ (line 1637) | def __delitem__(self, key: str) -> None:
    method __contains__ (line 1660) | def __contains__(self, key: str) -> bool:
    method __setitem__ (line 1671) | def __setitem__(self, key: str, value: Any) -> None:
    method __getitem__ (line 1694) | def __getitem__(self, key: str) -> Any:
    method keys (line 1706) | def keys(self) -> builtins.set[str]:
    method delete (line 1714) | def delete(
    method cache_file (line 1852) | def cache_file(self) -> str:
    method path (line 1860) | def path(self) -> Optional[str]:
    method mode (line 1865) | def mode(self) -> PersistentStoreMode:

FILE: apprise/plugins/__init__.py
  function _sanitize_token (line 61) | def _sanitize_token(tokens, default_delimiter):
  function details (line 150) | def details(plugin):
  function requirements (line 335) | def requirements(plugin):
  function url_to_dict (line 418) | def url_to_dict(url, secure_logging=True):

FILE: apprise/plugins/africas_talking.py
  class AfricasTalkingSMSMode (line 46) | class AfricasTalkingSMSMode:
  class NotifyAfricasTalking (line 86) | class NotifyAfricasTalking(NotifyBase):
    method __init__ (line 188) | def __init__(
    method send (line 277) | def send(self, body, title="", notify_type=NotifyType.INFO, **kwargs):
    method url_identifier (line 394) | def url_identifier(self):
    method url (line 402) | def url(self, privacy=False, *args, **kwargs):
    method __len__ (line 431) | def __len__(self):
    method parse_url (line 446) | def parse_url(url):

FILE: apprise/plugins/apprise_api.py
  class AppriseAPIMethod (line 43) | class AppriseAPIMethod:
  class NotifyAppriseAPI (line 56) | class NotifyAppriseAPI(NotifyBase):
    method __init__ (line 159) | def __init__(
    method url (line 198) | def url(self, privacy=False, *args, **kwargs):
    method send (line 255) | def send(
    method parse_native_url (line 448) | def parse_native_url(url):
    method parse_url (line 494) | def parse_url(url):

FILE: apprise/plugins/aprs.py
  class NotifyAprs (line 111) | class NotifyAprs(NotifyBase):
    method __init__ (line 219) | def __init__(self, targets=None, locale=None, delay=None, **kwargs):
    method socket_close (line 322) | def socket_close(self):
    method socket_open (line 329) | def socket_open(self):
    method aprsis_login (line 382) | def aprsis_login(self):
    method socket_send (line 461) | def socket_send(self, tx_data):
    method socket_reset (line 507) | def socket_reset(self):
    method socket_receive (line 514) | def socket_receive(self, rx_len):
    method send (line 565) | def send(self, body, title="", notify_type=NotifyType.INFO, **kwargs):
    method url (line 656) | def url(self, privacy=False, *args, **kwargs):
    method url_identifier (line 697) | def url_identifier(self):
    method __len__ (line 705) | def __len__(self):
    method __del__ (line 710) | def __del__(self):
    method parse_url (line 715) | def parse_url(url):

FILE: apprise/plugins/bark.py
  class NotifyBarkLevel (line 79) | class NotifyBarkLevel:
  class NotifyBark (line 99) | class NotifyBark(NotifyBase):
    method __init__ (line 224) | def __init__(
    method send (line 334) | def send(self, body, title="", notify_type=NotifyType.INFO, **kwargs):
    method url_identifier (line 480) | def url_identifier(self):
    method url (line 494) | def url(self, privacy=False, *args, **kwargs):
    method __len__ (line 561) | def __len__(self):
    method parse_url (line 566) | def parse_url(url):

FILE: apprise/plugins/base.py
  class RequirementsSpec (line 54) | class RequirementsSpec(TypedDict, total=False):
  class NotifyBase (line 62) | class NotifyBase(URLBase):
    method __init__ (line 302) | def __init__(self, **kwargs):
    method image_url (line 383) | def image_url(
    method image_path (line 403) | def image_path(
    method image_raw (line 418) | def image_raw(
    method color (line 433) | def color(
    method ascii (line 446) | def ascii(
    method notify (line 456) | def notify(self, *args: Any, **kwargs: Any) -> bool:
    method async_notify (line 472) | async def async_notify(self, *args: Any, **kwargs: Any) -> bool:
    method _build_send_calls (line 497) | def _build_send_calls(
    method _apply_overflow (line 579) | def _apply_overflow(
    method send (line 851) | def send(
    method url_parameters (line 863) | def url_parameters(
    method parse_url (line 893) | def parse_url(
    method parse_native_url (line 958) | def parse_native_url(url: str) -> Optional[dict[str, Any]]:
    method store (line 975) | def store(self):
    method tzinfo (line 1000) | def tzinfo(self) -> tzinfo:

FILE: apprise/plugins/bluesky.py
  class NotifyBlueSky (line 53) | class NotifyBlueSky(NotifyBase):
    method __init__ (line 132) | def __init__(self, **kwargs):
    method send (line 161) | def send(
    method get_identifier (line 290) | def get_identifier(self, user=None, login=False):
    method login (line 400) | def login(self):
    method _fetch (line 490) | def _fetch(
    method url_identifier (line 642) | def url_identifier(self):
    method url (line 654) | def url(self, privacy=False, *args, **kwargs):
    method parse_url (line 675) | def parse_url(url):

FILE: apprise/plugins/brevo.py
  class NotifyBrevo (line 70) | class NotifyBrevo(NotifyBase):
    method __init__ (line 158) | def __init__(
    method url_identifier (line 257) | def url_identifier(self):
    method url (line 265) | def url(self, privacy=False, *args, **kwargs):
    method __len__ (line 308) | def __len__(self):
    method send (line 312) | def send(
    method parse_url (line 517) | def parse_url(url):

FILE: apprise/plugins/bulksms.py
  class BulkSMSRoutingGroup (line 52) | class BulkSMSRoutingGroup:
  class BulkSMSEncoding (line 68) | class BulkSMSEncoding:
  class NotifyBulkSMS (line 76) | class NotifyBulkSMS(NotifyBase):
    method __init__ (line 177) | def __init__(
    method send (line 252) | def send(self, body, title="", notify_type=NotifyType.INFO, **kwargs):
    method url (line 415) | def url(self, privacy=False, *args, **kwargs):
    method url_identifier (line 453) | def url_identifier(self):
    method __len__ (line 465) | def __len__(self):
    method parse_url (line 483) | def parse_url(url):

FILE: apprise/plugins/bulkvs.py
  class NotifyBulkVS (line 46) | class NotifyBulkVS(NotifyBase):
    method __init__ (line 138) | def __init__(self, source=None, targets=None, batch=None, **kwargs):
    method send (line 187) | def send(self, body, title="", notify_type=NotifyType.INFO, **kwargs):
    method url_identifier (line 323) | def url_identifier(self):
    method url (line 331) | def url(self, privacy=False, *args, **kwargs):
    method __len__ (line 364) | def __len__(self):
    method parse_url (line 380) | def parse_url(url):

FILE: apprise/plugins/burstsms.py
  class BurstSMSCountryCode (line 47) | class BurstSMSCountryCode:
  class NotifyBurstSMS (line 66) | class NotifyBurstSMS(NotifyBase):
    method __init__ (line 173) | def __init__(
    method send (line 260) | def send(self, body, title="", notify_type=NotifyType.INFO, **kwargs):
    method url (line 367) | def url(self, privacy=False, *args, **kwargs):
    method url_identifier (line 396) | def url_identifier(self):
    method __len__ (line 404) | def __len__(self):
    method parse_url (line 419) | def parse_url(url):

FILE: apprise/plugins/chanify.py
  class NotifyChanify (line 43) | class NotifyChanify(NotifyBase):
    method __init__ (line 93) | def __init__(self, token, **kwargs):
    method send (line 107) | def send(self, body, title="", notify_type=NotifyType.INFO, **kwargs):
    method url (line 169) | def url(self, privacy=False, *args, **kwargs):
    method url_identifier (line 182) | def url_identifier(self):
    method parse_url (line 191) | def parse_url(url):

FILE: apprise/plugins/clickatell.py
  class NotifyClickatell (line 41) | class NotifyClickatell(NotifyBase):
    method __init__ (line 110) | def __init__(self, apikey, source=None, targets=None, **kwargs):
    method url_identifier (line 156) | def url_identifier(self):
    method url (line 164) | def url(self, privacy=False, *args, **kwargs):
    method __len__ (line 180) | def __len__(self):
    method send (line 187) | def send(self, body, title="", notify_type=NotifyType.INFO, **kwargs):
    method parse_url (line 274) | def parse_url(url):

FILE: apprise/plugins/clicksend.py
  class NotifyClickSend (line 57) | class NotifyClickSend(NotifyBase):
    method __init__ (line 137) | def __init__(self, targets=None, batch=False, **kwargs):
    method send (line 164) | def send(self, body, title="", notify_type=NotifyType.INFO, **kwargs):
    method url (line 266) | def url(self, privacy=False, *args, **kwargs):
    method url_identifier (line 295) | def url_identifier(self):
    method __len__ (line 303) | def __len__(self):
    method parse_url (line 318) | def parse_url(url):

FILE: apprise/plugins/custom_form.py
  class FORMPayloadField (line 38) | class FORMPayloadField:
  class NotifyForm (line 52) | class NotifyForm(NotifyBase):
    method __init__ (line 179) | def __init__(
    method send (line 278) | def send(
    method url_identifier (line 450) | def url_identifier(self):
    method url (line 465) | def url(self, privacy=False, *args, **kwargs):
    method parse_url (line 524) | def parse_url(url):

FILE: apprise/plugins/custom_json.py
  class JSONPayloadField (line 41) | class JSONPayloadField:
  class NotifyJSON (line 56) | class NotifyJSON(NotifyBase):
    method __init__ (line 155) | def __init__(
    method send (line 197) | def send(
    method url_identifier (line 352) | def url_identifier(self):
    method url (line 367) | def url(self, privacy=False, *args, **kwargs):
    method parse_url (line 421) | def parse_url(url):

FILE: apprise/plugins/custom_xml.py
  class XMLPayloadField (line 41) | class XMLPayloadField:
  class NotifyXML (line 55) | class NotifyXML(NotifyBase):
    method __init__ (line 155) | def __init__(
    method send (line 248) | def send(
    method url_identifier (line 442) | def url_identifier(self):
    method url (line 457) | def url(self, privacy=False, *args, **kwargs):
    method parse_url (line 512) | def parse_url(url):

FILE: apprise/plugins/d7networks.py
  class NotifyD7Networks (line 60) | class NotifyD7Networks(NotifyBase):
    method __init__ (line 151) | def __init__(
    method send (line 199) | def send(self, body, title="", notify_type=NotifyType.INFO, **kwargs):
    method url (line 345) | def url(self, privacy=False, *args, **kwargs):
    method url_identifier (line 370) | def url_identifier(self):
    method __len__ (line 378) | def __len__(self):
    method parse_url (line 386) | def parse_url(url):

FILE: apprise/plugins/dapnet.py
  class DapnetPriority (line 60) | class DapnetPriority:
  class NotifyDapnet (line 82) | class NotifyDapnet(NotifyBase):
    method __init__ (line 174) | def __init__(
    method send (line 227) | def send(self, body, title="", notify_type=NotifyType.INFO, **kwargs):
    method url (line 314) | def url(self, privacy=False, *args, **kwargs):
    method url_identifier (line 349) | def url_identifier(self):
    method __len__ (line 357) | def __len__(self):
    method parse_url (line 372) | def parse_url(url):

FILE: apprise/plugins/dbus.py
  class DBusUrgency (line 113) | class DBusUrgency:
  class NotifyDBus (line 145) | class NotifyDBus(NotifyBase):
    method __init__ (line 233) | def __init__(
    method send (line 291) | def send(self, body, title="", notify_type=NotifyType.INFO, **kwargs):
    method url (line 390) | def url(self, privacy=False, *args, **kwargs):
    method parse_url (line 417) | def parse_url(url):

FILE: apprise/plugins/dingtalk.py
  class NotifyDingTalk (line 57) | class NotifyDingTalk(NotifyBase):
    method __init__ (line 133) | def __init__(self, token, targets=None, secret=None, **kwargs):
    method get_signature (line 181) | def get_signature(self):
    method send (line 193) | def send(self, body, title="", notify_type=NotifyType.INFO, **kwargs):
    method title_maxlen (line 283) | def title_maxlen(self):
    method url (line 291) | def url(self, privacy=False, *args, **kwargs):
    method url_identifier (line 320) | def url_identifier(self):
    method __len__ (line 328) | def __len__(self):
    method parse_url (line 334) | def parse_url(url):

FILE: apprise/plugins/discord.py
  class NotifyDiscord (line 67) | class NotifyDiscord(NotifyBase):
    method __init__ (line 211) | def __init__(
    method send (line 305) | def send(
    method _send (line 463) | def _send(
    method url (line 645) | def url(self, privacy: bool = False, *args: Any, **kwargs: Any) -> str:
    method url_identifier (line 691) | def url_identifier(self) -> tuple[str, str, str]:
    method parse_url (line 696) | def parse_url(url: str) -> dict[str, Any] | None:
    method parse_native_url (line 784) | def parse_native_url(url: str) -> dict[str, Any] | None:
    method ping_payload (line 816) | def ping_payload(self, *args: str) -> dict[str, Any]:
    method extract_markdown_sections (line 869) | def extract_markdown_sections(

FILE: apprise/plugins/dot.py
  class NotifyDot (line 86) | class NotifyDot(NotifyBase):
    method __init__ (line 199) | def __init__(
    method send (line 278) | def send(
    method url_identifier (line 472) | def url_identifier(self):
    method url (line 483) | def url(self, privacy=False, *args, **kwargs):
    method __len__ (line 532) | def __len__(self):
    method parse_url (line 537) | def parse_url(url):

FILE: apprise/plugins/email/base.py
  class NotifyEmail (line 63) | class NotifyEmail(NotifyBase):
    method __init__ (line 213) | def __init__(
    method apply_email_defaults (line 439) | def apply_email_defaults(self, secure_mode=None, port=None, **kwargs):
    method send (line 531) | def send(
    method url (line 645) | def url(self, privacy=False, *args, **kwargs):
    method url_identifier (line 786) | def url_identifier(self):
    method __len__ (line 805) | def __len__(self):
    method parse_url (line 812) | def parse_url(url):
    method _get_charset (line 917) | def _get_charset(input_string):
    method prepare_emails (line 930) | def prepare_emails(

FILE: apprise/plugins/email/common.py
  class AppriseEmailException (line 34) | class AppriseEmailException(ApprisePluginException):
    method __init__ (line 39) | def __init__(self, message, error_code=601):
  class WebBaseLogin (line 43) | class WebBaseLogin:
  class SecureMailMode (line 57) | class SecureMailMode:
  class EmailMessage (line 78) | class EmailMessage:

FILE: apprise/plugins/emby.py
  class NotifyEmby (line 44) | class NotifyEmby(NotifyBase):
    method __init__ (line 119) | def __init__(self, modal=False, **kwargs):
    method login (line 151) | def login(self, **kwargs):
    method sessions (line 278) | def sessions(self, user_controlled=True):
    method logout (line 428) | def logout(self, **kwargs):
    method send (line 504) | def send(self, body, title="", notify_type=NotifyType.INFO, **kwargs):
    method url_identifier (line 605) | def url_identifier(self):
    method url (line 619) | def url(self, privacy=False, *args, **kwargs):
    method is_authenticated (line 658) | def is_authenticated(self):
    method emby_auth_header (line 663) | def emby_auth_header(self):
    method parse_url (line 681) | def parse_url(url):
    method __del__ (line 694) | def __del__(self):

FILE: apprise/plugins/enigma2.py
  class Enigma2MessageType (line 46) | class Enigma2MessageType:
  class NotifyEnigma2 (line 62) | class NotifyEnigma2(NotifyBase):
    method __init__ (line 158) | def __init__(self, timeout=None, headers=None, **kwargs):
    method url_identifier (line 188) | def url_identifier(self):
    method url (line 203) | def url(self, privacy=False, *args, **kwargs):
    method send (line 246) | def send(self, body, title="", notify_type=NotifyType.INFO, **kwargs):
    method parse_url (line 352) | def parse_url(url):

FILE: apprise/plugins/fcm/__init__.py
  class GoogleOAuth (line 77) | class GoogleOAuth:
  class NotifyFCM (line 89) | class NotifyFCM(NotifyBase):
    method __init__ (line 220) | def __init__(
    method access_token (line 328) | def access_token(self):
    method send (line 356) | def send(self, body, title="", notify_type=NotifyType.INFO, **kwargs):
    method url_identifier (line 532) | def url_identifier(self):
    method url (line 540) | def url(self, privacy=False, *args, **kwargs):
    method __len__ (line 583) | def __len__(self):
    method parse_url (line 588) | def parse_url(url):

FILE: apprise/plugins/fcm/color.py
  class FCMColorManager (line 42) | class FCMColorManager:
    method __init__ (line 59) | def __init__(self, color, asset=None):
    method get (line 94) | def get(self, notify_type=NotifyType.INFO):
    method __str__ (line 108) | def __str__(self):
    method __bool__ (line 116) | def __bool__(self):

FILE: apprise/plugins/fcm/common.py
  class FCMMode (line 29) | class FCMMode:

FILE: apprise/plugins/fcm/oauth.py
  class GoogleOAuth (line 49) | class GoogleOAuth:
    method __init__ (line 66) | def __init__(
    method __reset (line 83) | def __reset(self):
    method load (line 100) | def load(self, path):
    method access_token (line 184) | def access_token(self):
    method project_id (line 321) | def project_id(self):

FILE: apprise/plugins/fcm/priority.py
  class NotificationPriority (line 38) | class NotificationPriority:
  class FCMPriority (line 68) | class FCMPriority:
  class FCMPriorityManager (line 91) | class FCMPriorityManager:
    method __init__ (line 157) | def __init__(self, mode, priority=None):
    method payload (line 181) | def payload(self):
    method __str__ (line 189) | def __str__(self):
    method __bool__ (line 193) | def __bool__(self):

FILE: apprise/plugins/feishu.py
  class NotifyFeishu (line 45) | class NotifyFeishu(NotifyBase):
    method __init__ (line 99) | def __init__(self, token, **kwargs):
    method send (line 113) | def send(self, body, title="", notify_type=NotifyType.INFO, **kwargs):
    method url_identifier (line 197) | def url_identifier(self):
    method url (line 205) | def url(self, privacy=False, *args, **kwargs):
    method parse_url (line 218) | def parse_url(url):

FILE: apprise/plugins/flock.py
  class NotifyFlock (line 62) | class NotifyFlock(NotifyBase):
    method __init__ (line 147) | def __init__(self, token, targets=None, include_image=True, **kwargs):
    method send (line 196) | def send(self, body, title="", notify_type=NotifyType.INFO, **kwargs):
    method _post (line 256) | def _post(self, url, headers, payload):
    method url_identifier (line 310) | def url_identifier(self):
    method url (line 318) | def url(self, privacy=False, *args, **kwargs):
    method __len__ (line 338) | def __len__(self):
    method parse_url (line 344) | def parse_url(url):
    method parse_native_url (line 371) | def parse_native_url(url):

FILE: apprise/plugins/fluxer.py
  class FluxerMode (line 74) | class FluxerMode:
  class NotifyFluxer (line 90) | class NotifyFluxer(NotifyBase):
    method __init__ (line 265) | def __init__(
    method send (line 390) | def send(
    method _send (line 541) | def _send(
    method url (line 757) | def url(self, privacy: bool = False, *args: Any, **kwargs: Any) -> str:
    method url_identifier (line 830) | def url_identifier(self) -> tuple[Any, ...]:
    method parse_url (line 850) | def parse_url(url: str) -> dict[str, Any] | None:
    method parse_native_url (line 963) | def parse_native_url(url: str) -> dict[str, Any] | None:
    method ping_payload (line 996) | def ping_payload(self, *args: str) -> dict[str, Any]:
    method extract_markdown_sections (line 1042) | def extract_markdown_sections(

FILE: apprise/plugins/fortysixelks.py
  class Notify46Elks (line 54) | class Notify46Elks(NotifyBase):
    method __init__ (line 130) | def __init__(
    method send (line 180) | def send(
    method url_identifier (line 275) | def url_identifier(self):
    method url (line 283) | def url(self, privacy: bool = False, *args: Any, **kwargs: Any) -> str:
    method __len__ (line 307) | def __len__(self):
    method parse_native_url (line 313) | def parse_native_url(url):
    method parse_url (line 338) | def parse_url(url):

FILE: apprise/plugins/freemobile.py
  class NotifyFreeMobile (line 44) | class NotifyFreeMobile(NotifyBase):
    method __init__ (line 91) | def __init__(self, **kwargs):
    method url_identifier (line 105) | def url_identifier(self):
    method url (line 113) | def url(self, privacy=False, *args, **kwargs):
    method send (line 126) | def send(self, body, title="", notify_type=NotifyType.INFO, **kwargs):
    method parse_url (line 189) | def parse_url(url):

FILE: apprise/plugins/glib.py
  class GLibUrgency (line 80) | class GLibUrgency:
  class NotifyGLib (line 112) | class NotifyGLib(NotifyBase):
    method __init__ (line 194) | def __init__(self, urgency=None, x_axis=None, y_axis=None,
    method send (line 233) | def send(self, body, title="", notify_type=NotifyType.INFO, **kwargs):
    method url (line 332) | def url(self, privacy=False, *args, **kwargs):
    method parse_url (line 361) | def parse_url(url):

FILE: apprise/plugins/gnome.py
  class GnomeUrgency (line 61) | class GnomeUrgency:
  class NotifyGnome (line 92) | class NotifyGnome(NotifyBase):
    method __init__ (line 162) | def __init__(self, urgency=None, include_image=True, **kwargs):
    method send (line 184) | def send(self, body, title="", notify_type=NotifyType.INFO, **kwargs):
    method url (line 232) | def url(self, privacy=False, *args, **kwargs):
    method parse_url (line 251) | def parse_url(url):

FILE: apprise/plugins/google_chat.py
  class NotifyGoogleChat (line 67) | class NotifyGoogleChat(NotifyBase):
    method __init__ (line 150) | def __init__(
    method send (line 200) | def send(self, body, title="", notify_type=NotifyType.INFO, **kwargs):
    method url_identifier (line 290) | def url_identifier(self):
    method url (line 303) | def url(self, privacy=False, *args, **kwargs):
    method parse_url (line 323) | def parse_url(url):
    method parse_native_url (line 383) | def parse_native_url(url):

FILE: apprise/plugins/gotify.py
  class GotifyPriority (line 44) | class GotifyPriority:
  class NotifyGotify (line 89) | class NotifyGotify(NotifyBase):
    method __init__ (line 161) | def __init__(self, token, priority=None, **kwargs):
    method send (line 197) | def send(self, body, title="", notify_type=NotifyType.INFO, **kwargs):
    method url_identifier (line 276) | def url_identifier(self):
    method url (line 291) | def url(self, privacy=False, *args, **kwargs):
    method parse_url (line 323) | def parse_url(url):

FILE: apprise/plugins/growl.py
  class GrowlPriority (line 50) | class GrowlPriority:
  class NotifyGrowl (line 87) | class NotifyGrowl(NotifyBase):
    method __init__ (line 189) | def __init__(
    method register (line 232) | def register(self):
    method send (line 292) | def send(self, body, title="", notify_type=NotifyType.INFO, **kwargs):
    method url_identifier (line 362) | def url_identifier(self):
    method url (line 376) | def url(self, privacy=False, *args, **kwargs):
    method parse_url (line 417) | def parse_url(url):

FILE: apprise/plugins/guilded.py
  class NotifyGuilded (line 52) | class NotifyGuilded(discord.NotifyDiscord):
    method parse_native_url (line 71) | def parse_native_url(url):

FILE: apprise/plugins/home_assistant.py
  class NotifyHomeAssistant (line 43) | class NotifyHomeAssistant(NotifyBase):
    method __init__ (line 120) | def __init__(self, accesstoken, nid=None, **kwargs):
    method send (line 154) | def send(self, body, title="", notify_type=NotifyType.INFO, **kwargs):
    method url_identifier (line 242) | def url_identifier(self):
    method url (line 262) | def url(self, privacy=False, *args, **kwargs):
    method parse_url (line 318) | def parse_url(url):

FILE: apprise/plugins/httpsms.py
  class NotifyHttpSMS (line 41) | class NotifyHttpSMS(NotifyBase):
    method __init__ (line 123) | def __init__(self, apikey=None, source=None, targets=None, **kwargs):
    method send (line 166) | def send(self, body, title="", notify_type=NotifyType.INFO, **kwargs):
    method url_identifier (line 262) | def url_identifier(self):
    method url (line 270) | def url(self, privacy=False, *args, **kwargs):
    method __len__ (line 293) | def __len__(self):
    method parse_url (line 299) | def parse_url(url):

FILE: apprise/plugins/ifttt.py
  class NotifyIFTTT (line 53) | class NotifyIFTTT(NotifyBase):
    method __init__ (line 138) | def __init__(
    method send (line 190) | def send(self, body, title="", notify_type=NotifyType.INFO, **kwargs):
    method url_identifier (line 296) | def url_identifier(self):
    method url (line 304) | def url(self, privacy=False, *args, **kwargs):
    method __len__ (line 323) | def __len__(self):
    method parse_url (line 328) | def parse_url(url):
    method parse_native_url (line 365) | def parse_native_url(url):

FILE: apprise/plugins/irc/base.py
  class NotifyIRC (line 70) | class NotifyIRC(NotifyBase):
    method __init__ (line 176) | def __init__(
    method apply_irc_defaults (line 242) | def apply_irc_defaults(self, port=None, **kwargs):
    method send (line 278) | def send(
    method url_identifier (line 382) | def url_identifier(
    method url (line 392) | def url(self, privacy: bool = False, *args: Any, **kwargs: Any) -> str:
    method parse_url (line 456) | def parse_url(url: str) -> Optional[dict[str, Any]]:

FILE: apprise/plugins/irc/client.py
  class IRCClient (line 53) | class IRCClient:
    method __init__ (line 70) | def __init__(
    method nickname (line 119) | def nickname(self) -> str:
    method connect (line 123) | def connect(self) -> None:
    method close (line 126) | def close(self) -> None:
    method _queue (line 129) | def _queue(self, line: str) -> None:
    method _write (line 136) | def _write(self, line: Union[str, bytes], deadline: float) -> None:
    method _flush (line 150) | def _flush(self, deadline: float) -> None:
    method _read (line 156) | def _read(self, deadline: float) -> Optional[str]:
    method _nickname_collision_handler (line 179) | def _nickname_collision_handler(self, prefix: str) -> str:
    method _tick (line 194) | def _tick(self, deadline: float) -> float:
    method _handshake (line 200) | def _handshake(self, deadline: float, prefix: str) -> None:
    method register (line 225) | def register(self, timeout: float, prefix: str) -> None:
    method check_connection (line 266) | def check_connection(self, timeout: float) -> bool:
    method join (line 288) | def join(
    method privmsg (line 313) | def privmsg(self, target: str, message: str, timeout: float) -> None:
    method identify (line 320) | def identify(self, timeout: float) -> None:
    method quit (line 340) | def quit(self, message: str, timeout: float) -> None:
    method nick_generation (line 348) | def nick_generation(

FILE: apprise/plugins/irc/protocol.py
  class IRCAuthMode (line 55) | class IRCAuthMode:
  class IRCMessage (line 95) | class IRCMessage:
    method numeric (line 123) | def numeric(self) -> Optional[int]:
  function parse_irc_line (line 131) | def parse_irc_line(line: str) -> IRCMessage:
  function is_ping (line 177) | def is_ping(msg: IRCMessage) -> bool:
  function ping_payload (line 182) | def ping_payload(msg: IRCMessage) -> str:
  function extract_welcome_nick (line 189) | def extract_welcome_nick(msg: IRCMessage) -> Optional[str]:
  function normalise_channel (line 198) | def normalise_channel(name: str) -> str:

FILE: apprise/plugins/irc/state.py
  class IRCState (line 50) | class IRCState(Enum):
  class IRCActionKind (line 61) | class IRCActionKind(Enum):
  class IRCAction (line 70) | class IRCAction:
  class IRCContext (line 79) | class IRCContext:
  function _err (line 92) | def _err(msg: IRCMessage) -> str:
  class IRCStateMachine (line 117) | class IRCStateMachine:
    method __init__ (line 120) | def __init__(self, ctx: IRCContext) -> None:
    method start_registration (line 124) | def start_registration(self) -> list[IRCAction]:
    method on_message (line 141) | def on_message(self, msg: IRCMessage) -> list[IRCAction]:
    method request_join (line 203) | def request_join(
    method request_quit (line 213) | def request_quit(self, message: str) -> list[IRCAction]:

FILE: apprise/plugins/jellyfin.py
  class NotifyJellyfin (line 40) | class NotifyJellyfin(emby.NotifyEmby):

FILE: apprise/plugins/join.py
  class JoinPriority (line 66) | class JoinPriority:
  class NotifyJoin (line 103) | class NotifyJoin(NotifyBase):
    method __init__ (line 200) | def __init__(
    method send (line 258) | def send(self, body, title="", notify_type=NotifyType.INFO, **kwargs):
    method url_identifier (line 362) | def url_identifier(self):
    method url (line 370) | def url(self, privacy=False, *args, **kwargs):
    method __len__ (line 394) | def __len__(self):
    method parse_url (line 399) | def parse_url(url):

FILE: apprise/plugins/kavenegar.py
  class NotifyKavenegar (line 75) | class NotifyKavenegar(NotifyBase):
    method __init__ (line 155) | def __init__(self, apikey, source=None, targets=None, **kwargs):
    method send (line 196) | def send(self, body, title="", notify_type=NotifyType.INFO, **kwargs):
    method url_identifier (line 313) | def url_identifier(self):
    method url (line 321) | def url(self, privacy=False, *args, **kwargs):
    method __len__ (line 337) | def __len__(self):
    method parse_url (line 342) | def parse_url(url):

FILE: apprise/plugins/kodi.py
  class NotifyXBMC (line 40) | class NotifyXBMC(NotifyBase):
    method __init__ (line 137) | def __init__(self, include_image=True, duration=None, **kwargs):
    method _payload_60 (line 167) | def _payload_60(self, title, body, notify_type, **kwargs):
    method _payload_20 (line 205) | def _payload_20(self, title, body, notify_type, **kwargs):
    method send (line 235) | def send(self, body, title="", notify_type=NotifyType.INFO, **kwargs):
    method url_identifier (line 312) | def url_identifier(self):
    method url (line 340) | def url(self, privacy=False, *args, **kwargs):
    method parse_url (line 390) | def parse_url(url):

FILE: apprise/plugins/kumulos.py
  class NotifyKumulos (line 55) | class NotifyKumulos(NotifyBase):
    method __init__ (line 108) | def __init__(self, apikey, serverkey, **kwargs):
    method send (line 130) | def send(self, body, title="", notify_type=NotifyType.INFO, **kwargs):
    method url_identifier (line 201) | def url_identifier(self):
    method url (line 209) | def url(self, privacy=False, *args, **kwargs):
    method parse_url (line 223) | def parse_url(url):

FILE: apprise/plugins/lametric.py
  class LametricMode (line 111) | class LametricMode:
  class LametricPriority (line 127) | class LametricPriority:
  class LametricIconType (line 162) | class LametricIconType:
  class LametricSoundCategory (line 186) | class LametricSoundCategory:
  class LametricSound (line 193) | class LametricSound:
  class NotifyLametric (line 363) | class NotifyLametric(NotifyBase):
    method __init__ (line 529) | def __init__(
    method sound_lookup (line 664) | def sound_lookup(lookup):
    method _cloud_notification_payload (line 677) | def _cloud_notification_payload(self, body, notify_type, headers):
    method _device_notification_payload (line 732) | def _device_notification_payload(self, body, notify_type, headers):
    method send (line 800) | def send(self, body, title="", notify_type=NotifyType.INFO, **kwargs):
    method url_identifier (line 881) | def url_identifier(self):
    method url (line 911) | def url(self, privacy=False, *args, **kwargs):
    method parse_url (line 982) | def parse_url(url):
    method parse_native_url (line 1114) | def parse_native_url(url):

FILE: apprise/plugins/lark.py
  class NotifyLark (line 43) | class NotifyLark(NotifyBase):
    method __init__ (line 76) | def __init__(self, token, **kwargs):
    method url (line 95) | def url(self, privacy=False, *args, **kwargs):
    method send (line 104) | def send(self, body, title="", notify_type=NotifyType.INFO, **kwargs):
    method url_identifier (line 146) | def url_identifier(self):
    method parse_url (line 155) | def parse_url(url):
    method parse_native_url (line 174) | def parse_native_url(url):

FILE: apprise/plugins/line.py
  class NotifyLine (line 46) | class NotifyLine(NotifyBase):
    method __init__ (line 116) | def __init__(self, token, targets=None, include_image=True, **kwargs):
    method send (line 138) | def send(self, body, title="", notify_type=NotifyType.INFO, **kwargs):
    method url_identifier (line 239) | def url_identifier(self):
    method url (line 247) | def url(self, privacy=False, *args, **kwargs):
    method __len__ (line 270) | def __len__(self):
    method parse_url (line 275) | def parse_url(url):

FILE: apprise/plugins/macosx.py
  class NotifyMacOSX (line 53) | class NotifyMacOSX(NotifyBase):
    method __init__ (line 134) | def __init__(self, sound=None, include_image=True, click=None, **kwargs):
    method send (line 154) | def send(self, body, title="", notify_type=NotifyType.INFO, **kwargs):
    method url (line 209) | def url(self, privacy=False, *args, **kwargs):
    method parse_url (line 230) | def parse_url(url):

FILE: apprise/plugins/mailgun.py
  class MailgunRegion (line 78) | class MailgunRegion:
  class NotifyMailgun (line 96) | class NotifyMailgun(NotifyBase):
    method __init__ (line 213) | def __init__(
    method send (line 355) | def send(
    method url_identifier (line 619) | def url_identifier(self):
    method url (line 632) | def url(self, privacy=False, *args, **kwargs):
    method __len__ (line 693) | def __len__(self):
    method parse_url (line 707) | def parse_url(url):

FILE: apprise/plugins/mastodon.py
  class MastodonMessageVisibility (line 57) | class MastodonMessageVisibility:
  class NotifyMastodon (line 87) | class NotifyMastodon(NotifyBase):
    method __init__ (line 242) | def __init__(
    method url_identifier (line 362) | def url_identifier(self):
    method url (line 375) | def url(self, privacy=False, *args, **kwargs):
    method __len__ (line 421) | def __len__(self):
    method send (line 426) | def send(
    method _whoami (line 786) | def _whoami(self, lazy=True):
    method _request (line 844) | def _request(self, path, payload=None, method="POST"):
    method parse_url (line 998) | def parse_url(url):

FILE: apprise/plugins/matrix.py
  class MatrixDiscoveryException (line 60) | class MatrixDiscoveryException(AppriseException):
  class MatrixMessageType (line 89) | class MatrixMessageType:
  class MatrixVersion (line 103) | class MatrixVersion:
  class MatrixWebhookMode (line 118) | class MatrixWebhookMode:
  class NotifyMatrix (line 141) | class NotifyMatrix(NotifyBase):
    method __init__ (line 316) | def __init__(
    method send (line 452) | def send(self, body, title="", notify_type=NotifyType.INFO, **kwargs):
    method _send_webhook_notification (line 467) | def _send_webhook_notification(
    method _slack_webhook_payload (line 553) | def _slack_webhook_payload(
    method _matrix_webhook_payload (line 603) | def _matrix_webhook_payload(
    method _t2bot_webhook_payload (line 641) | def _t2bot_webhook_payload(
    method _send_server_notification (line 662) | def _send_server_notification(
    method _send_attachments (line 848) | def _send_attachments(self, attach):
    method _register (line 903) | def _register(self):
    method _login (line 961) | def _login(self):
    method _logout (line 1032) | def _logout(self):
    method _room_join (line 1073) | def _room_join(self, room):
    method _room_create (line 1211) | def _room_create(self, room):
    method _joined_rooms (line 1273) | def _joined_rooms(self):
    method _room_id (line 1291) | def _room_id(self, room):
    method _fetch (line 1336) | def _fetch(
    method __del__ (line 1536) | def __del__(self):
    method url_identifier (line 1557) | def url_identifier(self):
    method url (line 1575) | def url(self, privacy=False, *args, **kwargs):
    method __len__ (line 1625) | def __len__(self):
    method parse_url (line 1631) | def parse_url(url):
    method parse_native_url (line 1720) | def parse_native_url(url):
    method server_discovery (line 1750) | def server_discovery(self):
    method base_url (line 1948) | def base_url(self):
    method identity_url (line 1971) | def identity_url(self):

FILE: apprise/plugins/mattermost.py
  class MattermostMode (line 77) | class MattermostMode:
  class NotifyMattermost (line 94) | class NotifyMattermost(NotifyBase):
    method __init__ (line 231) | def __init__(
    method __len__ (line 314) | def __len__(self) -> int:
    method _channel_lookup (line 318) | def _channel_lookup(self, channel: str) -> str | None:
    method send (line 411) | def send(
    method url_identifier (line 554) | def url_identifier(self) -> tuple[Any, ...]:
    method url (line 570) | def url(self, privacy: bool = False, *args: Any, **kwargs: Any) -> str:
    method parse_url (line 638) | def parse_url(url: str):
    method parse_native_url (line 710) | def parse_native_url(url: str) -> dict[str, Any] | None:

FILE: apprise/plugins/messagebird.py
  class NotifyMessageBird (line 42) | class NotifyMessageBird(NotifyBase):
    method __init__ (line 118) | def __init__(self, apikey, source, targets=None, **kwargs):
    method send (line 164) | def send(self, body, title="", notify_type=NotifyType.INFO, **kwargs):
    method url_identifier (line 296) | def url_identifier(self):
    method url (line 304) | def url(self, privacy=False, *args, **kwargs):
    method __len__ (line 320) | def __len__(self):
    method parse_url (line 326) | def parse_url(url):

FILE: apprise/plugins/misskey.py
  class MisskeyVisibility (line 56) | class MisskeyVisibility:
  class NotifyMisskey (line 78) | class NotifyMisskey(NotifyBase):
    method __init__ (line 151) | def __init__(self, token=None, visibility=None, **kwargs):
    method url_identifier (line 196) | def url_identifier(self):
    method url (line 209) | def url(self, privacy=False, *args, **kwargs):
    method send (line 230) | def send(self, body, title="", notify_type=NotifyType.INFO, **kwargs):
    method parse_url (line 298) | def parse_url(url):

FILE: apprise/plugins/mqtt.py
  class NotifyMQTT (line 81) | class NotifyMQTT(NotifyBase):
    method __init__ (line 218) | def __init__(
    method send (line 330) | def send(self, body, title="", notify_type=NotifyType.INFO, **kwargs):
    method url_identifier (line 486) | def url_identifier(self):
    method url (line 510) | def url(self, privacy=False, *args, **kwargs):
    method __len__ (line 562) | def __len__(self):
    method parse_url (line 567) | def parse_url(url):
    method CA_CERTIFICATE_FILE_LOCATIONS (line 620) | def CA_CERTIFICATE_FILE_LOCATIONS(self):

FILE: apprise/plugins/msg91.py
  class MSG91PayloadField (line 53) | class MSG91PayloadField:
  class NotifyMSG91 (line 64) | class NotifyMSG91(NotifyBase):
    method __init__ (line 153) | def __init__(
    method send (line 214) | def send(self, body, title="", notify_type=NotifyType.INFO, **kwargs):
    method url_identifier (line 334) | def url_identifier(self):
    method url (line 342) | def url(self, privacy=False, *args, **kwargs):
    method __len__ (line 367) | def __len__(self):
    method parse_url (line 373) | def parse_url(url):

FILE: apprise/plugins/msteams.py
  class NotifyMSTeams (line 92) | class NotifyMSTeams(NotifyBase):
    method __init__ (line 227) | def __init__(
    method gen_payload (line 343) | def gen_payload(
    method send (line 440) | def send(self, body, title="", notify_type=NotifyType.INFO, **kwargs):
    method url_identifier (line 530) | def url_identifier(self):
    method url (line 544) | def url(self, privacy=False, *args, **kwargs):
    method parse_url (line 607) | def parse_url(url):
    method parse_native_url (line 683) | def parse_native_url(url):

FILE: apprise/plugins/nextcloud.py
  class NextcloudGroupDiscoveryException (line 55) | class NextcloudGroupDiscoveryException(AppriseException):
  class NotifyNextcloud (line 59) | class NotifyNextcloud(NotifyBase):
    method __init__ (line 186) | def __init__(
    method _fetch (line 249) | def _fetch(self, payload=None, target=None, group=None):
    method send (line 390) | def send(self, body, title="", notify_type=NotifyType.INFO, **kwargs):
    method users_by_group (line 436) | def users_by_group(self, group):
    method all_users (line 474) | def all_users(self):
    method url_identifier (line 512) | def url_identifier(self):
    method url (line 527) | def url(self, privacy=False, *args, **kwargs):
    method __len__ (line 584) | def __len__(self):
    method parse_url (line 590) | def parse_url(url):

FILE: apprise/plugins/nextcloudtalk.py
  class NotifyNextcloudTalk (line 39) | class NotifyNextcloudTalk(NotifyBase):
    method __init__ (line 127) | def __init__(self, targets=None, headers=None, url_prefix=None, **kwar...
    method send (line 149) | def send(self, body, title="", notify_type=NotifyType.INFO, **kwargs):
    method url_identifier (line 270) | def url_identifier(self):
    method url (line 284) | def url(self, privacy=False, *args, **kwargs):
    method __len__ (line 321) | def __len__(self):
    method parse_url (line 327) | def parse_url(url):

FILE: apprise/plugins/notica.py
  class NoticaMode (line 53) | class NoticaMode:
  class NotifyNotica (line 71) | class NotifyNotica(NotifyBase):
    method __init__ (line 161) | def __init__(self, token, headers=None, **kwargs):
    method send (line 187) | def send(self, body, title="", notify_type=NotifyType.INFO, **kwargs):
    method url_identifier (line 275) | def url_identifier(self):
    method url (line 291) | def url(self, privacy=False, *args, **kwargs):
    method parse_url (line 345) | def parse_url(url):
    method parse_native_url (line 389) | def parse_native_url(url):

FILE: apprise/plugins/notifiarr.py
  class NotifyNotifiarr (line 53) | class NotifyNotifiarr(NotifyBase):
    method __init__ (line 141) | def __init__(
    method url_identifier (line 207) | def url_identifier(self):
    method url (line 218) | def url(self, privacy=False, *args, **kwargs):
    method send (line 250) | def send(self, body, title="", notify_type=NotifyType.INFO, **kwargs):
    method _send (line 340) | def _send(self, payload):
    method __len__ (line 401) | def __len__(self):
    method parse_url (line 407) | def parse_url(url):

FILE: apprise/plugins/notificationapi.py
  class NotificationAPIRegion (line 58) | class NotificationAPIRegion:
  class NotificationAPIChannel (line 81) | class NotificationAPIChannel:
  class NotificationAPIMode (line 103) | class NotificationAPIMode:
  class NotifyNotificationAPI (line 117) | class NotifyNotificationAPI(NotifyBase):
    method __init__ (line 252) | def __init__(self, client_id, client_secret, message_type=None,
    method url_identifier (line 515) | def url_identifier(self):
    method url (line 523) | def url(self, privacy=False, *args, **kwargs):
    method __len__ (line 603) | def __len__(self):
    method gen_payload (line 610) | def gen_payload(self, body, title="", notify_type=NotifyType.INFO,
    method send (line 752) | def send(self, body, title="", notify_type=NotifyType.INFO, **kwargs):
    method parse_url (line 856) | def parse_url(url):

FILE: apprise/plugins/notifico.py
  class NotificoFormat (line 53) | class NotificoFormat:
  class NotificoColor (line 64) | class NotificoColor:
  class NotifyNotifico (line 87) | class NotifyNotifico(NotifyBase):
    method __init__ (line 161) | def __init__(self, project_id, msghook, color=True, prefix=True, **kwa...
    method url_identifier (line 202) | def url_identifier(self):
    method url (line 210) | def url(self, privacy=False, *args, **kwargs):
    method send (line 229) | def send(self, body, title="", notify_type=NotifyType.INFO, **kwargs):
    method parse_url (line 341) | def parse_url(url):
    method parse_native_url (line 371) | def parse_native_url(url):

FILE: apprise/plugins/ntfy.py
  class NtfyMode (line 59) | class NtfyMode:
  class NtfyAuth (line 79) | class NtfyAuth:
  class NtfyPriority (line 95) | class NtfyPriority:
  class NotifyNtfy (line 145) | class NotifyNtfy(NotifyBase):
    method __init__ (line 316) | def __init__(
    method send (line 434) | def send(
    method _send (line 510) | def _send(
    method url_identifier (line 730) | def url_identifier(self):
    method url (line 763) | def url(self, privacy=False, *args, **kwargs):
    method __len__ (line 846) | def __len__(self):
    method parse_url (line 851) | def parse_url(url):
    method parse_native_url (line 998) | def parse_native_url(url):

FILE: apprise/plugins/office365.py
  class NotifyOffice365 (line 57) | class NotifyOffice365(NotifyBase):
    method __init__ (line 178) | def __init__(
    method send (line 317) | def send(
    method upload_attachment (line 638) | def upload_attachment(self, attachment, message_id, name=None):
    method authenticate (line 717) | def authenticate(self):
    method _fetch (line 793) | def _fetch(
    method url_identifier (line 941) | def url_identifier(self):
    method url (line 955) | def url(self, privacy=False, *args, **kwargs):
    method __len__ (line 1002) | def __len__(self):
    method parse_url (line 1007) | def parse_url(url):

FILE: apprise/plugins/one_signal.py
  class OneSignalCategory (line 47) | class OneSignalCategory:
  class NotifyOneSignal (line 65) | class NotifyOneSignal(NotifyBase):
    method __init__ (line 204) | def __init__(
    method send (line 368) | def send(self, body, title="", notify_type=NotifyType.INFO, **kwargs):
    method url_identifier (line 517) | def url_identifier(self):
    method url (line 530) | def url(self, privacy=False, *args, **kwargs):
    method __len__ (line 609) | def __len__(self):
    method parse_url (line 629) | def parse_url(url):

FILE: apprise/plugins/opsgenie.py
  class OpsgenieCategory (line 59) | class OpsgenieCategory(NotifyBase):
  class OpsgenieAlertAction (line 76) | class OpsgenieAlertAction:
  class OpsgenieRegion (line 117) | class OpsgenieRegion:
  class OpsgeniePriority (line 136) | class OpsgeniePriority:
  class NotifyOpsgenie (line 179) | class NotifyOpsgenie(NotifyBase):
    method __init__ (line 334) | def __init__(
    method _fetch (line 523) | def _fetch(self, method, url, payload, params=None):
    method send (line 602) | def send(self, body, title="", notify_type=NotifyType.INFO, **kwargs):
    method url_identifier (line 751) | def url_identifier(self):
    method url (line 759) | def url(self, privacy=False, *args, **kwargs):
    method __len__ (line 828) | def __len__(self):
    method parse_url (line 842) | def parse_url(url):

FILE: apprise/plugins/pagerduty.py
  class PagerDutySeverity (line 44) | class PagerDutySeverity:
  class PagerDutyRegion (line 73) | class PagerDutyRegion:
  class NotifyPagerDuty (line 91) | class NotifyPagerDuty(NotifyBase):
    method __init__ (line 207) | def __init__(
    method send (line 326) | def send(self, body, title="", notify_type=NotifyType.INFO, **kwargs):
    method url_identifier (line 445) | def url_identifier(self):
    method url (line 458) | def url(self, privacy=False, *args, **kwargs):
    method parse_url (line 504) | def parse_url(url):

FILE: apprise/plugins/pagertree.py
  class PagerTreeAction (line 40) | class PagerTreeAction:
  class PagerTreeUrgency (line 47) | class PagerTreeUrgency:
  class NotifyPagerTree (line 79) | class NotifyPagerTree(NotifyBase):
    method __init__ (line 155) | def __init__(
    method send (line 226) | def send(self, body, title="", notify_type=NotifyType.INFO, **kwargs):
    method url_identifier (line 318) | def url_identifier(self):
    method url (line 326) | def url(self, privacy=False, *args, **kwargs):
    method parse_url (line 366) | def parse_url(url):

FILE: apprise/plugins/parseplatform.py
  class ParsePlatformDevice (line 43) | class ParsePlatformDevice:
  class NotifyParsePlatform (line 61) | class NotifyParsePlatform(NotifyBase):
    method __init__ (line 134) | def __init__(self, app_id, master_key, device=None, **kwargs):
    method send (line 189) | def send(self, body, title="", notify_type=NotifyType.INFO, **kwargs):
    method url_identifier (line 275) | def url_identifier(self):
    method url (line 290) | def url(self, privacy=False, *args, **kwargs):
    method parse_url (line 321) | def parse_url(url):

FILE: apprise/plugins/plivo.py
  class NotifyPlivo (line 49) | class NotifyPlivo(NotifyBase):
    method __init__ (line 144) | def __init__(
    method send (line 207) | def send(self, body, title="", notify_type=NotifyType.INFO, **kwargs):
    method url_identifier (line 319) | def url_identifier(self):
    method url (line 332) | def url(self, privacy=False, *args, **kwargs):
    method __len__ (line 356) | def __len__(self):
    method parse_url (line 364) | def parse_url(url):

FILE: apprise/plugins/popcorn_notify.py
  class NotifyPopcornNotify (line 42) | class NotifyPopcornNotify(NotifyBase):
    method __init__ (line 111) | def __init__(self, apikey, targets=None, batch=False, **kwargs):
    method send (line 148) | def send(self, body, title="", notify_type=NotifyType.INFO, **kwargs):
    method url_identifier (line 256) | def url_identifier(self):
    method url (line 264) | def url(self, privacy=False, *args, **kwargs):
    method __len__ (line 284) | def __len__(self):
    method parse_url (line 299) | def parse_url(url):

FILE: apprise/plugins/prowl.py
  class ProwlPriority (line 39) | class ProwlPriority:
  class NotifyProwl (line 82) | class NotifyProwl(NotifyBase):
    method __init__ (line 149) | def __init__(self, apikey, providerkey=None, priority=None, **kwargs):
    method send (line 195) | def send(self, body, title="", notify_type=NotifyType.INFO, **kwargs):
    method url_identifier (line 265) | def url_identifier(self):
    method url (line 273) | def url(self, privacy=False, *args, **kwargs):
    method parse_url (line 296) | def parse_url(url):

FILE: apprise/plugins/pushbullet.py
  class NotifyPushBullet (line 47) | class NotifyPushBullet(NotifyBase):
    method __init__ (line 121) | def __init__(self, accesstoken, targets=None, **kwargs):
    method send (line 141) | def send(
    method _send (line 306) | def _send(self, url, payload, **kwargs):
    method url_identifier (line 419) | def url_identifier(self):
    method url (line 427) | def url(self, privacy=False, *args, **kwargs):
    method __len__ (line 446) | def __len__(self):
    method parse_url (line 451) | def parse_url(url):

FILE: apprise/plugins/pushdeer.py
  class NotifyPushDeer (line 39) | class NotifyPushDeer(NotifyBase):
    method __init__ (line 94) | def __init__(self, pushkey, **kwargs):
    method send (line 107) | def send(self, body, title="", notify_type=NotifyType.INFO, **kwargs):
    method url_identifier (line 183) | def url_identifier(self):
    method url (line 196) | def url(self, privacy=False):
    method parse_url (line 212) | def parse_url(url):

FILE: apprise/plugins/pushed.py
  class NotifyPushed (line 47) | class NotifyPushed(NotifyBase):
    method __init__ (line 123) | def __init__(self, app_key, app_secret, targets=None, **kwargs):
    method send (line 182) | def send(self, body, title="", notify_type=NotifyType.INFO, **kwargs):
    method _send (line 249) | def _send(self, payload, notify_type, **kwargs):
    method url_identifier (line 313) | def url_identifier(self):
    method url (line 321) | def url(self, privacy=False, *args, **kwargs):
    method __len__ (line 345) | def __len__(self):
    method parse_url (line 351) | def parse_url(url):

FILE: apprise/plugins/pushjet.py
  class NotifyPushjet (line 39) | class NotifyPushjet(NotifyBase):
    method __init__ (line 108) | def __init__(self, secret_key, **kwargs):
    method url_identifier (line 124) | def url_identifier(self):
    method url (line 139) | def url(self, privacy=False, *args, **kwargs):
    method send (line 173) | def send(self, body, title="", notify_type=NotifyType.INFO, **kwargs):
    method parse_url (line 257) | def parse_url(url):

FILE: apprise/plugins/pushme.py
  class NotifyPushMe (line 36) | class NotifyPushMe(NotifyBase):
    method __init__ (line 88) | def __init__(self, token, status=None, **kwargs):
    method send (line 104) | def send(self, body, title="", notify_type=NotifyType.INFO, **kwargs):
    method url_identifier (line 177) | def url_identifier(self):
    method url (line 185) | def url(self, privacy=False, *args, **kwargs):
    method parse_url (line 204) | def parse_url(url):

FILE: apprise/plugins/pushover.py
  class PushoverPriority (line 49) | class PushoverPriority:
  class PushoverSound (line 58) | class PushoverSound:
  class NotifyPushover (line 142) | class NotifyPushover(NotifyBase):
    method __init__ (line 257) | def __init__(
    method send (line 367) | def send(
    method _send (line 439) | def _send(self, payload, attach=None):
    method url_identifier (line 584) | def url_identifier(self):
    method url (line 592) | def url(self, privacy=False, *args, **kwargs):
    method parse_url (line 632) | def parse_url(url):

FILE: apprise/plugins/pushplus.py
  class NotifyPushplus (line 44) | class NotifyPushplus(NotifyBase):
    method __init__ (line 77) | def __init__(self, token, **kwargs):
    method url (line 89) | def url(self, privacy=False, *args, **kwargs):
    method url_identifier (line 99) | def url_identifier(self):
    method send (line 103) | def send(self, body, title="", notify_type=NotifyType.INFO, **kwargs):
    method parse_url (line 144) | def parse_url(url):
    method parse_native_url (line 159) | def parse_native_url(url):

FILE: apprise/plugins/pushsafer.py
  class PushSaferSound (line 41) | class PushSaferSound:
  class PushSaferPriority (line 252) | class PushSaferPriority:
  class PushSaferVibration (line 286) | class PushSaferVibration:
  class NotifyPushSafer (line 317) | class NotifyPushSafer(NotifyBase):
    method __init__ (line 407) | def __init__(
    method send (line 569) | def send(
    method _send (line 708) | def _send(self, payload, **kwargs):
    method url_identifier (line 817) | def url_identifier(self):
    method url (line 828) | def url(self, privacy=False, *args, **kwargs):
    method __len__ (line 873) | def __len__(self):
    method parse_url (line 878) | def parse_url(url):

FILE: apprise/plugins/pushy.py
  class NotifyPushy (line 50) | class NotifyPushy(NotifyBase):
    method __init__ (line 127) | def __init__(self, apikey, targets=None, sound=None, badge=None, **kwa...
    method send (line 181) | def send(self, body, title="", notify_type=NotifyType.INFO, **kwargs):
    method url_identifier (line 315) | def url_identifier(self):
    method url (line 323) | def url(self, privacy=False, *args, **kwargs):
    method __len__ (line 352) | def __len__(self):
    method parse_url (line 357) | def parse_url(url):

FILE: apprise/plugins/qq.py
  class NotifyQQ (line 42) | class NotifyQQ(NotifyBase):
    method __init__ (line 75) | def __init__(self, token, **kwargs):
    method url (line 93) | def url(self, privacy=False, *args, **kwargs):
    method url_identifier (line 103) | def url_identifier(self):
    method send (line 107) | def send(self, body, title="", notify_type=NotifyType.INFO, **kwargs):
    method parse_url (line 142) | def parse_url(url):
    method parse_native_url (line 157) | def parse_native_url(url):

FILE: apprise/plugins/reddit.py
  class RedditMessageKind (line 67) | class RedditMessageKind:
  class NotifyReddit (line 88) | class NotifyReddit(NotifyBase):
    method __init__ (line 233) | def __init__(
    method url_identifier (line 334) | def url_identifier(self):
    method url (line 348) | def url(self, privacy=False, *args, **kwargs):
    method __len__ (line 395) | def __len__(self):
    method login (line 399) | def login(self):
    method send (line 464) | def send(self, body, title="", notify_type=NotifyType.INFO, **kwargs):
    method _fetch (line 542) | def _fetch(self, url, payload=None):
    method parse_url (line 721) | def parse_url(url):

FILE: apprise/plugins/resend.py
  class NotifyResend (line 65) | class NotifyResend(NotifyBase):
    method __init__ (line 163) | def __init__(
    method url_identifier (line 280) | def url_identifier(self):
    method url (line 288) | def url(self, privacy=False, *args, **kwargs):
    method __len__ (line 346) | def __len__(self):
    method send (line 350) | def send(
    method parse_url (line 535) | def parse_url(url):

FILE: apprise/plugins/revolt.py
  class NotifyRevolt (line 49) | class NotifyRevolt(NotifyBase):
    method __init__ (line 141) | def __init__(self, bot_token, targets, icon_url=None, link=None, **kwa...
    method send (line 181) | def send(self, body, title="", notify_type=NotifyType.INFO, **kwargs):
    method _send (line 223) | def _send(self, payload, channel_id, retries=1, **kwargs):
    method url_identifier (line 360) | def url_identifier(self):
    method url (line 368) | def url(self, privacy=False, *args, **kwargs):
    method __len__ (line 391) | def __len__(self):
    method parse_url (line 396) | def parse_url(url):

FILE: apprise/plugins/rocketchat.py
  class RocketChatAuthMode (line 51) | class RocketChatAuthMode:
  class NotifyRocketChat (line 72) | class NotifyRocketChat(NotifyBase):
    method __init__ (line 195) | def __init__(
    method url_identifier (line 325) | def url_identifier(self):
    method url (line 344) | def url(self, privacy=False, *args, **kwargs):
    method __len__ (line 402) | def __len__(self):
    method send (line 407) | def send(self, body, title="", notify_type=NotifyType.INFO, **kwargs):
    method _send_webhook_notification (line 422) | def _send_webhook_notification(
    method _send_basic_notification (line 465) | def _send_basic_notification(
    method _payload (line 516) | def _payload(self, body, title="", notify_type=NotifyType.INFO):
    method _send (line 530) | def _send(
    method login (line 600) | def login(self):
    method logout (line 676) | def logout(self):
    method parse_url (line 725) | def parse_url(url):

FILE: apprise/plugins/rsyslog.py
  class syslog (line 37) | class syslog:
  class SyslogFacility (line 68) | class SyslogFacility:
  class NotifyRSyslog (line 142) | class NotifyRSyslog(NotifyBase):
    method __init__ (line 211) | def __init__(self, facility=None, log_pid=True, **kwargs):
    method send (line 234) | def send(self, body, title="", notify_type=NotifyType.INFO, **kwargs):
    method url_identifier (line 305) | def url_identifier(self):
    method url (line 321) | def url(self, privacy=False, *args, **kwargs):
    method parse_url (line 350) | def parse_url(url):

FILE: apprise/plugins/ryver.py
  class RyverWebhookMode (line 47) | class RyverWebhookMode:
  class NotifyRyver (line 61) | class NotifyRyver(NotifyBase):
    method __init__ (line 132) | def __init__(
    method send (line 194) | def send(self, body, title="", notify_type=NotifyType.INFO, **kwargs):
    method url_identifier (line 282) | def url_identifier(self):
    method url (line 290) | def url(self, privacy=False, *args, **kwargs):
    method parse_url (line 318) | def parse_url(url):
    method parse_native_url (line 349) | def parse_native_url(url):

FILE: apprise/plugins/sendgrid.py
  class NotifySendGrid (line 74) | class NotifySendGrid(NotifyBase):
    method __init__ (line 171) | def __init__(
    method url_identifier (line 264) | def url_identifier(self):
    method url (line 272) | def url(self, privacy=False, *args, **kwargs):
    method __len__ (line 314) | def __len__(self):
    method send (line 318) | def send(
    method parse_url (line 514) | def parse_url(url):

FILE: apprise/plugins/sendpulse.py
  class NotifySendPulse (line 48) | class NotifySendPulse(NotifyBase):
    method __init__ (line 181) | def __init__(self, client_id, client_secret, from_addr=None, targets=N...
    method url_identifier (line 336) | def url_identifier(self):
    method url (line 344) | def url(self, privacy=False, *args, **kwargs):
    method __len__ (line 399) | def __len__(self):
    method login (line 405) | def login(self):
    method send (line 448) | def send(self, body, title="", notify_type=NotifyType.INFO, attach=None,
    method _fetch (line 587) | def _fetch(self, url, payload, target=None, retry=0):
    method parse_url (line 694) | def parse_url(url):

FILE: apprise/plugins/serverchan.py
  class NotifyServerChan (line 44) | class NotifyServerChan(NotifyBase):
    method __init__ (line 79) | def __init__(self, token, **kwargs):
    method send (line 92) | def send(self, body, title="", notify_type=NotifyType.INFO, **kwargs):
    method url_identifier (line 150) | def url_identifier(self):
    method url (line 158) | def url(self, privacy=False):
    method parse_url (line 167) | def parse_url(url):

FILE: apprise/plugins/ses.py
  class NotifySES (line 115) | class NotifySES(NotifyBase):
    method __init__ (line 225) | def __init__(
    method send (line 380) | def send(
    method _post (line 553) | def _post(self, payload, to):
    method aws_prepare_request (line 624) | def aws_prepare_request(self, payload, reference=None):
    method aws_auth_signature (line 707) | def aws_auth_signature(self, to_sign, reference):
    method aws_response_to_dict (line 730) | def aws_response_to_dict(aws_response):
    method url_identifier (line 810) | def url_identifier(self):
    method url (line 824) | def url(self, privacy=False, *args, **kwargs):
    method __len__ (line 892) | def __len__(self):
    method parse_url (line 898) | def parse_url(url):

FILE: apprise/plugins/seven.py
  class NotifySeven (line 42) | class NotifySeven(NotifyBase):
    method __init__ (line 124) | def __init__(
    method url_identifier (line 166) | def url_identifier(self):
    method send (line 174) | def send(self, body, title="", notify_type=NotifyType.INFO, **kwargs):
    method url (line 288) | def url(self, privacy=False, *args, **kwargs):
    method __len__ (line 311) | def __len__(self):
    method parse_url (line 317) | def parse_url(url):

FILE: apprise/plugins/sfr.py
  class NotifySFR (line 54) | class NotifySFR(NotifyBase):
    method __init__ (line 163) | def __init__(
    method send (line 233) | def send(self, body, title="", notify_type=NotifyType.INFO, **kwargs):
    method url_identifier (line 365) | def url_identifier(self):
    method url (line 378) | def url(self, privacy=False, *args, **kwargs):
    method __len__ (line 408) | def __len__(self):
    method parse_url (line 413) | def parse_url(url):

FILE: apprise/plugins/signal_api.py
  class NotifySignalAPI (line 49) | class NotifySignalAPI(NotifyBase):
    method __init__ (line 167) | def __init__(
    method send (line 227) | def send(
    method url_identifier (line 429) | def url_identifier(self):
    method url (line 444) | def url(self, privacy=False, *args, **kwargs):
    method __len__ (line 503) | def __len__(self):
    method parse_url (line 518) | def parse_url(url):

FILE: apprise/plugins/signl4.py
  class NotifySIGNL4 (line 44) | class NotifySIGNL4(NotifyBase):
    method __init__ (line 112) | def __init__(
    method send (line 160) | def send(self, body, title="", notify_type=NotifyType.INFO, **kwargs):
    method url_identifier (line 250) | def url_identifier(self):
    method url (line 260) | def url(self, privacy=False, *args, **kwargs):
    method parse_url (line 301) | def parse_url(url):

FILE: apprise/plugins/simplepush.py
  class NotifySimplePush (line 58) | class NotifySimplePush(NotifyBase):
    method __init__ (line 132) | def __init__(self, apikey, event=None, **kwargs):
    method _encrypt (line 163) | def _encrypt(self, content):
    method send (line 214) | def send(self, body, title="", notify_type=NotifyType.INFO, **kwargs):
    method url_identifier (line 316) | def url_identifier(self):
    method url (line 324) | def url(self, privacy=False, *args, **kwargs):
    method parse_url (line 353) | def parse_url(url):

FILE: apprise/plugins/sinch.py
  class SinchRegion (line 49) | class SinchRegion:
  class NotifySinch (line 60) | class NotifySinch(NotifyBase):
    method __init__ (line 171) | def __init__(
    method send (line 266) | def send(self, body, title="", notify_type=NotifyType.INFO, **kwargs):
    method url_identifier (line 405) | def url_identifier(self):
    method url (line 418) | def url(self, privacy=False, *args, **kwargs):
    method __len__ (line 442) | def __len__(self):
    method parse_url (line 448) | def parse_url(url):

FILE: apprise/plugins/slack.py
  class SlackMode (line 100) | class SlackMode:
  class NotifySlack (line 126) | class NotifySlack(NotifyBase):
    method __init__ (line 343) | def __init__(
    method send (line 470) | def send(
    method lookup_userid (line 823) | def lookup_userid(self, email):
    method _send (line 981) | def _send(
    method url_identifier (line 1149) | def url_identifier(self):
    method url (line 1163) | def url(self, privacy=False, *args, **kwargs):
    method __len__ (line 1211) | def __len__(self):
    method parse_url (line 1216) | def parse_url(url):
    method parse_native_url (line 1311) | def parse_native_url(url):

FILE: apprise/plugins/smpp.py
  class NotifySMPP (line 49) | class NotifySMPP(NotifyBase):
    method __init__ (line 135) | def __init__(self, source=None, targets=None, **kwargs):
    method url_identifier (line 178) | def url_identifier(self):
    method url (line 193) | def url(self, privacy=False, *args, **kwargs):
    method __len__ (line 213) | def __len__(self):
    method send (line 220) | def send(self, body, title="", notify_type=NotifyType.INFO, **kwargs):
    method parse_url (line 282) | def parse_url(url):

FILE: apprise/plugins/smseagle.py
  class SMSEaglePriority (line 54) | class SMSEaglePriority:
  class SMSEagleCategory (line 73) | class SMSEagleCategory:
  class NotifySMSEagle (line 89) | class NotifySMSEagle(NotifyBase):
    method __init__ (line 218) | def __init__(
    method send (line 350) | def send(
    method url_identifier (line 630) | def url_identifier(self):
    method url (line 643) | def url(self, privacy=False, *args, **kwargs):
    method __len__ (line 699) | def __len__(self):
    method parse_url (line 724) | def parse_url(url):

FILE: apprise/plugins/smsmanager.py
  class SMSManagerGateway (line 49) | class SMSManagerGateway:
  class NotifySMSManager (line 67) | class NotifySMSManager(NotifyBase):
    method __init__ (line 159) | def __init__(
    method send (line 221) | def send(self, body, title="", notify_type=NotifyType.INFO, **kwargs):
    method url_identifier (line 345) | def url_identifier(self):
    method url (line 353) | def url(self, privacy=False, *args, **kwargs):
    method __len__ (line 378) | def __len__(self):
    method parse_url (line 396) | def parse_url(url):

FILE: apprise/plugins/smtp2go.py
  class NotifySMTP2Go (line 65) | class NotifySMTP2Go(NotifyBase):
    method __init__ (line 166) | def __init__(
    method send (line 279) | def send(
    method url_identifier (line 509) | def url_identifier(self):
    method url (line 517) | def url(self, privacy=False, *args, **kwargs):
    method __len__ (line 574) | def __len__(self):
    method parse_url (line 589) | def parse_url(url):

FILE: apprise/plugins/sns.py
  class NotifySNS (line 69) | class NotifySNS(NotifyBase):
    method __init__ (line 164) | def __init__(
    method send (line 240) | def send(self, body, title="", notify_type=NotifyType.INFO, **kwargs):
    method _post (line 313) | def _post(self, payload, to):
    method aws_prepare_request (line 384) | def aws_prepare_request(self, payload, reference=None):
    method aws_auth_signature (line 471) | def aws_auth_signature(self, to_sign, reference):
    method aws_response_to_dict (line 494) | def aws_response_to_dict(aws_response):
    method url_identifier (line 570) | def url_identifier(self):
    method url (line 583) | def url(self, privacy=False, *args, **kwargs):
    method __len__ (line 614) | def __len__(self):
    method parse_url (line 619) | def parse_url(url):

FILE: apprise/plugins/sparkpost.py
  class SparkPostRegion (line 84) | class SparkPostRegion:
  class NotifySparkPost (line 104) | class NotifySparkPost(NotifyBase):
    method __init__ (line 228) | def __init__(
    method __post (line 368) | def __post(self, payload, retry):
    method send (line 495) | def send(
    method url_identifier (line 688) | def url_identifier(self):
    method url (line 696) | def url(self, privacy=False, *args, **kwargs):
    method __len__ (line 757) | def __len__(self):
    method parse_url (line 772) | def parse_url(url):

FILE: apprise/plugins/spike.py
  class NotifySpike (line 44) | class NotifySpike(NotifyBase):
    method __init__ (line 77) | def __init__(self, token, **kwargs):
    method url_identifier (line 92) | def url_identifier(self):
    method url (line 100) | def url(self, privacy=False, *args, **kwargs):
    method send (line 109) | def send(self, body, title="", notify_type=NotifyType.INFO, **kwargs):
    method parse_url (line 149) | def parse_url(url):
    method parse_native_url (line 167) | def parse_native_url(url):

FILE: apprise/plugins/splunk.py
  class SplunkAction (line 47) | class SplunkAction:
  class SplunkMessageType (line 84) | class SplunkMessageType:
  class NotifySplunk (line 113) | class NotifySplunk(NotifyBase):
    method __init__ (line 218) | def __init__(
    method send (line 304) | def send(self, body, title="", notify_type=NotifyType.INFO, **kwargs):
    method url_identifier (line 407) | def url_identifier(self):
    method url (line 420) | def url(self, privacy=False, *args, **kwargs):
    method parse_url (line 445) | def parse_url(url):
    method parse_native_url (line 500) | def parse_native_url(url):

FILE: apprise/plugins/spugpush.py
  class NotifySpugpush (line 44) | class NotifySpugpush(NotifyBase):
    method __init__ (line 77) | def __init__(self, token, **kwargs):
    method url (line 91) | def url(self, privacy=False, *args, **kwargs):
    method url_identifier (line 101) | def url_identifier(self):
    method send (line 105) | def send(self, body, title="", notify_type=NotifyType.INFO, **kwargs):
    method parse_url (line 144) | def parse_url(url):
    method parse_native_url (line 159) | def parse_native_url(url):

FILE: apprise/plugins/streamlabs.py
  class StrmlabsCall (line 47) | class StrmlabsCall:
  class StrmlabsAlert (line 60) | class StrmlabsAlert:
  class NotifyStreamlabs (line 76) | class NotifyStreamlabs(NotifyBase):
    method __init__ (line 178) | def __init__(
    method send (line 274) | def send(self, body, title="", notify_type=NotifyType.INFO, **kwargs):
    method url_identifier (line 379) | def url_identifier(self):
    method url (line 387) | def url(self, privacy=False, *args, **kwargs):
    method parse_url (line 415) | def parse_url(url):

FILE: apprise/plugins/synology.py
  class NotifySynology (line 41) | class NotifySynology(NotifyBase):
    method __init__ (line 133) | def __init__(self, token=None, headers=None, file_url=None, **kwargs):
    method url_identifier (line 161) | def url_identifier(self):
    method url (line 177) | def url(self, privacy=False, *args, **kwargs):
    method send (line 231) | def send(self, body, title="", notify_type=NotifyType.INFO, **kwargs):
    method parse_url (line 329) | def parse_url(url):

FILE: apprise/plugins/syslog.py
  class SyslogFacility (line 36) | class SyslogFacility:
  class NotifySyslog (line 110) | class NotifySyslog(NotifyBase):
    method __init__ (line 175) | def __init__(
    method send (line 218) | def send(self, body, title="", notify_type=NotifyType.INFO, **kwargs):
    method url (line 248) | def url(self, privacy=False, *args, **kwargs):
    method parse_url (line 271) | def parse_url(url):

FILE: apprise/plugins/techuluspush.py
  class NotifyTechulusPush (line 68) | class NotifyTechulusPush(NotifyBase):
    method __init__ (line 106) | def __init__(self, apikey, **kwargs):
    method send (line 119) | def send(self, body, title="", notify_type=NotifyType.INFO, **kwargs):
    method url_identifier (line 186) | def url_identifier(self):
    method url (line 194) | def url(self, privacy=False, *args, **kwargs):
    method parse_url (line 207) | def parse_url(url):

FILE: apprise/plugins/telegram.py
  class TelegramMarkdownVersion (line 84) | class TelegramMarkdownVersion:
  class TelegramContentPlacement (line 111) | class TelegramContentPlacement:
  class NotifyTelegram (line 127) | class NotifyTelegram(NotifyBase):
    method __init__ (line 416) | def __init__(
    method send_media (line 542) | def send_media(self, target, notify_type, payload=None, attach=None):
    method detect_bot_owner (line 663) | def detect_bot_owner(self):
    method send (line 785) | def send(
    method _send_attachments (line 1014) | def _send_attachments(self, target, notify_type, attach, payload=None):
    method url_identifier (line 1041) | def url_identifier(self):
    method url (line 1049) | def url(self, privacy=False, *args, **kwargs):
    method __len__ (line 1092) | def __len__(self):
    method parse_url (line 1097) | def parse_url(url):

FILE: apprise/plugins/threema.py
  class ThreemaRecipientTypes (line 45) | class ThreemaRecipientTypes:
  class NotifyThreema (line 53) | class NotifyThreema(NotifyBase):
    method __init__ (line 141) | def __init__(self, secret=None, targets=None, **kwargs):
    method send (line 202) | def send(self, body, title="", notify_type=NotifyType.INFO, **kwargs):
    method url_identifier (line 303) | def url_identifier(self):
    method url (line 311) | def url(self, privacy=False, *args, **kwargs):
    method __len__ (line 339) | def __len__(self):
    method parse_url (line 345) | def parse_url(url):

FILE: apprise/plugins/twilio.py
  class TwilioNotificationMethod (line 63) | class TwilioNotificationMethod:
  class TwilioMessageMode (line 76) | class TwilioMessageMode:
  class NotifyTwilio (line 86) | class NotifyTwilio(NotifyBase):
    method __init__ (line 211) | def __init__(
    method send (line 371) | def send(self, body, title="", notify_type=NotifyType.INFO, **kwargs):
    method body_maxlen (line 504) | def body_maxlen(self):
    method url_identifier (line 512) | def url_identifier(self):
    method url (line 525) | def url(self, privacy=False, *args, **kwargs):
    method __len__ (line 561) | def __len__(self):
    method parse_url (line 567) | def parse_url(url):

FILE: apprise/plugins/twist.py
  class NotifyTwist (line 59) | class NotifyTwist(NotifyBase):
    method __init__ (line 139) | def __init__(self, email=None, targets=None, **kwargs):
    method url_identifier (line 229) | def url_identifier(self):
    method url (line 243) | def url(self, privacy=False, *args, **kwargs):
    method __len__ (line 270) | def __len__(self):
    method login (line 274) | def login(self):
    method logout (line 364) | def logout(self):
    method get_workspaces (line 380) | def get_workspaces(self):
    method get_channels (line 424) | def get_channels(self, wid):
    method _channel_migration (line 475) | def _channel_migration(self):
    method send (line 567) | def send(self, body, title="", notify_type=NotifyType.INFO, **kwargs):
    method _fetch (line 621) | def _fetch(self, url, payload=None, method="POST", login=False):
    method parse_url (line 750) | def parse_url(url):
    method __del__ (line 807) | def __del__(self):

FILE: apprise/plugins/twitter.py
  class TwitterMessageMode (line 49) | class TwitterMessageMode:
  class NotifyTwitter (line 66) | class NotifyTwitter(NotifyBase):
    method __init__ (line 200) | def __init__(
    method send (line 294) | def send(
    method _send_tweet (line 400) | def _send_tweet(
    method _send_dm (line 518) | def _send_dm(
    method _whoami (line 604) | def _whoami(self, lazy=True):
    method _user_lookup (line 635) | def _user_lookup(self, screen_name, lazy=True):
    method _fetch (line 687) | def _fetch(self, url, payload=None, method="POST", json=True):
    method body_maxlen (line 836) | def body_maxlen(self):
    method url_identifier (line 843) | def url_identifier(self):
    method url (line 857) | def url(self, privacy=False, *args, **kwargs):
    method __len__ (line 894) | def __len__(self):
    method parse_url (line 900) | def parse_url(url):

FILE: apprise/plugins/vapid/__init__.py
  class VapidPushMode (line 45) | class VapidPushMode:
  class NotifyVapid (line 86) | class NotifyVapid(NotifyBase):
    method __init__ (line 212) | def __init__(
    method send (line 330) | def send(self, body, title="", notify_type=NotifyType.INFO, **kwargs):
    method url_identifier (line 468) | def url_identifier(self):
    method url (line 476) | def url(self, privacy=False, *args, **kwargs):
    method __len__ (line 519) | def __len__(self):
    method parse_url (line 525) | def parse_url(url):
    method jwt_token (line 588) | def jwt_token(self):
    method public_key (line 614) | def public_key(self):

FILE: apprise/plugins/vapid/subscription.py
  class WebPushSubscription (line 47) | class WebPushSubscription:
    method __init__ (line 58) | def __init__(self, content: Union[str, dict, None] = None) -> None:
    method load (line 72) | def load(self, content: Union[str, dict, None] = None) -> bool:
    method write (line 132) | def write(self, path: str, indent: int = 2) -> bool:
    method auth (line 152) | def auth(self) -> Optional[str]:
    method endpoint (line 156) | def endpoint(self) -> Optional[str]:
    method p256dh (line 160) | def p256dh(self) -> Optional[str]:
    method auth_secret (line 164) | def auth_secret(self) -> Optional[bytes]:
    method public_key (line 168) | def public_key(self) -> Optional["ec.EllipticCurvePublicKey"]:
    method dict (line 172) | def dict(self) -> dict:
    method json (line 191) | def json(self, indent: int = 2) -> str:
    method __bool__ (line 195) | def __bool__(self) -> bool:
    method __str__ (line 199) | def __str__(self) -> str:
  class WebPushSubscriptionManager (line 208) | class WebPushSubscriptionManager:
    method __init__ (line 232) | def __init__(self, asset: Optional["AppriseAsset"] = None) -> None:
    method __getitem__ (line 243) | def __getitem__(self, key: str) -> WebPushSubscription:
    method __setitem__ (line 247) | def __setitem__(
    method add (line 255) | def add(
    method __bool__ (line 276) | def __bool__(self) -> bool:
    method __len__ (line 280) | def __len__(self) -> int:
    method __iadd__ (line 289) | def __iadd__(
    method __contains__ (line 298) | def __contains__(self, key: str) -> bool:
    method clear (line 302) | def clear(self) -> None:
    method dict (line 307) | def dict(self) -> dict:
    method load (line 315) | def load(self, path: str, byte_limit=0) -> bool:
    method write (line 395) | def write(self, path: str, indent: int = 2) -> bool:
    method json (line 411) | def json(self, indent: int = 2) -> str:

FILE: apprise/plugins/viber.py
  class NotifyViber (line 45) | class NotifyViber(NotifyBase):
    method __init__ (line 114) | def __init__(
    method __len__ (line 144) | def __len__(self) -> int:
    method url (line 148) | def url(self, privacy: bool = False, *args: Any, **kwargs: Any) -> str:
    method url_identifier (line 175) | def url_identifier(self) -> str:
    method send (line 183) | def send(
    method parse_url (line 296) | def parse_url(url: str) -> dict[str, Any]:

FILE: apprise/plugins/voipms.py
  class NotifyVoipms (line 47) | class NotifyVoipms(NotifyBase):
    method __init__ (line 128) | def __init__(self, email, source=None, targets=None, **kwargs):
    method send (line 196) | def send(self, body, title="", notify_type=NotifyType.INFO, **kwargs):
    method url_identifier (line 319) | def url_identifier(self):
    method url (line 332) | def url(self, privacy=False, *args, **kwargs):
    method __len__ (line 354) | def __len__(self):
    method parse_url (line 360) | def parse_url(url):

FILE: apprise/plugins/vonage.py
  class NotifyVonage (line 44) | class NotifyVonage(NotifyBase):
    method __init__ (line 145) | def __init__(
    method send (line 214) | def send(self, body, title="", notify_type=NotifyType.INFO, **kwargs):
    method url_identifier (line 310) | def url_identifier(self):
    method url (line 318) | def url(self, privacy=False, *args, **kwargs):
    method __len__ (line 342) | def __len__(self):
    method parse_url (line 348) | def parse_url(url):

FILE: apprise/plugins/webexteams.py
  class NotifyWebexTeams (line 81) | class NotifyWebexTeams(NotifyBase):
    method __init__ (line 134) | def __init__(self, token, **kwargs):
    method send (line 147) | def send(self, body, title="", notify_type=NotifyType.INFO, **kwargs):
    method url_identifier (line 218) | def url_identifier(self):
    method url (line 226) | def url(self, privacy=False, *args, **kwargs):
    method parse_url (line 239) | def parse_url(url):
    method parse_native_url (line 259) | def parse_native_url(url):

FILE: apprise/plugins/wecombot.py
  class NotifyWeComBot (line 68) | class NotifyWeComBot(NotifyBase):
    method __init__ (line 119) | def __init__(self, key, **kwargs):
    method url_identifier (line 137) | def url_identifier(self):
    method url (line 145) | def url(self, privacy=False, *args, **kwargs):
    method send (line 157) | def send(self, body, title="", notify_type=NotifyType.INFO, **kwargs):
    method parse_url (line 225) | def parse_url(url):
    method parse_native_url (line 244) | def parse_native_url(url):

FILE: apprise/plugins/whatsapp.py
  class NotifyWhatsApp (line 54) | class NotifyWhatsApp(NotifyBase):
    method __init__ (line 174) | def __init__(
    method send (line 312) | def send(self, body, title="", notify_type=NotifyType.INFO, **kwargs):
    method url_identifier (line 486) | def url_identifier(self):
    method url (line 494) | def url(self, privacy=False, *args, **kwargs):
    method __len__ (line 530) | def __len__(self):
    method parse_url (line 536) | def parse_url(url):

FILE: apprise/plugins/windows.py
  class NotifyWindows (line 54) | class NotifyWindows(NotifyBase):
    method __init__ (line 114) | def __init__(self, include_image=True, duration=None, **kwargs):
    method _on_destroy (line 133) | def _on_destroy(self, hwnd, msg, wparam, lparam):
    method send (line 142) | def send(self, body, title="", notify_type=NotifyType.INFO, **kwargs):
    method url (line 250) | def url(self, privacy=False, *args, **kwargs):
    method parse_url (line 265) | def parse_url(url):

FILE: apprise/plugins/workflows.py
  class APIVersion (line 73) | class APIVersion:
  class NotifyWorkflows (line 81) | class NotifyWorkflows(NotifyBase):
    method __init__ (line 206) | def __init__(
    method gen_payload (line 293) | def gen_payload(
    method send (line 401) | def send(self, body, title="", notify_type=NotifyType.INFO, **kwargs):
    method url_identifier (line 497) | def url_identifier(self):
    method url (line 511) | def url(self, privacy=False, *args, **kwargs):
    method parse_url (line 552) | def parse_url(url):
    method parse_native_url (line 644) | def parse_native_url(url):

FILE: apprise/plugins/wxpusher.py
  class WxPusherContentType (line 80) | class WxPusherContentType:
  class SubscriptionType (line 88) | class SubscriptionType:
  class NotifyWxPusher (line 95) | class NotifyWxPusher(NotifyBase):
    method __init__ (line 167) | def __init__(self, token, targets=None, **kwargs):
    method send (line 209) | def send(self, body, title="", notify_type=NotifyType.INFO, **kwargs):
    method url_identifier (line 327) | def url_identifier(self):
    method url (line 335) | def url(self, privacy=False, *args, **kwargs):
    method parse_url (line 360) | def parse_url(url):

FILE: apprise/plugins/xmpp/adapter.py
  class XMPPConfig (line 67) | class XMPPConfig:
  function bridge_slixmpp_logging (line 86) | def bridge_slixmpp_logging() -> None:
  function _close_awaitable (line 118) | def _close_awaitable(obj: Any) -> None:
  function _get_client_subclass (line 138) | def _get_client_subclass(base_cls: type[Any]) -> type[Any]:
  function _build_client (line 289) | def _build_client(*args: Any, **kwargs: Any) -> Any:
  class SlixmppAdapter (line 297) | class SlixmppAdapter:
    method __init__ (line 317) | def __init__(
    method __del__ (line 356) | def __del__(self) -> None:
    method _ssl_context (line 362) | def _ssl_context(verify: bool) -> ssl.SSLContext:
    method _loop_tick (line 370) | def _loop_tick(loop: asyncio.AbstractEventLoop) -> None:
    method _finalize_loop (line 380) | def _finalize_loop(loop: asyncio.AbstractEventLoop) -> None:
    method close (line 419) | def close(self) -> None:
    method process (line 463) | def process(self) -> bool:
    method _ensure_keepalive_worker (line 627) | def _ensure_keepalive_worker(self) -> bool:
    method _keepalive_runner (line 660) | def _keepalive_runner(self) -> None:
    method _connect_if_required (line 741) | async def _connect_if_required(self) -> bool:
    method _send_keepalive_async (line 805) | async def _send_keepalive_async(
    method send_message (line 840) | def send_message(
    method package_dependency (line 898) | def package_dependency() -> str:
    method supported_version (line 904) | def supported_version(version: Optional[str] = None) -> bool:

FILE: apprise/plugins/xmpp/base.py
  class NotifyXMPP (line 59) | class NotifyXMPP(NotifyBase):
    method __init__ (line 153) | def __init__(
    method __del__ (line 239) | def __del__(self) -> None:
    method url_identifier (line 250) | def url_identifier(self) -> tuple[str, str, str, str, Optional[int]]:
    method url (line 257) | def url(self, privacy: bool = False, *args: Any, **kwargs: Any) -> str:
    method send (line 300) | def send(
    method title_maxlen (line 365) | def title_maxlen(self) -> Optional[int]:
    method normalize_jid (line 375) | def normalize_jid(value: str, default_host: str) -> str:
    method parse_url (line 406) | def parse_url(url: str) -> Optional[dict[str, Any]]:

FILE: apprise/plugins/xmpp/common.py
  class SecureXMPPMode (line 31) | class SecureXMPPMode:

FILE: apprise/plugins/zulip.py
  class NotifyZulip (line 89) | class NotifyZulip(NotifyBase):
    method __init__ (line 179) | def __init__(self, botname, organization, token, targets=None, **kwargs):
    method send (line 240) | def send(self, body, title="", notify_type=NotifyType.INFO, **kwargs):
    method url_identifier (line 341) | def url_identifier(self):
    method url (line 354) | def url(self, privacy=False, *args, **kwargs):
    method __len__ (line 381) | def __len__(self):
    method parse_url (line 386) | def parse_url(url):

FILE: apprise/url.py
  class PrivacyMode (line 51) | class PrivacyMode:
  class URLBase (line 76) | class URLBase:
    method __init__ (line 199) | def __init__(self, asset=None, **kwargs):
    method throttle (line 279) | def throttle(self, last_io=None, wait=None):
    method url (line 323) | def url(self, privacy=False, *args, **kwargs):
    method url_id (line 363) | def url_id(self, lazy=True, hash_engine=hashlib.sha256):
    method __contains__ (line 501) | def __contains__(self, tags):
    method __str__ (line 513) | def __str__(self):
    method escape_html (line 518) | def escape_html(html, convert_new_lines=False, whitespace=True):
    method unquote (line 546) | def unquote(content, encoding="utf-8", errors="replace"):
    method quote (line 573) | def quote(content, safe="/", encoding=None, errors=None):
    method pprint (line 600) | def pprint(
    method urlencode (line 645) | def urlencode(query, doseq=False, safe="", encoding=None, errors=None):
    method split_path (line 678) | def split_path(path, unquote=True):
    method parse_list (line 705) | def parse_list(content, allow_whitespace=True, unquote=True):
    method parse_phone_no (line 731) | def parse_phone_no(content, unquote=True, prefix=False):
    method app_id (line 759) | def app_id(self):
    method app_desc (line 763) | def app_desc(self):
    method app_url (line 767) | def app_url(self):
    method request_timeout (line 771) | def request_timeout(self):
    method request_auth (line 777) | def request_auth(self):
    method request_url (line 783) | def request_url(self):
    method url_parameters (line 799) | def url_parameters(self, *args, **kwargs):
    method post_process_parse_url_results (line 826) | def post_process_parse_url_results(results):
    method parse_url (line 897) | def parse_url(
    method http_response_code_lookup (line 938) | def http_response_code_lookup(code, response_mask=None):
    method __len__ (line 959) | def __len__(self):
    method schemas (line 967) | def schemas(self):

FILE: apprise/utils/base64.py
  function base64_urlencode (line 33) | def base64_urlencode(data: bytes) -> str:
  function base64_urldecode (line 43) | def base64_urldecode(data: str) -> bytes:
  function decode_b64_dict (line 56) | def decode_b64_dict(di: dict) -> dict:
  function encode_b64_dict (line 86) | def encode_b64_dict(di: dict, encoding="utf-8") -> tuple[dict, bool]:

FILE: apprise/utils/cwe312.py
  function cwe312_word (line 32) | def cwe312_word(word, force=False, advanced=True, threshold=5):
  function cwe312_url (line 117) | def cwe312_url(url):

FILE: apprise/utils/disk.py
  function path_decode (line 60) | def path_decode(path):
  function tidy_path (line 65) | def tidy_path(path):
  function dir_size (line 82) | def dir_size(path, max_depth=3, missing_okay=True, _depth=0, _errors=None):
  function bytes_to_str (line 142) | def bytes_to_str(value):

FILE: apprise/utils/format.py
  function html_adjust (line 49) | def html_adjust(
  function markdown_adjust (line 85) | def markdown_adjust(
  function smart_split (line 122) | def smart_split(

FILE: apprise/utils/json.py
  class AppriseJSONEncoder (line 36) | class AppriseJSONEncoder(json.JSONEncoder):
    method default (line 39) | def default(self, entry):

FILE: apprise/utils/logic.py
  function is_exclusive_match (line 33) | def is_exclusive_match(
  function dict_full_update (line 106) | def dict_full_update(dict1, dict2):

FILE: apprise/utils/module.py
  function import_module (line 34) | def import_module(path, name):

FILE: apprise/utils/parse.py
  function is_ipaddr (line 151) | def is_ipaddr(addr, ipv4=True, ipv6=True):
  function is_hostname (line 200) | def is_hostname(hostname, ipv4=True, ipv6=True, underscore=True):
  function is_uuid (line 244) | def is_uuid(uuid):
  function is_phone_no (line 265) | def is_phone_no(phone, min_len=10):
  function is_call_sign (line 354) | def is_call_sign(callsign):
  function is_email (line 385) | def is_email(address):
  function parse_qsd (line 439) | def parse_qsd(qs, simple=False, plus_to_space=False, sanitize=True):
  function parse_url (line 535) | def parse_url(
  function parse_bool (line 809) | def parse_bool(arg, default=False):
  function parse_phone_no (line 853) | def parse_phone_no(*args, store_unparseable=True, prefix=False, **kwargs):
  function parse_call_sign (line 887) | def parse_call_sign(*args, store_unparseable=True, **kwargs):
  function parse_emails (line 917) | def parse_emails(*args, store_unparseable=True, **kwargs):
  function url_assembly (line 945) | def url_assembly(encode=False, **kwargs):
  function urlencode (line 986) | def urlencode(query, doseq=False, safe="", encoding=None, errors=None):
  function parse_urls (line 1021) | def parse_urls(*args, store_unparseable=True, **kwargs):
  function parse_list (line 1049) | def parse_list(*args, cast=None, allow_whitespace=True, sort=True):
  function validate_regex (line 1109) | def validate_regex(value, regex=r"[^\s]+", flags=re.I, strip=True, fmt=N...

FILE: apprise/utils/pem.py
  class ApprisePEMException (line 71) | class ApprisePEMException(ApprisePluginException):
    method __init__ (line 74) | def __init__(self, message, error_code=612):
  class ApprisePEMController (line 78) | class ApprisePEMController:
    method __init__ (line 92) | def __init__(
    method load_private_key (line 137) | def load_private_key(
    method load_public_key (line 205) | def load_public_key(self, path: Optional[str] = None, *names: str) -> ...
    method keygen (line 269) | def keygen(self, name: "Optional[str]" = None, force: bool = False):
    method public_keyfile (line 390) | def public_keyfile(self, *names: str) -> Optional[str]:
    method private_keyfile (line 441) | def private_keyfile(self, *names: str) -> Optional[str]:
    method public_key (line 492) | def public_key(
    method private_key (line 528) | def private_key(
    method encrypt_webpush (line 554) | def encrypt_webpush(
    method encrypt (line 633) | def encrypt(
    method decrypt (line 709) | def decrypt(
    method sign (line 804) | def sign(self, content: bytes) -> Optional[bytes]:
    method pub_keyfile (line 824) | def pub_keyfile(self) -> Optional[Union[str, bool]]:
    method prv_keyfile (line 839) | def prv_keyfile(self) -> Optional[Union[str, bool]]:
    method x962_str (line 854) | def x962_str(self) -> str:
    method __bool__ (line 867) | def __bool__(self) -> bool:

FILE: apprise/utils/pgp.py
  class ApprisePGPException (line 48) | class ApprisePGPException(ApprisePluginException):
    method __init__ (line 52) | def __init__(self, message, error_code=602):
  class ApprisePGPController (line 56) | class ApprisePGPController:
    method __init__ (line 63) | def __init__(
    method keygen (line 101) | def keygen(self, email=None, name=None, force=False):
    method public_keyfile (line 216) | def public_keyfile(self, *emails):
    method public_key (line 270) | def public_key(self, *emails, autogen=None):
    method encrypt (line 318) | def encrypt(self, message, *emails):
    method prune (line 342) | def prune(self):
    method pub_keyfile (line 351) | def pub_keyfile(self):

FILE: apprise/utils/sanitize.py
  class SanitizeOptions (line 62) | class SanitizeOptions:
  function sanitize_payload (line 105) | def sanitize_payload(

FILE: apprise/utils/singleton.py
  class Singleton (line 29) | class Singleton(type):
    method __call__ (line 34) | def __call__(cls, *args, **kwargs):

FILE: apprise/utils/socket.py
  class AppriseSocketError (line 46) | class AppriseSocketError(AppriseException):
  class SocketTransport (line 50) | class SocketTransport:
    method __init__ (line 68) | def __init__(
    method _coerce_timeout (line 104) | def _coerce_timeout(
    method connected (line 135) | def connected(self) -> bool:
    method is_tls (line 139) | def is_tls(self) -> bool:
    method close (line 142) | def close(self) -> None:
    method _refresh_wrappers (line 174) | def _refresh_wrappers(self) -> None:
    method can_read (line 185) | def can_read(self, timeout: float = 0.0) -> Optional[bool]:
    method can_write (line 203) | def can_write(self, timeout: float = 0.0) -> Optional[bool]:
    method connect (line 218) | def connect(self) -> None:
    method _server_hostname_for_tls (line 276) | def _server_hostname_for_tls(self) -> str:
    method _build_ssl_context (line 298) | def _build_ssl_context(self) -> ssl.SSLContext:
    method start_tls (line 331) | def start_tls(self) -> None:
    method _attempt_reconnect (line 372) | def _attempt_reconnect(
    method read (line 414) | def read(
    method write (line 552) | def write(

FILE: apprise/utils/templates.py
  class TemplateType (line 31) | class TemplateType:
  function apply_template (line 43) | def apply_template(template, app_mode=TemplateType.RAW, **kwargs):

FILE: apprise/utils/time.py
  function zoneinfo (line 35) | def zoneinfo(name: str) -> Optional[ZoneInfo]:

FILE: setup.py
  function read_version (line 41) | def read_version() -> str:

FILE: tests/conftest.py
  function mimetypes_always_available (line 52) | def mimetypes_always_available():
  function no_throttling_everywhere (line 60) | def no_throttling_everywhere(session_mocker):
  function collect_all_garbage (line 75) | def collect_all_garbage(session_mocker):

FILE: tests/helpers/asyncio.py
  class OuterEventLoop (line 31) | class OuterEventLoop:
    method __init__ (line 35) | def __init__(self):
    method __enter__ (line 38) | def __enter__(self):
    method __exit__ (line 44) | def __exit__(self, type, value, traceback):

FILE: tests/helpers/environment.py
  function environ (line 39) | def environ(*remove, **update):

FILE: tests/helpers/module.py
  function reload_plugin (line 58) | def reload_plugin(name):

FILE: tests/helpers/rest.py
  class AppriseURLTester (line 53) | class AppriseURLTester:
    method __init__ (line 83) | def __init__(self, tests=None, *args, **kwargs):
    method add (line 102) | def add(self, url, meta):
    method run_all (line 109) | def run_all(self, tmpdir=None):
    method run (line 118) | def run(self, url, meta, tmpdir, mock_request, mock_post, mock_get):
    method __notify (line 335) | def __notify(

FILE: tests/test_api.py
  function test_apprise_object (line 71) | def test_apprise_object():
  function test_apprise_async (line 83) | def test_apprise_async():
  function apprise_test (line 98) | def apprise_test(do_notify):
  function test_apprise_pretty_print (line 611) | def test_apprise_pretty_print():
  function test_apprise_tagging (line 667) | def test_apprise_tagging(mock_request):
  function test_apprise_tagging_async (line 680) | def test_apprise_tagging_async(mock_request):
  function apprise_tagging_test (line 695) | def apprise_tagging_test(mock_request, do_notify):
  function test_apprise_schemas (line 835) | def test_apprise_schemas():
  function test_apprise_urlbase_object (line 899) | def test_apprise_urlbase_object():
  function test_apprise_unique_id (line 956) | def test_apprise_unique_id():
  function test_apprise_notify_formats (line 1078) | def test_apprise_notify_formats():
  function test_apprise_asset (line 1169) | def test_apprise_asset(tmpdir):
  function test_apprise_disabled_plugins (line 1340) | def test_apprise_disabled_plugins():
  function test_apprise_details (line 1461) | def test_apprise_details():
  function test_apprise_details_plugin_verification (line 1727) | def test_apprise_details_plugin_verification():
  function test_apprise_async_mode (line 2137) | def test_apprise_async_mode(mock_threadpool, mock_gather, mock_request):
  function test_notify_matrix_dynamic_importing (line 2224) | def test_notify_matrix_dynamic_importing(tmpdir):

FILE: tests/test_apprise_asset.py
  function test_timezone (line 44) | def test_timezone():

FILE: tests/test_apprise_attachments.py
  function test_apprise_attachment (line 52) | def test_apprise_attachment():
  function test_apprise_attachment_truncate (line 284) | def test_apprise_attachment_truncate(mock_request):
  function test_apprise_attachment_instantiate (line 356) | def test_apprise_attachment_instantiate():
  function test_attachment_matrix_dynamic_importing (line 391) | def test_attachment_matrix_dynamic_importing(tmpdir):

FILE: tests/test_apprise_cli.py
  function test_apprise_cli_nux_env (line 56) | def test_apprise_cli_nux_env(tmpdir):
  function test_apprise_cli_modules (line 907) | def test_apprise_cli_modules(tmpdir):
  function test_apprise_cli_persistent_storage (line 1007) | def test_apprise_cli_persistent_storage(tmpdir):
  function test_apprise_cli_details (line 1630) | def test_apprise_cli_details(tmpdir):
  function test_apprise_cli_print_help (line 1853) | def test_apprise_cli_print_help():
  function test_apprise_cli_plugin_loading (line 1876) | def test_apprise_cli_plugin_loading(mock_request, tmpdir):
  function test_apprise_cli_windows_env (line 2348) | def test_apprise_cli_windows_env(mock_system):

FILE: tests/test_apprise_config.py
  function test_apprise_config (line 59) | def test_apprise_config(tmpdir):
  function test_apprise_multi_config_entries (line 236) | def test_apprise_multi_config_entries(tmpdir):
  function test_apprise_add_config (line 294) | def test_apprise_add_config():
  function test_apprise_config_tagging (line 394) | def test_apprise_config_tagging(tmpdir):
  function test_apprise_config_instantiate (line 452) | def test_apprise_config_instantiate():
  function test_invalid_apprise_config (line 494) | def test_invalid_apprise_config(tmpdir):
  function test_apprise_config_with_apprise_obj (line 549) | def test_apprise_config_with_apprise_obj(tmpdir):
  function test_recursive_config_inclusion (line 722) | def test_recursive_config_inclusion(tmpdir):
  function test_apprise_config_file_loading (line 907) | def test_apprise_config_file_loading(tmpdir):
  function test_apprise_config_matrix_load (line 929) | def test_apprise_config_matrix_load():
  function test_configmatrix_dynamic_importing (line 987) | def test_configmatrix_dynamic_importing(tmpdir):
  function test_config_base_parse_inaccessible_text_file (line 1058) | def test_config_base_parse_inaccessible_text_file(mock_getsize, tmpdir):
  function test_config_base_parse_yaml_file01 (line 1084) | def test_config_base_parse_yaml_file01(tmpdir):
  function test_config_base_parse_yaml_file02 (line 1102) | def test_config_base_parse_yaml_file02(tmpdir):
  function test_config_base_parse_yaml_file03 (line 1148) | def test_config_base_parse_yaml_file03(tmpdir):
  function test_config_base_parse_yaml_file04 (line 1199) | def test_config_base_parse_yaml_file04(tmpdir):
  function test_apprise_config_template_parse (line 1252) | def test_apprise_config_template_parse(tmpdir):
  function test_config_base_yaml_trace_logging (line 1429) | def test_config_base_yaml_trace_logging(tmpdir):
  function test_config_base_clear_cache (line 1456) | def test_config_base_clear_cache(tmpdir):
  function test_config_base_parse_url_cache_variants (line 1482) | def test_config_base_parse_url_cache_variants():
  function test_config_base_parse_url_invalid_format_removed (line 1501) | def test_config_base_parse_url_invalid_format_removed():
  function test_config_base_expired_with_int_cache (line 1513) | def test_config_base_expired_with_int_cache(monkeypatch):

FILE: tests/test_apprise_emojis.py
  function test_emojis (line 40) | def test_emojis():

FILE: tests/test_apprise_helpers.py
  function test_environ_temporary_change (line 41) | def test_environ_temporary_change():

FILE: tests/test_apprise_jsonencoder.py
  function test_apprise_json_encoder_datetime_naive (line 50) | def test_apprise_json_encoder_datetime_naive():
  function test_apprise_json_encoder_datetime_aware (line 62) | def test_apprise_json_encoder_datetime_aware():
  function test_apprise_json_encoder_bytes (line 73) | def test_apprise_json_encoder_bytes():
  function test_apprise_json_encoder_set (line 92) | def test_apprise_json_encoder_set():
  function test_apprise_json_encoder_frozenset (line 105) | def test_apprise_json_encoder_frozenset():
  function test_apprise_json_encoder_tuple (line 118) | def test_apprise_json_encoder_tuple():
  function test_apprise_json_encoder_lazy_translation (line 131) | def test_apprise_json_encoder_lazy_translation():
  function test_apprise_json_encoder_unsupported_type (line 144) | def test_apprise_json_encoder_unsupported_type():
  function test_apprise_json_encoder_nested (line 154) | def test_apprise_json_encoder_nested():
  function test_apprise_json_method_no_path (line 177) | def test_apprise_json_method_no_path():
  function test_apprise_json_method_indent (line 200) | def test_apprise_json_method_indent():
  function test_apprise_json_method_with_path (line 212) | def test_apprise_json_method_with_path(tmpdir):
  function test_apprise_json_method_with_path_and_indent (line 229) | def test_apprise_json_method_with_path_and_indent(tmpdir):
  function test_apprise_json_method_write_failure (line 241) | def test_apprise_json_method_write_failure(tmpdir):
  function test_apprise_json_method_write_eoferror (line 252) | def test_apprise_json_method_write_eoferror(tmpdir):

FILE: tests/test_apprise_pickle.py
  function test_apprise_pickle_asset (line 41) | def test_apprise_pickle_asset(tmpdir):
  function test_apprise_pickle_locale (line 72) | def test_apprise_pickle_locale(tmpdir):
  function test_apprise_pickle_core (line 84) | def test_apprise_pickle_core(tmpdir):

FILE: tests/test_apprise_translations.py
  function test_apprise_trans (line 45) | def test_apprise_trans():
  function test_apprise_trans_gettext_init (line 54) | def test_apprise_trans_gettext_init():
  function test_apprise_trans_gettext_translations (line 75) | def test_apprise_trans_gettext_translations(
  function test_apprise_trans_gettext_lang_at (line 102) | def test_apprise_trans_gettext_lang_at(mock_getlocale):
  function test_apprise_trans_add (line 198) | def test_apprise_trans_add():
  function test_apprise_trans_windows_users_win (line 233) | def test_apprise_trans_windows_users_win(mock_getlocale):
  function test_apprise_trans_windows_users_nux (line 280) | def test_apprise_trans_windows_users_nux(mock_getlocale):
  function test_detect_language_using_env (line 332) | def test_detect_language_using_env(mock_getlocale):
  function test_apprise_trans_gettext_missing (line 375) | def test_apprise_trans_gettext_missing(tmpdir):
  function test_apprise_locale_add_existing_language (line 407) | def test_apprise_locale_add_existing_language(mock_translation):
  function test_apprise_trans_successful_translation (line 421) | def test_apprise_trans_successful_translation(

FILE: tests/test_apprise_utils.py
  function test_parse_qsd (line 50) | def test_parse_qsd():
  function test_parse_url_general (line 72) | def test_parse_url_general():
  function test_parse_url_simple (line 817) | def test_parse_url_simple():
  function test_url_assembly (line 1256) | def test_url_assembly():
  function test_parse_bool (line 1335) | def test_parse_bool():
  function test_is_uuid (line 1384) | def test_is_uuid():
  function test_is_hostname (line 1403) | def test_is_hostname():
  function test_is_ipaddr (line 1498) | def test_is_ipaddr():
  function test_is_email (line 1536) | def test_is_email():
  function test_is_call_sign_no (line 1669) | def test_is_call_sign_no():
  function test_is_phone_no (line 1736) | def test_is_phone_no():
  function test_parse_call_sign (line 1846) | def test_parse_call_sign():
  function test_parse_phone_no (line 1897) | def test_parse_phone_no():
  function test_parse_emails (line 1983) | def test_parse_emails():
  function test_parse_urls (line 2101) | def test_parse_urls():
  function test_dict_full_update (line 2233) | def test_dict_full_update():
  function test_parse_list (line 2275) | def test_parse_list():
  function test_import_module (line 2368) | def test_import_module(tmpdir):
  function test_module_detection (line 2384) | def test_module_detection(tmpdir):
  function test_exclusive_match (line 2720) | def test_exclusive_match():
  function test_apprise_validate_regex (line 2883) | def test_apprise_validate_regex():
  function test_apply_templating (line 2954) | def test_apply_templating():
  function test_cwe312_word (line 3016) | def test_cwe312_word():
  function test_cwe312_url (line 3043) | def test_cwe312_url():
  function test_base64_encode_decode (line 3086) | def test_base64_encode_decode():
  function test_dict_base64_codec (line 3107) | def test_dict_base64_codec(tmpdir):
  function test_dir_size (line 3130) | def test_dir_size(tmpdir):
  function test_bytes_to_str (line 3253) | def test_bytes_to_str():
  function test_time_zoneinfo (line 3274) | def test_time_zoneinfo():

FILE: tests/test_asyncio.py
  function test_apprise_asyncio_runtime_error (line 45) | def test_apprise_asyncio_runtime_error():

FILE: tests/test_attach_base.py
  function test_mimetype_initialization (line 39) | def test_mimetype_initialization():
  function test_attach_base (line 59) | def test_attach_base():

FILE: tests/test_attach_file.py
  function test_attach_file_parse_url (line 48) | def test_attach_file_parse_url():
  function test_file_expiry (line 61) | def test_file_expiry(tmpdir):
  function test_attach_mimetype (line 90) | def test_attach_mimetype():
  function test_attach_file (line 115) | def test_attach_file():
  function test_attach_file_base64 (line 277) | def test_attach_file_base64():

FILE: tests/test_attach_http.py
  function test_attach_http_parse_url (line 63) | def test_attach_http_parse_url():
  function test_attach_http_query_string_dictionary (line 76) | def test_attach_http_query_string_dictionary():
  function test_attach_http (line 118) | def test_attach_http(mock_get, mock_request):

FILE: tests/test_attach_memory.py
  function test_attach_memory_parse_url (line 43) | def test_attach_memory_parse_url():
  function test_attach_memory (line 99) | def test_attach_memory():

FILE: tests/test_compat_py39.py
  function test_compat_dataclass_no_exception (line 39) | def test_compat_dataclass_no_exception():
  function test_compat_dataclass_strips_slots_on_typeerror (line 54) | def test_compat_dataclass_strips_slots_on_typeerror():
  function test_compat_dataclass_reraises_when_no_slots (line 72) | def test_compat_dataclass_reraises_when_no_slots():

FILE: tests/test_config_base.py
  function requests_remote_config (line 50) | def requests_remote_config(mocker: MockerFixture) -> Mock:
  function test_config_base (line 83) | def test_config_base():
  function test_config_base_detect_config_format (line 145) | def test_config_base_detect_config_format():
  function test_config_base_config_parse (line 185) | def test_config_base_config_parse():
  function test_config_base_discord_bug_report_01 (line 276) | def test_config_base_discord_bug_report_01():
  function test_config_base_config_parse_text (line 305) | def test_config_base_config_parse_text():
  function test_config_base_config_tag_groups_text (line 441) | def test_config_base_config_tag_groups_text():
  function test_config_base_config_parse_text_with_url (line 587) | def test_config_base_config_parse_text_with_url():
  function test_config_base_config_parse_yaml (line 625) | def test_config_base_config_parse_yaml():
  function test_config_base_config_parse_yaml_includes (line 1290) | def test_config_base_config_parse_yaml_includes(
  function test_yaml_vs_text_tagging (line 1332) | def test_yaml_vs_text_tagging():
  function test_config_base_config_tag_groups_yaml_01 (line 1357) | def test_config_base_config_tag_groups_yaml_01():
  function test_config_base_config_tag_groups_yaml_02 (line 1473) | def test_config_base_config_tag_groups_yaml_02():
  function test_config_base_config_parse_yaml_globals (line 1595) | def test_config_base_config_parse_yaml_globals():
  function test_config_base_config_parse_yaml_list (line 1644) | def test_config_base_config_parse_yaml_list():
  function test_yaml_asset_timezone_and_asset_tokens (line 1677) | def test_yaml_asset_timezone_and_asset_tokens(tmpdir):
  function test_yaml_asset_timezone_invalid_and_precedence (line 1717) | def test_yaml_asset_timezone_invalid_and_precedence(tmpdir):
  function test_yaml_asset_tz_garbage_types_only (line 1756) | def test_yaml_asset_tz_garbage_types_only(tmpdir, garbage_yaml):
  function test_config_base_parse_yaml_file05_tags_alias_dict_form (line 1790) | def test_config_base_parse_yaml_file05_tags_alias_dict_form(tmpdir):
  function test_config_base_parse_yaml_file06_tags_alias_list_form (line 1832) | def test_config_base_parse_yaml_file06_tags_alias_list_form(tmpdir):
  function test_config_base_parse_yaml_file07_tag_priority_over_tags (line 1872) | def test_config_base_parse_yaml_file07_tag_priority_over_tags(tmpdir):

FILE: tests/test_config_file.py
  function test_config_file (line 39) | def test_config_file(tmpdir):
  function test_config_file_exceptions (line 103) | def test_config_file_exceptions(tmpdir):

FILE: tests/test_config_http.py
  function test_config_http (line 58) | def test_config_http(mock_post):

FILE: tests/test_config_memory.py
  function test_config_memory (line 36) | def test_config_memory():

FILE: tests/test_conversion.py
  function test_conversion_html_to_text (line 41) | def test_conversion_html_to_text():
  function test_conversion_text_to (line 231) | def test_conversion_text_to():
  function test_conversion_markdown_to_html (line 247) | def test_conversion_markdown_to_html():

FILE: tests/test_decorator_notify.py
  function test_notify_simple_decoration (line 51) | def test_notify_simple_decoration():
  function test_notify_complex_decoration (line 297) | def test_notify_complex_decoration():
  function test_notify_decorator_urls_with_space (line 451) | def test_notify_decorator_urls_with_space():
  function test_notify_multi_instance_decoration (line 527) | def test_notify_multi_instance_decoration(tmpdir):
  function test_custom_notify_plugin_decoration (line 670) | def test_custom_notify_plugin_decoration():

FILE: tests/test_escapes.py
  function test_apprise_interpret_escapes (line 37) | def test_apprise_interpret_escapes(mock_request):
  function test_apprise_escaping (line 132) | def test_apprise_escaping(mock_request):

FILE: tests/test_logger.py
  function test_apprise_logger (line 42) | def test_apprise_logger():
  function test_apprise_log_memory_captures (line 73) | def test_apprise_log_memory_captures():
  function test_apprise_log_file_captures (line 217) | def test_apprise_log_file_captures(tmpdir):
  function test_apprise_secure_logging (line 359) | def test_apprise_secure_logging(mock_request):

FILE: tests/test_notification_manager.py
  function test_notification_manager_general (line 47) | def test_notification_manager_general():
  function test_notification_manager_add_force_overrides_schema_without_unload (line 242) | def test_notification_manager_add_force_overrides_schema_without_unload():
  function test_notification_manager_module_loading (line 284) | def test_notification_manager_module_loading(tmpdir):
  function test_notification_manager_decorators (line 338) | def test_notification_manager_decorators(tmpdir):
  function test_notification_manager_add_force_returns_false_if_conflict_persists (line 446) | def test_notification_manager_add_force_returns_false_if_conflict_persists(

FILE: tests/test_notify_base.py
  function test_notify_base (line 42) | def test_notify_base():
  function test_notify_base_urls (line 286) | def test_notify_base_urls():

FILE: tests/test_persistent_store.py
  function test_persistent_storage_asset (line 58) | def test_persistent_storage_asset(tmpdir):
  function test_persistent_storage_bad_mode (line 73) | def test_persistent_storage_bad_mode(tmpdir):
  function test_disabled_persistent_storage (line 85) | def test_disabled_persistent_storage(tmpdir):
  function test_persistent_storage_init (line 135) | def test_persistent_storage_init(tmpdir):
  function test_persistent_storage_general (line 160) | def test_persistent_storage_general(tmpdir):
  function test_persistent_storage_auto_mode (line 214) | def test_persistent_storage_auto_mode(tmpdir):
  function test_persistent_storage_flush_mode (line 252) | def test_persistent_storage_flush_mode(tmpdir):
  function test_persistent_storage_corruption_handling (line 539) | def test_persistent_storage_corruption_handling(tmpdir):
  function test_persistent_custom_io (line 982) | def test_persistent_custom_io(tmpdir):
  function test_persistent_storage_cache_object (line 1156) | def test_persistent_storage_cache_object(tmpdir):
  function test_persistent_storage_disk_prune (line 1377) | def test_persistent_storage_disk_prune(tmpdir):
  function test_persistent_storage_disk_changes (line 1591) | def test_persistent_storage_disk_changes(tmpdir):

FILE: tests/test_plugin_africas_talking.py
  function test_plugin_atalk_urls (line 143) | def test_plugin_atalk_urls():
  function test_plugin_atalk_edge_cases (line 151) | def test_plugin_atalk_edge_cases(mock_post):

FILE: tests/test_plugin_apprise_api.py
  function test_plugin_apprise_api_urls (line 278) | def test_plugin_apprise_api_urls():
  function test_notify_apprise_api_payload_check (line 286) | def test_notify_apprise_api_payload_check(mock_post):
  function test_notify_apprise_api_attachments (line 367) | def test_notify_apprise_api_attachments(mock_post):

FILE: tests/test_plugin_aprs.py
  function test_plugin_aprs_urls (line 40) | def test_plugin_aprs_urls(mock_create_connection):
  function test_plugin_aprs_edge_cases (line 189) | def test_plugin_aprs_edge_cases(mock_create_connection):
  function test_plugin_aprs_config_files (line 359) | def test_plugin_aprs_config_files():

FILE: tests/test_plugin_bark.py
  function test_plugin_bark_urls (line 288) | def test_plugin_bark_urls():

FILE: tests/test_plugin_base_formatting.py
  function assert_body (line 50) | def assert_body(
  function test_notify_overflow_truncate_with_amalgamation (line 75) | def test_notify_overflow_truncate_with_amalgamation():
  function test_notify_overflow_truncate_no_amalgamation (line 311) | def test_notify_overflow_truncate_no_amalgamation():
  function test_notify_overflow_split_with_amalgamation (line 548) | def test_notify_overflow_split_with_amalgamation():
  function test_notify_overflow_split_with_amalgamation_force_title_always (line 1077) | def test_notify_overflow_split_with_amalgamation_force_title_always():
  function test_notify_overflow_split_with_amalgamation_force_title_once (line 1688) | def test_notify_overflow_split_with_amalgamation_force_title_once():
  function test_notify_overflow_split_no_amalgamation (line 2259) | def test_notify_overflow_split_no_amalgamation():
  function test_notify_overflow_split_no_amalgamation_force_title_always (line 2716) | def test_notify_overflow_split_no_amalgamation_force_title_always():
  function test_notify_overflow_split_no_amalgamation_force_title_once (line 3197) | def test_notify_overflow_split_no_amalgamation_force_title_once():
  function test_notify_markdown_general (line 3773) | def test_notify_markdown_general():
  function test_notify_emoji_general (line 3832) | def test_notify_emoji_general(mock_request):

FILE: tests/test_plugin_bluesky.py
  function good_response (line 217) | def good_response(data=None):
  function bad_response (line 255) | def bad_response(data=None):
  function bluesky_url (line 272) | def bluesky_url():
  function good_message_response (line 278) | def good_message_response():
  function bad_message_response (line 285) | def bad_message_response():
  function good_media_response (line 292) | def good_media_response():
  function test_plugin_bluesky_urls (line 308) | def test_plugin_bluesky_urls():
  function test_plugin_bluesky_general (line 315) | def test_plugin_bluesky_general(mocker):
  function test_plugin_bluesky_edge_cases (line 455) | def test_plugin_bluesky_edge_cases():
  function test_plugin_bluesky_attachments_basic (line 464) | def test_plugin_bluesky_attachments_basic(
  function test_plugin_bluesky_attachments_bad_message_response (line 525) | def test_plugin_bluesky_attachments_bad_message_response(
  function test_plugin_bluesky_attachments_upload_fails (line 580) | def test_plugin_bluesky_attachments_upload_fails(
  function test_plugin_bluesky_attachments_invalid_attachment (line 631) | def test_plugin_bluesky_attachments_invalid_attachment(
  function test_plugin_bluesky_attachments_multiple_batch (line 680) | def test_plugin_bluesky_attachments_multiple_batch(
  function test_plugin_bluesky_auth_failure (line 839) | def test_plugin_bluesky_auth_failure(
  function test_plugin_bluesky_did_web_and_plc_resolution (line 877) | def test_plugin_bluesky_did_web_and_plc_resolution(
  function test_plugin_bluesky_pds_resolution_failures (line 932) | def test_plugin_bluesky_pds_resolution_failures(mock_get):
  function test_plugin_bluesky_missing_pds_endpoint (line 954) | def test_plugin_bluesky_missing_pds_endpoint(mock_get):

FILE: tests/test_plugin_brevo.py
  function test_plugin_brevo_urls (line 176) | def test_plugin_brevo_urls():
  function test_plugin_brevo_edge_cases (line 185) | def test_plugin_brevo_edge_cases(mock_post, mock_get):
  function test_plugin_brevo_attachments (line 218) | def test_plugin_brevo_attachments(mock_post):

FILE: tests/test_plugin_bulksms.py
  function test_plugin_bulksms_urls (line 175) | def test_plugin_bulksms_urls():
  function test_plugin_bulksms_edge_cases (line 183) | def test_plugin_bulksms_edge_cases(mock_post):

FILE: tests/test_plugin_bulkvs.py
  function test_plugin_bulkvs_urls (line 154) | def test_plugin_bulkvs_urls():
  function test_plugin_bulkvs_edge_cases (line 162) | def test_plugin_bulkvs_edge_cases(mock_post):

FILE: tests/test_plugin_burstsms.py
  function test_plugin_burstsms_urls (line 176) | def test_plugin_burstsms_urls():
  function test_plugin_burstsms_edge_cases (line 184) | def test_plugin_burstsms_edge_cases(mock_post):

FILE: tests/test_plugin_chanify.py
  function test_plugin_chanify_urls (line 102) | def test_plugin_chanify_urls():

FILE: tests/test_plugin_clickatell.py
  function test_plugin_clickatell_urls (line 173) | def test_plugin_clickatell_urls():
  function test_plugin_clickatell_edge_cases (line 181) | def test_plugin_clickatell_edge_cases(mock_post):

FILE: tests/test_plugin_clicksend.py
  function test_plugin_clicksend_urls (line 112) | def test_plugin_clicksend_urls():

FILE: tests/test_plugin_custom_form.py
  function test_plugin_custom_form_urls (line 231) | def test_plugin_custom_form_urls():
  function test_plugin_custom_form_attachments (line 239) | def test_plugin_custom_form_attachments(mock_request):
  function test_plugin_custom_form_edge_cases (line 446) | def test_plugin_custom_form_edge_cases(mock_request):

FILE: tests/test_plugin_custom_json.py
  function test_plugin_custom_json_urls (line 227) | def test_plugin_custom_json_urls():
  function test_plugin_custom_json_edge_cases (line 235) | def test_plugin_custom_json_edge_cases(mock_request):
  function test_notify_json_plugin_attachments (line 351) | def test_notify_json_plugin_attachments(mock_request):
  function test_plugin_custom_form_for_synology (line 433) | def test_plugin_custom_form_for_synology(mock_request):

FILE: tests/test_plugin_custom_xml.py
  function test_plugin_custom_xml_urls (line 259) | def test_plugin_custom_xml_urls():
  function test_notify_xml_plugin_attachments (line 267) | def test_notify_xml_plugin_attachments(mock_request):
  function test_plugin_custom_xml_edge_cases (line 347) | def test_plugin_custom_xml_edge_cases(mock_request):

FILE: tests/test_plugin_d7networks.py
  function test_plugin_d7networks_urls (line 172) | def test_plugin_d7networks_urls():
  function test_plugin_d7networks_edge_cases (line 180) | def test_plugin_d7networks_edge_cases(mock_post):

FILE: tests/test_plugin_dapnet.py
  function test_plugin_dapnet_urls (line 180) | def test_plugin_dapnet_urls():
  function test_plugin_dapnet_edge_cases (line 188) | def test_plugin_dapnet_edge_cases(mock_post):
  function test_plugin_dapnet_config_files (line 219) | def test_plugin_dapnet_config_files(mock_post):

FILE: tests/test_plugin_dbus.py
  function mock_dbus_module (line 43) | def mock_dbus_module(mocker):
  function test_plugin_dbus_initialization_strategies (line 105) | def test_plugin_dbus_initialization_strategies(mock_dbus_module, mocker):
  function test_plugin_dbus_image_support_initialization (line 146) | def test_plugin_dbus_image_support_initialization(mock_dbus_module, mock...
  function test_plugin_dbus_send_success (line 162) | def test_plugin_dbus_send_success(mock_dbus_module, mocker):
  function test_plugin_dbus_send_no_title (line 194) | def test_plugin_dbus_send_no_title(mock_dbus_module):
  function test_plugin_dbus_send_connection_failure (line 211) | def test_plugin_dbus_send_connection_failure(mock_dbus_module):
  function test_plugin_dbus_send_notify_failure (line 227) | def test_plugin_dbus_send_notify_failure(mock_dbus_module):
  function test_plugin_dbus_image_loading_failure (line 242) | def test_plugin_dbus_image_loading_failure(mock_dbus_module, mocker):
  function test_plugin_dbus_url_parsing (line 266) | def test_plugin_dbus_url_parsing(mock_dbus_module):
  function test_plugin_dbus_schema_not_supported (line 299) | def test_plugin_dbus_schema_not_supported(mock_dbus_module, mocker):
  function test_plugin_dbus_send_sets_xy_meta_payload (line 314) | def test_plugin_dbus_send_sets_xy_meta_payload(mock_dbus_module):
  function test_plugin_dbus_send_image_condition_false_skips_pixbuf (line 333) | def test_plugin_dbus_send_image_condition_false_skips_pixbuf(

FILE: tests/test_plugin_dingtalk.py
  function test_plugin_dingtalk_urls (line 139) | def test_plugin_dingtalk_urls():

FILE: tests/test_plugin_discord.py
  function test_plugin_discord_urls (line 291) | def test_plugin_discord_urls():
  function test_plugin_discord_notifications (line 299) | def test_plugin_discord_notifications(mock_post):
  function test_plugin_discord_general (line 505) | def test_plugin_discord_general(mock_sleep, mock_post):
  function test_plugin_discord_overflow (line 862) | def test_plugin_discord_overflow(mock_post):
  function test_plugin_discord_markdown_extra (line 918) | def test_plugin_discord_markdown_extra(mock_post):
  function test_plugin_discord_attachments (line 962) | def test_plugin_discord_attachments(mock_post):
  function test_plugin_discord_markdown_fields_batches_exactly (line 1081) | def test_plugin_discord_markdown_fields_batches_exactly(mock_post):
  function test_plugin_discord_markdown_ping_is_additive (line 1107) | def test_plugin_discord_markdown_ping_is_additive(mock_post):
  function test_plugin_discord_html_ping_is_exclusive (line 1136) | def test_plugin_discord_html_ping_is_exclusive(mock_post):
  function test_plugin_discord_markdown_no_mentions_has_no_allow_mentions (line 1160) | def test_plugin_discord_markdown_no_mentions_has_no_allow_mentions(mock_...
  function test_plugin_discord_markdown_single_field_posts_once (line 1180) | def test_plugin_discord_markdown_single_field_posts_once(mock_post):

FILE: tests/test_plugin_dot.py
  class DummyAttachment (line 39) | class DummyAttachment:
    method __init__ (line 40) | def __init__(self, payload="ZmFjZQ=="):
    method base64 (line 43) | def base64(self):
  function test_plugin_dot_urls (line 160) | def test_plugin_dot_urls():
  function test_notify_dot_image_mode_requires_image (line 167) | def test_notify_dot_image_mode_requires_image():
  function test_notify_dot_image_mode_with_attachment (line 172) | def test_notify_dot_image_mode_with_attachment():
  function test_notify_dot_image_mode_with_existing_image_data (line 199) | def test_notify_dot_image_mode_with_existing_image_data():
  function test_notify_dot_text_mode_with_existing_icon (line 224) | def test_notify_dot_text_mode_with_existing_icon():
  function test_notify_dot_text_mode_uses_attachment_as_icon (line 252) | def test_notify_dot_text_mode_uses_attachment_as_icon():
  function test_notify_dot_text_mode_multiple_attachments_warning (line 278) | def test_notify_dot_text_mode_multiple_attachments_warning():
  function test_notify_dot_url_generation (line 310) | def test_notify_dot_url_generation():
  function test_notify_dot_parse_url_mode_and_image (line 342) | def test_notify_dot_parse_url_mode_and_image():
  function test_notify_dot_invalid_mode (line 355) | def test_notify_dot_invalid_mode():
  function test_notify_dot_image_data_in_text_mode (line 364) | def test_notify_dot_image_data_in_text_mode():
  function test_notify_dot_text_mode_with_title_and_body (line 375) | def test_notify_dot_text_mode_with_title_and_body():
  function test_notify_dot_no_device_id (line 395) | def test_notify_dot_no_device_id():
  function test_notify_dot_parse_url_with_all_params (line 402) | def test_notify_dot_parse_url_with_all_params():
  function test_notify_dot_url_identifier (line 420) | def test_notify_dot_url_identifier():
  function test_notify_dot_image_mode_with_failed_attachment (line 427) | def test_notify_dot_image_mode_with_failed_attachment():
  function test_notify_dot_url_generation_defaults (line 441) | def test_notify_dot_url_generation_defaults():
  function test_notify_dot_image_mode_with_multiple_attachments (line 463) | def test_notify_dot_image_mode_with_multiple_attachments():
  function test_notify_dot_text_mode_without_title (line 488) | def test_notify_dot_text_mode_without_title():
  function test_notify_dot_url_generation_with_link (line 506) | def test_notify_dot_url_generation_with_link():
  function test_notify_dot_title_handling (line 528) | def test_notify_dot_title_handling():
  function test_notify_dot_image_mode_no_border (line 556) | def test_notify_dot_image_mode_no_border():
  function test_notify_dot_image_mode_no_dither (line 579) | def test_notify_dot_image_mode_no_dither():
  function test_notify_dot_text_mode_no_optional_fields (line 604) | def test_notify_dot_text_mode_no_optional_fields():
  function test_notify_dot_url_generation_without_defaults (line 625) | def test_notify_dot_url_generation_without_defaults():
  function test_notify_dot_image_mode_attachment_exception (line 642) | def test_notify_dot_image_mode_attachment_exception():
  function test_notify_dot_image_mode_attachment_none (line 668) | def test_notify_dot_image_mode_attachment_none():
  function test_notify_dot_image_mode_attachment_falsy (line 680) | def test_notify_dot_image_mode_attachment_falsy():
  function test_notify_dot_text_mode_attachment_exception (line 699) | def test_notify_dot_text_mode_attachment_exception():
  function test_notify_dot_text_mode_attachment_none (line 737) | def test_notify_dot_text_mode_attachment_none():
  function test_notify_dot_text_mode_attachment_falsy (line 758) | def test_notify_dot_text_mode_attachment_falsy():
  function test_notify_dot_parse_url_no_host (line 786) | def test_notify_dot_parse_url_no_host():
  function test_notify_dot_url_with_border_not_none (line 796) | def test_notify_dot_url_with_border_not_none():
  function test_notify_dot_image_mode_with_only_title (line 810) | def test_notify_dot_image_mode_with_only_title():
  function test_notify_dot_image_mode_with_only_body (line 829) | def test_notify_dot_image_mode_with_only_body():
  function test_notify_dot_text_mode_without_body (line 848) | def test_notify_dot_text_mode_without_body():
  function test_notify_dot_parse_url_without_host (line 866) | def test_notify_dot_parse_url_without_host():
  function test_notify_dot_image_mode_without_title_and_body (line 874) | def test_notify_dot_image_mode_without_title_and_body():
  function test_notify_dot_parse_url_with_empty_refresh (line 899) | def test_notify_dot_parse_url_with_empty_refresh():
  function test_notify_dot_image_mode_first_attachment_fails (line 908) | def test_notify_dot_image_mode_first_attachment_fails():
  function test_notify_dot_image_mode_with_empty_attach_list (line 925) | def test_notify_dot_image_mode_with_empty_attach_list():
  function test_notify_dot_parse_url_without_host_field (line 939) | def test_notify_dot_parse_url_without_host_field():

FILE: tests/test_plugin_email.py
  function test_plugin_email (line 476) | def test_plugin_email(mock_smtp, mock_smtpssl):
  function test_plugin_email_webbase_lookup (line 666) | def test_plugin_email_webbase_lookup(mock_smtp, mock_smtpssl):
  function test_plugin_email_smtplib_init_fail (line 711) | def test_plugin_email_smtplib_init_fail(mock_smtplib):
  function test_plugin_email_smtplib_send_okay (line 736) | def test_plugin_email_smtplib_send_okay(mock_smtplib):
  function test_plugin_email_smtplib_send_multiple_recipients (line 825) | def test_plugin_email_smtplib_send_multiple_recipients(mock_smtplib):
  function test_plugin_email_timezone (line 880) | def test_plugin_email_timezone(mock_smtp):
  function test_plugin_email_smtplib_internationalization (line 952) | def test_plugin_email_smtplib_internationalization(mock_smtp):
  function test_plugin_email_url_escaping (line 1031) | def test_plugin_email_url_escaping():
  function test_plugin_email_url_variations (line 1061) | def test_plugin_email_url_variations():
  function test_plugin_email_dict_variations (line 1229) | def test_plugin_email_dict_variations():
  function test_plugin_email_url_parsing (line 1248) | def test_plugin_email_url_parsing(mock_smtp, mock_smtp_ssl):
  function test_plugin_email_plus_in_toemail (line 1943) | def test_plugin_email_plus_in_toemail(mock_smtp, mock_smtp_ssl):
  function test_plugin_email_formatting_990 (line 2085) | def test_plugin_email_formatting_990(mock_smtp, mock_smtp_ssl):
  function test_plugin_email_variables_1087 (line 2119) | def test_plugin_email_variables_1087():
  function test_plugin_email_to_handling_1356 (line 2182) | def test_plugin_email_to_handling_1356(mock_smtp, mock_smtp_ssl):
  function test_plugin_email_variables_1334 (line 2244) | def test_plugin_email_variables_1334(mock_smtp, mock_smtp_ssl):
  function test_plugin_host_detection_from_source_email (line 2332) | def test_plugin_host_detection_from_source_email(mock_smtp, mock_smtp_ssl):
  function test_plugin_email_by_ipaddr_1113 (line 2522) | def test_plugin_email_by_ipaddr_1113(mock_smtp, mock_smtp_ssl):
  function test_plugin_email_pgp (line 2565) | def test_plugin_email_pgp(mock_smtp, mock_smtpssl, tmpdir):
  function test_plugin_email_prepare (line 2920) | def test_plugin_email_prepare():
  function test_plugin_pgp (line 2946) | def test_plugin_pgp(tmpdir):
  function test_pgp_public_keyfile_skips_self_email (line 2989) | def test_pgp_public_keyfile_skips_self_email(tmpdir):
  function test_plugin_email_gmx_template_lookup (line 3009) | def test_plugin_email_gmx_template_lookup(mock_smtp):

FILE: tests/test_plugin_emby.py
  function test_plugin_template_urls (line 96) | def test_plugin_template_urls():
  function test_plugin_emby_general (line 108) | def test_plugin_emby_general(
  function test_plugin_emby_login (line 181) | def test_plugin_emby_login(mock_post, mock_get):
  function test_plugin_emby_sessions (line 292) | def test_plugin_emby_sessions(mock_post, mock_get, mock_logout, mock_log...
  function test_plugin_emby_logout (line 384) | def test_plugin_emby_logout(mock_post, mock_get, mock_login):

FILE: tests/test_plugin_enigma2.py
  function test_plugin_enigma2_urls (line 219) | def test_plugin_enigma2_urls():

FILE: tests/test_plugin_fcm.py
  function mock_post (line 283) | def mock_post(mocker):
  function mock_post_legacy (line 301) | def mock_post_legacy(mocker):
  function test_plugin_fcm_urls (line 317) | def test_plugin_fcm_urls():
  function test_plugin_fcm_legacy_default (line 327) | def test_plugin_fcm_legacy_default(mock_post_legacy):
  function test_plugin_fcm_legacy_priorities (line 370) | def test_plugin_fcm_legacy_priorities(mock_post_legacy):
  function test_plugin_fcm_legacy_no_colors (line 403) | def test_plugin_fcm_legacy_no_colors(mock_post_legacy):
  function test_plugin_fcm_legacy_colors (line 433) | def test_plugin_fcm_legacy_colors(mock_post_legacy):
  function test_plugin_fcm_oauth_default (line 464) | def test_plugin_fcm_oauth_default(mock_post):
  function test_plugin_fcm_oauth_invalid_project_id (line 496) | def test_plugin_fcm_oauth_invalid_project_id(mock_post):
  function test_plugin_fcm_oauth_keyfile_error (line 514) | def test_plugin_fcm_oauth_keyfile_error(mock_post):
  function test_plugin_fcm_oauth_data_parameters (line 533) | def test_plugin_fcm_oauth_data_parameters(mock_post):
  function test_plugin_fcm_oauth_priorities (line 607) | def test_plugin_fcm_oauth_priorities(mock_post):
  function test_plugin_fcm_oauth_no_colors (line 645) | def test_plugin_fcm_oauth_no_colors(mock_post):
  function test_plugin_fcm_oauth_colors (line 680) | def test_plugin_fcm_oauth_colors(mock_post):
  function test_plugin_fcm_keyfile_parse_default (line 716) | def test_plugin_fcm_keyfile_parse_default(mock_post):
  function test_plugin_fcm_keyfile_parse_no_expiry (line 744) | def test_plugin_fcm_keyfile_parse_no_expiry(mock_post):
  function test_plugin_fcm_keyfile_parse_user_agent (line 767) | def test_plugin_fcm_keyfile_parse_user_agent(mock_post):
  function test_plugin_fcm_keyfile_parse_keyfile_failures (line 783) | def test_plugin_fcm_keyfile_parse_keyfile_failures(mock_post: mock.Mock):
  function test_plugin_fcm_keyfile_parse_token_failures (line 836) | def test_plugin_fcm_keyfile_parse_token_failures(mock_post):
  function test_plugin_fcm_bad_keyfile_parse (line 883) | def test_plugin_fcm_bad_keyfile_parse():
  function test_plugin_fcm_keyfile_missing_entries_parse (line 894) | def test_plugin_fcm_keyfile_missing_entries_parse(tmpdir):
  function test_plugin_fcm_priority_manager (line 937) | def test_plugin_fcm_priority_manager():
  function test_plugin_fcm_color_manager (line 971) | def test_plugin_fcm_color_manager():
  function test_plugin_fcm_cryptography_import_error (line 1026) | def test_plugin_fcm_cryptography_import_error():
  function test_plugin_fcm_edge_cases (line 1045) | def test_plugin_fcm_edge_cases(mock_post):

FILE: tests/test_plugin_feishu.py
  function test_plugin_feishu_urls (line 102) | def test_plugin_feishu_urls():

FILE: tests/test_plugin_flock.py
  function test_plugin_flock_urls (line 235) | def test_plugin_flock_urls():
  function test_plugin_flock_edge_cases (line 244) | def test_plugin_flock_edge_cases(mock_post, mock_get):

FILE: tests/test_plugin_fluxer.py
  function _tokens (line 53) | def _tokens() -> tuple[str, str]:
  function test_plugin_fluxer_urls (line 318) | def test_plugin_fluxer_urls() -> None:
  function test_plugin_fluxer_notifications (line 326) | def test_plugin_fluxer_notifications(mock_post: mock.MagicMock) -> None:
  function test_plugin_fluxer_429 (line 442) | def test_plugin_fluxer_429(
  function test_plugin_fluxer_general (line 614) | def test_plugin_fluxer_general(
  function test_plugin_fluxer_overflow (line 882) | def test_plugin_fluxer_overflow(mock_post):
  function test_plugin_fluxer_markdown_extra (line 925) | def test_plugin_fluxer_markdown_extra(mock_post):
  function test_plugin_fluxer_markdown_attachments (line 967) | def test_plugin_fluxer_markdown_attachments(
  function test_plugin_fluxer_markdown_fields_batches_exactly (line 1085) | def test_plugin_fluxer_markdown_fields_batches_exactly(mock_post):
  function test_plugin_fluxer_markdown_ping_is_additive (line 1111) | def test_plugin_fluxer_markdown_ping_is_additive(
  function test_plugin_fluxer_html_ping_is_exclusive (line 1143) | def test_plugin_fluxer_html_ping_is_exclusive(
  function test_plugin_fluxer_markdown_no_mentions_has_no_allow_mentions (line 1168) | def test_plugin_fluxer_markdown_no_mentions_has_no_allow_mentions(
  function test_plugin_fluxer_markdown_single_field_posts_once (line 1191) | def test_plugin_fluxer_markdown_single_field_posts_once(
  function test_plugin_fluxer_threading (line 1215) | def test_plugin_fluxer_threading(mock_post: mock.MagicMock) -> None:
  function test_plugin_fluxer_429_attachment_closes_edge_cases (line 1248) | def test_plugin_fluxer_429_attachment_closes_edge_cases(

FILE: tests/test_plugin_fortysixelks.py
  function test_plugin_46elks_urls (line 99) | def test_plugin_46elks_urls():
  function test_plugin_46elks_edge_cases (line 107) | def test_plugin_46elks_edge_cases(mock_post):

FILE: tests/test_plugin_freemobile.py
  function test_plugin_freemobile_urls (line 96) | def test_plugin_freemobile_urls():

FILE: tests/test_plugin_glib.py
  function enabled_glib_environment (line 44) | def enabled_glib_environment(monkeypatch):
  function test_plugin_glib_gdkpixbuf_attribute_error (line 80) | def test_plugin_glib_gdkpixbuf_attribute_error(monkeypatch):
  function test_plugin_glib_basic_notify (line 115) | def test_plugin_glib_basic_notify(enabled_glib_environment):
  function test_plugin_glib_url_includes_coordinates (line 122) | def test_plugin_glib_url_includes_coordinates(enabled_glib_environment):
  function test_plugin_glib_icon_fails_gracefully (line 132) | def test_plugin_glib_icon_fails_gracefully(mocker, enabled_glib_environm...
  function test_plugin_glib_send_raises_glib_error (line 144) | def test_plugin_glib_send_raises_glib_error(mocker, enabled_glib_environ...
  function test_plugin_glib_send_raises_generic (line 153) | def test_plugin_glib_send_raises_generic(mocker, enabled_glib_environment):
  function test_plugin_glib_disabled (line 198) | def test_plugin_glib_disabled(mocker, enabled_glib_environment):
  function test_plugin_glib_invalid_coords (line 205) | def test_plugin_glib_invalid_coords():
  function test_plugin_glib_urgency_parsing (line 213) | def test_plugin_glib_urgency_parsing():
  function test_plugin_glib_parse_url_fields (line 221) | def test_plugin_glib_parse_url_fields():
  function test_plugin_glib_xy_axis_applied_to_variant (line 230) | def test_plugin_glib_xy_axis_applied_to_variant(enabled_glib_environment):
  function test_plugin_glib_no_image_support (line 247) | def test_plugin_glib_no_image_support(monkeypatch, enabled_glib_environm...
  function test_plugin_glib_url_redaction (line 255) | def test_plugin_glib_url_redaction(enabled_glib_environment):
  function test_plugin_glib_require_version_importerror (line 265) | def test_plugin_glib_require_version_importerror(monkeypatch):
  function test_plugin_glib_require_version_valueerror (line 275) | def test_plugin_glib_require_version_valueerror(monkeypatch):
  function test_plugin_glib_gdkpixbuf_require_version_valueerror (line 303) | def test_plugin_glib_gdkpixbuf_require_version_valueerror(monkeypatch):
  function test_plugin_glib_notify_generic_exception (line 328) | def test_plugin_glib_notify_generic_exception(

FILE: tests/test_plugin_gnome.py
  function setup_glib_environment (line 45) | def setup_glib_environment():
  function glib_environment (line 101) | def glib_environment():
  function obj (line 107) | def obj(glib_environment):
  function test_plugin_gnome_general_success (line 124) | def test_plugin_gnome_general_success(obj):
  function test_plugin_gnome_image_success (line 148) | def test_plugin_gnome_image_success(glib_environment):
  function test_plugin_gnome_priority (line 174) | def test_plugin_gnome_priority(glib_environment):
  function test_plugin_gnome_urgency (line 215) | def test_plugin_gnome_urgency(glib_environment):
  function test_plugin_gnome_parse_configuration (line 256) | def test_plugin_gnome_parse_configuration(obj):
  function test_plugin_gnome_missing_icon (line 329) | def test_plugin_gnome_missing_icon(mocker, obj):
  function test_plugin_gnome_disabled_plugin (line 353) | def test_plugin_gnome_disabled_plugin(obj):
  function test_plugin_gnome_set_urgency (line 364) | def test_plugin_gnome_set_urgency():
  function test_plugin_gnome_gi_croaks (line 370) | def test_plugin_gnome_gi_croaks():
  function test_plugin_gnome_notify_croaks (line 389) | def test_plugin_gnome_notify_croaks(mocker, obj):
  function test_plugin_gnome_show_exception (line 411) | def test_plugin_gnome_show_exception(mocker, obj):

FILE: tests/test_plugin_google_chat.py
  function test_plugin_google_chat_urls (line 154) | def test_plugin_google_chat_urls():
  function test_plugin_google_chat_general (line 162) | def test_plugin_google_chat_general(mock_post):
  function test_plugin_google_chat_edge_case (line 228) | def test_plugin_google_chat_edge_case():

FILE: tests/test_plugin_gotify.py
  function test_plugin_gotify_urls (line 143) | def test_plugin_gotify_urls():
  function test_plugin_gotify_edge_cases (line 150) | def test_plugin_gotify_edge_cases():
  function test_plugin_gotify_config_files (line 161) | def test_plugin_gotify_config_files(mock_post):

FILE: tests/test_plugin_growl.py
  function test_plugin_growl_gntp_import_error (line 60) | def test_plugin_growl_gntp_import_error():
  function test_plugin_growl_exception_handling (line 69) | def test_plugin_growl_exception_handling(mock_gntp):
  function test_plugin_growl_general (line 128) | def test_plugin_growl_general(mock_gntp):
  function test_plugin_growl_config_files (line 381) | def test_plugin_growl_config_files(mock_gntp):

FILE: tests/test_plugin_guilded.py
  function test_plugin_guilded_urls (line 195) | def test_plugin_guilded_urls():
  function test_plugin_guilded_general (line 203) | def test_plugin_guilded_general(mock_post):

FILE: tests/test_plugin_homeassistant.py
  function test_plugin_homeassistant_urls (line 168) | def test_plugin_homeassistant_urls():
  function test_plugin_homeassistant_general (line 176) | def test_plugin_homeassistant_general(mock_post):

FILE: tests/test_plugin_httpsms.py
  function test_plugin_httpsms_urls (line 140) | def test_plugin_httpsms_urls():
  function test_plugin_httpsms_edge_cases (line 148) | def test_plugin_httpsms_edge_cases(mock_post):

FILE: tests/test_plugin_ifttt.py
  function test_plugin_ifttt_urls (line 144) | def test_plugin_ifttt_urls():
  function test_plugin_ifttt_edge_cases (line 153) | def test_plugin_ifttt_edge_cases(mock_post, mock_get):

FILE: tests/test_plugin_irc.py
  class _DummyTransport (line 62) | class _DummyTransport:
    method __init__ (line 65) | def __init__(self) -> None:
    method connect (line 71) | def connect(self) -> None:
    method close (line 74) | def close(self) -> None:
    method write (line 77) | def write(self, payload: bytes, *, flush: bool, timeout: float) -> None:
    method read (line 80) | def read(
  function test_plugin_irc_init_targets (line 93) | def test_plugin_irc_init_targets() -> None:
  function test_plugin_irc_modes (line 109) | def test_plugin_irc_modes() -> None:
  function test_plugin_irc_defaults_port_noop (line 128) | def test_plugin_irc_defaults_port_noop() -> None:
  function test_plugin_irc_defaults_template_match (line 135) | def test_plugin_irc_defaults_template_match() -> None:
  function test_plugin_irc_defaults_template_none (line 143) | def test_plugin_irc_defaults_template_none() -> None:
  function test_plugin_irc_send_no_targets (line 149) | def test_plugin_irc_send_no_targets() -> None:
  function test_plugin_irc_send_ok (line 155) | def test_plugin_irc_send_ok() -> None:
  function test_plugin_irc_send_no_join (line 188) | def test_plugin_irc_send_no_join() -> None:
  function test_plugin_irc_send_error (line 207) | def test_plugin_irc_send_error() -> None:
  function test_plugin_irc_url_id (line 219) | def test_plugin_irc_url_id() -> None:
  function test_plugin_irc_url_format (line 229) | def test_plugin_irc_url_format() -> None:
  function test_plugin_irc_parse_url (line 285) | def test_plugin_irc_parse_url() -> None:
  function test_plugin_irc_protocol (line 317) | def test_plugin_irc_protocol() -> None:
  function test_plugin_irc_state_machine (line 332) | def test_plugin_irc_state_machine() -> None:
  function test_plugin_irc_client_nick_handling (line 364) | def test_plugin_irc_client_nick_handling() -> None:
  function test_plugin_irc_client_handshake (line 396) | def test_plugin_irc_client_handshake() -> None:
  function test_plugin_irc_client_register (line 439) | def test_plugin_irc_client_register() -> None:
  function test_plugin_irc_client_identify (line 475) | def test_plugin_irc_client_identify() -> None:
  function test_plugin_irc_client_join (line 514) | def test_plugin_irc_client_join() -> None:
  function test_plugin_irc_protocol_parse_blank_line (line 546) | def test_plugin_irc_protocol_parse_blank_line() -> None:
  function test_plugin_irc_protocol_parse_trailing_only (line 554) | def test_plugin_irc_protocol_parse_trailing_only() -> None:
  function test_plugin_irc_message (line 562) | def test_plugin_irc_message() -> None:
  function test_plugin_irc_protocol_ping_payload_from_params (line 569) | def test_plugin_irc_protocol_ping_payload_from_params() -> None:
  function test_plugin_irc_protocol_extract_welcome_nick_non_welcome (line 577) | def test_plugin_irc_protocol_extract_welcome_nick_non_welcome() -> None:
  function test_plugin_irc_protocol_normalise_channel_empty (line 584) | def test_plugin_irc_protocol_normalise_channel_empty() -> None:
  function test_plugin_irc_client_props_and_io (line 589) | def test_plugin_irc_client_props_and_io() -> None:
  function test_plugin_irc_client_write_timeout (line 601) | def test_plugin_irc_client_write_timeout() -> None:
  function test_plugin_irc_client_write_bytes_and_flush (line 610) | def test_plugin_irc_client_write_bytes_and_flush() -> None:
  function test_plugin_irc_client_read_buffer_and_timeout (line 628) | def test_plugin_irc_client_read_buffer_and_timeout() -> None:
  function test_plugin_irc_client_read_transport_paths (line 641) | def test_plugin_irc_client_read_transport_paths() -> None:
  function test_plugin_irc_client_tick (line 657) | def test_plugin_irc_client_tick() -> None:
  function test_plugin_irc_client_handshake_paths (line 668) | def test_plugin_irc_client_handshake_paths() -> None:
  function test_plugin_irc_client_register_auth_modes (line 723) | def test_plugin_irc_client_register_auth_modes() -> None:
  function test_plugin_irc_client_join_privmsg_identify_quit (line 774) | def test_plugin_irc_client_join_privmsg_identify_quit() -> None:
  function test_plugin_irc_client_nick_generation_default_length (line 851) | def test_plugin_irc_client_nick_generation_default_length() -> None:
  function test_plugin_irc_client_write_trace (line 857) | def test_plugin_irc_client_write_trace() -> None:
  function test_plugin_irc_client_handshake_send_without_line (line 878) | def test_plugin_irc_client_handshake_send_without_line() -> None:
  function test_plugin_irc_client_register_queue_ignores_empty_send (line 899) | def test_plugin_irc_client_register_queue_ignores_empty_send() -> None:
  function test_plugin_irc_client_join_queue_ignores_empty_send (line 924) | def test_plugin_irc_client_join_queue_ignores_empty_send() -> None:
  function test_plugin_irc_client_quit_queue_ignores_empty_send (line 948) | def test_plugin_irc_client_quit_queue_ignores_empty_send() -> None:
  function test_plugin_irc_send_znc_calls_check_connection (line 969) | def test_plugin_irc_send_znc_calls_check_connection() -> None:
  function test_plugin_irc_send_znc_check_connection_failure (line 995) | def test_plugin_irc_send_znc_check_connection_failure() -> None:
  function test_plugin_irc_send_znc_pass_rewrite (line 1021) | def test_plugin_irc_send_znc_pass_rewrite() -> None:
  function test_plugin_irc_client_check_connection_success_on_any_pong (line 1046) | def test_plugin_irc_client_check_connection_success_on_any_pong() -> None:
  function test_plugin_irc_client_handles_empty_reads (line 1067) | def test_plugin_irc_client_handles_empty_reads() -> None:
  function test_plugin_irc_client_join_queues_join_line_and_completes (line 1087) | def test_plugin_irc_client_join_queues_join_line_and_completes() -> None:
  function test_plugin_irc_client_join_timeout_logs_debug (line 1112) | def test_plugin_irc_client_join_timeout_logs_debug() -> None:
  function test_plugin_irc_state_machine_joining_numeric_443_adds_channel (line 1128) | def test_plugin_irc_state_machine_joining_numeric_443_adds_channel() -> ...
  function test_plugin_irc_client_connection_check (line 1145) | def test_plugin_irc_client_connection_check() -> None:
  function test_plugin_irc_client_join_non_send_actions (line 1168) | def test_plugin_irc_client_join_non_send_actions() -> None:

FILE: tests/test_plugin_irc_state.py
  function test_plugin_irc_state_err_trailing (line 42) | def test_plugin_irc_state_err_trailing() -> None:
  function test_plugin_irc_state_err_params (line 48) | def test_plugin_irc_state_err_params() -> None:
  function test_plugin_irc_state_err_default (line 55) | def test_plugin_irc_state_err_default() -> None:
  function test_plugin_irc_state_ignore_when_error_or_quitting (line 61) | def test_plugin_irc_state_ignore_when_error_or_quitting() -> None:
  function test_plugin_irc_state_register_error_sets_last_error (line 73) | def test_plugin_irc_state_register_error_sets_last_error() -> None:
  function test_plugin_irc_state_register_collision_433_sends_new_nick (line 86) | def test_plugin_irc_state_register_collision_433_sends_new_nick() -> None:
  function test_plugin_irc_state_register_collision_432_sends_new_nick (line 97) | def test_plugin_irc_state_register_collision_432_sends_new_nick() -> None:
  function test_plugin_irc_state_register_welcome_sets_accepted (line 108) | def test_plugin_irc_state_register_welcome_sets_accepted() -> None:
  function test_plugin_irc_state_register_welcome (line 120) | def test_plugin_irc_state_register_welcome() -> None:
  function test_plugin_irc_state_register_motd_done_before_registered (line 133) | def test_plugin_irc_state_register_motd_done_before_registered() -> None:
  function test_plugin_irc_state_register_motd_done_376_before_registered (line 144) | def test_plugin_irc_state_register_motd_done_376_before_registered() -> ...
  function test_plugin_irc_state_register_motd_done_after_registered (line 156) | def test_plugin_irc_state_register_motd_done_after_registered() -> None:
  function test_plugin_irc_state_join_error_sets_last_error (line 171) | def test_plugin_irc_state_join_error_sets_last_error() -> None:
  function test_plugin_irc_state_join_numeric_366_adds_channel (line 184) | def test_plugin_irc_state_join_numeric_366_adds_channel() -> None:
  function test_plugin_irc_state_join_command_trailing (line 195) | def test_plugin_irc_state_join_command_trailing() -> None:
  function test_plugin_irc_state_join_command_params (line 206) | def test_plugin_irc_state_join_command_params() -> None:
  function test_plugin_irc_state_request_join_key_and_no_key (line 217) | def test_plugin_irc_state_request_join_key_and_no_key() -> None:
  function test_plugin_irc_state_request_quit (line 230) | def test_plugin_irc_state_request_quit() -> None:
  function test_plugin_irc_state_register_unhandled_numeric (line 240) | def test_plugin_irc_state_register_unhandled_numeric() -> None:
  function test_plugin_irc_state_join_command_empty_channel (line 254) | def test_plugin_irc_state_join_command_empty_channel() -> None:
  function test_plugin_irc_state_ready_falls_through (line 267) | def test_plugin_irc_state_ready_falls_through() -> None:
  function test_plugin_irc_state_join_non_join_command (line 279) | def test_plugin_irc_state_join_non_join_command() -> None:

FILE: tests/test_plugin_jellyfin.py
  function test_plugin_jellyfin_urls (line 90) | def test_plugin_jellyfin_urls():
  function test_plugin_jellyfin_instantiation (line 97) | def test_plugin_jellyfin_instantiation():

FILE: tests/test_plugin_join.py
  function test_plugin_join_urls (line 170) | def test_plugin_join_urls():
  function test_plugin_join_edge_cases (line 179) | def test_plugin_join_edge_cases(mock_post, mock_get):
  function test_plugin_join_config_files (line 219) | def test_plugin_join_config_files(mock_post):

FILE: tests/test_plugin_kavenegar.py
  function test_plugin_kavenegar_urls (line 134) | def test_plugin_kavenegar_urls():

FILE: tests/test_plugin_kumulos.py
  function test_plugin_kumulos_urls (line 111) | def test_plugin_kumulos_urls():
  function test_plugin_kumulos_edge_cases (line 118) | def test_plugin_kumulos_edge_cases():

FILE: tests/test_plugin_lametric.py
  function test_plugin_lametric_urls (line 317) | def test_plugin_lametric_urls():
  function test_plugin_lametric_edge_cases (line 324) | def test_plugin_lametric_edge_cases():

FILE: tests/test_plugin_lark.py
  function test_plugin_lark_urls (line 96) | def test_plugin_lark_urls():

FILE: tests/test_plugin_line.py
  function test_plugin_line_urls (line 115) | def test_plugin_line_urls():

FILE: tests/test_plugin_macosx.py
  function pretend_macos (line 51) | def pretend_macos(mocker):
  function terminal_notifier (line 61) | def terminal_notifier(mocker, tmp_path):
  function macos_notify_environment (line 80) | def macos_notify_environment(pretend_macos, terminal_notifier):
  function test_plugin_macosx_general_success (line 88) | def test_plugin_macosx_general_success(macos_notify_environment):
  function test_plugin_macosx_terminal_notifier_not_executable (line 171) | def test_plugin_macosx_terminal_notifier_not_executable(
  function test_plugin_macosx_terminal_notifier_invalid (line 190) | def test_plugin_macosx_terminal_notifier_invalid(macos_notify_environment):
  function test_plugin_macosx_terminal_notifier_croaks (line 208) | def test_plugin_macosx_terminal_notifier_croaks(
  function test_plugin_macosx_pretend_linux (line 227) | def test_plugin_macosx_pretend_linux(mocker, pretend_macos):
  function test_plugin_macosx_pretend_old_macos (line 241) | def test_plugin_macosx_pretend_old_macos(mocker, macos_version):

FILE: tests/test_plugin_mailgun.py
  function test_plugin_mailgun_urls (line 267) | def test_plugin_mailgun_urls():
  function test_plugin_mailgun_attachments (line 275) | def test_plugin_mailgun_attachments(mock_post):
  function test_plugin_mailgun_header_check (line 446) | def test_plugin_mailgun_header_check(mock_post):

FILE: tests/test_plugin_mastodon.py
  function test_plugin_mastodon_urls (line 201) | def test_plugin_mastodon_urls():
  function test_plugin_mastodon_general (line 210) | def test_plugin_mastodon_general(mock_post, mock_get):
  function test_plugin_mastodon_attachments (line 369) | def test_plugin_mastodon_attachments(mock_get, mock_post):

FILE: tests/test_plugin_matrix.py
  function test_plugin_matrix_urls (line 322) | def test_plugin_matrix_urls():
  function test_plugin_matrix_general (line 332) | def test_plugin_matrix_general(mock_post, mock_get, mock_put):
  function test_plugin_matrix_fetch (line 505) | def test_plugin_matrix_fetch(mock_post, mock_get, mock_put):
  function test_plugin_matrix_auth (line 612) | def test_plugin_matrix_auth(mock_post, mock_get, mock_put):
  function test_plugin_matrix_rooms (line 707) | def test_plugin_matrix_rooms(mock_post, mock_get, mock_put):
  function test_plugin_matrix_url_parsing (line 966) | def test_plugin_matrix_url_parsing():
  function test_plugin_matrix_image_errors (line 1008) | def test_plugin_matrix_image_errors(mock_post, mock_get, mock_put):
  function test_plugin_matrix_attachments_api_v3 (line 1095) | def test_plugin_matrix_attachments_api_v3(mock_post, mock_put):
  function test_plugin_matrix_discovery_service (line 1213) | def test_plugin_matrix_discovery_service(mock_post, mock_get):
  function test_plugin_matrix_attachments_api_v2 (line 1415) | def test_plugin_matrix_attachments_api_v2(mock_post, mock_get):
  function test_plugin_matrix_transaction_ids_api_v3_no_cache (line 1633) | def test_plugin_matrix_transaction_ids_api_v3_no_cache(
  function test_plugin_matrix_transaction_ids_api_v3_w_cache (line 1727) | def test_plugin_matrix_transaction_ids_api_v3_w_cache(
  function test_plugin_matrix_v3_url_with_port_assembly (line 1840) | def test_plugin_matrix_v3_url_with_port_assembly(
  function test_plugin_matrix_no_room_create_on_non_not_found_join (line 1923) | def test_plugin_matrix_no_room_create_on_non_not_found_join(
  function test_plugin_matrix_room_create_on_not_found_join (line 1985) | def test_plugin_matrix_room_create_on_not_found_join(

FILE: tests/test_plugin_mattermost.py
  function request_get_mock (line 241) | def request_get_mock(mocker):
  function request_post_mock (line 251) | def request_post_mock(mocker):
  function test_plugin_mattermost_urls (line 260) | def test_plugin_mattermost_urls():
  function test_plugin_mattermost_edge_cases (line 267) | def test_plugin_mattermost_edge_cases():
  function test_plugin_mattermost_len_webhook_and_bot (line 277) | def test_plugin_mattermost_len_webhook_and_bot(
  function test_plugin_mattermost_channels (line 353) | def test_plugin_mattermost_channels(request_post_mock):
  function test_mattermost_post_default_port (line 393) | def test_mattermost_post_default_port(request_post_mock):
  function test_mattermost_icon_override (line 416) | def test_mattermost_icon_override(request_post_mock):
  function test_plugin_mattermost_webhook_payload_variants (line 440) | def test_plugin_mattermost_webhook_payload_variants(request_post_mock, m...
  function test_plugin_mattermost_webhook_http_error_and_exception (line 479) | def test_plugin_mattermost_webhook_http_error_and_exception(
  function test_plugin_mattermost_bot_mode_success_and_payload (line 496) | def test_plugin_mattermost_bot_mode_success_and_payload(request_post_mock):
  function test_plugin_mattermost_bot_mode_requires_channel_id (line 526) | def test_plugin_mattermost_bot_mode_requires_channel_id(request_post_mock):
  function test_plugin_mattermost_bot_mode_http_error_and_exception (line 538) | def test_plugin_mattermost_bot_mode_http_error_and_exception(
  function test_plugin_mattermost_bot_channel_lookup_success (line 556) | def test_plugin_mattermost_bot_channel_lookup_success(
  function test_plugin_mattermost_bot_channel_lookup_partial_success (line 589) | def test_plugin_mattermost_bot_channel_lookup_partial_success(

FILE: tests/test_plugin_messagebird.py
  function test_plugin_messagebird_urls (line 129) | def test_plugin_messagebird_urls():
  function test_plugin_messagebird_edge_cases (line 137) | def test_plugin_messagebird_edge_cases(mock_post):

FILE: tests/test_plugin_misskey.py
  function test_plugin_misskey_urls (line 133) | def test_plugin_misskey_urls():

FILE: tests/test_plugin_mqtt.py
  function mqtt_client_mock (line 44) | def mqtt_client_mock(mocker):
  function test_plugin_mqtt_paho_import_error (line 71) | def test_plugin_mqtt_paho_import_error():
  function test_plugin_mqtt_default_success (line 80) | def test_plugin_mqtt_default_success(mqtt_client_mock):
  function test_plugin_mqtt_multiple_topics_success (line 122) | def test_plugin_mqtt_multiple_topics_success(mqtt_client_mock):
  function test_plugin_mqtt_to_success (line 155) | def test_plugin_mqtt_to_success(mqtt_client_mock):
  function test_plugin_mqtt_valid_settings_success (line 173) | def test_plugin_mqtt_valid_settings_success(mqtt_client_mock):
  function test_plugin_mqtt_invalid_settings_failure (line 188) | def test_plugin_mqtt_invalid_settings_failure(mqtt_client_mock):
  function test_plugin_mqtt_bad_url_failure (line 209) | def test_plugin_mqtt_bad_url_failure(mqtt_client_mock):
  function test_plugin_mqtt_no_topic_failure (line 215) | def test_plugin_mqtt_no_topic_failure(mqtt_client_mock):
  function test_plugin_mqtt_tls_connect_success (line 224) | def test_plugin_mqtt_tls_connect_success(mqtt_client_mock):
  function test_plugin_mqtt_tls_no_certificates_failure (line 255) | def test_plugin_mqtt_tls_no_certificates_failure(mqtt_client_mock, mocker):
  function test_plugin_mqtt_tls_no_verify_success (line 279) | def test_plugin_mqtt_tls_no_verify_success(mqtt_client_mock):
  function test_plugin_mqtt_session_client_id_success (line 296) | def test_plugin_mqtt_session_client_id_success(mqtt_client_mock):
  function test_plugin_mqtt_retain (line 313) | def test_plugin_mqtt_retain(mqtt_client_mock):
  function test_plugin_mqtt_connect_failure (line 328) | def test_plugin_mqtt_connect_failure(mqtt_client_mock):
  function test_plugin_mqtt_reconnect_failure (line 342) | def test_plugin_mqtt_reconnect_failure(mqtt_client_mock):
  function test_plugin_mqtt_publish_failure (line 357) | def test_plugin_mqtt_publish_failure(mqtt_client_mock):
  function test_plugin_mqtt_exception_failure (line 372) | def test_plugin_mqtt_exception_failure(mqtt_client_mock):
  function test_plugin_mqtt_not_published_failure (line 388) | def test_plugin_mqtt_not_published_failure(mqtt_client_mock, mocker):
  function test_plugin_mqtt_not_published_recovery_success (line 408) | def test_plugin_mqtt_not_published_recovery_success(mqtt_client_mock):

FILE: tests/test_plugin_msg91.py
  function test_plugin_msg91_urls (line 139) | def test_plugin_msg91_urls():
  function test_plugin_msg91_edge_cases (line 147) | def test_plugin_msg91_edge_cases(mock_post):
  function test_plugin_msg91_keywords (line 172) | def test_plugin_msg91_keywords(mock_post):

FILE: tests/test_plugin_msteams.py
  function test_plugin_msteams_urls (line 231) | def test_plugin_msteams_urls():
  function msteams_url (line 239) | def msteams_url():
  function request_mock (line 244) | def request_mock(mocker):
  function simple_template (line 253) | def simple_template(tmpdir):
  function test_plugin_msteams_templating_basic_success (line 273) | def test_plugin_msteams_templating_basic_success(
  function test_plugin_msteams_templating_invalid_json (line 327) | def test_plugin_msteams_templating_invalid_json(
  function test_plugin_msteams_templating_json_missing_type (line 354) | def test_plugin_msteams_templating_json_missing_type(
  function test_plugin_msteams_templating_json_missing_context (line 396) | def test_plugin_msteams_templating_json_missing_context(
  function test_plugin_msteams_templating_load_json_failure (line 437) | def test_plugin_msteams_templating_load_json_failure(
  function test_plugin_msteams_templating_target_success (line 459) | def test_plugin_msteams_templating_target_success(
  function test_msteams_yaml_config_invalid_template_filename (line 534) | def test_msteams_yaml_config_invalid_template_filename(
  function test_msteams_yaml_config_token_identifiers (line 566) | def test_msteams_yaml_config_token_identifiers(
  function test_msteams_yaml_config_no_bullet_under_url_1 (line 610) | def test_msteams_yaml_config_no_bullet_under_url_1(
  function test_msteams_yaml_config_dictionary_file (line 655) | def test_msteams_yaml_config_dictionary_file(
  function test_msteams_yaml_config_no_bullet_under_url_2 (line 701) | def test_msteams_yaml_config_no_bullet_under_url_2(
  function test_msteams_yaml_config_combined (line 747) | def test_msteams_yaml_config_combined(
  function test_msteams_yaml_config_token_mismatch (line 793) | def test_msteams_yaml_config_token_mismatch(
  function test_plugin_msteams_edge_cases (line 821) | def test_plugin_msteams_edge_cases():

FILE: tests/test_plugin_nextcloud.py
  function test_plugin_nextcloud_urls (line 250) | def test_plugin_nextcloud_urls():
  function test_plugin_nextcloud_edge_cases (line 258) | def test_plugin_nextcloud_edge_cases(mock_post):
  function test_plugin_nextcloud_url_prefix (line 285) | def test_plugin_nextcloud_url_prefix(mock_post):
  function test_plugin_nextcloud_groups_and_all (line 315) | def test_plugin_nextcloud_groups_and_all(mock_get, mock_post):
  function test_plugin_nextcloud_persistent_storage (line 387) | def test_plugin_nextcloud_persistent_storage(mock_get, mock_post, tmpdir):
  function test_plugin_nextcloud_groups_errors_and_dedup (line 468) | def test_plugin_nextcloud_groups_errors_and_dedup(mock_get, mock_post):
  function test_plugin_nextcloud_req_exception_and_empty_targets (line 509) | def test_plugin_nextcloud_req_exception_and_empty_targets(mock_get, mock...
  function test_plugin_nextcloud_json_empty_returns_empty (line 540) | def test_plugin_nextcloud_json_empty_returns_empty(mock_get, mock_post):
  function test_plugin_nextcloud_caching_group_and_all (line 580) | def test_plugin_nextcloud_caching_group_and_all(mock_get, mock_post):

FILE: tests/test_plugin_nextcloudtalk.py
  function test_plugin_nextcloudtalk_urls (line 170) | def test_plugin_nextcloudtalk_urls():
  function test_plugin_nextcloudtalk_edge_cases (line 178) | def test_plugin_nextcloudtalk_edge_cases(mock_post):
  function test_plugin_nextcloud_talk_url_prefix (line 203) | def test_plugin_nextcloud_talk_url_prefix(mock_post):

FILE: tests/test_plugin_notica.py
  function test_plugin_notica_urls (line 193) | def test_plugin_notica_urls():

FILE: tests/test_plugin_notifiarr.py
  function test_plugin_notifiarr_urls (line 233) | def test_plugin_notifiarr_urls():
  function test_plugin_notifiarr_notifications (line 241) | def test_plugin_notifiarr_notifications(mock_post):

FILE: tests/test_plugin_notificationapi.py
  function test_plugin_napi_urls (line 332) | def test_plugin_napi_urls():
  function test_plugin_napi_template_sms_payloads (line 343) | def test_plugin_napi_template_sms_payloads(mock_post):
  function test_plugin_napi_template_email_payloads (line 411) | def test_plugin_napi_template_email_payloads(mock_post):
  function test_plugin_napi_message_payloads (line 489) | def test_plugin_napi_message_payloads(mock_post):
  function test_plugin_napi_edge_cases (line 661) | def test_plugin_napi_edge_cases():

FILE: tests/test_plugin_notifico.py
  function test_plugin_notifico_urls (line 169) | def test_plugin_notifico_urls():

FILE: tests/test_plugin_ntfy.py
  function test_plugin_ntfy_chat_urls (line 416) | def test_plugin_ntfy_chat_urls():
  function test_plugin_ntfy_attachments (line 424) | def test_plugin_ntfy_attachments(mock_post):
  function test_plugin_custom_ntfy_edge_cases (line 536) | def test_plugin_custom_ntfy_edge_cases(mock_post):
  function test_plugin_ntfy_config_files (line 634) | def test_plugin_ntfy_config_files(mock_post, mock_get):
  function test_plugin_ntfy_internationalized_urls (line 708) | def test_plugin_ntfy_internationalized_urls(mock_post):
  function test_plugin_ntfy_message_to_attach (line 746) | def test_plugin_ntfy_message_to_attach(mock_post):

FILE: tests/test_plugin_office365.py
  function test_plugin_office365_urls (line 306) | def test_plugin_office365_urls():
  function test_plugin_office365_general (line 315) | def test_plugin_office365_general(mock_get, mock_post):
  function test_plugin_office365_authentication (line 411) | def test_plugin_office365_authentication(mock_get, mock_post):
  function test_plugin_office365_queries (line 502) | def test_plugin_office365_queries(mock_post, mock_get, mock_put):
  function test_plugin_office365_attachments (line 612) | def test_plugin_office365_attachments(mock_post, mock_get, mock_put):

FILE: tests/test_plugin_onesignal.py
  function test_plugin_onesignal_urls (line 203) | def test_plugin_onesignal_urls():
  function test_plugin_onesignal_edge_cases (line 210) | def test_plugin_onesignal_edge_cases():
  function test_plugin_onesignal_notifications (line 350) | def test_plugin_onesignal_notifications(mock_post):

FILE: tests/test_plugin_opsgenie.py
  function test_plugin_opsgenie_urls (line 376) | def test_plugin_opsgenie_urls(tmpdir):
  function test_plugin_opsgenie_config_files (line 384) | def test_plugin_opsgenie_config_files(mock_post):
  function test_plugin_opsgenie_edge_case (line 454) | def test_plugin_opsgenie_edge_case(mock_post):

FILE: tests/test_plugin_pagerduty.py
  function test_plugin_pagerduty_urls (line 190) | def test_plugin_pagerduty_urls():
  function test_plugin_pagerduty_notify_type_is_string (line 198) | def test_plugin_pagerduty_notify_type_is_string(mock_post):

FILE: tests/test_plugin_pagertree.py
  function test_plugin_pagertree_urls (line 159) | def test_plugin_pagertree_urls():
  function test_plugin_pagertree_general (line 167) | def test_plugin_pagertree_general(mock_post):

FILE: tests/test_plugin_parse_platform.py
  function test_plugin_parse_platform_urls (line 136) | def test_plugin_parse_platform_urls():

FILE: tests/test_plugin_plivo.py
  function test_plugin_plivo_urls (line 150) | def test_plugin_plivo_urls():

FILE: tests/test_plugin_popcorn_notify.py
  function test_plugin_popcorn_notify_urls (line 121) | def test_plugin_popcorn_notify_urls():

FILE: tests/test_plugin_prowl.py
  function test_plugin_prowl (line 169) | def test_plugin_prowl():
  function test_plugin_prowl_edge_cases (line 176) | def test_plugin_prowl_edge_cases():
  function test_plugin_prowl_config_files (line 193) | def test_plugin_prowl_config_files(mock_post):

FILE: tests/test_plugin_pushbullet.py
  function test_plugin_pushbullet_urls (line 243) | def test_plugin_pushbullet_urls():
  function test_plugin_pushbullet_attachments (line 251) | def test_plugin_pushbullet_attachments(mock_post):
  function test_plugin_pushbullet_edge_cases (line 417) | def test_plugin_pushbullet_edge_cases(mock_post, mock_get):

FILE: tests/test_plugin_pushdeer.py
  function test_plugin_pushdeer_urls (line 111) | def test_plugin_pushdeer_urls():
  function test_plugin_pushdeer_general (line 119) | def test_plugin_pushdeer_general(mock_post):

FILE: tests/test_plugin_pushed.py
  function test_plugin_pushed_urls (line 197) | def test_plugin_pushed_urls():
  function test_plugin_pushed_edge_cases (line 206) | def test_plugin_pushed_edge_cases(mock_post, mock_get):

FILE: tests/test_plugin_pushjet.py
  function test_plugin_pushjet_urls (line 126) | def test_plugin_pushjet_urls():
  function test_plugin_pushjet_edge_cases (line 133) | def test_plugin_pushjet_edge_cases():

FILE: tests/test_plugin_pushme.py
  function test_plugin_pushme_urls (line 127) | def test_plugin_pushme_urls():

FILE: tests/test_plugin_pushover.py
  function test_plugin_pushover_urls (line 266) | def test_plugin_pushover_urls():
  function test_plugin_pushover_attachments (line 274) | def test_plugin_pushover_attachments(mock_post, tmpdir):
  function test_plugin_pushover_edge_cases (line 398) | def test_plugin_pushover_edge_cases(mock_post):
  function test_plugin_pushover_config_files (line 468) | def test_plugin_pushover_config_files(mock_post):

FILE: tests/test_plugin_pushplus.py
  function test_plugin_pushplus_urls (line 97) | def test_plugin_pushplus_urls():

FILE: tests/test_plugin_pushsafer.py
  function test_plugin_pushsafer_urls (line 285) | def test_plugin_pushsafer_urls():
  function test_plugin_pushsafer_general (line 293) | def test_plugin_pushsafer_general(mock_post):

FILE: tests/test_plugin_pushy.py
  function test_plugin_pushy_urls (line 197) | def test_plugin_pushy_urls():

FILE: tests/test_plugin_qq.py
  function test_plugin_qq_urls (line 96) | def test_plugin_qq_urls():

FILE: tests/test_plugin_reddit.py
  function test_plugin_reddit_urls (line 280) | def test_plugin_reddit_urls():
  function test_plugin_reddit_general (line 288) | def test_plugin_reddit_general(mock_post):

FILE: tests/test_plugin_resend.py
  function test_plugin_resend_urls (line 223) | def test_plugin_resend_urls():
  function test_plugin_resend_edge_cases (line 232) | def test_plugin_resend_edge_cases(mock_post, mock_get):
  function test_plugin_resend_attachments (line 266) | def test_plugin_resend_attachments(mock_post, mock_get):

FILE: tests/test_plugin_revolt.py
  function test_plugin_revolt_urls (line 259) | def test_plugin_revolt_urls():
  function test_plugin_revolt_notifications (line 267) | def test_plugin_revolt_notifications(mock_post):
  function test_plugin_revolt_general (line 343) | def test_plugin_revolt_general(mock_sleep, mock_post):
  function test_plugin_revolt_overflow (line 475) | def test_plugin_revolt_overflow(mock_post):
  function test_plugin_revolt_markdown_extra (line 533) | def test_plugin_revolt_markdown_extra(mock_post):

FILE: tests/test_plugin_rocket_chat.py
  function test_plugin_rocket_chat_urls (line 326) | def test_plugin_rocket_chat_urls():
  function test_plugin_rocket_chat_edge_cases (line 335) | def test_plugin_rocket_chat_edge_cases(mock_post, mock_get):

FILE: tests/test_plugin_rsyslog.py
  function test_plugin_rsyslog_by_url (line 44) | def test_plugin_rsyslog_by_url(mock_getpid, mock_socket):
  function test_plugin_rsyslog_edge_cases (line 161) | def test_plugin_rsyslog_edge_cases():

FILE: tests/test_plugin_ryver.py
  function test_plugin_ryver_urls (line 165) | def test_plugin_ryver_urls():
  function test_plugin_ryver_edge_cases (line 172) | def test_plugin_ryver_edge_cases():

FILE: tests/test_plugin_sendgrid.py
  function test_plugin_sendgrid_urls (line 189) | def test_plugin_sendgrid_urls():
  function test_plugin_sendgrid_edge_cases (line 198) | def test_plugin_sendgrid_edge_cases(mock_post, mock_get):
  function test_plugin_sendgrid_attachments (line 232) | def test_plugin_sendgrid_attachments(mock_post, mock_get):

FILE: tests/test_plugin_sendpulse.py
  function test_plugin_sendpulse_urls (line 258) | def test_plugin_sendpulse_urls():
  function test_plugin_sendpulse_edge_cases (line 269) | def test_plugin_sendpulse_edge_cases(mock_post):
  function test_plugin_sendpulse_fail_cases (line 486) | def test_plugin_sendpulse_fail_cases():
  function test_plugin_sendpulse_attachments (line 527) | def test_plugin_sendpulse_attachments(mock_post):

FILE: tests/test_plugin_serverchan.py
  function test_plugin_serverchan_urls (line 83) | def test_plugin_serverchan_urls():

FILE: tests/test_plugin_ses.py
  function test_plugin_ses_urls (line 249) | def test_plugin_ses_urls():
  function test_plugin_ses_edge_cases (line 259) | def test_plugin_ses_edge_cases(mock_post):
  function test_plugin_ses_url_parsing (line 319) | def test_plugin_ses_url_parsing():
  function test_plugin_ses_aws_response_handling (line 366) | def test_plugin_ses_aws_response_handling():
  function test_plugin_ses_attachments (line 429) | def test_plugin_ses_attachments(mock_post):

FILE: tests/test_plugin_seven.py
  function test_plugin_seven_urls (line 142) | def test_plugin_seven_urls():
  function test_plugin_seven_edge_cases (line 150) | def test_plugin_seven_edge_cases(mock_post):

FILE: tests/test_plugin_sfr.py
  function test_plugin_sfr_urls (line 219) | def test_plugin_sfr_urls():
  function test_plugin_sfr_notification_ok (line 226) | def test_plugin_sfr_notification_ok(mock_post):
  function test_plugin_sfr_notification_multiple_targets_ok (line 268) | def test_plugin_sfr_notification_multiple_targets_ok(mock_post):
  function test_plugin_sfr_notification_ko (line 314) | def test_plugin_sfr_notification_ko(mock_post):
  function test_plugin_sfr_notification_multiple_targets_all_ko (line 360) | def test_plugin_sfr_notification_multiple_targets_all_ko(mock_post):
  function test_plugin_sfr_notification_multiple_targets_one_ko (line 395) | def test_plugin_sfr_notification_multiple_targets_one_ko(mock_post):
  function test_plugin_sfr_notification_exceptions (line 442) | def test_plugin_sfr_notification_exceptions(mock_post):
  function test_plugin_sfr_notification_exceptions_requests (line 530) | def test_plugin_sfr_notification_exceptions_requests(mock_post):
  function test_plugin_sfr_failure (line 576) | def test_plugin_sfr_failure(mock_post):

FILE: tests/test_plugin_signal.py
  function request_mock (line 52) | def request_mock(mocker):
  function test_plugin_signal_urls (line 238) | def test_plugin_signal_urls():
  function test_plugin_signal_edge_cases (line 245) | def test_plugin_signal_edge_cases(request_mock):
  function test_plugin_signal_yaml_config (line 286) | def test_plugin_signal_yaml_config(request_mock):
  function test_plugin_signal_based_on_feedback (line 333) | def test_plugin_signal_based_on_feedback(request_mock):
  function test_notify_signal_plugin_attachments (line 422) | def test_notify_signal_plugin_attachments(request_mock):
  function test_plugin_signal_text_mode_markdown_from_url (line 498) | def test_plugin_signal_text_mode_markdown_from_url(request_mock):
  function test_plugin_signal_text_mode_markdown_from_library (line 519) | def test_plugin_signal_text_mode_markdown_from_library(request_mock):

FILE: tests/test_plugin_signl4.py
  function test_plugin_signl4_urls (line 170) | def test_plugin_signl4_urls():

FILE: tests/test_plugin_simplepush.py
  function test_plugin_simplepush_urls (line 131) | def test_plugin_simplepush_urls():
  function test_plugin_simpepush_cryptography_import_error (line 141) | def test_plugin_simpepush_cryptography_import_error():
  function test_plugin_simplepush_edge_cases (line 155) | def test_plugin_simplepush_edge_cases():
  function test_plugin_simplepush_general (line 180) | def test_plugin_simplepush_general(mock_post):

FILE: tests/test_plugin_sinch.py
  function test_plugin_sinch_urls (line 185) | def test_plugin_sinch_urls():
  function test_plugin_sinch_edge_cases (line 193) | def test_plugin_sinch_edge_cases(mock_post):

FILE: tests/test_plugin_slack.py
  function test_plugin_slack_urls (line 506) | def test_plugin_slack_urls():
  function test_plugin_slack_oauth_access_token (line 514) | def test_plugin_slack_oauth_access_token(mock_request):
  function test_plugin_slack_webhook_mode (line 713) | def test_plugin_slack_webhook_mode(mock_request):
  function test_plugin_slack_send_by_email (line 765) | def test_plugin_slack_send_by_email(mock_get, mock_request):
  function test_plugin_slack_markdown (line 996) | def test_plugin_slack_markdown(mock_get, mock_request):
  function test_plugin_slack_single_thread_reply (line 1059) | def test_plugin_slack_single_thread_reply(mock_request):
  function test_plugin_slack_multiple_thread_reply (line 1100) | def test_plugin_slack_multiple_thread_reply(mock_request):
  function test_plugin_slack_file_upload_success (line 1147) | def test_plugin_slack_file_upload_success(mock_request):
  function test_plugin_slack_file_upload_fails_missing_files (line 1194) | def test_plugin_slack_file_upload_fails_missing_files(mock_request):

FILE: tests/test_plugin_smpp.py
  function test_plugin_smpplib_import_error (line 139) | def test_plugin_smpplib_import_error():
  function test_plugin_smpp_urls (line 152) | def test_plugin_smpp_urls():
  function test_plugin_smpp_edge_case (line 170) | def test_plugin_smpp_edge_case():

FILE: tests/test_plugin_sms_manager.py
  function test_plugin_smsmgr_urls (line 132) | def test_plugin_smsmgr_urls():
  function test_plugin_smsmgr_edge_cases (line 140) | def test_plugin_smsmgr_edge_cases(mock_get):

FILE: tests/test_plugin_smseagle.py
  function test_plugin_smseagle_urls (line 354) | def test_plugin_smseagle_urls():
  function test_plugin_smseagle_edge_cases (line 362) | def test_plugin_smseagle_edge_cases(mock_post):
  function test_plugin_smseagle_result_set (line 406) | def test_plugin_smseagle_result_set(mock_post):
  function test_notify_smseagle_plugin_result_list (line 626) | def test_notify_smseagle_plugin_result_list(mock_post):
  function test_notify_smseagle_plugin_attachments (line 661) | def test_notify_smseagle_plugin_attachments(mock_post):

FILE: tests/test_plugin_smtp2go.py
  function test_plugin_smtp2go_urls (line 217) | def test_plugin_smtp2go_urls():
  function test_plugin_smtp2go_attachments (line 225) | def test_plugin_smtp2go_attachments(mock_post):

FILE: tests/test_plugin_sns.py
  function test_plugin_sns_urls (
Condensed preview — 431 files, each showing path, character count, and a content snippet. Download the .json file or copy for the full structured content (6,443K chars).
[
  {
    "path": ".codecov.yml",
    "chars": 338,
    "preview": "codecov:\n  require_ci_to_pass: false\n\ncomment:\n  layout: \"diff, flags, files\"\n  behavior: default\n  require_changes: fal"
  },
  {
    "path": ".github/FUNDING.yml",
    "chars": 91,
    "preview": "github: caronc\ncustom:\n  - 'https://www.paypal.com/donate/?hosted_button_id=CR6YF7KLQWQ5E'\n"
  },
  {
    "path": ".github/ISSUE_TEMPLATE/1_bug_report.md",
    "chars": 1005,
    "preview": "---\nname: 🐛 Bug Report\nabout: Report errors and problems in Apprise\ntitle: ''\nlabels: 'bug'\nassignees: ''\n\n---\n\n## Notif"
  },
  {
    "path": ".github/ISSUE_TEMPLATE/2_enhancement_request.md",
    "chars": 770,
    "preview": "---\nname: 💡 Enhancement Request\nabout: Suggest an improvement to Apprise\ntitle: ''\nlabels: 'enhancement'\nassignees: ''\n\n"
  },
  {
    "path": ".github/ISSUE_TEMPLATE/3_new-notification-request.md",
    "chars": 1248,
    "preview": "---\nname: 📣 New Notification Request\nabout: Request a new notification service integration\ntitle: ''\nlabels: ['enhanceme"
  },
  {
    "path": ".github/ISSUE_TEMPLATE/4_question.md",
    "chars": 433,
    "preview": "---\nname: ❓ Support Question\nabout: Ask a question about Apprise (prefer Discussions)\ntitle: ''\nlabels: 'question'\nassig"
  },
  {
    "path": ".github/ISSUE_TEMPLATE/config.yaml",
    "chars": 464,
    "preview": "blank_issues_enabled: false\ncontact_links:\n  - name: Documentation (Apprise Docs)\n    url: https://appriseit.com\n    abo"
  },
  {
    "path": ".github/PULL_REQUEST_TEMPLATE.md",
    "chars": 2441,
    "preview": "## Description\n**Related issue (if applicable):** #<!-- apprise issue number goes here -->\n\n<!--\n  -- Have anything else"
  },
  {
    "path": ".github/workflows/codeql-analysis.yml",
    "chars": 916,
    "preview": "name: \"CodeQL\"\n\non:\n  push:\n    branches: [ master ]\n  pull_request:\n    branches: [ master ]\n  schedule:\n    - cron: '4"
  },
  {
    "path": ".github/workflows/lint.yml",
    "chars": 462,
    "preview": "# .github/workflows/lint.yml\nname: Run Lint Checks\n\non:\n  push:\n    paths:\n      - '**.py'\n  pull_request:\n    paths:\n  "
  },
  {
    "path": ".github/workflows/loc-badge.yml",
    "chars": 1181,
    "preview": "# LoC = Lines of Code\nname: LoC Badge\n\non:\n  # Manual only\n  workflow_dispatch:\n\npermissions:\n  contents: write\n  pull-r"
  },
  {
    "path": ".github/workflows/pkgbuild.yml",
    "chars": 2976,
    "preview": "#\n# Verify on CI/GHA that RPM package building works.\n#\nname: RPM Packaging\n\non:\n  push:\n    branches: [main, master, 'r"
  },
  {
    "path": ".github/workflows/tests.yml",
    "chars": 3728,
    "preview": "name: Run Tests\n\non:\n  # Run tests on push to main, master, or any release/ branch\n  push:\n    branches: [main, master, "
  },
  {
    "path": ".gitignore",
    "chars": 842,
    "preview": "# Byte-compiled / optimized / DLL files\n__pycache__/\n*.py[cod]\n*$py.class\n\n# left-over-files from conflicts/merges\n*.ori"
  },
  {
    "path": ".vscode/settings.json",
    "chars": 489,
    "preview": "{\n  \"python.testing.pytestArgs\": [\n    \"tests\"\n  ],\n  \"python.linting.enabled\": true,\n  \"python.linting.ruffEnabled\": tr"
  },
  {
    "path": "ACKNOWLEDGEMENTS.md",
    "chars": 1131,
    "preview": "# Contributions to the Apprise Project\n\n## Creator & Maintainer\n\n* Chris Caron <lead2gold@gmail.com>\n\n## Contributors\n\nT"
  },
  {
    "path": "CONTRIBUTING.md",
    "chars": 2579,
    "preview": "# 🤝 Contributing to Apprise\n\nThank you for your interest in contributing to Apprise!\n\nWe welcome bug reports, feature re"
  },
  {
    "path": "LICENSE",
    "chars": 1343,
    "preview": "BSD 2-Clause License\n\nCopyright (c) 2026, Chris Caron <lead2gold@gmail.com>\nAll rights reserved.\n\nRedistribution and use"
  },
  {
    "path": "MANIFEST.in",
    "chars": 555,
    "preview": "include LICENSE\ninclude README.md\ninclude CONTRIBUTING.md\ninclude ACKNOWLEDGEMENTS.md\ninclude SECURITY.md\ninclude pyproj"
  },
  {
    "path": "README.md",
    "chars": 53415,
    "preview": "![Apprise Logo](https://raw.githubusercontent.com/caronc/apprise/master/apprise/assets/themes/default/apprise-logo.png)\n"
  },
  {
    "path": "SECURITY.md",
    "chars": 379,
    "preview": "# Security Policy\n\n## Supported Versions\n\n| Version | Supported          |\n| ------- | ------------------ |\n| 0.9.x   | "
  },
  {
    "path": "all-plugin-requirements.txt",
    "chars": 636,
    "preview": "#\n# Note: This file is being kept for backwards compatibility with\n#       legacy systems that point here.  All future c"
  },
  {
    "path": "apprise/__init__.py",
    "chars": 3851,
    "preview": "# BSD 2-Clause License\n#\n# Apprise - Push Notification Library.\n# Copyright (c) 2026, Chris Caron <lead2gold@gmail.com>\n"
  },
  {
    "path": "apprise/apprise.py",
    "chars": 37527,
    "preview": "# BSD 2-Clause License\n#\n# Apprise - Push Notification Library.\n# Copyright (c) 2026, Chris Caron <lead2gold@gmail.com>\n"
  },
  {
    "path": "apprise/apprise_attachment.py",
    "chars": 13642,
    "preview": "# BSD 2-Clause License\n#\n# Apprise - Push Notification Library.\n# Copyright (c) 2026, Chris Caron <lead2gold@gmail.com>\n"
  },
  {
    "path": "apprise/apprise_config.py",
    "chars": 17964,
    "preview": "# BSD 2-Clause License\n#\n# Apprise - Push Notification Library.\n# Copyright (c) 2026, Chris Caron <lead2gold@gmail.com>\n"
  },
  {
    "path": "apprise/asset.py",
    "chars": 17701,
    "preview": "# BSD 2-Clause License\n#\n# Apprise - Push Notification Library.\n# Copyright (c) 2026, Chris Caron <lead2gold@gmail.com>\n"
  },
  {
    "path": "apprise/assets/NotifyXML-1.0.xsd",
    "chars": 986,
    "preview": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<xs:schema attributeFormDefault=\"unqualified\" elementFormDefault=\"qualified\" xmln"
  },
  {
    "path": "apprise/assets/NotifyXML-1.1.xsd",
    "chars": 1758,
    "preview": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<xs:schema attributeFormDefault=\"unqualified\" elementFormDefault=\"qualified\" xmln"
  },
  {
    "path": "apprise/attachment/__init__.py",
    "chars": 1654,
    "preview": "# BSD 2-Clause License\n#\n# Apprise - Push Notification Library.\n# Copyright (c) 2026, Chris Caron <lead2gold@gmail.com>\n"
  },
  {
    "path": "apprise/attachment/base.py",
    "chars": 16463,
    "preview": "# BSD 2-Clause License\n#\n# Apprise - Push Notification Library.\n# Copyright (c) 2026, Chris Caron <lead2gold@gmail.com>\n"
  },
  {
    "path": "apprise/attachment/file.py",
    "chars": 4953,
    "preview": "# BSD 2-Clause License\n#\n# Apprise - Push Notification Library.\n# Copyright (c) 2026, Chris Caron <lead2gold@gmail.com>\n"
  },
  {
    "path": "apprise/attachment/http.py",
    "chars": 13843,
    "preview": "# BSD 2-Clause License\n#\n# Apprise - Push Notification Library.\n# Copyright (c) 2026, Chris Caron <lead2gold@gmail.com>\n"
  },
  {
    "path": "apprise/attachment/memory.py",
    "chars": 6873,
    "preview": "# BSD 2-Clause License\n#\n# Apprise - Push Notification Library.\n# Copyright (c) 2026, Chris Caron <lead2gold@gmail.com>\n"
  },
  {
    "path": "apprise/cli.py",
    "chars": 37773,
    "preview": "# BSD 2-Clause License\n#\n# Apprise - Push Notification Library.\n# Copyright (c) 2026, Chris Caron <lead2gold@gmail.com>\n"
  },
  {
    "path": "apprise/common.py",
    "chars": 7022,
    "preview": "# BSD 2-Clause License\n#\n# Apprise - Push Notification Library.\n# Copyright (c) 2026, Chris Caron <lead2gold@gmail.com>\n"
  },
  {
    "path": "apprise/compat.py",
    "chars": 2043,
    "preview": "# BSD 2-Clause License\n#\n# Apprise - Push Notification Library.\n# Copyright (c) 2026, Chris Caron <lead2gold@gmail.com>\n"
  },
  {
    "path": "apprise/config/__init__.py",
    "chars": 1655,
    "preview": "# BSD 2-Clause License\n#\n# Apprise - Push Notification Library.\n# Copyright (c) 2026, Chris Caron <lead2gold@gmail.com>\n"
  },
  {
    "path": "apprise/config/base.py",
    "chars": 55848,
    "preview": "# BSD 2-Clause License\n#\n# Apprise - Push Notification Library.\n# Copyright (c) 2026, Chris Caron <lead2gold@gmail.com>\n"
  },
  {
    "path": "apprise/config/file.py",
    "chars": 6119,
    "preview": "# BSD 2-Clause License\n#\n# Apprise - Push Notification Library.\n# Copyright (c) 2026, Chris Caron <lead2gold@gmail.com>\n"
  },
  {
    "path": "apprise/config/http.py",
    "chars": 9445,
    "preview": "# BSD 2-Clause License\n#\n# Apprise - Push Notification Library.\n# Copyright (c) 2026, Chris Caron <lead2gold@gmail.com>\n"
  },
  {
    "path": "apprise/config/memory.py",
    "chars": 2733,
    "preview": "# BSD 2-Clause License\n#\n# Apprise - Push Notification Library.\n# Copyright (c) 2026, Chris Caron <lead2gold@gmail.com>\n"
  },
  {
    "path": "apprise/conversion.py",
    "chars": 6393,
    "preview": "# BSD 2-Clause License\n#\n# Apprise - Push Notification Library.\n# Copyright (c) 2026, Chris Caron <lead2gold@gmail.com>\n"
  },
  {
    "path": "apprise/decorators/__init__.py",
    "chars": 1456,
    "preview": "# BSD 2-Clause License\n#\n# Apprise - Push Notification Library.\n# Copyright (c) 2026, Chris Caron <lead2gold@gmail.com>\n"
  },
  {
    "path": "apprise/decorators/base.py",
    "chars": 7879,
    "preview": "# BSD 2-Clause License\n#\n# Apprise - Push Notification Library.\n# Copyright (c) 2026, Chris Caron <lead2gold@gmail.com>\n"
  },
  {
    "path": "apprise/decorators/notify.py",
    "chars": 5066,
    "preview": "# BSD 2-Clause License\n#\n# Apprise - Push Notification Library.\n# Copyright (c) 2026, Chris Caron <lead2gold@gmail.com>\n"
  },
  {
    "path": "apprise/emojis.py",
    "chars": 79672,
    "preview": "# BSD 2-Clause License\n#\n# Apprise - Push Notification Library.\n# Copyright (c) 2026, Chris Caron <lead2gold@gmail.com>\n"
  },
  {
    "path": "apprise/exception.py",
    "chars": 2512,
    "preview": "# BSD 2-Clause License\n#\n# Apprise - Push Notification Library.\n# Copyright (c) 2026, Chris Caron <lead2gold@gmail.com>\n"
  },
  {
    "path": "apprise/i18n/__init__.py",
    "chars": 0,
    "preview": ""
  },
  {
    "path": "apprise/i18n/en/LC_MESSAGES/apprise.po",
    "chars": 47553,
    "preview": "# English translations for apprise.\n# Copyright (C) 2026 Chris Caron\n# This file is distributed under the same license a"
  },
  {
    "path": "apprise/locale.py",
    "chars": 8983,
    "preview": "# BSD 2-Clause License\n#\n# Apprise - Push Notification Library.\n# Copyright (c) 2026, Chris Caron <lead2gold@gmail.com>\n"
  },
  {
    "path": "apprise/logger.py",
    "chars": 6715,
    "preview": "# BSD 2-Clause License\n#\n# Apprise - Push Notification Library.\n# Copyright (c) 2026, Chris Caron <lead2gold@gmail.com>\n"
  },
  {
    "path": "apprise/manager.py",
    "chars": 29624,
    "preview": "# BSD 2-Clause License\n#\n# Apprise - Push Notification Library.\n# Copyright (c) 2026, Chris Caron <lead2gold@gmail.com>\n"
  },
  {
    "path": "apprise/manager_attachment.py",
    "chars": 2134,
    "preview": "# BSD 2-Clause License\n#\n# Apprise - Push Notification Library.\n# Copyright (c) 2026, Chris Caron <lead2gold@gmail.com>\n"
  },
  {
    "path": "apprise/manager_config.py",
    "chars": 2139,
    "preview": "# BSD 2-Clause License\n#\n# Apprise - Push Notification Library.\n# Copyright (c) 2026, Chris Caron <lead2gold@gmail.com>\n"
  },
  {
    "path": "apprise/manager_plugins.py",
    "chars": 2137,
    "preview": "# BSD 2-Clause License\n#\n# Apprise - Push Notification Library.\n# Copyright (c) 2026, Chris Caron <lead2gold@gmail.com>\n"
  },
  {
    "path": "apprise/persistent_store.py",
    "chars": 60934,
    "preview": "# BSD 2-Clause License\n#\n# Apprise - Push Notification Library.\n# Copyright (c) 2026, Chris Caron <lead2gold@gmail.com>\n"
  },
  {
    "path": "apprise/plugins/__init__.py",
    "chars": 18665,
    "preview": "# BSD 2-Clause License\n#\n# Apprise - Push Notification Library.\n# Copyright (c) 2026, Chris Caron <lead2gold@gmail.com>\n"
  },
  {
    "path": "apprise/plugins/africas_talking.py",
    "chars": 16658,
    "preview": "# BSD 2-Clause License\n#\n# Apprise - Push Notification Library.\n# Copyright (c) 2026, Chris Caron <lead2gold@gmail.com>\n"
  },
  {
    "path": "apprise/plugins/apprise_api.py",
    "chars": 18134,
    "preview": "# BSD 2-Clause License\n#\n# Apprise - Push Notification Library.\n# Copyright (c) 2026, Chris Caron <lead2gold@gmail.com>\n"
  },
  {
    "path": "apprise/plugins/aprs.py",
    "chars": 25110,
    "preview": "# BSD 2-Clause License\n#\n# Apprise - Push Notification Library.\n# Copyright (c) 2026, Chris Caron <lead2gold@gmail.com>\n"
  },
  {
    "path": "apprise/plugins/bark.py",
    "chars": 19003,
    "preview": "# BSD 2-Clause License\n#\n# Apprise - Push Notification Library.\n# Copyright (c) 2026, Chris Caron <lead2gold@gmail.com>\n"
  },
  {
    "path": "apprise/plugins/base.py",
    "chars": 37080,
    "preview": "# BSD 2-Clause License\n#\n# Apprise - Push Notification Library.\n# Copyright (c) 2026, Chris Caron <lead2gold@gmail.com>\n"
  },
  {
    "path": "apprise/plugins/bluesky.py",
    "chars": 23303,
    "preview": "# BSD 2-Clause License\n#\n# Apprise - Push Notification Library.\n# Copyright (c) 2026, Chris Caron <lead2gold@gmail.com>\n"
  },
  {
    "path": "apprise/plugins/brevo.py",
    "chars": 19388,
    "preview": "# BSD 2-Clause License\n#\n# Apprise - Push Notification Library.\n# Copyright (c) 2026, Chris Caron <lead2gold@gmail.com>\n"
  },
  {
    "path": "apprise/plugins/bulksms.py",
    "chars": 17036,
    "preview": "# BSD 2-Clause License\n#\n# Apprise - Push Notification Library.\n# Copyright (c) 2026, Chris Caron <lead2gold@gmail.com>\n"
  },
  {
    "path": "apprise/plugins/bulkvs.py",
    "chars": 13922,
    "preview": "# BSD 2-Clause License\n#\n# Apprise - Push Notification Library.\n# Copyright (c) 2026, Chris Caron <lead2gold@gmail.com>\n"
  },
  {
    "path": "apprise/plugins/burstsms.py",
    "chars": 16149,
    "preview": "# BSD 2-Clause License\n#\n# Apprise - Push Notification Library.\n# Copyright (c) 2026, Chris Caron <lead2gold@gmail.com>\n"
  },
  {
    "path": "apprise/plugins/chanify.py",
    "chars": 6887,
    "preview": "# BSD 2-Clause License\n#\n# Apprise - Push Notification Library.\n# Copyright (c) 2026, Chris Caron <lead2gold@gmail.com>\n"
  },
  {
    "path": "apprise/plugins/clickatell.py",
    "chars": 10367,
    "preview": "# BSD 2-Clause License\n#\n# Apprise - Push Notification Library.\n# Copyright (c) 2026, Chris Caron <lead2gold@gmail.com>\n"
  },
  {
    "path": "apprise/plugins/clicksend.py",
    "chars": 12014,
    "preview": "# BSD 2-Clause License\n#\n# Apprise - Push Notification Library.\n# Copyright (c) 2026, Chris Caron <lead2gold@gmail.com>\n"
  },
  {
    "path": "apprise/plugins/custom_form.py",
    "chars": 18926,
    "preview": "# BSD 2-Clause License\n#\n# Apprise - Push Notification Library.\n# Copyright (c) 2026, Chris Caron <lead2gold@gmail.com>\n"
  },
  {
    "path": "apprise/plugins/custom_json.py",
    "chars": 14659,
    "preview": "# BSD 2-Clause License\n#\n# Apprise - Push Notification Library.\n# Copyright (c) 2026, Chris Caron <lead2gold@gmail.com>\n"
  },
  {
    "path": "apprise/plugins/custom_xml.py",
    "chars": 18088,
    "preview": "# BSD 2-Clause License\n#\n# Apprise - Push Notification Library.\n# Copyright (c) 2026, Chris Caron <lead2gold@gmail.com>\n"
  },
  {
    "path": "apprise/plugins/d7networks.py",
    "chars": 15494,
    "preview": "# BSD 2-Clause License\n#\n# Apprise - Push Notification Library.\n# Copyright (c) 2026, Chris Caron <lead2gold@gmail.com>\n"
  },
  {
    "path": "apprise/plugins/dapnet.py",
    "chars": 13855,
    "preview": "# BSD 2-Clause License\n#\n# Apprise - Push Notification Library.\n# Copyright (c) 2026, Chris Caron <lead2gold@gmail.com>\n"
  },
  {
    "path": "apprise/plugins/dbus.py",
    "chars": 14495,
    "preview": "# BSD 2-Clause License\n#\n# Apprise - Push Notification Library.\n# Copyright (c) 2026, Chris Caron <lead2gold@gmail.com>\n"
  },
  {
    "path": "apprise/plugins/dingtalk.py",
    "chars": 12343,
    "preview": "# BSD 2-Clause License\n#\n# Apprise - Push Notification Library.\n# Copyright (c) 2026, Chris Caron <lead2gold@gmail.com>\n"
  },
  {
    "path": "apprise/plugins/discord.py",
    "chars": 30859,
    "preview": "# BSD 2-Clause License\n#\n# Apprise - Push Notification Library.\n# Copyright (c) 2026, Chris Caron <lead2gold@gmail.com>\n"
  },
  {
    "path": "apprise/plugins/dot.py",
    "chars": 20700,
    "preview": "# BSD 2-Clause License\n#\n# Apprise - Push Notification Library.\n# Copyright (c) 2026, Chris Caron <lead2gold@gmail.com>\n"
  },
  {
    "path": "apprise/plugins/email/__init__.py",
    "chars": 1917,
    "preview": "# BSD 2-Clause License\n#\n# Apprise - Push Notification Library.\n# Copyright (c) 2026, Chris Caron <lead2gold@gmail.com>\n"
  },
  {
    "path": "apprise/plugins/email/base.py",
    "chars": 41169,
    "preview": "# BSD 2-Clause License\n#\n# Apprise - Push Notification Library.\n# Copyright (c) 2026, Chris Caron <lead2gold@gmail.com>\n"
  },
  {
    "path": "apprise/plugins/email/common.py",
    "chars": 2527,
    "preview": "# BSD 2-Clause License\n#\n# Apprise - Push Notification Library.\n# Copyright (c) 2026, Chris Caron <lead2gold@gmail.com>\n"
  },
  {
    "path": "apprise/plugins/email/templates.py",
    "chars": 10353,
    "preview": "# BSD 2-Clause License\n#\n# Apprise - Push Notification Library.\n# Copyright (c) 2026, Chris Caron <lead2gold@gmail.com>\n"
  },
  {
    "path": "apprise/plugins/emby.py",
    "chars": 22567,
    "preview": "# BSD 2-Clause License\n#\n# Apprise - Push Notification Library.\n# Copyright (c) 2026, Chris Caron <lead2gold@gmail.com>\n"
  },
  {
    "path": "apprise/plugins/enigma2.py",
    "chars": 12109,
    "preview": "# BSD 2-Clause License\n#\n# Apprise - Push Notification Library.\n# Copyright (c) 2026, Chris Caron <lead2gold@gmail.com>\n"
  },
  {
    "path": "apprise/plugins/fcm/__init__.py",
    "chars": 22158,
    "preview": "# BSD 2-Clause License\n#\n# Apprise - Push Notification Library.\n# Copyright (c) 2026, Chris Caron <lead2gold@gmail.com>\n"
  },
  {
    "path": "apprise/plugins/fcm/color.py",
    "chars": 4609,
    "preview": "# BSD 2-Clause License\n#\n# Apprise - Push Notification Library.\n# Copyright (c) 2026, Chris Caron <lead2gold@gmail.com>\n"
  },
  {
    "path": "apprise/plugins/fcm/common.py",
    "chars": 1687,
    "preview": "# BSD 2-Clause License\n#\n# Apprise - Push Notification Library.\n# Copyright (c) 2026, Chris Caron <lead2gold@gmail.com>\n"
  },
  {
    "path": "apprise/plugins/fcm/oauth.py",
    "chars": 11147,
    "preview": "# BSD 2-Clause License\n#\n# Apprise - Push Notification Library.\n# Copyright (c) 2026, Chris Caron <lead2gold@gmail.com>\n"
  },
  {
    "path": "apprise/plugins/fcm/priority.py",
    "chars": 6904,
    "preview": "# BSD 2-Clause License\n#\n# Apprise - Push Notification Library.\n# Copyright (c) 2026, Chris Caron <lead2gold@gmail.com>\n"
  },
  {
    "path": "apprise/plugins/feishu.py",
    "chars": 7497,
    "preview": "# BSD 2-Clause License\n#\n# Apprise - Push Notification Library.\n# Copyright (c) 2026, Chris Caron <lead2gold@gmail.com>\n"
  },
  {
    "path": "apprise/plugins/flock.py",
    "chars": 13092,
    "preview": "# BSD 2-Clause License\n#\n# Apprise - Push Notification Library.\n# Copyright (c) 2026, Chris Caron <lead2gold@gmail.com>\n"
  },
  {
    "path": "apprise/plugins/fluxer.py",
    "chars": 36141,
    "preview": "# BSD 2-Clause License\n#\n# Apprise - Push Notification Library.\n# Copyright (c) 2026, Chris Caron <lead2gold@gmail.com>\n"
  },
  {
    "path": "apprise/plugins/fortysixelks.py",
    "chars": 11937,
    "preview": "# BSD 2-Clause License\n#\n# Apprise - Push Notification Library.\n# Copyright (c) 2026, Chris Caron <lead2gold@gmail.com>\n"
  },
  {
    "path": "apprise/plugins/freemobile.py",
    "chars": 6910,
    "preview": "# BSD 2-Clause License\n#\n# Apprise - Push Notification Library.\n# Copyright (c) 2026, Chris Caron <lead2gold@gmail.com>\n"
  },
  {
    "path": "apprise/plugins/glib.py",
    "chars": 12920,
    "preview": "# BSD 2-Clause License\n#\n# Apprise - Push Notification Library.\n# Copyright (c) 2026, Chris Caron <lead2gold@gmail.com>\n"
  },
  {
    "path": "apprise/plugins/gnome.py",
    "chars": 9295,
    "preview": "# BSD 2-Clause License\n#\n# Apprise - Push Notification Library.\n# Copyright (c) 2026, Chris Caron <lead2gold@gmail.com>\n"
  },
  {
    "path": "apprise/plugins/google_chat.py",
    "chars": 13588,
    "preview": "# BSD 2-Clause License\n#\n# Apprise - Push Notification Library.\n# Copyright (c) 2026, Chris Caron <lead2gold@gmail.com>\n"
  },
  {
    "path": "apprise/plugins/gotify.py",
    "chars": 11162,
    "preview": "# BSD 2-Clause License\n#\n# Apprise - Push Notification Library.\n# Copyright (c) 2026, Chris Caron <lead2gold@gmail.com>\n"
  },
  {
    "path": "apprise/plugins/growl.py",
    "chars": 14947,
    "preview": "# BSD 2-Clause License\n#\n# Apprise - Push Notification Library.\n# Copyright (c) 2026, Chris Caron <lead2gold@gmail.com>\n"
  },
  {
    "path": "apprise/plugins/guilded.py",
    "chars": 3801,
    "preview": "# BSD 2-Clause License\n#\n# Apprise - Push Notification Library.\n# Copyright (c) 2026, Chris Caron <lead2gold@gmail.com>\n"
  },
  {
    "path": "apprise/plugins/home_assistant.py",
    "chars": 11669,
    "preview": "# BSD 2-Clause License\n#\n# Apprise - Push Notification Library.\n# Copyright (c) 2026, Chris Caron <lead2gold@gmail.com>\n"
  },
  {
    "path": "apprise/plugins/httpsms.py",
    "chars": 11457,
    "preview": "# BSD 2-Clause License\n#\n# Apprise - Push Notification Library.\n# Copyright (c) 2026, Chris Caron <lead2gold@gmail.com>\n"
  },
  {
    "path": "apprise/plugins/ifttt.py",
    "chars": 13899,
    "preview": "# BSD 2-Clause License\n#\n# Apprise - Push Notification Library.\n# Copyright (c) 2026, Chris Caron <lead2gold@gmail.com>\n"
  },
  {
    "path": "apprise/plugins/irc/__init__.py",
    "chars": 1493,
    "preview": "# BSD 2-Clause License\n#\n# Apprise - Push Notification Library.\n# Copyright (c) 2026, Chris Caron <lead2gold@gmail.com>\n"
  },
  {
    "path": "apprise/plugins/irc/base.py",
    "chars": 16894,
    "preview": "# BSD 2-Clause License\n#\n# Apprise - Push Notification Library.\n# Copyright (c) 2026, Chris Caron <lead2gold@gmail.com>\n"
  },
  {
    "path": "apprise/plugins/irc/client.py",
    "chars": 12788,
    "preview": "# BSD 2-Clause License\n#\n# Apprise - Push Notification Library.\n# Copyright (c) 2026, Chris Caron <lead2gold@gmail.com>\n"
  },
  {
    "path": "apprise/plugins/irc/protocol.py",
    "chars": 6539,
    "preview": "# BSD 2-Clause License\n#\n# Apprise - Push Notification Library.\n# Copyright (c) 2026, Chris Caron <lead2gold@gmail.com>\n"
  },
  {
    "path": "apprise/plugins/irc/state.py",
    "chars": 7301,
    "preview": "# BSD 2-Clause License\n#\n# Apprise - Push Notification Library.\n# Copyright (c) 2026, Chris Caron <lead2gold@gmail.com>\n"
  },
  {
    "path": "apprise/plugins/irc/templates.py",
    "chars": 2709,
    "preview": "# BSD 2-Clause License\n#\n# Apprise - Push Notification Library.\n# Copyright (c) 2026, Chris Caron <lead2gold@gmail.com>\n"
  },
  {
    "path": "apprise/plugins/jellyfin.py",
    "chars": 2155,
    "preview": "# BSD 2-Clause License\n#\n# Apprise - Push Notification Library.\n# Copyright (c) 2026, Chris Caron <lead2gold@gmail.com>\n"
  },
  {
    "path": "apprise/plugins/join.py",
    "chars": 14201,
    "preview": "# BSD 2-Clause License\n#\n# Apprise - Push Notification Library.\n# Copyright (c) 2026, Chris Caron <lead2gold@gmail.com>\n"
  },
  {
    "path": "apprise/plugins/kavenegar.py",
    "chars": 12937,
    "preview": "# BSD 2-Clause License\n#\n# Apprise - Push Notification Library.\n# Copyright (c) 2026, Chris Caron <lead2gold@gmail.com>\n"
  },
  {
    "path": "apprise/plugins/kodi.py",
    "chars": 13071,
    "preview": "# BSD 2-Clause License\n#\n# Apprise - Push Notification Library.\n# Copyright (c) 2026, Chris Caron <lead2gold@gmail.com>\n"
  },
  {
    "path": "apprise/plugins/kumulos.py",
    "chars": 8464,
    "preview": "# BSD 2-Clause License\n#\n# Apprise - Push Notification Library.\n# Copyright (c) 2026, Chris Caron <lead2gold@gmail.com>\n"
  },
  {
    "path": "apprise/plugins/lametric.py",
    "chars": 40171,
    "preview": "# BSD 2-Clause License\n#\n# Apprise - Push Notification Library.\n# Copyright (c) 2026, Chris Caron <lead2gold@gmail.com>\n"
  },
  {
    "path": "apprise/plugins/lark.py",
    "chars": 6424,
    "preview": "# BSD 2-Clause License\n#\n# Apprise - Push Notification Library.\n# Copyright (c) 2026, Chris Caron <lead2gold@gmail.com>\n"
  },
  {
    "path": "apprise/plugins/line.py",
    "chars": 10972,
    "preview": "# BSD 2-Clause License\n#\n# Apprise - Push Notification Library.\n# Copyright (c) 2026, Chris Caron <lead2gold@gmail.com>\n"
  },
  {
    "path": "apprise/plugins/macosx.py",
    "chars": 8365,
    "preview": "# BSD 2-Clause License\n#\n# Apprise - Push Notification Library.\n# Copyright (c) 2026, Chris Caron <lead2gold@gmail.com>\n"
  },
  {
    "path": "apprise/plugins/mailgun.py",
    "chars": 26591,
    "preview": "# BSD 2-Clause License\n#\n# Apprise - Push Notification Library.\n# Copyright (c) 2026, Chris Caron <lead2gold@gmail.com>\n"
  },
  {
    "path": "apprise/plugins/mastodon.py",
    "chars": 36655,
    "preview": "# BSD 2-Clause License\n#\n# Apprise - Push Notification Library.\n# Copyright (c) 2026, Chris Caron <lead2gold@gmail.com>\n"
  },
  {
    "path": "apprise/plugins/matrix.py",
    "chars": 65394,
    "preview": "# BSD 2-Clause License\n#\n# Apprise - Push Notification Library.\n# Copyright (c) 2026, Chris Caron <lead2gold@gmail.com>\n"
  },
  {
    "path": "apprise/plugins/mattermost.py",
    "chars": 25346,
    "preview": "# BSD 2-Clause License\n#\n# Apprise - Push Notification Library.\n# Copyright (c) 2026, Chris Caron <lead2gold@gmail.com>\n"
  },
  {
    "path": "apprise/plugins/messagebird.py",
    "chars": 12569,
    "preview": "# BSD 2-Clause License\n#\n# Apprise - Push Notification Library.\n# Copyright (c) 2026, Chris Caron <lead2gold@gmail.com>\n"
  },
  {
    "path": "apprise/plugins/misskey.py",
    "chars": 10055,
    "preview": "# BSD 2-Clause License\n#\n# Apprise - Push Notification Library.\n# Copyright (c) 2026, Chris Caron <lead2gold@gmail.com>\n"
  },
  {
    "path": "apprise/plugins/mqtt.py",
    "chars": 21556,
    "preview": "# BSD 2-Clause License\n#\n# Apprise - Push Notification Library.\n# Copyright (c) 2026, Chris Caron <lead2gold@gmail.com>\n"
  },
  {
    "path": "apprise/plugins/msg91.py",
    "chars": 13153,
    "preview": "# BSD 2-Clause License\n#\n# Apprise - Push Notification Library.\n# Copyright (c) 2026, Chris Caron <lead2gold@gmail.com>\n"
  },
  {
    "path": "apprise/plugins/msteams.py",
    "chars": 27533,
    "preview": "# BSD 2-Clause License\n#\n# Apprise - Push Notification Library.\n# Copyright (c) 2026, Chris Caron <lead2gold@gmail.com>\n"
  },
  {
    "path": "apprise/plugins/nextcloud.py",
    "chars": 20174,
    "preview": "# BSD 2-Clause License\n#\n# Apprise - Push Notification Library.\n# Copyright (c) 2026, Chris Caron <lead2gold@gmail.com>\n"
  },
  {
    "path": "apprise/plugins/nextcloudtalk.py",
    "chars": 11667,
    "preview": "# BSD 2-Clause License\n#\n# Apprise - Push Notification Library.\n# Copyright (c) 2026, Chris Caron <lead2gold@gmail.com>\n"
  },
  {
    "path": "apprise/plugins/notica.py",
    "chars": 13541,
    "preview": "# BSD 2-Clause License\n#\n# Apprise - Push Notification Library.\n# Copyright (c) 2026, Chris Caron <lead2gold@gmail.com>\n"
  },
  {
    "path": "apprise/plugins/notifiarr.py",
    "chars": 15826,
    "preview": "# BSD 2-Clause License\n#\n# Apprise - Push Notification Library.\n# Copyright (c) 2026, Chris Caron <lead2gold@gmail.com>\n"
  },
  {
    "path": "apprise/plugins/notificationapi.py",
    "chars": 33874,
    "preview": "# BSD 2-Clause License\n#\n# Apprise - Push Notification Library.\n# Copyright (c) 2026, Chris Caron <lead2gold@gmail.com>\n"
  },
  {
    "path": "apprise/plugins/notifico.py",
    "chars": 12615,
    "preview": "# BSD 2-Clause License\n#\n# Apprise - Push Notification Library.\n# Copyright (c) 2026, Chris Caron <lead2gold@gmail.com>\n"
  },
  {
    "path": "apprise/plugins/ntfy.py",
    "chars": 32547,
    "preview": "# BSD 2-Clause License\n#\n# Apprise - Push Notification Library.\n# Copyright (c) 2026, Chris Caron <lead2gold@gmail.com>\n"
  },
  {
    "path": "apprise/plugins/office365.py",
    "chars": 38391,
    "preview": "# BSD 2-Clause License\n#\n# Apprise - Push Notification Library.\n# Copyright (c) 2026, Chris Caron <lead2gold@gmail.com>\n"
  },
  {
    "path": "apprise/plugins/one_signal.py",
    "chars": 23941,
    "preview": "# BSD 2-Clause License\n#\n# Apprise - Push Notification Library.\n# Copyright (c) 2026, Chris Caron <lead2gold@gmail.com>\n"
  },
  {
    "path": "apprise/plugins/opsgenie.py",
    "chars": 29465,
    "preview": "# BSD 2-Clause License\n#\n# Apprise - Push Notification Library.\n# Copyright (c) 2026, Chris Caron <lead2gold@gmail.com>\n"
  },
  {
    "path": "apprise/plugins/pagerduty.py",
    "chars": 18843,
    "preview": "# BSD 2-Clause License\n#\n# Apprise - Push Notification Library.\n# Copyright (c) 2026, Chris Caron <lead2gold@gmail.com>\n"
  },
  {
    "path": "apprise/plugins/pagertree.py",
    "chars": 14225,
    "preview": "# BSD 2-Clause License\n#\n# Apprise - Push Notification Library.\n# Copyright (c) 2026, Chris Caron <lead2gold@gmail.com>\n"
  },
  {
    "path": "apprise/plugins/parseplatform.py",
    "chars": 11087,
    "preview": "# BSD 2-Clause License\n#\n# Apprise - Push Notification Library.\n# Copyright (c) 2026, Chris Caron <lead2gold@gmail.com>\n"
  },
  {
    "path": "apprise/plugins/plivo.py",
    "chars": 14124,
    "preview": "# BSD 2-Clause License\n#\n# Apprise - Push Notification Library.\n# Copyright (c) 2026, Chris Caron <lead2gold@gmail.com>\n"
  },
  {
    "path": "apprise/plugins/popcorn_notify.py",
    "chars": 11091,
    "preview": "# BSD 2-Clause License\n#\n# Apprise - Push Notification Library.\n# Copyright (c) 2026, Chris Caron <lead2gold@gmail.com>\n"
  },
  {
    "path": "apprise/plugins/prowl.py",
    "chars": 10242,
    "preview": "# BSD 2-Clause License\n#\n# Apprise - Push Notification Library.\n# Copyright (c) 2026, Chris Caron <lead2gold@gmail.com>\n"
  },
  {
    "path": "apprise/plugins/pushbullet.py",
    "chars": 16236,
    "preview": "# BSD 2-Clause License\n#\n# Apprise - Push Notification Library.\n# Copyright (c) 2026, Chris Caron <lead2gold@gmail.com>\n"
  },
  {
    "path": "apprise/plugins/pushdeer.py",
    "chars": 7246,
    "preview": "# BSD 2-Clause License\n#\n# Apprise - Push Notification Library.\n# Copyright (c) 2026, Chris Caron <lead2gold@gmail.com>\n"
  },
  {
    "path": "apprise/plugins/pushed.py",
    "chars": 12638,
    "preview": "# BSD 2-Clause License\n#\n# Apprise - Push Notification Library.\n# Copyright (c) 2026, Chris Caron <lead2gold@gmail.com>\n"
  },
  {
    "path": "apprise/plugins/pushjet.py",
    "chars": 9591,
    "preview": "# BSD 2-Clause License\n#\n# Apprise - Push Notification Library.\n# Copyright (c) 2026, Chris Caron <lead2gold@gmail.com>\n"
  },
  {
    "path": "apprise/plugins/pushme.py",
    "chars": 7415,
    "preview": "# BSD 2-Clause License\n#\n# Apprise - Push Notification Library.\n# Copyright (c) 2026, Chris Caron <lead2gold@gmail.com>\n"
  },
  {
    "path": "apprise/plugins/pushover.py",
    "chars": 22103,
    "preview": "# BSD 2-Clause License\n#\n# Apprise - Push Notification Library.\n# Copyright (c) 2026, Chris Caron <lead2gold@gmail.com>\n"
  },
  {
    "path": "apprise/plugins/pushplus.py",
    "chars": 5683,
    "preview": "#\n# BSD 2-Clause License\n#\n# Apprise - Push Notification Library.\n# Copyright (c) 2026, Chris Caron <lead2gold@gmail.com"
  },
  {
    "path": "apprise/plugins/pushsafer.py",
    "chars": 28301,
    "preview": "# BSD 2-Clause License\n#\n# Apprise - Push Notification Library.\n# Copyright (c) 2026, Chris Caron <lead2gold@gmail.com>\n"
  },
  {
    "path": "apprise/plugins/pushy.py",
    "chars": 12837,
    "preview": "# BSD 2-Clause License\n#\n# Apprise - Push Notification Library.\n# Copyright (c) 2026, Chris Caron <lead2gold@gmail.com>\n"
  },
  {
    "path": "apprise/plugins/qq.py",
    "chars": 5655,
    "preview": "#\n# BSD 2-Clause License\n#\n# Apprise - Push Notification Library.\n# Copyright (c) 2026, Chris Caron <lead2gold@gmail.com"
  },
  {
    "path": "apprise/plugins/reddit.py",
    "chars": 26183,
    "preview": "# BSD 2-Clause License\n#\n# Apprise - Push Notification Library.\n# Copyright (c) 2026, Chris Caron <lead2gold@gmail.com>\n"
  },
  {
    "path": "apprise/plugins/resend.py",
    "chars": 20362,
    "preview": "# BSD 2-Clause License\n#\n# Apprise - Push Notification Library.\n# Copyright (c) 2026, Chris Caron <lead2gold@gmail.com>\n"
  },
  {
    "path": "apprise/plugins/revolt.py",
    "chars": 14934,
    "preview": "# BSD 2-Clause License\n#\n# Apprise - Push Notification Library.\n# Copyright (c) 2026, Chris Caron <lead2gold@gmail.com>\n"
  },
  {
    "path": "apprise/plugins/rocketchat.py",
    "chars": 26677,
    "preview": "# BSD 2-Clause License\n#\n# Apprise - Push Notification Library.\n# Copyright (c) 2026, Chris Caron <lead2gold@gmail.com>\n"
  },
  {
    "path": "apprise/plugins/rsyslog.py",
    "chars": 12516,
    "preview": "# BSD 2-Clause License\n#\n# Apprise - Push Notification Library.\n# Copyright (c) 2026, Chris Caron <lead2gold@gmail.com>\n"
  },
  {
    "path": "apprise/plugins/ryver.py",
    "chars": 12249,
    "preview": "# BSD 2-Clause License\n#\n# Apprise - Push Notification Library.\n# Copyright (c) 2026, Chris Caron <lead2gold@gmail.com>\n"
  },
  {
    "path": "apprise/plugins/sendgrid.py",
    "chars": 19228,
    "preview": "# BSD 2-Clause License\n#\n# Apprise - Push Notification Library.\n# Copyright (c) 2026, Chris Caron <lead2gold@gmail.com>\n"
  },
  {
    "path": "apprise/plugins/sendpulse.py",
    "chars": 27459,
    "preview": "# BSD 2-Clause License\n#\n# Apprise - Push Notification Library.\n# Copyright (c) 2026, Chris Caron <lead2gold@gmail.com>\n"
  },
  {
    "path": "apprise/plugins/serverchan.py",
    "chars": 6026,
    "preview": "# BSD 2-Clause License\n#\n# Apprise - Push Notification Library.\n# Copyright (c) 2026, Chris Caron <lead2gold@gmail.com>\n"
  },
  {
    "path": "apprise/plugins/ses.py",
    "chars": 34848,
    "preview": "# BSD 2-Clause License\n#\n# Apprise - Push Notification Library.\n# Copyright (c) 2026, Chris Caron <lead2gold@gmail.com>\n"
  },
  {
    "path": "apprise/plugins/seven.py",
    "chars": 12485,
    "preview": "# BSD 2-Clause License\n#\n# Apprise - Push Notification Library.\n# Copyright (c) 2026, Chris Caron <lead2gold@gmail.com>\n"
  },
  {
    "path": "apprise/plugins/sfr.py",
    "chars": 15180,
    "preview": "# BSD 2-Clause License\n#\n# Apprise - Push Notification Library.\n# Copyright (c) 2026, Chris Caron <lead2gold@gmail.com>\n"
  },
  {
    "path": "apprise/plugins/signal_api.py",
    "chars": 18656,
    "preview": "# BSD 2-Clause License\n#\n# Apprise - Push Notification Library.\n# Copyright (c) 2026, Chris Caron <lead2gold@gmail.com>\n"
  },
  {
    "path": "apprise/plugins/signl4.py",
    "chars": 11260,
    "preview": "# BSD 2-Clause License\n#\n# Apprise - Push Notification Library.\n# Copyright (c) 2026, Chris Caron <lead2gold@gmail.com>\n"
  },
  {
    "path": "apprise/plugins/simplepush.py",
    "chars": 12137,
    "preview": "# BSD 2-Clause License\n#\n# Apprise - Push Notification Library.\n# Copyright (c) 2026, Chris Caron <lead2gold@gmail.com>\n"
  },
  {
    "path": "apprise/plugins/sinch.py",
    "chars": 17456,
    "preview": "# BSD 2-Clause License\n#\n# Apprise - Push Notification Library.\n# Copyright (c) 2026, Chris Caron <lead2gold@gmail.com>\n"
  },
  {
    "path": "apprise/plugins/slack.py",
    "chars": 48665,
    "preview": "# BSD 2-Clause License\n#\n# Apprise - Push Notification Library.\n# Copyright (c) 2026, Chris Caron <lead2gold@gmail.com>\n"
  },
  {
    "path": "apprise/plugins/smpp.py",
    "chars": 11759,
    "preview": "# BSD 2-Clause License\n#\n# Apprise - Push Notification Library.\n# Copyright (c) 2026, Chris Caron <lead2gold@gmail.com>\n"
  },
  {
    "path": "apprise/plugins/smseagle.py",
    "chars": 26241,
    "preview": "# BSD 2-Clause License\n#\n# Apprise - Push Notification Library.\n# Copyright (c) 2026, Chris Caron <lead2gold@gmail.com>\n"
  },
  {
    "path": "apprise/plugins/smsmanager.py",
    "chars": 14706,
    "preview": "# BSD 2-Clause License\n#\n# Apprise - Push Notification Library.\n# Copyright (c) 2026, Chris Caron <lead2gold@gmail.com>\n"
  },
  {
    "path": "apprise/plugins/smtp2go.py",
    "chars": 20956,
    "preview": "# BSD 2-Clause License\n#\n# Apprise - Push Notification Library.\n# Copyright (c) 2026, Chris Caron <lead2gold@gmail.com>\n"
  },
  {
    "path": "apprise/plugins/sns.py",
    "chars": 24519,
    "preview": "# BSD 2-Clause License\n#\n# Apprise - Push Notification Library.\n# Copyright (c) 2026, Chris Caron <lead2gold@gmail.com>\n"
  },
  {
    "path": "apprise/plugins/sparkpost.py",
    "chars": 28205,
    "preview": "# BSD 2-Clause License\n#\n# Apprise - Push Notification Library.\n# Copyright (c) 2026, Chris Caron <lead2gold@gmail.com>\n"
  },
  {
    "path": "apprise/plugins/spike.py",
    "chars": 5963,
    "preview": "#\n# BSD 2-Clause License\n#\n# Apprise - Push Notification Library.\n# Copyright (c) 2026, Chris Caron <lead2gold@gmail.com"
  },
  {
    "path": "apprise/plugins/splunk.py",
    "chars": 17182,
    "preview": "# BSD 2-Clause License\n#\n# Apprise - Push Notification Library.\n# Copyright (c) 2026, Chris Caron <lead2gold@gmail.com>\n"
  },
  {
    "path": "apprise/plugins/spugpush.py",
    "chars": 5616,
    "preview": "#\n# BSD 2-Clause License\n#\n# Apprise - Push Notification Library.\n# Copyright (c) 2026, Chris Caron <lead2gold@gmail.com"
  },
  {
    "path": "apprise/plugins/streamlabs.py",
    "chars": 16622,
    "preview": "# BSD 2-Clause License\n#\n# Apprise - Push Notification Library.\n# Copyright (c) 2026, Chris Caron <lead2gold@gmail.com>\n"
  },
  {
    "path": "apprise/plugins/synology.py",
    "chars": 11857,
    "preview": "# BSD 2-Clause License\n#\n# Apprise - Push Notification Library.\n# Copyright (c) 2026, Chris Caron <lead2gold@gmail.com>\n"
  },
  {
    "path": "apprise/plugins/syslog.py",
    "chars": 10799,
    "preview": "# BSD 2-Clause License\n#\n# Apprise - Push Notification Library.\n# Copyright (c) 2026, Chris Caron <lead2gold@gmail.com>\n"
  },
  {
    "path": "apprise/plugins/techuluspush.py",
    "chars": 7412,
    "preview": "# BSD 2-Clause License\n#\n# Apprise - Push Notification Library.\n# Copyright (c) 2026, Chris Caron <lead2gold@gmail.com>\n"
  },
  {
    "path": "apprise/plugins/telegram.py",
    "chars": 40886,
    "preview": "# BSD 2-Clause License\n#\n# Apprise - Push Notification Library.\n# Copyright (c) 2026, Chris Caron <lead2gold@gmail.com>\n"
  },
  {
    "path": "apprise/plugins/threema.py",
    "chars": 12287,
    "preview": "# BSD 2-Clause License\n#\n# Apprise - Push Notification Library.\n# Copyright (c) 2026, Chris Caron <lead2gold@gmail.com>\n"
  },
  {
    "path": "apprise/plugins/twilio.py",
    "chars": 21752,
    "preview": "# BSD 2-Clause License\n#\n# Apprise - Push Notification Library.\n# Copyright (c) 2026, Chris Caron <lead2gold@gmail.com>\n"
  },
  {
    "path": "apprise/plugins/twist.py",
    "chars": 26944,
    "preview": "# BSD 2-Clause License\n#\n# Apprise - Push Notification Library.\n# Copyright (c) 2026, Chris Caron <lead2gold@gmail.com>\n"
  },
  {
    "path": "apprise/plugins/twitter.py",
    "chars": 31870,
    "preview": "# BSD 2-Clause License\n#\n# Apprise - Push Notification Library.\n# Copyright (c) 2026, Chris Caron <lead2gold@gmail.com>\n"
  },
  {
    "path": "apprise/plugins/vapid/__init__.py",
    "chars": 20545,
    "preview": "# BSD 2-Clause License\n#\n# Apprise - Push Notification Library.\n# Copyright (c) 2026, Chris Caron <lead2gold@gmail.com>\n"
  },
  {
    "path": "apprise/plugins/vapid/subscription.py",
    "chars": 13139,
    "preview": "# BSD 2-Clause License\n#\n# Apprise - Push Notification Library.\n# Copyright (c) 2026, Chris Caron <lead2gold@gmail.com>\n"
  },
  {
    "path": "apprise/plugins/viber.py",
    "chars": 11541,
    "preview": "# BSD 2-Clause License\n#\n# Apprise - Push Notification Library.\n# Copyright (c) 2026, Chris Caron <lead2gold@gmail.com>\n"
  },
  {
    "path": "apprise/plugins/voipms.py",
    "chars": 13369,
    "preview": "# BSD 2-Clause License\n#\n# Apprise - Push Notification Library.\n# Copyright (c) 2026, Chris Caron <lead2gold@gmail.com>\n"
  },
  {
    "path": "apprise/plugins/vonage.py",
    "chars": 13752,
    "preview": "# BSD 2-Clause License\n#\n# Apprise - Push Notification Library.\n# Copyright (c) 2026, Chris Caron <lead2gold@gmail.com>\n"
  },
  {
    "path": "apprise/plugins/webexteams.py",
    "chars": 9886,
    "preview": "# BSD 2-Clause License\n#\n# Apprise - Push Notification Library.\n# Copyright (c) 2026, Chris Caron <lead2gold@gmail.com>\n"
  },
  {
    "path": "apprise/plugins/wecombot.py",
    "chars": 9159,
    "preview": "# BSD 2-Clause License\n#\n# Apprise - Push Notification Library.\n# Copyright (c) 2026, Chris Caron <lead2gold@gmail.com>\n"
  },
  {
    "path": "apprise/plugins/whatsapp.py",
    "chars": 20880,
    "preview": "# BSD 2-Clause License\n#\n# Apprise - Push Notification Library.\n# Copyright (c) 2026, Chris Caron <lead2gold@gmail.com>\n"
  },
  {
    "path": "apprise/plugins/windows.py",
    "chars": 9289,
    "preview": "# BSD 2-Clause License\n#\n# Apprise - Push Notification Library.\n# Copyright (c) 2026, Chris Caron <lead2gold@gmail.com>\n"
  },
  {
    "path": "apprise/plugins/workflows.py",
    "chars": 22918,
    "preview": "# BSD 2-Clause License\n#\n# Apprise - Push Notification Library.\n# Copyright (c) 2026, Chris Caron <lead2gold@gmail.com>\n"
  }
]

// ... and 231 more files (download for full content)

About this extraction

This page contains the full source code of the caronc/apprise GitHub repository, extracted and formatted as plain text for AI agents and large language models (LLMs). The extraction includes 431 files (5.8 MB), approximately 1.5M tokens, and a symbol index with 2670 extracted functions, classes, methods, constants, and types. Use this with OpenClaw, Claude, ChatGPT, Cursor, Windsurf, or any other AI tool that accepts text input. You can copy the full output to your clipboard or download it as a .txt file.

Extracted by GitExtract — free GitHub repo to text converter for AI. Built by Nikandr Surkov.

Copied to clipboard!