mirror of
				https://github.com/craigerl/aprsd.git
				synced 2025-11-03 21:20:23 -05:00 
			
		
		
		
	Merge pull request #40 from craigerl/new_plugins
Added new time plugins
This commit is contained in:
		
						commit
						f8c001dc49
					
				@ -10,9 +10,9 @@ RUN apk add --update git wget py3-pip py3-virtualenv bash fortune
 | 
			
		||||
 | 
			
		||||
# Setup Timezone
 | 
			
		||||
ENV TZ=US/Eastern
 | 
			
		||||
RUN ln -snf /usr/share/zoneinfo/$TZ /etc/localtime && echo $TZ > /etc/timezone
 | 
			
		||||
RUN apt-get install -y tzdata
 | 
			
		||||
RUN dpkg-reconfigure --frontend noninteractive tzdata
 | 
			
		||||
#RUN ln -snf /usr/share/zoneinfo/$TZ /etc/localtime && echo $TZ > /etc/timezone
 | 
			
		||||
#RUN apt-get install -y tzdata
 | 
			
		||||
#RUN dpkg-reconfigure --frontend noninteractive tzdata
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
RUN addgroup --gid 1000 $APRS_USER
 | 
			
		||||
 | 
			
		||||
@ -2,11 +2,11 @@ FROM alpine:latest as aprsd
 | 
			
		||||
 | 
			
		||||
# Dockerfile for building a container during aprsd development.
 | 
			
		||||
 | 
			
		||||
ENV VERSION=1.0.0
 | 
			
		||||
ENV VERSION=1.5.1
 | 
			
		||||
ENV APRS_USER=aprs
 | 
			
		||||
ENV HOME=/home/aprs
 | 
			
		||||
ENV APRSD=http://github.com/craigerl/aprsd.git
 | 
			
		||||
ENV APRSD_BRANCH="v1.1.0"
 | 
			
		||||
ENV APRSD_BRANCH="master"
 | 
			
		||||
ENV VIRTUAL_ENV=$HOME/.venv3
 | 
			
		||||
 | 
			
		||||
ENV INSTALL=$HOME/install
 | 
			
		||||
 | 
			
		||||
							
								
								
									
										72
									
								
								aprsd/plugin_utils.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										72
									
								
								aprsd/plugin_utils.py
									
									
									
									
									
										Normal file
									
								
							@ -0,0 +1,72 @@
 | 
			
		||||
#  Utilities for plugins to use
 | 
			
		||||
import json
 | 
			
		||||
import logging
 | 
			
		||||
 | 
			
		||||
import requests
 | 
			
		||||
 | 
			
		||||
LOG = logging.getLogger("APRSD")
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
def get_aprs_fi(api_key, callsign):
 | 
			
		||||
    LOG.debug("Fetch aprs.fi location for '{}'".format(callsign))
 | 
			
		||||
    try:
 | 
			
		||||
        url = (
 | 
			
		||||
            "http://api.aprs.fi/api/get?"
 | 
			
		||||
            "&what=loc&apikey={}&format=json"
 | 
			
		||||
            "&name={}".format(api_key, callsign)
 | 
			
		||||
        )
 | 
			
		||||
        response = requests.get(url)
 | 
			
		||||
    except Exception:
 | 
			
		||||
        raise Exception("Failed to get aprs.fi location")
 | 
			
		||||
    else:
 | 
			
		||||
        response.raise_for_status()
 | 
			
		||||
        return json.loads(response.text)
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
def get_weather_gov_for_gps(lat, lon):
 | 
			
		||||
    LOG.debug("Fetch station at {}, {}".format(lat, lon))
 | 
			
		||||
    try:
 | 
			
		||||
        url2 = (
 | 
			
		||||
            "https://forecast.weather.gov/MapClick.php?lat=%s"
 | 
			
		||||
            "&lon=%s&FcstType=json" % (lat, lon)
 | 
			
		||||
        )
 | 
			
		||||
        LOG.debug("Fetching weather '{}'".format(url2))
 | 
			
		||||
        response = requests.get(url2)
 | 
			
		||||
    except Exception as e:
 | 
			
		||||
        LOG.error(e)
 | 
			
		||||
        raise Exception("Failed to get weather")
 | 
			
		||||
    else:
 | 
			
		||||
        response.raise_for_status()
 | 
			
		||||
        return json.loads(response.text)
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
def get_weather_gov_metar(station):
 | 
			
		||||
    LOG.debug("Fetch metar for station '{}'".format(station))
 | 
			
		||||
    try:
 | 
			
		||||
        url = "https://api.weather.gov/stations/{}/observations/latest".format(
 | 
			
		||||
            station,
 | 
			
		||||
        )
 | 
			
		||||
        response = requests.get(url)
 | 
			
		||||
    except Exception:
 | 
			
		||||
        raise Exception("Failed to fetch metar")
 | 
			
		||||
    else:
 | 
			
		||||
        response.raise_for_status()
 | 
			
		||||
        return json.loads(response)
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
def fetch_openweathermap(api_key, lat, lon, exclude=None):
 | 
			
		||||
    LOG.debug("Fetch openweathermap for {}, {}".format(lat, lon))
 | 
			
		||||
    if not exclude:
 | 
			
		||||
        exclude = "minutely,hourly,daily,alerts"
 | 
			
		||||
    try:
 | 
			
		||||
        url = (
 | 
			
		||||
            "https://api.openweathermap.org/data/2.5/onecall?"
 | 
			
		||||
            "lat={}&lon={}&appid={}&exclude={}".format(lat, lon, api_key, exclude)
 | 
			
		||||
        )
 | 
			
		||||
        response = requests.get(url)
 | 
			
		||||
    except Exception as e:
 | 
			
		||||
        LOG.error(e)
 | 
			
		||||
        raise Exception("Failed to get weather")
 | 
			
		||||
    else:
 | 
			
		||||
        response.raise_for_status()
 | 
			
		||||
        return json.loads(response.text)
 | 
			
		||||
@ -1,10 +1,8 @@
 | 
			
		||||
import json
 | 
			
		||||
import logging
 | 
			
		||||
import re
 | 
			
		||||
import time
 | 
			
		||||
 | 
			
		||||
from aprsd import plugin
 | 
			
		||||
import requests
 | 
			
		||||
from aprsd import plugin, plugin_utils, utils
 | 
			
		||||
 | 
			
		||||
LOG = logging.getLogger("APRSD")
 | 
			
		||||
 | 
			
		||||
@ -16,11 +14,15 @@ class LocationPlugin(plugin.APRSDPluginBase):
 | 
			
		||||
    command_regex = "^[lL]"
 | 
			
		||||
    command_name = "location"
 | 
			
		||||
 | 
			
		||||
    config_items = {"apikey": "aprs.fi api key here"}
 | 
			
		||||
 | 
			
		||||
    def command(self, fromcall, message, ack):
 | 
			
		||||
        LOG.info("Location Plugin")
 | 
			
		||||
        # get last location of a callsign, get descriptive name from weather service
 | 
			
		||||
        try:
 | 
			
		||||
            utils.check_config_option(self.config, "aprs.fi", "apiKey")
 | 
			
		||||
        except Exception as ex:
 | 
			
		||||
            LOG.error("Failed to find config aprs.fi:apikey {}".format(ex))
 | 
			
		||||
            return "No aprs.fi apikey found"
 | 
			
		||||
 | 
			
		||||
        api_key = self.config["aprs.fi"]["apiKey"]
 | 
			
		||||
        try:
 | 
			
		||||
            # optional second argument is a callsign to search
 | 
			
		||||
@ -31,14 +33,13 @@ class LocationPlugin(plugin.APRSDPluginBase):
 | 
			
		||||
            else:
 | 
			
		||||
                # if no second argument, search for calling station
 | 
			
		||||
                searchcall = fromcall
 | 
			
		||||
            url = (
 | 
			
		||||
                "http://api.aprs.fi/api/get?name="
 | 
			
		||||
                + searchcall
 | 
			
		||||
                + "&what=loc&apikey={}&format=json".format(api_key)
 | 
			
		||||
            )
 | 
			
		||||
            response = requests.get(url)
 | 
			
		||||
            # aprs_data = json.loads(response.read())
 | 
			
		||||
            aprs_data = json.loads(response.text)
 | 
			
		||||
 | 
			
		||||
            try:
 | 
			
		||||
                aprs_data = plugin_utils.get_aprs_fi(api_key, searchcall)
 | 
			
		||||
            except Exception as ex:
 | 
			
		||||
                LOG.error("Failed to fetch aprs.fi '{}'".format(ex))
 | 
			
		||||
                return "Failed to fetch aprs.fi location"
 | 
			
		||||
 | 
			
		||||
            LOG.debug("LocationPlugin: aprs_data = {}".format(aprs_data))
 | 
			
		||||
            lat = aprs_data["entries"][0]["lat"]
 | 
			
		||||
            lon = aprs_data["entries"][0]["lng"]
 | 
			
		||||
@ -53,15 +54,12 @@ class LocationPlugin(plugin.APRSDPluginBase):
 | 
			
		||||
            # )  # unicode to ascii
 | 
			
		||||
            delta_seconds = time.time() - int(aprs_lasttime_seconds)
 | 
			
		||||
            delta_hours = delta_seconds / 60 / 60
 | 
			
		||||
            url2 = (
 | 
			
		||||
                "https://forecast.weather.gov/MapClick.php?lat="
 | 
			
		||||
                + str(lat)
 | 
			
		||||
                + "&lon="
 | 
			
		||||
                + str(lon)
 | 
			
		||||
                + "&FcstType=json"
 | 
			
		||||
            )
 | 
			
		||||
            response2 = requests.get(url2)
 | 
			
		||||
            wx_data = json.loads(response2.text)
 | 
			
		||||
 | 
			
		||||
            try:
 | 
			
		||||
                wx_data = plugin_utils.get_weather_gov_for_gps(lat, lon)
 | 
			
		||||
            except Exception as ex:
 | 
			
		||||
                LOG.error("Couldn't fetch forecast.weather.gov '{}'".format(ex))
 | 
			
		||||
                wx_data["location"]["areaDescription"] = "Unkown Location"
 | 
			
		||||
 | 
			
		||||
            reply = "{}: {} {}' {},{} {}h ago".format(
 | 
			
		||||
                searchcall,
 | 
			
		||||
 | 
			
		||||
@ -1,7 +1,8 @@
 | 
			
		||||
import logging
 | 
			
		||||
import time
 | 
			
		||||
 | 
			
		||||
from aprsd import fuzzyclock, plugin
 | 
			
		||||
from aprsd import fuzzyclock, plugin, plugin_utils, utils
 | 
			
		||||
from opencage.geocoder import OpenCageGeocode
 | 
			
		||||
import pytz
 | 
			
		||||
 | 
			
		||||
LOG = logging.getLogger("APRSD")
 | 
			
		||||
@ -20,15 +21,7 @@ class TimePlugin(plugin.APRSDPluginBase):
 | 
			
		||||
    def _get_utcnow(self):
 | 
			
		||||
        return pytz.datetime.datetime.utcnow()
 | 
			
		||||
 | 
			
		||||
    def command(self, fromcall, message, ack):
 | 
			
		||||
        LOG.info("TIME COMMAND")
 | 
			
		||||
        # So we can mock this in unit tests
 | 
			
		||||
        localzone = self._get_local_tz()
 | 
			
		||||
 | 
			
		||||
        # This is inefficient for now, but this enables
 | 
			
		||||
        # us to add the ability to provide time in the TZ
 | 
			
		||||
        # of the caller, if we can get the TZ from their callsign location
 | 
			
		||||
        # This also accounts for running aprsd in different timezones
 | 
			
		||||
    def build_date_str(self, localzone):
 | 
			
		||||
        utcnow = self._get_utcnow()
 | 
			
		||||
        gmt_t = pytz.utc.localize(utcnow)
 | 
			
		||||
        local_t = gmt_t.astimezone(localzone)
 | 
			
		||||
@ -38,10 +31,94 @@ class TimePlugin(plugin.APRSDPluginBase):
 | 
			
		||||
        local_min = local_t.strftime("%M")
 | 
			
		||||
        cur_time = fuzzyclock.fuzzy(int(local_hour), int(local_min), 1)
 | 
			
		||||
 | 
			
		||||
        reply = "{} ({}) ({})".format(
 | 
			
		||||
        reply = "{} ({})".format(
 | 
			
		||||
            cur_time,
 | 
			
		||||
            local_short_str,
 | 
			
		||||
            message.rstrip(),
 | 
			
		||||
        )
 | 
			
		||||
 | 
			
		||||
        return reply
 | 
			
		||||
 | 
			
		||||
    def command(self, fromcall, message, ack):
 | 
			
		||||
        LOG.info("TIME COMMAND")
 | 
			
		||||
        # So we can mock this in unit tests
 | 
			
		||||
        localzone = self._get_local_tz()
 | 
			
		||||
        return self.build_date_str(localzone)
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
class TimeOpenCageDataPlugin(TimePlugin):
 | 
			
		||||
    """geocage based timezone fetching."""
 | 
			
		||||
 | 
			
		||||
    version = "1.0"
 | 
			
		||||
    command_regex = "^[tT]"
 | 
			
		||||
    command_name = "Time"
 | 
			
		||||
 | 
			
		||||
    def command(self, fromcall, message, ack):
 | 
			
		||||
        api_key = self.config["aprs.fi"]["apiKey"]
 | 
			
		||||
        try:
 | 
			
		||||
            aprs_data = plugin_utils.get_aprs_fi(api_key, fromcall)
 | 
			
		||||
        except Exception as ex:
 | 
			
		||||
            LOG.error("Failed to fetch aprs.fi data {}".format(ex))
 | 
			
		||||
            return "Failed to fetch location"
 | 
			
		||||
 | 
			
		||||
        # LOG.debug("LocationPlugin: aprs_data = {}".format(aprs_data))
 | 
			
		||||
        lat = aprs_data["entries"][0]["lat"]
 | 
			
		||||
        lon = aprs_data["entries"][0]["lng"]
 | 
			
		||||
 | 
			
		||||
        try:
 | 
			
		||||
            utils.check_config_option(self.config, "opencagedata", "apiKey")
 | 
			
		||||
        except Exception as ex:
 | 
			
		||||
            LOG.error("Failed to find config opencage:apiKey {}".format(ex))
 | 
			
		||||
            return "No opencage apiKey found"
 | 
			
		||||
 | 
			
		||||
        try:
 | 
			
		||||
            opencage_key = self.config["opencagedata"]["apiKey"]
 | 
			
		||||
            geocoder = OpenCageGeocode(opencage_key)
 | 
			
		||||
            results = geocoder.reverse_geocode(lat, lon)
 | 
			
		||||
        except Exception as ex:
 | 
			
		||||
            LOG.error("Couldn't fetch opencagedata api '{}'".format(ex))
 | 
			
		||||
            # Default to UTC instead
 | 
			
		||||
            localzone = pytz.timezone("UTC")
 | 
			
		||||
        else:
 | 
			
		||||
            tzone = results[0]["annotations"]["timezone"]["name"]
 | 
			
		||||
            localzone = pytz.timezone(tzone)
 | 
			
		||||
 | 
			
		||||
        return self.build_date_str(localzone)
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
class TimeOWMPlugin(TimePlugin):
 | 
			
		||||
    """OpenWeatherMap based timezone fetching."""
 | 
			
		||||
 | 
			
		||||
    version = "1.0"
 | 
			
		||||
    command_regex = "^[tT]"
 | 
			
		||||
    command_name = "Time"
 | 
			
		||||
 | 
			
		||||
    def command(self, fromcall, message, ack):
 | 
			
		||||
        api_key = self.config["aprs.fi"]["apiKey"]
 | 
			
		||||
        try:
 | 
			
		||||
            aprs_data = plugin_utils.get_aprs_fi(api_key, fromcall)
 | 
			
		||||
        except Exception as ex:
 | 
			
		||||
            LOG.error("Failed to fetch aprs.fi data {}".format(ex))
 | 
			
		||||
            return "Failed to fetch location"
 | 
			
		||||
 | 
			
		||||
        # LOG.debug("LocationPlugin: aprs_data = {}".format(aprs_data))
 | 
			
		||||
        lat = aprs_data["entries"][0]["lat"]
 | 
			
		||||
        lon = aprs_data["entries"][0]["lng"]
 | 
			
		||||
 | 
			
		||||
        try:
 | 
			
		||||
            utils.check_config_option(self.config, "openweathermap", "apiKey")
 | 
			
		||||
        except Exception as ex:
 | 
			
		||||
            LOG.error("Failed to find config openweathermap:apiKey {}".format(ex))
 | 
			
		||||
            return "No openweathermap apiKey found"
 | 
			
		||||
 | 
			
		||||
        api_key = self.config["openweathermap"]["apiKey"]
 | 
			
		||||
        try:
 | 
			
		||||
            results = plugin_utils.fetch_openweathermap(api_key, lat, lon)
 | 
			
		||||
        except Exception as ex:
 | 
			
		||||
            LOG.error("Couldn't fetch openweathermap api '{}'".format(ex))
 | 
			
		||||
            # default to UTC
 | 
			
		||||
            localzone = pytz.timezone("UTC")
 | 
			
		||||
        else:
 | 
			
		||||
            tzone = results["timezone"]
 | 
			
		||||
            localzone = pytz.timezone(tzone)
 | 
			
		||||
 | 
			
		||||
        return self.build_date_str(localzone)
 | 
			
		||||
 | 
			
		||||
@ -22,6 +22,8 @@ DEFAULT_CONFIG_DICT = {
 | 
			
		||||
        "logfile": "/tmp/aprsd.log",
 | 
			
		||||
    },
 | 
			
		||||
    "aprs.fi": {"apiKey": "set me"},
 | 
			
		||||
    "openweathermap": {"apiKey": "set me"},
 | 
			
		||||
    "opencagedata": {"apiKey": "set me"},
 | 
			
		||||
    "shortcuts": {
 | 
			
		||||
        "aa": "5551239999@vtext.com",
 | 
			
		||||
        "cl": "craiglamparter@somedomain.org",
 | 
			
		||||
@ -111,6 +113,24 @@ def add_config_comments(raw_yaml):
 | 
			
		||||
            end_idx,
 | 
			
		||||
        )
 | 
			
		||||
 | 
			
		||||
    end_idx = end_substr(raw_yaml, "opencagedata:")
 | 
			
		||||
    if end_idx != -1:
 | 
			
		||||
        # lets insert a comment
 | 
			
		||||
        raw_yaml = insert_str(
 | 
			
		||||
            raw_yaml,
 | 
			
		||||
            "\n  # Get the apiKey from your opencagedata account here:  https://opencagedata.com/dashboard#api-keys",
 | 
			
		||||
            end_idx,
 | 
			
		||||
        )
 | 
			
		||||
 | 
			
		||||
    end_idx = end_substr(raw_yaml, "openweathermap:")
 | 
			
		||||
    if end_idx != -1:
 | 
			
		||||
        # lets insert a comment
 | 
			
		||||
        raw_yaml = insert_str(
 | 
			
		||||
            raw_yaml,
 | 
			
		||||
            "\n  # Get the apiKey from your openweathermap account here:  https://home.openweathermap.org/api_keys",
 | 
			
		||||
            end_idx,
 | 
			
		||||
        )
 | 
			
		||||
 | 
			
		||||
    return raw_yaml
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
@ -154,6 +174,35 @@ def get_config(config_file):
 | 
			
		||||
        sys.exit(-1)
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
def check_config_option(config, section, name=None, default=None, default_fail=None):
 | 
			
		||||
    if section in config:
 | 
			
		||||
 | 
			
		||||
        if name and name not in config[section]:
 | 
			
		||||
            if not default:
 | 
			
		||||
                raise Exception(
 | 
			
		||||
                    "'{}' was not in '{}' section of config file".format(
 | 
			
		||||
                        name,
 | 
			
		||||
                        section,
 | 
			
		||||
                    ),
 | 
			
		||||
                )
 | 
			
		||||
            else:
 | 
			
		||||
                config[section][name] = default
 | 
			
		||||
        else:
 | 
			
		||||
            if (
 | 
			
		||||
                default_fail
 | 
			
		||||
                and name in config[section]
 | 
			
		||||
                and config[section][name] == default_fail
 | 
			
		||||
            ):
 | 
			
		||||
                # We have to fail and bail if the user hasn't edited
 | 
			
		||||
                # this config option.
 | 
			
		||||
                raise Exception(
 | 
			
		||||
                    "Config file needs to be edited from provided defaults.",
 | 
			
		||||
                )
 | 
			
		||||
    else:
 | 
			
		||||
        raise Exception("'%s' section wasn't in config file" % section)
 | 
			
		||||
    return config
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
# This method tries to parse the config yaml file
 | 
			
		||||
# and consume the settings.
 | 
			
		||||
# If the required params don't exist,
 | 
			
		||||
@ -167,30 +216,12 @@ def parse_config(config_file):
 | 
			
		||||
        sys.exit(-1)
 | 
			
		||||
 | 
			
		||||
    def check_option(config, section, name=None, default=None, default_fail=None):
 | 
			
		||||
        if section in config:
 | 
			
		||||
 | 
			
		||||
            if name and name not in config[section]:
 | 
			
		||||
                if not default:
 | 
			
		||||
                    fail(
 | 
			
		||||
                        "'{}' was not in '{}' section of config file".format(
 | 
			
		||||
                            name,
 | 
			
		||||
                            section,
 | 
			
		||||
                        ),
 | 
			
		||||
                    )
 | 
			
		||||
                else:
 | 
			
		||||
                    config[section][name] = default
 | 
			
		||||
            else:
 | 
			
		||||
                if (
 | 
			
		||||
                    default_fail
 | 
			
		||||
                    and name in config[section]
 | 
			
		||||
                    and config[section][name] == default_fail
 | 
			
		||||
                ):
 | 
			
		||||
                    # We have to fail and bail if the user hasn't edited
 | 
			
		||||
                    # this config option.
 | 
			
		||||
                    fail("Config file needs to be edited from provided defaults.")
 | 
			
		||||
        try:
 | 
			
		||||
            config = check_config_option(config, section, name, default, default_fail)
 | 
			
		||||
        except Exception as ex:
 | 
			
		||||
            fail(repr(ex))
 | 
			
		||||
        else:
 | 
			
		||||
            fail("'%s' section wasn't in config file" % section)
 | 
			
		||||
        return config
 | 
			
		||||
            return config
 | 
			
		||||
 | 
			
		||||
    config = get_config(config_file)
 | 
			
		||||
    check_option(config, "shortcuts")
 | 
			
		||||
 | 
			
		||||
@ -23,4 +23,4 @@ if [ ! -e "$APRSD_CONFIG" ]; then
 | 
			
		||||
    echo "'$APRSD_CONFIG' File does not exist. Creating."
 | 
			
		||||
    aprsd sample-config > $APRSD_CONFIG
 | 
			
		||||
fi
 | 
			
		||||
$VIRTUAL_ENV/bin/aprsd server -c $APRSD_CONFIG
 | 
			
		||||
$VIRTUAL_ENV/bin/aprsd server -c $APRSD_CONFIG --loglevel DEBUG
 | 
			
		||||
 | 
			
		||||
@ -11,3 +11,4 @@ aprslib
 | 
			
		||||
py3-validate-email
 | 
			
		||||
pre-commit
 | 
			
		||||
pytz
 | 
			
		||||
opencage
 | 
			
		||||
 | 
			
		||||
@ -2,14 +2,18 @@
 | 
			
		||||
# This file is autogenerated by pip-compile
 | 
			
		||||
# To update, run:
 | 
			
		||||
#
 | 
			
		||||
#    pip-compile requirements.in
 | 
			
		||||
#    pip-compile
 | 
			
		||||
#
 | 
			
		||||
appdirs==1.4.4
 | 
			
		||||
    # via virtualenv
 | 
			
		||||
aprslib==0.6.47
 | 
			
		||||
    # via -r requirements.in
 | 
			
		||||
backoff==1.10.0
 | 
			
		||||
    # via opencage
 | 
			
		||||
certifi==2020.12.5
 | 
			
		||||
    # via requests
 | 
			
		||||
cffi==1.14.4
 | 
			
		||||
    # via cryptography
 | 
			
		||||
cfgv==3.2.0
 | 
			
		||||
    # via pre-commit
 | 
			
		||||
chardet==4.0.0
 | 
			
		||||
@ -20,6 +24,8 @@ click==7.1.2
 | 
			
		||||
    # via
 | 
			
		||||
    #   -r requirements.in
 | 
			
		||||
    #   click-completion
 | 
			
		||||
cryptography==3.3.1
 | 
			
		||||
    # via pyopenssl
 | 
			
		||||
distlib==0.3.1
 | 
			
		||||
    # via virtualenv
 | 
			
		||||
dnspython==2.1.0
 | 
			
		||||
@ -42,6 +48,8 @@ markupsafe==1.1.1
 | 
			
		||||
    # via jinja2
 | 
			
		||||
nodeenv==1.5.0
 | 
			
		||||
    # via pre-commit
 | 
			
		||||
opencage==1.2.2
 | 
			
		||||
    # via -r requirements.in
 | 
			
		||||
pbr==5.5.1
 | 
			
		||||
    # via -r requirements.in
 | 
			
		||||
pluggy==0.13.1
 | 
			
		||||
@ -50,6 +58,10 @@ pre-commit==2.9.3
 | 
			
		||||
    # via -r requirements.in
 | 
			
		||||
py3-validate-email==0.2.12
 | 
			
		||||
    # via -r requirements.in
 | 
			
		||||
pycparser==2.20
 | 
			
		||||
    # via cffi
 | 
			
		||||
pyopenssl==20.0.1
 | 
			
		||||
    # via opencage
 | 
			
		||||
pytz==2020.5
 | 
			
		||||
    # via -r requirements.in
 | 
			
		||||
pyyaml==5.3.1
 | 
			
		||||
@ -57,14 +69,19 @@ pyyaml==5.3.1
 | 
			
		||||
    #   -r requirements.in
 | 
			
		||||
    #   pre-commit
 | 
			
		||||
requests==2.25.1
 | 
			
		||||
    # via -r requirements.in
 | 
			
		||||
    # via
 | 
			
		||||
    #   -r requirements.in
 | 
			
		||||
    #   opencage
 | 
			
		||||
shellingham==1.3.2
 | 
			
		||||
    # via click-completion
 | 
			
		||||
six==1.15.0
 | 
			
		||||
    # via
 | 
			
		||||
    #   -r requirements.in
 | 
			
		||||
    #   click-completion
 | 
			
		||||
    #   cryptography
 | 
			
		||||
    #   imapclient
 | 
			
		||||
    #   opencage
 | 
			
		||||
    #   pyopenssl
 | 
			
		||||
    #   virtualenv
 | 
			
		||||
thesmuggler==1.0.1
 | 
			
		||||
    # via -r requirements.in
 | 
			
		||||
 | 
			
		||||
@ -97,10 +97,9 @@ class TestPlugin(unittest.TestCase):
 | 
			
		||||
 | 
			
		||||
        message = "time"
 | 
			
		||||
        local_short_str = local_t.strftime("%H:%M %Z")
 | 
			
		||||
        expected = "{} ({}) ({})".format(
 | 
			
		||||
        expected = "{} ({})".format(
 | 
			
		||||
            cur_time,
 | 
			
		||||
            local_short_str,
 | 
			
		||||
            message.rstrip(),
 | 
			
		||||
        )
 | 
			
		||||
        actual = time.run(fromcall, message, ack)
 | 
			
		||||
        self.assertEqual(expected, actual)
 | 
			
		||||
 | 
			
		||||
		Loading…
	
	
			
			x
			
			
		
	
		Reference in New Issue
	
	Block a user