Skip to content

Commit

Permalink
CLI environment variable over-ride support (#1231)
Browse files Browse the repository at this point in the history
  • Loading branch information
caronc authored Oct 27, 2024
1 parent 1065c02 commit 01c1082
Show file tree
Hide file tree
Showing 10 changed files with 337 additions and 143 deletions.
12 changes: 12 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,7 @@ System Administrators and DevOps who wish to send a notification now no longer n
* [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)
Expand Down Expand Up @@ -352,6 +353,17 @@ apprise -vv --title 'custom override' \

You can read more about creating your own custom notifications and/or hooks [here](https://github.com/caronc/apprise/wiki/decorator_notify).

## 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 over-rides 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 (over-riding 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 (over-riding 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 (over-riding the default).

# Developer API Usage

To send a notification from within your python application, just do the following:
Expand Down
89 changes: 70 additions & 19 deletions apprise/cli.py
Original file line number Diff line number Diff line change
Expand Up @@ -68,6 +68,21 @@
DEFAULT_STORAGE_UID_LENGTH = \
int(os.environ.get('APPRISE_STORAGE_UID_LENGTH', 8))

# Defines the envrionment variable to parse if defined. This is ONLY
# Referenced if:
# - No Configuration Files were found/loaded/specified
# - No URLs were provided directly into the CLI Call
DEFAULT_ENV_APPRISE_URLS = 'APPRISE_URLS'

# Defines the over-ride path for the configuration files read
DEFAULT_ENV_APPRISE_CONFIG_PATH = 'APPRISE_CONFIG_PATH'

# Defines the over-ride path for the plugins to load
DEFAULT_ENV_APPRISE_PLUGIN_PATH = 'APPRISE_PLUGIN_PATH'

# Defines the over-ride path for the persistent storage
DEFAULT_ENV_APPRISE_STORAGE_PATH = 'APPRISE_STORAGE_PATH'

# Defines our click context settings adding -h to the additional options that
# can be specified to get the help menu to come up
CONTEXT_SETTINGS = dict(help_option_names=['-h', '--help'])
Expand Down Expand Up @@ -496,11 +511,53 @@ def main(ctx, body, title, config, attach, urls, notification_type, theme, tag,
# issue. For consistency, we also return a 2
ctx.exit(2)

if not plugin_path:
# Prepare a default set of plugin path
plugin_path = \
[path for path in DEFAULT_PLUGIN_PATHS
if exists(path_decode(path))]
#
# Apply Environment Over-rides if defined
#
_config_paths = DEFAULT_CONFIG_PATHS
if 'APPRISE_CONFIG' in os.environ:
# Deprecate (this was from previous versions of Apprise <= 1.9.1)
logger.deprecate(
'APPRISE_CONFIG environment variable has been changed to '
f'{DEFAULT_ENV_APPRISE_CONFIG_PATH}')
logger.debug(
'Loading provided APPRISE_CONFIG (deprecated) environment '
'variable')
_config_paths = (os.environ.get('APPRISE_CONFIG', '').strip(), )

elif DEFAULT_ENV_APPRISE_CONFIG_PATH in os.environ:
logger.debug(
f'Loading provided {DEFAULT_ENV_APPRISE_CONFIG_PATH} '
'environment variable')
_config_paths = re.split(
r'[\r\n;]+', os.environ.get(
DEFAULT_ENV_APPRISE_CONFIG_PATH).strip())

_plugin_paths = DEFAULT_PLUGIN_PATHS
if DEFAULT_ENV_APPRISE_PLUGIN_PATH in os.environ:
logger.debug(
f'Loading provided {DEFAULT_ENV_APPRISE_PLUGIN_PATH} environment '
'variable')
_plugin_paths = re.split(
r'[\r\n;]+', os.environ.get(
DEFAULT_ENV_APPRISE_PLUGIN_PATH).strip())

if DEFAULT_ENV_APPRISE_STORAGE_PATH in os.environ:
logger.debug(
f'Loading provided {DEFAULT_ENV_APPRISE_STORAGE_PATH} environment '
'variable')
storage_path = \
os.environ.get(DEFAULT_ENV_APPRISE_STORAGE_PATH).strip()

#
# Continue with initialization process
#

# Prepare a default set of plugin paths to scan; anything specified
# on the CLI always trumps
plugin_paths = \
[path for path in _plugin_paths if exists(path_decode(path))] \
if not plugin_path else plugin_path

if storage_uid_length < 2:
click.echo(
Expand Down Expand Up @@ -533,7 +590,7 @@ def main(ctx, body, title, config, attach, urls, notification_type, theme, tag,
async_mode=disable_async is not True,

# Load our plugins
plugin_paths=plugin_path,
plugin_paths=plugin_paths,

# Load our persistent storage path
storage_path=path_decode(storage_path),
Expand Down Expand Up @@ -636,8 +693,7 @@ def main(ctx, body, title, config, attach, urls, notification_type, theme, tag,
# 1. URLs by command line
# 2. Configuration by command line
# 3. URLs by environment variable: APPRISE_URLS
# 4. Configuration by environment variable: APPRISE_CONFIG
# 5. Default Configuration File(s) (if found)
# 4. Default Configuration File(s)
#
elif urls and not storage_action:
if tag:
Expand All @@ -662,28 +718,23 @@ def main(ctx, body, title, config, attach, urls, notification_type, theme, tag,
a.add(AppriseConfig(
paths=config, asset=asset, recursion=recursion_depth))

elif os.environ.get('APPRISE_URLS', '').strip():
logger.debug('Loading provided APPRISE_URLS environment variable')
elif os.environ.get(DEFAULT_ENV_APPRISE_URLS, '').strip():
logger.debug(
f'Loading provided {DEFAULT_ENV_APPRISE_URLS} environment '
'variable')
if tag:
# Ignore any tags specified
logger.warning(
'--tag (-g) entries are ignored when using specified URLs')
tag = None

# Attempt to use our APPRISE_URLS environment variable (if populated)
a.add(os.environ['APPRISE_URLS'].strip())

elif os.environ.get('APPRISE_CONFIG', '').strip():
logger.debug('Loading provided APPRISE_CONFIG environment variable')
# Fall back to config environment variable (if populated)
a.add(AppriseConfig(
paths=os.environ['APPRISE_CONFIG'].strip(),
asset=asset, recursion=recursion_depth))
a.add(os.environ[DEFAULT_ENV_APPRISE_URLS].strip())

else:
# Load default configuration
a.add(AppriseConfig(
paths=[f for f in DEFAULT_CONFIG_PATHS if isfile(path_decode(f))],
paths=[f for f in _config_paths if isfile(path_decode(f))],
asset=asset, recursion=recursion_depth))

if not dry_run and not (a or storage_action):
Expand Down
35 changes: 0 additions & 35 deletions apprise/utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -29,10 +29,8 @@
import re
import sys
import json
import contextlib
import os
import binascii
import locale
import platform
import typing
import base64
Expand Down Expand Up @@ -1518,39 +1516,6 @@ def cwe312_url(url):
)


@contextlib.contextmanager
def environ(*remove, **update):
"""
Temporarily updates the ``os.environ`` dictionary in-place.
The ``os.environ`` dictionary is updated in-place so that the modification
is sure to work in all situations.
:param remove: Environment variable(s) to remove.
:param update: Dictionary of environment variables and values to
add/update.
"""

# Create a backup of our environment for restoration purposes
env_orig = os.environ.copy()
loc_orig = locale.getlocale()
try:
os.environ.update(update)
[os.environ.pop(k, None) for k in remove]
yield

finally:
# Restore our snapshot
os.environ = env_orig.copy()
try:
# Restore locale
locale.setlocale(locale.LC_ALL, loc_orig)

except locale.Error:
# Handle this case
pass


def apply_template(template, app_mode=TemplateType.RAW, **kwargs):
"""
Takes a template in a str format and applies all of the keywords
Expand Down
29 changes: 29 additions & 0 deletions packaging/man/apprise.md
Original file line number Diff line number Diff line change
Expand Up @@ -159,6 +159,9 @@ visit the [Apprise GitHub page][serviceurls] and see what's available.

[serviceurls]: https://github.com/caronc/apprise/wiki#notification-services

The **environment variable** of `APPRISE_URLS` (comma/space delimited) can be specified to
provide the default set of URLs you wish to notify if none are otherwise specified.

## EXAMPLES

Send a notification to as many servers as you want to specify as you can
Expand Down Expand Up @@ -215,8 +218,13 @@ files and loads them:
~/.config/apprise/plugins
/var/lib/apprise/plugins

The **environment variable** of `APPRISE_PLUGIN_PATH` can be specified to override
the list identified above with one of your own. use a semi-colon (`;`), line-feed (`\n`),
and/or carriage return (`\r`) to delimit multiple entries.

Simply create your own python file with the following bare minimum content in
it:

from apprise.decorators import notify

# This example assumes you want your function to trigger on foobar://
Expand Down Expand Up @@ -263,6 +271,10 @@ in the following local locations for configuration files and loads them:
The **configuration files** specified above can also be identified with a `.yml`
extension or even just entirely removing the `.conf` extension altogether.

The **environment variable** of `APPRISE_CONFIG_PATH` can be specified to override
the list identified above with one of your own. use a semi-colon (`;`), line-feed (`\n`),
and/or carriage return (`\r`) to delimit multiple entries.

If a default configuration file is referenced in any way by the **apprise**
tool, you no longer need to provide it a Service URL. Usage of the **apprise**
tool simplifies to:
Expand All @@ -281,6 +293,23 @@ configuration that you want and only specifically notify a subset of them:
[tagging]: https://github.com/caronc/apprise/wiki/CLI_Usage#label-leverage-tagging
[pstorage]: https://github.com/caronc/apprise/wiki/persistent_storage

## ENVIRONMENT VARIABLES
`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 over-rides 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 (over-riding the default).
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 (over-riding 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 (over-riding the default).

## BUGS

If you find any bugs, please make them known at:
Expand Down
2 changes: 2 additions & 0 deletions test/helpers/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -29,9 +29,11 @@
from .rest import AppriseURLTester
from .asyncio import OuterEventLoop
from .module import reload_plugin
from .environment import environ

__all__ = [
'AppriseURLTester',
'OuterEventLoop',
'reload_plugin',
'environ',
]
68 changes: 68 additions & 0 deletions test/helpers/environment.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,68 @@
# -*- coding: utf-8 -*-
# BSD 2-Clause License
#
# Apprise - Push Notification Library.
# Copyright (c) 2024, Chris Caron <[email protected]>
#
# 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 contextlib
import locale

# Disable logging for a cleaner testing output
import logging
logging.disable(logging.CRITICAL)


@contextlib.contextmanager
def environ(*remove, **update):
"""
Temporarily updates the ``os.environ`` dictionary in-place.
The ``os.environ`` dictionary is updated in-place so that the modification
is sure to work in all situations.
:param remove: Environment variable(s) to remove.
:param update: Dictionary of environment variables and values to
add/update.
"""

# Create a backup of our environment for restoration purposes
env_orig = os.environ.copy()
loc_orig = locale.getlocale()
try:
os.environ.update(update)
[os.environ.pop(k, None) for k in remove]
yield

finally:
# Restore our snapshot
os.environ = env_orig.copy()
try:
# Restore locale
locale.setlocale(locale.LC_ALL, loc_orig)

except locale.Error:
# Handle this case
pass
Loading

0 comments on commit 01c1082

Please sign in to comment.